Merge branch 'develop' of https://codeberg.org/mkljczk/pl-fe into develop
Some checks failed
pl-api CI / Test for pl-api formatting (22.x) (push) Has been cancelled
pl-fe CI / Test and upload artifacts (22.x) (push) Has been cancelled
pl-hooks CI / Test for a successful build (22.x) (push) Has been cancelled
pl-fe CI / deploy (push) Has been cancelled

This commit is contained in:
2026-01-30 23:22:21 +00:00
42 changed files with 291 additions and 159 deletions

View File

@ -36,6 +36,7 @@ const statusEventSchema = v.object({
end_time: v.fallback(v.nullable(datetimeSchema), null),
join_mode: v.fallback(v.nullable(v.picklist(['free', 'restricted', 'invite', 'external'])), null),
participants_count: v.fallback(v.number(), 0),
participation_request_count: v.fallback(v.number(), 0),
location: v.fallback(v.nullable(locationSchema), null),
join_state: v.fallback(v.nullable(v.picklist(['pending', 'reject', 'accept'])), null),
});

View File

@ -308,13 +308,7 @@
"@stylistic/member-delimiter-style": "error",
"promise/catch-or-return": "error",
"react-hooks/rules-of-hooks": "error",
"tailwindcss/classnames-order": [
"error",
{
"classRegex": "^(base|container|icon|item|list|outer|wrapper)?[c|C]lass(Name)?$",
"config": "tailwind.config.ts"
}
],
"tailwindcss/classnames-order": "off",
"tailwindcss/migration-from-tailwind-2": "error",
"tailwindcss/no-custom-classname": "off",

View File

