From d7f004047d48f46268aa2986b188bbfa9df87149 Mon Sep 17 00:00:00 2001 From: Luca Cannarozzo Date: Wed, 17 Jan 2024 10:21:55 +0100 Subject: [PATCH] feat(chat): add usersQuery to context The users promise will be used to handle the mention functionality directly in app. --- src/stories/chat/_types.tsx | 2 + src/stories/chat/context/chatContext.tsx | 5 ++ src/stories/chat/index.stories.tsx | 67 +++++++++++++++++++++++- src/stories/chat/parts/bar.tsx | 5 +- src/stories/chat/parts/comment.tsx | 7 ++- src/stories/chat/parts/commentBox.tsx | 5 +- src/stories/chat/parts/editor.tsx | 62 +++------------------- src/stories/chat/parts/mentionList.tsx | 56 ++++++++++++++------ src/stories/shared/editorStyle.tsx | 3 +- 9 files changed, 132 insertions(+), 80 deletions(-) diff --git a/src/stories/chat/_types.tsx b/src/stories/chat/_types.tsx index 1126dbde..8ad43cab 100644 --- a/src/stories/chat/_types.tsx +++ b/src/stories/chat/_types.tsx @@ -3,6 +3,8 @@ import { BubbleMenuProps, EditorOptions } from "@tiptap/react"; type validationStatus = "success" | "warning" | "error"; +export type SuggestedUser = { id: number; fullName: string; avatar: string }; + export interface ChatEditorArgs extends Partial { placeholderOptions?: Partial; hasInlineMenu?: boolean; diff --git a/src/stories/chat/context/chatContext.tsx b/src/stories/chat/context/chatContext.tsx index 5a2e11ef..4efad97d 100644 --- a/src/stories/chat/context/chatContext.tsx +++ b/src/stories/chat/context/chatContext.tsx @@ -1,5 +1,6 @@ import { Editor } from "@tiptap/react"; import React, { createContext, useContext, useMemo, useState } from "react"; +import { SuggestedUser } from "../_types"; export type ChatContextType = { isEditing: boolean; @@ -9,16 +10,19 @@ export type ChatContextType = { triggerSave: () => void; editor?: Editor; setEditor: React.Dispatch>; + mentionableUsers: (props: { query: string }) => Promise; }; export const ChatContext = createContext(null); export const ChatContextProvider = ({ onSave, + setMentionableUsers, children, }: { onSave?: (editor: Editor) => void; children: React.ReactNode; + setMentionableUsers: (props: { query: string }) => Promise; }) => { const [isEditing, setIsEditing] = useState(false); const [comment, setComment] = useState(""); @@ -38,6 +42,7 @@ export const ChatContextProvider = ({ editor.commands.clearContent(); } }, + mentionableUsers: setMentionableUsers, }), [comment, setComment, isEditing, setIsEditing, editor, setEditor, onSave] ); diff --git a/src/stories/chat/index.stories.tsx b/src/stories/chat/index.stories.tsx index a46e00db..f8d9f7ec 100644 --- a/src/stories/chat/index.stories.tsx +++ b/src/stories/chat/index.stories.tsx @@ -6,7 +6,7 @@ import { Button } from "../buttons/button"; import { Col } from "../grid/col"; import { Grid } from "../grid/grid"; import { Row } from "../grid/row"; -import { ChatEditorArgs } from "./_types"; +import { ChatEditorArgs, SuggestedUser } from "./_types"; import { Comment } from "./parts/comment"; interface EditorStoryArgs extends ChatEditorArgs { @@ -47,11 +47,64 @@ const ChatPanel = ({ background, ...args }: EditorStoryArgs) => { }; const Template: StoryFn = ({ children, ...args }) => { + const getUsers = async ({ query }: { query: string }) => { + return [ + { + id: 1, + fullName: "John Doe", + avatar: "https://i.pravatar.cc/150?img=1", + }, + { + id: 2, + fullName: "Jane Doe", + avatar: "https://i.pravatar.cc/150?img=2", + }, + { + id: 3, + fullName: "John Smith", + avatar: "https://i.pravatar.cc/150?img=3", + }, + { + id: 4, + fullName: "Jane Smith", + avatar: "https://i.pravatar.cc/150?img=4", + }, + { + id: 5, + fullName: "Pippo Baudo", + avatar: "https://i.pravatar.cc/150?img=5", + }, + { + id: 6, + fullName: "Pippo Franco", + avatar: "https://i.pravatar.cc/150?img=6", + }, + { + id: 7, + fullName: "Pippo Inzaghi", + avatar: "https://i.pravatar.cc/150?img=7", + }, + { + id: 8, + fullName: "Pippo Civati", + avatar: "https://i.pravatar.cc/150?img=8", + }, + { + id: 9, + fullName: "Pippo Delbono", + avatar: "https://i.pravatar.cc/150?img=9", + }, + ].filter((item) => { + if (!query) return item; + return item.fullName.toLowerCase().startsWith(query.toLowerCase()); + }); + }; + return ( - + @@ -65,6 +118,16 @@ const defaultArgs: EditorStoryArgs = { "

I'm a stupid editor!

", onSave: (editor: TipTapEditor) => { console.log("we have to save this", editor.getHTML()); + const result: any[] = []; + + editor.state.doc.descendants((node) => { + if (node.type.name === "mention") { + // Add only if it's not already in the array + if (!result.some((r) => r.id === node.attrs.id)) + result.push(node.attrs); + } + }); + console.log("mentions", result); }, author: { avatar: "LC", diff --git a/src/stories/chat/parts/bar.tsx b/src/stories/chat/parts/bar.tsx index 1a79e096..4c9dc701 100644 --- a/src/stories/chat/parts/bar.tsx +++ b/src/stories/chat/parts/bar.tsx @@ -71,7 +71,10 @@ const CommentBar = ({ { editor.chain().focus(); - editor.commands.insertContent("@"); + const { from } = editor.state.selection; + + const char = from > 1 ? " @" : "@"; + editor.commands.insertContent(char); }} isPill={false} > diff --git a/src/stories/chat/parts/comment.tsx b/src/stories/chat/parts/comment.tsx index 1eefd5eb..1fdbd23b 100644 --- a/src/stories/chat/parts/comment.tsx +++ b/src/stories/chat/parts/comment.tsx @@ -5,6 +5,7 @@ import { styled } from "styled-components"; import { Author } from "../_types"; import { Avatar } from "../../avatar"; import { CommentEditor } from "./editor"; +import { useChatContext } from "../context/chatContext"; const CommentCard = styled(Card)` padding: ${({ theme }) => `${theme.space.base * 3}px ${theme.space.sm}`}; @@ -52,6 +53,8 @@ export const Comment = ({ children, date, }: PropsWithChildren<{ author: Author; message: string; date: string }>) => { + const { mentionableUsers } = useChatContext(); + return ( @@ -67,7 +70,9 @@ export const Comment = ({ {date} - {message} + + {message} + diff --git a/src/stories/chat/parts/commentBox.tsx b/src/stories/chat/parts/commentBox.tsx index cc077abd..1fc98aa9 100644 --- a/src/stories/chat/parts/commentBox.tsx +++ b/src/stories/chat/parts/commentBox.tsx @@ -1,6 +1,6 @@ import styled from "styled-components"; import { ChatEditorArgs } from "../_types"; -import { PropsWithChildren, useEffect, useState } from "react"; +import { PropsWithChildren } from "react"; import { FloatingMenu } from "../../editor/floatingMenu"; import { Avatar } from "../../avatar"; import { useChatContext } from "../context/chatContext"; @@ -32,7 +32,7 @@ export const CommentBox = ({ }: PropsWithChildren) => { const { children, hasInlineMenu, hasButtonsMenu, bubbleOptions, author } = props; - const { editor } = useChatContext(); + const { editor, mentionableUsers } = useChatContext(); return ( <> @@ -47,6 +47,7 @@ export const CommentBox = ({ diff --git a/src/stories/chat/parts/editor.tsx b/src/stories/chat/parts/editor.tsx index 1cda4b4a..19d81c0b 100644 --- a/src/stories/chat/parts/editor.tsx +++ b/src/stories/chat/parts/editor.tsx @@ -17,11 +17,12 @@ import TextAlign from "@tiptap/extension-text-align"; import { KeyboardEvent as ReactKeyboardEvent, ReactNode } from "react"; import { useChatContext } from "../context/chatContext"; import { CustomMention as Mention } from "./mention"; -import { MentionList, MentionListRef, SuggestedUser } from "./mentionList"; +import { MentionList, MentionListRef } from "./mentionList"; import tippy, { type Instance as TippyInstance } from "tippy.js"; import { FauxInput } from "@zendeskgarden/react-forms"; import styled from "styled-components"; import { editorStyle, readOnlyStyle } from "../../shared/editorStyle"; +import { SuggestedUser } from "../_types"; const EditorContainer = styled(FauxInput)<{ editable: boolean }>` ${({ editable, theme }) => @@ -82,10 +83,12 @@ const DOM_RECT_FALLBACK: DOMRect = { export const CommentEditor = ({ placeholderOptions, children, + mentionableUsers, ...props }: { placeholderOptions?: Partial; children?: ReactNode; + mentionableUsers: ({ query }: { query: string }) => Promise; } & Partial) => { const { editor, setEditor, triggerSave } = useChatContext(); const isEditable = props.editable !== false; @@ -119,60 +122,7 @@ export const CommentEditor = ({ }), Mention.configure({ suggestion: { - items: ({ query }): SuggestedUser[] => { - return [ - { - id: 1, - fullName: "John Doe", - avatar: "https://i.pravatar.cc/150?img=1", - }, - { - id: 2, - fullName: "Jane Doe", - avatar: "https://i.pravatar.cc/150?img=2", - }, - { - id: 3, - fullName: "John Smith", - avatar: "https://i.pravatar.cc/150?img=3", - }, - { - id: 4, - fullName: "Jane Smith", - avatar: "https://i.pravatar.cc/150?img=4", - }, - { - id: 5, - fullName: "Pippo Baudo", - avatar: "https://i.pravatar.cc/150?img=5", - }, - { - id: 6, - fullName: "Pippo Franco", - avatar: "https://i.pravatar.cc/150?img=6", - }, - { - id: 7, - fullName: "Pippo Inzaghi", - avatar: "https://i.pravatar.cc/150?img=7", - }, - { - id: 8, - fullName: "Pippo Civati", - avatar: "https://i.pravatar.cc/150?img=8", - }, - { - id: 9, - fullName: "Pippo Delbono", - avatar: "https://i.pravatar.cc/150?img=9", - }, - ].filter((item) => { - if (!query) return item; - return item.fullName - .toLowerCase() - .startsWith(query.toLowerCase()); - }); - }, + items: mentionableUsers, render: () => { let component: ReactRenderer | undefined; let popup: TippyInstance | undefined; @@ -196,7 +146,7 @@ export const CommentEditor = ({ showOnCreate: true, interactive: true, trigger: "manual", - placement: "bottom-start", + placement: "auto", })[0]; }, diff --git a/src/stories/chat/parts/mentionList.tsx b/src/stories/chat/parts/mentionList.tsx index d36d82f9..d55a6a89 100644 --- a/src/stories/chat/parts/mentionList.tsx +++ b/src/stories/chat/parts/mentionList.tsx @@ -2,6 +2,11 @@ import { forwardRef, useEffect, useImperativeHandle, useState } from "react"; import type { SuggestionOptions, SuggestionProps } from "@tiptap/suggestion"; import { Card } from "../../cards"; import { Button } from "../../buttons/button"; +import { Menu } from "../../dropdowns/menu"; +import { Item } from "../../dropdowns/item"; +import { styled } from "styled-components"; +import { useChatContext } from "../context/chatContext"; +import { SuggestedUser } from "../_types"; export type MentionListRef = { onKeyDown: NonNullable< @@ -11,7 +16,19 @@ export type MentionListRef = { >; }; -export type SuggestedUser = { id: number; fullName: string; avatar: string }; +const StyledCard = styled(Card)` + padding: ${({ theme }) => theme.space.xxs}; +`; + +const List = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.space.xs}; + + & .selected { + background-color: ${({ theme }) => theme.palette.grey[100]}; + } +`; type MentionListProps = SuggestionProps; @@ -66,22 +83,27 @@ export const MentionList = forwardRef( return (
- - {props.items.length ? ( - props.items.map((item, index) => ( - - )) - ) : ( -
No result
- )} -
+ + + {props.items.length ? ( + props.items.map((item, index) => ( +
+ +
+ )) + ) : ( +
No result
+ )} +
+
); } diff --git a/src/stories/shared/editorStyle.tsx b/src/stories/shared/editorStyle.tsx index 4a96ae6f..c7277874 100644 --- a/src/stories/shared/editorStyle.tsx +++ b/src/stories/shared/editorStyle.tsx @@ -256,8 +256,9 @@ export const editorStyle = css` word-break: break-word; mention { - color: ${({ theme }) => theme.palette.green[500]}; + color: ${({ theme }) => theme.palette.azure[600]}; border-radius: ${({ theme }) => theme.borderRadii.xl}; + font-weight: ${({ theme }) => theme.fontWeights.semibold}; padding: ${({ theme }) => `${theme.space.xxs} 0`}; } `;