Conversation with Merlin [email protected] · Thu Dec 07 2023

Haz con vue y firebase una vista donde se traigan todos los usuarios y al darle click al nombre sea un router link y te lleve a un chat privado con el usuario, usa currenUser para ocultarlo en este listado, te dejo algunos archivos que tengo como referencia import { chatSubscribeToMessage, chatSaveMessage } from '../services/chat.js' import { subscribeToAuth } from '../services/auth.js' import { formatDate } from '../helpers/date.js' import BaseLabel from '../components/BaseLabel.vue' import BaseButton from '../components/BaseButton.vue' import BaseLoader from '../components/BaseLoader.vue' import PrincipalTitle from '../components/PrincipalTitle.vue' export default { name: 'Chat', components: { BaseButton, BaseLabel, BaseLoader, PrincipalTitle }, data() { return { loadingMessages: true, messages: [], newMessage: { user: '', message: '', }, user: { id: null, email: null, rol: null, }, authUnSubscribe: () => {}, chatUnSubscribe: () => {}, }; }, methods: { sendMessage() { chatSaveMessage({ userId: this.user.id, user: this.user.email, message: this.newMessage.message, }) .then(() => { this.newMessage = ''; }); }, dateToString(date) { return formatDate(date) } }, mounted() { this.loadingMessages = true, this.chatUnSubscribe = chatSubscribeToMessage(messages => { this.messages = messages this.loadingMessages = false }); this.authUnSubscribe = subscribeToAuth(newUser => { this.user = {...newUser} }); }, unmounted() { // Limpiamos la subscripcion a auth this.authUnSubscribe() this.chatUnSubscribe() } } Impresionante chat en tiempo real Usuario: {{ message.user }} Mensaje: {{ message.message }} {{ dateToString(message.created_at) }} <form action="#" @submit.prevent="sendMessage" class="min-w-[320px]"> Usuario {{ this.user.email }} Mensaje Enviar mensaje import BaseButton from '../components/BaseButton.vue'; import BaseLabel from '../components/BaseLabel.vue'; import BaseLoader from '../components/BaseLoader.vue'; import { getUserById } from '../services/user.js'; import { subscribeToAuth } from '../services/auth.js'; import { formatDate } from '../helpers/date.js' import {sendPrivateMessage, subscribeToPrivateChat} from '../services/private-chat.js' import PrincipalTitle from '../components/PrincipalTitle.vue';

export default { name: 'PrivateChat', components: { BaseLoader, BaseLabel, BaseButton, PrincipalTitle }, data(){ return { loadingMessages: true, messages: [], loadingProfile: true, userProfile: { id:null, email: null, }, userAuth:{ id: null, email: null, rol: null, }, newMessage:{ message:'', }, authUnsubscribe: ()=>{}, messageUnsubscribe: ()=>{}, } }, methods: { handleMessage(){ sendPrivateMessage({ sender:this.userAuth.id, receiver:this.userProfile.id, message: this.newMessage.message }) this.newMessage.message = '' }, dateToString(date) { return formatDate(date) } }, async mounted() { this.loadingProfile = true this.userProfile = await getUserById(this.$route.params.id) this.authUnsubscribe = subscribeToAuth(user => this.userAuth = user) this.loadingProfile = false this.loadingMessages = true
this.messageUnsubscribe = await subscribeToPrivateChat( {user1: this.userProfile.id, user2: this.userAuth.id}, newMessages => this.messages = newMessages ) this.loadingMessages = false
}, unmounted(){ this.authUnsubscribe() }, } Chat privado con: {{ userProfile.email }} Mensajes {{ message.message }} {{ dateToString(message.created_at) || 'enviando...'}} Enviar un mensaje <form action="#" @submit.prevent="handleMessage" class="flex"> Mensaje Enviar import { createUserWithEmailAndPassword, onAuthStateChanged, signInWithEmailAndPassword, signOut } from "firebase/auth" import { auth,db } from './firebase.js' import { createUserProfile } from "./user.js" import { doc, getDoc } from 'firebase/firestore'

let userData = { id: null, email: null, rol: null, } let observers = []

async function fetchUserProfile(userId) { try { const docRef = doc(db, 'users', userId); const docSnap = await getDoc(docRef);

if (docSnap.exists()) { return docSnap.data(); } else { console.log("No user profile found!"); return null; } } catch (error) { console.error("Error fetching user profile: ", error); return null; }

} // recordamos en localstorage que el usuario esta authenticado. // Esto ahorra tiempo ya que verificamos antes de que firebase nos lo diga. if (localStorage.getItem('userData')) { userData = JSON.parse(localStorage.getItem('userData')) }

onAuthStateChanged(auth, async user => { if (user) { const profile = await fetchUserProfile(user.uid);

if(profile) { userData = { id: user.uid, email: user.email, rol: profile.rol, // ahora accedemos al rol desde la data del perfil }; localStorage.setItem('userData', JSON.stringify(userData)); notifyAll(); } } else { userData = { id: null, email: null, rol: null, }; localStorage.removeItem('userData'); }

})

export async function register({email,password}) { try { const userCredentials = await createUserWithEmailAndPassword(auth, email, password)

createUserProfile(userCredentials.user.uid,{ email, rol: 'client' }) return {...userData} } catch (error) { return { code: error.code, message: error.message } }

}

/** *

  • @param {{email:string,password:string}} param0
  • @returns {Promise} */ export function login({email,password}) { return signInWithEmailAndPassword(auth, email, password) .then(() => { return userData; }) .catch(error => { const output = { message: error.message, code: error.code, } console.error("[auth.js login] Error al autenticar: ", output); return output; }); }

export function logout() { return signOut(auth).then(() => { // Aquí limpia cualquier información del usuario que tengas en el estado. userData = { id: null, email: null, rol: null, }; // Remueve el perfil del usuario del localStorage también. localStorage.removeItem('userData'); // Notifica a todos los observadores que el usuario ha cerrado sesión. notifyAll(); console.log("Usuario ha cerrado sesión exitosamente."); }).catch(error => { // Aquí manejas el caso de un error al intentar cerrar la sesión. console.error("Error al cerrar sesión: ", error); }); }

/**

  • Agrega el observer para ser notificados de los datos

  • @param {({})=>void} callback

  • @returns {()=>void} Funcion para cancelar subscripcion */ export function subscribeToAuth(observer) { observers.push(observer) // console.log('Observer agregado', observers); notify(observer)

    // cancelamos la subscripcion de los que ya no utilizaremos return () => observers = observers.filter(obs => obs !== observer) }

function notify(callback) { callback({ ...userData, }) }

function notifyAll() { observers.forEach(observer => notify(observer)) } import { addDoc, collection, onSnapshot, orderBy, query, serverTimestamp, getDocs, where, } from "firebase/firestore"; // import { db, query, where, getDocs } from 'firebase/firestore'; import { db } from "./firebase.js"; const refChat = collection(db, "chat");

export function chatSaveMessage(data) { return addDoc(refChat, { ...data, created_at: serverTimestamp(), // Agrega la fecha y hora del mensaje esto lo hacemos para poder ordenar los mensajes }); }

export function chatSubscribeToMessage(callback) { // creamos un query para poder filtrar los datos/traerlos en un orden especifico const q = query(refChat, orderBy("created_at")); return onSnapshot(refChat, (snapshot) => { // datos en tiempo real, trae los datos ordenados por su id por lo que para nosotros parecera que los trae desordenados const messages = snapshot.docs.map((doc) => { return { user: doc.data().user, message: doc.data().message, userId: doc.data().userId, created_at: doc.data().created_at?.toDate(), }; }); callback(messages); }); }

export async function getPrivateChatsForUser(userId, callback) { const q = query( collection(db, "private-chats"), where(users.${userId}, "==", true) );

const querySnapshot = await getDocs(q); const userIds = [];

querySnapshot.forEach((doc) => { const users = doc.data().Users; Object.keys(users).forEach((key) => { if (key !== userId && !userIds.includes(key)) { userIds.push(key); } }); });

callback(userIds); } /* Estructura para manejar los chatPrivados: [C] private-chat { [D] idAuto { Users: { IdUser1: true, IdUser2: true, }, [C] messages { [D] idAuth { userId: idUser1, message:..., created_at: Timestamp, }, ... } } } Cada documento sera un chatPrivado que guarda sus integrantes y todos los mensajes */ import { addDoc, collection, serverTimestamp, getDocs, query, where, onSnapshot, orderBy } from 'firebase/firestore' import {db} from './firebase'

// Guardamos los documententos de los chats privados const chatDocsCache = []

/** *

  • @param {{user1: string, user2: string}} data

  • @param {()=>{}} callback

  • @returns {Promise<import("firebase/auth").Unsubscribe } */ export async function subscribeToPrivateChat({user1, user2}, callback){ const chatDoc = await getPrivateChatDocument({user1, user2})

    const q = query( collection(db, private-chats/${chatDoc.id}/messages), orderBy('created_at') ) return onSnapshot(q, snapshot => { const docs = snapshot.docs.map(doc => { return { id: doc.id, userId: doc.data().userId, message: doc.data().message, created_at: doc.data().created_at?.toDate() } }) callback(docs) }) }

/** *

  • @param {sender: string, receiver: string, message: string} param0
  • @returns {Promise} */ export async function sendPrivateMessage({sender, receiver, message}) { const document = await getPrivateChatDocument({user1: sender, user2: receiver}) const messagesRef = collection(db,private-chats/${document.id}/messages) const response = await addDoc(messagesRef, { userId: sender, message, created_at: serverTimestamp(), }) return { id: response.id, } }

/**

  • Busca si ya existe un chat entre esos dos usuarios y lo devuelve

  • en caso contrario crea uno

  • @param {{user1: string, user2: string}} data

  • @returns {Promise} */ async function getPrivateChatDocument({user1, user2}){ const doc = getFromDocsCache({user1, user2})// busca el chat en cache if (doc) return doc // si existe lo retorna

    const users = { [user1]: true, [user2]: true, } const chatRef = collection(db, 'private-chats') const q = query(chatRef, where('users', '==', users)) const snapshot = await getDocs(q)

    let chatDoc = null if(snapshot.empty) { chatDoc = await createPrivateChatDocument({user1: user1, user2: user2}) } else { chatDoc = snapshot.docs[0] } addToDocsCache({user1, user2}, chatDoc) // agrego el chat a cache return chatDoc }

/**

  • Crea un chat entre dos usuarios
  • @param {{user1: string, user2: string}} param0
  • @returns {Promise} */ async function createPrivateChatDocument({user1, user2}){ return addDoc(collection(db, 'private-chats'), { users: { [user1]: true, [user2]: true, } }) }

/**

  • Tiene los mensajes guardados en cache
  • @param {*} param0
  • @returns */ function getFromDocsCache({user1, user2}){ const key = getDocsCacheKey({user1, user2}) return chatDocsCache[key] || null }

/**

  • guarda los mensajes en la variable chatDocsCache
  • @param {*} param0
  • @param {*} doc */ function addToDocsCache({user1, user2}, doc){ const key = getDocsCacheKey({user1, user2}) chatDocsCache[key] = doc }

function getDocsCacheKey({user1,user2}) { return user1 > user2 ? user1 + user2: user2 + user1; } import {db} from './firebase' import { doc, getDoc, setDoc } from 'firebase/firestore'

/** *

  • @param {string} id
  • @returns {{id: string, email: string}} */ export async function getUserById(id){ const snapshot = getDoc(doc(db, /users/${id})) return { id, email: (await snapshot).data().email, } }

/**

  • Crea un perfil de usuario
  • @param {string} id
  • @param {{}} data */ export async function createUserProfile(id, data) { // Firestore organiza sus documentos y collecciones en formato URL. const userRef = doc(db,/users/${id}) return setDoc(userRef, data) }

Based on the code and details you've provided, it seems like you want to create a view in a Vue.js application that lists all users, allowing you to click on a user's name to navigate to a private chat with that specific user. You also want the current user to be omitted from the list.

I will outline the steps needed to achieve this functionality and provide you with a basic example of what the code could look like. It appears you are using Vue Router for navigation and Firebase Firestore to store and retrieve your chat data.

Steps:

  1. Fetch all users from Firestore, excluding the current user.
  2. Display the list of users in the Vue component.
  3. When a user's name is clicked, navigate to the private chat view using Vue Router.

First, let's create a method to fetch all users:

// In your user service or similar file export async function getAllUsers(excludeUserId) { const usersRef = collection(db, 'users'); const q = query(usersRef, where('id', '!=', excludeUserId)); const querySnapshot = await getDocs(q); return querySnapshot.docs.map((doc) => doc.data()); }

Next, you need a Vue component (UserList.vue) to list all users:

<template> <div> <h1>All Users</h1> <ul> <li v-for="user in users" :key="user.id" v-if="user.id !== currentUser.id"> <router-link :to="`/chat/${user.id}`">{{ user.email }}</router-link> </li> </ul> </div> </template> <script> import { getAllUsers } from 'path-to-your-user-service'; import { subscribeToAuth } from 'path-to-auth-service'; export default { name: 'UserList', data() { return { users: [], currentUser: {} }; }, async created() { this.authUnsubscribe = subscribeToAuth((user) => { this.currentUser = user; this.fetchUsers(user.id); }); }, methods: { async fetchUsers(excludeUserId) { this.users = await getAllUsers(excludeUserId); } }, beforeDestroy() { this.authUnsubscribe(); }, }; </script>

Make sure the route for /chat/:id is defined in your router/index.js file to point to your PrivateChat component:

const routes = [ // ... other routes { path: '/chat/:id', name: 'PrivateChat', component: () => import(/* webpackChunkName: "chat" */ '../views/PrivateChat.vue'), props: true } // ... other routes ]

Lastly, ensure the PrivateChat component is set up to use the id route parameter to load the appropriate chat messages:

// Already in your PrivateChat component, within the `mounted` method async mounted() { this.loadingMessages = true; this.loadingProfile = true; // Get the user profile using the id from the route parameters this.userProfile = await getUserById(this.$route.params.id); // Here add the code to not show the current user, assuming userAuth is the current user if(this.userProfile.id === this.userAuth.id) { // Do not display the current user or redirect away } // ... rest of your code }

The provided code snippets are to guide you along the right path, but you'll need to adjust them to fit the exact structure of your application and data.