chore: initial commit
This commit is contained in:
114
components/Messages/Message/MessageContent/MessageFile.tsx
Normal file
114
components/Messages/Message/MessageContent/MessageFile.tsx
Normal file
@ -0,0 +1,114 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import prettyBytes from 'pretty-bytes'
|
||||
|
||||
import { useAuthentication } from 'utils/authentication'
|
||||
import { MessageContentProps } from '.'
|
||||
import { Loader } from 'components/design/Loader'
|
||||
import { IconButton } from 'components/design/IconButton'
|
||||
|
||||
export interface FileData {
|
||||
blob: Blob
|
||||
url: string
|
||||
}
|
||||
|
||||
export const MessageFile: React.FC<MessageContentProps> = (props) => {
|
||||
const { authentication } = useAuthentication()
|
||||
const [file, setFile] = useState<FileData | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
const fetchData = async (): Promise<void> => {
|
||||
const { data } = await authentication.api.get(props.value, {
|
||||
responseType: 'blob'
|
||||
})
|
||||
const fileURL = URL.createObjectURL(data)
|
||||
setFile({ blob: data, url: fileURL })
|
||||
}
|
||||
fetchData().catch(() => {})
|
||||
}, [])
|
||||
|
||||
if (file == null) {
|
||||
return <Loader />
|
||||
}
|
||||
if (props.mimetype.startsWith('image/')) {
|
||||
return (
|
||||
<>
|
||||
<a href={file.url} target='_blank' rel='noreferrer'>
|
||||
<img src={file.url} />
|
||||
</a>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
img {
|
||||
max-width: 30vw;
|
||||
max-height: 30vw;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
if (props.mimetype.startsWith('audio/')) {
|
||||
return (
|
||||
<audio controls>
|
||||
<source src={file.url} type={props.mimetype} />
|
||||
</audio>
|
||||
)
|
||||
}
|
||||
if (props.mimetype.startsWith('video/')) {
|
||||
return (
|
||||
<>
|
||||
<video controls>
|
||||
<source src={file.url} type={props.mimetype} />
|
||||
</video>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
video {
|
||||
max-width: 250px;
|
||||
max-height: 250px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<div className='message-file'>
|
||||
<div className='file-informations'>
|
||||
<div className='file-icon'>
|
||||
<img src='/images/svg/icons/file.svg' alt='file' />
|
||||
</div>
|
||||
<div className='file-title'>
|
||||
<div className='file-name'>{file.blob.type}</div>
|
||||
<div className='file-size'>{prettyBytes(file.blob.size)}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='download-button'>
|
||||
<a href={file.url} download>
|
||||
<IconButton icon='download' />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.message-file {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.file-informations {
|
||||
display: flex;
|
||||
}
|
||||
.file-title {
|
||||
margin-left: 10px;
|
||||
}
|
||||
.file-size {
|
||||
color: var(--color-tertiary);
|
||||
margin-top: 5px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
62
components/Messages/Message/MessageContent/MessageText.tsx
Normal file
62
components/Messages/Message/MessageContent/MessageText.tsx
Normal file
@ -0,0 +1,62 @@
|
||||
import { useMemo } from 'react'
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import gfm from 'remark-gfm'
|
||||
import Tex from '@matejmazur/react-katex'
|
||||
import math from 'remark-math'
|
||||
import 'katex/dist/katex.min.css'
|
||||
|
||||
import { Emoji, emojiPlugin, isStringWithOnlyOneEmoji } from 'components/Emoji'
|
||||
|
||||
export interface MessageTextProps {
|
||||
value: string
|
||||
}
|
||||
|
||||
export const MessageText: React.FC<MessageTextProps> = (props) => {
|
||||
const isMessageWithOnlyOneEmoji = useMemo(() => {
|
||||
return isStringWithOnlyOneEmoji(props.value)
|
||||
}, [props.value])
|
||||
|
||||
if (isMessageWithOnlyOneEmoji) {
|
||||
return (
|
||||
<div className='message-content'>
|
||||
<p>
|
||||
<Emoji value={props.value} size={40} />
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ReactMarkdown
|
||||
disallowedTypes={['heading', 'table']}
|
||||
unwrapDisallowed
|
||||
plugins={[[gfm], [emojiPlugin], [math]]}
|
||||
linkTarget='_blank'
|
||||
renderers={{
|
||||
inlineMath: ({ value }) => <Tex math={value} />,
|
||||
math: ({ value }) => <Tex block math={value} />,
|
||||
emoji: ({ value }) => {
|
||||
return <Emoji value={value} size={20} />
|
||||
}
|
||||
}}
|
||||
>
|
||||
{props.value}
|
||||
</ReactMarkdown>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
.message-content p {
|
||||
margin: 0;
|
||||
line-height: 30px;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
.message-content .katex,
|
||||
.message-content .katex-display {
|
||||
text-align: initial;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
40
components/Messages/Message/MessageContent/index.tsx
Normal file
40
components/Messages/Message/MessageContent/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { Loader } from 'components/design/Loader'
|
||||
import { MessageType } from 'contexts/Messages'
|
||||
import { MessageFile } from './MessageFile'
|
||||
import { MessageText } from './MessageText'
|
||||
|
||||
export interface MessageContentProps {
|
||||
value: string
|
||||
type: MessageType
|
||||
mimetype: string
|
||||
}
|
||||
|
||||
export const MessageContent: React.FC<MessageContentProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<div className='message-content'>
|
||||
{props.type === 'text' ? (
|
||||
<MessageText value={props.value} />
|
||||
) : props.type === 'file' ? (
|
||||
<MessageFile {...props} />
|
||||
) : (
|
||||
<Loader />
|
||||
)}
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.message-content {
|
||||
font-family: 'Roboto', 'Arial', 'sans-serif';
|
||||
font-size: 16px;
|
||||
font-weight: 400;
|
||||
position: relative;
|
||||
margin-left: -75px;
|
||||
padding-left: 75px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
48
components/Messages/Message/MessageHeader.tsx
Normal file
48
components/Messages/Message/MessageHeader.tsx
Normal file
@ -0,0 +1,48 @@
|
||||
import date from 'date-and-time'
|
||||
import { User } from 'utils/authentication'
|
||||
|
||||
export interface MessageHeaderProps {
|
||||
user: User
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
export const MessageHeader: React.FC<MessageHeaderProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<h2 className='message-header'>
|
||||
<span className='username'>{props.user.name}</span>
|
||||
<span className='date'>
|
||||
{date.format(new Date(props.createdAt), 'DD/MM/YYYY - HH:mm:ss')}
|
||||
</span>
|
||||
</h2>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.message-header {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
position: relative;
|
||||
line-height: 1.375rem;
|
||||
min-height: 1.375rem;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.username {
|
||||
font-family: 'Poppins', 'Arial', 'sans-serif';
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--color-secondary);
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
.date {
|
||||
font-family: 'Poppins', 'Arial', 'sans-serif';
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
margin-left: 1em;
|
||||
color: var(--color-tertiary);
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
34
components/Messages/Message/UserAvatar.tsx
Normal file
34
components/Messages/Message/UserAvatar.tsx
Normal file
@ -0,0 +1,34 @@
|
||||
import { Avatar } from 'components/design/Avatar'
|
||||
import { API_URL } from 'utils/api'
|
||||
import { User } from 'utils/authentication'
|
||||
|
||||
export interface UserAvatarProps {
|
||||
user: User
|
||||
}
|
||||
|
||||
export const UserAvatar: React.FC<UserAvatarProps> = (props) => {
|
||||
return (
|
||||
<>
|
||||
<span className='user-avatar'>
|
||||
<Avatar
|
||||
src={`${API_URL}${props.user.logo}`}
|
||||
alt={props.user.name}
|
||||
width={50}
|
||||
height={50}
|
||||
/>
|
||||
</span>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.user-avatar {
|
||||
cursor: pointer;
|
||||
position: absolute;
|
||||
flex: 0 0 auto;
|
||||
left: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
40
components/Messages/Message/index.tsx
Normal file
40
components/Messages/Message/index.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { memo } from 'react'
|
||||
|
||||
import { MessageContent } from './MessageContent'
|
||||
import { MessageHeader } from './MessageHeader'
|
||||
import { UserAvatar } from './UserAvatar'
|
||||
import { Message as MessageProps } from 'contexts/Messages'
|
||||
|
||||
export const Message: React.FunctionComponent<MessageProps> = memo((props) => {
|
||||
return (
|
||||
<>
|
||||
<div className='message'>
|
||||
<UserAvatar user={props.user} />
|
||||
<MessageHeader createdAt={props.createdAt} user={props.user} />
|
||||
<MessageContent
|
||||
value={props.value}
|
||||
type={props.type}
|
||||
mimetype={props.mimetype}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
.message:hover {
|
||||
background-color: var(--color-background-tertiary);
|
||||
}
|
||||
.message {
|
||||
transition: background-color 0.15s ease-in-out;
|
||||
margin-top: 2.3rem;
|
||||
min-height: 2.75rem;
|
||||
padding-left: 72px;
|
||||
position: relative;
|
||||
word-wrap: break-word;
|
||||
flex: 0 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
})
|
58
components/Messages/index.tsx
Normal file
58
components/Messages/index.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { useEffect } from 'react'
|
||||
import InfiniteScroll from 'react-infinite-scroll-component'
|
||||
|
||||
import { Message } from './Message'
|
||||
import { Loader } from 'components/design/Loader'
|
||||
import { useMessages } from 'contexts/Messages'
|
||||
import { Emoji } from 'emoji-mart'
|
||||
import { emojiSet } from 'components/Emoji'
|
||||
|
||||
export const Messages: React.FC = () => {
|
||||
const { messages, nextPage } = useMessages()
|
||||
|
||||
useEffect(() => {
|
||||
window.scrollTo(0, document.body.scrollHeight)
|
||||
}, [])
|
||||
|
||||
if (messages.rows.length === 0) {
|
||||
return (
|
||||
<div id='messages'>
|
||||
<p>
|
||||
Nothing to show here!{' '}
|
||||
<Emoji set={emojiSet} emoji=':ghost:' size={20} />
|
||||
</p>
|
||||
<p>Start chatting to kill this Ghost!</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div id='messages'>
|
||||
<InfiniteScroll
|
||||
dataLength={messages.rows.length}
|
||||
next={nextPage}
|
||||
inverse
|
||||
scrollableTarget='messages'
|
||||
hasMore={messages.hasMore}
|
||||
loader={<Loader />}
|
||||
>
|
||||
{messages.rows.map((message) => {
|
||||
return <Message key={message.id} {...message} />
|
||||
})}
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
|
||||
<style jsx>
|
||||
{`
|
||||
#messages {
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column-reverse;
|
||||
height: 800px;
|
||||
}
|
||||
`}
|
||||
</style>
|
||||
</>
|
||||
)
|
||||
}
|
Reference in New Issue
Block a user