mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge pull request #3394 from ClearlyClaire/glitch-soc/merge-upstream
Merge upstream changes up to fb89198460
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 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 && (
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
45
app/javascript/flavours/glitch/hooks/useVisibility.ts
Normal file
45
app/javascript/flavours/glitch/hooks/useVisibility.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
45
app/javascript/mastodon/hooks/useVisibility.ts
Normal file
45
app/javascript/mastodon/hooks/useVisibility.ts
Normal 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,
|
||||
};
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
1
app/javascript/material-icons/400-24px/language-fill.svg
Normal file
1
app/javascript/material-icons/400-24px/language-fill.svg
Normal file
@@ -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 |
1
app/javascript/material-icons/400-24px/language.svg
Normal file
1
app/javascript/material-icons/400-24px/language.svg
Normal file
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user