Skip to content

Commit

Permalink
Merge pull request #850 from The-Commit-Company/fix-mobile-app-renderer
Browse files Browse the repository at this point in the history
fix: use tiptap renderer on mobile app
  • Loading branch information
nikkothari22 authored Apr 10, 2024
2 parents 35aecdd + 2c370cf commit bdc6dfe
Show file tree
Hide file tree
Showing 10 changed files with 363 additions and 5 deletions.
1 change: 1 addition & 0 deletions mobile/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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,
Expand Down Expand Up @@ -232,10 +232,7 @@ const MessageContent = ({ message, onReplyMessageClick, onLongPressDisabled, onL

const TextMessageBlock = ({ message, truncate = false }: { message: TextMessage, truncate?: boolean }) => {


return <div className={'py-0.5 rounded-lg' + (truncate ? ' line-clamp-3' : '')}>
<MarkdownRenderer content={message.text} truncate={truncate} />
</div>
return <TiptapRenderer message={message} />
}
const options = {
root: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<NodeViewWrapper>
<Blockquote m='2' size='3'>
{node.textContent}
</Blockquote>
</NodeViewWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -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,
];
},
})
Original file line number Diff line number Diff line change
@@ -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 <a href={href} target='_blank'>
// <Flex direction='column' gap='2' py='2'>
// {linkPreview ? linkPreview.site_name && linkPreview.description ? <Flex gap='4'>
// {(linkPreview.absolute_image || linkPreview.image) &&
// <Box className='relative min-w-[18rem] min-h-[9rem] w-72 h-36'>
// {/* Absolute positioned skeleton loader */}
// <Box className='absolute top-0 z-0 left-0 w-72 h-36' >
// <Box className='animate-pulse bg-gray-3 z-0 w-72 h-36 dark:bg-gray-5 rounded-md'>

// </Box>
// </Box>

// <img
// width='100%'
// className='absolute object-cover min-w-[18rem] min-h-[9rem] w-72 h-36 rounded-md z-50 top-0 left-0'
// src={linkPreview.absolute_image || linkPreview.image}
// alt={linkPreview.title} />

// </Box>
// }
// <Flex direction='column' gap='1' py='1' className='w-84'>
// <Flex gap='1' direction='column'>
// <Text as='span' weight='bold' size='5' className='cal-sans'>{linkPreview.title}</Text>
// <Text as='span' color='gray' size='2' weight='medium'>{linkPreview.site_name}</Text>
// </Flex>
// <Text as='p' size='2' className='whitespace-break-spaces'>{linkPreview.description}</Text>
// </Flex>
// </Flex> : null :

// null}
// </Flex>
// </a>

// })
Original file line number Diff line number Diff line change
@@ -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 (
<NodeViewWrapper as={'span'}>
<HoverCard.Root>
<HoverCard.Trigger>
<Link size='2'>
@{user?.full_name ?? node.attrs.label}
</Link>
</HoverCard.Trigger>
<HoverCard.Content>
<Flex gap='2' align='center'>
<UserAvatar src={user?.user_image} alt={user?.full_name ?? node.attrs.label} size='4' />
<Flex direction='column'>
<Flex gap='3' align='center'>
<Text className='text-gray-12' weight='bold' size='3'>{user?.full_name ?? node.attrs.label}</Text>
{isActive && <Flex gap='1' align='center'>
<BsFillCircleFill className='text-green-400' size='8' />
<Text className='text-gray-10' size='1'>Online</Text>
</Flex>}
</Flex>
{user && <Text className='text-gray-11' size='1'>{user?.name}</Text>}
</Flex>
</Flex>

</HoverCard.Content>
</HoverCard.Root>
{/* <Link>
@{node.attrs.label}
</Link> */}
</NodeViewWrapper>
);
};



const ChannelMentionRenderer = ({ node }: NodeViewRendererProps) => {


return (
<NodeViewWrapper as={'span'}>
<Link asChild>
<RouterLink to={`/channels/${node.attrs.id}`}>
@{node.attrs.label}
</RouterLink>
</Link>
</NodeViewWrapper>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<Box className={clsx('overflow-x-hidden text-ellipsis', props.className)} {...props}>
<EditorContext.Provider value={{ editor }}>
<EditorContent
contentEditable={false}
editor={editor}
readOnly />
{/* {showLinkPreview && <LinkPreview isScrolling={isScrolling} />} */}
</EditorContext.Provider>
</Box>
)
}
Loading

0 comments on commit bdc6dfe

Please sign in to comment.