[Glitch] Profile redesign: Quality pass

Port fb89198460f9cebf624cc5ca0b139563ae4fd120 to glitch-soc

Signed-off-by: Claire <claire.github-309c@sitedethib.com>
This commit is contained in:
Echo 2026-02-10 11:56:03 +01:00 committed by Claire
parent d89db37e93
commit b7d5607dd1
12 changed files with 430 additions and 198 deletions

View File

@ -5,7 +5,6 @@ import { useIntl, defineMessages } from 'react-intl';
import classNames from 'classnames';
import { useIdentity } from '@/flavours/glitch/identity_context';
import { isClientFeatureEnabled } from '@/flavours/glitch/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.edit_profile);
} 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);

View File

@ -1,23 +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 '@/flavours/glitch/actions/modal';
import { AccountBio } from '@/flavours/glitch/components/account_bio';
import { Avatar } from '@/flavours/glitch/components/avatar';
import { AnimateEmojiProvider } from '@/flavours/glitch/components/emoji/context';
import { openModal } from 'flavours/glitch/actions/modal';
import { Avatar } from 'flavours/glitch/components/avatar';
import { AccountNote } from 'flavours/glitch/features/account/components/account_note';
import FollowRequestNoteContainer from 'flavours/glitch/features/account/containers/follow_request_note_container';
import { AccountNote } from '@/flavours/glitch/features/account/components/account_note';
import FollowRequestNoteContainer from '@/flavours/glitch/features/account/containers/follow_request_note_container';
import { useLayout } from '@/flavours/glitch/hooks/useLayout';
import { useVisibility } from '@/flavours/glitch/hooks/useVisibility';
import {
autoPlayGif,
me,
domain as localDomain,
} from 'flavours/glitch/initial_state';
import type { Account } from 'flavours/glitch/models/account';
import { getAccountHidden } from 'flavours/glitch/selectors/accounts';
import { useAppSelector, useAppDispatch } from 'flavours/glitch/store';
} from '@/flavours/glitch/initial_state';
import type { Account } from '@/flavours/glitch/models/account';
import { getAccountHidden } from '@/flavours/glitch/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/flavours/glitch/store';
import { ActionBar } from '../../account/components/action_bar';
import { isRedesignEnabled } from '../common';
@ -50,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) =>
@ -82,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;
@ -122,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'>
@ -139,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} />
)}
@ -156,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}
@ -169,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'
@ -184,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}
@ -216,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} />
@ -232,11 +220,11 @@ export const AccountHeader: React.FC<{
</div>
)}
{isRedesignEnabled() && (
{isRedesign && (
<AccountButtons
className={classNames(
redesignClasses.buttonsMobile,
!isFooterIntersecting && redesignClasses.buttonsMobileIsStuck,
!isIntersecting && redesignClasses.buttonsMobileIsStuck,
)}
accountId={accountId}
noShare
@ -248,7 +236,7 @@ export const AccountHeader: React.FC<{
<ActionBar account={account} />
{!hideTabs && !hidden && <AccountTabs acct={account.acct} />}
<div ref={handleObserverRef} />
<div ref={observedRef} />
<Helmet>
<title>{titleFromAccount(account)}</title>

View File

@ -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 '@/flavours/glitch/components/display_name';
import { Icon } from '@/flavours/glitch/components/icon';
import { useAccount } from '@/flavours/glitch/hooks/useAccount';
import { useAppSelector } from '@/flavours/glitch/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 accounts 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 users 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>
</>
);
};

View File

@ -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'

View File

@ -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 && (

View File

@ -37,7 +37,6 @@ import {
import type { AppDispatch } from '@/flavours/glitch/store';
import { useAppDispatch, useAppSelector } from '@/flavours/glitch/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';
@ -54,6 +53,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(() => {
@ -64,7 +67,7 @@ export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {
if (isRedesignEnabled()) {
return redesignMenuItems({
account,
signedIn,
signedIn: !isMe && signedIn,
permissions,
intl,
relationship,
@ -79,7 +82,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}
@ -446,6 +449,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',
@ -516,22 +523,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: () => {
@ -546,36 +561,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,
);
}
@ -611,59 +596,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),
@ -725,7 +729,7 @@ function redesignMenuItems({
}
if (remoteDomain) {
items.push({
items.push(null, {
text: intl.formatMessage(
relationship?.domain_blocking
? redesignMessages.domainUnblock

View File

@ -63,7 +63,7 @@ export const AccountNote: FC<{ accountId: string }> = ({ accountId }) => {
/>
}
>
{relationship.note}
<div className={classes.noteContent}>{relationship.note}</div>
</Callout>
);
};

View File

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

View File

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

View File

@ -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;

View File

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

View File

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