Profile redesign: Quality pass (#37766)

pull/37792/head
Echo 2 weeks ago committed by GitHub
parent 2774e0fbfa
commit fb89198460
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -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 [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);
}
const { layout } = useLayout();
const { observedRef, isIntersecting } = useVisibility({
observerOptions: {
rootMargin: layout === 'mobile' ? '0px 0px -55px 0px' : '', // Height of bottom nav bar.
},
[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 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>
</>
);
};

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

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

Loading…
Cancel
Save