@@ -139,8 +116,13 @@ export const AccountHeader: React.FC<{
)}
-
- {me !== account.id && relationship && !isRedesignEnabled() && (
+
+ {me !== account.id && relationship && !isRedesign && (
)}
@@ -156,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 ? (
) : (
@@ -232,11 +220,11 @@ export const AccountHeader: React.FC<{
)}
- {isRedesignEnabled() && (
+ {isRedesign && (
{!hideTabs && !hidden &&
}
-
+
{titleFromAccount(account)}
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/account_name.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/account_name.tsx
index f685472e7f..9046956e50 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/account_name.tsx
+++ b/app/javascript/flavours/glitch/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 '@/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 }) => {
)}
@@ -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/flavours/glitch/features/account_timeline/components/badges.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/badges.tsx
index 6450f13fb4..c66a2f5c79 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/badges.tsx
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/account_timeline/components/menu.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/menu.tsx
index 590f5d1fa6..de63aa45d8 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/menu.tsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/menu.tsx
@@ -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 (
{
- 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
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/note.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/note.tsx
index 5da393e712..67a6da3030 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/note.tsx
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/account_timeline/components/number_fields.tsx b/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx
index a5ef239e48..a1118d9c5a 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx
+++ b/app/javascript/flavours/glitch/features/account_timeline/components/number_fields.tsx
@@ -70,24 +70,22 @@ export const AccountNumberFields: FC<{ accountId: string }> = ({
{isRedesignEnabled() && (
-
-
-
-
- ),
- }}
- />
-
+
+
+
+ ),
+ }}
+ />
)}
);
diff --git a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss b/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss
index 6f71a5ae89..3d360bfdd2 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/components/redesign.module.scss
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/features/account_timeline/v2/styles.module.scss b/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss
index 35bf330166..469ac3a894 100644
--- a/app/javascript/flavours/glitch/features/account_timeline/v2/styles.module.scss
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/hooks/useVisibility.ts b/app/javascript/flavours/glitch/hooks/useVisibility.ts
new file mode 100644
index 0000000000..8ed1c54635
--- /dev/null
+++ b/app/javascript/flavours/glitch/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/flavours/glitch/styles/mastodon/components.scss b/app/javascript/flavours/glitch/styles/mastodon/components.scss
index d3095fe372..6780100ec0 100644
--- a/app/javascript/flavours/glitch/styles/mastodon/components.scss
+++ b/app/javascript/flavours/glitch/styles/mastodon/components.scss
@@ -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 {
diff --git a/app/javascript/mastodon/components/follow_button.tsx b/app/javascript/mastodon/components/follow_button.tsx
index 97c1fbcae4..ffb2ed1e5b 100644
--- a/app/javascript/mastodon/components/follow_button.tsx
+++ b/app/javascript/mastodon/components/follow_button.tsx
@@ -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 = ;
- } 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);
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
index f08db79f72..e7de3d7be7 100644
--- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
+++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx
@@ -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 = 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 (
@@ -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 20bff5aeee..ac6ab2735e 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 09335cee88..9bfc3b5da5 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 a926a651f7..f03878eb8c 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 dd22d92e7b..b344e81d6b 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 20df6a0125..41d5c36ed7 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 6f71a5ae89..3d360bfdd2 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 35bf330166..469ac3a894 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 0000000000..8ed1c54635
--- /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 a646d479d7..b383546cb2 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 0000000000..ee45d243a5
--- /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 0000000000..ee45d243a5
--- /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 acec3bc2d6..ee2da64519 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 {