perf: optimize load of pagination items with caching
This commit is contained in:
		@@ -22,12 +22,15 @@ export const JoinGuildsPublic: React.FC = () => {
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on('guilds', (data: SocketData<GuildPublicType>) => {
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      handleSocketData({ data, setItems })
 | 
					      'guilds',
 | 
				
			||||||
    })
 | 
					      (data: SocketData<GuildPublicType>) => {
 | 
				
			||||||
 | 
					        handleSocketData({ data, setItems })
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('guilds')
 | 
					      authentication?.socket?.off('guilds')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, setItems])
 | 
					  }, [authentication.socket, setItems])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -72,7 +72,7 @@ export const Authentication: React.FC<AuthenticationProps> = (props) => {
 | 
				
			|||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        const { data } = await api.post<Tokens>('/users/signin', formData)
 | 
					        const { data } = await api.post<Tokens>('/users/signin', formData)
 | 
				
			||||||
        const authentication = new AuthenticationClass(data)
 | 
					        const authentication = new AuthenticationClass(data, true)
 | 
				
			||||||
        authentication.signin()
 | 
					        authentication.signin()
 | 
				
			||||||
        await router.push('/application')
 | 
					        await router.push('/application')
 | 
				
			||||||
        return null
 | 
					        return null
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -24,7 +24,7 @@ export const AuthenticationSocialMedia: React.FC = () => {
 | 
				
			|||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    const data = router.query
 | 
					    const data = router.query
 | 
				
			||||||
    if (isTokens(data)) {
 | 
					    if (isTokens(data)) {
 | 
				
			||||||
      const authentication = new Authentication(data)
 | 
					      const authentication = new Authentication(data, true)
 | 
				
			||||||
      authentication.signin()
 | 
					      authentication.signin()
 | 
				
			||||||
      router.push('/application').catch(() => {})
 | 
					      router.push('/application').catch(() => {})
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { useAuthentication } from '../tools/authentication'
 | 
				
			|||||||
import { Channel, ChannelWithDefaultChannelId } from '../models/Channel'
 | 
					import { Channel, ChannelWithDefaultChannelId } from '../models/Channel'
 | 
				
			||||||
import { GuildsChannelsPath } from '../components/Application'
 | 
					import { GuildsChannelsPath } from '../components/Application'
 | 
				
			||||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
					import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
				
			||||||
 | 
					import { CacheKey, CHANNELS_CACHE_KEY } from '../tools/cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Channels {
 | 
					export interface Channels {
 | 
				
			||||||
  channels: Channel[]
 | 
					  channels: Channel[]
 | 
				
			||||||
@@ -27,6 +28,8 @@ export const ChannelsProvider: React.FC<
 | 
				
			|||||||
  const router = useRouter()
 | 
					  const router = useRouter()
 | 
				
			||||||
  const { authentication } = useAuthentication()
 | 
					  const { authentication } = useAuthentication()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cacheKey: CacheKey = `${path.guildId}-${CHANNELS_CACHE_KEY}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    items: channels,
 | 
					    items: channels,
 | 
				
			||||||
    hasMore,
 | 
					    hasMore,
 | 
				
			||||||
@@ -35,14 +38,15 @@ export const ChannelsProvider: React.FC<
 | 
				
			|||||||
    setItems
 | 
					    setItems
 | 
				
			||||||
  } = usePagination<Channel>({
 | 
					  } = usePagination<Channel>({
 | 
				
			||||||
    api: authentication.api,
 | 
					    api: authentication.api,
 | 
				
			||||||
    url: `/guilds/${path.guildId}/channels`
 | 
					    url: `/guilds/${path.guildId}/channels`,
 | 
				
			||||||
 | 
					    cacheKey
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on(
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      'channels',
 | 
					      'channels',
 | 
				
			||||||
      async (data: SocketData<ChannelWithDefaultChannelId>) => {
 | 
					      async (data: SocketData<ChannelWithDefaultChannelId>) => {
 | 
				
			||||||
        handleSocketData({ data, setItems })
 | 
					        handleSocketData({ data, setItems, cacheKey })
 | 
				
			||||||
        if (data.action === 'delete') {
 | 
					        if (data.action === 'delete') {
 | 
				
			||||||
          await router.push(
 | 
					          await router.push(
 | 
				
			||||||
            `/application/${path.guildId}/${data.item.defaultChannelId}`
 | 
					            `/application/${path.guildId}/${data.item.defaultChannelId}`
 | 
				
			||||||
@@ -52,9 +56,9 @@ export const ChannelsProvider: React.FC<
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('channels')
 | 
					      authentication?.socket?.off('channels')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, path.guildId, router, setItems])
 | 
					  }, [authentication.socket, path.guildId, router, setItems, cacheKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    resetPagination()
 | 
					    resetPagination()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -47,7 +47,7 @@ export const GuildMemberProvider: React.FC<
 | 
				
			|||||||
  }, [path, authentication.api])
 | 
					  }, [path, authentication.api])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on(
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      'guilds',
 | 
					      'guilds',
 | 
				
			||||||
      async (data: SocketData<GuildWithDefaultChannelId>) => {
 | 
					      async (data: SocketData<GuildWithDefaultChannelId>) => {
 | 
				
			||||||
        if (data.item.id === path.guildId) {
 | 
					        if (data.item.id === path.guildId) {
 | 
				
			||||||
@@ -72,7 +72,7 @@ export const GuildMemberProvider: React.FC<
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('guilds')
 | 
					      authentication?.socket?.off('guilds')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, path.guildId, router])
 | 
					  }, [authentication.socket, path.guildId, router])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,6 +4,7 @@ import { NextPage, usePagination } from '../hooks/usePagination'
 | 
				
			|||||||
import { useAuthentication } from '../tools/authentication'
 | 
					import { useAuthentication } from '../tools/authentication'
 | 
				
			||||||
import { GuildWithDefaultChannelId } from '../models/Guild'
 | 
					import { GuildWithDefaultChannelId } from '../models/Guild'
 | 
				
			||||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
					import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
				
			||||||
 | 
					import { GUILDS_CACHE_KEY } from '../tools/cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Guilds {
 | 
					export interface Guilds {
 | 
				
			||||||
  guilds: GuildWithDefaultChannelId[]
 | 
					  guilds: GuildWithDefaultChannelId[]
 | 
				
			||||||
@@ -29,19 +30,20 @@ export const GuildsProvider: React.FC<React.PropsWithChildren<{}>> = (
 | 
				
			|||||||
    setItems
 | 
					    setItems
 | 
				
			||||||
  } = usePagination<GuildWithDefaultChannelId>({
 | 
					  } = usePagination<GuildWithDefaultChannelId>({
 | 
				
			||||||
    api: authentication.api,
 | 
					    api: authentication.api,
 | 
				
			||||||
    url: '/guilds'
 | 
					    url: '/guilds',
 | 
				
			||||||
 | 
					    cacheKey: GUILDS_CACHE_KEY
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on(
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      'guilds',
 | 
					      'guilds',
 | 
				
			||||||
      (data: SocketData<GuildWithDefaultChannelId>) => {
 | 
					      (data: SocketData<GuildWithDefaultChannelId>) => {
 | 
				
			||||||
        handleSocketData({ data, setItems })
 | 
					        handleSocketData({ data, setItems, cacheKey: GUILDS_CACHE_KEY })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('guilds')
 | 
					      authentication?.socket?.off('guilds')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, setItems])
 | 
					  }, [authentication.socket, setItems])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,6 +6,7 @@ import { MemberWithPublicUser } from '../models/Member'
 | 
				
			|||||||
import { GuildsChannelsPath } from '../components/Application'
 | 
					import { GuildsChannelsPath } from '../components/Application'
 | 
				
			||||||
import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
					import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
				
			||||||
import { User } from '../models/User'
 | 
					import { User } from '../models/User'
 | 
				
			||||||
 | 
					import { CacheKey, MEMBERS_CACHE_KEY } from '../tools/cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Members {
 | 
					export interface Members {
 | 
				
			||||||
  members: MemberWithPublicUser[]
 | 
					  members: MemberWithPublicUser[]
 | 
				
			||||||
@@ -27,6 +28,8 @@ export const MembersProviders: React.FC<
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  const { authentication } = useAuthentication()
 | 
					  const { authentication } = useAuthentication()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cacheKey: CacheKey = `${path.guildId}-${MEMBERS_CACHE_KEY}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    items: members,
 | 
					    items: members,
 | 
				
			||||||
    hasMore,
 | 
					    hasMore,
 | 
				
			||||||
@@ -35,18 +38,19 @@ export const MembersProviders: React.FC<
 | 
				
			|||||||
    setItems
 | 
					    setItems
 | 
				
			||||||
  } = usePagination<MemberWithPublicUser>({
 | 
					  } = usePagination<MemberWithPublicUser>({
 | 
				
			||||||
    api: authentication.api,
 | 
					    api: authentication.api,
 | 
				
			||||||
    url: `/guilds/${path.guildId}/members`
 | 
					    url: `/guilds/${path.guildId}/members`,
 | 
				
			||||||
 | 
					    cacheKey
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on(
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      'members',
 | 
					      'members',
 | 
				
			||||||
      (data: SocketData<MemberWithPublicUser>) => {
 | 
					      (data: SocketData<MemberWithPublicUser>) => {
 | 
				
			||||||
        handleSocketData({ data, setItems })
 | 
					        handleSocketData({ data, setItems, cacheKey })
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    authentication.socket.on('users', (data: SocketData<User>) => {
 | 
					    authentication?.socket?.on('users', (data: SocketData<User>) => {
 | 
				
			||||||
      setItems((oldItems) => {
 | 
					      setItems((oldItems) => {
 | 
				
			||||||
        const newItems = [...oldItems]
 | 
					        const newItems = [...oldItems]
 | 
				
			||||||
        switch (data.action) {
 | 
					        switch (data.action) {
 | 
				
			||||||
@@ -65,10 +69,10 @@ export const MembersProviders: React.FC<
 | 
				
			|||||||
    })
 | 
					    })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('members')
 | 
					      authentication?.socket?.off('members')
 | 
				
			||||||
      authentication.socket.off('users')
 | 
					      authentication?.socket?.off('users')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, setItems])
 | 
					  }, [authentication.socket, setItems, cacheKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    resetPagination()
 | 
					    resetPagination()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,7 @@ 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'
 | 
					import { handleSocketData, SocketData } from '../tools/handleSocketData'
 | 
				
			||||||
 | 
					import { CacheKey, MESSAGES_CACHE_KEY } from '../tools/cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Messages {
 | 
					export interface Messages {
 | 
				
			||||||
  messages: MessageWithMember[]
 | 
					  messages: MessageWithMember[]
 | 
				
			||||||
@@ -25,6 +26,8 @@ export const MessagesProvider: React.FC<
 | 
				
			|||||||
  const { path, children } = props
 | 
					  const { path, children } = props
 | 
				
			||||||
  const { authentication, user } = useAuthentication()
 | 
					  const { authentication, user } = useAuthentication()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const cacheKey: CacheKey = `${path.channelId}-${MESSAGES_CACHE_KEY}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const {
 | 
					  const {
 | 
				
			||||||
    items: messages,
 | 
					    items: messages,
 | 
				
			||||||
    hasMore,
 | 
					    hasMore,
 | 
				
			||||||
@@ -34,11 +37,12 @@ export const MessagesProvider: React.FC<
 | 
				
			|||||||
  } = usePagination<MessageWithMember>({
 | 
					  } = usePagination<MessageWithMember>({
 | 
				
			||||||
    api: authentication.api,
 | 
					    api: authentication.api,
 | 
				
			||||||
    url: `/channels/${path.channelId}/messages`,
 | 
					    url: `/channels/${path.channelId}/messages`,
 | 
				
			||||||
    inverse: true
 | 
					    inverse: true,
 | 
				
			||||||
 | 
					    cacheKey
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    authentication.socket.on(
 | 
					    authentication?.socket?.on(
 | 
				
			||||||
      'messages',
 | 
					      'messages',
 | 
				
			||||||
      (data: SocketData<MessageWithMember>) => {
 | 
					      (data: SocketData<MessageWithMember>) => {
 | 
				
			||||||
        if (data.item.channelId === path.channelId) {
 | 
					        if (data.item.channelId === path.channelId) {
 | 
				
			||||||
@@ -48,7 +52,7 @@ export const MessagesProvider: React.FC<
 | 
				
			|||||||
          const isAtBottom =
 | 
					          const isAtBottom =
 | 
				
			||||||
            messagesDiv.scrollHeight - messagesDiv.scrollTop <=
 | 
					            messagesDiv.scrollHeight - messagesDiv.scrollTop <=
 | 
				
			||||||
            messagesDiv.clientHeight
 | 
					            messagesDiv.clientHeight
 | 
				
			||||||
          handleSocketData({ data, setItems })
 | 
					          handleSocketData({ data, setItems, cacheKey })
 | 
				
			||||||
          if (
 | 
					          if (
 | 
				
			||||||
            data.action === 'create' &&
 | 
					            data.action === 'create' &&
 | 
				
			||||||
            (isAtBottom || data.item.member.userId === user.id)
 | 
					            (isAtBottom || data.item.member.userId === user.id)
 | 
				
			||||||
@@ -60,9 +64,9 @@ export const MessagesProvider: React.FC<
 | 
				
			|||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return () => {
 | 
					    return () => {
 | 
				
			||||||
      authentication.socket.off('messages')
 | 
					      authentication?.socket?.off('messages')
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }, [authentication.socket, setItems, path, user.id])
 | 
					  }, [authentication.socket, setItems, path, user.id, cacheKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    resetPagination()
 | 
					    resetPagination()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,11 @@ import { useState, useRef, useCallback } from 'react'
 | 
				
			|||||||
import { AxiosInstance } from 'axios'
 | 
					import { AxiosInstance } from 'axios'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { FetchState } from './useFetchState'
 | 
					import { FetchState } from './useFetchState'
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  CacheKey,
 | 
				
			||||||
 | 
					  getPaginationCache,
 | 
				
			||||||
 | 
					  savePaginationCache
 | 
				
			||||||
 | 
					} from '../tools/cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Query {
 | 
					export interface Query {
 | 
				
			||||||
  [key: string]: string
 | 
					  [key: string]: string
 | 
				
			||||||
@@ -13,6 +18,11 @@ export interface UsePaginationOptions {
 | 
				
			|||||||
  api: AxiosInstance
 | 
					  api: AxiosInstance
 | 
				
			||||||
  url: string
 | 
					  url: string
 | 
				
			||||||
  inverse?: boolean
 | 
					  inverse?: boolean
 | 
				
			||||||
 | 
					  cacheKey?: CacheKey
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PaginationItem {
 | 
				
			||||||
 | 
					  id: number
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SetItems<T> = React.Dispatch<React.SetStateAction<T[]>>
 | 
					export type SetItems<T> = React.Dispatch<React.SetStateAction<T[]>>
 | 
				
			||||||
@@ -25,16 +35,25 @@ export interface UsePaginationResult<T> {
 | 
				
			|||||||
  setItems: SetItems<T>
 | 
					  setItems: SetItems<T>
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const usePagination = <T extends { id: number }>(
 | 
					export const usePagination = <T extends PaginationItem>(
 | 
				
			||||||
  options: UsePaginationOptions
 | 
					  options: UsePaginationOptions
 | 
				
			||||||
): UsePaginationResult<T> => {
 | 
					): UsePaginationResult<T> => {
 | 
				
			||||||
  const { api, url, inverse = false } = options
 | 
					  const { api, url, inverse = false, cacheKey } = options
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const [items, setItems] = useState<T[]>([])
 | 
					  const [items, setItems] = useState<T[]>([])
 | 
				
			||||||
  const [hasMore, setHasMore] = useState(true)
 | 
					  const [hasMore, setHasMore] = useState(true)
 | 
				
			||||||
  const fetchState = useRef<FetchState>('idle')
 | 
					  const fetchState = useRef<FetchState>('idle')
 | 
				
			||||||
  const afterId = useRef<number | null>(null)
 | 
					  const afterId = useRef<number | null>(null)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const updateAfterId = (newItems: T[]): void => {
 | 
				
			||||||
 | 
					    if (!inverse) {
 | 
				
			||||||
 | 
					      afterId.current =
 | 
				
			||||||
 | 
					        newItems.length > 0 ? newItems[newItems.length - 1].id : null
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      afterId.current = newItems.length > 0 ? newItems[0].id : null
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const nextPageAsync: NextPageAsync = useCallback(
 | 
					  const nextPageAsync: NextPageAsync = useCallback(
 | 
				
			||||||
    async (query) => {
 | 
					    async (query) => {
 | 
				
			||||||
      if (fetchState.current !== 'idle') {
 | 
					      if (fetchState.current !== 'idle') {
 | 
				
			||||||
@@ -49,19 +68,22 @@ export const usePagination = <T extends { id: number }>(
 | 
				
			|||||||
      const { data: newItems } = await api.get<T[]>(
 | 
					      const { data: newItems } = await api.get<T[]>(
 | 
				
			||||||
        `${url}?${searchParameters.toString()}`
 | 
					        `${url}?${searchParameters.toString()}`
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      if (!inverse) {
 | 
					      updateAfterId(newItems)
 | 
				
			||||||
        afterId.current =
 | 
					 | 
				
			||||||
          newItems.length > 0 ? newItems[newItems.length - 1].id : null
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        afterId.current = newItems.length > 0 ? newItems[0].id : null
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      setItems((oldItems) => {
 | 
					      setItems((oldItems) => {
 | 
				
			||||||
        return inverse ? [...newItems, ...oldItems] : [...oldItems, ...newItems]
 | 
					        const updatedItems = inverse
 | 
				
			||||||
 | 
					          ? [...newItems, ...oldItems]
 | 
				
			||||||
 | 
					          : [...oldItems, ...newItems]
 | 
				
			||||||
 | 
					        if (cacheKey != null) {
 | 
				
			||||||
 | 
					          savePaginationCache(cacheKey, updatedItems)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return updatedItems
 | 
				
			||||||
      })
 | 
					      })
 | 
				
			||||||
      setHasMore(newItems.length > 0)
 | 
					      setHasMore(newItems.length > 0)
 | 
				
			||||||
      fetchState.current = 'idle'
 | 
					      fetchState.current = 'idle'
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    [api, url, inverse]
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
 | 
				
			||||||
 | 
					    [api, url, inverse, cacheKey]
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const nextPage: NextPage = useCallback(
 | 
					  const nextPage: NextPage = useCallback(
 | 
				
			||||||
@@ -80,9 +102,17 @@ export const usePagination = <T extends { id: number }>(
 | 
				
			|||||||
  )
 | 
					  )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const resetPagination = useCallback((): void => {
 | 
					  const resetPagination = useCallback((): void => {
 | 
				
			||||||
    afterId.current = null
 | 
					    if (cacheKey == null) {
 | 
				
			||||||
    setItems([])
 | 
					      afterId.current = null
 | 
				
			||||||
  }, [])
 | 
					      setItems([])
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      const newItems = getPaginationCache<T>(cacheKey)
 | 
				
			||||||
 | 
					      setItems(newItems)
 | 
				
			||||||
 | 
					      updateAfterId(newItems)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps -- We don't want infinite loops with updateAfterId
 | 
				
			||||||
 | 
					  }, [cacheKey])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return { items, hasMore, nextPage, resetPagination, setItems }
 | 
					  return { items, hasMore, nextPage, resetPagination, setItems }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -7,7 +7,7 @@ const nextTranslate = require('next-translate')
 | 
				
			|||||||
/** @type {import("next").NextConfig} */
 | 
					/** @type {import("next").NextConfig} */
 | 
				
			||||||
module.exports = nextTranslate(
 | 
					module.exports = nextTranslate(
 | 
				
			||||||
  nextPWA({
 | 
					  nextPWA({
 | 
				
			||||||
    reactStrictMode: true,
 | 
					    reactStrictMode: false,
 | 
				
			||||||
    images: {
 | 
					    images: {
 | 
				
			||||||
      domains: [
 | 
					      domains: [
 | 
				
			||||||
        'api.thream.divlo.fr',
 | 
					        'api.thream.divlo.fr',
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -28,6 +28,9 @@ const Application = ({ Component, pageProps }: AppProps): JSX.Element => {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    window.addEventListener('resize', appHeight)
 | 
					    window.addEventListener('resize', appHeight)
 | 
				
			||||||
    appHeight()
 | 
					    appHeight()
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      window.removeEventListener('resize', appHeight)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
  }, [lang])
 | 
					  }, [lang])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,27 +9,36 @@ import { fetchRefreshToken } from './authenticationFromServerSide'
 | 
				
			|||||||
export class Authentication {
 | 
					export class Authentication {
 | 
				
			||||||
  public tokens: Tokens
 | 
					  public tokens: Tokens
 | 
				
			||||||
  public accessTokenAge: number
 | 
					  public accessTokenAge: number
 | 
				
			||||||
  public socket: Socket
 | 
					  public socket?: Socket
 | 
				
			||||||
  public api: AxiosInstance
 | 
					  public api: AxiosInstance
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  constructor(tokens: Tokens) {
 | 
					  constructor(tokens: Tokens, disableSocketIO: boolean = false) {
 | 
				
			||||||
    this.tokens = tokens
 | 
					    this.tokens = tokens
 | 
				
			||||||
    this.accessTokenAge = Date.now()
 | 
					    this.accessTokenAge = Date.now()
 | 
				
			||||||
    this.socket = io(API_URL, {
 | 
					    if (disableSocketIO || typeof window === 'undefined') {
 | 
				
			||||||
      auth: { token: `Bearer ${tokens.accessToken}` }
 | 
					      this.socket = undefined
 | 
				
			||||||
    })
 | 
					    } else {
 | 
				
			||||||
    this.socket.on('connect_error', (error) => {
 | 
					      this.socket = io(API_URL, {
 | 
				
			||||||
      if (error.message.startsWith('Unauthorized')) {
 | 
					        auth: { token: `Bearer ${tokens.accessToken}` }
 | 
				
			||||||
        fetchRefreshToken(this.tokens.refreshToken)
 | 
					      })
 | 
				
			||||||
          .then(({ accessToken }) => {
 | 
					      this.socket.on('connect', () => {
 | 
				
			||||||
            this.setAccessToken(accessToken)
 | 
					        console.log(
 | 
				
			||||||
          })
 | 
					          `Connected to socket with clientId: ${this.socket?.id ?? 'undefined'}`
 | 
				
			||||||
          .catch(async () => {
 | 
					        )
 | 
				
			||||||
            this.signout()
 | 
					      })
 | 
				
			||||||
            return await Promise.reject(error)
 | 
					      this.socket.on('connect_error', (error) => {
 | 
				
			||||||
          })
 | 
					        if (error.message.startsWith('Unauthorized')) {
 | 
				
			||||||
      }
 | 
					          fetchRefreshToken(this.tokens.refreshToken)
 | 
				
			||||||
    })
 | 
					            .then(({ accessToken }) => {
 | 
				
			||||||
 | 
					              this.setAccessToken(accessToken)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .catch(async () => {
 | 
				
			||||||
 | 
					              this.signout()
 | 
				
			||||||
 | 
					              return await Promise.reject(error)
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    this.api = axios.create({
 | 
					    this.api = axios.create({
 | 
				
			||||||
      baseURL: API_URL,
 | 
					      baseURL: API_URL,
 | 
				
			||||||
      headers: {
 | 
					      headers: {
 | 
				
			||||||
@@ -83,13 +92,14 @@ export class Authentication {
 | 
				
			|||||||
    this.tokens.accessToken = accessToken
 | 
					    this.tokens.accessToken = accessToken
 | 
				
			||||||
    this.accessTokenAge = Date.now()
 | 
					    this.accessTokenAge = Date.now()
 | 
				
			||||||
    const token = `${this.tokens.type} ${this.tokens.accessToken}`
 | 
					    const token = `${this.tokens.type} ${this.tokens.accessToken}`
 | 
				
			||||||
    if (typeof this.socket.auth !== 'function') {
 | 
					    if (typeof this?.socket?.auth !== 'function' && this.socket != null) {
 | 
				
			||||||
      this.socket.auth.token = token
 | 
					      this.socket.auth.token = token
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public signout(): void {
 | 
					  public signout(): void {
 | 
				
			||||||
    cookies.remove('refreshToken')
 | 
					    cookies.remove('refreshToken')
 | 
				
			||||||
 | 
					    window.localStorage.clear()
 | 
				
			||||||
    window.location.href = '/authentication/signin'
 | 
					    window.location.href = '/authentication/signin'
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,8 @@ export const AuthenticationProvider: React.FC<
 | 
				
			|||||||
  const [user, setUser] = useState<UserCurrent>(props.authentication.user)
 | 
					  const [user, setUser] = useState<UserCurrent>(props.authentication.user)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const authentication = useMemo(() => {
 | 
					  const authentication = useMemo(() => {
 | 
				
			||||||
    return new Authentication(props.authentication.tokens)
 | 
					    const disableSocketIO = typeof window === 'undefined'
 | 
				
			||||||
 | 
					    return new Authentication(props.authentication.tokens, disableSocketIO)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this memo once
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
@@ -33,7 +34,9 @@ export const AuthenticationProvider: React.FC<
 | 
				
			|||||||
  useEffect(() => {
 | 
					  useEffect(() => {
 | 
				
			||||||
    setLanguage(props.authentication.user.settings.language).catch(() => {})
 | 
					    setLanguage(props.authentication.user.settings.language).catch(() => {})
 | 
				
			||||||
    setTheme(props.authentication.user.settings.theme)
 | 
					    setTheme(props.authentication.user.settings.theme)
 | 
				
			||||||
 | 
					    return () => {
 | 
				
			||||||
 | 
					      authentication?.socket?.disconnect()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect once
 | 
					    // eslint-disable-next-line react-hooks/exhaustive-deps -- We only want to run this effect once
 | 
				
			||||||
  }, [])
 | 
					  }, [])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,7 +71,7 @@ export const authenticationFromServerSide = (
 | 
				
			|||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          let data: any = {}
 | 
					          let data: any = {}
 | 
				
			||||||
          const authentication = new Authentication(tokens)
 | 
					          const authentication = new Authentication(tokens, true)
 | 
				
			||||||
          const { data: currentUser } = await authentication.api.get<
 | 
					          const { data: currentUser } = await authentication.api.get<
 | 
				
			||||||
            unknown,
 | 
					            unknown,
 | 
				
			||||||
            AxiosResponse<UserCurrent>
 | 
					            AxiosResponse<UserCurrent>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										34
									
								
								tools/cache.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								tools/cache.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
				
			|||||||
 | 
					import { PaginationItem } from '../hooks/usePagination'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const GUILDS_CACHE_KEY = 'guilds' as const
 | 
				
			||||||
 | 
					export const CHANNELS_CACHE_KEY = 'channels' as const
 | 
				
			||||||
 | 
					export const MEMBERS_CACHE_KEY = 'members' as const
 | 
				
			||||||
 | 
					export const MESSAGES_CACHE_KEY = 'messages' as const
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type CacheKey =
 | 
				
			||||||
 | 
					  | typeof GUILDS_CACHE_KEY
 | 
				
			||||||
 | 
					  | `${number}-${typeof CHANNELS_CACHE_KEY}`
 | 
				
			||||||
 | 
					  | `${number}-${typeof MEMBERS_CACHE_KEY}`
 | 
				
			||||||
 | 
					  | `${number}-${typeof MESSAGES_CACHE_KEY}`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getPaginationCache = <T extends PaginationItem>(
 | 
				
			||||||
 | 
					  key: CacheKey
 | 
				
			||||||
 | 
					): T[] => {
 | 
				
			||||||
 | 
					  const cache = localStorage.getItem(key)
 | 
				
			||||||
 | 
					  if (cache != null) {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const data = JSON.parse(cache)
 | 
				
			||||||
 | 
					      if (Array.isArray(data)) {
 | 
				
			||||||
 | 
					        return data
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } catch {}
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return []
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const savePaginationCache = <T extends PaginationItem>(
 | 
				
			||||||
 | 
					  key: CacheKey,
 | 
				
			||||||
 | 
					  data: T[]
 | 
				
			||||||
 | 
					): void => {
 | 
				
			||||||
 | 
					  localStorage.setItem(key, JSON.stringify(data))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import { SetItems } from '../hooks/usePagination'
 | 
					import { SetItems } from '../hooks/usePagination'
 | 
				
			||||||
 | 
					import { CacheKey, savePaginationCache } from './cache'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface Item {
 | 
					export interface Item {
 | 
				
			||||||
  id: number
 | 
					  id: number
 | 
				
			||||||
@@ -13,6 +14,7 @@ export interface SocketData<T extends Item = Item> {
 | 
				
			|||||||
export interface HandleSocketDataOptions<T extends Item = Item> {
 | 
					export interface HandleSocketDataOptions<T extends Item = Item> {
 | 
				
			||||||
  setItems: SetItems<T>
 | 
					  setItems: SetItems<T>
 | 
				
			||||||
  data: SocketData<T>
 | 
					  data: SocketData<T>
 | 
				
			||||||
 | 
					  cacheKey?: CacheKey
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type SocketListener = (data: SocketData) => void
 | 
					export type SocketListener = (data: SocketData) => void
 | 
				
			||||||
@@ -20,7 +22,8 @@ export type SocketListener = (data: SocketData) => void
 | 
				
			|||||||
export const handleSocketData = <T extends Item = Item>(
 | 
					export const handleSocketData = <T extends Item = Item>(
 | 
				
			||||||
  options: HandleSocketDataOptions<T>
 | 
					  options: HandleSocketDataOptions<T>
 | 
				
			||||||
): void => {
 | 
					): void => {
 | 
				
			||||||
  const { data, setItems } = options
 | 
					  const { data, setItems, cacheKey } = options
 | 
				
			||||||
 | 
					  console.log('socket.io data received: ', data)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  setItems((oldItems) => {
 | 
					  setItems((oldItems) => {
 | 
				
			||||||
    const newItems = [...oldItems]
 | 
					    const newItems = [...oldItems]
 | 
				
			||||||
@@ -47,6 +50,9 @@ export const handleSocketData = <T extends Item = Item>(
 | 
				
			|||||||
        break
 | 
					        break
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    if (cacheKey != null) {
 | 
				
			||||||
 | 
					      savePaginationCache(cacheKey, newItems)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    return newItems
 | 
					    return newItems
 | 
				
			||||||
  })
 | 
					  })
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user