@ -126,7 +126,7 @@ const DropdownMenuItem = ({ index, item, onClick, autoFocus, onSetTab }: IDropdo
onClick={handleClick}
onAuxClick={handleAuxClick}
onKeyPress={handleItemKeyPress}
target={item.target}
target={typeof item.target === 'string' ? item.target : '_blank'}
title={item.text}
className={
clsx('mx-2 my-1 flex cursor-pointer items-center rounded-md px-2 py-1.5 text-sm text-gray-700 hover:bg-gray-100 hover:text-gray-800 focus:bg-gray-100 focus:text-gray-800 focus:outline-none black:hover:bg-gray-900 black:focus:bg-gray-900 dark:text-gray-300 dark:hover:bg-gray-800 dark:hover:text-gray-200 dark:focus:bg-gray-800 dark:focus:text-gray-200', {

View File

@ -272,7 +272,7 @@ const SidebarNavigation: React.FC<ISidebarNavigation> = React.memo(({ shrink })
{features.chats && (
<SidebarNavigationLink
to='/chats/{-$chatId}'
to='/chats'
icon={require('@phosphor-icons/core/regular/chats-teardrop.svg')}
activeIcon={require('@phosphor-icons/core/fill/chats-teardrop-fill.svg')}
count={unreadChatsCount}

View File

@ -675,7 +675,7 @@ const MenuButton: React.FC<IMenuButton> = ({
const account = status.account;
getOrCreateChatByAccountId(account.id)
.then((chat) => navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } }))
.then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } }))
.catch(() => { });
};

View File

@ -120,7 +120,7 @@ const ThumbNavigation: React.FC = React.memo((): JSX.Element => {
src={require('@phosphor-icons/core/regular/chats-teardrop.svg')}
activeSrc={require('@phosphor-icons/core/fill/chats-teardrop-fill.svg')}
text={intl.formatMessage(messages.chats)}
to='/chats/{-$chatId}'
to='/chats'
exact
count={unreadChatsCount}
countMax={9}

View File

@ -166,7 +166,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
toast.error(data?.error);
},
onSuccess: (response) => {
navigate({ to: '/chats/{-$chatId}', params: { chatId: response.id } });
navigate({ to: '/chats/$chatId', params: { chatId: response.id } });
queryClient.invalidateQueries({
queryKey: ['chats', 'search'],
});

View File

@ -59,7 +59,7 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
deleteChat.mutate(undefined, {
onSuccess() {
if (isUsingMainChatPage) {
navigate({ to: '/chats/{-$chatId}' });
navigate({ to: '/chats' });
}
},
});

View File

@ -1,10 +1,6 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import Button from 'pl-fe/components/ui/button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
const messages = defineMessages({
title: { id: 'chat_pane.blankslate.title', defaultMessage: 'No messages yet' },
body: { id: 'chat_pane.blankslate.body', defaultMessage: 'Search for someone to chat with.' },
@ -19,30 +15,21 @@ const Blankslate = ({ onSearch }: IBlankslate) => {
const intl = useIntl();
return (
<Stack
alignItems='center'
justifyContent='center'
className='h-full grow'
data-testid='chat-pane-blankslate'
>
<Stack space={4}>
<Stack space={1} className='mx-auto max-w-[80%]'>
<Text size='lg' weight='bold' align='center'>
{intl.formatMessage(messages.title)}
</Text>
<div className='⁂-chat-widget__blankslate' data-testid='chat-pane-blankslate'>
<div className='⁂-chat-widget__blankslate__text'>
<p className='⁂-chat-widget__blankslate__text__title'>
{intl.formatMessage(messages.title)}
</p>
<Text theme='muted' align='center'>
{intl.formatMessage(messages.body)}
</Text>
</Stack>
<p className='⁂-chat-widget__blankslate__text__body'>
{intl.formatMessage(messages.body)}
</p>
</div>
<div className='mx-auto'>
<Button theme='primary' onClick={onSearch}>
{intl.formatMessage(messages.action)}
</Button>
</div>
</Stack>
</Stack>
<button onClick={onSearch}>
{intl.formatMessage(messages.action)}
</button>
</div>
);
};

View File

@ -1,7 +1,6 @@
import React from 'react';
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 { useChats } from 'pl-fe/queries/chats';
@ -37,9 +36,9 @@ const ChatPane = () => {
const renderBody = () => {
if (Number(chats?.length) > 0 || showShoutbox || isLoading) {
return (
<Stack space={4} className='h-full grow'>
<div className='⁂-chat-widget__list'>
<ChatList onClickChat={handleClickChat} />
</Stack>
</div>
);
} else if (chats?.length === 0) {
return (

View File

@ -53,7 +53,7 @@ const ChatSearch: React.FC<IChatSearch> = ({ isMainPage = false }) => {
},
onSuccess: (response) => {
if (isMainPage) {
navigate({ to: '/chats/{-$chatId}', params: { chatId: response.id } });
navigate({ to: '/chats/$chatId', params: { chatId: response.id } });
} else {
changeScreen(ChatWidgetScreens.CHAT, response.id);
}

View File

@ -1,9 +1,6 @@
import clsx from 'clsx';
import React, { HTMLAttributes } from 'react';
import HStack from 'pl-fe/components/ui/hstack';
import IconButton from 'pl-fe/components/ui/icon-button';
import Text from 'pl-fe/components/ui/text';
import { useSettings } from 'pl-fe/stores/settings';
interface IChatPaneHeader {
@ -37,45 +34,40 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
}
return (
<HStack {...rest} alignItems='center' justifyContent='between' className='h-16 rounded-t-xl px-4 py-3'>
<div {...rest} className='⁂-chat-widget__header'>
<ButtonComp
className='flex h-16 grow flex-row items-center space-x-1'
className='⁂-chat-widget__header__title'
data-testid='title'
{...buttonProps}
>
<Text weight='semibold' tag='div'>
{title}
</Text>
<div>{title}</div>
{(!demetricator && typeof unreadCount !== 'undefined' && unreadCount > 0) && (
<HStack alignItems='center' space={2}>
<Text weight='semibold' data-testid='unread-count'>
{(!demetricator && unreadCount !== undefined && unreadCount > 0) && (
<div className='⁂-chat-widget__header__count'>
<p data-testid='unread-count'>
({unreadCount})
</Text>
</p>
<div className='size-2.5 rounded-full bg-accent-300' />
</HStack>
<div className='⁂-chat-widget__header__count__dot' />
</div>
)}
</ButtonComp>
<HStack space={2} alignItems='center'>
<div className='⁂-chat-widget__header__actions'>
{secondaryAction ? (
<IconButton
onClick={secondaryAction}
src={secondaryActionIcon as string}
iconClassName='h-5 w-5 text-gray-600'
/>
) : null}
<IconButton
onClick={onToggle}
src={require('@phosphor-icons/core/regular/caret-up.svg')}
iconClassName={clsx('size-5 text-gray-600 transition-transform', {
'rotate-180': isOpen,
})}
className='⁂-chat-widget__header__open-button'
/>
</HStack>
</HStack>
</div>
</div>
);
};

View File

@ -1,7 +1,6 @@
import React from 'react';
import { defineMessages, useIntl } from 'react-intl';
import HStack from 'pl-fe/components/ui/hstack';
import Icon from 'pl-fe/components/ui/icon';
import Text from 'pl-fe/components/ui/text';
import { ChatWidgetScreens, useChatContext } from 'pl-fe/contexts/chat-context';
@ -21,7 +20,7 @@ const ChatSearchHeader = () => {
<ChatPaneHeader
data-testid='pane-header'
title={
<HStack alignItems='center' space={2}>
<div className='⁂-chat-widget__search-header'>
<button
onClick={() => {
changeScreen(ChatWidgetScreens.INBOX);
@ -36,7 +35,7 @@ const ChatSearchHeader = () => {
<Text size='sm' weight='bold' truncate>
{intl.formatMessage(messages.title)}
</Text>
</HStack>
</div>
}
isOpen={isOpen}
isToggleable={false}

View File

@ -3,18 +3,15 @@ import clsx from 'clsx';
import React, { useEffect, useLayoutEffect, useRef, useState } from 'react';
import Stack from 'pl-fe/components/ui/stack';
import { chatRoute, chatsNewRoute, chatsSettingsRoute, shoutboxRoute } from 'pl-fe/features/ui/router';
import { chatsEmptyRoute } from 'pl-fe/features/ui/router';
import { useChats } from 'pl-fe/queries/chats';
import ChatPageSidebar from './components/chat-page-sidebar';
import ChatsPageSidebar from './components/chats-page-sidebar';
const ChatPage: React.FC = () => {
const chatMatch = useMatch({ from: chatRoute.id, shouldThrow: false });
const onChatRoute = !!chatMatch?.params?.chatId;
const onNewRoute = !!useMatch({ from: chatsNewRoute.id, shouldThrow: false });
const onSettingsRoute = !!useMatch({ from: chatsSettingsRoute.id, shouldThrow: false });
const onShoutboxRoute = !!useMatch({ from: shoutboxRoute.id, shouldThrow: false });
const ChatsPage: React.FC = () => {
const { chatsQuery: { data: chats } } = useChats();
const isSidebarHidden = onChatRoute || onNewRoute || onSettingsRoute || onShoutboxRoute;
const isSidebarHidden = !useMatch({ from: chatsEmptyRoute.id, shouldThrow: false }) || chats?.length === 0;
const containerRef = useRef<HTMLDivElement>(null);
const [height, setHeight] = useState<string | number>('100%');
@ -49,18 +46,18 @@ const ChatPage: React.FC = () => {
<div
ref={containerRef}
style={{ height }}
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg black:bg-transparent dark:bg-primary-900 dark:text-gray-100 dark:shadow-none sm:rounded-t-xl'
className='h-screen overflow-hidden bg-white text-gray-900 shadow-lg sm:rounded-t-xl dark:bg-primary-900 dark:text-gray-100 dark:shadow-none black:bg-transparent'
>
<div
className='grid h-full grid-cols-9 overflow-hidden black:divide-gray-800 dark:divide-solid dark:divide-primary-800 sm:black:divide-x sm:dark:divide-x-2'
className='grid h-full grid-cols-9 overflow-hidden dark:divide-solid sm:dark:divide-x-2 dark:divide-primary-800 sm:black:divide-x black:divide-gray-800'
data-testid='chat-page'
>
<Stack
className={clsx('dark:inset col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 black:bg-black dark:bg-gray-900 dark:bg-none sm:col-span-3', {
className={clsx('col-span-9 overflow-hidden bg-gradient-to-r from-white to-gray-100 sm:col-span-3 dark:inset dark:bg-gray-900 dark:bg-none black:bg-black', {
'hidden sm:block': isSidebarHidden,
})}
>
<ChatPageSidebar />
<ChatsPageSidebar />
</Stack>
<Stack
@ -75,4 +72,4 @@ const ChatPage: React.FC = () => {
);
};
export { ChatPage as default };
export { ChatsPage as default };

View File

@ -9,18 +9,14 @@ import IconButton from 'pl-fe/components/ui/icon-button';
import Stack from 'pl-fe/components/ui/stack';
import Text from 'pl-fe/components/ui/text';
import VerificationBadge from 'pl-fe/components/verification-badge';
import { useChatContext } from 'pl-fe/contexts/chat-context';
import { chatRoute } from 'pl-fe/features/ui/router';
import { useFeatures } from 'pl-fe/hooks/use-features';
import { useUnblockAccountMutation, useRelationshipQuery } from 'pl-fe/queries/accounts/use-relationship';
import { useChat, useChatActions, useChats } from 'pl-fe/queries/chats';
import { useChat, useChatActions } from 'pl-fe/queries/chats';
import { useModalsActions } from 'pl-fe/stores/modals';
import Chat from '../../chat';
import BlankslateEmpty from './blankslate-empty';
import BlankslateWithChats from './blankslate-with-chats';
const messages = defineMessages({
blockMessage: { id: 'chat_settings.block.message', defaultMessage: 'Blocking will prevent this profile from direct messaging you and viewing your content. You can unblock later.' },
blockHeading: { id: 'chat_settings.block.heading', defaultMessage: 'Block @{acct}' },
@ -36,7 +32,7 @@ const messages = defineMessages({
leaveChat: { id: 'chat_settings.options.leave_chat', defaultMessage: 'Leave chat' },
});
const ChatPageMain = () => {
const ChatsPageChat = () => {
const intl = useIntl();
const features = useFeatures();
const navigate = useNavigate();
@ -45,8 +41,6 @@ const ChatPageMain = () => {
const { openModal } = useModalsActions();
const { data: chat } = useChat(chatId);
const { currentChatId } = useChatContext();
const { chatsQuery: { data: chats, isLoading } } = useChats();
const { mutate: unblockAccount } = useUnblockAccountMutation(chat?.account.id!);
@ -80,25 +74,13 @@ const ChatPageMain = () => {
onConfirm: () => {
deleteChat.mutate(undefined, {
onSuccess() {
navigate({ to: '/chats/{-$chatId}' });
navigate({ to: '/chats' });
},
});
},
});
};
if (isLoading) {
return null;
}
if (!currentChatId && chats && chats.length > 0) {
return <BlankslateWithChats />;
}
if (!currentChatId) {
return <BlankslateEmpty />;
}
if (!chat) {
return null;
}
@ -171,4 +153,4 @@ const ChatPageMain = () => {
);
};
export { ChatPageMain as default };
export { ChatsPageChat as default };

View File

@ -0,0 +1,22 @@
import React from 'react';
import { useChats } from 'pl-fe/queries/chats';
import BlankslateEmpty from './blankslate-empty';
import BlankslateWithChats from './blankslate-with-chats';
const ChatsPageEmpty = () => {
const { chatsQuery: { data: chats, isLoading } } = useChats();
if (isLoading) {
return null;
}
if (chats && chats.length > 0) {
return <BlankslateWithChats />;
}
return <BlankslateEmpty />;
};
export { ChatsPageEmpty as default };

View File

@ -13,11 +13,8 @@ const messages = defineMessages({
title: { id: 'chat.new_message.title', defaultMessage: 'New Message' },
});
interface IChatPageNew {
}
/** New message form to create a chat. */
const ChatPageNew: React.FC<IChatPageNew> = () => {
const ChatsPageNew: React.FC = () => {
const intl = useIntl();
const navigate = useNavigate();
@ -28,7 +25,7 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<CardTitle title={intl.formatMessage(messages.title)} />
@ -40,4 +37,4 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
);
};
export { ChatPageNew as default };
export { ChatsPageNew as default };

View File

@ -30,7 +30,7 @@ const messages = defineMessages({
submit: { id: 'chat.page_settings.submit', defaultMessage: 'Save' },
});
const ChatPageSettings = () => {
const ChatsPageSettings = () => {
const { account } = useOwnAccount();
const intl = useIntl();
const navigate = useNavigate();
@ -58,7 +58,7 @@ const ChatPageSettings = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<CardTitle title={intl.formatMessage(messages.title)} />
@ -98,4 +98,4 @@ const ChatPageSettings = () => {
);
};
export { ChatPageSettings as default };
export { ChatsPageSettings as default };

View File

@ -12,7 +12,7 @@ import { usePlFeConfig } from 'pl-fe/hooks/use-pl-fe-config';
import Shoutbox from '../../shoutbox';
const ChatPageShoutbox = () => {
const ChatsPageShoutbox = () => {
const navigate = useNavigate();
const instance = useInstance();
const { logo } = usePlFeConfig();
@ -25,7 +25,7 @@ const ChatPageShoutbox = () => {
<IconButton
src={require('@phosphor-icons/core/regular/arrow-left.svg')}
className='mr-2 size-7 sm:mr-0 sm:hidden rtl:rotate-180'
onClick={() => navigate({ to: '/chats/{-$chatId}' })}
onClick={() => navigate({ to: '/chats' })}
/>
<Avatar src={logo} alt='' size={40} className='flex-none' />
@ -48,4 +48,4 @@ const ChatPageShoutbox = () => {
);
};
export { ChatPageShoutbox as default };
export { ChatsPageShoutbox as default };

View File

@ -15,7 +15,7 @@ const messages = defineMessages({
title: { id: 'column.chats', defaultMessage: 'Chats' },
});
const ChatPageSidebar = () => {
const ChatsPageSidebar = () => {
const intl = useIntl();
const navigate = useNavigate();
@ -23,7 +23,7 @@ const ChatPageSidebar = () => {
if (chat === 'shoutbox') {
navigate({ to: '/chats/shoutbox' });
} else {
navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } });
navigate({ to: '/chats/$chatId', params: { chatId: chat.id } });
}
};
@ -64,4 +64,4 @@ const ChatPageSidebar = () => {
);
};
export { ChatPageSidebar as default };
export { ChatsPageSidebar as default };

View File

@ -11,10 +11,7 @@ interface IPane {
/** Chat pane UI component for desktop. */
const Pane: React.FC<IPane> = ({ isOpen = false, children }) => (
<div
className={clsx('fixed bottom-0 z-[99] flex w-96 flex-col rounded-t-lg bg-white shadow-3xl black:border black:border-b-0 black:border-gray-800 black:bg-black dark:bg-gray-900 ltr:right-5 rtl:left-5', {
'h-[550px] max-h-[100vh]': isOpen,
'h-16': !isOpen,
})}
className={clsx('⁂-chat-widget', { '⁂-chat-widget--open': isOpen })}
data-testid='pane'
>
{children}

View File

@ -8,10 +8,15 @@ import Spinner from 'pl-fe/components/ui/spinner';
import Stack from 'pl-fe/components/ui/stack';
import AccountContainer from 'pl-fe/containers/account-container';
import { useAcceptEventParticipationRequestMutation, useEventParticipationRequests, useRejectEventParticipationRequestMutation } from 'pl-fe/queries/events/use-event-participation-requests';
import toast from 'pl-fe/toast';
const messages = defineMessages({
authorize: { id: 'compose_event.participation_requests.authorize', defaultMessage: 'Authorize' },
authorizeSuccess: { id: 'compose_event.participation_requests.authorize.success', defaultMessage: 'Event participation request authorized successfully' },
authorizeFail: { id: 'compose_event.participation_requests.authorize.fail', defaultMessage: 'Failed to authorize event participation request' },
reject: { id: 'compose_event.participation_requests.reject', defaultMessage: 'Reject' },
rejectSuccess: { id: 'compose_event.participation_requests.reject.success', defaultMessage: 'Event participation request rejected successfully' },
rejectFail: { id: 'compose_event.participation_requests.reject.fail', defaultMessage: 'Failed to reject event participation request' },
});
interface IAccount {
@ -36,13 +41,27 @@ const Account: React.FC<IAccount> = ({ eventId, id, participationMessage }) => {
theme='secondary'
size='sm'
text={intl.formatMessage(messages.authorize)}
onClick={() => acceptEventParticipationRequest()}
onClick={() => acceptEventParticipationRequest(undefined, {
onSuccess: () => {
toast.success(messages.authorizeSuccess);
},
onError: () => {
toast.error(messages.authorizeFail);
},
})}
/>
<Button
theme='danger'
size='sm'
text={intl.formatMessage(messages.reject)}
onClick={() => rejectEventParticipationRequest()}
onClick={() => rejectEventParticipationRequest(undefined, {
onSuccess: () => {
toast.success(messages.rejectSuccess);
},
onError: () => {
toast.error(messages.rejectFail);
},
})}
/>
</HStack>
}
@ -63,7 +82,7 @@ const ManagePendingParticipants: React.FC<IManagePendingParticipants> = ({ statu
scrollKey={`eventPendingParticipants:${statusId}`}
emptyMessageText={<FormattedMessage id='empty_column.event_participant_requests' defaultMessage='There are no pending event participation requests.' />}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
>
{accounts.map(({ account_id, participation_message }) =>

View File

@ -1,6 +1,6 @@
import clsx from 'clsx';
import fuzzysort from 'fuzzysort';
import React, { useEffect, useMemo, useRef, useState } from 'react';
import React, { useDeferredValue, useEffect, useMemo, useRef, useState } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { addComposeLanguage, changeComposeLanguage, changeComposeModifiedLanguage, deleteComposeLanguage } from 'pl-fe/actions/compose';
@ -96,8 +96,8 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
setSearchValue('');
};
const search = () => {
if (searchValue === '') {
const search = (value: string) => {
if (value === '') {
return [...languages].sort((a, b) => {
// Push current selection to the top of the list
@ -124,7 +124,7 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
});
}
return fuzzysort.go(searchValue, languages, {
return fuzzysort.go(value, languages, {
keys: ['0', '1'],
limit: 5,
threshold: -10000,
@ -143,7 +143,9 @@ const getLanguageDropdown = (composeId: string): React.FC<ILanguageDropdown> =>
}, [node.current]);
const isSearching = searchValue !== '';
const results = useMemo(search, [searchValue]);
const deferredSearchValue = useDeferredValue(searchValue);
const results = useMemo(() => search(deferredSearchValue), [deferredSearchValue]);
return (
<>

View File

@ -171,7 +171,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
const handleChatClick = () => {
getOrCreateChatByAccountId(account.id)
.then((chat) => navigate({ to: '/chats/{-$chatId}', params: { chatId: chat.id } }))
.then((chat) => navigate({ to: '/chats/$chatId', params: { chatId: chat.id } }))
.catch(() => {});
};
@ -443,7 +443,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
</span>
</HStack>
{event.join_mode !== 'external' || event.participants_count > 0 && (
{(event.join_mode !== 'external' || event.participants_count > 0) && (
<HStack alignItems='center' space={2}>
<Icon src={require('@phosphor-icons/core/regular/users.svg')} />
<a href='#' className='hover:underline' onClick={handleParticipantsClick}>

View File

@ -176,8 +176,8 @@ const UI: React.FC = React.memo(() => {
</Suspense>
{me && features.chats && (
<div className='hidden xl:block'>
<Suspense fallback={<div className='fixed bottom-0 z-[99] flex h-16 w-96 animate-pulse flex-col rounded-t-lg bg-white shadow-3xl dark:bg-gray-900 ltr:right-5 rtl:left-5' />}>
<div className='⁂-chat-widget__container'>
<Suspense fallback={<div className='⁂-chat-widget--placeholder' />}>
<ChatWidget />
</Suspense>
</div>

View File

@ -39,10 +39,11 @@ import StatusLayout from 'pl-fe/layouts/status-layout';
import { instanceInitialState } from 'pl-fe/reducers/instance';
import { isStandalone } from 'pl-fe/utils/state';
import ChatPageMain from '../../chats/components/chat-page/components/chat-page-main';
import ChatPageNew from '../../chats/components/chat-page/components/chat-page-new';
import ChatPageSettings from '../../chats/components/chat-page/components/chat-page-settings';
import ChatPageShoutbox from '../../chats/components/chat-page/components/chat-page-shoutbox';
import ChatsPageChat from '../../chats/components/chats-page/components/chats-page-chat';
import ChatsPageEmpty from '../../chats/components/chats-page/components/chats-page-empty';
import ChatsPageNew from '../../chats/components/chats-page/components/chats-page-new';
import ChatsPageSettings from '../../chats/components/chats-page/components/chats-page-settings';
import ChatsPageShoutbox from '../../chats/components/chats-page/components/chats-page-shoutbox';
import ColumnLoading from '../components/column-loading';
import {
AboutPage,
@ -566,25 +567,31 @@ export const chatsRoute = createRoute({
export const chatsNewRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/new',
component: ChatPageNew,
component: ChatsPageNew,
});
export const chatsSettingsRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/settings',
component: ChatPageSettings,
component: ChatsPageSettings,
});
export const shoutboxRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/shoutbox',
component: ChatPageShoutbox,
component: ChatsPageShoutbox,
});
export const chatRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/{-$chatId}',
component: ChatPageMain,
path: '/$chatId',
component: ChatsPageChat,
});
export const chatsEmptyRoute = createRoute({
getParentRoute: () => chatsRoute,
path: '/',
component: ChatsPageEmpty,
});
// Follow requests and blocks
@ -736,8 +743,8 @@ export const eventInformationRoute = createRoute({
});
export const eventEditRoute = createRoute({
getParentRoute: () => layouts.event,
path: '/edit',
getParentRoute: () => layouts.events,
path: '/@{$username}/events/$statusId/edit',
component: EditEvent,
beforeLoad: requireAuthMiddleware(({ context: { features } }) => {
if (!features.events) throw notFound();
@ -1311,6 +1318,7 @@ const routeTree = rootRoute.addChildren([
chatsSettingsRoute,
shoutboxRoute,
chatRoute,
chatsEmptyRoute,
]),
]),
layouts.default.addChildren([

View File

@ -548,8 +548,12 @@
"compose_event.fields.start_time_label": "Event start date",
"compose_event.fields.start_time_placeholder": "Event begins on…",
"compose_event.participation_requests.authorize": "Authorize",
"compose_event.participation_requests.authorize.fail": "Failed to authorize event participation request",
"compose_event.participation_requests.authorize.success": "Event participation request authorized successfully",
"compose_event.participation_requests.authorize_success": "User accepted",
"compose_event.participation_requests.reject": "Reject",
"compose_event.participation_requests.reject.fail": "Failed to reject event participation request",
"compose_event.participation_requests.reject.success": "Event participation request rejected successfully",
"compose_event.participation_requests.reject_success": "User rejected",
"compose_event.reset_location": "Reset location",
"compose_event.submit_success": "Your event was created",

View File

@ -36,7 +36,7 @@ const DislikesModal: React.FC<BaseModalProps & DislikesModalProps> = ({ onClose,
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
useWindowScroll={false}
>

View File

@ -34,8 +34,9 @@ const EventParticipantsModal: React.FC<BaseModalProps & EventParticipantsModalPr
emptyMessageText={emptyMessage}
listClassName='max-w-full'
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
useWindowScroll={false}
>

View File

@ -36,7 +36,7 @@ const FavouritesModal: React.FC<BaseModalProps & FavouritesModalProps> = ({ onCl
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
useWindowScroll={false}
>

View File

@ -91,7 +91,7 @@ const ReactionsModal: React.FC<BaseModalProps & ReactionsModalProps> = ({ onClos
})}
itemClassName='pb-3'
style={{ height: reactions.length > 0 ? 'calc(80vh - 159px)' : 'calc(80vh - 88px)' }}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
useWindowScroll={false}
>
{accounts.map((account) =>

View File

@ -36,7 +36,7 @@ const ReblogsModal: React.FC<BaseModalProps & ReblogsModalProps> = ({ onClose, s
itemClassName='pb-3'
style={{ height: 'calc(80vh - 88px)' }}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
useWindowScroll={false}
>

View File

@ -1,6 +1,6 @@
import fuzzysort from 'fuzzysort';
import { BookmarkFolder } from 'pl-api';
import React, { useCallback, useMemo, useState } from 'react';
import React, { useCallback, useDeferredValue, useMemo, useState } from 'react';
import { FormattedMessage } from 'react-intl';
import { ListItem } from 'pl-fe/components/list';
@ -38,6 +38,7 @@ const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseM
const [selectedFolder, setSelectedFolder] = useState(status.bookmark_folder);
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
const handleSearchChange: React.ChangeEventHandler<HTMLInputElement> = e => {
setSearchTerm(e.target.value);
@ -73,10 +74,10 @@ const SelectBookmarkFolderModal: React.FC<SelectBookmarkFolderModalProps & BaseM
const filteredFolders = useMemo(() => {
if (!bookmarkFolders) return [];
const filtered = search(bookmarkFolders, searchTerm);
const filtered = search(bookmarkFolders, deferredSearchTerm);
return filtered;
}, [bookmarkFolders, searchTerm]);
}, [bookmarkFolders, deferredSearchTerm]);
let items;

View File

@ -81,7 +81,7 @@ const FollowRequestsPage: React.FC = () => {
<ScrollableList
scrollKey='followRequests'
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
emptyMessageText={<FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />}
>

View File

@ -22,7 +22,7 @@ const OutgoingFollowRequestsPage: React.FC = () => {
<ScrollableList
scrollKey='outgoingFollowRequests'
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
emptyMessageText={<FormattedMessage id='empty_column.outgoing_follow_requests' defaultMessage="You don't have any outgoing follow requests yet. When you try to follow a user, it will show up here." />}
itemClassName='p-2.5'

View File

@ -1,11 +1,11 @@
import React from 'react';
import { ChatProvider } from 'pl-fe/contexts/chat-context';
import ChatPage from 'pl-fe/features/chats/components/chat-page/chat-page';
import ChatsPage from 'pl-fe/features/chats/components/chats-page/chats-page';
const ChatIndex: React.FC = () => (
<ChatProvider>
<ChatPage />
<ChatsPage />
</ChatProvider>
);

View File

@ -35,7 +35,7 @@ const QuotesPage: React.FC = () => {
statusIds={statusIds}
scrollKey={`quotes:${statusId}`}
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={handleLoadMore}
emptyMessageText={emptyMessage}
/>

View File

@ -22,7 +22,7 @@ const ScheduledStatusesPage = () => {
<Column label={intl.formatMessage(messages.heading)}>
<ScrollableList
hasMore={hasNextPage}
isLoading={typeof isLoading === 'boolean' ? isLoading : true}
isLoading={isLoading}
onLoadMore={() => fetchNextPage({ cancelRefetch: false })}
emptyMessageText={emptyMessage}
listClassName='divide-y divide-solid divide-gray-200 black:divide-gray-800 dark:divide-primary-800'

View File

@ -0,0 +1,129 @@
@use 'mixins';
@use 'variables';
.-chat-widget {
@apply fixed bottom-0 z-[99] flex w-96 flex-col rounded-t-lg bg-white shadow-3xl black:border black:border-b-0 black:border-gray-800 black:bg-black dark:bg-gray-900 ltr:right-5 rtl:left-5 h-16;
&--open {
height: 550px;
max-height: 100vh;
.-chat-widget__header__open-button svg {
transform: rotate(180deg);
}
}
&--placeholder {
@apply fixed bottom-0 z-[99] flex h-16 w-96 animate-pulse flex-col rounded-t-lg bg-white shadow-3xl dark:bg-gray-900 ltr:right-5 rtl:left-5;
}
&__container {
display: none;
@media (min-width: variables.$breakpoint-xl) {
display: block;
}
}
&__header {
display: flex;
align-items: center;
justify-content: space-between;
height: 4rem;
border-top-left-radius: 0.75rem;
border-top-right-radius: 0.75rem;
padding: 0.75rem 1rem;
gap: 0.5rem;
overflow: hidden;
&__title {
display: flex;
height: 4rem;
flex-grow: 1;
flex-direction: row;
align-items: center;
gap: 0.25rem;
}
&__title div:first-child,
&__count p {
@include mixins.text($weight: semibold);
overflow: hidden;
text-overflow: ellipsis;
}
&__count {
display: flex;
align-items: center;
gap: 0.5rem;
&__dot {
height: 0.625rem;
width: 0.625rem;
border-radius: 50%;
background: rgb(var(--color-accent-300));
}
}
&__actions {
display: flex;
align-items: center;
gap: 0.5rem;
svg {
height: 1.25rem;
width: 1.25rem;
color: rgb(var(--color-gray-600));
}
}
&__open-button svg {
transform: rotate(0deg);
transition: transform 0.15s cubic-bezier(0.4, 0, 0.2, 1);
}
}
&__search-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
&__list {
display: flex;
flex-direction: column;
gap: 1rem;
height: 100%;
flex-grow: 1;
}
&__blankslate {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
flex-grow: 1;
gap: 1rem;
&__text {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.25rem;
max-width: 80%;
&__title {
@include mixins.text($size: lg, $weight: bold, $align: center);
}
&__body {
@include mixins.text($theme: muted, $align: center);
}
}
button {
@include mixins.button($theme: primary);
}
}
}

View File

@ -6,4 +6,5 @@
@use 'timelines';
@use 'compose';
@use 'drive';
@use 'chats';
@use 'events';