chore: initial commit

This commit is contained in:
Divlo
2021-10-24 05:19:39 +02:00
commit 21123c4477
145 changed files with 48821 additions and 0 deletions

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
}

View 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>
</>
)
})

View 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>
</>
)
}