@@ -135,8 +116,13 @@ export const AccountHeader: React.FC<{
)}
-
- {me !== account.id && relationship && !isRedesignEnabled() && (
+
+ {me !== account.id && relationship && !isRedesign && (
)}
@@ -152,10 +138,15 @@ export const AccountHeader: React.FC<{
-
+
- {!isRedesignEnabled() && (
+ {!isRedesign && (
- {isRedesignEnabled() && (
+ {isRedesign && (
)}
- {me && account.id !== me && !suspendedOrHidden && (
+ {!isMe && !suspendedOrHidden && (
)}
- {!isRedesignEnabled() && (
+ {!isRedesign && (
{me &&
account.id !== me &&
- (isRedesignEnabled() ? (
+ (isRedesign ? (
) : (
@@ -230,11 +222,11 @@ export const AccountHeader: React.FC<{
)}
- {isRedesignEnabled() && (
+ {isRedesign && (
{!hideTabs && !hidden && }
-
+
{titleFromAccount(account)}
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx
index 20bff5aeeed..ac6ab2735e2 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx
@@ -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 }) => {
)}
@@ -65,15 +80,112 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => {
@{username}@{domain}
-
-
-
+ isSelf={account.id === me}
+ />
);
};
+
+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
(null);
+
+ const handleClick = useCallback(() => {
+ setOpen((prev) => !prev);
+ }, []);
+
+ return (
+ <>
+
+
+
+ {({ props }) => (
+
+
+
+ -
+
+ {isSelf ? (
+ {username} }}
+ tagName='p'
+ />
+ ) : (
+ {username} }}
+ tagName='p'
+ />
+ )}
+
+ -
+
+ {isSelf ? (
+ {domain} }}
+ tagName='p'
+ />
+ ) : (
+ {domain} }}
+ tagName='p'
+ />
+ )}
+
+
+
+
+ )}
+
+ >
+ );
+};
diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx
index 09335cee88f..9bfc3b5da53 100644
--- a/app/javascript/mastodon/features/account_timeline/components/badges.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx
@@ -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();
}
- if (isRedesignEnabled() && relationship) {
+ if (isRedesign && relationship) {
if (relationship.blocking) {
badges.push(
= ({ accountId }) => {
className={classNames(className, classes.badgeBlocked)}
/>,
);
- } else if (relationship.domain_blocking) {
+ }
+ if (relationship.domain_blocking) {
badges.push(
= ({ accountId }) => {
}
/>,
);
- } else if (relationship.muting) {
+ }
+ if (relationship.muting) {
badges.push(
= ({
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 = ({
{!hidden && (
)}
- {accountId !== me && }
+ {(accountId !== me || forceMenu) && }
);
};
@@ -93,6 +97,7 @@ const AccountButtonsOther: FC<
accountId={accountId}
className='account__header__follow-button'
labelLength='long'
+ withUnmute={!isRedesignEnabled()}
/>
)}
{isFollowing && (
diff --git a/app/javascript/mastodon/features/account_timeline/components/menu.tsx b/app/javascript/mastodon/features/account_timeline/components/menu.tsx
index a926a651f77..f03878eb8c8 100644
--- a/app/javascript/mastodon/features/account_timeline/components/menu.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/menu.tsx
@@ -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 (
{
- 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
diff --git a/app/javascript/mastodon/features/account_timeline/components/note.tsx b/app/javascript/mastodon/features/account_timeline/components/note.tsx
index dd22d92e7b6..b344e81d6bf 100644
--- a/app/javascript/mastodon/features/account_timeline/components/note.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/note.tsx
@@ -63,7 +63,7 @@ export const AccountNote: FC<{ accountId: string }> = ({ accountId }) => {
/>
}
>
- {relationship.note}
+ {relationship.note}
);
};
diff --git a/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx
index 20df6a0125f..41d5c36ed72 100644
--- a/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx
@@ -70,24 +70,22 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
{isRedesignEnabled() && (
-
-
-
-
- ),
- }}
- />
-
+
+
+
+ ),
+ }}
+ />
)}
);
diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss
index 6f71a5ae89c..3d360bfdd26 100644
--- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss
+++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss
@@ -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 {
diff --git a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
index 35bf3301661..469ac3a8948 100644
--- a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
+++ b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
@@ -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;
diff --git a/app/javascript/mastodon/hooks/useVisibility.ts b/app/javascript/mastodon/hooks/useVisibility.ts
new file mode 100644
index 00000000000..8ed1c54635e
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useVisibility.ts
@@ -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
= useCallback(
+ (node) => {
+ if (node) {
+ observer.observe(node);
+ }
+ },
+ [observer],
+ );
+
+ useEffect(() => {
+ return () => {
+ observer.disconnect();
+ };
+ }, [observer]);
+
+ return {
+ isIntersecting,
+ observedRef: handleObserverRef,
+ };
+}
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index a646d479d74..b383546cb20 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -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",
diff --git a/app/javascript/material-icons/400-24px/language-fill.svg b/app/javascript/material-icons/400-24px/language-fill.svg
new file mode 100644
index 00000000000..ee45d243a55
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/language-fill.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/material-icons/400-24px/language.svg b/app/javascript/material-icons/400-24px/language.svg
new file mode 100644
index 00000000000..ee45d243a55
--- /dev/null
+++ b/app/javascript/material-icons/400-24px/language.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index acec3bc2d65..ee2da64519a 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -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 {