pl-fe: minify pleroma shoutbox messages
Signed-off-by: nicole mikołajczyk <git@mkljczk.pl>
This commit is contained in:
@ -1109,7 +1109,6 @@ const getFeatures = (instance: Instance) => {
|
||||
*/
|
||||
mastodonAdminMetrics: v.software === MASTODON && gte(v.version, '3.5.0'),
|
||||
|
||||
|
||||
/**
|
||||
* Can perform moderation actions with account and reports.
|
||||
* @see {@link https://docs.joinmastodon.org/methods/admin/}
|
||||
|
||||
@ -3,7 +3,7 @@ import { Entities } from 'pl-fe/entity-store/entities';
|
||||
import { normalizeGroup } from 'pl-fe/normalizers/group';
|
||||
|
||||
import type { Account as BaseAccount, Group as BaseGroup, Poll as BasePoll, Relationship as BaseRelationship, Status as BaseStatus } from 'pl-api';
|
||||
import type { AppDispatch } from 'pl-fe/store';
|
||||
import type { AppDispatch, RootState } from 'pl-fe/store';
|
||||
|
||||
const STATUS_IMPORT = 'STATUS_IMPORT' as const;
|
||||
const STATUSES_IMPORT = 'STATUSES_IMPORT' as const;
|
||||
@ -51,11 +51,17 @@ const importEntities = (entities: {
|
||||
statuses?: Array<BaseStatus & { expectsCard?: boolean } | undefined | null>;
|
||||
relationships?: Array<BaseRelationship | undefined | null>;
|
||||
}, options: {
|
||||
// Whether to replace existing entities. Set to false when working with potentially outdated data. Currently, only implemented for accounts.
|
||||
override?: boolean;
|
||||
withParents?: boolean;
|
||||
idempotencyKey?: string;
|
||||
} = {
|
||||
withParents: true,
|
||||
}) => (dispatch: AppDispatch) => {
|
||||
}) => (dispatch: AppDispatch, getState: () => RootState) => {
|
||||
const override = options.override ?? true;
|
||||
|
||||
const state: RootState = !override ? getState() : undefined as any;
|
||||
|
||||
const accounts: Record<string, BaseAccount> = {};
|
||||
const groups: Record<string, BaseGroup> = {};
|
||||
const polls: Record<string, BasePoll> = {};
|
||||
@ -63,6 +69,8 @@ const importEntities = (entities: {
|
||||
const statuses: Record<string, BaseStatus> = {};
|
||||
|
||||
const processAccount = (account: BaseAccount, withSelf = true) => {
|
||||
if (!override && state.entities[Entities.ACCOUNTS]?.store[account.id]) return;
|
||||
|
||||
if (withSelf) accounts[account.id] = account;
|
||||
|
||||
if (account.moved) processAccount(account.moved);
|
||||
|
||||
@ -11,7 +11,7 @@ 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) }));
|
||||
dispatch(importEntities({ accounts: messages.map((message) => message.author) }, { override: false }));
|
||||
|
||||
return dispatch({
|
||||
type: SHOUTBOX_MESSAGES_IMPORT,
|
||||
@ -20,7 +20,7 @@ const importShoutboxMessages = (messages: ShoutMessage[]) => (dispatch: AppDispa
|
||||
};
|
||||
|
||||
const importShoutboxMessage = (message: ShoutMessage) => (dispatch: AppDispatch): ShoutboxAction => {
|
||||
dispatch(importEntities({ accounts: [message.author] }));
|
||||
dispatch(importEntities({ accounts: [message.author] }, { override: false }));
|
||||
|
||||
return dispatch({
|
||||
type: SHOUTBOX_MESSAGE_IMPORT,
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
import HStack from 'pl-fe/components/ui/hstack';
|
||||
@ -28,6 +29,7 @@ const ChatListShoutbox: React.FC<IChatListShoutboxInterface> = ({ onClick }) =>
|
||||
};
|
||||
|
||||
const lastMessage = messages.at(-1);
|
||||
const { account: lastMessageAuthor } = useAccount(lastMessage?.author_id);
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -59,10 +61,12 @@ const ChatListShoutbox: React.FC<IChatListShoutboxInterface> = ({ onClick }) =>
|
||||
truncate
|
||||
className='truncate-child pointer-events-none h-5 w-full'
|
||||
>
|
||||
<Text weight='bold' size='sm' align='left' theme='muted' truncate tag='span'>
|
||||
{lastMessage.author.display_name || `@${lastMessage.author.username}`}:
|
||||
{' '}
|
||||
</Text>
|
||||
{lastMessageAuthor && (
|
||||
<Text weight='bold' size='sm' align='left' theme='muted' truncate tag='span'>
|
||||
{lastMessageAuthor.display_name || `@${lastMessageAuthor.username}`}:
|
||||
{' '}
|
||||
</Text>
|
||||
)}
|
||||
<ParsedContent html={lastMessage.text} />
|
||||
</Text>
|
||||
</>
|
||||
|
||||
@ -3,6 +3,7 @@ import React, { useState, useEffect, useRef, useMemo } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
||||
|
||||
import { useAccount } from 'pl-fe/api/hooks/accounts/use-account';
|
||||
import HoverAccountWrapper from 'pl-fe/components/hover-account-wrapper';
|
||||
import { ParsedContent } from 'pl-fe/components/parsed-content';
|
||||
import Avatar from 'pl-fe/components/ui/avatar';
|
||||
@ -15,8 +16,81 @@ import { useAppSelector } from 'pl-fe/hooks/use-app-selector';
|
||||
|
||||
import { ChatMessageListList, ChatMessageListScroller } from './chat-message-list';
|
||||
|
||||
import type { ShoutMessage } from 'pl-fe/reducers/shoutbox';
|
||||
|
||||
const START_INDEX = 10000;
|
||||
|
||||
interface IShoutboxMessage {
|
||||
message: ShoutMessage;
|
||||
isMyMessage: boolean;
|
||||
}
|
||||
|
||||
const ShoutboxMessage: React.FC<IShoutboxMessage> = ({ message, isMyMessage }) => {
|
||||
const { account } = useAccount(message.author_id);
|
||||
|
||||
if (!account) return null;
|
||||
|
||||
return (
|
||||
<div key={message.id} className='group relative px-4 py-2 hover:bg-gray-200/40 dark:hover:bg-gray-800/40'>
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems='bottom'
|
||||
justifyContent={isMyMessage ? 'end' : 'start'}
|
||||
className={clsx({
|
||||
'ml-auto': isMyMessage,
|
||||
})}
|
||||
>
|
||||
{!isMyMessage && (
|
||||
<HoverAccountWrapper accountId={account.id} element='span'>
|
||||
<Link className='mb-0.5' to={`/@${account.acct}`} title={account.acct}>
|
||||
<Avatar
|
||||
src={account.avatar}
|
||||
alt={account.avatar_description}
|
||||
size={32}
|
||||
/>
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
space={0.5}
|
||||
className={clsx({
|
||||
'max-w-[85%]': true,
|
||||
'order-3': isMyMessage,
|
||||
'order-1': !isMyMessage,
|
||||
})}
|
||||
alignItems={isMyMessage ? 'end' : 'start'}
|
||||
>
|
||||
<HStack alignItems='bottom' className='max-w-full'>
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'text-ellipsis break-words relative rounded-md py-2 px-3 max-w-full space-y-2 [&_.mention]:underline': true,
|
||||
'[&_.mention]:text-primary-600 dark:[&_.mention]:text-accent-blue': !isMyMessage,
|
||||
'[&_.mention]:text-white dark:[&_.mention]:white': isMyMessage,
|
||||
'bg-primary-500 text-white': isMyMessage,
|
||||
'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100': !isMyMessage,
|
||||
// '!bg-transparent !p-0 emoji-lg': isOnlyEmoji,
|
||||
})
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text size='sm' theme='inherit' className='break-word-nested'>
|
||||
<ParsedContent html={message.text} />
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
{!isMyMessage && (
|
||||
<Text size='xs' theme='muted'>
|
||||
<Emojify text={account.display_name} emojis={account.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
/** Scrollable list of shoutbox messages. */
|
||||
const ShoutboxMessageList: React.FC = () => {
|
||||
const node = useRef<VirtuosoHandle>(null);
|
||||
@ -70,69 +144,13 @@ const ShoutboxMessageList: React.FC = () => {
|
||||
{...initialScrollPositionProps}
|
||||
data={shoutboxMessages}
|
||||
followOutput='auto'
|
||||
itemContent={(index, shoutboxMessage) => {
|
||||
const isMyMessage = shoutboxMessage.author.id === me;
|
||||
|
||||
return (
|
||||
<div key={shoutboxMessage.id} className='group relative px-4 py-2 hover:bg-gray-200/40 dark:hover:bg-gray-800/40'>
|
||||
<HStack
|
||||
space={2}
|
||||
alignItems='bottom'
|
||||
justifyContent={isMyMessage ? 'end' : 'start'}
|
||||
className={clsx({
|
||||
'ml-auto': isMyMessage,
|
||||
})}
|
||||
>
|
||||
{!isMyMessage && (
|
||||
<HoverAccountWrapper accountId={shoutboxMessage.author.id} element='span'>
|
||||
<Link className='mb-0.5' to={`/@${shoutboxMessage.author.acct}`} title={shoutboxMessage.author.acct}>
|
||||
<Avatar
|
||||
src={shoutboxMessage.author.avatar}
|
||||
alt={shoutboxMessage.author.avatar_description}
|
||||
size={32}
|
||||
/>
|
||||
</Link>
|
||||
</HoverAccountWrapper>
|
||||
)}
|
||||
|
||||
<Stack
|
||||
space={0.5}
|
||||
className={clsx({
|
||||
'max-w-[85%]': true,
|
||||
'order-3': isMyMessage,
|
||||
'order-1': !isMyMessage,
|
||||
})}
|
||||
alignItems={isMyMessage ? 'end' : 'start'}
|
||||
>
|
||||
<HStack alignItems='bottom' className='max-w-full'>
|
||||
<div
|
||||
className={
|
||||
clsx({
|
||||
'text-ellipsis break-words relative rounded-md py-2 px-3 max-w-full space-y-2 [&_.mention]:underline': true,
|
||||
'[&_.mention]:text-primary-600 dark:[&_.mention]:text-accent-blue': !isMyMessage,
|
||||
'[&_.mention]:text-white dark:[&_.mention]:white': isMyMessage,
|
||||
'bg-primary-500 text-white': isMyMessage,
|
||||
'bg-gray-200 dark:bg-gray-800 text-gray-900 dark:text-gray-100': !isMyMessage,
|
||||
// '!bg-transparent !p-0 emoji-lg': isOnlyEmoji,
|
||||
})
|
||||
}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Text size='sm' theme='inherit' className='break-word-nested'>
|
||||
<ParsedContent html={shoutboxMessage.text} />
|
||||
</Text>
|
||||
</div>
|
||||
</HStack>
|
||||
{!isMyMessage && (
|
||||
<Text size='xs' theme='muted'>
|
||||
<Emojify text={shoutboxMessage.author.display_name} emojis={shoutboxMessage.author.emojis} />
|
||||
</Text>
|
||||
)}
|
||||
</Stack>
|
||||
</HStack>
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
itemContent={(index, shoutboxMessage) => (
|
||||
<ShoutboxMessage
|
||||
key={shoutboxMessage.id}
|
||||
message={shoutboxMessage}
|
||||
isMyMessage={shoutboxMessage.author_id === me}
|
||||
/>
|
||||
)}
|
||||
components={{
|
||||
List: ChatMessageListList,
|
||||
Scroller: ChatMessageListScroller,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import { SHOUTBOX_CONNECT, SHOUTBOX_MESSAGES_IMPORT, SHOUTBOX_MESSAGE_IMPORT, type ShoutboxAction } from 'pl-fe/actions/shoutbox';
|
||||
|
||||
import type { PlApiClient, ShoutMessage } from 'pl-api';
|
||||
import type { PlApiClient, ShoutMessage as BaseShoutMessage } from 'pl-api';
|
||||
|
||||
interface ShoutMessage extends Omit<BaseShoutMessage, 'author'> {
|
||||
author_id: string;
|
||||
}
|
||||
|
||||
interface State {
|
||||
socket: ReturnType<(InstanceType<typeof PlApiClient>)['shoutbox']['connect']> | null;
|
||||
@ -14,17 +18,22 @@ const initialState: State = {
|
||||
messages: [],
|
||||
};
|
||||
|
||||
const minifyMessage = ({ author, ...message }: BaseShoutMessage): ShoutMessage => ({
|
||||
author_id: author.id,
|
||||
...message,
|
||||
});
|
||||
|
||||
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, isLoading: false };
|
||||
return { ...state, messages: action.messages.map(minifyMessage), isLoading: false };
|
||||
case SHOUTBOX_MESSAGE_IMPORT:
|
||||
return { ...state, messages: [...state.messages, action.message] };
|
||||
return { ...state, messages: [...state.messages, minifyMessage(action.message)] };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
export { shoutboxReducer as default };
|
||||
export { shoutboxReducer as default, type ShoutMessage };
|
||||
|
||||
Reference in New Issue
Block a user