diff --git a/app/soapbox/components/account-search.tsx b/app/soapbox/components/account-search.tsx index 8b3f50b20..c519b0243 100644 --- a/app/soapbox/components/account-search.tsx +++ b/app/soapbox/components/account-search.tsx @@ -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 = ({ onSelected, className, showButtons = true, ...rest }) => { +const AccountSearch: React.FC = ({ onSelected, ...rest }) => { const intl = useIntl(); const [value, setValue] = useState(''); @@ -71,7 +60,7 @@ const AccountSearch: React.FC = ({ onSelected, className, showBu
= ({ onSelected, className, showBu {...rest} /> - {showButtons && ( -
- +
+ - -
- )} + +
); diff --git a/app/soapbox/components/autosuggest-account-input.tsx b/app/soapbox/components/autosuggest-account-input.tsx index 352be593b..b2a205e3c 100644 --- a/app/soapbox/components/autosuggest-account-input.tsx +++ b/app/soapbox/components/autosuggest-account-input.tsx @@ -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 = ({ @@ -31,7 +29,6 @@ const AutosuggestAccountInput: React.FC = ({ onSelected, value = '', limit = 4, - followers = false, ...rest }) => { const dispatch = useAppDispatch(); @@ -48,7 +45,7 @@ const AutosuggestAccountInput: React.FC = ({ }; 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 }[]) => { diff --git a/app/soapbox/components/autosuggest-input.tsx b/app/soapbox/components/autosuggest-input.tsx index 87f1c41f5..35460131a 100644 --- a/app/soapbox/components/autosuggest-input.tsx +++ b/app/soapbox/components/autosuggest-input.tsx @@ -31,7 +31,6 @@ export interface IAutosuggestInput extends Pick, hidePortal?: boolean, theme?: InputThemes, @@ -43,7 +42,6 @@ export default class AutosuggestInput extends ImmutablePureComponent { @@ -260,19 +258,15 @@ export default class AutosuggestInput extends ImmutablePureComponent = ({ account }) => { return info; }; + const renderHeader = () => { + let header: React.ReactNode; + + if (account.header) { + header = ( + + ); + + if (!isDefaultHeader(account.header)) { + header = ( + + {header} + + ); + } + } + + return header; + }; + const renderMessageButton = () => { if (features.chatsWithFollowers) { // Truth Social if (!ownAccount || !account || account.id === ownAccount?.id) { @@ -570,14 +593,7 @@ const Header: React.FC = ({ account }) => {
- {account.header && ( - - - - )} + {renderHeader()}
@@ -594,7 +610,7 @@ const Header: React.FC = ({ account }) => {
diff --git a/app/soapbox/features/aliases/index.tsx b/app/soapbox/features/aliases/index.tsx index 54e0d254d..268ca8cde 100644 --- a/app/soapbox/features/aliases/index.tsx +++ b/app/soapbox/features/aliases/index.tsx @@ -20,7 +20,6 @@ const messages = defineMessages({ delete: { id: 'column.aliases.delete', defaultMessage: 'Delete' }, }); - const Aliases = () => { const intl = useIntl(); const dispatch = useAppDispatch(); diff --git a/app/soapbox/features/chats/components/chat-composer.tsx b/app/soapbox/features/chats/components/chat-composer.tsx index 20db9636d..358fbb6f3 100644 --- a/app/soapbox/features/chats/components/chat-composer.tsx +++ b/app/soapbox/features/chats/components/chat-composer.tsx @@ -151,6 +151,9 @@ const ChatComposer = React.forwardRef return (
+ {/* Spacer */} +
+ {features.chatsMedia && ( diff --git a/app/soapbox/features/chats/components/chat-list-item.tsx b/app/soapbox/features/chats/components/chat-list-item.tsx index 72584e2df..6099955e7 100644 --- a/app/soapbox/features/chats/components/chat-list-item.tsx +++ b/app/soapbox/features/chats/components/chat-list-item.tsx @@ -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 = ({ chat, onClick }) => { {features.chatsDelete && (
- {/* TODO: fix nested buttons here */} - + + +
)} diff --git a/app/soapbox/features/chats/components/chat-list.tsx b/app/soapbox/features/chats/components/chat-list.tsx index 9de6d5c2d..65629ce14 100644 --- a/app/soapbox/features/chats/components/chat-list.tsx +++ b/app/soapbox/features/chats/components/chat-list.tsx @@ -63,8 +63,7 @@ const ChatList: React.FC = ({ onClickChat, useWindowScroll = false, s
- ) - } + )} components={{ ScrollSeekPlaceholder: () => , Footer: () => hasNextPage ? : null, diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index 1f6fd6adb..4b5098a7d 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -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 = ({ chat }) => { return emojify(formatted, emojiMap.toJS()); }; - const renderDivider = (key: React.Key, text: string) => ; + const renderDivider = (key: React.Key, text: string) => ; const handleCopyText = (chatMessage: ChatMessageEntity) => { if (navigator.clipboard) { @@ -286,11 +286,14 @@ const ChatMessageList: React.FC = ({ chat }) => { })} data-testid='chat-message-menu' > - + + +
)} @@ -447,7 +450,7 @@ const ChatMessageList: React.FC = ({ chat }) => { return (
-
+
{ label={intl.formatMessage(messages.autoDeleteLabel)} hint={intl.formatMessage(messages.autoDeleteHint)} /> - handleUpdateChat(MessageExpirationValues.TWO_MINUTES)} - isSelected={chat.message_expiration === MessageExpirationValues.TWO_MINUTES} - /> handleUpdateChat(MessageExpirationValues.SEVEN)} diff --git a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx index 5b441026e..c362a4159 100644 --- a/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx +++ b/app/soapbox/features/chats/components/chat-page/components/chat-page-new.tsx @@ -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 = () => { 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 ( - - + + = () => { - - - - - - - - + + ); }; diff --git a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx index ad0dd8e9e..59953b54c 100644 --- a/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx +++ b/app/soapbox/features/chats/components/chat-pane/chat-pane.tsx @@ -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 ; + return ( + + + + {isOpen ? : null} + + ); } return ( diff --git a/app/soapbox/features/chats/components/chat-search-input.tsx b/app/soapbox/features/chats/components/chat-search-input.tsx index 2474ac6cb..bc8644aab 100644 --- a/app/soapbox/features/chats/components/chat-search-input.tsx +++ b/app/soapbox/features/chats/components/chat-search-input.tsx @@ -29,6 +29,7 @@ const ChatSearchInput: React.FC = ({ value, onChange, onClear className='rounded-full' value={value} onChange={onChange} + outerClassName='mt-0' theme='search' append={ + } + /> +
- - {intl.formatMessage(messages.title)} - - - } - isOpen={isOpen} - isToggleable={false} - onToggle={toggleChatPane} - /> - - {isOpen ? ( - -
- setValue(event.target.value)} - theme='search' - append={ - - } - /> -
- - - {renderBody()} - -
- ) : null} - + + {renderBody()} + + ); }; diff --git a/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx index 15daef888..3e360347e 100644 --- a/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx +++ b/app/soapbox/features/chats/components/chat-search/empty-results-blankslate.tsx @@ -13,7 +13,7 @@ const EmptyResultsBlankslate = () => { return ( - + {intl.formatMessage(messages.title)} diff --git a/app/soapbox/features/chats/components/chat-search/results.tsx b/app/soapbox/features/chats/components/chat-search/results.tsx index 26127a20b..48909f67e 100644 --- a/app/soapbox/features/chats/components/chat-search/results.tsx +++ b/app/soapbox/features/chats/components/chat-search/results.tsx @@ -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 onSelect(id: string): void } -const Results = ({ accounts, onSelect }: IResults) => ( - <> - {(accounts || []).map((account: any) => ( - - ))} - -); + const [isNearBottom, setNearBottom] = useState(false); + const [isNearTop, setNearTop] = useState(true); -export default Results; \ No newline at end of file + const handleLoadMore = () => { + if (hasNextPage && !isFetching) { + fetchNextPage(); + } + }; + + const renderAccount = useCallback((_index, account) => ( + + ), []); + + return ( +
+ ( +
+ {renderAccount(index, chat)} +
+ )} + endReached={handleLoadMore} + atTopStateChange={(atTop) => setNearTop(atTop)} + atBottomStateChange={(atBottom) => setNearBottom(atBottom)} + /> + + <> +
+
+ +
+ ); +}; + +export default Results; diff --git a/app/soapbox/features/chats/components/chat-widget/chat-pane-header.tsx b/app/soapbox/features/chats/components/chat-widget/chat-pane-header.tsx index 5b2e17299..20967d171 100644 --- a/app/soapbox/features/chats/components/chat-widget/chat-pane-header.tsx +++ b/app/soapbox/features/chats/components/chat-widget/chat-pane-header.tsx @@ -37,11 +37,9 @@ const ChatPaneHeader = (props: IChatPaneHeader) => { data-testid='title' {...buttonProps} > - {typeof title === 'string' ? ( - - {title} - - ) : (title)} + + {title} + {(typeof unreadCount !== 'undefined' && unreadCount > 0) && ( diff --git a/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx new file mode 100644 index 000000000..d0fd40921 --- /dev/null +++ b/app/soapbox/features/chats/components/chat-widget/headers/chat-search-header.tsx @@ -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 ( + + + + + {intl.formatMessage(messages.title)} + + + } + isOpen={isOpen} + isToggleable={false} + onToggle={toggleChatPane} + /> + ); +}; + +export default ChatSearchHeader; \ No newline at end of file diff --git a/app/soapbox/features/edit-profile/index.tsx b/app/soapbox/features/edit-profile/index.tsx index 97fdd7e50..d1624ddf5 100644 --- a/app/soapbox/features/edit-profile/index.tsx +++ b/app/soapbox/features/edit-profile/index.tsx @@ -244,7 +244,6 @@ const EditProfile: React.FC = () => { const handleHideNetworkChange: React.ChangeEventHandler = e => { const hide = e.target.checked; - setData(prevData => { return { ...prevData, @@ -310,6 +309,26 @@ const EditProfile: React.FC = () => { return (
+
+ + +
+ } + hintText={} + > + + + + } + hintText={} + > + + +
+
+ } > @@ -369,26 +388,6 @@ const EditProfile: React.FC = () => { /> -
- - -
- } - hintText={} - > - - - - } - hintText={} - > - - -
-
- {features.followRequests && ( { hint={} > diff --git a/app/soapbox/features/event/components/event-header.tsx b/app/soapbox/features/event/components/event-header.tsx index dd7a66d88..adfb2dc32 100644 --- a/app/soapbox/features/event/components/event-header.tsx +++ b/app/soapbox/features/event/components/event-header.tsx @@ -95,6 +95,7 @@ const EventHeader: React.FC = ({ status }) => { const username = account.username; const handleHeaderClick: React.MouseEventHandler = (e) => { + e.preventDefault(); e.stopPropagation(); dispatch(openModal('MEDIA', { media: ImmutableList([event.banner]) })); diff --git a/app/soapbox/features/events/index.tsx b/app/soapbox/features/events/index.tsx index 039d60b69..8af41449e 100644 --- a/app/soapbox/features/events/index.tsx +++ b/app/soapbox/features/events/index.tsx @@ -34,14 +34,14 @@ const Events = () => { return ( - + } /> @@ -52,7 +52,7 @@ const Events = () => { /> - + } /> ({ describe('', () => { let store: any; - describe('with "feedUserFiltering" disabled', () => { + describe('with "carousel" disabled', () => { beforeEach(() => { store = { instance: { @@ -42,7 +42,7 @@ describe('', () => { }); }); - describe('with "feedUserFiltering" enabled', () => { + describe('with "carousel" enabled', () => { beforeEach(() => { store = { instance: { @@ -61,11 +61,17 @@ describe('', () => { __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: '; rel=\'prev\'', + }); + + mock.onPost('/api/v1/truth/carousels/avatars/seen').reply(200); }); }); @@ -74,6 +80,29 @@ describe('', () => { await waitFor(() => { expect(screen.queryAllByTestId('feed-carousel')).toHaveLength(1); + expect(screen.queryAllByTestId('carousel-item')).toHaveLength(4); + }); + }); + + it('should handle the "seen" state', async() => { + render(, 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'); }); }); }); diff --git a/app/soapbox/features/feed-filtering/feed-carousel.tsx b/app/soapbox/features/feed-filtering/feed-carousel.tsx index 621c48374..481ae138e 100644 --- a/app/soapbox/features/feed-filtering/feed-carousel.tsx +++ b/app/soapbox/features/feed-filtering/feed-carousel.tsx @@ -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(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 ( -
+
{isSelected && (
- +
)} @@ -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' />
@@ -63,6 +75,7 @@ const FeedCarousel = () => { const [cardRef, setCardRef, { width }] = useDimensions(); + const [seenAccountIds, setSeenAccountIds] = useState([]); const [pageSize, setPageSize] = useState(0); const [currentPage, setCurrentPage] = useState(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 = () => { )) )} diff --git a/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx b/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx index f5f9f7274..936dd160d 100644 --- a/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx +++ b/app/soapbox/features/onboarding/steps/avatar-selection-step.tsx @@ -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(); diff --git a/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx b/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx index e1a9c5f30..d87852699 100644 --- a/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx +++ b/app/soapbox/features/onboarding/steps/cover-photo-selection-step.tsx @@ -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(); diff --git a/app/soapbox/features/status/components/status-interaction-bar.tsx b/app/soapbox/features/status/components/status-interaction-bar.tsx index ef22ba14b..42ce3e691 100644 --- a/app/soapbox/features/status/components/status-interaction-bar.tsx +++ b/app/soapbox/features/status/components/status-interaction-bar.tsx @@ -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 = ({ count, onClick, chi > - + {shortNumberFormat(count)} diff --git a/app/soapbox/features/ui/components/modals/policy-modal.tsx b/app/soapbox/features/ui/components/modals/policy-modal.tsx index 5fd53b2a4..b5048e797 100644 --- a/app/soapbox/features/ui/components/modals/policy-modal.tsx +++ b/app/soapbox/features/ui/components/modals/policy-modal.tsx @@ -56,7 +56,6 @@ const DirectMessageUpdates = () => { - Privacy Policy Updates diff --git a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx index 72b310e0e..7686c1617 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx @@ -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'; diff --git a/app/soapbox/features/ui/index.tsx b/app/soapbox/features/ui/index.tsx index 2771afe01..8d8f399ea 100644 --- a/app/soapbox/features/ui/index.tsx +++ b/app/soapbox/features/ui/index.tsx @@ -272,8 +272,8 @@ const SwitchingColumnsArea: React.FC = ({ children }) => { - - + {features.events && } + {features.events && } diff --git a/app/soapbox/locales/it.json b/app/soapbox/locales/it.json index 0f107119e..d96db99ab 100644 --- a/app/soapbox/locales/it.json +++ b/app/soapbox/locales/it.json @@ -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", diff --git a/app/soapbox/locales/pl.json b/app/soapbox/locales/pl.json index 527b55b60..d3af3e128 100644 --- a/app/soapbox/locales/pl.json +++ b/app/soapbox/locales/pl.json @@ -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ć" } diff --git a/app/soapbox/middleware/sounds.ts b/app/soapbox/middleware/sounds.ts index 61b9c2058..a1055701c 100644 --- a/app/soapbox/middleware/sounds.ts +++ b/app/soapbox/middleware/sounds.ts @@ -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 diff --git a/app/soapbox/pages/home-page.tsx b/app/soapbox/pages/home-page.tsx index 1982d4605..c65d9b86c 100644 --- a/app/soapbox/pages/home-page.tsx +++ b/app/soapbox/pages/home-page.tsx @@ -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 }) => { - + - +
+ +
)} - {features.feedUserFiltering && } + {features.carousel && } {children} diff --git a/app/soapbox/queries/__tests__/carousels.test.ts b/app/soapbox/queries/__tests__/carousels.test.ts index 9ee5fa4c2..eb9501638 100644 --- a/app/soapbox/queries/__tests__/carousels.test.ts +++ b/app/soapbox/queries/__tests__/carousels.test.ts @@ -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', () => { diff --git a/app/soapbox/queries/__tests__/chats.test.ts b/app/soapbox/queries/__tests__/chats.test.ts index 7640fd511..3d72b9c0f 100644 --- a/app/soapbox/queries/__tests__/chats.test.ts +++ b/app/soapbox/queries/__tests__/chats.test.ts @@ -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: { diff --git a/app/soapbox/queries/__tests__/suggestions.test.ts b/app/soapbox/queries/__tests__/suggestions.test.ts index aa352abe9..cfd8cbb8a 100644 --- a/app/soapbox/queries/__tests__/suggestions.test.ts +++ b/app/soapbox/queries/__tests__/suggestions.test.ts @@ -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) => { diff --git a/app/soapbox/queries/carousels.ts b/app/soapbox/queries/carousels.ts index 7d295183e..d6e05798a 100644 --- a/app/soapbox/queries/carousels.ts +++ b/app/soapbox/queries/carousels.ts @@ -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(['carouselAvatars'], getCarouselAvatars, { + const result = useQuery(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 }; \ No newline at end of file diff --git a/app/soapbox/queries/chats.ts b/app/soapbox/queries/chats.ts index 68fa7fc21..4c377abb7 100644 --- a/app/soapbox/queries/chats.ts +++ b/app/soapbox/queries/chats.ts @@ -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)); }, diff --git a/app/soapbox/queries/search.ts b/app/soapbox/queries/search.ts index 2b3fd1e70..41c4b7fb3 100644 --- a/app/soapbox/queries/search.ts +++ b/app/soapbox/queries/search.ts @@ -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> => { + 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, + }; } diff --git a/app/soapbox/utils/__mocks__/queries.ts b/app/soapbox/utils/__mocks__/queries.ts deleted file mode 100644 index efc7447a3..000000000 --- a/app/soapbox/utils/__mocks__/queries.ts +++ /dev/null @@ -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 = (queryData: UseInfiniteQueryResult>['data']) => { - return queryData?.pages.reduce( - (prev: T[], curr) => [...curr.result, ...prev], - [], - ); -}; - -const updatePageItem = (queryKey: QueryKey, newItem: T, isItem: (item: T, newItem: T) => boolean) => { - queryClient.setQueriesData>>(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 = (queryKey: QueryKey, newItem: T) => { - queryClient.setQueryData>>(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 = (queryKey: QueryKey, itemToRemove: T, isItem: (item: T, newItem: T) => boolean) => { - queryClient.setQueriesData>>(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, -}; \ No newline at end of file diff --git a/app/soapbox/utils/accounts.ts b/app/soapbox/utils/accounts.ts index 3a1cadbe9..2e5e53cfe 100644 --- a/app/soapbox/utils/accounts.ts +++ b/app/soapbox/utils/accounts.ts @@ -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)); +}; diff --git a/app/soapbox/utils/chats.ts b/app/soapbox/utils/chats.ts index 53676c898..71a416562 100644 --- a/app/soapbox/utils/chats.ts +++ b/app/soapbox/utils/chats.ts @@ -26,7 +26,10 @@ const updateChatInChatSearchQuery = (newChat: ChatPayload) => { */ const reOrderChatListItems = () => { sortQueryData(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 }; \ No newline at end of file +export { updateChatListItem, getUnreadChatsCount, reOrderChatListItems }; \ No newline at end of file diff --git a/app/soapbox/utils/features.ts b/app/soapbox/utils/features.ts index 51aae6475..363f08c18 100644 --- a/app/soapbox/utils/features.ts +++ b/app/soapbox/utils/features.ts @@ -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/} diff --git a/app/styles/application.scss b/app/styles/application.scss index 2582b0b63..ff2bf20de 100644 --- a/app/styles/application.scss +++ b/app/styles/application.scss @@ -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';