pl-fe: migrate shoutbox away from redux, this might break stuff

Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
nicole mikołajczyk
2025-10-14 07:34:32 +02:00
parent a87b7bafd2
commit c4f9068e37
11 changed files with 95 additions and 135 deletions

View File

@ -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<typeof PlApiClient>)['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,
};

View File

@ -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<IChatListShoutboxInterface> = ({ onClick }) => {
const instance = useInstance();
const { logo } = usePlFeConfig();
const messages = useAppSelector((state) => state.shoutbox.messages);
const messages = useShoutboxStore().messages;
const handleKeyDown: React.KeyboardEventHandler<HTMLDivElement> = (event) => {
if (event.key === 'Enter' || event.key === ' ') {

View File

@ -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<IChatList> = ({ 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<Chat | 'shoutbox'> | undefined = showShoutbox ? ['shoutbox', ...(chats || [])] : chats;

View File

@ -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();

View File

@ -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;

View File

@ -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<ChatInterface> = ({ inputRef, className }) => {
const dispatch = useAppDispatch();
const [content, setContent] = useState<string>('');
const [resetContentKey, setResetContentKey] = useState<number>(fileKeyGen());
const [errorMessage] = useState<string>();
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<ChatInterface> = ({ inputRef, className }) => {
onSubmit={sendMessage}
errorMessage={errorMessage}
resetContentKey={resetContentKey}
disabled={!createShoutboxMessage}
/>
</Stack>
);

View File

@ -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<IUI> = 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<IUI> = React.memo(({ children }) => {
setTimeout(() => prefetchFollowRequests(client), 700);
}
if (features.shoutbox) {
dispatch(connectShoutbox());
}
if (features.scheduledStatuses) {
setTimeout(() => {
queryClient.prefetchInfiniteQuery(scheduledStatusesQueryOptions);

View File

@ -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,
};

View File

@ -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<BaseShoutMessage, 'author'> {
author_id: string;
}
import type { PlApiClient } from 'pl-api';
import type { AnyAction } from 'redux';
interface State {
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']> | null;
isLoading: boolean;
messages: Array<ShoutMessage>;
}
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 };

View File

@ -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';

View File

@ -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<typeof minifyMessage>;
type State = {
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']> | null;
messages: Array<ShoutMessage>;
isLoading: boolean;
setMessages: (messages: Array<BaseShoutMessage>) => void;
pushMessage: (message: BaseShoutMessage) => void;
};
const useShoutboxStore = create<State>()(mutative((set) => ({
socket: null,
messages: [],
isLoading: true,
setMessages: (messages: Array<BaseShoutMessage>) => 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<typeof PlApiClient>)['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 };