From 2c370cf56f1a56946b504cf67b8c42e7f09f911e Mon Sep 17 00:00:00 2001 From: Nikhil Kothari Date: Wed, 10 Apr 2024 23:39:09 +0530 Subject: [PATCH] fix: use tiptap renderer on mobile app --- mobile/package.json | 1 + .../chat-space/chat-view/MessageBlock.tsx | 7 +- .../components/TiptapRenderer/Blockquote.tsx | 19 +++ .../components/TiptapRenderer/Bold.tsx | 14 ++ .../components/TiptapRenderer/Link.tsx | 127 ++++++++++++++++++ .../components/TiptapRenderer/Mention.tsx | 75 +++++++++++ .../TiptapRenderer/TiptapRenderer.tsx | 105 +++++++++++++++ .../components/TiptapRenderer/Underline.tsx | 15 +++ mobile/yarn.lock | 5 + .../Renderers/TiptapRenderer/List.tsx | 0 10 files changed, 363 insertions(+), 5 deletions(-) create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Blockquote.tsx create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Bold.tsx create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Link.tsx create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Mention.tsx create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/TiptapRenderer.tsx create mode 100644 mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Underline.tsx delete mode 100644 raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx diff --git a/mobile/package.json b/mobile/package.json index 08778f3d8..eb0af0657 100644 --- a/mobile/package.json +++ b/mobile/package.json @@ -21,6 +21,7 @@ "@radix-ui/themes": "^3.0.2", "@tiptap/extension-code-block-lowlight": "^2.2.3", "@tiptap/extension-highlight": "^2.2.3", + "@tiptap/extension-image": "^2.2.3", "@tiptap/extension-link": "^2.2.3", "@tiptap/extension-mention": "^2.2.3", "@tiptap/extension-placeholder": "^2.2.3", diff --git a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx index cd2309724..df57a323a 100644 --- a/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx +++ b/mobile/src/components/features/chat-space/chat-view/MessageBlock.tsx @@ -1,7 +1,6 @@ import { memo, useContext, useEffect, useMemo, useRef, useState } from 'react' import { FileMessage, ImageMessage, Message, TextMessage, PollMessage } from '../../../../../../types/Messaging/Message' import { IonIcon, IonSkeletonText, IonText } from '@ionic/react' -import { MarkdownRenderer } from '@/components/common/MarkdownRenderer' import { UserFields } from '@/utils/users/UserListProvider' import { DateObjectToFormattedDateStringWithoutYear, DateObjectToTimeString } from '@/utils/operations/operations' import { ChannelMembersContext } from '../ChatInterface' @@ -22,6 +21,7 @@ import { RavenPoll } from '@/types/RavenMessaging/RavenPoll' import { RavenPollOption } from '@/types/RavenMessaging/RavenPollOption' import { MdOutlineBarChart } from 'react-icons/md' import { ViewPollVotes } from '../../polls/ViewPollVotes' +import { TiptapRenderer } from './components/TiptapRenderer/TiptapRenderer' type Props = { message: Message, @@ -232,10 +232,7 @@ const MessageContent = ({ message, onReplyMessageClick, onLongPressDisabled, onL const TextMessageBlock = ({ message, truncate = false }: { message: TextMessage, truncate?: boolean }) => { - - return
- -
+ return } const options = { root: null, diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Blockquote.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Blockquote.tsx new file mode 100644 index 000000000..b579d247d --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Blockquote.tsx @@ -0,0 +1,19 @@ +import { Blockquote } from '@radix-ui/themes'; +import TiptapBlockquote from '@tiptap/extension-blockquote' +import { NodeViewRendererProps, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; + +export const CustomBlockquote = TiptapBlockquote.extend({ + addNodeView() { + return ReactNodeViewRenderer(BlockquoteRenderer) + } +}) + +const BlockquoteRenderer = ({ node }: NodeViewRendererProps) => { + return ( + +
+ {node.textContent} +
+
+ ); +}; \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Bold.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Bold.tsx new file mode 100644 index 000000000..120721f45 --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Bold.tsx @@ -0,0 +1,14 @@ +import TiptapBold from '@tiptap/extension-bold' +import { mergeAttributes } from "@tiptap/react"; + +export const CustomBold = TiptapBold.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "strong", + mergeAttributes(HTMLAttributes, { + class: 'rt-Strong' + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}) \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Link.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Link.tsx new file mode 100644 index 000000000..6258607dd --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Link.tsx @@ -0,0 +1,127 @@ +// import { Skeleton } from '@/components/common/Skeleton'; +// import { Box, Flex, Text } from '@radix-ui/themes'; +import TiptapLink from '@tiptap/extension-link' +import { mergeAttributes, useCurrentEditor } from "@tiptap/react"; +// import { useFrappeGetCall } from 'frappe-react-sdk'; +// import { memo, useMemo } from 'react'; + +export const CustomLink = TiptapLink.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "a", + mergeAttributes(HTMLAttributes, { + class: 'rt-Text rt-reset rt-Link rt-underline-auto break-all' + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}).configure({ + protocols: ['mailto', 'https', 'http'], + openOnClick: false, +}) + +export type LinkPreviewDetails = { + title: string, + description: string, + image: string, + force_title: string, + absolute_image: string, + site_name: string +} + +// export const LinkPreview = memo(({ isScrolling }: { isScrolling?: boolean }) => { + +// const { editor } = useCurrentEditor() + +// // We need to find the first mark of type link in a message and extract the href. + +// const json = editor?.getJSON() + +// const href = useMemo(() => { +// if (!json) return null + +// let firstLink = '' + +// // At every level of the json, we need to find the first mark of type link and extract the href. +// // Once we find the first link, we can stop searching. +// const findFirstLink = (json: any) => { +// if (firstLink) return firstLink + +// if (Array.isArray(json)) { +// for (const item of json) { +// if (typeof item === 'object') { +// findFirstLink(item) +// } +// } +// } else { +// if (json?.type === 'link') { +// const link = json?.attrs?.href +// if (link?.startsWith('mailto')) { +// } else { +// firstLink = json?.attrs?.href +// } +// } else { +// for (const key in json) { +// if (typeof json?.[key] === 'object') { +// findFirstLink(json?.[key]) +// } +// } +// } +// } +// } + +// findFirstLink(json) + +// return firstLink +// }, [json]) + +// // const href = editor?.getAttributes('link').href + + +// const { data, isLoading } = useFrappeGetCall<{ message: LinkPreviewDetails[] }>('raven.api.preview_links.get_preview_link', { +// urls: JSON.stringify([href]) +// }, href ? undefined : null, { +// revalidateIfStale: false, +// revalidateOnFocus: false, +// revalidateOnReconnect: false, +// shouldRetryOnError: false, +// }) + +// if (!href) return null + +// const linkPreview = data?.message?.[0] + +// return +// +// {linkPreview ? linkPreview.site_name && linkPreview.description ? +// {(linkPreview.absolute_image || linkPreview.image) && +// +// {/* Absolute positioned skeleton loader */} +// +// + +// +// + +// {linkPreview.title} + +// +// } +// +// +// {linkPreview.title} +// {linkPreview.site_name} +// +// {linkPreview.description} +// +// : null : + +// null} +// +// + +// }) \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Mention.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Mention.tsx new file mode 100644 index 000000000..76a110dae --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Mention.tsx @@ -0,0 +1,75 @@ +import { UserAvatar } from '@/components/common/UserAvatar'; +import { useGetUser } from '@/hooks/useGetUser'; +import { useIsUserActive } from '@/hooks/useIsUserActive'; +import { Flex, HoverCard, Link, Text } from '@radix-ui/themes'; +import Mention from '@tiptap/extension-mention' +import { NodeViewRendererProps, NodeViewWrapper, ReactNodeViewRenderer } from "@tiptap/react"; +import { BsFillCircleFill } from 'react-icons/bs'; +import { Link as RouterLink } from 'react-router-dom'; + +export const CustomUserMention = Mention.extend({ + name: 'userMention', + addNodeView() { + return ReactNodeViewRenderer(UserMentionRenderer) + } +}) + +export const CustomChannelMention = Mention.extend({ + name: 'channelMention', + addNodeView() { + return ReactNodeViewRenderer(ChannelMentionRenderer) + } +}) + +const UserMentionRenderer = ({ node }: NodeViewRendererProps) => { + + const user = useGetUser(node.attrs.id) + const isActive = useIsUserActive(node.attrs.id) + + return ( + + + + + @{user?.full_name ?? node.attrs.label} + + + + + + + + {user?.full_name ?? node.attrs.label} + {isActive && + + Online + } + + {user && {user?.name}} + + + + + + {/* + @{node.attrs.label} + */} + + ); +}; + + + +const ChannelMentionRenderer = ({ node }: NodeViewRendererProps) => { + + + return ( + + + + @{node.attrs.label} + + + + ); +}; \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/TiptapRenderer.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/TiptapRenderer.tsx new file mode 100644 index 000000000..d8e85123d --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/TiptapRenderer.tsx @@ -0,0 +1,105 @@ +import { EditorContent, EditorContext, useEditor } from '@tiptap/react' +import { TextMessage } from '../../../../../../../../types/Messaging/Message' +import { UserFields } from '@/utils/users/UserListProvider' +import { Box, BoxProps } from '@radix-ui/themes' +import Highlight from '@tiptap/extension-highlight' +import StarterKit from '@tiptap/starter-kit' +import css from 'highlight.js/lib/languages/css' +import js from 'highlight.js/lib/languages/javascript' +import ts from 'highlight.js/lib/languages/typescript' +import html from 'highlight.js/lib/languages/xml' +import json from 'highlight.js/lib/languages/json' +import CodeBlockLowlight from '@tiptap/extension-code-block-lowlight' +import { common, createLowlight } from 'lowlight' +import python from 'highlight.js/lib/languages/python' +import { CustomBlockquote } from './Blockquote' +import { CustomBold } from './Bold' +import { CustomUserMention } from './Mention' +import { CustomLink } from './Link' +import { CustomUnderline } from './Underline' +import { Image } from '@tiptap/extension-image' +import { clsx } from 'clsx' +import Italic from '@tiptap/extension-italic'; +const lowlight = createLowlight(common) + +lowlight.register('html', html) +lowlight.register('css', css) +lowlight.register('js', js) +lowlight.register('ts', ts) +lowlight.register('json', json) +lowlight.register('python', python) +type TiptapRendererProps = BoxProps & { + message: TextMessage, + user?: UserFields, + showLinkPreview?: boolean, + isScrolling?: boolean, + isTruncated?: boolean +} + +export const TiptapRenderer = ({ message, user, isScrolling = false, isTruncated = false, showLinkPreview = true, ...props }: TiptapRendererProps) => { + + const editor = useEditor({ + content: message.text, + editable: false, + editorProps: { + attributes: { + class: isTruncated ? 'line-clamp-3' : '' + } + }, + enableCoreExtensions: true, + extensions: [ + StarterKit.configure({ + heading: false, + codeBlock: false, + bold: false, + blockquote: false, + italic: false, + listItem: { + HTMLAttributes: { + class: 'ml-5 rt-Text text-base' + } + }, + paragraph: { + HTMLAttributes: { + class: 'rt-Text text-base' + } + } + }), + Highlight.configure({ + multicolor: true, + HTMLAttributes: { + class: 'bg-[var(--yellow-6)] dark:bg-[var(--yellow-11)] px-2 py-1' + } + }), + CustomUnderline, + CodeBlockLowlight.configure({ + lowlight + }), + CustomBlockquote, + CustomBold, + CustomUserMention, + CustomLink, + Italic, + Image.configure({ + HTMLAttributes: { + class: 'mt-2 object-cover max-w-[280px]' + }, + inline: true + }), + // TODO: Add channel mention + // CustomChannelMention + ] + }) + + return ( + + + + {/* {showLinkPreview && } */} + + + ) +} \ No newline at end of file diff --git a/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Underline.tsx b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Underline.tsx new file mode 100644 index 000000000..a4da9fa7e --- /dev/null +++ b/mobile/src/components/features/chat-space/chat-view/components/TiptapRenderer/Underline.tsx @@ -0,0 +1,15 @@ +import TiptapUnderline from '@tiptap/extension-underline' +import { mergeAttributes } from "@tiptap/react"; + +export const CustomUnderline = TiptapUnderline.extend({ + renderHTML({ HTMLAttributes }) { + return [ + "span", + mergeAttributes(HTMLAttributes, { + class: 'rt-Text rt-reset rt-Link rt-underline-always text-gray-12', + 'data-accent-color': 'gray', + }), // mergeAttributes is a exported function from @tiptap/core + 0, + ]; + }, +}) \ No newline at end of file diff --git a/mobile/yarn.lock b/mobile/yarn.lock index bc2a55aed..56609c796 100644 --- a/mobile/yarn.lock +++ b/mobile/yarn.lock @@ -2627,6 +2627,11 @@ resolved "https://registry.yarnpkg.com/@tiptap/extension-horizontal-rule/-/extension-horizontal-rule-2.3.0.tgz#e4934b32c005b1cf0f03fab5f4caff8cacbf4c7e" integrity sha512-4DB8GU3uuDzzyqUmONIb3CHXcQ6Nuy4mHHkFSmUyEjg1i5eMQU5H7S6mNvZbltcJB2ImgCSwSMlj1kVN3MLIPg== +"@tiptap/extension-image@^2.2.3": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@tiptap/extension-image/-/extension-image-2.3.0.tgz#914c4c5030fa4278f53e808c03819f7422da4497" + integrity sha512-v1fLEEzrfXWavsLFUEkTiYYxwm1WDNrjuUriU5tG2Jv22NL1BL4BLVbZbGdkAk+qHWy8QWszrDJbcgGh2VNCoQ== + "@tiptap/extension-italic@^2.3.0": version "2.3.0" resolved "https://registry.yarnpkg.com/@tiptap/extension-italic/-/extension-italic-2.3.0.tgz#72279632a2652642abed2078a920e2e05aff030e" diff --git a/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx b/raven-app/src/components/feature/chat/ChatMessage/Renderers/TiptapRenderer/List.tsx deleted file mode 100644 index e69de29bb..000000000