sforkowany z mirror/soapbox
Merge remote-tracking branch 'soapbox/develop' into mastodon-groups
commit
d4024e927d
|
@ -5,7 +5,6 @@ import { defineMessages, useIntl } from 'react-intl';
|
|||
import AutosuggestAccountInput from 'soapbox/components/autosuggest-account-input';
|
||||
|
||||
import SvgIcon from './ui/icon/svg-icon';
|
||||
import { InputThemes } from './ui/input/input';
|
||||
|
||||
const messages = defineMessages({
|
||||
placeholder: { id: 'account_search.placeholder', defaultMessage: 'Search for an account' },
|
||||
|
@ -16,20 +15,10 @@ interface IAccountSearch {
|
|||
onSelected: (accountId: string) => void,
|
||||
/** Override the default placeholder of the input. */
|
||||
placeholder?: string,
|
||||
/** Position of results relative to the input. */
|
||||
resultsPosition?: 'above' | 'below',
|
||||
/** Optional class for the input */
|
||||
className?: string,
|
||||
autoFocus?: boolean,
|
||||
hidePortal?: boolean,
|
||||
theme?: InputThemes,
|
||||
showButtons?: boolean,
|
||||
/** Search only among people who follow you (TruthSocial). */
|
||||
followers?: boolean,
|
||||
}
|
||||
|
||||
/** Input to search for accounts. */
|
||||
const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, className, showButtons = true, ...rest }) => {
|
||||
const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, ...rest }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
@ -71,7 +60,7 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, className, showBu
|
|||
|
||||
<div className='relative'>
|
||||
<AutosuggestAccountInput
|
||||
className={classNames('rounded-full', className)}
|
||||
className='rounded-full'
|
||||
placeholder={intl.formatMessage(messages.placeholder)}
|
||||
value={value}
|
||||
onChange={handleChange}
|
||||
|
@ -80,25 +69,23 @@ const AccountSearch: React.FC<IAccountSearch> = ({ onSelected, className, showBu
|
|||
{...rest}
|
||||
/>
|
||||
|
||||
{showButtons && (
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/search.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
/>
|
||||
<div
|
||||
role='button'
|
||||
tabIndex={0}
|
||||
className='absolute inset-y-0 right-0 px-3 flex items-center cursor-pointer'
|
||||
onClick={handleClear}
|
||||
>
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/search.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: !isEmpty() })}
|
||||
/>
|
||||
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<SvgIcon
|
||||
src={require('@tabler/icons/x.svg')}
|
||||
className={classNames('h-4 w-4 text-gray-400', { hidden: isEmpty() })}
|
||||
aria-label={intl.formatMessage(messages.placeholder)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -22,8 +22,6 @@ interface IAutosuggestAccountInput {
|
|||
menu?: Menu,
|
||||
onKeyDown?: React.KeyboardEventHandler,
|
||||
theme?: InputThemes,
|
||||
/** Search only among people who follow you (TruthSocial). */
|
||||
followers?: boolean,
|
||||
}
|
||||
|
||||
const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
||||
|
@ -31,7 +29,6 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
|||
onSelected,
|
||||
value = '',
|
||||
limit = 4,
|
||||
followers = false,
|
||||
...rest
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
@ -48,7 +45,7 @@ const AutosuggestAccountInput: React.FC<IAutosuggestAccountInput> = ({
|
|||
};
|
||||
|
||||
const handleAccountSearch = useCallback(throttle(q => {
|
||||
const params = { q, limit, followers, resolve: false };
|
||||
const params = { q, limit, resolve: false };
|
||||
|
||||
dispatch(accountSearch(params, controller.current.signal))
|
||||
.then((accounts: { id: string }[]) => {
|
||||
|
|
|
@ -31,7 +31,6 @@ export interface IAutosuggestInput extends Pick<React.HTMLAttributes<HTMLInputEl
|
|||
searchTokens: string[],
|
||||
maxLength?: number,
|
||||
menu?: Menu,
|
||||
resultsPosition: string,
|
||||
renderSuggestion?: React.FC<{ id: string }>,
|
||||
hidePortal?: boolean,
|
||||
theme?: InputThemes,
|
||||
|
@ -43,7 +42,6 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
autoFocus: false,
|
||||
autoSelect: true,
|
||||
searchTokens: ImmutableList(['@', ':', '#']),
|
||||
resultsPosition: 'below',
|
||||
};
|
||||
|
||||
getFirstIndex = () => {
|
||||
|
@ -260,19 +258,15 @@ export default class AutosuggestInput extends ImmutablePureComponent<IAutosugges
|
|||
|
||||
const { top, height, left, width } = this.input.getBoundingClientRect();
|
||||
|
||||
if (this.props.resultsPosition === 'below') {
|
||||
return { left, width, top: top + height };
|
||||
}
|
||||
|
||||
return { left, width, top, transform: 'translate(0, -100%)' };
|
||||
return { left, width, top: top + height };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { hidePortal, value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
||||
const { value, suggestions, disabled, placeholder, onKeyUp, autoFocus, className, id, maxLength, menu, theme } = this.props;
|
||||
const { suggestionsHidden } = this.state;
|
||||
const style: React.CSSProperties = { direction: 'ltr' };
|
||||
|
||||
const visible = !hidePortal && !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
||||
const visible = !suggestionsHidden && (!suggestions.isEmpty() || (menu && value));
|
||||
|
||||
if (isRtl(value)) {
|
||||
style.direction = 'rtl';
|
||||
|
|
|
@ -1,3 +1,7 @@
|
|||
[data-markup] {
|
||||
@apply whitespace-pre-wrap;
|
||||
}
|
||||
|
||||
[data-markup] p {
|
||||
@apply mb-4 whitespace-pre-wrap;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@ import { normalizeAttachment } from 'soapbox/normalizers';
|
|||
import { ChatKeys, useChats } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { Account } from 'soapbox/types/entities';
|
||||
import { isRemote } from 'soapbox/utils/accounts';
|
||||
import { isDefaultHeader, isRemote } from 'soapbox/utils/accounts';
|
||||
|
||||
import type { Menu as MenuType } from 'soapbox/components/dropdown-menu';
|
||||
|
||||
|
@ -502,6 +502,29 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
return info;
|
||||
};
|
||||
|
||||
const renderHeader = () => {
|
||||
let header: React.ReactNode;
|
||||
|
||||
if (account.header) {
|
||||
header = (
|
||||
<StillImage
|
||||
src={account.header}
|
||||
alt={intl.formatMessage(messages.header)}
|
||||
/>
|
||||
);
|
||||
|
||||
if (!isDefaultHeader(account.header)) {
|
||||
header = (
|
||||
<a href={account.header} onClick={handleHeaderClick} target='_blank'>
|
||||
{header}
|
||||
</a>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return header;
|
||||
};
|
||||
|
||||
const renderMessageButton = () => {
|
||||
if (features.chatsWithFollowers) { // Truth Social
|
||||
if (!ownAccount || !account || account.id === ownAccount?.id) {
|
||||
|
@ -570,14 +593,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
|
||||
<div>
|
||||
<div className='relative flex flex-col justify-center h-32 w-full lg:h-48 md:rounded-t-xl bg-gray-200 dark:bg-gray-900/50 overflow-hidden isolate'>
|
||||
{account.header && (
|
||||
<a href={account.header} onClick={handleHeaderClick} target='_blank'>
|
||||
<StillImage
|
||||
src={account.header}
|
||||
alt={intl.formatMessage(messages.header)}
|
||||
/>
|
||||
</a>
|
||||
)}
|
||||
{renderHeader()}
|
||||
|
||||
<div className='absolute top-2 left-2'>
|
||||
<HStack alignItems='center' space={1}>
|
||||
|
@ -594,7 +610,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
|||
<Avatar
|
||||
src={account.avatar}
|
||||
size={96}
|
||||
className='relative h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900'
|
||||
className='relative h-24 w-24 rounded-full ring-4 ring-white dark:ring-primary-900 bg-white dark:bg-primary-900'
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
|
|
|
@ -20,7 +20,6 @@ const messages = defineMessages({
|
|||
delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' },
|
||||
});
|
||||
|
||||
|
||||
const Aliases = () => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useAppDispatch();
|
||||
|
|
|
@ -151,6 +151,9 @@ const ChatComposer = React.forwardRef<HTMLTextAreaElement | null, IChatComposer>
|
|||
|
||||
return (
|
||||
<div className='mt-auto px-4 shadow-3xl'>
|
||||
{/* Spacer */}
|
||||
<div className='h-5' />
|
||||
|
||||
<HStack alignItems='stretch' justifyContent='between' space={4}>
|
||||
{features.chatsMedia && (
|
||||
<Stack justifyContent='end' alignItems='center' className='w-10 mb-1.5'>
|
||||
|
|
|
@ -4,7 +4,7 @@ import { useHistory } from 'react-router-dom';
|
|||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import RelativeTimestamp from 'soapbox/components/relative-timestamp';
|
||||
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import { Avatar, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||
import { useChatContext } from 'soapbox/contexts/chat-context';
|
||||
|
@ -115,12 +115,14 @@ const ChatListItem: React.FC<IChatListItemInterface> = ({ chat, onClick }) => {
|
|||
<HStack alignItems='center' space={2}>
|
||||
{features.chatsDelete && (
|
||||
<div className='text-gray-600 hidden group-hover:block hover:text-gray-100'>
|
||||
{/* TODO: fix nested buttons here */}
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
title='Settings'
|
||||
/>
|
||||
<DropdownMenuContainer items={menu}>
|
||||
<IconButton
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
title='Settings'
|
||||
className='text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
||||
iconClassName='w-4 h-4'
|
||||
/>
|
||||
</DropdownMenuContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
|
|
@ -63,8 +63,7 @@ const ChatList: React.FC<IChatList> = ({ onClickChat, useWindowScroll = false, s
|
|||
<div className='px-2'>
|
||||
<ChatListItem chat={chat} onClick={onClickChat} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
)}
|
||||
components={{
|
||||
ScrollSeekPlaceholder: () => <PlaceholderChat />,
|
||||
Footer: () => hasNextPage ? <Spinner withText={false} /> : null,
|
||||
|
|
|
@ -8,7 +8,7 @@ import { Components, Virtuoso, VirtuosoHandle } from 'react-virtuoso';
|
|||
|
||||
import { openModal } from 'soapbox/actions/modals';
|
||||
import { initReport } from 'soapbox/actions/reports';
|
||||
import { Avatar, Button, Divider, HStack, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { Avatar, Button, Divider, HStack, Icon, IconButton, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import DropdownMenuContainer from 'soapbox/containers/dropdown-menu-container';
|
||||
import emojify from 'soapbox/features/emoji/emoji';
|
||||
import PlaceholderChatMessage from 'soapbox/features/placeholder/components/placeholder-chat-message';
|
||||
|
@ -208,7 +208,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
return emojify(formatted, emojiMap.toJS());
|
||||
};
|
||||
|
||||
const renderDivider = (key: React.Key, text: string) => <Divider key={key} text={text} textSize='sm' />;
|
||||
const renderDivider = (key: React.Key, text: string) => <Divider key={key} text={text} textSize='xs' />;
|
||||
|
||||
const handleCopyText = (chatMessage: ChatMessageEntity) => {
|
||||
if (navigator.clipboard) {
|
||||
|
@ -286,11 +286,14 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
})}
|
||||
data-testid='chat-message-menu'
|
||||
>
|
||||
<DropdownMenuContainer
|
||||
items={menu}
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
/>
|
||||
<DropdownMenuContainer items={menu}>
|
||||
<IconButton
|
||||
src={require('@tabler/icons/dots.svg')}
|
||||
title={intl.formatMessage(messages.more)}
|
||||
className='text-gray-600 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-500'
|
||||
iconClassName='w-4 h-4'
|
||||
/>
|
||||
</DropdownMenuContainer>
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -447,7 +450,7 @@ const ChatMessageList: React.FC<IChatMessageList> = ({ chat }) => {
|
|||
|
||||
return (
|
||||
<div className='h-full flex flex-col flex-grow space-y-6'>
|
||||
<div className='flex-grow flex flex-col justify-end pb-2'>
|
||||
<div className='flex-grow flex flex-col justify-end'>
|
||||
<Virtuoso
|
||||
ref={node}
|
||||
alignToBottom
|
||||
|
|
|
@ -183,11 +183,6 @@ const ChatPageMain = () => {
|
|||
label={intl.formatMessage(messages.autoDeleteLabel)}
|
||||
hint={intl.formatMessage(messages.autoDeleteHint)}
|
||||
/>
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.autoDelete2Minutes)}
|
||||
onSelect={() => handleUpdateChat(MessageExpirationValues.TWO_MINUTES)}
|
||||
isSelected={chat.message_expiration === MessageExpirationValues.TWO_MINUTES}
|
||||
/>
|
||||
<ListItem
|
||||
label={intl.formatMessage(messages.autoDelete7Days)}
|
||||
onSelect={() => handleUpdateChat(MessageExpirationValues.SEVEN)}
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
import React from 'react';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import AccountSearch from 'soapbox/components/account-search';
|
||||
import { CardTitle, HStack, IconButton, Stack, Text } from 'soapbox/components/ui';
|
||||
import { ChatKeys, useChats } from 'soapbox/queries/chats';
|
||||
import { queryClient } from 'soapbox/queries/client';
|
||||
import { CardTitle, HStack, IconButton, Stack } from 'soapbox/components/ui';
|
||||
|
||||
import ChatSearch from '../../chat-search/chat-search';
|
||||
|
||||
interface IChatPageNew {
|
||||
}
|
||||
|
@ -13,17 +11,10 @@ interface IChatPageNew {
|
|||
/** New message form to create a chat. */
|
||||
const ChatPageNew: React.FC<IChatPageNew> = () => {
|
||||
const history = useHistory();
|
||||
const { getOrCreateChatByAccountId } = useChats();
|
||||
|
||||
const handleAccountSelected = async (accountId: string) => {
|
||||
const { data } = await getOrCreateChatByAccountId(accountId);
|
||||
history.push(`/chats/${data.id}`);
|
||||
queryClient.invalidateQueries(ChatKeys.chatSearch());
|
||||
};
|
||||
|
||||
return (
|
||||
<Stack className='h-full'>
|
||||
<Stack className='flex-grow py-6 px-4 sm:p-6 space-y-4'>
|
||||
<Stack className='h-full space-y-4'>
|
||||
<Stack className='flex-grow pt-6 px-4 sm:px-6'>
|
||||
<HStack alignItems='center'>
|
||||
<IconButton
|
||||
src={require('@tabler/icons/arrow-left.svg')}
|
||||
|
@ -33,26 +24,9 @@ const ChatPageNew: React.FC<IChatPageNew> = () => {
|
|||
|
||||
<CardTitle title='New Message' />
|
||||
</HStack>
|
||||
|
||||
<HStack space={2} alignItems='center'>
|
||||
<Text>
|
||||
<FormattedMessage
|
||||
id='chats.new.to'
|
||||
defaultMessage='To:'
|
||||
/>
|
||||
</Text>
|
||||
|
||||
<AccountSearch
|
||||
onSelected={handleAccountSelected}
|
||||
placeholder='Type a name'
|
||||
theme='search'
|
||||
showButtons={false}
|
||||
autoFocus
|
||||
className='mb-0.5'
|
||||
followers
|
||||
/>
|
||||
</HStack>
|
||||
</Stack>
|
||||
|
||||
<ChatSearch isMainPage />
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -13,6 +13,7 @@ import ChatSearch from '../chat-search/chat-search';
|
|||
import EmptyResultsBlankslate from '../chat-search/empty-results-blankslate';
|
||||
import ChatPaneHeader from '../chat-widget/chat-pane-header';
|
||||
import ChatWindow from '../chat-widget/chat-window';
|
||||
import ChatSearchHeader from '../chat-widget/headers/chat-search-header';
|
||||
import { Pane } from '../ui';
|
||||
|
||||
import Blankslate from './blankslate';
|
||||
|
@ -86,7 +87,13 @@ const ChatPane = () => {
|
|||
}
|
||||
|
||||
if (screen === ChatWidgetScreens.SEARCH) {
|
||||
return <ChatSearch />;
|
||||
return (
|
||||
<Pane isOpen={isOpen} index={0} main>
|
||||
<ChatSearchHeader />
|
||||
|
||||
{isOpen ? <ChatSearch /> : null}
|
||||
</Pane>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
|
|
|
@ -29,6 +29,7 @@ const ChatSearchInput: React.FC<IChatSearchInput> = ({ value, onChange, onClear
|
|||
className='rounded-full'
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
outerClassName='mt-0'
|
||||
theme='search'
|
||||
append={
|
||||
<button onClick={onClear}>
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { VirtuosoMockContext } from 'react-virtuoso';
|
||||
|
||||
import { __stub } from 'soapbox/api';
|
||||
import { ChatProvider } from 'soapbox/contexts/chat-context';
|
||||
|
@ -8,36 +9,24 @@ import { render, screen, waitFor } from '../../../../../jest/test-helpers';
|
|||
import ChatSearch from '../chat-search';
|
||||
|
||||
const renderComponent = () => render(
|
||||
<ChatProvider>
|
||||
<ChatSearch />
|
||||
</ChatProvider>,
|
||||
<VirtuosoMockContext.Provider value={{ viewportHeight: 300, itemHeight: 100 }}>
|
||||
<ChatProvider>
|
||||
<ChatSearch />
|
||||
</ChatProvider>,
|
||||
</VirtuosoMockContext.Provider>,
|
||||
);
|
||||
|
||||
describe('<ChatSearch />', () => {
|
||||
it('renders correctly', () => {
|
||||
beforeEach(async() => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByTestId('pane-header')).toHaveTextContent('Messages');
|
||||
});
|
||||
|
||||
describe('when the pane is closed', () => {
|
||||
it('does not render the search input', () => {
|
||||
renderComponent();
|
||||
expect(screen.queryAllByTestId('search')).toHaveLength(0);
|
||||
});
|
||||
it('renders the search input', () => {
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when the pane is open', () => {
|
||||
beforeEach(async() => {
|
||||
renderComponent();
|
||||
await userEvent.click(screen.getByTestId('icon-button'));
|
||||
});
|
||||
|
||||
it('renders the search input', () => {
|
||||
expect(screen.getByTestId('search')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('when searching', () => {
|
||||
describe('when searching', () => {
|
||||
describe('with results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/accounts/search').reply(200, [{
|
||||
|
@ -51,8 +40,6 @@ describe('<ChatSearch />', () => {
|
|||
});
|
||||
|
||||
it('renders accounts', async() => {
|
||||
renderComponent();
|
||||
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByTestId('search'), 'ste');
|
||||
|
||||
|
@ -61,5 +48,22 @@ describe('<ChatSearch />', () => {
|
|||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('without results', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/accounts/search').reply(200, []);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders accounts', async() => {
|
||||
const user = userEvent.setup();
|
||||
await user.type(screen.getByTestId('search'), 'ste');
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('no-results')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
import { useMutation } from '@tanstack/react-query';
|
||||
import { AxiosError } from 'axios';
|
||||
import React, { useState } from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { HStack, Icon, Input, Stack, Text } from 'soapbox/components/ui';
|
||||
import { Icon, Input, Stack } from 'soapbox/components/ui';
|
||||
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
|
||||
import { useAppDispatch, useDebounce } from 'soapbox/hooks';
|
||||
import { useChats } from 'soapbox/queries/chats';
|
||||
|
@ -12,29 +12,30 @@ import { queryClient } from 'soapbox/queries/client';
|
|||
import useAccountSearch from 'soapbox/queries/search';
|
||||
|
||||
import { ChatKeys } from '../../../../queries/chats';
|
||||
import ChatPaneHeader from '../chat-widget/chat-pane-header';
|
||||
import { Pane } from '../ui';
|
||||
|
||||
import Blankslate from './blankslate';
|
||||
import EmptyResultsBlankslate from './empty-results-blankslate';
|
||||
import Results from './results';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'chat_search.title', defaultMessage: 'Messages' },
|
||||
});
|
||||
interface IChatSearch {
|
||||
isMainPage?: boolean
|
||||
}
|
||||
|
||||
const ChatSearch = (props: IChatSearch) => {
|
||||
const { isMainPage = false } = props;
|
||||
|
||||
const ChatSearch = () => {
|
||||
const debounce = useDebounce;
|
||||
const dispatch = useAppDispatch();
|
||||
const intl = useIntl();
|
||||
const history = useHistory();
|
||||
|
||||
const { isOpen, changeScreen, toggleChatPane } = useChatContext();
|
||||
const { changeScreen } = useChatContext();
|
||||
const { getOrCreateChatByAccountId } = useChats();
|
||||
|
||||
const [value, setValue] = useState<string>('');
|
||||
const debouncedValue = debounce(value as string, 300);
|
||||
|
||||
const { data: accounts, isFetching } = useAccountSearch(debouncedValue);
|
||||
const accountSearchResult = useAccountSearch(debouncedValue);
|
||||
const { data: accounts, isFetching } = accountSearchResult;
|
||||
|
||||
const hasSearchValue = debouncedValue && debouncedValue.length > 0;
|
||||
const hasSearchResults = (accounts || []).length > 0;
|
||||
|
@ -47,7 +48,12 @@ const ChatSearch = () => {
|
|||
dispatch(snackbar.error(data?.error));
|
||||
},
|
||||
onSuccess: (response) => {
|
||||
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
|
||||
if (isMainPage) {
|
||||
history.push(`/chats/${response.data.id}`);
|
||||
} else {
|
||||
changeScreen(ChatWidgetScreens.CHAT, response.data.id);
|
||||
}
|
||||
|
||||
queryClient.invalidateQueries(ChatKeys.chatSearch());
|
||||
},
|
||||
});
|
||||
|
@ -56,7 +62,7 @@ const ChatSearch = () => {
|
|||
if (hasSearchResults) {
|
||||
return (
|
||||
<Results
|
||||
accounts={accounts}
|
||||
accountSearchResult={accountSearchResult}
|
||||
onSelect={(id) => {
|
||||
handleClickOnSearchResult.mutate(id);
|
||||
clearValue();
|
||||
|
@ -77,62 +83,33 @@ const ChatSearch = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<Pane isOpen={isOpen} index={0} main>
|
||||
<ChatPaneHeader
|
||||
data-testid='pane-header'
|
||||
title={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<button
|
||||
onClick={() => {
|
||||
changeScreen(ChatWidgetScreens.INBOX);
|
||||
}}
|
||||
>
|
||||
<Stack space={4} className='flex-grow h-full'>
|
||||
<div className='px-4'>
|
||||
<Input
|
||||
data-testid='search'
|
||||
type='text'
|
||||
autoFocus
|
||||
placeholder='Type a name'
|
||||
value={value || ''}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
outerClassName='mt-0'
|
||||
theme='search'
|
||||
append={
|
||||
<button onClick={clearValue}>
|
||||
<Icon
|
||||
src={require('@tabler/icons/arrow-left.svg')}
|
||||
className='h-6 w-6 text-gray-600 dark:text-gray-400'
|
||||
src={hasSearchValue ? require('@tabler/icons/x.svg') : require('@tabler/icons/search.svg')}
|
||||
className='h-4 w-4 text-gray-700 dark:text-gray-600'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Text size='sm' weight='bold' truncate>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
</HStack>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
isToggleable={false}
|
||||
onToggle={toggleChatPane}
|
||||
/>
|
||||
|
||||
{isOpen ? (
|
||||
<Stack space={4} className='flex-grow h-full'>
|
||||
<div className='px-4'>
|
||||
<Input
|
||||
data-testid='search'
|
||||
type='text'
|
||||
autoFocus
|
||||
placeholder='Type a name'
|
||||
className='rounded-full'
|
||||
value={value || ''}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
theme='search'
|
||||
append={
|
||||
<button onClick={clearValue}>
|
||||
<Icon
|
||||
src={hasSearchValue ? require('@tabler/icons/x.svg') : require('@tabler/icons/search.svg')}
|
||||
className='h-4 w-4 text-gray-700 dark:text-gray-600'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Stack className='overflow-y-scroll flex-grow h-full' space={2}>
|
||||
{renderBody()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
) : null}
|
||||
</Pane>
|
||||
<Stack className='flex-grow'>
|
||||
{renderBody()}
|
||||
</Stack>
|
||||
</Stack>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ const EmptyResultsBlankslate = () => {
|
|||
|
||||
return (
|
||||
<Stack justifyContent='center' alignItems='center' space={2} className='h-full w-2/3 mx-auto'>
|
||||
<Text weight='bold' size='lg' align='center'>
|
||||
<Text weight='bold' size='lg' align='center' data-testid='no-results'>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
|
||||
|
|
|
@ -1,43 +1,80 @@
|
|||
import React from 'react';
|
||||
import classNames from 'clsx';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import { Virtuoso } from 'react-virtuoso';
|
||||
|
||||
import { Avatar, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||
import VerificationBadge from 'soapbox/components/verification-badge';
|
||||
import useAccountSearch from 'soapbox/queries/search';
|
||||
|
||||
interface IResults {
|
||||
accounts: {
|
||||
display_name: string
|
||||
acct: string
|
||||
id: string
|
||||
avatar: string
|
||||
verified: boolean
|
||||
}[]
|
||||
accountSearchResult: ReturnType<typeof useAccountSearch>
|
||||
onSelect(id: string): void
|
||||
}
|
||||
|
||||
const Results = ({ accounts, onSelect }: IResults) => (
|
||||
<>
|
||||
{(accounts || []).map((account: any) => (
|
||||
<button
|
||||
key={account.id}
|
||||
type='button'
|
||||
className='px-4 py-2 w-full flex flex-col hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
onClick={() => onSelect(account.id)}
|
||||
data-testid='account'
|
||||
>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Avatar src={account.avatar} size={40} />
|
||||
const Results = ({ accountSearchResult, onSelect }: IResults) => {
|
||||
const { data: accounts, isFetching, hasNextPage, fetchNextPage } = accountSearchResult;
|
||||
|
||||
<Stack alignItems='start'>
|
||||
<div className='flex items-center space-x-1 flex-grow'>
|
||||
<Text weight='bold' size='sm' truncate>{account.display_name}</Text>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
<Text size='sm' weight='medium' theme='muted' truncate>@{account.acct}</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
</button>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
const [isNearBottom, setNearBottom] = useState<boolean>(false);
|
||||
const [isNearTop, setNearTop] = useState<boolean>(true);
|
||||
|
||||
export default Results;
|
||||
const handleLoadMore = () => {
|
||||
if (hasNextPage && !isFetching) {
|
||||
fetchNextPage();
|
||||
}
|
||||
};
|
||||
|
||||
const renderAccount = useCallback((_index, account) => (
|
||||
<button
|
||||
key={account.id}
|
||||
type='button'
|
||||
className='px-2 py-3 w-full rounded-lg flex flex-col hover:bg-gray-100 dark:hover:bg-gray-800'
|
||||
onClick={() => onSelect(account.id)}
|
||||
data-testid='account'
|
||||
>
|
||||
<HStack alignItems='center' space={2}>
|
||||
<Avatar src={account.avatar} size={40} />
|
||||
|
||||
<Stack alignItems='start'>
|
||||
<div className='flex items-center space-x-1 flex-grow'>
|
||||
<Text weight='bold' size='sm' truncate>{account.display_name}</Text>
|
||||
{account.verified && <VerificationBadge />}
|
||||
</div>
|
||||
<Text size='sm' weight='medium' theme='muted' truncate>@{account.acct}</Text>
|
||||
</Stack>
|
||||
</HStack>
|
||||
</button>
|
||||
), []);
|
||||
|
||||
return (
|
||||
<div className='relative flex-grow'>
|
||||
<Virtuoso
|
||||
data={accounts}
|
||||
itemContent={(index, chat) => (
|
||||
<div className='px-2'>
|
||||
{renderAccount(index, chat)}
|
||||
</div>
|
||||
)}
|
||||
endReached={handleLoadMore}
|
||||
atTopStateChange={(atTop) => setNearTop(atTop)}
|
||||
atBottomStateChange={(atBottom) => setNearBottom(atBottom)}
|
||||
/>
|
||||
|
||||
<>
|
||||
<div
|
||||
className={classNames('inset-x-0 top-0 flex rounded-t-lg justify-center bg-gradient-to-b from-white to-transparent pb-12 pt-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearTop,
|
||||
'opacity-100': !isNearTop,
|
||||
})}
|
||||
/>
|
||||
<div
|
||||
className={classNames('inset-x-0 bottom-0 flex rounded-b-lg justify-center bg-gradient-to-t from-white to-transparent pt-12 pb-8 pointer-events-none dark:from-gray-900 absolute transition-opacity duration-500', {
|
||||
'opacity-0': isNearBottom,
|
||||
'opacity-100': !isNearBottom,
|
||||
})}
|
||||
/>
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Results;
|
||||
|
|
|
@ -37,11 +37,9 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
|
|||
data-testid='title'
|
||||
{...buttonProps}
|
||||
>
|
||||
{typeof title === 'string' ? (
|
||||
<Text weight='semibold'>
|
||||
{title}
|
||||
</Text>
|
||||
) : (title)}
|
||||
<Text weight='semibold' tag='div'>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{(typeof unreadCount !== 'undefined' && unreadCount > 0) && (
|
||||
<HStack alignItems='center' space={2}>
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
import React from 'react';
|
||||
import { defineMessages, useIntl } from 'react-intl';
|
||||
|
||||
import { HStack, Icon, Text } from 'soapbox/components/ui';
|
||||
import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context';
|
||||
|
||||
import ChatPaneHeader from '../chat-pane-header';
|
||||
|
||||
const messages = defineMessages({
|
||||
title: { id: 'chat_search.title', defaultMessage: 'Messages' },
|
||||
});
|
||||
|
||||
const ChatSearchHeader = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { changeScreen, isOpen, toggleChatPane } = useChatContext();
|
||||
|
||||
return (
|
||||
<ChatPaneHeader
|
||||
data-testid='pane-header'
|
||||
title={
|
||||
<HStack alignItems='center' space={2}>
|
||||
<button
|
||||
onClick={() => {
|
||||
changeScreen(ChatWidgetScreens.INBOX);
|
||||
}}
|
||||
>
|
||||
<Icon
|
||||
src={require('@tabler/icons/arrow-left.svg')}
|
||||
className='h-6 w-6 text-gray-600 dark:text-gray-400'
|
||||
/>
|
||||
</button>
|
||||
|
||||
<Text size='sm' weight='bold' truncate>
|
||||
{intl.formatMessage(messages.title)}
|
||||
</Text>
|
||||
</HStack>
|
||||
}
|
||||
isOpen={isOpen}
|
||||
isToggleable={false}
|
||||
onToggle={toggleChatPane}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatSearchHeader;
|
|
@ -244,7 +244,6 @@ const EditProfile: React.FC = () => {
|
|||
|
||||
const handleHideNetworkChange: React.ChangeEventHandler<HTMLInputElement> = e => {
|
||||
const hide = e.target.checked;
|
||||
|
||||
setData(prevData => {
|
||||
return {
|
||||
...prevData,
|
||||
|
@ -310,6 +309,26 @@ const EditProfile: React.FC = () => {
|
|||
return (
|
||||
<Column label={intl.formatMessage(messages.header)}>
|
||||
<Form onSubmit={handleSubmit}>
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<ProfilePreview account={previewAccount} />
|
||||
|
||||
<div className='space-y-4'>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.header_label' defaultMessage='Choose Background Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.header' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '1920x1080px' }} />}
|
||||
>
|
||||
<FileInput onChange={handleFileChange('header', 1920 * 1080)} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.avatar_label' defaultMessage='Choose Profile Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.avatar' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '400x400px' }} />}
|
||||
>
|
||||
<FileInput onChange={handleFileChange('avatar', 400 * 400)} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.display_name_label' defaultMessage='Display name' />}
|
||||
>
|
||||
|
@ -369,26 +388,6 @@ const EditProfile: React.FC = () => {
|
|||
/>
|
||||
</FormGroup>
|
||||
|
||||
<div className='grid grid-cols-1 md:grid-cols-2 gap-4'>
|
||||
<ProfilePreview account={previewAccount} />
|
||||
|
||||
<div className='space-y-4'>
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.avatar_label' defaultMessage='Choose Profile Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.avatar' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '400x400px' }} />}
|
||||
>
|
||||
<FileInput onChange={handleFileChange('avatar', 400 * 400)} />
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
labelText={<FormattedMessage id='edit_profile.fields.header_label' defaultMessage='Choose Background Picture' />}
|
||||
hintText={<FormattedMessage id='edit_profile.hints.header' defaultMessage='PNG, GIF or JPG. Will be downscaled to {size}' values={{ size: '1920x1080px' }} />}
|
||||
>
|
||||
<FileInput onChange={handleFileChange('header', 1920 * 1080)} />
|
||||
</FormGroup>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<List>
|
||||
{features.followRequests && (
|
||||
<ListItem
|
||||
|
@ -408,7 +407,7 @@ const EditProfile: React.FC = () => {
|
|||
hint={<FormattedMessage id='edit_profile.hints.hide_network' defaultMessage='Who you follow and who follows you will not be shown on your profile' />}
|
||||
>
|
||||
<Toggle
|
||||
checked={account ? hidesNetwork(account) : false}
|
||||
checked={account ? (data.hide_followers && data.hide_follows && data.hide_followers_count && data.hide_follows_count) : false}
|
||||
onChange={handleHideNetworkChange}
|
||||
/>
|
||||
</ListItem>
|
||||
|
|
|
@ -95,6 +95,7 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
|||
const username = account.username;
|
||||
|
||||
const handleHeaderClick: React.MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
dispatch(openModal('MEDIA', { media: ImmutableList([event.banner]) }));
|
||||
|
|
|
@ -34,14 +34,14 @@ const Events = () => {
|
|||
return (
|
||||
<Column label={intl.formatMessage(messages.title)}>
|
||||
<HStack className='mb-4' space={2} justifyContent='between'>
|
||||
<CardTitle title='Recent events' />
|
||||
<CardTitle title={<FormattedMessage id='events.recent_events' defaultMessage='Recent events' />} />
|
||||
<Button
|
||||
className='ml-auto'
|
||||
theme='primary'
|
||||
size='sm'
|
||||
onClick={onComposeEvent}
|
||||
>
|
||||
Create event
|
||||
<FormattedMessage id='events.create_event' defaultMessage='Create event' />
|
||||
</Button>
|
||||
</HStack>
|
||||
<CardBody className='mb-2'>
|
||||
|
@ -52,7 +52,7 @@ const Events = () => {
|
|||
/>
|
||||
</CardBody>
|
||||
<CardHeader>
|
||||
<CardTitle title='Joined events' />
|
||||
<CardTitle title={<FormattedMessage id='events.joined_events' defaultMessage='Joined events' />} />
|
||||
</CardHeader>
|
||||
<CardBody>
|
||||
<EventCarousel
|
||||
|
|
|
@ -21,7 +21,7 @@ jest.mock('../../../hooks/useDimensions', () => ({
|
|||
describe('<FeedCarousel />', () => {
|
||||
let store: any;
|
||||
|
||||
describe('with "feedUserFiltering" disabled', () => {
|
||||
describe('with "carousel" disabled', () => {
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
instance: {
|
||||
|
@ -42,7 +42,7 @@ describe('<FeedCarousel />', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('with "feedUserFiltering" enabled', () => {
|
||||
describe('with "carousel" enabled', () => {
|
||||
beforeEach(() => {
|
||||
store = {
|
||||
instance: {
|
||||
|
@ -61,11 +61,17 @@ describe('<FeedCarousel />', () => {
|
|||
__stub((mock) => {
|
||||
mock.onGet('/api/v1/truth/carousels/avatars')
|
||||
.reply(200, [
|
||||
{ account_id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg' },
|
||||
{ account_id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg' },
|
||||
{ account_id: '3', acct: 'c', account_avatar: 'https://example.com/some.jpg' },
|
||||
{ account_id: '4', acct: 'd', account_avatar: 'https://example.com/some.jpg' },
|
||||
{ account_id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg', seen: false },
|
||||
{ account_id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg', seen: false },
|
||||
{ account_id: '3', acct: 'c', account_avatar: 'https://example.com/some.jpg', seen: false },
|
||||
{ account_id: '4', acct: 'd', account_avatar: 'https://example.com/some.jpg', seen: false },
|
||||
]);
|
||||
|
||||
mock.onGet('/api/v1/accounts/1/statuses').reply(200, [], {
|
||||
link: '<https://example.com/api/v1/accounts/1/statuses?since_id=1>; rel=\'prev\'',
|
||||
});
|
||||
|
||||
mock.onPost('/api/v1/truth/carousels/avatars/seen').reply(200);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -74,6 +80,29 @@ describe('<FeedCarousel />', () => {
|
|||
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('feed-carousel')).toHaveLength(1);
|
||||
expect(screen.queryAllByTestId('carousel-item')).toHaveLength(4);
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle the "seen" state', async() => {
|
||||
render(<FeedCarousel />, undefined, store);
|
||||
|
||||
// Unseen
|
||||
await waitFor(() => {
|
||||
expect(screen.queryAllByTestId('carousel-item')).toHaveLength(4);
|
||||
});
|
||||
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-accent-500');
|
||||
|
||||
// Selected
|
||||
await userEvent.click(screen.getAllByTestId('carousel-item-avatar')[0]);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-primary-600');
|
||||
});
|
||||
|
||||
// Marked as seen, not selected
|
||||
await userEvent.click(screen.getAllByTestId('carousel-item-avatar')[0]);
|
||||
await waitFor(() => {
|
||||
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-transparent');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -4,15 +4,17 @@ import { FormattedMessage } from 'react-intl';
|
|||
|
||||
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
|
||||
import { useAppDispatch, useAppSelector, useDimensions } from 'soapbox/hooks';
|
||||
import useCarouselAvatars from 'soapbox/queries/carousels';
|
||||
import { Avatar, useCarouselAvatars, useMarkAsSeen } from 'soapbox/queries/carousels';
|
||||
|
||||
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
|
||||
import PlaceholderAvatar from '../placeholder/components/placeholder-avatar';
|
||||
|
||||
const CarouselItem = ({ avatar }: { avatar: any }) => {
|
||||
const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolean, onViewed: (account_id: string) => void }) => {
|
||||
const dispatch = useAppDispatch();
|
||||
|
||||
const selectedAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId);
|
||||
const markAsSeen = useMarkAsSeen();
|
||||
|
||||
const selectedAccountId = useAppSelector(state => state.timelines.getIn(['home', 'feedAccountId']) as string);
|
||||
const isSelected = avatar.account_id === selectedAccountId;
|
||||
|
||||
const [isFetching, setLoading] = useState<boolean>(false);
|
||||
|
@ -27,17 +29,25 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
|||
if (isSelected) {
|
||||
dispatch(replaceHomeTimeline(null, { maxId: null }, () => setLoading(false)));
|
||||
} else {
|
||||
onViewed(avatar.account_id);
|
||||
markAsSeen.mutate(avatar.account_id);
|
||||
dispatch(replaceHomeTimeline(avatar.account_id, { maxId: null }, () => setLoading(false)));
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div aria-disabled={isFetching} onClick={handleClick} className='cursor-pointer' role='filter-feed-by-user'>
|
||||
<div
|
||||
aria-disabled={isFetching}
|
||||
onClick={handleClick}
|
||||
className='cursor-pointer'
|
||||
role='filter-feed-by-user'
|
||||
data-testid='carousel-item'
|
||||
>
|
||||
<Stack className='w-16 h-auto' space={3}>
|
||||
<div className='block mx-auto relative w-14 h-14 rounded-full'>
|
||||
{isSelected && (
|
||||
<div className='absolute inset-0 bg-primary-600 bg-opacity-50 rounded-full flex items-center justify-center'>
|
||||
<Icon src={require('@tabler/icons/x.svg')} className='text-white h-6 w-6' />
|
||||
<Icon src={require('@tabler/icons/check.svg')} className='text-white h-6 w-6' />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
@ -45,10 +55,12 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
|
|||
src={avatar.account_avatar}
|
||||
className={classNames({
|
||||
'w-14 h-14 min-w-[56px] rounded-full ring-2 ring-offset-4 dark:ring-offset-primary-900': true,
|
||||
'ring-transparent': !isSelected,
|
||||
'ring-transparent': !isSelected && seen,
|
||||
'ring-primary-600': isSelected,
|
||||
'ring-accent-500': !seen && !isSelected,
|
||||
})}
|
||||
alt={avatar.acct}
|
||||
data-testid='carousel-item-avatar'
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -63,6 +75,7 @@ const FeedCarousel = () => {
|
|||
|
||||
const [cardRef, setCardRef, { width }] = useDimensions();
|
||||
|
||||
const [seenAccountIds, setSeenAccountIds] = useState<string[]>([]);
|
||||
const [pageSize, setPageSize] = useState<number>(0);
|
||||
const [currentPage, setCurrentPage] = useState<number>(1);
|
||||
|
||||
|
@ -75,6 +88,20 @@ const FeedCarousel = () => {
|
|||
const handleNextPage = () => setCurrentPage((prevPage) => prevPage + 1);
|
||||
const handlePrevPage = () => setCurrentPage((prevPage) => prevPage - 1);
|
||||
|
||||
const markAsSeen = (account_id: string) => {
|
||||
setSeenAccountIds((prev) => [...prev, account_id]);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (avatars.length > 0) {
|
||||
setSeenAccountIds(
|
||||
avatars
|
||||
.filter((avatar) => avatar.seen !== false)
|
||||
.map((avatar) => avatar.account_id),
|
||||
);
|
||||
}
|
||||
}, [avatars]);
|
||||
|
||||
useEffect(() => {
|
||||
if (width) {
|
||||
setPageSize(Math.round(width / widthPerAvatar));
|
||||
|
@ -130,6 +157,8 @@ const FeedCarousel = () => {
|
|||
<CarouselItem
|
||||
key={avatar.account_id}
|
||||
avatar={avatar}
|
||||
seen={seenAccountIds?.includes(avatar.account_id)}
|
||||
onViewed={markAsSeen}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
|
|
|
@ -7,6 +7,7 @@ import { patchMe } from 'soapbox/actions/me';
|
|||
import snackbar from 'soapbox/actions/snackbar';
|
||||
import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import { isDefaultAvatar } from 'soapbox/utils/accounts';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
@ -15,17 +16,6 @@ const messages = defineMessages({
|
|||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
/** Default avatar filenames from various backends */
|
||||
const DEFAULT_AVATARS = [
|
||||
'/avatars/original/missing.png', // Mastodon
|
||||
'/images/avi.png', // Pleroma
|
||||
];
|
||||
|
||||
/** Check if the avatar is a default avatar */
|
||||
const isDefaultAvatar = (url: string) => {
|
||||
return DEFAULT_AVATARS.every(avatar => url.endsWith(avatar));
|
||||
};
|
||||
|
||||
const AvatarSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const dispatch = useDispatch();
|
||||
const account = useOwnAccount();
|
||||
|
|
|
@ -8,6 +8,7 @@ import snackbar from 'soapbox/actions/snackbar';
|
|||
import StillImage from 'soapbox/components/still-image';
|
||||
import { Avatar, Button, Card, CardBody, Icon, Spinner, Stack, Text } from 'soapbox/components/ui';
|
||||
import { useOwnAccount } from 'soapbox/hooks';
|
||||
import { isDefaultHeader } from 'soapbox/utils/accounts';
|
||||
import resizeImage from 'soapbox/utils/resize-image';
|
||||
|
||||
import type { AxiosError } from 'axios';
|
||||
|
@ -17,17 +18,6 @@ const messages = defineMessages({
|
|||
error: { id: 'onboarding.error', defaultMessage: 'An unexpected error occurred. Please try again or skip this step.' },
|
||||
});
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
'/headers/original/missing.png', // Mastodon
|
||||
'/images/banner.png', // Pleroma
|
||||
];
|
||||
|
||||
/** Check if the avatar is a default avatar */
|
||||
const isDefaultHeader = (url: string) => {
|
||||
return DEFAULT_HEADERS.every(header => url.endsWith(header));
|
||||
};
|
||||
|
||||
const CoverPhotoSelectionStep = ({ onNext }: { onNext: () => void }) => {
|
||||
const intl = useIntl();
|
||||
const dispatch = useDispatch();
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import classNames from 'clsx';
|
||||
import { List as ImmutableList } from 'immutable';
|
||||
import React from 'react';
|
||||
import { FormattedMessage, FormattedNumber } from 'react-intl';
|
||||
import { FormattedMessage } from 'react-intl';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
|
||||
|
@ -9,6 +9,7 @@ import { openModal } from 'soapbox/actions/modals';
|
|||
import { HStack, Text, Emoji } from 'soapbox/components/ui';
|
||||
import { useAppSelector, useSoapboxConfig, useFeatures } from 'soapbox/hooks';
|
||||
import { reduceEmoji } from 'soapbox/utils/emoji-reacts';
|
||||
import { shortNumberFormat } from 'soapbox/utils/numbers';
|
||||
|
||||
import type { Status } from 'soapbox/types/entities';
|
||||
|
||||
|
@ -196,7 +197,7 @@ const InteractionCounter: React.FC<IInteractionCounter> = ({ count, onClick, chi
|
|||
>
|
||||
<HStack space={1} alignItems='center'>
|
||||
<Text theme='primary' weight='bold'>
|
||||
<FormattedNumber value={count} />
|
||||
{shortNumberFormat(count)}
|
||||
</Text>
|
||||
|
||||
<Text tag='div' theme='muted'>
|
||||
|
|
|
@ -56,7 +56,6 @@ const DirectMessageUpdates = () => {
|
|||
</defs>
|
||||
</svg>
|
||||
|
||||
|
||||
<Text weight='bold'>Privacy Policy Updates</Text>
|
||||
</HStack>
|
||||
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||
|
||||
|
||||
import { blockAccount } from 'soapbox/actions/accounts';
|
||||
import { submitReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
|
||||
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
||||
|
|
|
@ -272,8 +272,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => {
|
|||
<WrappedRoute path='/@:username/pins' component={PinnedStatuses} page={ProfilePage} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId' publicRoute exact page={StatusPage} component={Status} content={children} />
|
||||
<WrappedRoute path='/@:username/posts/:statusId/quotes' publicRoute page={StatusPage} component={Quotes} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId' publicRoute exact page={EventPage} component={EventInformation} content={children} />
|
||||
<WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />
|
||||
{features.events && <WrappedRoute path='/@:username/events/:statusId' publicRoute exact page={EventPage} component={EventInformation} content={children} />}
|
||||
{features.events && <WrappedRoute path='/@:username/events/:statusId/discussion' publicRoute exact page={EventPage} component={EventDiscussion} content={children} />}
|
||||
<Redirect from='/@:username/:statusId' to='/@:username/posts/:statusId' />
|
||||
|
||||
<WrappedRoute path='/groups' exact page={GroupsPage} component={Groups} content={children} />
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
"account.deactivated": "Disattivato",
|
||||
"account.direct": "Scrivi direttamente a @{name}",
|
||||
"account.domain_blocked": "Istanza nascosta",
|
||||
"account.edit_profile": "Modifica profilo",
|
||||
"account.edit_profile": "Modifica",
|
||||
"account.endorse": "Promuovi sul tuo profilo",
|
||||
"account.endorse.success": "Stai promuovendo @{acct} dal tuo profilo",
|
||||
"account.familiar_followers": "Seguito da {accounts}",
|
||||
|
@ -31,7 +31,7 @@
|
|||
"account.link_verified_on": "Link verificato il {date}",
|
||||
"account.locked_info": "Il livello di riservatezza è «chiuso». L'autore esamina manualmente ogni richiesta di follow.",
|
||||
"account.login": "Accedi",
|
||||
"account.media": "Media caricati",
|
||||
"account.media": "Media",
|
||||
"account.member_since": "Insieme a noi da {date}",
|
||||
"account.mention": "Menziona questo profilo",
|
||||
"account.mute": "Silenzia @{name}",
|
||||
|
@ -146,7 +146,7 @@
|
|||
"alert.unexpected.links.status": "Stato",
|
||||
"alert.unexpected.links.support": "Assistenza",
|
||||
"alert.unexpected.message": "Si è verificato un errore.",
|
||||
"alert.unexpected.return_home": "Torna alla pagina iniziale",
|
||||
"alert.unexpected.return_home": "Torna alla Home",
|
||||
"alert.unexpected.title": "Oops!",
|
||||
"aliases.account.add": "Crea un alias",
|
||||
"aliases.account_label": "Vecchio indirizzo:",
|
||||
|
@ -252,7 +252,7 @@
|
|||
"column.follow_requests": "Richieste di amicizia",
|
||||
"column.followers": "Followers",
|
||||
"column.following": "Following",
|
||||
"column.home": "Pagina iniziale",
|
||||
"column.home": "Home",
|
||||
"column.import_data": "Importazione dati",
|
||||
"column.info": "Informazioni server",
|
||||
"column.lists": "Liste",
|
||||
|
@ -277,8 +277,8 @@
|
|||
"column.soapbox_config": "Soapbox config",
|
||||
"column.test": "Test timeline",
|
||||
"column_back_button.label": "Indietro",
|
||||
"column_forbidden.body": "You do not have permission to access this page.",
|
||||
"column_forbidden.title": "Forbidden",
|
||||
"column_forbidden.body": "Non hai il permesso di accedere a questa pagina",
|
||||
"column_forbidden.title": "Accesso negato",
|
||||
"common.cancel": "Annulla",
|
||||
"common.error": "Something isn't right. Try reloading the page.",
|
||||
"compare_history_modal.header": "Edit history",
|
||||
|
@ -376,7 +376,7 @@
|
|||
"confirmations.unfollow.message": "Confermi che non vuoi più seguire {name}?",
|
||||
"crypto_donate.explanation_box.message": "{siteTitle} accetta donazioni in cripto valuta. Puoi spedire la tua donazione ad uno di questi indirizzi. Grazie per la solidarietà",
|
||||
"crypto_donate.explanation_box.title": "Spedire donazioni in cripto valuta",
|
||||
"crypto_donate_panel.actions.view": "Click to see {count} {count, plural, one {wallet} other {wallets}}",
|
||||
"crypto_donate_panel.actions.view": "Guarda {count} wallet",
|
||||
"crypto_donate_panel.heading": "Donazioni cripto",
|
||||
"crypto_donate_panel.intro.message": "{siteTitle} accetta donazioni in cripto valuta. Grazie per la tua solidarietà!",
|
||||
"datepicker.day": "Giorno",
|
||||
|
@ -411,13 +411,13 @@
|
|||
"edit_email.header": "Change Email",
|
||||
"edit_email.placeholder": "me@example.com",
|
||||
"edit_federation.followers_only": "Pubblica soltanto alle persone Follower",
|
||||
"edit_federation.force_nsfw": "Force attachments to be marked sensitive",
|
||||
"edit_federation.media_removal": "Strip media",
|
||||
"edit_federation.reject": "Reject all activities",
|
||||
"edit_federation.force_nsfw": "Obbliga la protezione degli allegati (NSFW)",
|
||||
"edit_federation.media_removal": "Rimuovi i media",
|
||||
"edit_federation.reject": "Rifiuta tutte le attività",
|
||||
"edit_federation.save": "Salva",
|
||||
"edit_federation.success": "{host} federation was updated",
|
||||
"edit_federation.success": "Modalità di federazione di {host}, aggiornata",
|
||||
"edit_federation.unlisted": "Forza le pubblicazioni non in elenco",
|
||||
"edit_password.header": "Change Password",
|
||||
"edit_password.header": "Modifica la password",
|
||||
"edit_profile.error": "Impossibile salvare le modifiche",
|
||||
"edit_profile.fields.accepts_email_list_label": "Autorizzo gli amministratori al trattamento dei dati per l'invio di comunicazioni ",
|
||||
"edit_profile.fields.avatar_label": "Emblema o immagine",
|
||||
|
@ -569,7 +569,7 @@
|
|||
"hashtag.column_header.tag_mode.all": "e {additional}",
|
||||
"hashtag.column_header.tag_mode.any": "o {additional}",
|
||||
"hashtag.column_header.tag_mode.none": "senza {additional}",
|
||||
"header.home.label": "Pagina iniziale",
|
||||
"header.home.label": "Home",
|
||||
"header.login.forgot_password": "Password dimenticata?",
|
||||
"header.login.label": "Accedi",
|
||||
"header.login.password.label": "Password",
|
||||
|
@ -609,14 +609,14 @@
|
|||
"keyboard_shortcuts.favourite": "per segnare come preferita",
|
||||
"keyboard_shortcuts.favourites": "per aprire l'elenco di pubblicazioni preferite",
|
||||
"keyboard_shortcuts.heading": "Tasti di scelta rapida",
|
||||
"keyboard_shortcuts.home": "per aprire la pagina iniziale",
|
||||
"keyboard_shortcuts.home": "per aprire la Home",
|
||||
"keyboard_shortcuts.hotkey": "Tasto di scelta rapida",
|
||||
"keyboard_shortcuts.legend": "per mostrare questa spiegazione",
|
||||
"keyboard_shortcuts.mention": "per menzionare l'autore",
|
||||
"keyboard_shortcuts.muted": "per aprire l'elenco delle persone silenziate",
|
||||
"keyboard_shortcuts.my_profile": "per aprire il tuo profilo",
|
||||
"keyboard_shortcuts.notifications": "per aprire la colonna delle notifiche",
|
||||
"keyboard_shortcuts.open_media": "to open media",
|
||||
"keyboard_shortcuts.open_media": "per aprire i media",
|
||||
"keyboard_shortcuts.pinned": "per aprire l'elenco pubblicazioni fissate in cima",
|
||||
"keyboard_shortcuts.profile": "per aprire il profilo dell'autore",
|
||||
"keyboard_shortcuts.react": "per reagire",
|
||||
|
@ -624,7 +624,7 @@
|
|||
"keyboard_shortcuts.requests": "per aprire l'elenco delle richieste di seguirti",
|
||||
"keyboard_shortcuts.search": "per spostare il focus sulla ricerca",
|
||||
"keyboard_shortcuts.toggle_hidden": "per mostrare/nascondere il testo dei CW",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere media",
|
||||
"keyboard_shortcuts.toggle_sensitivity": "mostrare/nascondere i media",
|
||||
"keyboard_shortcuts.toot": "per iniziare a scrivere un toot completamente nuovo",
|
||||
"keyboard_shortcuts.unfocus": "per uscire dall'area di composizione o dalla ricerca",
|
||||
"keyboard_shortcuts.up": "per spostarsi in alto nella lista",
|
||||
|
@ -663,7 +663,7 @@
|
|||
"login_external.errors.network_fail": "Connection failed. Is a browser extension blocking it?",
|
||||
"login_form.header": "Accedi",
|
||||
"media_panel.empty_message": "Non ha caricato niente",
|
||||
"media_panel.title": "Media caricati",
|
||||
"media_panel.title": "Media",
|
||||
"mfa.confirm.success_message": "Autenticazione a due fattori, attivata!",
|
||||
"mfa.disable.success_message": "Autenticazione a due fattori, disattivata!",
|
||||
"mfa.disabled": "Disattivo",
|
||||
|
@ -713,7 +713,7 @@
|
|||
"navigation.dashboard": "Cruscotto",
|
||||
"navigation.developers": "Sviluppatori",
|
||||
"navigation.direct_messages": "Messaggi diretti",
|
||||
"navigation.home": "Pagina iniziale",
|
||||
"navigation.home": "Home",
|
||||
"navigation.notifications": "Notifiche",
|
||||
"navigation.search": "Cerca",
|
||||
"navigation_bar.account_aliases": "Account aliases",
|
||||
|
@ -760,7 +760,7 @@
|
|||
"notifications.filter.polls": "Risultati del sondaggio",
|
||||
"notifications.filter.statuses": "Updates from people you follow",
|
||||
"notifications.group": "{count} notifiche",
|
||||
"notifications.queue_label": "Click to see {count} new {count, plural, one {notification} other {notifications}}",
|
||||
"notifications.queue_label": "Ci sono {count} {count, plural, one {notifica} other {notifiche}}",
|
||||
"oauth_consumer.tooltip": "Sign in with {provider}",
|
||||
"oauth_consumers.title": "Other ways to sign in",
|
||||
"onboarding.avatar.subtitle": "Scegline una accattivante, o divertente!",
|
||||
|
@ -826,14 +826,14 @@
|
|||
"preferences.fields.expand_spoilers_label": "Espandi automaticamente le pubblicazioni segnate «con avvertimento» (CW)",
|
||||
"preferences.fields.language_label": "Lingua",
|
||||
"preferences.fields.media_display_label": "Visualizzazione dei media",
|
||||
"preferences.fields.missing_description_modal_label": "Show confirmation dialog before sending a post without media descriptions",
|
||||
"preferences.fields.missing_description_modal_label": "Richiedi conferma per pubblicare allegati senza descrizione",
|
||||
"preferences.fields.privacy_label": "Livello di privacy predefinito",
|
||||
"preferences.fields.reduce_motion_label": "Reduce motion in animations",
|
||||
"preferences.fields.system_font_label": "Use system's default font",
|
||||
"preferences.fields.system_font_label": "Sfrutta i caratteri del sistema operativo",
|
||||
"preferences.fields.theme": "Tema grafico",
|
||||
"preferences.fields.underline_links_label": "Always underline links in posts",
|
||||
"preferences.fields.unfollow_modal_label": "Show confirmation dialog before unfollowing someone",
|
||||
"preferences.hints.demetricator": "Decrease social media anxiety by hiding all numbers from the site.",
|
||||
"preferences.fields.underline_links_label": "Link sempre sottolineati",
|
||||
"preferences.fields.unfollow_modal_label": "Richiedi conferma per smettere di seguire",
|
||||
"preferences.hints.demetricator": "Diminuisci l'ansia, nascondendo tutti i conteggi",
|
||||
"preferences.hints.feed": "Nella timeline personale",
|
||||
"preferences.notifications.advanced": "Show all notification categories",
|
||||
"preferences.options.content_type_markdown": "Markdown",
|
||||
|
@ -850,13 +850,13 @@
|
|||
"privacy.public.short": "Pubblico",
|
||||
"privacy.unlisted.long": "Pubblico ma non visibile nella timeline pubblica",
|
||||
"privacy.unlisted.short": "Non elencato",
|
||||
"profile_dropdown.add_account": "Cambia profilo (esistente)",
|
||||
"profile_dropdown.add_account": "Aggiungi profilo (esistente)",
|
||||
"profile_dropdown.logout": "Disconnetti @{acct}",
|
||||
"profile_dropdown.switch_account": "Switch accounts",
|
||||
"profile_dropdown.switch_account": "Cambia profilo",
|
||||
"profile_dropdown.theme": "Tema",
|
||||
"profile_fields_panel.title": "Altre informazioni",
|
||||
"reactions.all": "Tutte",
|
||||
"regeneration_indicator.label": "Attendere prego …",
|
||||
"regeneration_indicator.label": "Attendere prego…",
|
||||
"regeneration_indicator.sublabel": "Stiamo preparando il tuo home feed!",
|
||||
"register_invite.lead": "Completa questo modulo per creare il tuo profilo ed essere dei nostri!",
|
||||
"register_invite.title": "Hai ricevuto un invito su {siteTitle}, iscriviti!",
|
||||
|
@ -1142,7 +1142,7 @@
|
|||
"tabs_bar.chats": "Chat",
|
||||
"tabs_bar.dashboard": "Cruscotto",
|
||||
"tabs_bar.fediverse": "Timeline Federata",
|
||||
"tabs_bar.home": "Pagina iniziale",
|
||||
"tabs_bar.home": "Home",
|
||||
"tabs_bar.local": "Timeline Locale",
|
||||
"tabs_bar.more": "Altro",
|
||||
"tabs_bar.notifications": "Notifiche",
|
||||
|
|
|
@ -25,7 +25,7 @@
|
|||
"account.follows": "Obserwowani",
|
||||
"account.follows.empty": "Ten użytkownik nie obserwuje jeszcze nikogo.",
|
||||
"account.follows_you": "Obserwuje Cię",
|
||||
"account.header.alt": "Profile header",
|
||||
"account.header.alt": "Nagłówek profilu",
|
||||
"account.hide_reblogs": "Ukryj podbicia od @{name}",
|
||||
"account.last_status": "Ostatnia aktywność",
|
||||
"account.link_verified_on": "Własność tego odnośnika została potwierdzona {date}",
|
||||
|
@ -136,7 +136,7 @@
|
|||
"admin_nav.awaiting_approval": "Oczekujące zgłoszenia",
|
||||
"admin_nav.dashboard": "Panel administracyjny",
|
||||
"admin_nav.reports": "Zgłoszenia",
|
||||
"age_verification.body": "{siteTitle} requires users to be at least {ageMinimum} years old to access its platform. Anyone under the age of {ageMinimum} years old cannot access this platform.",
|
||||
"age_verification.body": "{siteTitle} wymaga, by użytkownicy mieli przynajmniej {ageMinimum} lat aby korzystać z platformy. Osoby poniżej {ageMinimum} roku życia nie mogą korzystać z tej platformy.",
|
||||
"age_verification.fail": "Musisz mieć przynajmniej {ageMinimum, plural, one {# rok} few {# lata} many {# lat} other {# lat}}.",
|
||||
"age_verification.header": "Wprowadź datę urodzenia",
|
||||
"alert.unexpected.body": "Przepraszamy za niedogodności. Jeżeli problem nie ustanie, skontaktuj się z naszym wsparciem technicznym. Możesz też spróbować {clearCookies} (zostaniesz wylogowany(-a)).",
|
||||
|
@ -186,12 +186,75 @@
|
|||
"bundle_modal_error.message": "Coś poszło nie tak podczas ładowania tego składnika.",
|
||||
"bundle_modal_error.retry": "Spróbuj ponownie",
|
||||
"card.back.label": "Wstecz",
|
||||
"chat.actions.send": "Wyślij",
|
||||
"chat.failed_to_send": "Nie udało się wysłać",
|
||||
"chat.input.placeholder": "Wprowadź wiadomość",
|
||||
"chat.page_settings.accepting_messages.label": "Pozwól innym rozpoczynać rozmowy z Tobą",
|
||||
"chat.page_settings.play_sounds.label": "Odtwarzaj dźwięk gdy dostaniesz wiadomość",
|
||||
"chat.page_settings.preferences": "Preferencje",
|
||||
"chat.page_settings.privacy": "Prywatność",
|
||||
"chat.page_settings.submit": "Zapisz",
|
||||
"chat.page_settings.title": "Ustawienia wiadomości",
|
||||
"chat.retry": "Spróbować ponownie?",
|
||||
"chat.welcome.accepting_messages.label": "Pozwól innym rozpoczynać rozmowy z Tobą",
|
||||
"chat.welcome.notice": "Możesz zmienić te ustawienia później.",
|
||||
"chat.welcome.submit": "Zapisz i kontynuuj",
|
||||
"chat.welcome.subtitle": "Wymieniaj się bezpośrednimi wiadomościami z innymi.",
|
||||
"chat.welcome.title": "Witaj w {br} Czatach!",
|
||||
"chat_composer.unblock": "Odblokuj",
|
||||
"chat_list_item.blocked_you": "Ten użytkownik Cię zablokował",
|
||||
"chat_list_item.blocking": "Zablokowałeś(-aś) tego użytkownika",
|
||||
"chat_message_list.blocked": "Blokujesz tego użytkownika",
|
||||
"chat_message_list.blockedBy": "Jesteś zablokowany(-a)",
|
||||
"chat_message_list.network_failure.action": "Spróbuj ponownie",
|
||||
"chat_message_list.network_failure.subtitle": "Wystąpił błąd sieci.",
|
||||
"chat_message_list.network_failure.title": "O nie!",
|
||||
"chat_message_list_intro.actions.accept": "Akceptuj",
|
||||
"chat_message_list_intro.actions.leave_chat": "Opuść czat",
|
||||
"chat_message_list_intro.actions.message_lifespan": "Wiadomości starsze niż {day} dni są usuwane.",
|
||||
"chat_message_list_intro.actions.report": "Zgłoś",
|
||||
"chat_message_list_intro.intro": "chce rozpocząć z Tobą rozmowę",
|
||||
"chat_message_list_intro.leave_chat.confirm": "Opuść czat",
|
||||
"chat_message_list_intro.leave_chat.heading": "Opuść czat",
|
||||
"chat_message_list_intro.leave_chat.message": "Czy na pewno chcesz opuścić ten czat? Wiadomości zostaną usunięte dla Ciebie, a rozmowa zniknie ze skrzynki.",
|
||||
"chat_box.actions.send": "Wyślij",
|
||||
"chat_box.input.placeholder": "Wyślij wiadomość…",
|
||||
"chat_panels.main_window.empty": "Nie znaleziono rozmów. Aby zacząć rozmowę, odwiedź profil użytkownika.",
|
||||
"chat_panels.main_window.title": "Rozmowy",
|
||||
"chat_window.close": "Close chat",
|
||||
"chat_search.blankslate.body": "Znajdź kogoś, z kim chcesz rozpocząć rozmowę.",
|
||||
"chat_search.blankslate.title": "Rozpocznij czat",
|
||||
"chat_search.empty_results_blankslate.action": "Napisz do kogoś",
|
||||
"chat_search.empty_results_blankslate.body": "Szukaj dla innej nazwy.",
|
||||
"chat_search.empty_results_blankslate.title": "Brak dopasowań",
|
||||
"chat_search.title": "Wiadomości",
|
||||
"chat_settings.auto_delete.14days": "14 dni",
|
||||
"chat_settings.auto_delete.2minutes": "2 minuty",
|
||||
"chat_settings.auto_delete.30days": "30 dni",
|
||||
"chat_settings.auto_delete.7days": "7 dni",
|
||||
"chat_settings.auto_delete.90days": "90 dni",
|
||||
"chat_settings.auto_delete.days": "{day} dni",
|
||||
"chat_settings.auto_delete.hint": "Wysyłane wiadomości będą automatycznie usuwane po wybranym czasie",
|
||||
"chat_settings.auto_delete.label": "Automatycznie usuwaj wiadomości",
|
||||
"chat_settings.block.confirm": "Blokuj",
|
||||
"chat_settings.block.heading": "Blokuj @{acct}",
|
||||
"chat_settings.block.message": "Po zablokowaniu profilu, ta osoba nie będzie mogła wysyłać Ci wiadomości i przeglądać Twoich treści. Możesz odblokować ją później.",
|
||||
"chat_settings.leave.confirm": "Opuść czat",
|
||||
"chat_settings.leave.heading": "Opuść czat",
|
||||
"chat_settings.leave.message": "Czy na pewno chcesz opuścić ten czat? Wiadomości zostaną usunięte dla Ciebie, a rozmowa zniknie ze skrzynki.",
|
||||
"chat_settings.options.block_user": "Blokuj @{acct}",
|
||||
"chat_settings.options.leave_chat": "Opuść czat",
|
||||
"chat_settings.options.report_user": "Zgłoś @{acct}",
|
||||
"chat_settings.options.unblock_user": "Odblokuj @{acct}",
|
||||
"chat_settings.title": "Szczegóły czatu",
|
||||
"chat_settings.unblock.confirm": "Odblokuj",
|
||||
"chat_settings.unblock.heading": "Odblokuj @{acct}",
|
||||
"chat_settings.unblock.message": "Po odblokowaniu, ta osoba może wysyłać Ci wiadomości i przeglądać treści z Twojego profilu.",
|
||||
"chat_window.auto_delete_label": "Automatycznie usuwaj po {day} dni",
|
||||
"chat_window.auto_delete_tooltip": "Wiadomości z czatu będą usuwane po {day} dni od wysłania.",
|
||||
"chat_window.close": "Zamknij czat",
|
||||
"chats.actions.copy": "Kopiuj",
|
||||
"chats.actions.delete": "Usuń wiadomość",
|
||||
"chats.actions.deleteForMe": "Usuń dla mnie",
|
||||
"chats.actions.more": "Więcej",
|
||||
"chats.actions.report": "Zgłoś użytkownika",
|
||||
"chats.attachment": "Załącznik",
|
||||
|
@ -199,6 +262,12 @@
|
|||
"chats.audio_toggle_off": "Wyłączono dźwięk powiadomień",
|
||||
"chats.audio_toggle_on": "Włączono dźwięk powiadomień",
|
||||
"chats.dividers.today": "Dzisiaj",
|
||||
"chats.main.blankslate.new_chat": "Napisz do kogoś",
|
||||
"chats.main.blankslate.subtitle": "Znajdź kogoś, z kim chcesz rozpocząć rozmowę",
|
||||
"chats.main.blankslate.title": "Brak wiadomości",
|
||||
"chats.main.blankslate_with_chats.subtitle": "Wybierz jeden z rozpoczętych czatów lub utwórz nową wiadomość.",
|
||||
"chats.main.blankslate_with_chats.title": "Wybierz czat",
|
||||
"chats.new.to": "Do:",
|
||||
"chats.search_placeholder": "Rozpocznij rozmowę z…",
|
||||
"column.admin.awaiting_approval": "Oczekujące na przyjęcie",
|
||||
"column.admin.dashboard": "Panel administracyjny",
|
||||
|
@ -226,6 +295,9 @@
|
|||
"column.directory": "Przeglądaj profile",
|
||||
"column.domain_blocks": "Ukryte domeny",
|
||||
"column.edit_profile": "Edytuj profil",
|
||||
"column.event_map": "Miejsce wydarzenia",
|
||||
"column.event_participants": "Uczestnicy wydarzenia",
|
||||
"column.events": "Wydarzenia",
|
||||
"column.export_data": "Eksportuj dane",
|
||||
"column.familiar_followers": "Obserwujący {name} których znasz",
|
||||
"column.favourited_statuses": "Polubione wpisy",
|
||||
|
@ -268,6 +340,7 @@
|
|||
"column.pins": "Przypięte wpisy",
|
||||
"column.preferences": "Preferencje",
|
||||
"column.public": "Globalna oś czasu",
|
||||
"column.quotes": "Cytaty wpisu",
|
||||
"column.reactions": "Reakcje",
|
||||
"column.reblogs": "Podbicia",
|
||||
"column.remote": "Sfederowana oś czasu",
|
||||
|
@ -286,7 +359,33 @@
|
|||
"compose.edit_success": "Twój wpis został zedytowany",
|
||||
"compose.invalid_schedule": "Musisz zaplanować wpis przynajmniej 5 minut wcześniej.",
|
||||
"compose.submit_success": "Twój wpis został wysłany",
|
||||
"compose_event.create": "Utwórz",
|
||||
"compose_event.edit_success": "Wydarzenie zostało zedytowane",
|
||||
"compose_event.fields.approval_required": "Chcę ręcznie zatwierdzać prośby o dołączenie",
|
||||
"compose_event.fields.banner_label": "Baner wydarzenia",
|
||||
"compose_event.fields.description_hint": "Obsługiwana jest składnia Markdown",
|
||||
"compose_event.fields.description_label": "Opis wydarzenia",
|
||||
"compose_event.fields.description_placeholder": "Opis",
|
||||
"compose_event.fields.end_time_label": "Data zakończenia wydarzenia",
|
||||
"compose_event.fields.end_time_placeholder": "Wydarzenie kończy się…",
|
||||
"compose_event.fields.has_end_time": "Wydarzenie ma datę zakończenia",
|
||||
"compose_event.fields.location_label": "Miejsce wydarzenia",
|
||||
"compose_event.fields.name_label": "Nazwa wydarzenia",
|
||||
"compose_event.fields.name_placeholder": "Nazwa",
|
||||
"compose_event.fields.start_time_label": "Data rozpoczęcia wydarzenia",
|
||||
"compose_event.fields.start_time_placeholder": "Wydarzenie rozpoczyna się…",
|
||||
"compose_event.participation_requests.authorize": "Przyjmij",
|
||||
"compose_event.participation_requests.authorize_success": "Przyjęto użytkownika",
|
||||
"compose_event.participation_requests.reject": "Odrzuć",
|
||||
"compose_event.participation_requests.reject_success": "Odrzucono użytkownika",
|
||||
"compose_event.reset_location": "Resetuj miejsce",
|
||||
"compose_event.submit_success": "Wydarzenie zostało utworzone",
|
||||
"compose_event.tabs.edit": "Edytuj szczegóły",
|
||||
"compose_event.tabs.pending": "Zarządzaj prośbami",
|
||||
"compose_event.update": "Aktualizuj",
|
||||
"compose_event.upload_banner": "Wyślij baner wydarzenia",
|
||||
"compose_form.direct_message_warning": "Ten wpis będzie widoczny tylko dla wszystkich wspomnianych użytkowników.",
|
||||
"compose_form.event_placeholder": "Opublikuj dla tego wydarzenia",
|
||||
"compose_form.hashtag_warning": "Ten wpis nie będzie widoczny pod podanymi hashtagami, ponieważ jest oznaczony jako niewidoczny. Tylko publiczne wpisy mogą zostać znalezione z użyciem hashtagów.",
|
||||
"compose_form.lock_disclaimer": "Twoje konto nie jest {locked}. Każdy, kto Cię obserwuje, może wyświetlać Twoje wpisy przeznaczone tylko dla obserwujących.",
|
||||
"compose_form.lock_disclaimer.lock": "zablokowane",
|
||||
|
@ -342,15 +441,22 @@
|
|||
"confirmations.cancel_editing.confirm": "Anuluj edycję",
|
||||
"confirmations.cancel_editing.heading": "Anuluj edycję wpisu",
|
||||
"confirmations.cancel_editing.message": "Czy na pewno chcesz anulować edytowanie wpisu? Niezapisane zmiany zostaną utracone.",
|
||||
"confirmations.cancel_event_editing.heading": "Anuluj edycję wydarzenia",
|
||||
"confirmations.cancel_event_editing.message": "Czy na pewno chcesz anulować edytowanie wydarzenia? Niezapisane zmiany zostaną utracone.",
|
||||
"confirmations.delete.confirm": "Usuń",
|
||||
"confirmations.delete.heading": "Usuń wpis",
|
||||
"confirmations.delete.message": "Czy na pewno chcesz usunąć ten wpis?",
|
||||
"confirmations.delete_event.confirm": "Usuń",
|
||||
"confirmations.delete_event.heading": "Usuń wydarzenie",
|
||||
"confirmations.delete_event.message": "Czy na pewno chcesz usunąć to wydarzenie?",
|
||||
"confirmations.delete_list.confirm": "Usuń",
|
||||
"confirmations.delete_list.heading": "Usuń listę",
|
||||
"confirmations.delete_list.message": "Czy na pewno chcesz bezpowrotnie usunąć tą listę?",
|
||||
"confirmations.domain_block.confirm": "Ukryj wszysyko z domeny",
|
||||
"confirmations.domain_block.heading": "Zablokuj {domain}",
|
||||
"confirmations.domain_block.message": "Czy na pewno chcesz zablokować całą domenę {domain}? Zwykle lepszym rozwiązaniem jest blokada lub wyciszenie kilku użytkowników.",
|
||||
"confirmations.leave_event.confirm": "Opuść wydarzenie",
|
||||
"confirmations.leave_event.message": "Jeśli będziesz chciał(a) dołączyć do wydarzenia jeszcze raz, prośba będzie musiała zostać ponownie zatwierdzona. Czy chcesz kontynuować?",
|
||||
"confirmations.mute.confirm": "Wycisz",
|
||||
"confirmations.mute.heading": "Wycisz @{name}",
|
||||
"confirmations.mute.message": "Czy na pewno chcesz wyciszyć {name}?",
|
||||
|
@ -401,7 +507,7 @@
|
|||
"developers.navigation.service_worker_label": "Service Worker",
|
||||
"developers.navigation.settings_store_label": "Settings store",
|
||||
"developers.navigation.test_timeline_label": "Testowa oś czasu",
|
||||
"developers.settings_store.advanced": "Advanced settings",
|
||||
"developers.settings_store.advanced": "Zaawansowane ustawienia",
|
||||
"developers.settings_store.hint": "Możesz tu bezpośrednio edytować swoje ustawienia. UWAŻAJ! Edytowanie tej sekcji może uszkodzić Twoje konto, co może zostać naprawione tylko przez API.",
|
||||
"direct.search_placeholder": "Wyślij wiadomość do…",
|
||||
"directory.federated": "Z całego znanego Fediwersum",
|
||||
|
@ -466,12 +572,12 @@
|
|||
"email_passthru.token_expired.heading": "Token wygasł",
|
||||
"email_passthru.token_not_found.body": "Nie odnaleziono tokenu e-mail. Poproś o nowe potwierdzenie adresu e-mail z {bold}, z którego został wysłany ten e-mail.",
|
||||
"email_passthru.token_not_found.heading": "Nieprawidłowy token",
|
||||
"email_verification.email.label": "E-mail address",
|
||||
"email_verification.email.label": "Adres e-mail",
|
||||
"email_verification.fail": "Nie udało się zażądać weryfikacji e-mail",
|
||||
"email_verification.header": "Wprowadź adres e-mail",
|
||||
"email_verification.success": "Pomyślnie wysłano weryfikacyjny e-mail.",
|
||||
"email_verification.taken": "jest zajęty",
|
||||
"email_verifilcation.exists": "This email has already been taken.",
|
||||
"email_verifilcation.exists": "Ten adres e-mail jest już zajęty.",
|
||||
"embed.instructions": "Osadź ten wpis na swojej stronie wklejając poniższy kod.",
|
||||
"emoji_button.activity": "Aktywność",
|
||||
"emoji_button.custom": "Niestandardowe",
|
||||
|
@ -498,6 +604,8 @@
|
|||
"empty_column.community": "Lokalna oś czasu jest pusta. Napisz coś publicznie, aby zagaić!",
|
||||
"empty_column.direct": "Nie masz żadnych wiadomości bezpośrednich. Kiedy dostaniesz lub wyślesz jakąś, pojawi się ona tutaj.",
|
||||
"empty_column.domain_blocks": "Brak ukrytych domen.",
|
||||
"empty_column.event_participant_requests": "Brak oczekujących próśb o dołączenie.",
|
||||
"empty_column.event_participants": "Nikt jeszcze nie dołączył do tego wydarzenia. Gdy ktoś dołączy, pojawi się tutaj.",
|
||||
"empty_column.favourited_statuses": "Nie polubiłeś(-aś) żadnego wpisu. Kiedy to zrobisz, pojawi się on tutaj.",
|
||||
"empty_column.favourites": "Nikt nie dodał tego wpisu do ulubionych. Gdy ktoś to zrobi, pojawi się tutaj.",
|
||||
"empty_column.filters": "Nie wyciszyłeś(-aś) jeszcze żadnego słowa.",
|
||||
|
@ -514,12 +622,33 @@
|
|||
"empty_column.notifications": "Nie masz żadnych powiadomień. Rozpocznij interakcje z innymi użytkownikami.",
|
||||
"empty_column.notifications_filtered": "Nie masz żadnych powiadomień o tej kategorii.",
|
||||
"empty_column.public": "Tu nic nie ma! Napisz coś publicznie, lub dodaj ludzi z innych serwerów, aby to wyświetlić",
|
||||
"empty_column.quotes": "Ten wpis nie został jeszcze zacytowany.",
|
||||
"empty_column.remote": "Tu nic nie ma! Zaobserwuj użytkowników {instance}, aby wypełnić tę oś.",
|
||||
"empty_column.scheduled_statuses": "Nie masz żadnych zaplanowanych wpisów. Kiedy dodasz jakiś, pojawi się on tutaj.",
|
||||
"empty_column.search.accounts": "Brak wyników wyszukiwania osób dla „{term}”",
|
||||
"empty_column.search.hashtags": "Brak wyników wyszukiwania hashtagów dla „{term}”",
|
||||
"empty_column.search.statuses": "Brak wyników wyszukiwania wpisów dla „{term}”",
|
||||
"empty_column.test": "Testowa oś czasu jest pusta.",
|
||||
"event.banner": "Baner wydarzenia",
|
||||
"event.copy": "Skopiuj link do wydarzenia",
|
||||
"event.date": "Data",
|
||||
"event.description": "Opis",
|
||||
"event.discussion.empty": "Nikt jeszcze nie skomentował tego wydarzenia. Gdy ktoś skomentuje, pojawi się tutaj.",
|
||||
"event.export_ics": "Eksportuj do kalendarza",
|
||||
"event.external": "Zobacz wydarzenie na {domain}",
|
||||
"event.join_state.accept": "Biorę udział",
|
||||
"event.join_state.empty": "Weź udział",
|
||||
"event.join_state.pending": "Oczekujące",
|
||||
"event.join_state.rejected": "Biorę udział",
|
||||
"event.location": "Lokalizacja",
|
||||
"event.manage": "Zarządzaj",
|
||||
"event.organized_by": "Organizowane przez {name}",
|
||||
"event.participants": "{count} {rawCount, plural, one {osoba bierze} few {osoby biorą} other {osób bierze}} udział",
|
||||
"event.show_on_map": "Pokaż na mapie",
|
||||
"event.website": "Zewnętrzne linki",
|
||||
"event_map.navigate": "Nawiguj",
|
||||
"events.joined_events.empty": "Jeszcze nie dołączyłeś(-aś) do żadnego wydarzenia.",
|
||||
"events.recent_events.empty": "Nie ma jeszcze żadnych publicznych wydarzeń.",
|
||||
"export_data.actions.export": "Eksportuj dane",
|
||||
"export_data.actions.export_blocks": "Eksportuj blokady",
|
||||
"export_data.actions.export_follows": "Eksportuj obserwacje",
|
||||
|
@ -574,7 +703,7 @@
|
|||
"header.login.label": "Zaloguj się",
|
||||
"header.login.password.label": "Hasło",
|
||||
"header.login.username.placeholder": "Nazwa użytkownika lub e-mail",
|
||||
"header.menu.title": "Open menu",
|
||||
"header.menu.title": "Otwórz menu",
|
||||
"header.register.label": "Rejestracja",
|
||||
"home.column_settings.show_reblogs": "Pokazuj podbicia",
|
||||
"home.column_settings.show_replies": "Pokazuj odpowiedzi",
|
||||
|
@ -600,6 +729,12 @@
|
|||
"intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
|
||||
"intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
|
||||
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
|
||||
"join_event.hint": "Powiedz organizatorom, dlaczego chcesz wziąć udział w tym wydarzeniu:",
|
||||
"join_event.join": "Poproś o dołączenie",
|
||||
"join_event.placeholder": "Wiadomość do organizatora",
|
||||
"join_event.request_success": "Poproszono o dołączenie do wydarzenia",
|
||||
"join_event.success": "Dołączono do wydarzenia",
|
||||
"join_event.title": "Dołącz do wydarzenia",
|
||||
"keyboard_shortcuts.back": "cofnij się",
|
||||
"keyboard_shortcuts.blocked": "przejdź do listy zablokowanych",
|
||||
"keyboard_shortcuts.boost": "podbij wpis",
|
||||
|
@ -648,6 +783,7 @@
|
|||
"lists.search": "Szukaj wśród osób które obserwujesz",
|
||||
"lists.subheading": "Twoje listy",
|
||||
"loading_indicator.label": "Ładowanie…",
|
||||
"location_search.placeholder": "Znajdź adres",
|
||||
"login.fields.instance_label": "Instancja",
|
||||
"login.fields.instance_placeholder": "example.com",
|
||||
"login.fields.otp_code_hint": "Wprowadź kod uwierzytelniania dwuetapowego wygenerowany przez aplikację mobilną lub jeden z kodów zapasowych",
|
||||
|
@ -696,13 +832,15 @@
|
|||
"missing_description_modal.text": "Nie podałeś(-aś) opisu dla wszystkich załączników.",
|
||||
"missing_indicator.label": "Nie znaleziono",
|
||||
"missing_indicator.sublabel": "Nie można odnaleźć tego zasobu",
|
||||
"modals.policy.submit": "Akceptuj i kontynuuj",
|
||||
"modals.policy.updateTitle": "Jesteś na najnowszej wersji {siteTitle}! Poświęć chwilę aby zobaczyć ekscytujące nowości, nad którymi pracowaliśmy.",
|
||||
"moderation_overlay.contact": "Kontakt",
|
||||
"moderation_overlay.hide": "Ukryj",
|
||||
"moderation_overlay.show": "Wyświetl",
|
||||
"moderation_overlay.subtitle": "This Post has been sent to Moderation for review and is only visible to you. If you believe this is an error please contact Support.",
|
||||
"moderation_overlay.title": "Content Under Review",
|
||||
"mute_modal.auto_expire": "Automatically expire mute?",
|
||||
"mute_modal.duration": "Duration",
|
||||
"mute_modal.auto_expire": "Automatycznie wygasić wyciszenie?",
|
||||
"mute_modal.duration": "Czas trwania",
|
||||
"mute_modal.hide_notifications": "Chcesz ukryć powiadomienia od tego użytkownika?",
|
||||
"navbar.login.action": "Zaloguj się",
|
||||
"navbar.login.forgot_password": "Nie pamiętasz hasła?",
|
||||
|
@ -722,8 +860,10 @@
|
|||
"navigation_bar.compose": "Utwórz nowy wpis",
|
||||
"navigation_bar.compose_direct": "Wiadomość bezpośrednia",
|
||||
"navigation_bar.compose_edit": "Edytuj wpis",
|
||||
"navigation_bar.compose_event": "Zarządzaj wydarzeniem",
|
||||
"navigation_bar.compose_quote": "Cytuj wpis",
|
||||
"navigation_bar.compose_reply": "Odpowiedz na wpis",
|
||||
"navigation_bar.create_event": "Utwórz nowe wydarzenie",
|
||||
"navigation_bar.domain_blocks": "Ukryte domeny",
|
||||
"navigation_bar.favourites": "Ulubione",
|
||||
"navigation_bar.filters": "Wyciszone słowa",
|
||||
|
@ -765,14 +905,14 @@
|
|||
"oauth_consumers.title": "Inne opcje logowania",
|
||||
"onboarding.avatar.subtitle": "Just have fun with it.",
|
||||
"onboarding.avatar.title": "Wybierz zdjęcie profilowe",
|
||||
"onboarding.bio.hint": "Max 500 characters",
|
||||
"onboarding.bio.placeholder": "Tell the world a little about yourself…",
|
||||
"onboarding.display_name.label": "Display name",
|
||||
"onboarding.display_name.placeholder": "Eg. John Smith",
|
||||
"onboarding.bio.hint": "Maks. 500 znaków",
|
||||
"onboarding.bio.placeholder": "Powiedz światu coś o sobie…",
|
||||
"onboarding.display_name.label": "Nazwa wyświetlana",
|
||||
"onboarding.display_name.placeholder": "Np. John Smith",
|
||||
"onboarding.display_name.subtitle": "Możesz ją zawsze zmienić później.",
|
||||
"onboarding.display_name.title": "Wybierz wyświetlaną nazwę",
|
||||
"onboarding.done": "Gotowe",
|
||||
"onboarding.error": "An unexpected error occurred. Please try again or skip this step.",
|
||||
"onboarding.error": "Wystąpił nieoczekiwany błąd. Spróbuj ponownie lub pomiń ten krok.",
|
||||
"onboarding.fediverse.its_you": "Oto Twoje konto! Inni ludzie mogą Cię obserwować z innych serwerów używając pełnej @nazwy.",
|
||||
"onboarding.fediverse.message": "The Fediverse is a social network made up of thousands of diverse and independently-run social media sites (aka \"servers\"). You can follow users — and like, repost, and reply to posts — from most other Fediverse servers, because they can communicate with {siteTitle}.",
|
||||
"onboarding.fediverse.next": "Dalej",
|
||||
|
@ -887,12 +1027,12 @@
|
|||
"registrations.create_account": "Utwórz konto",
|
||||
"registrations.error": "Nie udało się zarejestrować konta.",
|
||||
"registrations.get_started": "Rozpocznijmy!",
|
||||
"registrations.password.label": "Password",
|
||||
"registrations.password.label": "Hasło",
|
||||
"registrations.success": "Witamy na {siteTitle}!",
|
||||
"registrations.tagline": "Media społecznościowe, które nie wykluczają",
|
||||
"registrations.unprocessable_entity": "Ta nazwa użytkownika jest już zajęta.",
|
||||
"registrations.username.hint": "Może zawierać wyłącznie A-Z, 0-9 i podkreślniki",
|
||||
"registrations.username.label": "Your username",
|
||||
"registrations.username.label": "Twoja nazwa użytkownika",
|
||||
"relative_time.days": "{number} dni",
|
||||
"relative_time.hours": "{number} godz.",
|
||||
"relative_time.just_now": "teraz",
|
||||
|
@ -907,6 +1047,8 @@
|
|||
"remote_instance.unpin_host": "Odepnij {host}",
|
||||
"remote_interaction.account_placeholder": "Wprowadź nazwę@domenę użytkownika, z którego chcesz wykonać działanie",
|
||||
"remote_interaction.divider": "lub",
|
||||
"remote_interaction.event_join": "Przejdź do dołączania",
|
||||
"remote_interaction.event_join_title": "Dołącz do wydarzenia zdalnie",
|
||||
"remote_interaction.favourite": "Przejdź do polubienia",
|
||||
"remote_interaction.favourite_title": "Polub wpis zdalnie",
|
||||
"remote_interaction.follow": "Przejdź do obserwacji",
|
||||
|
@ -928,6 +1070,8 @@
|
|||
"reply_mentions.reply_empty": "W odpowiedzi na wpis",
|
||||
"report.block": "Zablokuj {target}",
|
||||
"report.block_hint": "Czy chcesz też zablokować to konto?",
|
||||
"report.chatMessage.context": "Gdy zgłosisz wiadomość, pięć wiadomości poprzedzających i pięć następujących zostanie przekazane naszym moderatorem dla kontekstu.",
|
||||
"report.chatMessage.title": "Zgłoś wiadomość",
|
||||
"report.confirmation.content": "Jeżeli uznamy, że to konto narusza {link}, podejmiemy działania z tym związane.",
|
||||
"report.confirmation.title": "Dziękujemy za wysłanie zgłoszenia.",
|
||||
"report.done": "Gotowe",
|
||||
|
@ -940,14 +1084,14 @@
|
|||
"report.otherActions.hideAdditional": "Ukryj dodatkowe wpisy",
|
||||
"report.otherActions.otherStatuses": "Uwzględnić inne wpisy?",
|
||||
"report.placeholder": "Dodatkowe komentarze",
|
||||
"report.previous": "Previous",
|
||||
"report.previous": "Wstecz",
|
||||
"report.reason.blankslate": "Usunięto zaznaczenie wszystkich wpisów.",
|
||||
"report.reason.title": "Powód zgłoszenia",
|
||||
"report.submit": "Wyślij",
|
||||
"report.target": "Zgłaszanie {target}",
|
||||
"reset_password.fail": "Token wygasł, spróbuj ponownie.",
|
||||
"reset_password.header": "Ustaw nowe hasło",
|
||||
"reset_password.password.label": "Password",
|
||||
"reset_password.password.label": "Hasło",
|
||||
"reset_password.password.placeholder": "Placeholder",
|
||||
"save": "Zapisz",
|
||||
"schedule.post_time": "Data/godzina publikacji",
|
||||
|
@ -989,6 +1133,7 @@
|
|||
"settings.configure_mfa": "Konfiguruj uwierzytelnianie wieloskładnikowe",
|
||||
"settings.delete_account": "Usuń konto",
|
||||
"settings.edit_profile": "Edytuj profil",
|
||||
"settings.messages.label": "Pozwól innym rozpoczynać rozmowy z Tobą",
|
||||
"settings.other": "Pozostałe opcje",
|
||||
"settings.preferences": "Preferencje",
|
||||
"settings.profile": "Profil",
|
||||
|
@ -1011,9 +1156,9 @@
|
|||
"sms_verification.modal.verify_number": "Zweryfikuj numer telefonu",
|
||||
"sms_verification.modal.verify_sms": "Zweryfikuj SMS-em",
|
||||
"sms_verification.modal.verify_title": "Zweryfikuj numer telefonu",
|
||||
"sms_verification.phone.label": "Phone number",
|
||||
"sms_verification.sent.actions.resend": "Resend verification code?",
|
||||
"sms_verification.sent.body": "We sent you a 6-digit code via SMS. Enter it below.",
|
||||
"sms_verification.phone.label": "Numer telefonu",
|
||||
"sms_verification.sent.actions.resend": "Wysłać kod weryfikacyjny ponownie?",
|
||||
"sms_verification.sent.body": "Wysłaliśmy Ci 6-cyfrowy kod SMS-em. Wprowadź go poniżej.",
|
||||
"sms_verification.sent.header": "Kod weryfikujący",
|
||||
"sms_verification.success": "Kod weryfikujący został wysłany na Twój numer telefonu.",
|
||||
"snackbar.view": "Wyświetl",
|
||||
|
@ -1039,6 +1184,7 @@
|
|||
"soapbox_config.greentext_label": "Aktywuj greentext",
|
||||
"soapbox_config.headings.advanced": "Zaawansowane",
|
||||
"soapbox_config.headings.cryptocurrency": "Kryptowaluty",
|
||||
"soapbox_config.headings.events": "Wydarzenia",
|
||||
"soapbox_config.headings.navigation": "Nawigacja",
|
||||
"soapbox_config.headings.options": "Opcje",
|
||||
"soapbox_config.headings.theme": "Motyw",
|
||||
|
@ -1060,6 +1206,8 @@
|
|||
"soapbox_config.single_user_mode_label": "Tryb jednego użytkownika",
|
||||
"soapbox_config.single_user_mode_profile_hint": "@nazwa",
|
||||
"soapbox_config.single_user_mode_profile_label": "Nazwa głównego użytkownika",
|
||||
"soapbox_config.tile_server_attribution_label": "Uznanie kafelków mapy",
|
||||
"soapbox_config.tile_server_label": "Serwer kafelków mapy",
|
||||
"soapbox_config.verified_can_edit_name_label": "Pozwól zweryfikowanym użytkownikom na zmianę swojej nazwy wyświetlanej.",
|
||||
"sponsored.info.message": "{siteTitle} wyświetla reklamy, aby utrzymać naszą usługę.",
|
||||
"sponsored.info.title": "Dlaczego widzę tę reklamę?",
|
||||
|
@ -1080,8 +1228,9 @@
|
|||
"status.external": "View post on {domain}",
|
||||
"status.favourite": "Zareaguj",
|
||||
"status.filtered": "Filtrowany(-a)",
|
||||
"status.interactions.favourites": "{count, plural, one {Like} other {Likes}}",
|
||||
"status.interactions.reblogs": "{count, plural, one {Repost} other {Reposts}}",
|
||||
"status.interactions.favourites": "{count, plural, one {Polubienie} few {Polubienia} other {Polubień}}",
|
||||
"status.interactions.quotes": "{count, plural, one {Cytat} few {Cytaty} other {Cytatów}}",
|
||||
"status.interactions.reblogs": "{count, plural, one {Podanie dalej} few {Podania dalej} other {Podań dalej}}",
|
||||
"status.load_more": "Załaduj więcej",
|
||||
"status.mention": "Wspomnij o @{name}",
|
||||
"status.more": "Więcej",
|
||||
|
@ -1143,7 +1292,7 @@
|
|||
"tabs_bar.dashboard": "Panel administracyjny",
|
||||
"tabs_bar.fediverse": "Fediwersum",
|
||||
"tabs_bar.home": "Strona główna",
|
||||
"tabs_bar.local": "Local",
|
||||
"tabs_bar.local": "Lokalna",
|
||||
"tabs_bar.more": "Więcej",
|
||||
"tabs_bar.notifications": "Powiadomienia",
|
||||
"tabs_bar.profile": "Profil",
|
||||
|
@ -1188,7 +1337,7 @@
|
|||
"video.pause": "Pauzuj",
|
||||
"video.play": "Odtwórz",
|
||||
"video.unmute": "Cofnij wyciszenie",
|
||||
"waitlist.actions.verify_number": "Verify phone number",
|
||||
"waitlist.actions.verify_number": "Zweryfikuj numer telefonu",
|
||||
"waitlist.body": "Welcome back to {title}! You were previously placed on our waitlist. Please verify your phone number to receive immediate access to your account!",
|
||||
"who_to_follow.title": "Kogo obserwować"
|
||||
}
|
||||
|
|
|
@ -7,7 +7,6 @@ import { play, soundCache } from 'soapbox/utils/sounds';
|
|||
import type { ThunkMiddleware } from 'redux-thunk';
|
||||
import type { Sounds } from 'soapbox/utils/sounds';
|
||||
|
||||
|
||||
interface Action extends AnyAction {
|
||||
meta: {
|
||||
sound: Sounds
|
||||
|
|
|
@ -16,11 +16,9 @@ import {
|
|||
} from 'soapbox/features/ui/util/async-components';
|
||||
import { useAppSelector, useOwnAccount, useFeatures, useSoapboxConfig } from 'soapbox/hooks';
|
||||
|
||||
import Avatar from '../components/avatar';
|
||||
import { Card, CardBody, HStack, Layout } from '../components/ui';
|
||||
import { Avatar, Card, CardBody, HStack, Layout } from '../components/ui';
|
||||
import ComposeForm from '../features/compose/components/compose-form';
|
||||
import BundleContainer from '../features/ui/containers/bundle-container';
|
||||
// import GroupSidebarPanel from '../features/groups/sidebar_panel';
|
||||
|
||||
const HomePage: React.FC = ({ children }) => {
|
||||
const me = useAppSelector(state => state.me);
|
||||
|
@ -35,6 +33,7 @@ const HomePage: React.FC = ({ children }) => {
|
|||
const cryptoLimit = soapboxConfig.cryptoDonatePanel.get('limit', 0);
|
||||
|
||||
const acct = account ? account.acct : '';
|
||||
const avatar = account ? account.avatar : '';
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -44,21 +43,23 @@ const HomePage: React.FC = ({ children }) => {
|
|||
<CardBody>
|
||||
<HStack alignItems='start' space={4}>
|
||||
<Link to={`/@${acct}`}>
|
||||
<Avatar account={account} size={46} />
|
||||
<Avatar src={avatar} size={46} />
|
||||
</Link>
|
||||
|
||||
<ComposeForm
|
||||
id='home'
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
/>
|
||||
<div className='translate-y-0.5 w-full'>
|
||||
<ComposeForm
|
||||
id='home'
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{features.feedUserFiltering && <FeedCarousel />}
|
||||
{features.carousel && <FeedCarousel />}
|
||||
|
||||
{children}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import useCarouselAvatars from '../carousels';
|
||||
import { useCarouselAvatars } from '../carousels';
|
||||
|
||||
describe('useCarouselAvatars', () => {
|
||||
describe('with a successful query', () => {
|
||||
|
|
|
@ -11,8 +11,6 @@ import { flattenPages } from 'soapbox/utils/queries';
|
|||
import { IAccount } from '../accounts';
|
||||
import { ChatKeys, IChat, IChatMessage, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats';
|
||||
|
||||
jest.mock('soapbox/utils/queries');
|
||||
|
||||
const chat: IChat = {
|
||||
accepted: true,
|
||||
account: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
|||
|
||||
import { useOnboardingSuggestions } from '../suggestions';
|
||||
|
||||
describe('useCarouselAvatars', () => {
|
||||
describe('useOnboardingSuggestions', () => {
|
||||
describe('with a successful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
type Avatar = {
|
||||
export type Avatar = {
|
||||
account_id: string
|
||||
account_avatar: string
|
||||
username: string
|
||||
acct: string
|
||||
seen?: boolean
|
||||
}
|
||||
|
||||
export default function useCarouselAvatars() {
|
||||
const CarouselKeys = {
|
||||
avatars: ['carouselAvatars'] as const,
|
||||
};
|
||||
|
||||
function useCarouselAvatars() {
|
||||
const api = useApi();
|
||||
|
||||
const getCarouselAvatars = async() => {
|
||||
|
@ -16,8 +21,9 @@ export default function useCarouselAvatars() {
|
|||
return data;
|
||||
};
|
||||
|
||||
const result = useQuery<Avatar[]>(['carouselAvatars'], getCarouselAvatars, {
|
||||
const result = useQuery<Avatar[]>(CarouselKeys.avatars, getCarouselAvatars, {
|
||||
placeholderData: [],
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const avatars = result.data;
|
||||
|
@ -27,3 +33,18 @@ export default function useCarouselAvatars() {
|
|||
data: avatars || [],
|
||||
};
|
||||
}
|
||||
|
||||
function useMarkAsSeen() {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
return useMutation(async (accountId: string) => {
|
||||
if (features.carouselSeen) {
|
||||
await void api.post('/api/v1/truth/carousels/avatars/seen', {
|
||||
account_id: accountId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { useCarouselAvatars, useMarkAsSeen };
|
|
@ -8,6 +8,7 @@ import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context
|
|||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { reOrderChatListItems } from 'soapbox/utils/chats';
|
||||
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
|
||||
|
||||
import { queryClient } from './client';
|
||||
|
@ -15,14 +16,13 @@ import { useFetchRelationships } from './relationships';
|
|||
|
||||
import type { IAccount } from './accounts';
|
||||
|
||||
export const messageExpirationOptions = [120, 604800, 1209600, 2592000, 7776000];
|
||||
export const messageExpirationOptions = [604800, 1209600, 2592000, 7776000];
|
||||
|
||||
export enum MessageExpirationValues {
|
||||
'TWO_MINUTES' = messageExpirationOptions[0],
|
||||
'SEVEN' = messageExpirationOptions[1],
|
||||
'FOURTEEN' = messageExpirationOptions[2],
|
||||
'THIRTY' = messageExpirationOptions[3],
|
||||
'NINETY' = messageExpirationOptions[4]
|
||||
'SEVEN' = messageExpirationOptions[0],
|
||||
'FOURTEEN' = messageExpirationOptions[1],
|
||||
'THIRTY' = messageExpirationOptions[2],
|
||||
'NINETY' = messageExpirationOptions[3]
|
||||
}
|
||||
|
||||
export interface IChat {
|
||||
|
@ -281,6 +281,7 @@ const useChatActions = (chatId: string) => {
|
|||
onSuccess: (response, variables) => {
|
||||
const nextChat = { ...chat, last_message: response.data };
|
||||
updatePageItem(ChatKeys.chatSearch(), nextChat, (o, n) => o.id === n.id);
|
||||
reOrderChatListItems();
|
||||
|
||||
queryClient.invalidateQueries(ChatKeys.chatMessages(variables.chatId));
|
||||
},
|
||||
|
|
|
@ -1,27 +1,51 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useInfiniteQuery } from '@tanstack/react-query';
|
||||
|
||||
import { getNextLink } from 'soapbox/api';
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { Account } from 'soapbox/types/entities';
|
||||
import { flattenPages, PaginatedResult } from 'soapbox/utils/queries';
|
||||
|
||||
export default function useAccountSearch(q: string) {
|
||||
const api = useApi();
|
||||
|
||||
const getAccountSearch = async(q: string) => {
|
||||
if (typeof q === 'undefined') {
|
||||
return null;
|
||||
}
|
||||
const getAccountSearch = async(q: string, pageParam: { link?: string }): Promise<PaginatedResult<Account>> => {
|
||||
const nextPageLink = pageParam?.link;
|
||||
const uri = nextPageLink || '/api/v1/accounts/search';
|
||||
|
||||
const { data } = await api.get('/api/v1/accounts/search', {
|
||||
const response = await api.get(uri, {
|
||||
params: {
|
||||
q,
|
||||
limit: 10,
|
||||
followers: true,
|
||||
},
|
||||
});
|
||||
const { data } = response;
|
||||
|
||||
return data;
|
||||
const link = getNextLink(response);
|
||||
const hasMore = !!link;
|
||||
|
||||
return {
|
||||
result: data,
|
||||
link,
|
||||
hasMore,
|
||||
};
|
||||
};
|
||||
|
||||
return useQuery(['search', 'accounts', q], () => getAccountSearch(q), {
|
||||
const queryInfo = useInfiniteQuery(['search', 'accounts', q], ({ pageParam }) => getAccountSearch(q, pageParam), {
|
||||
keepPreviousData: true,
|
||||
placeholderData: [],
|
||||
getNextPageParam: (config) => {
|
||||
if (config.hasMore) {
|
||||
return { link: config.link };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
},
|
||||
});
|
||||
|
||||
const data = flattenPages(queryInfo.data);
|
||||
|
||||
return {
|
||||
...queryInfo,
|
||||
data,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { InfiniteData, QueryKey, UseInfiniteQueryResult } from '@tanstack/react-query';
|
||||
|
||||
import { queryClient } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import { PaginatedResult } from '../queries';
|
||||
|
||||
const flattenPages = <T>(queryData: UseInfiniteQueryResult<PaginatedResult<T>>['data']) => {
|
||||
return queryData?.pages.reduce<T[]>(
|
||||
(prev: T[], curr) => [...curr.result, ...prev],
|
||||
[],
|
||||
);
|
||||
};
|
||||
|
||||
const updatePageItem = <T>(queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.map(item => isItem(item, newItem) ? newItem : item);
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Insert the new item at the beginning of the first page. */
|
||||
const appendPageItem = <T>(queryKey: QueryKey, newItem: T) => {
|
||||
queryClient.setQueryData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = [...data.pages];
|
||||
pages[0] = { ...pages[0], result: [...pages[0].result, newItem] };
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/** Remove an item inside if found. */
|
||||
const removePageItem = <T>(queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => {
|
||||
queryClient.setQueriesData<InfiniteData<PaginatedResult<T>>>(queryKey, (data) => {
|
||||
if (data) {
|
||||
const pages = data.pages.map(page => {
|
||||
const result = page.result.filter(item => !isItem(item, itemToRemove));
|
||||
return { ...page, result };
|
||||
});
|
||||
return { ...data, pages };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
export {
|
||||
flattenPages,
|
||||
updatePageItem,
|
||||
appendPageItem,
|
||||
removePageItem,
|
||||
};
|
|
@ -32,3 +32,25 @@ export const isLocal = (account: Account): boolean => {
|
|||
};
|
||||
|
||||
export const isRemote = (account: Account): boolean => !isLocal(account);
|
||||
|
||||
/** Default header filenames from various backends */
|
||||
const DEFAULT_HEADERS = [
|
||||
'/headers/original/missing.png', // Mastodon
|
||||
'/images/banner.png', // Pleroma
|
||||
];
|
||||
|
||||
/** Check if the avatar is a default avatar */
|
||||
export const isDefaultHeader = (url: string) => {
|
||||
return DEFAULT_HEADERS.some(header => url.endsWith(header));
|
||||
};
|
||||
|
||||
/** Default avatar filenames from various backends */
|
||||
const DEFAULT_AVATARS = [
|
||||
'/avatars/original/missing.png', // Mastodon
|
||||
'/images/avi.png', // Pleroma
|
||||
];
|
||||
|
||||
/** Check if the avatar is a default avatar */
|
||||
export const isDefaultAvatar = (url: string) => {
|
||||
return DEFAULT_AVATARS.some(avatar => url.endsWith(avatar));
|
||||
};
|
||||
|
|
|
@ -26,7 +26,10 @@ const updateChatInChatSearchQuery = (newChat: ChatPayload) => {
|
|||
*/
|
||||
const reOrderChatListItems = () => {
|
||||
sortQueryData<ChatPayload>(ChatKeys.chatSearch(), (chatA, chatB) => {
|
||||
return compareDate(chatA.last_message?.created_at as string, chatB.last_message?.created_at as string);
|
||||
return compareDate(
|
||||
chatA.last_message?.created_at as string,
|
||||
chatB.last_message?.created_at as string,
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
|
@ -81,4 +84,4 @@ const getUnreadChatsCount = (): number => {
|
|||
return sumBy(chats, chat => chat.unread);
|
||||
};
|
||||
|
||||
export { updateChatListItem, getUnreadChatsCount };
|
||||
export { updateChatListItem, getUnreadChatsCount, reOrderChatListItems };
|
|
@ -205,6 +205,19 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
v.software === PLEROMA,
|
||||
]),
|
||||
|
||||
/**
|
||||
* Whether to show the Feed Carousel for suggested Statuses.
|
||||
* @see GET /api/v1/truth/carousels/avatars
|
||||
* @see GET /api/v1/truth/carousels/suggestions
|
||||
*/
|
||||
carousel: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Ability to mark a carousel avatar as "seen."
|
||||
* @see POST /api/v1/truth/carousels/avatars/seen
|
||||
*/
|
||||
carouselSeen: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Ability to accept a chat.
|
||||
* POST /api/v1/pleroma/chats/:id/accept
|
||||
|
@ -371,9 +384,6 @@ const getInstanceFeatures = (instance: Instance) => {
|
|||
/** Whether the instance federates. */
|
||||
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
|
||||
|
||||
/** Whether or not to show the Feed Carousel for suggested Statuses */
|
||||
feedUserFiltering: v.software === TRUTHSOCIAL,
|
||||
|
||||
/**
|
||||
* Can edit and manage timeline filters (aka "muted words").
|
||||
* @see {@link https://docs.joinmastodon.org/methods/accounts/filters/}
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
@import '~@fontsource/inter/300.css';
|
||||
@import '~@fontsource/inter/400.css';
|
||||
@import '~@fontsource/inter/500.css';
|
||||
@import '~@fontsource/inter/600.css';
|
||||
@import '~@fontsource/inter/700.css';
|
||||
@import '~@fontsource/inter/900.css';
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue