Merge remote-tracking branch 'soapbox/develop' into mastodon-groups

i-1329
marcin mikołajczak 2022-12-16 21:33:23 +01:00
commit d4024e927d
46 zmienionych plików z 708 dodań i 454 usunięć

Wyświetl plik

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

Wyświetl plik

@ -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 }[]) => {

Wyświetl plik

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

Wyświetl plik

@ -1,3 +1,7 @@
[data-markup] {
@apply whitespace-pre-wrap;
}
[data-markup] p {
@apply mb-4 whitespace-pre-wrap;
}

Wyświetl plik

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

Wyświetl plik

@ -20,7 +20,6 @@ const messages = defineMessages({
delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' },
});
const Aliases = () => {
const intl = useIntl();
const dispatch = useAppDispatch();

Wyświetl plik

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

Wyświetl plik

@ -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>
)}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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)}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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]) }));

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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}
/>
))
)}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -56,7 +56,6 @@ const DirectMessageUpdates = () => {
</defs>
</svg>
<Text weight='bold'>Privacy Policy Updates</Text>
</HStack>

Wyświetl plik

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

Wyświetl plik

@ -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} />

Wyświetl plik

@ -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",

Wyświetl plik

@ -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ć"
}

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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', () => {

Wyświetl plik

@ -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: {

Wyświetl plik

@ -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) => {

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

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

Wyświetl plik

@ -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/}

Wyświetl plik

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