Merge remote-tracking branch 'origin/develop' into fix-avatar-ring
This commit is contained in:
commit
7138bec3b0
17 changed files with 188 additions and 147 deletions
|
@ -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'>
|
||||
{title}
|
||||
</Text>
|
||||
) : (title)}
|
||||
<Text weight='semibold' tag='div'>
|
||||
{title}
|
||||
</Text>
|
||||
|
||||
{(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>
|
||||
|
||||
<ComposeForm
|
||||
id='home'
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
/>
|
||||
<div className='translate-y-0.5 w-full'>
|
||||
<ComposeForm
|
||||
id='home'
|
||||
shouldCondense
|
||||
autoFocus={false}
|
||||
clickableAreaRef={composeBlock}
|
||||
/>
|
||||
</div>
|
||||
</HStack>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{features.feedUserFiltering && <FeedCarousel />}
|
||||
{features.carousel && <FeedCarousel />}
|
||||
|
||||
{children}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { __stub } from 'soapbox/api';
|
||||
import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
||||
|
||||
import useCarouselAvatars from '../carousels';
|
||||
import { useCarouselAvatars } from '../carousels';
|
||||
|
||||
describe('useCarouselAvatars', () => {
|
||||
describe('with a successful query', () => {
|
||||
|
|
|
@ -11,8 +11,6 @@ import { flattenPages } from 'soapbox/utils/queries';
|
|||
import { IAccount } from '../accounts';
|
||||
import { ChatKeys, IChat, IChatMessage, isLastMessage, useChat, useChatActions, useChatMessages, useChats } from '../chats';
|
||||
|
||||
jest.mock('soapbox/utils/queries');
|
||||
|
||||
const chat: IChat = {
|
||||
accepted: true,
|
||||
account: {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { renderHook, waitFor } from 'soapbox/jest/test-helpers';
|
|||
|
||||
import { useOnboardingSuggestions } from '../suggestions';
|
||||
|
||||
describe('useCarouselAvatars', () => {
|
||||
describe('useOnboardingSuggestions', () => {
|
||||
describe('with a successful query', () => {
|
||||
beforeEach(() => {
|
||||
__stub((mock) => {
|
||||
|
|
|
@ -1,14 +1,19 @@
|
|||
import { useQuery } from '@tanstack/react-query';
|
||||
import { useMutation, useQuery } from '@tanstack/react-query';
|
||||
|
||||
import { useApi } from 'soapbox/hooks';
|
||||
import { useApi, useFeatures } from 'soapbox/hooks';
|
||||
|
||||
type Avatar = {
|
||||
export type Avatar = {
|
||||
account_id: string
|
||||
account_avatar: string
|
||||
username: string
|
||||
acct: string
|
||||
seen?: boolean
|
||||
}
|
||||
|
||||
export default function useCarouselAvatars() {
|
||||
const CarouselKeys = {
|
||||
avatars: ['carouselAvatars'] as const,
|
||||
};
|
||||
|
||||
function useCarouselAvatars() {
|
||||
const api = useApi();
|
||||
|
||||
const getCarouselAvatars = async() => {
|
||||
|
@ -16,8 +21,9 @@ export default function useCarouselAvatars() {
|
|||
return data;
|
||||
};
|
||||
|
||||
const result = useQuery<Avatar[]>(['carouselAvatars'], getCarouselAvatars, {
|
||||
const result = useQuery<Avatar[]>(CarouselKeys.avatars, getCarouselAvatars, {
|
||||
placeholderData: [],
|
||||
keepPreviousData: true,
|
||||
});
|
||||
|
||||
const avatars = result.data;
|
||||
|
@ -27,3 +33,18 @@ export default function useCarouselAvatars() {
|
|||
data: avatars || [],
|
||||
};
|
||||
}
|
||||
|
||||
function useMarkAsSeen() {
|
||||
const api = useApi();
|
||||
const features = useFeatures();
|
||||
|
||||
return useMutation(async (accountId: string) => {
|
||||
if (features.carouselSeen) {
|
||||
await void api.post('/api/v1/truth/carousels/avatars/seen', {
|
||||
account_id: accountId,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export { useCarouselAvatars, useMarkAsSeen };
|
|
@ -8,6 +8,7 @@ import { ChatWidgetScreens, useChatContext } from 'soapbox/contexts/chat-context
|
|||
import { useStatContext } from 'soapbox/contexts/stat-context';
|
||||
import { useApi, useAppDispatch, useAppSelector, useFeatures, useOwnAccount } from 'soapbox/hooks';
|
||||
import { normalizeChatMessage } from 'soapbox/normalizers';
|
||||
import { reOrderChatListItems } from 'soapbox/utils/chats';
|
||||
import { flattenPages, PaginatedResult, updatePageItem } from 'soapbox/utils/queries';
|
||||
|
||||
import { queryClient } from './client';
|
||||
|
@ -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…
Reference in a new issue