feat: add support for files and math for messages (#5)

This commit is contained in:
Divlo
2022-01-07 21:21:38 +01:00
committed by GitHub
parent fdc2a2d1de
commit 5c03a9b944
57 changed files with 3403 additions and 2992 deletions

View File

@ -6,13 +6,13 @@ import { useMediaQuery } from 'react-responsive'
import { useSwipeable } from 'react-swipeable'
import { Sidebar, DirectionSidebar } from './Sidebar'
import { IconButton } from 'components/design/IconButton'
import { IconLink } from 'components/design/IconLink'
import { IconButton } from '../design/IconButton'
import { IconLink } from '../design/IconLink'
import { Guilds } from './Guilds/Guilds'
import { Divider } from '../design/Divider'
import { Members } from './Members'
import { useAuthentication } from 'tools/authentication'
import { API_URL } from 'tools/api'
import { useAuthentication } from '../../tools/authentication'
import { API_URL } from '../../tools/api'
export interface GuildsChannelsPath {
guildId: number

View File

@ -1,6 +1,6 @@
import { Meta, Story } from '@storybook/react'
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
import { Channel as Component, ChannelProps } from './Channel'
const Stories: Meta = {

View File

@ -1,6 +1,6 @@
import { render } from '@testing-library/react'
import { channelExample } from 'cypress/fixtures/channels/channel'
import { channelExample } from '../../../../cypress/fixtures/channels/channel'
import { Channel } from './Channel'
describe('<Channel />', () => {

View File

@ -1,9 +1,9 @@
import InfiniteScroll from 'react-infinite-scroll-component'
import { useChannels } from '../../../contexts/Channels'
import { GuildsChannelsPath } from '../Application'
import { Loader } from 'components/design/Loader'
import { Loader } from '../../design/Loader'
import { Channel } from './Channel'
import { useChannels } from 'contexts/Channels'
export interface ChannelsProps {
path: GuildsChannelsPath

View File

@ -1,9 +1,9 @@
import { CogIcon, PlusIcon } from '@heroicons/react/solid'
import { useGuildMember } from 'contexts/GuildMember'
import { Divider } from 'components/design/Divider'
import { Channels } from 'components/Application/Channels'
import { IconButton } from 'components/design/IconButton'
import { useGuildMember } from '../../../contexts/GuildMember'
import { Divider } from '../../design/Divider'
import { Channels } from '../../Application/Channels'
import { IconButton } from '../../design/IconButton'
import { GuildsChannelsPath } from '..'
export interface GuildLeftSidebarProps {

View File

@ -1,8 +1,8 @@
import InfiniteScroll from 'react-infinite-scroll-component'
import { Loader } from 'components/design/Loader'
import { Loader } from '../../design/Loader'
import { Guild } from './Guild'
import { useGuilds } from 'contexts/Guilds'
import { useGuilds } from '../../../contexts/Guilds'
import { GuildsChannelsPath } from '..'
export interface GuildsProps {

View File

@ -2,11 +2,11 @@ import useTranslation from 'next-translate/useTranslation'
import { useEffect, useState } from 'react'
import InfiniteScroll from 'react-infinite-scroll-component'
import { useAuthentication } from 'tools/authentication'
import { GuildPublic as GuildPublicType } from 'models/Guild'
import { Loader } from 'components/design/Loader'
import { useAuthentication } from '../../../tools/authentication'
import { GuildPublic as GuildPublicType } from '../../../models/Guild'
import { Loader } from '../../design/Loader'
import { GuildPublic } from './GuildPublic'
import { usePagination } from 'hooks/usePagination'
import { usePagination } from '../../../hooks/usePagination'
export const JoinGuildsPublic: React.FC = () => {
const [search, setSearch] = useState('')

View File

@ -2,8 +2,8 @@ import useTranslation from 'next-translate/useTranslation'
import InfiniteScroll from 'react-infinite-scroll-component'
import { Divider } from '../../design/Divider'
import { Loader } from 'components/design/Loader'
import { useMembers } from 'contexts/Members'
import { Loader } from '../../design/Loader'
import { useMembers } from '../../../contexts/Members'
import { Member } from './Member'
import { capitalize } from '../../../tools/utils/capitalize'

View File

@ -4,7 +4,9 @@ import date from 'date-and-time'
import { MessageWithMember } from '../../../../models/Message'
import { API_URL } from '../../../../tools/api'
import { MessageContent } from './MessageContent'
import { MessageText } from './MessageText'
import { Loader } from '../../../design/Loader'
import { MessageFile } from './MessageFile'
export interface MessageProps {
message: MessageWithMember
@ -14,7 +16,10 @@ export const Message: React.FC<MessageProps> = (props) => {
const { message } = props
return (
<div className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'>
<div
className='p-4 flex transition hover:bg-gray-200 dark:hover:bg-gray-900'
data-cy={`message-${message.id}`}
>
<Link href={`/application/users/${message.member.user.id}`}>
<a>
<div className='w-12 h-12 mr-4 flex flex-shrink-0 items-center justify-center'>
@ -54,7 +59,13 @@ export const Message: React.FC<MessageProps> = (props) => {
{date.format(new Date(message.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
</span>
</div>
<MessageContent message={message} />
{message.type === 'text' ? (
<MessageText message={message} />
) : message.type === 'file' ? (
<MessageFile message={message} />
) : (
<Loader />
)}
</div>
</div>
)

View File

@ -1 +0,0 @@
export * from './MessageContent'

View File

@ -0,0 +1,14 @@
export const FileIcon: React.FC = () => {
return (
<svg
className='dark:text-white text-black fill-current'
width='21'
height='26'
viewBox='0 0 21 26'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path d='M2.625 0C1.92881 0 1.26113 0.273928 0.768845 0.761522C0.276562 1.24912 0 1.91044 0 2.6V23.4C0 24.0896 0.276562 24.7509 0.768845 25.2385C1.26113 25.7261 1.92881 26 2.625 26H18.375C19.0712 26 19.7389 25.7261 20.2312 25.2385C20.7234 24.7509 21 24.0896 21 23.4V7.8L13.125 0H2.625ZM13.125 9.1H11.8125V2.6L18.375 9.1H13.125Z' />
</svg>
)
}

View File

@ -0,0 +1,96 @@
import { useState, useEffect } from 'react'
import Image from 'next/image'
import axios from 'axios'
import prettyBytes from 'pretty-bytes'
import { DownloadIcon } from '@heroicons/react/solid'
import { useAuthentication } from '../../../../../tools/authentication'
import { MessageWithMember } from '../../../../../models/Message'
import { Loader } from '../../../../design/Loader'
import { FileIcon } from './FileIcon'
export interface FileData {
blob: Blob
url: string
}
export interface MessageContentProps {
message: MessageWithMember
}
export const MessageFile: React.FC<MessageContentProps> = (props) => {
const { message } = props
const { authentication } = useAuthentication()
const [file, setFile] = useState<FileData | null>(null)
useEffect(() => {
const ourRequest = axios.CancelToken.source()
const fetchData = async (): Promise<void> => {
const { data } = await authentication.api.get(message.value, {
responseType: 'blob',
cancelToken: ourRequest.token
})
const fileURL = URL.createObjectURL(data)
setFile({ blob: data, url: fileURL })
}
fetchData().catch(() => {})
return () => {
ourRequest.cancel()
}
}, [message.value, authentication.api])
if (file == null) {
return <Loader />
}
if (message.mimetype.startsWith('image/')) {
return (
<a href={file.url} target='_blank' rel='noreferrer'>
<Image
data-cy={`message-file-image-${message.id}`}
className='max-w-xs max-h-xs'
src={file.url}
alt={message.value}
width={320}
height={320}
/>
</a>
)
}
if (message.mimetype.startsWith('audio/')) {
return (
<audio controls data-cy={`message-file-audio-${message.id}`}>
<source src={file.url} type={message.mimetype} />
</audio>
)
}
if (message.mimetype.startsWith('video/')) {
return (
<video
className='max-w-xs max-h-xs'
controls
data-cy={`message-file-video-${message.id}`}
>
<source src={file.url} type={message.mimetype} />
</video>
)
}
return (
<a href={file.url} download data-cy={`message-file-download-${message.id}`}>
<div className='flex items-center'>
<div className='flex items-center'>
<div>
<FileIcon />
</div>
<div className='ml-4'>
<p>{file.blob.type}</p>
<p className='mt-1'>{prettyBytes(file.blob.size)}</p>
</div>
</div>
<DownloadIcon className='ml-4 w-8 h-8' />
</div>
</a>
)
}

View File

@ -0,0 +1 @@
export * from './MessageFile'

View File

@ -2,6 +2,10 @@ import { useMemo } from 'react'
import ReactMarkdown from 'react-markdown'
import gfm from 'remark-gfm'
import remarkBreaks from 'remark-breaks'
import remarkMath from 'remark-math'
import rehypeKatex from 'rehype-katex'
import 'katex/dist/katex.min.css'
import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from '../../../../Emoji'
import { MessageWithMember } from '../../../../../models/Message'
@ -10,7 +14,7 @@ export interface MessageContentProps {
message: MessageWithMember
}
export const MessageContent: React.FC<MessageContentProps> = (props) => {
export const MessageText: React.FC<MessageContentProps> = (props) => {
const { message } = props
const isMessageWithOnlyOneEmoji = useMemo(() => {
@ -31,8 +35,8 @@ export const MessageContent: React.FC<MessageContentProps> = (props) => {
<ReactMarkdown
disallowedElements={['table']}
unwrapDisallowed
remarkPlugins={[[gfm], [remarkBreaks]]}
rehypePlugins={[emojiPlugin]}
remarkPlugins={[[gfm], [remarkBreaks], [remarkMath]]}
rehypePlugins={[[emojiPlugin], [rehypeKatex]]}
linkTarget='_blank'
components={{
emoji: (props) => {

View File

@ -0,0 +1 @@
export * from './MessageText'

View File

@ -1,9 +1,9 @@
import InfiniteScroll from 'react-infinite-scroll-component'
import { Loader } from 'components/design/Loader'
import { Loader } from '../../design/Loader'
import { Message } from './Message'
import { useMessages } from 'contexts/Messages'
import { Emoji } from 'components/Emoji'
import { useMessages } from '../../../contexts/Messages'
import { Emoji } from '../../Emoji'
export const Messages: React.FC = () => {
const { messages, hasMore, nextPage } = useMessages()

View File

@ -1,9 +1,9 @@
import { Meta, Story } from '@storybook/react'
import {
userExample,
userSettingsExample
} from '../../../cypress/fixtures/users/user'
import { UserProfile as Component, UserProfileProps } from './UserProfile'
const Stories: Meta = {

View File

@ -1,13 +1,15 @@
import { render } from '@testing-library/react'
import { user, userSettings } from '../../../cypress/fixtures/users/user'
import {
userExample,
userSettingsExample
} from '../../../cypress/fixtures/users/user'
import { UserProfile } from './UserProfile'
describe('<UserProfile />', () => {
it('should render successfully', () => {
const { baseElement } = render(
<UserProfile user={{ ...user, settings: userSettings }} />
<UserProfile user={{ ...userExample, settings: userSettingsExample }} />
)
expect(baseElement).toBeTruthy()
})