Move pinned posts to a carousel (#34754)
Co-authored-by: diondiondion <mail@diondiondion.com>
This commit is contained in:
parent
47512fe518
commit
ba5320671c
7 changed files with 282 additions and 49 deletions
211
app/javascript/mastodon/components/featured_carousel.tsx
Normal file
211
app/javascript/mastodon/components/featured_carousel.tsx
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
import type { ComponentPropsWithRef } from 'react';
|
||||||
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import type { Map as ImmutableMap } from 'immutable';
|
||||||
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
|
import type { AnimatedProps } from '@react-spring/web';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
import { useDrag } from '@use-gesture/react';
|
||||||
|
|
||||||
|
import { expandAccountFeaturedTimeline } from '@/mastodon/actions/timelines';
|
||||||
|
import { IconButton } from '@/mastodon/components/icon_button';
|
||||||
|
import StatusContainer from '@/mastodon/containers/status_container';
|
||||||
|
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
|
||||||
|
import ChevronLeftIcon from '@/material-icons/400-24px/chevron_left.svg?react';
|
||||||
|
import ChevronRightIcon from '@/material-icons/400-24px/chevron_right.svg?react';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
previous: { id: 'featured_carousel.previous', defaultMessage: 'Previous' },
|
||||||
|
next: { id: 'featured_carousel.next', defaultMessage: 'Next' },
|
||||||
|
slide: {
|
||||||
|
id: 'featured_carousel.slide',
|
||||||
|
defaultMessage: '{index} of {total}',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
export const FeaturedCarousel: React.FC<{
|
||||||
|
accountId: string;
|
||||||
|
tagged?: string;
|
||||||
|
}> = ({ accountId, tagged }) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
// Load pinned statuses
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
useEffect(() => {
|
||||||
|
if (accountId) {
|
||||||
|
void dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
|
||||||
|
}
|
||||||
|
}, [accountId, dispatch, tagged]);
|
||||||
|
const pinnedStatuses = useAppSelector(
|
||||||
|
(state) =>
|
||||||
|
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
||||||
|
[`account:${accountId}:pinned${tagged ? `:${tagged}` : ''}`, 'items'],
|
||||||
|
ImmutableList(),
|
||||||
|
) as ImmutableList<string>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle slide change
|
||||||
|
const [slideIndex, setSlideIndex] = useState(0);
|
||||||
|
const wrapperRef = useRef<HTMLDivElement>(null);
|
||||||
|
const handleSlideChange = useCallback(
|
||||||
|
(direction: number) => {
|
||||||
|
setSlideIndex((prev) => {
|
||||||
|
const max = pinnedStatuses.size - 1;
|
||||||
|
let newIndex = prev + direction;
|
||||||
|
if (newIndex < 0) {
|
||||||
|
newIndex = max;
|
||||||
|
} else if (newIndex > max) {
|
||||||
|
newIndex = 0;
|
||||||
|
}
|
||||||
|
const slide = wrapperRef.current?.children[newIndex];
|
||||||
|
if (slide) {
|
||||||
|
setCurrentSlideHeight(slide.scrollHeight);
|
||||||
|
}
|
||||||
|
return newIndex;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[pinnedStatuses.size],
|
||||||
|
);
|
||||||
|
|
||||||
|
// Handle slide heights
|
||||||
|
const [currentSlideHeight, setCurrentSlideHeight] = useState(
|
||||||
|
wrapperRef.current?.scrollHeight ?? 0,
|
||||||
|
);
|
||||||
|
const observerRef = useRef<ResizeObserver>(
|
||||||
|
new ResizeObserver(() => {
|
||||||
|
handleSlideChange(0);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const wrapperStyles = useSpring({
|
||||||
|
x: `-${slideIndex * 100}%`,
|
||||||
|
height: currentSlideHeight,
|
||||||
|
});
|
||||||
|
useEffect(() => {
|
||||||
|
// Update slide height when the component mounts
|
||||||
|
if (currentSlideHeight === 0) {
|
||||||
|
handleSlideChange(0);
|
||||||
|
}
|
||||||
|
}, [currentSlideHeight, handleSlideChange]);
|
||||||
|
|
||||||
|
// Handle swiping animations
|
||||||
|
const bind = useDrag(({ swipe: [swipeX] }) => {
|
||||||
|
handleSlideChange(swipeX * -1); // Invert swipe as swiping left loads the next slide.
|
||||||
|
});
|
||||||
|
const handlePrev = useCallback(() => {
|
||||||
|
handleSlideChange(-1);
|
||||||
|
}, [handleSlideChange]);
|
||||||
|
const handleNext = useCallback(() => {
|
||||||
|
handleSlideChange(1);
|
||||||
|
}, [handleSlideChange]);
|
||||||
|
|
||||||
|
if (!accountId || pinnedStatuses.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className='featured-carousel'
|
||||||
|
{...bind()}
|
||||||
|
aria-roledescription='carousel'
|
||||||
|
aria-labelledby='featured-carousel-title'
|
||||||
|
role='region'
|
||||||
|
>
|
||||||
|
<div className='featured-carousel__header'>
|
||||||
|
<h4 className='featured-carousel__title' id='featured-carousel-title'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='featured_carousel.header'
|
||||||
|
defaultMessage='{count, plural, one {Pinned Post} other {Pinned Posts}}'
|
||||||
|
values={{ count: pinnedStatuses.size }}
|
||||||
|
/>
|
||||||
|
</h4>
|
||||||
|
{pinnedStatuses.size > 1 && (
|
||||||
|
<>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.previous)}
|
||||||
|
icon='chevron-left'
|
||||||
|
iconComponent={ChevronLeftIcon}
|
||||||
|
onClick={handlePrev}
|
||||||
|
/>
|
||||||
|
<span aria-live='polite'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='featured_carousel.post'
|
||||||
|
defaultMessage='Post'
|
||||||
|
>
|
||||||
|
{(text) => <span className='sr-only'>{text}</span>}
|
||||||
|
</FormattedMessage>
|
||||||
|
{slideIndex + 1} / {pinnedStatuses.size}
|
||||||
|
</span>
|
||||||
|
<IconButton
|
||||||
|
title={intl.formatMessage(messages.next)}
|
||||||
|
icon='chevron-right'
|
||||||
|
iconComponent={ChevronRightIcon}
|
||||||
|
onClick={handleNext}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<animated.div
|
||||||
|
className='featured-carousel__slides'
|
||||||
|
ref={wrapperRef}
|
||||||
|
style={wrapperStyles}
|
||||||
|
aria-atomic='false'
|
||||||
|
aria-live='polite'
|
||||||
|
>
|
||||||
|
{pinnedStatuses.map((statusId, index) => (
|
||||||
|
<FeaturedCarouselItem
|
||||||
|
key={`f-${statusId}`}
|
||||||
|
data-index={index}
|
||||||
|
aria-label={intl.formatMessage(messages.slide, {
|
||||||
|
index: index + 1,
|
||||||
|
total: pinnedStatuses.size,
|
||||||
|
})}
|
||||||
|
statusId={statusId}
|
||||||
|
observer={observerRef.current}
|
||||||
|
active={index === slideIndex}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</animated.div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FeaturedCarouselItemProps {
|
||||||
|
statusId: string;
|
||||||
|
active: boolean;
|
||||||
|
observer: ResizeObserver;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FeaturedCarouselItem: React.FC<
|
||||||
|
FeaturedCarouselItemProps & AnimatedProps<ComponentPropsWithRef<'div'>>
|
||||||
|
> = ({ statusId, active, observer, ...props }) => {
|
||||||
|
const handleRef = useCallback(
|
||||||
|
(instance: HTMLDivElement | null) => {
|
||||||
|
if (instance) {
|
||||||
|
observer.observe(instance);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[observer],
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<animated.div
|
||||||
|
className='featured-carousel__slide'
|
||||||
|
// @ts-expect-error inert in not in this version of React
|
||||||
|
inert={!active ? 'true' : undefined}
|
||||||
|
aria-roledescription='slide'
|
||||||
|
role='group'
|
||||||
|
ref={handleRef}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<StatusContainer
|
||||||
|
// @ts-expect-error inferred props are wrong
|
||||||
|
id={statusId}
|
||||||
|
contextType='account'
|
||||||
|
withCounters
|
||||||
|
/>
|
||||||
|
</animated.div>
|
||||||
|
);
|
||||||
|
};
|
|
@ -29,7 +29,7 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||||
message = (
|
message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='empty_column.account_featured.me'
|
id='empty_column.account_featured.me'
|
||||||
defaultMessage='You have not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friend’s accounts on your profile?'
|
defaultMessage='You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?'
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (suspended) {
|
} else if (suspended) {
|
||||||
|
@ -52,7 +52,7 @@ export const EmptyMessage: React.FC<EmptyMessageProps> = ({
|
||||||
message = (
|
message = (
|
||||||
<FormattedMessage
|
<FormattedMessage
|
||||||
id='empty_column.account_featured.other'
|
id='empty_column.account_featured.other'
|
||||||
defaultMessage='{acct} has not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friend’s accounts on your profile?'
|
defaultMessage='{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?'
|
||||||
values={{ acct }}
|
values={{ acct }}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,17 +4,14 @@ import { FormattedMessage } from 'react-intl';
|
||||||
|
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
|
|
||||||
import type { Map as ImmutableMap } from 'immutable';
|
|
||||||
import { List as ImmutableList } from 'immutable';
|
import { List as ImmutableList } from 'immutable';
|
||||||
|
|
||||||
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
|
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
|
||||||
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
|
import { fetchFeaturedTags } from 'mastodon/actions/featured_tags';
|
||||||
import { expandAccountFeaturedTimeline } from 'mastodon/actions/timelines';
|
|
||||||
import { Account } from 'mastodon/components/account';
|
import { Account } from 'mastodon/components/account';
|
||||||
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
import { ColumnBackButton } from 'mastodon/components/column_back_button';
|
||||||
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
|
||||||
import { RemoteHint } from 'mastodon/components/remote_hint';
|
import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||||
import { StatusQuoteManager } from 'mastodon/components/status_quoted';
|
|
||||||
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
|
||||||
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
|
||||||
import Column from 'mastodon/features/ui/components/column';
|
import Column from 'mastodon/features/ui/components/column';
|
||||||
|
@ -43,7 +40,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (accountId) {
|
if (accountId) {
|
||||||
void dispatch(expandAccountFeaturedTimeline(accountId));
|
|
||||||
void dispatch(fetchFeaturedTags({ accountId }));
|
void dispatch(fetchFeaturedTags({ accountId }));
|
||||||
void dispatch(fetchEndorsedAccounts({ accountId }));
|
void dispatch(fetchEndorsedAccounts({ accountId }));
|
||||||
}
|
}
|
||||||
|
@ -52,10 +48,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||||
const isLoading = useAppSelector(
|
const isLoading = useAppSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
!accountId ||
|
!accountId ||
|
||||||
!!(state.timelines as ImmutableMap<string, unknown>).getIn([
|
|
||||||
`account:${accountId}:pinned`,
|
|
||||||
'isLoading',
|
|
||||||
]) ||
|
|
||||||
!!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
|
!!state.user_lists.getIn(['featured_tags', accountId, 'isLoading']),
|
||||||
);
|
);
|
||||||
const featuredTags = useAppSelector(
|
const featuredTags = useAppSelector(
|
||||||
|
@ -65,13 +57,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||||
ImmutableList(),
|
ImmutableList(),
|
||||||
) as ImmutableList<TagMap>,
|
) as ImmutableList<TagMap>,
|
||||||
);
|
);
|
||||||
const featuredStatusIds = useAppSelector(
|
|
||||||
(state) =>
|
|
||||||
(state.timelines as ImmutableMap<string, unknown>).getIn(
|
|
||||||
[`account:${accountId}:pinned`, 'items'],
|
|
||||||
ImmutableList(),
|
|
||||||
) as ImmutableList<string>,
|
|
||||||
);
|
|
||||||
const featuredAccountIds = useAppSelector(
|
const featuredAccountIds = useAppSelector(
|
||||||
(state) =>
|
(state) =>
|
||||||
state.user_lists.getIn(
|
state.user_lists.getIn(
|
||||||
|
@ -94,11 +79,7 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
if (featuredTags.isEmpty() && featuredAccountIds.isEmpty()) {
|
||||||
featuredStatusIds.isEmpty() &&
|
|
||||||
featuredTags.isEmpty() &&
|
|
||||||
featuredAccountIds.isEmpty()
|
|
||||||
) {
|
|
||||||
return (
|
return (
|
||||||
<AccountFeaturedWrapper accountId={accountId}>
|
<AccountFeaturedWrapper accountId={accountId}>
|
||||||
<EmptyMessage
|
<EmptyMessage
|
||||||
|
@ -133,23 +114,6 @@ const AccountFeatured: React.FC<{ multiColumn: boolean }> = ({
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{!featuredStatusIds.isEmpty() && (
|
|
||||||
<>
|
|
||||||
<h4 className='column-subheading'>
|
|
||||||
<FormattedMessage
|
|
||||||
id='account.featured.posts'
|
|
||||||
defaultMessage='Posts'
|
|
||||||
/>
|
|
||||||
</h4>
|
|
||||||
{featuredStatusIds.map((statusId) => (
|
|
||||||
<StatusQuoteManager
|
|
||||||
key={`f-${statusId}`}
|
|
||||||
id={statusId}
|
|
||||||
contextType='account'
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{!featuredAccountIds.isEmpty() && (
|
{!featuredAccountIds.isEmpty() && (
|
||||||
<>
|
<>
|
||||||
<h4 className='column-subheading'>
|
<h4 className='column-subheading'>
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { RemoteHint } from 'mastodon/components/remote_hint';
|
||||||
|
|
||||||
import { AccountHeader } from './components/account_header';
|
import { AccountHeader } from './components/account_header';
|
||||||
import { LimitedAccountHint } from './components/limited_account_hint';
|
import { LimitedAccountHint } from './components/limited_account_hint';
|
||||||
|
import { FeaturedCarousel } from '@/mastodon/components/featured_carousel';
|
||||||
|
|
||||||
const emptyList = ImmutableList();
|
const emptyList = ImmutableList();
|
||||||
|
|
||||||
|
@ -169,7 +170,12 @@ class AccountTimeline extends ImmutablePureComponent {
|
||||||
<ColumnBackButton />
|
<ColumnBackButton />
|
||||||
|
|
||||||
<StatusList
|
<StatusList
|
||||||
prepend={<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />}
|
prepend={
|
||||||
|
<>
|
||||||
|
<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={this.props.params.tagged} />
|
||||||
|
<FeaturedCarousel accountId={this.props.accountId} />
|
||||||
|
</>
|
||||||
|
}
|
||||||
alwaysPrepend
|
alwaysPrepend
|
||||||
append={<RemoteHint accountId={accountId} />}
|
append={<RemoteHint accountId={accountId} />}
|
||||||
scrollKey='account_timeline'
|
scrollKey='account_timeline'
|
||||||
|
|
|
@ -34,7 +34,6 @@
|
||||||
"account.featured": "Featured",
|
"account.featured": "Featured",
|
||||||
"account.featured.accounts": "Profiles",
|
"account.featured.accounts": "Profiles",
|
||||||
"account.featured.hashtags": "Hashtags",
|
"account.featured.hashtags": "Hashtags",
|
||||||
"account.featured.posts": "Posts",
|
|
||||||
"account.featured_tags.last_status_at": "Last post on {date}",
|
"account.featured_tags.last_status_at": "Last post on {date}",
|
||||||
"account.featured_tags.last_status_never": "No posts",
|
"account.featured_tags.last_status_never": "No posts",
|
||||||
"account.follow": "Follow",
|
"account.follow": "Follow",
|
||||||
|
@ -173,7 +172,7 @@
|
||||||
"column.lists": "Lists",
|
"column.lists": "Lists",
|
||||||
"column.mutes": "Muted users",
|
"column.mutes": "Muted users",
|
||||||
"column.notifications": "Notifications",
|
"column.notifications": "Notifications",
|
||||||
"column.pins": "Featured posts",
|
"column.pins": "Pinned posts",
|
||||||
"column.public": "Federated timeline",
|
"column.public": "Federated timeline",
|
||||||
"column_back_button.label": "Back",
|
"column_back_button.label": "Back",
|
||||||
"column_header.hide_settings": "Hide settings",
|
"column_header.hide_settings": "Hide settings",
|
||||||
|
@ -309,8 +308,8 @@
|
||||||
"emoji_button.search_results": "Search results",
|
"emoji_button.search_results": "Search results",
|
||||||
"emoji_button.symbols": "Symbols",
|
"emoji_button.symbols": "Symbols",
|
||||||
"emoji_button.travel": "Travel & Places",
|
"emoji_button.travel": "Travel & Places",
|
||||||
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friend’s accounts on your profile?",
|
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
|
||||||
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your posts, hashtags you use the most, and even your friend’s accounts on your profile?",
|
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
|
||||||
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
|
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
|
||||||
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
"empty_column.account_hides_collections": "This user has chosen to not make this information available",
|
||||||
"empty_column.account_suspended": "Account suspended",
|
"empty_column.account_suspended": "Account suspended",
|
||||||
|
@ -344,6 +343,11 @@
|
||||||
"explore.trending_links": "News",
|
"explore.trending_links": "News",
|
||||||
"explore.trending_statuses": "Posts",
|
"explore.trending_statuses": "Posts",
|
||||||
"explore.trending_tags": "Hashtags",
|
"explore.trending_tags": "Hashtags",
|
||||||
|
"featured_carousel.header": "{count, plural, one {Pinned Post} other {Pinned Posts}}",
|
||||||
|
"featured_carousel.next": "Next",
|
||||||
|
"featured_carousel.post": "Post",
|
||||||
|
"featured_carousel.previous": "Previous",
|
||||||
|
"featured_carousel.slide": "{index} of {total}",
|
||||||
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
"filter_modal.added.context_mismatch_explanation": "This filter category does not apply to the context in which you have accessed this post. If you want the post to be filtered in this context too, you will have to edit the filter.",
|
||||||
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
"filter_modal.added.context_mismatch_title": "Context mismatch!",
|
||||||
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
"filter_modal.added.expired_explanation": "This filter category has expired, you will need to change the expiration date for it to apply.",
|
||||||
|
@ -484,7 +488,7 @@
|
||||||
"keyboard_shortcuts.my_profile": "Open your profile",
|
"keyboard_shortcuts.my_profile": "Open your profile",
|
||||||
"keyboard_shortcuts.notifications": "Open notifications column",
|
"keyboard_shortcuts.notifications": "Open notifications column",
|
||||||
"keyboard_shortcuts.open_media": "Open media",
|
"keyboard_shortcuts.open_media": "Open media",
|
||||||
"keyboard_shortcuts.pinned": "Open featured posts list",
|
"keyboard_shortcuts.pinned": "Open pinned posts list",
|
||||||
"keyboard_shortcuts.profile": "Open author's profile",
|
"keyboard_shortcuts.profile": "Open author's profile",
|
||||||
"keyboard_shortcuts.reply": "Reply to post",
|
"keyboard_shortcuts.reply": "Reply to post",
|
||||||
"keyboard_shortcuts.requests": "Open follow requests list",
|
"keyboard_shortcuts.requests": "Open follow requests list",
|
||||||
|
@ -567,7 +571,7 @@
|
||||||
"navigation_bar.mutes": "Muted users",
|
"navigation_bar.mutes": "Muted users",
|
||||||
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
|
"navigation_bar.opened_in_classic_interface": "Posts, accounts, and other specific pages are opened by default in the classic web interface.",
|
||||||
"navigation_bar.personal": "Personal",
|
"navigation_bar.personal": "Personal",
|
||||||
"navigation_bar.pins": "Featured posts",
|
"navigation_bar.pins": "Pinned posts",
|
||||||
"navigation_bar.preferences": "Preferences",
|
"navigation_bar.preferences": "Preferences",
|
||||||
"navigation_bar.public_timeline": "Federated timeline",
|
"navigation_bar.public_timeline": "Federated timeline",
|
||||||
"navigation_bar.search": "Search",
|
"navigation_bar.search": "Search",
|
||||||
|
@ -863,7 +867,7 @@
|
||||||
"status.mute": "Mute @{name}",
|
"status.mute": "Mute @{name}",
|
||||||
"status.mute_conversation": "Mute conversation",
|
"status.mute_conversation": "Mute conversation",
|
||||||
"status.open": "Expand this post",
|
"status.open": "Expand this post",
|
||||||
"status.pin": "Feature on profile",
|
"status.pin": "Pin on profile",
|
||||||
"status.quote_error.filtered": "Hidden due to one of your filters",
|
"status.quote_error.filtered": "Hidden due to one of your filters",
|
||||||
"status.quote_error.not_found": "This post cannot be displayed.",
|
"status.quote_error.not_found": "This post cannot be displayed.",
|
||||||
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
|
"status.quote_error.pending_approval": "This post is pending approval from the original author.",
|
||||||
|
@ -895,7 +899,7 @@
|
||||||
"status.translated_from_with": "Translated from {lang} using {provider}",
|
"status.translated_from_with": "Translated from {lang} using {provider}",
|
||||||
"status.uncached_media_warning": "Preview not available",
|
"status.uncached_media_warning": "Preview not available",
|
||||||
"status.unmute_conversation": "Unmute conversation",
|
"status.unmute_conversation": "Unmute conversation",
|
||||||
"status.unpin": "Don't feature on profile",
|
"status.unpin": "Unpin from profile",
|
||||||
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
"subscribed_languages.lead": "Only posts in selected languages will appear on your home and list timelines after the change. Select none to receive posts in all languages.",
|
||||||
"subscribed_languages.save": "Save changes",
|
"subscribed_languages.save": "Save changes",
|
||||||
"subscribed_languages.target": "Change subscribed languages for {target}",
|
"subscribed_languages.target": "Change subscribed languages for {target}",
|
||||||
|
|
|
@ -567,3 +567,9 @@ a.sparkline {
|
||||||
opacity: 0.25;
|
opacity: 0.25;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.featured-carousel {
|
||||||
|
background: var(--nested-card-background);
|
||||||
|
border-bottom: var(--nested-card-border);
|
||||||
|
color: var(--nested-card-text);
|
||||||
|
}
|
||||||
|
|
|
@ -3674,7 +3674,8 @@ $ui-header-logo-wordmark-width: 99px;
|
||||||
-webkit-tap-highlight-color: transparent;
|
-webkit-tap-highlight-color: transparent;
|
||||||
}
|
}
|
||||||
|
|
||||||
.react-toggle-screenreader-only {
|
.react-toggle-screenreader-only,
|
||||||
|
.sr-only {
|
||||||
border: 0;
|
border: 0;
|
||||||
clip: rect(0 0 0 0);
|
clip: rect(0 0 0 0);
|
||||||
height: 1px;
|
height: 1px;
|
||||||
|
@ -8536,6 +8537,8 @@ noscript {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
bottom: 3px;
|
bottom: 3px;
|
||||||
inset-inline-end: 0;
|
inset-inline-end: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11033,3 +11036,42 @@ noscript {
|
||||||
.lists-scrollable {
|
.lists-scrollable {
|
||||||
min-height: 50vh;
|
min-height: 50vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.featured-carousel {
|
||||||
|
background: var(--surface-background-color);
|
||||||
|
overflow: hidden;
|
||||||
|
flex-shrink: 0;
|
||||||
|
border-bottom: 1px solid var(--background-border-color);
|
||||||
|
touch-action: pan-x;
|
||||||
|
|
||||||
|
&__slides {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: start;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__slide {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__header {
|
||||||
|
padding: 8px 16px;
|
||||||
|
color: $darker-text-color;
|
||||||
|
inset-inline-end: 0;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&__title {
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 500;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue