feat: add realtime with socket.io
This commit is contained in:
parent
5c03a9b944
commit
9229131c1a
@ -35,6 +35,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"@typescript-eslint/no-namespace": "off"
|
"@typescript-eslint/no-namespace": "off",
|
||||||
|
"@next/next/no-img-element": "off"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { useState, useEffect } from 'react'
|
import { useState, useEffect } from 'react'
|
||||||
import Image from 'next/image'
|
|
||||||
import axios from 'axios'
|
import axios from 'axios'
|
||||||
import prettyBytes from 'pretty-bytes'
|
import prettyBytes from 'pretty-bytes'
|
||||||
import { DownloadIcon } from '@heroicons/react/solid'
|
import { DownloadIcon } from '@heroicons/react/solid'
|
||||||
@ -48,13 +47,11 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
|
|||||||
if (message.mimetype.startsWith('image/')) {
|
if (message.mimetype.startsWith('image/')) {
|
||||||
return (
|
return (
|
||||||
<a href={file.url} target='_blank' rel='noreferrer'>
|
<a href={file.url} target='_blank' rel='noreferrer'>
|
||||||
<Image
|
<img
|
||||||
data-cy={`message-file-image-${message.id}`}
|
data-cy={`message-file-image-${message.id}`}
|
||||||
className='max-w-xs max-h-xs'
|
className='sm:max-w-xs max-h-80'
|
||||||
src={file.url}
|
src={file.url}
|
||||||
alt={message.value}
|
alt={message.value}
|
||||||
width={320}
|
|
||||||
height={320}
|
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
)
|
)
|
||||||
@ -69,7 +66,7 @@ export const MessageFile: React.FC<MessageContentProps> = (props) => {
|
|||||||
if (message.mimetype.startsWith('video/')) {
|
if (message.mimetype.startsWith('video/')) {
|
||||||
return (
|
return (
|
||||||
<video
|
<video
|
||||||
className='max-w-xs max-h-xs'
|
className='max-w-xs max-h-80'
|
||||||
controls
|
controls
|
||||||
data-cy={`message-file-video-${message.id}`}
|
data-cy={`message-file-video-${message.id}`}
|
||||||
>
|
>
|
||||||
|
@ -29,7 +29,7 @@ export const Messages: React.FC = () => {
|
|||||||
>
|
>
|
||||||
<InfiniteScroll
|
<InfiniteScroll
|
||||||
scrollableTarget='messages'
|
scrollableTarget='messages'
|
||||||
className='messages-list'
|
className='messages-list !overflow-x-hidden'
|
||||||
dataLength={messages.length}
|
dataLength={messages.length}
|
||||||
next={nextPage}
|
next={nextPage}
|
||||||
inverse
|
inverse
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import { Meta, Story } from '@storybook/react'
|
import { Meta, Story } from '@storybook/react'
|
||||||
|
|
||||||
import { SendMessage as Component } from './SendMessage'
|
import { channelExample } from '../../../cypress/fixtures/channels/channel'
|
||||||
|
import { guildExample } from '../../../cypress/fixtures/guilds/guild'
|
||||||
|
import { SendMessage as Component, SendMessageProps } from './SendMessage'
|
||||||
|
|
||||||
const Stories: Meta = {
|
const Stories: Meta = {
|
||||||
title: 'SendMessage',
|
title: 'SendMessage',
|
||||||
@ -9,7 +11,9 @@ const Stories: Meta = {
|
|||||||
|
|
||||||
export default Stories
|
export default Stories
|
||||||
|
|
||||||
export const SendMessage: Story = (arguments_) => {
|
export const SendMessage: Story<SendMessageProps> = (arguments_) => {
|
||||||
return <Component {...arguments_} />
|
return <Component {...arguments_} />
|
||||||
}
|
}
|
||||||
SendMessage.args = {}
|
SendMessage.args = {
|
||||||
|
path: { channelId: channelExample.id, guildId: guildExample.id }
|
||||||
|
}
|
||||||
|
@ -1,10 +0,0 @@
|
|||||||
import { render } from '@testing-library/react'
|
|
||||||
|
|
||||||
import { SendMessage } from './SendMessage'
|
|
||||||
|
|
||||||
describe('<SendMessage />', () => {
|
|
||||||
it('should render successfully', () => {
|
|
||||||
const { baseElement } = render(<SendMessage />)
|
|
||||||
expect(baseElement).toBeTruthy()
|
|
||||||
})
|
|
||||||
})
|
|
@ -1,28 +1,115 @@
|
|||||||
|
import { useState, useRef } from 'react'
|
||||||
import useTranslation from 'next-translate/useTranslation'
|
import useTranslation from 'next-translate/useTranslation'
|
||||||
import TextareaAutosize from 'react-textarea-autosize'
|
import TextareaAutosize from 'react-textarea-autosize'
|
||||||
|
|
||||||
export const SendMessage: React.FC = () => {
|
import { GuildsChannelsPath } from '..'
|
||||||
|
import { useAuthentication } from '../../../tools/authentication'
|
||||||
|
import { EmojiPicker, EmojiPickerOnClick } from '../../Emoji'
|
||||||
|
|
||||||
|
export interface SendMessageProps {
|
||||||
|
path: GuildsChannelsPath
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SendMessage: React.FC<SendMessageProps> = (props) => {
|
||||||
|
const { path } = props
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { authentication } = useAuthentication()
|
||||||
|
|
||||||
|
const [isVisibleEmojiPicker, setIsVisibleEmojiPicker] = useState(false)
|
||||||
|
const [message, setMessage] = useState('')
|
||||||
|
const textareaReference = useRef<HTMLTextAreaElement>(null)
|
||||||
|
|
||||||
|
const handleTextareaKeyDown: React.KeyboardEventHandler<HTMLFormElement> = (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
if (event.key === 'Enter' && !event.shiftKey) {
|
||||||
|
event.preventDefault()
|
||||||
|
event.currentTarget.dispatchEvent(
|
||||||
|
new Event('submit', { cancelable: true, bubbles: true })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit: React.FormEventHandler<HTMLFormElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
event.preventDefault()
|
||||||
|
if (typeof message === 'string' && message.length > 0) {
|
||||||
|
await authentication.api.post(`/channels/${path.channelId}/messages`, {
|
||||||
|
value: message
|
||||||
|
})
|
||||||
|
setMessage('')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleTextareaChange: React.ChangeEventHandler<HTMLTextAreaElement> = (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
setMessage(event.target.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFileChange: React.ChangeEventHandler<HTMLInputElement> = async (
|
||||||
|
event
|
||||||
|
) => {
|
||||||
|
const files = event?.target?.files
|
||||||
|
if (files != null && files.length === 1) {
|
||||||
|
const file = files[0]
|
||||||
|
const formData = new FormData()
|
||||||
|
formData.append('file', file)
|
||||||
|
await authentication.api.post(
|
||||||
|
`/channels/${path.channelId}/messages/uploads`,
|
||||||
|
formData
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleVisibleEmojiPicker = (): void => {
|
||||||
|
setIsVisibleEmojiPicker((isVisible) => !isVisible)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleEmojiPicker: EmojiPickerOnClick = (emoji) => {
|
||||||
|
const emojiColons = emoji.colons ?? ''
|
||||||
|
setMessage((oldMessage) => {
|
||||||
|
return oldMessage + emojiColons
|
||||||
|
})
|
||||||
|
handleVisibleEmojiPicker()
|
||||||
|
textareaReference.current?.focus()
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<>
|
||||||
|
{isVisibleEmojiPicker && <EmojiPicker onClick={handleEmojiPicker} />}
|
||||||
<div className='p-6 pb-4'>
|
<div className='p-6 pb-4'>
|
||||||
<div className='w-full h-full py-1 flex rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-200'>
|
<div className='w-full h-full py-1 flex rounded-lg bg-gray-200 dark:bg-gray-800 text-gray-600 dark:text-gray-200'>
|
||||||
<form className='w-full h-full flex items-center'>
|
<form
|
||||||
|
className='w-full h-full flex items-center'
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onKeyDown={handleTextareaKeyDown}
|
||||||
|
>
|
||||||
<TextareaAutosize
|
<TextareaAutosize
|
||||||
className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
|
className='w-full scrollbar-firefox-support p-2 px-6 my-2 bg-transparent outline-none font-paragraph tracking-wide resize-none'
|
||||||
placeholder={t('application:write-a-message')}
|
placeholder={t('application:write-a-message')}
|
||||||
wrap='soft'
|
wrap='soft'
|
||||||
maxRows={6}
|
maxRows={6}
|
||||||
|
name='message'
|
||||||
|
onChange={handleTextareaChange}
|
||||||
|
value={message}
|
||||||
|
ref={textareaReference}
|
||||||
|
autoFocus
|
||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
<div className='h-full flex items-center justify-around pr-6'>
|
<div className='h-full flex items-center justify-around pr-6'>
|
||||||
<button className='w-full h-full flex items-center justify-center p-1 text-2xl transition hover:-translate-y-1'>
|
<button
|
||||||
|
className='w-full h-full flex items-center justify-center p-1 text-2xl transition hover:-translate-y-1'
|
||||||
|
onClick={handleVisibleEmojiPicker}
|
||||||
|
>
|
||||||
🙂
|
🙂
|
||||||
</button>
|
</button>
|
||||||
<button className='relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
|
<button className='cursor-pointer relative w-full h-full flex items-center justify-center p-1 text-green-800 dark:text-green-400 transition hover:-translate-y-1'>
|
||||||
<input
|
<input
|
||||||
type='file'
|
type='file'
|
||||||
className='absolute w-full h-full opacity-0 cursor-pointer'
|
className='absolute w-full h-full opacity-0 cursor-pointer'
|
||||||
|
onChange={handleFileChange}
|
||||||
/>
|
/>
|
||||||
<svg width='25' height='25' viewBox='0 0 22 22'>
|
<svg width='25' height='25' viewBox='0 0 22 22'>
|
||||||
<path
|
<path
|
||||||
@ -34,5 +121,6 @@ export const SendMessage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,7 @@ import { NextPage, usePagination } from 'hooks/usePagination'
|
|||||||
import { useAuthentication } from 'tools/authentication'
|
import { useAuthentication } from 'tools/authentication'
|
||||||
import { MessageWithMember } from 'models/Message'
|
import { MessageWithMember } from 'models/Message'
|
||||||
import { GuildsChannelsPath } from 'components/Application'
|
import { GuildsChannelsPath } from 'components/Application'
|
||||||
|
import { handleSocketData, SocketData } from 'tools/handleSocketData'
|
||||||
|
|
||||||
export interface Messages {
|
export interface Messages {
|
||||||
messages: MessageWithMember[]
|
messages: MessageWithMember[]
|
||||||
@ -20,20 +21,44 @@ export interface MessagesProviderProps {
|
|||||||
|
|
||||||
export const MessagesProvider: React.FC<MessagesProviderProps> = (props) => {
|
export const MessagesProvider: React.FC<MessagesProviderProps> = (props) => {
|
||||||
const { path, children } = props
|
const { path, children } = props
|
||||||
|
|
||||||
const { authentication } = useAuthentication()
|
const { authentication } = useAuthentication()
|
||||||
|
|
||||||
const {
|
const {
|
||||||
items: messages,
|
items: messages,
|
||||||
hasMore,
|
hasMore,
|
||||||
nextPage,
|
nextPage,
|
||||||
resetPagination
|
resetPagination,
|
||||||
|
setItems
|
||||||
} = usePagination<MessageWithMember>({
|
} = usePagination<MessageWithMember>({
|
||||||
api: authentication.api,
|
api: authentication.api,
|
||||||
url: `/channels/${path.channelId}/messages`,
|
url: `/channels/${path.channelId}/messages`,
|
||||||
inverse: true
|
inverse: true
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
authentication.socket.on(
|
||||||
|
'messages',
|
||||||
|
(data: SocketData<MessageWithMember>) => {
|
||||||
|
if (data.item.channelId === path.channelId) {
|
||||||
|
const messagesDiv = window.document.getElementById(
|
||||||
|
'messages'
|
||||||
|
) as HTMLDivElement
|
||||||
|
const isAtBottom =
|
||||||
|
messagesDiv.scrollHeight - messagesDiv.scrollTop <=
|
||||||
|
messagesDiv.clientHeight
|
||||||
|
handleSocketData({ data, setItems })
|
||||||
|
if (data.action === 'create' && isAtBottom) {
|
||||||
|
messagesDiv.scrollTo(0, messagesDiv.scrollHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
authentication.socket.off('messages')
|
||||||
|
}
|
||||||
|
}, [authentication.socket, setItems, path])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
resetPagination()
|
resetPagination()
|
||||||
nextPage(undefined, () => {
|
nextPage(undefined, () => {
|
||||||
|
@ -9,7 +9,7 @@ const errorObject: ErrorObject = {
|
|||||||
schemaPath: '/path'
|
schemaPath: '/path'
|
||||||
}
|
}
|
||||||
|
|
||||||
describe('Authentication/getErrorTranslationKey', () => {
|
describe('hooks/useForm/getErrorTranslationKey', () => {
|
||||||
it('returns `errors:invalid` with unknown keyword', async () => {
|
it('returns `errors:invalid` with unknown keyword', async () => {
|
||||||
expect(
|
expect(
|
||||||
getErrorTranslationKey({
|
getErrorTranslationKey({
|
||||||
|
@ -15,11 +15,14 @@ export interface UsePaginationOptions {
|
|||||||
inverse?: boolean
|
inverse?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type SetItems<T> = React.Dispatch<React.SetStateAction<T[]>>
|
||||||
|
|
||||||
export interface UsePaginationResult<T> {
|
export interface UsePaginationResult<T> {
|
||||||
items: T[]
|
items: T[]
|
||||||
nextPage: NextPage
|
nextPage: NextPage
|
||||||
resetPagination: () => void
|
resetPagination: () => void
|
||||||
hasMore: boolean
|
hasMore: boolean
|
||||||
|
setItems: SetItems<T>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const usePagination = <T extends { id: number }>(
|
export const usePagination = <T extends { id: number }>(
|
||||||
@ -81,5 +84,5 @@ export const usePagination = <T extends { id: number }>(
|
|||||||
setItems([])
|
setItems([])
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
return { items, hasMore, nextPage, resetPagination }
|
return { items, hasMore, nextPage, resetPagination, setItems }
|
||||||
}
|
}
|
||||||
|
2940
package-lock.json
generated
2940
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
30
package.json
30
package.json
@ -18,7 +18,7 @@
|
|||||||
"generate": "plop",
|
"generate": "plop",
|
||||||
"lint:commit": "commitlint",
|
"lint:commit": "commitlint",
|
||||||
"lint:editorconfig": "editorconfig-checker",
|
"lint:editorconfig": "editorconfig-checker",
|
||||||
"lint:markdown": "markdownlint '**/*.md' --dot --ignore 'node_modules'",
|
"lint:markdown": "markdownlint '**/*.md' --dot --ignore-path '.gitignore'",
|
||||||
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
"lint:typescript": "eslint '**/*.{js,ts,jsx,tsx}'",
|
||||||
"lint:prettier": "prettier '.' --check",
|
"lint:prettier": "prettier '.' --check",
|
||||||
"lint:staged": "lint-staged",
|
"lint:staged": "lint-staged",
|
||||||
@ -43,9 +43,9 @@
|
|||||||
"ajv-formats": "2.1.1",
|
"ajv-formats": "2.1.1",
|
||||||
"axios": "0.24.0",
|
"axios": "0.24.0",
|
||||||
"classnames": "2.3.1",
|
"classnames": "2.3.1",
|
||||||
"date-and-time": "2.0.1",
|
"date-and-time": "2.1.0",
|
||||||
"emoji-mart": "3.0.1",
|
"emoji-mart": "3.0.1",
|
||||||
"katex": "0.15.1",
|
"katex": "0.15.2",
|
||||||
"next": "12.0.7",
|
"next": "12.0.7",
|
||||||
"next-pwa": "5.4.4",
|
"next-pwa": "5.4.4",
|
||||||
"next-themes": "0.0.15",
|
"next-themes": "0.0.15",
|
||||||
@ -71,16 +71,16 @@
|
|||||||
"universal-cookie": "4.0.4"
|
"universal-cookie": "4.0.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@commitlint/cli": "16.0.1",
|
"@commitlint/cli": "16.0.2",
|
||||||
"@commitlint/config-conventional": "16.0.0",
|
"@commitlint/config-conventional": "16.0.0",
|
||||||
"@lhci/cli": "0.8.2",
|
"@lhci/cli": "0.8.2",
|
||||||
"@saithodev/semantic-release-backmerge": "2.1.0",
|
"@saithodev/semantic-release-backmerge": "2.1.0",
|
||||||
"@storybook/addon-essentials": "6.4.9",
|
"@storybook/addon-essentials": "6.4.12",
|
||||||
"@storybook/addon-links": "6.4.9",
|
"@storybook/addon-links": "6.4.12",
|
||||||
"@storybook/addon-postcss": "2.0.0",
|
"@storybook/addon-postcss": "2.0.0",
|
||||||
"@storybook/builder-webpack5": "6.4.9",
|
"@storybook/builder-webpack5": "6.4.12",
|
||||||
"@storybook/manager-webpack5": "6.4.9",
|
"@storybook/manager-webpack5": "6.4.12",
|
||||||
"@storybook/react": "6.4.9",
|
"@storybook/react": "6.4.12",
|
||||||
"@testing-library/jest-dom": "5.16.1",
|
"@testing-library/jest-dom": "5.16.1",
|
||||||
"@testing-library/react": "12.1.2",
|
"@testing-library/react": "12.1.2",
|
||||||
"@types/date-and-time": "0.13.0",
|
"@types/date-and-time": "0.13.0",
|
||||||
@ -93,8 +93,8 @@
|
|||||||
"@types/react-responsive": "8.0.5",
|
"@types/react-responsive": "8.0.5",
|
||||||
"@types/unist": "2.0.6",
|
"@types/unist": "2.0.6",
|
||||||
"@typescript-eslint/eslint-plugin": "4.33.0",
|
"@typescript-eslint/eslint-plugin": "4.33.0",
|
||||||
"autoprefixer": "10.4.1",
|
"autoprefixer": "10.4.2",
|
||||||
"cypress": "9.2.0",
|
"cypress": "9.2.1",
|
||||||
"editorconfig-checker": "4.0.2",
|
"editorconfig-checker": "4.0.2",
|
||||||
"eslint": "7.32.0",
|
"eslint": "7.32.0",
|
||||||
"eslint-config-next": "12.0.7",
|
"eslint-config-next": "12.0.7",
|
||||||
@ -106,10 +106,10 @@
|
|||||||
"eslint-plugin-promise": "5.1.1",
|
"eslint-plugin-promise": "5.1.1",
|
||||||
"eslint-plugin-storybook": "0.5.5",
|
"eslint-plugin-storybook": "0.5.5",
|
||||||
"eslint-plugin-unicorn": "40.0.0",
|
"eslint-plugin-unicorn": "40.0.0",
|
||||||
"husky": "7.0.4",
|
|
||||||
"html-w3c-validator": "1.0.0",
|
"html-w3c-validator": "1.0.0",
|
||||||
|
"husky": "7.0.4",
|
||||||
"jest": "27.4.7",
|
"jest": "27.4.7",
|
||||||
"lint-staged": "12.1.5",
|
"lint-staged": "12.1.7",
|
||||||
"markdownlint-cli": "0.30.0",
|
"markdownlint-cli": "0.30.0",
|
||||||
"mockttp": "2.5.0",
|
"mockttp": "2.5.0",
|
||||||
"next-secure-headers": "2.2.0",
|
"next-secure-headers": "2.2.0",
|
||||||
@ -120,9 +120,9 @@
|
|||||||
"serve": "13.0.2",
|
"serve": "13.0.2",
|
||||||
"start-server-and-test": "1.14.0",
|
"start-server-and-test": "1.14.0",
|
||||||
"storybook-tailwind-dark-mode": "1.0.11",
|
"storybook-tailwind-dark-mode": "1.0.11",
|
||||||
"tailwindcss": "3.0.11",
|
"tailwindcss": "3.0.13",
|
||||||
"typescript": "4.4.4",
|
"typescript": "4.4.4",
|
||||||
"vercel": "23.1.2",
|
"vercel": "23.1.2",
|
||||||
"webpack": "5.65.0"
|
"webpack": "5.66.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -47,7 +47,7 @@ const ChannelPage: NextPage<ChannelPageProps> = (props) => {
|
|||||||
title={`# ${selectedChannel.name}`}
|
title={`# ${selectedChannel.name}`}
|
||||||
>
|
>
|
||||||
<Messages />
|
<Messages />
|
||||||
<SendMessage />
|
<SendMessage path={path} />
|
||||||
</Application>
|
</Application>
|
||||||
</MessagesProvider>
|
</MessagesProvider>
|
||||||
</ChannelsProvider>
|
</ChannelsProvider>
|
||||||
|
@ -23,7 +23,9 @@ export const AuthenticationProvider: React.FC<PagePropsWithAuthentication> = (
|
|||||||
|
|
||||||
const authentication = useMemo(() => {
|
const authentication = useMemo(() => {
|
||||||
return new Authentication(props.authentication.tokens)
|
return new Authentication(props.authentication.tokens)
|
||||||
}, [props.authentication.tokens])
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setLanguage(user.settings.language).catch(() => {})
|
setLanguage(user.settings.language).catch(() => {})
|
||||||
|
49
tools/handleSocketData.ts
Normal file
49
tools/handleSocketData.ts
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
import { SetItems } from '../hooks/usePagination'
|
||||||
|
|
||||||
|
export interface Item {
|
||||||
|
id: number
|
||||||
|
[key: string]: any
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SocketData<T extends Item = Item> {
|
||||||
|
action: 'create' | 'update' | 'delete'
|
||||||
|
item: T
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface HandleSocketDataOptions<T extends Item = Item> {
|
||||||
|
setItems: SetItems<T>
|
||||||
|
data: SocketData<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export type SocketListener = (data: SocketData) => void
|
||||||
|
|
||||||
|
export const handleSocketData = <T extends Item = Item>(
|
||||||
|
options: HandleSocketDataOptions<T>
|
||||||
|
): void => {
|
||||||
|
const { data, setItems } = options
|
||||||
|
|
||||||
|
setItems((oldItems) => {
|
||||||
|
const newItems = [...oldItems]
|
||||||
|
switch (data.action) {
|
||||||
|
case 'create': {
|
||||||
|
newItems.push(data.item)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'delete': {
|
||||||
|
const itemIndex = newItems.findIndex((item) => item.id === data.item.id)
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
newItems.splice(itemIndex, 1)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'update': {
|
||||||
|
const itemIndex = newItems.findIndex((item) => item.id === data.item.id)
|
||||||
|
if (itemIndex !== -1) {
|
||||||
|
newItems[itemIndex] = data.item
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newItems
|
||||||
|
})
|
||||||
|
}
|
Reference in New Issue
Block a user