- {layout !== 'single-column' && (
-
-
-
- )}
-
+
diff --git a/app/javascript/mastodon/features/account_timeline/hooks/useFieldHtml.tsx b/app/javascript/mastodon/features/account_timeline/hooks/useFieldHtml.tsx
new file mode 100644
index 0000000000..8ab991e85b
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/hooks/useFieldHtml.tsx
@@ -0,0 +1,38 @@
+import type { Key } from 'react';
+import { useCallback } from 'react';
+
+import htmlConfig from '@/config/html-tags.json';
+import type { OnElementHandler } from '@/mastodon/utils/html';
+
+export function useFieldHtml(
+ hasCustomEmoji: boolean,
+ onElement?: OnElementHandler,
+): OnElementHandler {
+ return useCallback(
+ (element, props, children, extra) => {
+ if (element instanceof HTMLAnchorElement) {
+ // Don't allow custom emoji and links in the same field to prevent verification spoofing.
+ if (hasCustomEmoji) {
+ return (
+
+ {children}
+
+ );
+ }
+ return onElement?.(element, props, children, extra);
+ }
+ return undefined;
+ },
+ [onElement, hasCustomEmoji],
+ );
+}
+
+function filterAttributesForSpan(props: Record) {
+ const validAttributes: Record = {};
+ for (const key of Object.keys(props)) {
+ if (key in htmlConfig.tags.span.attributes) {
+ validAttributes[key] = props[key];
+ }
+ }
+ return validAttributes;
+}
diff --git a/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx
new file mode 100644
index 0000000000..33e2e22891
--- /dev/null
+++ b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx
@@ -0,0 +1,44 @@
+import type { FC } from 'react';
+
+import { FormattedMessage } from 'react-intl';
+
+import { EmojiHTML } from '@/mastodon/components/emoji/html';
+
+import type { AccountField } from '../common';
+import { useFieldHtml } from '../hooks/useFieldHtml';
+
+import classes from './styles.module.css';
+
+export const AccountFieldModal: FC<{
+ onClose: () => void;
+ field: AccountField;
+}> = ({ onClose, field }) => {
+ const handleLabelElement = useFieldHtml(field.nameHasEmojis);
+ const handleValueElement = useFieldHtml(field.valueHasEmojis);
+ return (
+
+ );
+};
diff --git a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx
index 0d736b3467..45fe4d7105 100644
--- a/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx
+++ b/app/javascript/mastodon/features/account_timeline/modals/note_modal.tsx
@@ -13,7 +13,7 @@ import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { ConfirmationModal } from '../../ui/components/confirmation_modals';
-import classes from './modals.module.css';
+import classes from './styles.module.css';
const messages = defineMessages({
newTitle: {
diff --git a/app/javascript/mastodon/features/account_timeline/modals/modals.module.css b/app/javascript/mastodon/features/account_timeline/modals/styles.module.css
similarity index 80%
rename from app/javascript/mastodon/features/account_timeline/modals/modals.module.css
rename to app/javascript/mastodon/features/account_timeline/modals/styles.module.css
index cee0bc498a..4740a42cb9 100644
--- a/app/javascript/mastodon/features/account_timeline/modals/modals.module.css
+++ b/app/javascript/mastodon/features/account_timeline/modals/styles.module.css
@@ -19,3 +19,9 @@
outline: var(--outline-focus-default);
outline-offset: 2px;
}
+
+.fieldValue {
+ color: var(--color-text-primary);
+ font-weight: 600;
+ margin-top: 4px;
+}
diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx
index 163a04fdd6..7cacfab800 100644
--- a/app/javascript/mastodon/features/ui/components/modal_root.jsx
+++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx
@@ -92,6 +92,7 @@ export const MODAL_COMPONENTS = {
'ANNUAL_REPORT': AnnualReportModal,
'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }),
'ACCOUNT_NOTE': () => import('@/mastodon/features/account_timeline/modals/note_modal').then(module => ({ default: module.AccountNoteModal })),
+ 'ACCOUNT_FIELD_OVERFLOW': () => import('@/mastodon/features/account_timeline/modals/field_modal').then(module => ({ default: module.AccountFieldModal })),
'ACCOUNT_EDIT_NAME': () => import('@/mastodon/features/account_edit/components/name_modal').then(module => ({ default: module.NameModal })),
'ACCOUNT_EDIT_BIO': () => import('@/mastodon/features/account_edit/components/bio_modal').then(module => ({ default: module.BioModal })),
};
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 9e61158f14..17822929a7 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -22,7 +22,7 @@ import { identityContextPropShape, withIdentity } from 'mastodon/identity_contex
import { layoutFromWindow } from 'mastodon/is_mobile';
import { WithRouterPropTypes } from 'mastodon/utils/react_router';
import { checkAnnualReport } from '@/mastodon/reducers/slices/annual_report';
-import { isClientFeatureEnabled, isServerFeatureEnabled } from '@/mastodon/utils/environment';
+import { isClientFeatureEnabled } from '@/mastodon/utils/environment';
import { uploadCompose, resetCompose, changeComposeSpoilerness } from '../../actions/compose';
import { clearHeight } from '../../actions/height_cache';
@@ -80,7 +80,6 @@ import {
PrivacyPolicy,
TermsOfService,
AccountFeatured,
- AccountAbout,
AccountEdit,
AccountEditFeaturedTags,
Quotes,
@@ -166,36 +165,6 @@ class SwitchingColumnsArea extends PureComponent {
}
const profileRedesignRoutes = [];
- if (isServerFeatureEnabled('profile_redesign')) {
- profileRedesignRoutes.push(
- ,
- );
- // Check if we're in single-column mode. Confusingly, the singleColumn prop includes mobile.
- if (this.props.layout === 'single-column') {
- // When in single column mode (desktop w/o advanced view), redirect both the root and about to the posts tab.
- profileRedesignRoutes.push(
- ,
- ,
- ,
- ,
- );
- } else {
- // Otherwise, provide and redirect to the /about page.
- profileRedesignRoutes.push(
- ,
- ,
-
- );
- }
- } else {
- profileRedesignRoutes.push(
- ,
- // If the redesign is not enabled but someone shares an /about link, redirect to the root.
- ,
-
- );
- }
-
if (isClientFeatureEnabled('profile_editing')) {
profileRedesignRoutes.push(
,
@@ -257,6 +226,7 @@ class SwitchingColumnsArea extends PureComponent {
{...profileRedesignRoutes}
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index ef9125d06f..d6c0f70c70 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -93,11 +93,6 @@ export function AccountFeatured() {
return import('../../account_featured');
}
-export function AccountAbout() {
- return import('../../account_about')
- .then((module) => ({ default: module.AccountAbout }));
-}
-
export function AccountEdit() {
return import('../../account_edit')
.then((module) => ({ default: module.AccountEdit }));
diff --git a/app/javascript/mastodon/hooks/useObserver.ts b/app/javascript/mastodon/hooks/useObserver.ts
new file mode 100644
index 0000000000..4b979f6d32
--- /dev/null
+++ b/app/javascript/mastodon/hooks/useObserver.ts
@@ -0,0 +1,29 @@
+import { useEffect, useRef } from 'react';
+
+export function useResizeObserver(callback: ResizeObserverCallback) {
+ const observerRef = useRef(null);
+ observerRef.current ??= new ResizeObserver(callback);
+
+ useEffect(() => {
+ const observer = observerRef.current;
+ return () => {
+ observer?.disconnect();
+ };
+ }, []);
+
+ return observerRef.current;
+}
+
+export function useMutationObserver(callback: MutationCallback) {
+ const observerRef = useRef(null);
+ observerRef.current ??= new MutationObserver(callback);
+
+ useEffect(() => {
+ const observer = observerRef.current;
+ return () => {
+ observer?.disconnect();
+ };
+ }, []);
+
+ return observerRef.current;
+}
diff --git a/app/javascript/mastodon/hooks/useOverflow.ts b/app/javascript/mastodon/hooks/useOverflow.ts
index b306fb4871..b85222cf56 100644
--- a/app/javascript/mastodon/hooks/useOverflow.ts
+++ b/app/javascript/mastodon/hooks/useOverflow.ts
@@ -1,6 +1,8 @@
import type { MutableRefObject, RefCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
+import { useMutationObserver, useResizeObserver } from './useObserver';
+
/**
* Hook to manage overflow of items in a container with a "more" button.
*
@@ -182,48 +184,30 @@ export function useOverflowObservers({
// This is the item container element.
const listRef = useRef(null);
- // Set up observers to watch for size and content changes.
- const resizeObserverRef = useRef(null);
- const mutationObserverRef = useRef(null);
-
- // Helper to get or create the resize observer.
- const resizeObserver = useCallback(() => {
- const observer = (resizeObserverRef.current ??= new ResizeObserver(
- onRecalculate,
- ));
- return observer;
- }, [onRecalculate]);
+ const resizeObserver = useResizeObserver(onRecalculate);
// Iterate through children and observe them for size changes.
const handleChildrenChange = useCallback(() => {
const listEle = listRef.current;
- const observer = resizeObserver();
-
if (listEle) {
for (const child of listEle.children) {
if (child instanceof HTMLElement) {
- observer.observe(child);
+ resizeObserver.observe(child);
}
}
}
onRecalculate();
}, [onRecalculate, resizeObserver]);
- // Helper to get or create the mutation observer.
- const mutationObserver = useCallback(() => {
- const observer = (mutationObserverRef.current ??= new MutationObserver(
- handleChildrenChange,
- ));
- return observer;
- }, [handleChildrenChange]);
+ const mutationObserver = useMutationObserver(handleChildrenChange);
// Set up observers.
const handleObserve = useCallback(() => {
if (wrapperRef.current) {
- resizeObserver().observe(wrapperRef.current);
+ resizeObserver.observe(wrapperRef.current);
}
if (listRef.current) {
- mutationObserver().observe(listRef.current, { childList: true });
+ mutationObserver.observe(listRef.current, { childList: true });
handleChildrenChange();
}
}, [handleChildrenChange, mutationObserver, resizeObserver]);
@@ -233,12 +217,12 @@ export function useOverflowObservers({
const wrapperRefCallback = useCallback(
(node: HTMLElement | null) => {
if (node) {
- wrapperRef.current = node;
+ wrapperRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
handleObserve();
if (typeof onWrapperRef === 'function') {
onWrapperRef(node);
} else if (onWrapperRef && 'current' in onWrapperRef) {
- onWrapperRef.current = node;
+ onWrapperRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
}
}
},
@@ -254,28 +238,13 @@ export function useOverflowObservers({
if (typeof onListRef === 'function') {
onListRef(node);
} else if (onListRef && 'current' in onListRef) {
- onListRef.current = node;
+ onListRef.current = node; // eslint-disable-line react-hooks/immutability -- https://github.com/facebook/react/issues/34955
}
}
},
[handleObserve, onListRef],
);
- useEffect(() => {
- handleObserve();
-
- return () => {
- if (resizeObserverRef.current) {
- resizeObserverRef.current.disconnect();
- resizeObserverRef.current = null;
- }
- if (mutationObserverRef.current) {
- mutationObserverRef.current.disconnect();
- mutationObserverRef.current = null;
- }
- };
- }, [handleObserve]);
-
return {
wrapperRefCallback,
listRefCallback,
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index d3d9a2a4f3..e95ac60420 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -13,7 +13,6 @@
"about.not_available": "This information has not been made available on this server.",
"about.powered_by": "Decentralized social media powered by {mastodon}",
"about.rules": "Server rules",
- "account.about": "About",
"account.account_note_header": "Personal note",
"account.activity": "Activity",
"account.add_note": "Add a personal note",
@@ -49,6 +48,7 @@
"account.featured.hashtags": "Hashtags",
"account.featured_tags.last_status_at": "Last post on {date}",
"account.featured_tags.last_status_never": "No posts",
+ "account.field_overflow": "Show full content",
"account.filters.all": "All activity",
"account.filters.boosts_toggle": "Show boosts",
"account.filters.posts_boosts": "Posts and boosts",
@@ -499,8 +499,6 @@
"emoji_button.search_results": "Search results",
"emoji_button.symbols": "Symbols",
"emoji_button.travel": "Travel & Places",
- "empty_column.account_about.me": "You have not added any information about yourself yet.",
- "empty_column.account_about.other": "{acct} has not added any information about themselves yet.",
"empty_column.account_featured.me": "You have not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
"empty_column.account_featured.other": "{acct} has not featured anything yet. Did you know that you can feature your hashtags you use the most, and even your friend’s accounts on your profile?",
"empty_column.account_featured_other.unknown": "This account has not featured anything yet.",
diff --git a/config/routes.rb b/config/routes.rb
index 7aeece5d00..b516a48866 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -155,9 +155,7 @@ Rails.application.routes.draw do
constraints(username: %r{[^@/.]+}) do
with_options to: 'accounts#show' do
get '/@:username', as: :short_account
- get '/@:username/posts'
get '/@:username/featured'
- get '/@:username/about'
get '/@:username/with_replies', as: :short_account_with_replies
get '/@:username/media', as: :short_account_media
get '/@:username/tagged/:tag', as: :short_account_tag