Merge remote-tracking branch 'origin/develop' into fix-avatar-ring

environments/review-fix-avatar-w3rque/deployments/1731
Alex Gleason 2 years ago
commit 7138bec3b0
No known key found for this signature in database
GPG Key ID: 7211D1F99744FBB7

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

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

@ -37,11 +37,9 @@ const ChatPaneHeader = (props: IChatPaneHeader) => {
data-testid='title'
{...buttonProps}
>
{typeof title === 'string' ? (
<Text weight='semibold'>
<Text weight='semibold' tag='div'>
{title}
</Text>
) : (title)}
{(typeof unreadCount !== 'undefined' && unreadCount > 0) && (
<HStack alignItems='center' space={2}>

@ -310,6 +310,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 +389,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

@ -21,7 +21,7 @@ jest.mock('../../../hooks/useDimensions', () => ({
describe('<FeedCarousel />', () => {
let store: any;
describe('with "feedUserFiltering" disabled', () => {
describe('with "carousel" disabled', () => {
beforeEach(() => {
store = {
instance: {
@ -42,7 +42,7 @@ describe('<FeedCarousel />', () => {
});
});
describe('with "feedUserFiltering" enabled', () => {
describe('with "carousel" enabled', () => {
beforeEach(() => {
store = {
instance: {
@ -61,11 +61,17 @@ describe('<FeedCarousel />', () => {
__stub((mock) => {
mock.onGet('/api/v1/truth/carousels/avatars')
.reply(200, [
{ account_id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg' },
{ account_id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg' },
{ account_id: '3', acct: 'c', account_avatar: 'https://example.com/some.jpg' },
{ account_id: '4', acct: 'd', account_avatar: 'https://example.com/some.jpg' },
{ account_id: '1', acct: 'a', account_avatar: 'https://example.com/some.jpg', seen: false },
{ account_id: '2', acct: 'b', account_avatar: 'https://example.com/some.jpg', seen: false },
{ account_id: '3', acct: 'c', account_avatar: 'https://example.com/some.jpg', seen: false },
{ account_id: '4', acct: 'd', account_avatar: 'https://example.com/some.jpg', seen: false },
]);
mock.onGet('/api/v1/accounts/1/statuses').reply(200, [], {
link: '<https://example.com/api/v1/accounts/1/statuses?since_id=1>; rel=\'prev\'',
});
mock.onPost('/api/v1/truth/carousels/avatars/seen').reply(200);
});
});
@ -74,6 +80,29 @@ describe('<FeedCarousel />', () => {
await waitFor(() => {
expect(screen.queryAllByTestId('feed-carousel')).toHaveLength(1);
expect(screen.queryAllByTestId('carousel-item')).toHaveLength(4);
});
});
it('should handle the "seen" state', async() => {
render(<FeedCarousel />, undefined, store);
// Unseen
await waitFor(() => {
expect(screen.queryAllByTestId('carousel-item')).toHaveLength(4);
});
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-accent-500');
// Selected
await userEvent.click(screen.getAllByTestId('carousel-item-avatar')[0]);
await waitFor(() => {
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-primary-600');
});
// Marked as seen, not selected
await userEvent.click(screen.getAllByTestId('carousel-item-avatar')[0]);
await waitFor(() => {
expect(screen.getAllByTestId('carousel-item-avatar')[0]).toHaveClass('ring-transparent');
});
});
});

@ -4,15 +4,17 @@ import { FormattedMessage } from 'react-intl';
import { replaceHomeTimeline } from 'soapbox/actions/timelines';
import { useAppDispatch, useAppSelector, useDimensions } from 'soapbox/hooks';
import useCarouselAvatars from 'soapbox/queries/carousels';
import { Avatar, useCarouselAvatars, useMarkAsSeen } from 'soapbox/queries/carousels';
import { Card, HStack, Icon, Stack, Text } from '../../components/ui';
import PlaceholderAvatar from '../placeholder/components/placeholder-avatar';
const CarouselItem = ({ avatar }: { avatar: any }) => {
const CarouselItem = ({ avatar, seen, onViewed }: { avatar: Avatar, seen: boolean, onViewed: (account_id: string) => void }) => {
const dispatch = useAppDispatch();
const selectedAccountId = useAppSelector(state => state.timelines.get('home')?.feedAccountId);
const markAsSeen = useMarkAsSeen();
const selectedAccountId = useAppSelector(state => state.timelines.getIn(['home', 'feedAccountId']) as string);
const isSelected = avatar.account_id === selectedAccountId;
const [isFetching, setLoading] = useState<boolean>(false);
@ -27,17 +29,25 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
if (isSelected) {
dispatch(replaceHomeTimeline(null, { maxId: null }, () => setLoading(false)));
} else {
onViewed(avatar.account_id);
markAsSeen.mutate(avatar.account_id);
dispatch(replaceHomeTimeline(avatar.account_id, { maxId: null }, () => setLoading(false)));
}
};
return (
<div aria-disabled={isFetching} onClick={handleClick} className='cursor-pointer' role='filter-feed-by-user'>
<div
aria-disabled={isFetching}
onClick={handleClick}
className='cursor-pointer'
role='filter-feed-by-user'
data-testid='carousel-item'
>
<Stack className='w-16 h-auto' space={3}>
<div className='block mx-auto relative w-14 h-14 rounded-full'>
{isSelected && (
<div className='absolute inset-0 bg-primary-600 bg-opacity-50 rounded-full flex items-center justify-center'>
<Icon src={require('@tabler/icons/x.svg')} className='text-white h-6 w-6' />
<Icon src={require('@tabler/icons/check.svg')} className='text-white h-6 w-6' />
</div>
)}
@ -45,10 +55,12 @@ const CarouselItem = ({ avatar }: { avatar: any }) => {
src={avatar.account_avatar}
className={classNames({
'w-14 h-14 min-w-[56px] rounded-full ring-2 ring-offset-4 dark:ring-offset-primary-900': true,
'ring-transparent': !isSelected,
'ring-transparent': !isSelected && seen,
'ring-primary-600': isSelected,
'ring-accent-500': !seen && !isSelected,
})}
alt={avatar.acct}
data-testid='carousel-item-avatar'
/>
</div>
@ -63,6 +75,7 @@ const FeedCarousel = () => {
const [cardRef, setCardRef, { width }] = useDimensions();
const [seenAccountIds, setSeenAccountIds] = useState<string[]>([]);
const [pageSize, setPageSize] = useState<number>(0);
const [currentPage, setCurrentPage] = useState<number>(1);
@ -75,6 +88,20 @@ const FeedCarousel = () => {
const handleNextPage = () => setCurrentPage((prevPage) => prevPage + 1);
const handlePrevPage = () => setCurrentPage((prevPage) => prevPage - 1);
const markAsSeen = (account_id: string) => {
setSeenAccountIds((prev) => [...prev, account_id]);
};
useEffect(() => {
if (avatars.length > 0) {
setSeenAccountIds(
avatars
.filter((avatar) => avatar.seen !== false)
.map((avatar) => avatar.account_id),
);
}
}, [avatars]);
useEffect(() => {
if (width) {
setPageSize(Math.round(width / widthPerAvatar));
@ -130,6 +157,8 @@ const FeedCarousel = () => {
<CarouselItem
key={avatar.account_id}
avatar={avatar}
seen={seenAccountIds?.includes(avatar.account_id)}
onViewed={markAsSeen}
/>
))
)}

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

@ -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>
<div className='translate-y-0.5 w-full'>
<ComposeForm
id='home'
shouldCondense
autoFocus={false}
clickableAreaRef={composeBlock}
/>
</div>
</HStack>
</CardBody>
</Card>
)}
{features.feedUserFiltering && <FeedCarousel />}
{features.carousel && <FeedCarousel />}
{children}

@ -1,7 +1,7 @@
import { __stub } from 'soapbox/api';
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import useCarouselAvatars from '../carousels';
import { useCarouselAvatars } from '../carousels';
describe('useCarouselAvatars', () => {
describe('with a successful query', () => {

@ -11,8 +11,6 @@ import { flattenPages } from 'soapbox/utils/queries';
import { IAccount } from '../accounts';
import { ChatKeys, IChat, IChatMessage, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats';
jest.mock('soapbox/utils/queries');
const chat: IChat = {
accepted: true,
account: {

@ -3,7 +3,7 @@ import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
import { useOnboardingSuggestions } from '../suggestions';
describe('useCarouselAvatars', () => {
describe('useOnboardingSuggestions', () => {
describe('with a successful query', () => {
beforeEach(() => {
__stub((mock) => {

@ -1,14 +1,19 @@
import { useQuery } from '@tanstack/react-query';
import { useMutation, useQuery } from '@tanstack/react-query';
import { useApi } from 'soapbox/hooks';
import { useApi, useFeatures } from 'soapbox/hooks';
type Avatar = {
export type Avatar = {
account_id: string
account_avatar: string
username: string
acct: string
seen?: boolean
}
export default function useCarouselAvatars() {
const CarouselKeys = {
avatars: ['carouselAvatars'] as const,
};
function useCarouselAvatars() {
const api = useApi();
const getCarouselAvatars = async() => {
@ -16,8 +21,9 @@ export default function useCarouselAvatars() {
return data;
};
const result = useQuery<Avatar[]>(['carouselAvatars'], getCarouselAvatars, {
const result = useQuery<Avatar[]>(CarouselKeys.avatars, getCarouselAvatars, {
placeholderData: [],
keepPreviousData: true,
});
const avatars = result.data;
@ -27,3 +33,18 @@ export default function useCarouselAvatars() {
data: avatars || [],
};
}
function useMarkAsSeen() {
const api = useApi();
const features = useFeatures();
return useMutation(async (accountId: string) => {
if (features.carouselSeen) {
await void api.post('/api/v1/truth/carousels/avatars/seen', {
account_id: accountId,
});
}
});
}
export { useCarouselAvatars, useMarkAsSeen };

@ -8,6 +8,7 @@ import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context
import { useStatContext } from 'soapbox/contexts/stat-context';
import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
import { normalizeChatMessage } from 'soapbox/normalizers';
import { reOrderChatListItems } from 'soapbox/utils/chats';
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
import { queryClient } from './client';
@ -280,6 +281,7 @@ const useChatActions = (chatId: string) => {
onSuccess: (response, variables) => {
const nextChat = { ...chat, last_message: response.data };
updatePageItem(ChatKeys.chatSearch(), nextChat, (o, n) => o.id === n.id);
reOrderChatListItems();
queryClient.invalidateQueries(ChatKeys.chatMessages(variables.chatId));
},

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

@ -26,7 +26,10 @@ const updateChatInChatSearchQuery = (newChat: ChatPayload) => {
*/
const reOrderChatListItems = () => {
sortQueryData<ChatPayload>(ChatKeys.chatSearch(), (chatA, chatB) => {
return compareDate(chatA.last_message?.created_at as string, chatB.last_message?.created_at as string);
return compareDate(
chatA.last_message?.created_at as string,
chatB.last_message?.created_at as string,
);
});
};
@ -81,4 +84,4 @@ const getUnreadChatsCount = (): number => {
return sumBy(chats, chat => chat.unread);
};
export { updateChatListItem, getUnreadChatsCount };
export { updateChatListItem, getUnreadChatsCount, reOrderChatListItems };

@ -205,6 +205,19 @@ const getInstanceFeatures = (instance: Instance) => {
v.software === PLEROMA,
]),
/**
* Whether to show the Feed Carousel for suggested Statuses.
* @see GET /api/v1/truth/carousels/avatars
* @see GET /api/v1/truth/carousels/suggestions
*/
carousel: v.software === TRUTHSOCIAL,
/**
* Ability to mark a carousel avatar as "seen."
* @see POST /api/v1/truth/carousels/avatars/seen
*/
carouselSeen: v.software === TRUTHSOCIAL,
/**
* Ability to accept a chat.
* POST /api/v1/pleroma/chats/:id/accept
@ -371,9 +384,6 @@ const getInstanceFeatures = (instance: Instance) => {
/** Whether the instance federates. */
federating: federation.get('enabled', true) === true, // Assume true unless explicitly false
/** Whether or not to show the Feed Carousel for suggested Statuses */
feedUserFiltering: v.software === TRUTHSOCIAL,
/**
* Can edit and manage timeline filters (aka "muted words").
* @see {@link https://docs.joinmastodon.org/methods/accounts/filters/}

@ -4,6 +4,7 @@
@import '~@fontsource/inter/300.css';
@import '~@fontsource/inter/400.css';
@import '~@fontsource/inter/500.css';
@import '~@fontsource/inter/600.css';
@import '~@fontsource/inter/700.css';
@import '~@fontsource/inter/900.css';

Loading…
Cancel
Save