From 244bdba9e16677e75b6ee1ac533f8e61d9d04ebd Mon Sep 17 00:00:00 2001 From: Arjun Patel Date: Sun, 13 Feb 2022 11:25:12 -0800 Subject: [PATCH] creating and retrieving links successfully --- packages/common/models/conversation.ts | 6 +- packages/common/services/collections.ts | 1 + .../common/services/conversationService.ts | 48 ++++++++ .../web/components/Drawer/CreateItemModal.tsx | 115 ++++++++++++++++-- packages/web/components/Drawer/LinkIcon.tsx | 5 + .../web/components/Drawer/SharedItemsRow.tsx | 57 +++++++++ .../components/MainTabsOrPages/ViewConvo.tsx | 111 +++++++++-------- 7 files changed, 276 insertions(+), 67 deletions(-) create mode 100644 packages/web/components/Drawer/SharedItemsRow.tsx diff --git a/packages/common/models/conversation.ts b/packages/common/models/conversation.ts index 253caad..bbcc271 100644 --- a/packages/common/models/conversation.ts +++ b/packages/common/models/conversation.ts @@ -12,7 +12,7 @@ export default class Conversation { membersInLiveRoom: string[] = [] as string[]; // all members in a live call right now for this convo cachedAudioClip?: AudioClip; // last message pretty much - cachedDrawerItem?: Link; // last link pretty much + cachedLink?: Link; // last link pretty much tldr?: string; // description almost that people keep up to day in the convo @@ -92,7 +92,7 @@ export class Link { url: string; name: string; - type: LinkType; + // type: LinkType; createdByUserId: string; createdDate: Timestamp = Timestamp.now(); @@ -101,7 +101,7 @@ export class Link { this.createdByUserId = _senderUserId; this.url = _linkUrl; this.name = _name; - this.type = Link.getLinkType(_linkUrl); + // this.type = Link.getLinkType(_linkUrl); } static getLinkType(url: string): LinkType { diff --git a/packages/common/services/collections.ts b/packages/common/services/collections.ts index 46c4329..3c89d4d 100644 --- a/packages/common/services/collections.ts +++ b/packages/common/services/collections.ts @@ -15,6 +15,7 @@ enum Collections { conversations = "conversations", conversationMembers = "members", conversationAudioClips = "audioClips", + conversationLinks = "links", } export default Collections; diff --git a/packages/common/services/conversationService.ts b/packages/common/services/conversationService.ts index dbf81e4..0be8346 100644 --- a/packages/common/services/conversationService.ts +++ b/packages/common/services/conversationService.ts @@ -15,6 +15,7 @@ import Conversation, { AudioClip, ConversationMember, ConversationMemberState, + Link, } from "../models/conversation"; import Collections from "./collections"; import { cloudStorageService } from "./index"; @@ -210,4 +211,51 @@ export default class ConversationService { { merge: true } ); } + + async shareLink(createdByUserId: string, link: Link, convoId: string) { + const linkRef = doc( + this.db, + Collections.conversations, + convoId, + Collections.conversationLinks, + link.id + ); + const conversationDoc = doc(this.db, Collections.conversations, convoId); + const userConvoAssocDoc = doc( + this.db, + Collections.conversations, + convoId, + Collections.conversationMembers, + createdByUserId + ); + + await runTransaction(this.db, async (transaction) => { + // want to add to the long term collection of all links for a conversation + transaction.set( + linkRef, + { ...link, createdDate: serverTimestamp() }, + { merge: true } + ); + + // want to make updates to the conversation document itself: set the last item for just the basic information to be + // published to users + transaction.set( + conversationDoc, + { + lastActivityDate: serverTimestamp(), + cachedLink: { ...link, createdDate: serverTimestamp() }, + }, + { merge: true } + ); + + // update the user's convo assoc so that their last interaction was set properly + transaction.set( + userConvoAssocDoc, + { + lastInteractionDate: serverTimestamp(), + }, + { merge: true } + ); + }); + } } diff --git a/packages/web/components/Drawer/CreateItemModal.tsx b/packages/web/components/Drawer/CreateItemModal.tsx index be63741..4a54707 100644 --- a/packages/web/components/Drawer/CreateItemModal.tsx +++ b/packages/web/components/Drawer/CreateItemModal.tsx @@ -1,16 +1,27 @@ import { Link } from "@nirvana/common/models/conversation"; +import { conversationService } from "@nirvana/common/services"; import Modal from "antd/lib/modal/Modal"; -import { useState, useEffect } from "react"; +import Image from "next/image"; +import { useState, useEffect, useRef } from "react"; import toast from "react-hot-toast"; -import LinkIcon from "./LinkIcon"; +import { useRecoilValue } from "recoil"; +import { useAuth } from "../../contexts/authContext"; +import { selectedConvoAtom } from "../../recoil/main"; +import LinkIcon, { getFavicon } from "./LinkIcon"; export default function CreateItemModal(props: { pastedLink: string; show: boolean; handleClose: () => void; }) { + const { currUser } = useAuth(); + const [linkVal, setLinkVal] = useState(""); const [linkDesc, setLinkDesc] = useState(""); + const linkInput = useRef(); + const [loading, setLoading] = useState(false); + + const selectedConvoId = useRecoilValue(selectedConvoAtom); useEffect(() => { if (props.pastedLink) { @@ -18,9 +29,55 @@ export default function CreateItemModal(props: { } }, [props.pastedLink]); - const handleCreate = () => { - console.log("added drawer item"); - toast.success("added drawer item to conversation"); + useEffect(() => { + linkInput.current?.focus(); + }, [props.show]); + + const handleCreate = async () => { + if (loading) { + toast("in the process of sharing..."); + return; + } + + setLoading(true); + + try { + // validations + if (!linkVal) { + toast.error("must put a link"); + setLoading(false); + return; + } + if (!linkDesc) { + toast.error("must put a name of some sort"); + setLoading(false); + return; + } + + // service to add link to the collection + // change the cacheddraweritem on the convo doc + const newLink = new Link(linkDesc, linkVal, currUser!.uid); + + await conversationService.shareLink( + currUser!.uid, + newLink, + selectedConvoId! + ); + + toast.success("shared"); + } catch (error) { + console.error(error); + toast.error("problem in sharing link"); + } + + setLoading(false); + resetForm(); + props.handleClose(); + }; + + const resetForm = () => { + setLinkDesc(""); + setLinkVal(""); }; const handleCancel = () => { @@ -28,15 +85,44 @@ export default function CreateItemModal(props: { props.handleClose(); }; + const handleChangeLinkInput = (e) => { + setLinkVal(e.target.value); + + // todo: find the tab name of the url + }; + + function addDefaultSrc(ev) { + ev.target.src = getFavicon("www.com"); + } + return ( + + + + + } visible={props.show} {...props} > - +
Link @@ -44,17 +130,22 @@ export default function CreateItemModal(props: { {/* icon and link input */} - + {linkVal && ( + + )} setLinkVal(e.target.value)} + onChange={handleChangeLinkInput} /> @@ -72,7 +163,7 @@ export default function CreateItemModal(props: { onChange={(e) => setLinkDesc(e.target.value)} /> - +
); } diff --git a/packages/web/components/Drawer/LinkIcon.tsx b/packages/web/components/Drawer/LinkIcon.tsx index 046f73a..e14c894 100644 --- a/packages/web/components/Drawer/LinkIcon.tsx +++ b/packages/web/components/Drawer/LinkIcon.tsx @@ -12,6 +12,11 @@ import Link, { LinkType } from "@nirvana/common/models/conversation"; import { SiGooglemeet, SiZoom } from "react-icons/si"; +const faviconGrabberAPI = "https://api.statvoo.com/favicon/?url="; +export function getFavicon(url: string): string { + return faviconGrabberAPI + url; +} + export default function LinkIcon(props: { className?: string; linkType: LinkType; diff --git a/packages/web/components/Drawer/SharedItemsRow.tsx b/packages/web/components/Drawer/SharedItemsRow.tsx new file mode 100644 index 0000000..a169c90 --- /dev/null +++ b/packages/web/components/Drawer/SharedItemsRow.tsx @@ -0,0 +1,57 @@ +import { Link } from "@nirvana/common/models/conversation"; +import { Tooltip } from "antd"; +import { linkWithCredential } from "firebase/auth"; +import moment from "moment"; +import link from "next/link"; +import { FaImages, FaExternalLinkAlt } from "react-icons/fa"; +import { getFavicon } from "./LinkIcon"; + +export default function SharedItemsRow(props: { link: Link }) { + const { link } = props; + + function addDefaultSrc(ev) { + ev.target.src = getFavicon("www.com"); + } + + return ( + + {/* */} + + + + + + {link.name} + + {moment(link.createdDate.toDate()).fromNow()} + + + + + + + + + + + + + window.open(link.url, "_blank")} + className="p-2 rounded-full hover:cursor-pointer hover:bg-slate-200 " + > + + + + + + ); +} diff --git a/packages/web/components/MainTabsOrPages/ViewConvo.tsx b/packages/web/components/MainTabsOrPages/ViewConvo.tsx index 57d7c2e..8c9b9bc 100644 --- a/packages/web/components/MainTabsOrPages/ViewConvo.tsx +++ b/packages/web/components/MainTabsOrPages/ViewConvo.tsx @@ -1,4 +1,4 @@ -import { LinkType } from "@nirvana/common/models/conversation"; +import { Link, LinkType } from "@nirvana/common/models/conversation"; import { UserStatus } from "@nirvana/common/models/user"; import { Tooltip } from "antd"; import { duration } from "moment"; @@ -49,6 +49,7 @@ import { onSnapshot, orderBy, query, + Unsubscribe, where, } from "firebase/firestore"; @@ -59,6 +60,8 @@ import Modal from "antd/lib/modal/Modal"; import { configure, GlobalHotKeys, KeyMap } from "react-hotkeys"; import CreateItemModal from "../Drawer/CreateItemModal"; import isValidHttpUrl from "../../helpers/urlHelper"; +import { FaPlus } from "react-icons/fa"; +import SharedItemsRow from "../Drawer/SharedItemsRow"; const testDrawerItems: { linkType: LinkType; @@ -88,6 +91,7 @@ const testDrawerItems: { ]; const AUDIO_CLIP_FETCH_LIMIT = 50; +const LINK_FETCH_LIMIT = 5; export default function ViewConvo(props: { conversationId: string }) { const { currUser } = useAuth(); @@ -114,11 +118,15 @@ export default function ViewConvo(props: { conversationId: string }) { const setSelectedConvoId = useSetRecoilState(selectedConvoAtom); + const [sharedItems, setSharedItems] = useState([] as Link[]); + useEffect(() => { endOfTimeline.current?.scrollIntoView({ behavior: "smooth" }); }, [audioClips]); useEffect(() => { + const unsubs: Unsubscribe[] = [] as Unsubscribe[]; + // should have this convo, otherwise unauthorized if (!convo || !convo.activeMembers.includes(currUser!.uid)) { toast.error("Not authorized for this conversation"); @@ -147,7 +155,7 @@ export default function ViewConvo(props: { conversationId: string }) { orderBy("createdDate", "desc"), limit(AUDIO_CLIP_FETCH_LIMIT) ); - const unsubscribe = onSnapshot(audioClipsQuery, (querySnapshot) => { + const unsubscribeAudClips = onSnapshot(audioClipsQuery, (querySnapshot) => { const audioClips: AudioClip[] = [] as AudioClip[]; querySnapshot.forEach((doc) => { @@ -162,7 +170,40 @@ export default function ViewConvo(props: { conversationId: string }) { console.log(audioClips); }); - return unsubscribe; + unsubs.push(unsubscribeAudClips); + + // links retrieval real time + const linksQuery = query( + collection( + db, + Collections.conversations, + props.conversationId, + Collections.conversationLinks + ), + orderBy("createdDate", "desc"), + limit(LINK_FETCH_LIMIT) + ); + const unsubscribeLinks = onSnapshot(linksQuery, (querySnapshot) => { + const links: Link[] = [] as Link[]; + + querySnapshot.forEach((doc) => { + const link = doc.data() as Link; + link.id = doc.id; + + // appending all messages in sort order + links.push(link); + }); + + setSharedItems(links); + }); + + unsubs.push(unsubscribeLinks); + + return () => { + unsubs.forEach((unsub) => { + unsub(); + }); + }; }, []); const handleOrganizeConversation = async ( @@ -454,50 +495,25 @@ export default function ViewConvo(props: { conversationId: string }) { {/* drawer items */}
- - Shared + + + Shared + + + + + + + {/* row of cards drawer items */} - {testDrawerItems.map((link) => ( - - {/* */} - - - - - - - {link.linkName} - - - {link.relativeSentTime} - - - - - - - - - - - - - - - - - - + {sharedItems.map((link) => ( + ))}
@@ -531,15 +547,6 @@ export default function ViewConvo(props: { conversationId: string }) { - - - - - - );