Merge commit 'fb89198460f9cebf624cc5ca0b139563ae4fd120' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2026-02-10 18:02:15 +01:00
15 changed files with 444 additions and 198 deletions
@@ -5,7 +5,6 @@ import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import { useIdentity } from '@/mastodon/identity_context';
import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import {
fetchRelationships,
followAccount,
@@ -60,7 +59,14 @@ export const FollowButton: React.FC<{
compact?: boolean;
labelLength?: 'auto' | 'short' | 'long';
className?: string;
}> = ({ accountId, compact, labelLength = 'auto', className }) => {
withUnmute?: boolean;
}> = ({
accountId,
compact,
labelLength = 'auto',
className,
withUnmute = true,
}) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const { signedIn } = useIdentity();
@@ -102,10 +108,7 @@ export const FollowButton: React.FC<{
modalProps: { account },
}),
);
} else if (
relationship.muting &&
!isClientFeatureEnabled('profile_redesign')
) {
} else if (relationship.muting && withUnmute) {
dispatch(unmuteAccount(accountId));
} else if (account && relationship.following) {
dispatch(
@@ -121,7 +124,7 @@ export const FollowButton: React.FC<{
} else {
dispatch(followAccount(accountId));
}
}, [dispatch, accountId, relationship, account, signedIn]);
}, [signedIn, relationship, accountId, withUnmute, account, dispatch]);
const isNarrow = useBreakpoint('narrow');
const useShortLabel =
@@ -140,10 +143,7 @@ export const FollowButton: React.FC<{
label = intl.formatMessage(messages.editProfile);
} else if (!relationship) {
label = <LoadingIndicator />;
} else if (
relationship.muting &&
!isClientFeatureEnabled('profile_redesign')
) {
} else if (relationship.muting && withUnmute) {
label = intl.formatMessage(messages.unmute);
} else if (relationship.following) {
label = intl.formatMessage(messages.unfollow);
@@ -1,19 +1,24 @@
import type { RefCallback } from 'react';
import { useCallback, useEffect, useState } from 'react';
import { useCallback } from 'react';
import classNames from 'classnames';
import { Helmet } from 'react-helmet';
import { openModal } from '@/mastodon/actions/modal';
import { AccountBio } from '@/mastodon/components/account_bio';
import { Avatar } from '@/mastodon/components/avatar';
import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context';
import { openModal } from 'mastodon/actions/modal';
import { Avatar } from 'mastodon/components/avatar';
import { AccountNote } from 'mastodon/features/account/components/account_note';
import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container';
import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state';
import type { Account } from 'mastodon/models/account';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
import { AccountNote } from '@/mastodon/features/account/components/account_note';
import FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
import { useLayout } from '@/mastodon/hooks/useLayout';
import { useVisibility } from '@/mastodon/hooks/useVisibility';
import {
autoPlayGif,
me,
domain as localDomain,
} from '@/mastodon/initial_state';
import type { Account } from '@/mastodon/models/account';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { isRedesignEnabled } from '../common';
@@ -46,6 +51,8 @@ export const AccountHeader: React.FC<{
accountId: string;
hideTabs?: boolean;
}> = ({ accountId, hideTabs }) => {
const isRedesign = isRedesignEnabled();
const dispatch = useAppDispatch();
const account = useAppSelector((state) => state.accounts.get(accountId));
const relationship = useAppSelector((state) =>
@@ -78,39 +85,12 @@ export const AccountHeader: React.FC<{
[dispatch, account],
);
const [isFooterIntersecting, setIsIntersecting] = useState(false);
const handleIntersect: IntersectionObserverCallback = useCallback(
(entries) => {
const entry = entries.at(0);
if (!entry) {
return;
}
setIsIntersecting(entry.isIntersecting);
const { layout } = useLayout();
const { observedRef, isIntersecting } = useVisibility({
observerOptions: {
rootMargin: layout === 'mobile' ? '0px 0px -55px 0px' : '', // Height of bottom nav bar.
},
[],
);
const [observer] = useState(
() =>
new IntersectionObserver(handleIntersect, {
rootMargin: '0px 0px -55px 0px', // Height of bottom nav bar.
}),
);
const handleObserverRef: RefCallback<HTMLDivElement> = useCallback(
(node) => {
if (node) {
observer.observe(node);
}
},
[observer],
);
useEffect(() => {
return () => {
observer.disconnect();
};
}, [observer]);
});
if (!account) {
return null;
@@ -118,6 +98,7 @@ export const AccountHeader: React.FC<{
const suspendedOrHidden = hidden || account.suspended;
const isLocal = !account.acct.includes('@');
const isMe = me && account.id === me;
return (
<div className='account-timeline__header'>
@@ -135,8 +116,13 @@ export const AccountHeader: React.FC<{
<FollowRequestNoteContainer account={account} />
)}
<div className='account__header__image'>
{me !== account.id && relationship && !isRedesignEnabled() && (
<div
className={classNames(
'account__header__image',
isRedesign && redesignClasses.header,
)}
>
{me !== account.id && relationship && !isRedesign && (
<AccountInfo relationship={relationship} />
)}
@@ -152,10 +138,15 @@ export const AccountHeader: React.FC<{
<div
className={classNames(
'account__header__bar',
isRedesignEnabled() && redesignClasses.barWrapper,
isRedesign && redesignClasses.barWrapper,
)}
>
<div className='account__header__tabs'>
<div
className={classNames(
'account__header__tabs',
isRedesign && redesignClasses.avatarWrapper,
)}
>
<a
className='avatar'
href={account.avatar}
@@ -165,11 +156,11 @@ export const AccountHeader: React.FC<{
>
<Avatar
account={suspendedOrHidden ? undefined : account}
size={92}
size={isRedesign ? 80 : 92}
/>
</a>
{!isRedesignEnabled() && (
{!isRedesign && (
<AccountButtons
accountId={accountId}
className='account__header__buttons--desktop'
@@ -180,26 +171,27 @@ export const AccountHeader: React.FC<{
<div
className={classNames(
'account__header__tabs__name',
isRedesignEnabled() && redesignClasses.nameWrapper,
isRedesign && redesignClasses.nameWrapper,
)}
>
<AccountName accountId={accountId} />
{isRedesignEnabled() && (
{isRedesign && (
<AccountButtons
accountId={accountId}
className={redesignClasses.buttonsDesktop}
noShare
noShare={!isMe || 'share' in navigator}
forceMenu={'share' in navigator}
/>
)}
</div>
<AccountBadges accountId={accountId} />
{me && account.id !== me && !suspendedOrHidden && (
{!isMe && !suspendedOrHidden && (
<FamiliarFollowers accountId={accountId} />
)}
{!isRedesignEnabled() && (
{!isRedesign && (
<AccountButtons
className='account__header__buttons--mobile'
accountId={accountId}
@@ -212,7 +204,7 @@ export const AccountHeader: React.FC<{
<div className='account__header__bio'>
{me &&
account.id !== me &&
(isRedesignEnabled() ? (
(isRedesign ? (
<AccountNoteRedesign accountId={accountId} />
) : (
<AccountNote accountId={accountId} />
@@ -230,11 +222,11 @@ export const AccountHeader: React.FC<{
</div>
)}
{isRedesignEnabled() && (
{isRedesign && (
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isFooterIntersecting && redesignClasses.buttonsMobileIsStuck,
!isIntersecting && redesignClasses.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare
@@ -244,7 +236,7 @@ export const AccountHeader: React.FC<{
</AnimateEmojiProvider>
{!hideTabs && !hidden && <AccountTabs acct={account.acct} />}
<div ref={handleObserverRef} />
<div ref={observedRef} />
<Helmet>
<title>{titleFromAccount(account)}</title>
@@ -1,12 +1,19 @@
import { useCallback, useId, useRef, useState } from 'react';
import type { FC } from 'react';
import { useIntl } from 'react-intl';
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay';
import { DisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAppSelector } from '@/mastodon/store';
import AtIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import { DomainPill } from '../../account/components/domain_pill';
@@ -14,6 +21,18 @@ import { isRedesignEnabled } from '../common';
import classes from './redesign.module.scss';
const messages = defineMessages({
lockedInfo: {
id: 'account.locked_info',
defaultMessage:
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
},
nameInfo: {
id: 'account.name_info',
defaultMessage: 'What does this mean?',
},
});
export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
const intl = useIntl();
const account = useAccount(accountId);
@@ -46,11 +65,7 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
<Icon
id='lock'
icon={LockIcon}
aria-label={intl.formatMessage({
id: 'account.locked_info',
defaultMessage:
'This account privacy status is set to locked. The owner manually reviews who can follow them.',
})}
aria-label={intl.formatMessage(messages.lockedInfo)}
/>
)}
</small>
@@ -65,15 +80,112 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
</h1>
<p className={classes.username}>
@{username}@{domain}
<DomainPill
<AccountNameHelp
username={username}
domain={domain}
isSelf={me === account.id}
className={classes.domainPill}
>
<Icon id='help' icon={HelpIcon} />
</DomainPill>
isSelf={account.id === me}
/>
</p>
</div>
);
};
const AccountNameHelp: FC<{
username: string;
domain: string;
isSelf: boolean;
}> = ({ username, domain, isSelf }) => {
const accessibilityId = useId();
const intl = useIntl();
const [open, setOpen] = useState(false);
const triggerRef = useRef<HTMLButtonElement>(null);
const handleClick = useCallback(() => {
setOpen((prev) => !prev);
}, []);
return (
<>
<button
type='button'
ref={triggerRef}
className={classes.handleHelpButton}
onClick={handleClick}
aria-expanded={open}
aria-controls={accessibilityId}
>
<Icon
id='help'
icon={HelpIcon}
aria-label={intl.formatMessage(messages.nameInfo)}
/>
</button>
<Overlay
show={open}
rootClose
target={triggerRef}
onHide={handleClick}
offset={[5, 5]}
>
{({ props }) => (
<div
{...props}
role='region'
id={accessibilityId}
className={classNames('dropdown-animation', classes.handleHelp)}
>
<FormattedMessage
id='account.name.help.header'
defaultMessage='A handle is like an email address'
tagName='h3'
/>
<ol>
<li>
<Icon id='at' icon={AtIcon} />
{isSelf ? (
<FormattedMessage
id='account.name.help.username_self'
defaultMessage='{username} is your username on this server. Someone on another server might have the same username.'
values={{ username: <strong>{username}</strong> }}
tagName='p'
/>
) : (
<FormattedMessage
id='account.name.help.username'
defaultMessage='{username} is this account’s username on their server. Someone on another server might have the same username.'
values={{ username: <strong>{username}</strong> }}
tagName='p'
/>
)}
</li>
<li>
<Icon id='domain' icon={DomainIcon} />
{isSelf ? (
<FormattedMessage
id='account.name.help.domain_self'
defaultMessage='{domain} is your server that hosts your profile and posts.'
values={{ domain: <strong>{domain}</strong> }}
tagName='p'
/>
) : (
<FormattedMessage
id='account.name.help.domain'
defaultMessage='{domain} is the server that hosts the user’s profile and posts.'
values={{ domain: <strong>{domain}</strong> }}
tagName='p'
/>
)}
</li>
</ol>
<FormattedMessage
id='account.name.help.footer'
defaultMessage='Just like you can send emails to people using different email clients, you can interact with people on other Mastodon servers – and with anyone on other social apps powered by the same set of rules as Mastodon uses (the ActivityPub protocol).'
tagName='p'
/>
</div>
)}
</Overlay>
</>
);
};
@@ -46,7 +46,8 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
return null;
}
const className = isRedesignEnabled() ? classes.badge : '';
const isRedesign = isRedesignEnabled();
const className = isRedesign ? classes.badge : '';
const domain = account.acct.includes('@')
? account.acct.split('@')[1]
@@ -68,7 +69,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
key={role.id}
label={role.name}
className={className}
domain={isRedesignEnabled() ? `(${domain})` : domain}
domain={isRedesign ? `(${domain})` : domain}
roleId={role.id}
/>,
);
@@ -81,7 +82,7 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
if (account.group) {
badges.push(<GroupBadge key='group-badge' className={className} />);
}
if (isRedesignEnabled() && relationship) {
if (isRedesign && relationship) {
if (relationship.blocking) {
badges.push(
<BlockedBadge
@@ -89,7 +90,8 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
className={classNames(className, classes.badgeBlocked)}
/>,
);
} else if (relationship.domain_blocking) {
}
if (relationship.domain_blocking) {
badges.push(
<BlockedBadge
key='domain-blocking'
@@ -103,7 +105,8 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => {
}
/>,
);
} else if (relationship.muting) {
}
if (relationship.muting) {
badges.push(
<MutedBadge
key='muted-badge'
@@ -16,6 +16,8 @@ import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react
import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { isRedesignEnabled } from '../common';
import { AccountMenu } from './menu';
const messages = defineMessages({
@@ -35,12 +37,14 @@ interface AccountButtonsProps {
accountId: string;
className?: string;
noShare?: boolean;
forceMenu?: boolean;
}
export const AccountButtons: FC<AccountButtonsProps> = ({
accountId,
className,
noShare,
forceMenu,
}) => {
const hidden = useAppSelector((state) => getAccountHidden(state, accountId));
const me = useAppSelector((state) => state.meta.get('me') as string);
@@ -50,7 +54,7 @@ export const AccountButtons: FC<AccountButtonsProps> = ({
{!hidden && (
<AccountButtonsOther accountId={accountId} noShare={noShare} />
)}
{accountId !== me && <AccountMenu accountId={accountId} />}
{(accountId !== me || forceMenu) && <AccountMenu accountId={accountId} />}
</div>
);
};
@@ -93,6 +97,7 @@ const AccountButtonsOther: FC<
accountId={accountId}
className='account__header__follow-button'
labelLength='long'
withUnmute={!isRedesignEnabled()}
/>
)}
{isFollowing && (
@@ -34,7 +34,6 @@ import {
import type { AppDispatch } from '@/mastodon/store';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import BlockIcon from '@/material-icons/400-24px/block.svg?react';
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
import LinkIcon from '@/material-icons/400-24px/link_2.svg?react';
import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react';
@@ -51,6 +50,10 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
const relationship = useAppSelector((state) =>
state.relationships.get(accountId),
);
const currentAccountId = useAppSelector(
(state) => state.meta.get('me') as string,
);
const isMe = currentAccountId === accountId;
const dispatch = useAppDispatch();
const menuItems = useMemo(() => {
@@ -61,7 +64,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
if (isRedesignEnabled()) {
return redesignMenuItems({
account,
signedIn,
signedIn: !isMe && signedIn,
permissions,
intl,
relationship,
@@ -76,7 +79,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
relationship,
dispatch,
});
}, [account, signedIn, permissions, intl, relationship, dispatch]);
}, [account, signedIn, isMe, permissions, intl, relationship, dispatch]);
return (
<Dropdown
disabled={menuItems.length === 0}
@@ -443,6 +446,10 @@ const redesignMessages = defineMessages({
defaultMessage: 'Copied account link to clipboard',
},
mention: { id: 'account.menu.mention', defaultMessage: 'Mention' },
noteDescription: {
id: 'account.menu.note.description',
defaultMessage: 'Visible only to you',
},
direct: {
id: 'account.menu.direct',
defaultMessage: 'Privately mention',
@@ -513,22 +520,30 @@ function redesignMenuItems({
icon: ShareIcon,
});
}
items.push(
{
text: intl.formatMessage(redesignMessages.copy),
action: () => {
void navigator.clipboard.writeText(account.url);
dispatch(showAlert({ message: redesignMessages.copied }));
},
icon: LinkIcon,
items.push({
text: intl.formatMessage(redesignMessages.copy),
action: () => {
void navigator.clipboard.writeText(account.url);
dispatch(showAlert({ message: redesignMessages.copied }));
},
null,
);
icon: LinkIcon,
});
}
// Open on remote page.
if (isRemote) {
items.push({
text: intl.formatMessage(redesignMessages.openOriginalPage, {
domain: remoteDomain,
}),
href: account.url,
});
}
// Mention and direct message options
if (signedIn && !account.suspended) {
items.push(
null,
{
text: intl.formatMessage(redesignMessages.mention),
action: () => {
@@ -543,36 +558,6 @@ function redesignMenuItems({
},
},
null,
{
text: intl.formatMessage(
relationship?.note ? messages.editNote : messages.addNote,
),
action: () => {
dispatch(
openModal({
modalType: 'ACCOUNT_NOTE',
modalProps: {
accountId: account.id,
},
}),
);
},
icon: EditIcon,
},
null,
);
}
// Open on remote page.
if (isRemote) {
items.push(
{
text: intl.formatMessage(redesignMessages.openOriginalPage, {
domain: remoteDomain,
}),
href: account.url,
},
null,
);
}
@@ -608,59 +593,78 @@ function redesignMenuItems({
}
},
},
null,
);
}
// Timeline options
if (!relationship.muting) {
items.push(
{
text: intl.formatMessage(
relationship.showing_reblogs
? redesignMessages.hideReblogs
: redesignMessages.showReblogs,
),
action: () => {
dispatch(
followAccount(account.id, {
reblogs: !relationship.showing_reblogs,
}),
);
},
},
{
text: intl.formatMessage(messages.languages),
action: () => {
dispatch(
openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.id,
},
}),
);
},
},
);
}
items.push(
{
text: intl.formatMessage(
relationship?.note ? messages.editNote : messages.addNote,
),
description: intl.formatMessage(redesignMessages.noteDescription),
action: () => {
dispatch(
openModal({
modalType: 'ACCOUNT_NOTE',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
// Timeline options
if (relationship && !relationship.muting) {
items.push(
{
text: intl.formatMessage(
relationship.muting ? redesignMessages.unmute : redesignMessages.mute,
relationship.showing_reblogs
? redesignMessages.hideReblogs
: redesignMessages.showReblogs,
),
action: () => {
if (relationship.muting) {
dispatch(unmuteAccount(account.id));
} else {
dispatch(initMuteModal(account));
}
dispatch(
followAccount(account.id, {
reblogs: !relationship.showing_reblogs,
}),
);
},
},
{
text: intl.formatMessage(messages.languages),
action: () => {
dispatch(
openModal({
modalType: 'SUBSCRIBED_LANGUAGES',
modalProps: {
accountId: account.id,
},
}),
);
},
},
null,
);
}
items.push(
{
text: intl.formatMessage(
relationship?.muting ? redesignMessages.unmute : redesignMessages.mute,
),
action: () => {
if (relationship?.muting) {
dispatch(unmuteAccount(account.id));
} else {
dispatch(initMuteModal(account));
}
},
},
null,
);
if (relationship?.followed_by) {
items.push({
text: intl.formatMessage(redesignMessages.removeFollower),
@@ -722,7 +726,7 @@ function redesignMenuItems({
}
if (remoteDomain) {
items.push({
items.push(null, {
text: intl.formatMessage(
relationship?.domain_blocking
? redesignMessages.domainUnblock
@@ -63,7 +63,7 @@ export const AccountNote: FC<{ accountId: string }> = ({ accountId }) => {
/>
}
>
{relationship.note}
<div className={classes.noteContent}>{relationship.note}</div>
</Callout>
);
};
@@ -70,24 +70,22 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
</NavLink>
{isRedesignEnabled() && (
<NavLink exact to={`/@${account.acct}`}>
<FormattedMessage
id='account.joined_long'
defaultMessage='Joined on {date}'
values={{
date: (
<strong>
<FormattedDateWrapper
value={account.created_at}
year='numeric'
month='short'
day='2-digit'
/>
</strong>
),
}}
/>
</NavLink>
<FormattedMessage
id='account.joined_long'
defaultMessage='Joined on {date}'
values={{
date: (
<strong>
<FormattedDateWrapper
value={account.created_at}
year='numeric'
month='short'
day='2-digit'
/>
</strong>
),
}}
/>
)}
</div>
);
@@ -1,9 +1,24 @@
.header {
height: 120px;
background: var(--color-bg-secondary);
@container (width >= 500px) {
height: 160px;
}
}
.barWrapper {
border-bottom: none;
}
.avatarWrapper {
margin-top: -64px;
padding-top: 0;
}
.nameWrapper {
display: flex;
align-items: start;
gap: 16px;
}
@@ -12,6 +27,10 @@
font-size: 22px;
white-space: initial;
line-height: normal;
> h1 {
white-space: initial;
}
}
.username {
@@ -23,15 +42,13 @@
margin-top: 4px;
}
.domainPill {
.handleHelpButton {
appearance: none;
border: none;
background: none;
padding: 0;
text-decoration: underline;
color: inherit;
font-size: 1em;
font-weight: initial;
margin-left: 2px;
width: 16px;
height: 16px;
@@ -43,12 +60,53 @@
}
&:hover,
&:global(.active) {
background: none;
&:focus {
color: var(--color-text-brand-soft);
}
}
.handleHelp {
padding: 16px;
background: var(--color-bg-primary);
color: var(--color-text-primary);
border-radius: 12px;
box-shadow: var(--dropdown-shadow);
max-width: 400px;
box-sizing: border-box;
> h3 {
font-size: 17px;
font-weight: 600;
}
> ol {
margin: 12px 0;
}
li {
display: flex;
gap: 8px;
align-items: start;
&:first-child {
margin-bottom: 12px;
}
}
svg {
background: var(--color-bg-brand-softer);
width: 28px;
height: 28px;
padding: 5px;
border-radius: 9999px;
box-sizing: border-box;
}
strong {
font-weight: 600;
}
}
$button-breakpoint: 420px;
$button-fallback-breakpoint: #{$button-breakpoint} + 55px;
@@ -66,7 +124,9 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
.buttonsMobile {
position: sticky;
bottom: 55px; // Height of bottom nav bar.
bottom: var(--mobile-bottom-nav-height);
padding: 12px 16px;
margin: 0 -20px;
@container (width >= #{$button-breakpoint}) {
display: none;
@@ -77,13 +137,16 @@ $button-fallback-breakpoint: #{$button-breakpoint} + 55px;
display: none;
}
}
// Multi-column layout
@media (width >= #{$button-breakpoint}) {
bottom: 0;
}
}
.buttonsMobileIsStuck {
padding: 12px 16px;
background-color: var(--color-bg-primary);
border-top: 1px solid var(--color-border-primary);
margin: 0 -20px;
}
.badge {
@@ -116,6 +179,10 @@ svg.badgeIcon {
margin-bottom: 16px;
}
.noteContent {
white-space-collapse: preserve-breaks;
}
.noteEditButton {
color: inherit;
@@ -237,6 +304,11 @@ svg.badgeIcon {
a {
font-weight: unset;
}
strong {
font-weight: 600;
color: var(--color-text-primary);
}
}
.modalCloseButton {
@@ -100,7 +100,7 @@
.pinnedStatusHeader {
display: grid;
grid-template-columns: max-content auto;
grid-template-columns: 1fr auto;
grid-template-rows: 1fr 1fr;
gap: 4px;
@@ -0,0 +1,45 @@
import type { RefCallback } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
export function useVisibility({
observerOptions,
}: {
observerOptions?: IntersectionObserverInit;
} = {}) {
const [isIntersecting, setIsIntersecting] = useState(false);
const handleIntersect: IntersectionObserverCallback = useCallback(
(entries) => {
const entry = entries.at(0);
if (!entry) {
return;
}
setIsIntersecting(entry.isIntersecting);
},
[],
);
const observer = useMemo(
() => new IntersectionObserver(handleIntersect, observerOptions),
[handleIntersect, observerOptions],
);
const handleObserverRef: RefCallback<HTMLElement> = useCallback(
(node) => {
if (node) {
observer.observe(node);
}
},
[observer],
);
useEffect(() => {
return () => {
observer.disconnect();
};
}, [observer]);
return {
isIntersecting,
observedRef: handleObserverRef,
};
}
+8
View File
@@ -89,6 +89,7 @@
"account.menu.hide_reblogs": "Hide boosts in timeline",
"account.menu.mention": "Mention",
"account.menu.mute": "Mute account",
"account.menu.note.description": "Visible only to you",
"account.menu.open_original_page": "View on {domain}",
"account.menu.remove_follower": "Remove follower",
"account.menu.report": "Report account",
@@ -104,6 +105,13 @@
"account.muted": "Muted",
"account.muting": "Muting",
"account.mutual": "You follow each other",
"account.name.help.domain": "{domain} is the server that hosts the user’s profile and posts.",
"account.name.help.domain_self": "{domain} is your server that hosts your profile and posts.",
"account.name.help.footer": "Just like you can send emails to people using different email clients, you can interact with people on other Mastodon servers – and with anyone on other social apps powered by the same set of rules as Mastodon uses (the ActivityPub protocol).",
"account.name.help.header": "A handle is like an email address",
"account.name.help.username": "{username} is this account’s username on their server. Someone on another server might have the same username.",
"account.name.help.username_self": "{username} is your username on this server. Someone on another server might have the same username.",
"account.name_info": "What does this mean?",
"account.no_bio": "No description provided.",
"account.node_modal.callout": "Personal notes are visible only to you.",
"account.node_modal.edit_title": "Edit personal note",
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M325-111.5q-73-31.5-127.5-86t-86-127.5Q80-398 80-480.5t31.5-155q31.5-72.5 86-127t127.5-86Q398-880 480.5-880t155 31.5q72.5 31.5 127 86t86 127Q880-563 880-480.5T848.5-325q-31.5 73-86 127.5t-127 86Q563-80 480.5-80T325-111.5ZM480-162q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>

After

Width:  |  Height:  |  Size: 977 B

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" height="24" viewBox="0 -960 960 960" width="24"><path d="M325-111.5q-73-31.5-127.5-86t-86-127.5Q80-398 80-480.5t31.5-155q31.5-72.5 86-127t127.5-86Q398-880 480.5-880t155 31.5q72.5 31.5 127 86t86 127Q880-563 880-480.5T848.5-325q-31.5 73-86 127.5t-127 86Q563-80 480.5-80T325-111.5ZM480-162q26-36 45-75t31-83H404q12 44 31 83t45 75Zm-104-16q-18-33-31.5-68.5T322-320H204q29 50 72.5 87t99.5 55Zm208 0q56-18 99.5-55t72.5-87H638q-9 38-22.5 73.5T584-178ZM170-400h136q-3-20-4.5-39.5T300-480q0-21 1.5-40.5T306-560H170q-5 20-7.5 39.5T160-480q0 21 2.5 40.5T170-400Zm216 0h188q3-20 4.5-39.5T580-480q0-21-1.5-40.5T574-560H386q-3 20-4.5 39.5T380-480q0 21 1.5 40.5T386-400Zm268 0h136q5-20 7.5-39.5T800-480q0-21-2.5-40.5T790-560H654q3 20 4.5 39.5T660-480q0 21-1.5 40.5T654-400Zm-16-240h118q-29-50-72.5-87T584-782q18 33 31.5 68.5T638-640Zm-234 0h152q-12-44-31-83t-45-75q-26 36-45 75t-31 83Zm-200 0h118q9-38 22.5-73.5T376-782q-56 18-99.5 55T204-640Z"/></svg>

After

Width:  |  Height:  |  Size: 977 B

@@ -2869,6 +2869,7 @@ a.account__display-name {
}
&-subtitle {
color: var(--color-text-tertiary);
font-weight: 400;
}
@@ -2911,6 +2912,10 @@ a.account__display-name {
outline: 0;
}
}
button:is(:disabled, [aria-disabled='true']) &-subtitle {
color: inherit;
}
}
.reblog-menu-item {