diff --git a/packages/pl-fe/src/actions/shoutbox.ts b/packages/pl-fe/src/actions/shoutbox.ts deleted file mode 100644 index f4b5bac57..000000000 --- a/packages/pl-fe/src/actions/shoutbox.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { getClient } from 'pl-fe/api'; - -import { importEntities } from './importer'; -import { getMeUrl } from './me'; - -import type { PlApiClient, ShoutMessage } from 'pl-api'; -import type { AppDispatch, RootState } from 'pl-fe/store'; - -const SHOUTBOX_MESSAGE_IMPORT = 'SHOUTBOX_MESSAGE_IMPORT' as const; -const SHOUTBOX_MESSAGES_IMPORT = 'SHOUTBOX_MESSAGES_IMPORT' as const; -const SHOUTBOX_CONNECT = 'SHOUTBOX_CONNECT' as const; - -const importShoutboxMessages = (messages: ShoutMessage[]) => (dispatch: AppDispatch): ShoutboxAction => { - dispatch(importEntities({ accounts: messages.map((message) => message.author) }, { override: false })); - - return dispatch({ - type: SHOUTBOX_MESSAGES_IMPORT, - messages, - }); -}; - -const importShoutboxMessage = (message: ShoutMessage) => (dispatch: AppDispatch): ShoutboxAction => { - dispatch(importEntities({ accounts: [message.author] }, { override: false })); - - return dispatch({ - type: SHOUTBOX_MESSAGE_IMPORT, - message, - }); -}; - -const createShoutboxMessage = (message: string) => (dispatch: AppDispatch, getState: () => RootState) => { - const socket = getState().shoutbox.socket; - - if (!socket) return; - - socket.message(message); -}; - -const connectShoutbox = () => (dispatch: AppDispatch, getState: () => RootState) => { - const state = getState(); - const accountUrl = getMeUrl(state); - - if (!accountUrl) return; - - const client = getClient(state); - - return client.settings.verifyCredentials().then((account) => { - if (account.__meta.pleroma?.chat_token) { - const socket = client.shoutbox.connect(account.__meta.pleroma?.chat_token, { - onMessage: (message) => dispatch(importShoutboxMessage(message)), - onMessages: (messages) => dispatch(importShoutboxMessages(messages)), - }); - return dispatch({ - type: SHOUTBOX_CONNECT, - socket, - }); - } - }); -}; - -type ShoutboxAction = - | { - type: typeof SHOUTBOX_CONNECT; - socket: ReturnType<(InstanceType)['shoutbox']['connect']>; - } - | { - type: typeof SHOUTBOX_MESSAGE_IMPORT; - message: ShoutMessage; - } - | { - type: typeof SHOUTBOX_MESSAGES_IMPORT; - messages: ShoutMessage[]; - } - -export { - SHOUTBOX_MESSAGES_IMPORT, - SHOUTBOX_MESSAGE_IMPORT, - SHOUTBOX_CONNECT, - importShoutboxMessages, - importShoutboxMessage, - connectShoutbox, - createShoutboxMessage, - type ShoutboxAction, -}; diff --git a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx index 7fd86be3e..ce11a80ce 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list-shoutbox.tsx @@ -7,9 +7,9 @@ import Avatar from 'pl-fe/components/ui/avatar'; import HStack from 'pl-fe/components/ui/hstack'; import Stack from 'pl-fe/components/ui/stack'; import Text from 'pl-fe/components/ui/text'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useInstance } from 'pl-fe/hooks/use-instance'; import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config'; +import { useShoutboxStore } from 'pl-fe/stores/shoutbox'; import type { Chat } from 'pl-api'; @@ -20,7 +20,7 @@ interface IChatListShoutboxInterface { const ChatListShoutbox: React.FC = ({ onClick }) => { const instance = useInstance(); const { logo } = usePlFeConfig(); - const messages = useAppSelector((state) => state.shoutbox.messages); + const messages = useShoutboxStore().messages; const handleKeyDown: React.KeyboardEventHandler = (event) => { if (event.key === 'Enter' || event.key === ' ') { diff --git a/packages/pl-fe/src/features/chats/components/chat-list.tsx b/packages/pl-fe/src/features/chats/components/chat-list.tsx index b01e1cede..e6db023af 100644 --- a/packages/pl-fe/src/features/chats/components/chat-list.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-list.tsx @@ -6,8 +6,8 @@ import PullToRefresh from 'pl-fe/components/pull-to-refresh'; import Spinner from 'pl-fe/components/ui/spinner'; import Stack from 'pl-fe/components/ui/stack'; import PlaceholderChat from 'pl-fe/features/placeholder/components/placeholder-chat'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useChats } from 'pl-fe/queries/chats'; +import { useShoutboxStore } from 'pl-fe/stores/shoutbox'; import ChatListItem from './chat-list-item'; import ChatListShoutbox from './chat-list-shoutbox'; @@ -20,7 +20,8 @@ interface IChatList { } const ChatList: React.FC = ({ onClickChat, useWindowScroll = false }) => { - const showShoutbox = useAppSelector((state) => !state.shoutbox.isLoading); + const showShoutbox = !useShoutboxStore().isLoading; + const { chatsQuery: { data: chats, isFetching, hasNextPage, fetchNextPage, refetch } } = useChats(); const allChats: Array | undefined = showShoutbox ? ['shoutbox', ...(chats || [])] : chats; diff --git a/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx b/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx index 48583d1ba..f16557bd9 100644 --- a/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx +++ b/packages/pl-fe/src/features/chats/components/chat-pane/chat-pane.tsx @@ -4,8 +4,8 @@ import { FormattedMessage } from 'react-intl'; import Stack from 'pl-fe/components/ui/stack'; import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context'; import { useStatContext } from 'pl-fe/contexts/stat-context'; -import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; import { useChats } from 'pl-fe/queries/chats'; +import { useShoutboxStore } from 'pl-fe/stores/shoutbox'; import ChatList from '../chat-list'; import ChatSearch from '../chat-search/chat-search'; @@ -21,7 +21,7 @@ import type { Chat } from 'pl-api'; const ChatPane = () => { const { unreadChatsCount } = useStatContext(); - const showShoutbox = useAppSelector((state) => !state.shoutbox.isLoading); + const showShoutbox = !useShoutboxStore().isLoading; const { screen, changeScreen, isOpen, toggleChatPane } = useChatContext(); const { chatsQuery: { data: chats, isLoading } } = useChats(); diff --git a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx index 21df8d49e..7e1e74f7d 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox-message-list.tsx @@ -13,10 +13,11 @@ import Text from 'pl-fe/components/ui/text'; import Emojify from 'pl-fe/features/emoji/emojify'; import PlaceholderChatMessage from 'pl-fe/features/placeholder/components/placeholder-chat-message'; import { useAppSelector } from 'pl-fe/hooks/use-app-selector'; +import { useShoutboxStore, type ShoutMessage } from 'pl-fe/stores/shoutbox'; import { ChatMessageListList, ChatMessageListScroller } from './chat-message-list'; -import type { ShoutMessage } from 'pl-fe/reducers/shoutbox'; +import type { } from 'pl-fe/reducers/shoutbox'; const START_INDEX = 10000; @@ -99,7 +100,7 @@ const ShoutboxMessageList: React.FC = () => { const [firstItemIndex, setFirstItemIndex] = useState(START_INDEX - 20); const me = useAppSelector(state => state.me); - const { isLoading, messages: shoutboxMessages } = useAppSelector(state => state.shoutbox); + const { messages: shoutboxMessages = [], isLoading } = useShoutboxStore(); const lastShoutboxMessage = shoutboxMessages?.at(-1) || null; diff --git a/packages/pl-fe/src/features/chats/components/shoutbox.tsx b/packages/pl-fe/src/features/chats/components/shoutbox.tsx index d4650603d..3d9962409 100644 --- a/packages/pl-fe/src/features/chats/components/shoutbox.tsx +++ b/packages/pl-fe/src/features/chats/components/shoutbox.tsx @@ -1,9 +1,8 @@ import clsx from 'clsx'; import React, { MutableRefObject, useEffect, useState } from 'react'; -import { createShoutboxMessage } from 'pl-fe/actions/shoutbox'; import Stack from 'pl-fe/components/ui/stack'; -import { useAppDispatch } from 'pl-fe/hooks/use-app-dispatch'; +import { useCreateShoutboxMessage } from 'pl-fe/stores/shoutbox'; import { clearNativeInputValue } from './chat'; import ShoutboxComposer from './shoutbox-composer'; @@ -17,16 +16,16 @@ interface ChatInterface { } const Shoutbox: React.FC = ({ inputRef, className }) => { - const dispatch = useAppDispatch(); - const [content, setContent] = useState(''); const [resetContentKey, setResetContentKey] = useState(fileKeyGen()); const [errorMessage] = useState(); + const { mutate: createShoutboxMessage } = useCreateShoutboxMessage(); + const isSubmitDisabled = content.length === 0; const submitMessage = () => { - dispatch(createShoutboxMessage(content)); + createShoutboxMessage?.(content); clearState(); }; @@ -81,6 +80,7 @@ const Shoutbox: React.FC = ({ inputRef, className }) => { onSubmit={sendMessage} errorMessage={errorMessage} resetContentKey={resetContentKey} + disabled={!createShoutboxMessage} /> ); diff --git a/packages/pl-fe/src/features/ui/index.tsx b/packages/pl-fe/src/features/ui/index.tsx index 71dab6e6f..f8bf48cfc 100644 --- a/packages/pl-fe/src/features/ui/index.tsx +++ b/packages/pl-fe/src/features/ui/index.tsx @@ -7,7 +7,6 @@ import { fetchFilters } from 'pl-fe/actions/filters'; import { fetchMarker } from 'pl-fe/actions/markers'; import { expandNotifications } from 'pl-fe/actions/notifications'; import { register as registerPushNotifications } from 'pl-fe/actions/push-notifications/registerer'; -import { connectShoutbox } from 'pl-fe/actions/shoutbox'; import { fetchHomeTimeline } from 'pl-fe/actions/timelines'; import { useUserStream } from 'pl-fe/api/hooks/streaming/use-user-stream'; import { WITH_LANDING_PAGE } from 'pl-fe/build-config'; @@ -44,6 +43,7 @@ import { prefetchFollowRequests } from 'pl-fe/queries/accounts/use-follow-reques import { queryClient } from 'pl-fe/queries/client'; import { prefetchCustomEmojis } from 'pl-fe/queries/instance/use-custom-emojis'; import { scheduledStatusesQueryOptions } from 'pl-fe/queries/statuses/scheduled-statuses'; +import { useShoutboxSubscription } from 'pl-fe/stores/shoutbox'; import { useUiStore } from 'pl-fe/stores/ui'; import { getVapidKey } from 'pl-fe/utils/auth'; import { isStandalone } from 'pl-fe/utils/state'; @@ -394,6 +394,8 @@ const UI: React.FC = React.memo(({ children }) => { const { isDropdownMenuOpen } = useUiStore(); const standalone = useAppSelector(isStandalone); + useShoutboxSubscription(); + const { isDragging } = useDraggedFiles(node); const handleServiceWorkerPostMessage = ({ data }: MessageEvent) => { @@ -434,10 +436,6 @@ const UI: React.FC = React.memo(({ children }) => { setTimeout(() => prefetchFollowRequests(client), 700); } - if (features.shoutbox) { - dispatch(connectShoutbox()); - } - if (features.scheduledStatuses) { setTimeout(() => { queryClient.prefetchInfiniteQuery(scheduledStatusesQueryOptions); diff --git a/packages/pl-fe/src/reducers/index.ts b/packages/pl-fe/src/reducers/index.ts index f264289cd..b9d55c69d 100644 --- a/packages/pl-fe/src/reducers/index.ts +++ b/packages/pl-fe/src/reducers/index.ts @@ -19,7 +19,6 @@ import pending_statuses from './pending-statuses'; import plfe from './pl-fe'; import polls from './polls'; import push_notifications from './push-notifications'; -import shoutbox from './shoutbox'; import statuses from './statuses'; import timelines from './timelines'; @@ -40,7 +39,6 @@ const reducers = { plfe, polls, push_notifications, - shoutbox, statuses, timelines, }; diff --git a/packages/pl-fe/src/reducers/shoutbox.ts b/packages/pl-fe/src/reducers/shoutbox.ts index 4cb798366..4abd1ce38 100644 --- a/packages/pl-fe/src/reducers/shoutbox.ts +++ b/packages/pl-fe/src/reducers/shoutbox.ts @@ -1,39 +1,14 @@ -import { SHOUTBOX_CONNECT, SHOUTBOX_MESSAGES_IMPORT, SHOUTBOX_MESSAGE_IMPORT, type ShoutboxAction } from 'pl-fe/actions/shoutbox'; - -import type { PlApiClient, ShoutMessage as BaseShoutMessage } from 'pl-api'; - -interface ShoutMessage extends Omit { - author_id: string; -} +import type { PlApiClient } from 'pl-api'; +import type { AnyAction } from 'redux'; interface State { socket: ReturnType<(InstanceType)['shoutbox']['connect']> | null; - isLoading: boolean; - messages: Array; } const initialState: State = { socket: null, - isLoading: true, - messages: [], }; -const minifyMessage = ({ author, ...message }: BaseShoutMessage): ShoutMessage => ({ - author_id: author.id, - ...message, -}); +const shoutboxReducer = (state = initialState, action: AnyAction) => state; -const shoutboxReducer = (state = initialState, action: ShoutboxAction) => { - switch (action.type) { - case SHOUTBOX_CONNECT: - return { ...state, socket: action.socket }; - case SHOUTBOX_MESSAGES_IMPORT: - return { ...state, messages: action.messages.map(minifyMessage), isLoading: false }; - case SHOUTBOX_MESSAGE_IMPORT: - return { ...state, messages: [...state.messages, minifyMessage(action.message)] }; - default: - return state; - } -}; - -export { shoutboxReducer as default, type ShoutMessage }; +export { shoutboxReducer as default }; diff --git a/packages/pl-fe/src/stores/modals.ts b/packages/pl-fe/src/stores/modals.ts index 27e9c2b72..b485abe04 100644 --- a/packages/pl-fe/src/stores/modals.ts +++ b/packages/pl-fe/src/stores/modals.ts @@ -1,8 +1,6 @@ import { create } from 'zustand'; import { mutative } from 'zustand-mutative'; -import { MuteModalProps } from 'pl-fe/modals/mute-modal'; - import type { ICryptoAddress } from 'pl-fe/features/crypto-donate/components/crypto-address'; import type { ModalType } from 'pl-fe/features/ui/components/modal-root'; import type { AltTextModalProps } from 'pl-fe/modals/alt-text-modal'; @@ -31,6 +29,7 @@ import type { ListEditorModalProps } from 'pl-fe/modals/list-editor-modal'; import type { MediaModalProps } from 'pl-fe/modals/media-modal'; import type { MentionsModalProps } from 'pl-fe/modals/mentions-modal'; import type { MissingDescriptionModalProps } from 'pl-fe/modals/missing-description-modal'; +import type { MuteModalProps } from 'pl-fe/modals/mute-modal'; import type { ReactionsModalProps } from 'pl-fe/modals/reactions-modal'; import type { ReblogsModalProps } from 'pl-fe/modals/reblogs-modal'; import type { ReplyMentionsModalProps } from 'pl-fe/modals/reply-mentions-modal'; diff --git a/packages/pl-fe/src/stores/shoutbox.ts b/packages/pl-fe/src/stores/shoutbox.ts new file mode 100644 index 000000000..194f11671 --- /dev/null +++ b/packages/pl-fe/src/stores/shoutbox.ts @@ -0,0 +1,72 @@ +import { useEffect } from 'react'; +import { create } from 'zustand'; +import { mutative } from 'zustand-mutative'; + +import { useClient } from 'pl-fe/hooks/use-client'; +import { useInstance } from 'pl-fe/hooks/use-instance'; +import { useLoggedIn } from 'pl-fe/hooks/use-logged-in'; + +import type { PlApiClient, ShoutMessage as BaseShoutMessage } from 'pl-api'; + +const minifyMessage = ({ author, ...message }: BaseShoutMessage) => ({ + author_id: author.id, + ...message, +}); + +type ShoutMessage = ReturnType; + +type State = { + socket: ReturnType<(InstanceType)['shoutbox']['connect']> | null; + messages: Array; + isLoading: boolean; + setMessages: (messages: Array) => void; + pushMessage: (message: BaseShoutMessage) => void; +}; + +const useShoutboxStore = create()(mutative((set) => ({ + socket: null, + messages: [], + isLoading: true, + setMessages: (messages: Array) => set((state: State) => { + state.messages = messages.map(minifyMessage); + state.isLoading = false; + }), + pushMessage: (message: BaseShoutMessage) => set((state: State) => { + state.messages.push(minifyMessage(message)); + }), +}), { + enableAutoFreeze: false, +})); + +const useShoutboxSubscription = () => { + const client = useClient(); + const instance = useInstance(); + const { isLoggedIn } = useLoggedIn(); + const shoutboxStore = useShoutboxStore(); + + useEffect(() => { + if (!(instance.fetched && isLoggedIn)) return; + + let socket: ReturnType<(InstanceType)['shoutbox']['connect']>; + + client.settings.verifyCredentials().then((account) => { + if (account.__meta.pleroma?.chat_token) { + socket = client.shoutbox.connect(account.__meta.pleroma?.chat_token, { + onMessage: (message) => shoutboxStore.pushMessage(message), + onMessages: (messages) => shoutboxStore.setMessages(messages), + }); + } + }).catch(() => {}); + + return () => { + socket?.close(); + }; + }, [instance.fetched && isLoggedIn]); +}; + +const useCreateShoutboxMessage = () => { + const { socket } = useShoutboxStore(); + return { mutate: socket?.message }; +}; + +export { useShoutboxStore, useShoutboxSubscription, useCreateShoutboxMessage, type ShoutMessage };