Remove and move profile code (#38863)

This commit is contained in:
Echo 2026-04-30 15:58:22 +02:00 committed by GitHub
parent 3021cd8002
commit 945ac23910
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
35 changed files with 248 additions and 569 deletions

View File

@ -4,6 +4,10 @@ import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import { fetchRelationships } from '@/mastodon/actions/accounts';
import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import {
AdminBadge,
AutomatedBadge,
@ -11,10 +15,7 @@ import {
BlockedBadge,
GroupBadge,
MutedBadge,
} from '@/mastodon/components/badge';
import { useAccount } from '@/mastodon/hooks/useAccount';
import type { AccountRole } from '@/mastodon/models/account';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
} from '../badge';
import classes from './styles.module.scss';

View File

@ -6,9 +6,6 @@ import { defineMessages, useIntl } from 'react-intl';
import classNames from 'classnames';
import { followAccount } from '@/mastodon/actions/accounts';
import { CopyIconButton } from '@/mastodon/components/copy_icon_button';
import { FollowButton } from '@/mastodon/components/follow_button';
import { IconButton } from '@/mastodon/components/icon_button';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
@ -16,6 +13,10 @@ 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 { CopyIconButton } from '../copy_icon_button';
import { FollowButton } from '../follow_button';
import { IconButton } from '../icon_button';
import { AccountMenu } from './menu';
const messages = defineMessages({

View File

@ -7,21 +7,21 @@ import classNames from 'classnames';
import IconVerified from '@/images/icons/icon_verified.svg?react';
import { openModal } from '@/mastodon/actions/modal';
import { CustomEmojiProvider } from '@/mastodon/components/emoji/context';
import type { EmojiHTMLProps } from '@/mastodon/components/emoji/html';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import { Icon } from '@/mastodon/components/icon';
import { IconButton } from '@/mastodon/components/icon_button';
import { MiniCard } from '@/mastodon/components/mini_card';
import { useElementHandledLink } from '@/mastodon/components/status/handled_link';
import { useFieldHtml } from '@/mastodon/features/account_timeline/hooks/useFieldHtml';
import { cleanExtraEmojis } from '@/mastodon/features/emoji/normalize';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useResizeObserver } from '@/mastodon/hooks/useObserver';
import type { AccountFieldShape } from '@/mastodon/models/account';
import { useAppDispatch } from '@/mastodon/store';
import MoreIcon from '@/material-icons/400-24px/more_horiz.svg?react';
import { cleanExtraEmojis } from '../../emoji/normalize';
import type { AccountField } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import { CustomEmojiProvider } from '../emoji/context';
import type { EmojiHTMLProps } from '../emoji/html';
import { EmojiHTML } from '../emoji/html';
import { Icon } from '../icon';
import { IconButton } from '../icon_button';
import { MiniCard } from '../mini_card';
import { useElementHandledLink } from '../status/handled_link';
import classes from './styles.module.scss';
@ -37,6 +37,12 @@ const dateFormatOptions: Intl.DateTimeFormatOptions = {
minute: '2-digit',
};
export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean;
value_plain: string;
valueHasEmojis: boolean;
}
export const AccountHeaderFields: FC<{ accountId: string }> = ({
accountId,
}) => {

View File

@ -4,9 +4,6 @@ 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 FollowRequestNoteContainer from '@/mastodon/features/account/containers/follow_request_note_container';
import { useLayout } from '@/mastodon/hooks/useLayout';
import { useVisibility } from '@/mastodon/hooks/useVisibility';
@ -19,17 +16,20 @@ import type { Account } from '@/mastodon/models/account';
import { getAccountHidden } from '@/mastodon/selectors/accounts';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { FamiliarFollowers } from '../../../components/familiar_followers';
import { AccountBio } from '../account_bio';
import { Avatar } from '../avatar';
import { AnimateEmojiProvider } from '../emoji/context';
import { FamiliarFollowers } from '../familiar_followers';
import { AccountName } from './account_name';
import { AccountSubscriptionForm } from './account_subscription_form';
import { AccountButtons } from './buttons';
import { AccountHeaderFields } from './fields';
import { MemorialNote } from './memorial_note';
import { MovedNote } from './moved_note';
import { AccountName } from './name';
import { AccountNote } from './note';
import { AccountNumberFields } from './number_fields';
import classes from './styles.module.scss';
import { AccountSubscriptionForm } from './subscription_form';
import { AccountTabs } from './tabs';
const titleFromAccount = (account: Account) => {

View File

@ -21,7 +21,6 @@ import {
import { openModal } from '@/mastodon/actions/modal';
import { initMuteModal } from '@/mastodon/actions/mutes';
import { initReport } from '@/mastodon/actions/reports';
import { Dropdown } from '@/mastodon/components/dropdown_menu';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useIdentity } from '@/mastodon/identity_context';
import type { Account } from '@/mastodon/models/account';
@ -40,6 +39,8 @@ import PersonRemoveIcon from '@/material-icons/400-24px/person_remove.svg?react'
import ReportIcon from '@/material-icons/400-24px/report.svg?react';
import ShareIcon from '@/material-icons/400-24px/share.svg?react';
import { Dropdown } from '../dropdown_menu';
import classes from './styles.module.scss';
export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => {

View File

@ -2,9 +2,10 @@ import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
import { DisplayName } from '@/mastodon/components/display_name';
import { AvatarOverlay } from 'mastodon/components/avatar_overlay';
import { useAppSelector } from 'mastodon/store';
import { useAppSelector } from '@/mastodon/store';
import { AvatarOverlay } from '../avatar_overlay';
import { DisplayName } from '../display_name';
export const MovedNote: React.FC<{
accountId: string;

View File

@ -8,10 +8,6 @@ import classNames from 'classnames';
import Overlay from 'react-overlays/esm/Overlay';
import { showAlert } from '@/mastodon/actions/alerts';
import { FollowsYouBadge } from '@/mastodon/components/badge';
import { Button } from '@/mastodon/components/button';
import { DisplayName } from '@/mastodon/components/display_name';
import { Icon } from '@/mastodon/components/icon';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useRelationship } from '@/mastodon/hooks/useRelationship';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
@ -20,6 +16,11 @@ import ContentCopyIcon from '@/material-icons/400-24px/content_copy.svg?react';
import HelpIcon from '@/material-icons/400-24px/help.svg?react';
import DomainIcon from '@/material-icons/400-24px/language.svg?react';
import { FollowsYouBadge } from '../badge';
import { Button } from '../button';
import { DisplayName } from '../display_name';
import { Icon } from '../icon';
import { AccountBadges } from './badges';
import classes from './styles.module.scss';

View File

@ -5,11 +5,12 @@ import { defineMessages, useIntl } from 'react-intl';
import { fetchRelationships } from '@/mastodon/actions/accounts';
import { openModal } from '@/mastodon/actions/modal';
import { Callout } from '@/mastodon/components/callout';
import { IconButton } from '@/mastodon/components/icon_button';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import EditIcon from '@/material-icons/400-24px/edit_square.svg?react';
import { Callout } from '../callout';
import { IconButton } from '../icon_button';
import classes from './styles.module.scss';
const messages = defineMessages({

View File

@ -4,15 +4,13 @@ import type { FC } from 'react';
import { FormattedMessage, useIntl } from 'react-intl';
import { openModal } from '@/mastodon/actions/modal';
import { FormattedDateWrapper } from '@/mastodon/components/formatted_date';
import {
NumberFields,
NumberFieldsItem,
} from '@/mastodon/components/number_fields';
import { ShortNumber } from '@/mastodon/components/short_number';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAppDispatch } from '@/mastodon/store';
import { FormattedDateWrapper } from '../formatted_date';
import { NumberFields, NumberFieldsItem } from '../number_fields';
import { ShortNumber } from '../short_number';
export const AccountNumberFields: FC<{ accountId: string }> = ({
accountId,
}) => {

View File

@ -8,16 +8,17 @@ import { Link } from 'react-router-dom';
import { AxiosError } from 'axios';
import { apiSubscribeByEmail } from 'mastodon/api/accounts';
import { apiSubscribeByEmail } from '@/mastodon/api/accounts';
import type {
ValidationErrorResponse,
ValidationError,
} from 'mastodon/api_types/errors';
import { Button } from 'mastodon/components/button';
import { DisplayName } from 'mastodon/components/display_name';
import type { FieldStatus } from 'mastodon/components/form_fields';
import { TextInputField } from 'mastodon/components/form_fields/text_input_field';
import { useAppSelector } from 'mastodon/store';
} from '@/mastodon/api_types/errors';
import { useAppSelector } from '@/mastodon/store';
import { Button } from '../button';
import { DisplayName } from '../display_name';
import type { FieldStatus } from '../form_fields';
import { TextInputField } from '../form_fields/text_input_field';
import classes from './styles.module.scss';

View File

@ -4,10 +4,11 @@ import { FormattedMessage } from 'react-intl';
import type { NavLinkProps } from 'react-router-dom';
import { TabLink, TabList } from '@/mastodon/components/tab_list';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { TabLink, TabList } from '../tab_list';
import classes from './styles.module.scss';
const isActive: Required<NavLinkProps>['isActive'] = (match, location) =>

View File

@ -2,10 +2,11 @@ import { useCallback } from 'react';
import { FormattedMessage } from 'react-intl';
import { revealAccount } from 'mastodon/actions/accounts_typed';
import { Button } from 'mastodon/components/button';
import { domain } from 'mastodon/initial_state';
import { useAppDispatch } from 'mastodon/store';
import { revealAccount } from '@/mastodon/actions/accounts_typed';
import { domain } from '@/mastodon/initial_state';
import { useAppDispatch } from '@/mastodon/store';
import { Button } from './button';
export const LimitedAccountHint: React.FC<{ accountId: string }> = ({
accountId,

View File

@ -8,7 +8,7 @@ import { debounce } from 'lodash';
import { TIMELINE_GAP, TIMELINE_PINNED_VIEW_ALL, TIMELINE_SUGGESTIONS } from 'mastodon/actions/timelines';
import { RegenerationIndicator } from 'mastodon/components/regeneration_indicator';
import { InlineFollowSuggestions } from 'mastodon/features/home_timeline/components/inline_follow_suggestions';
import { PinnedShowAllButton } from '@/mastodon/features/account_timeline/v2/pinned_statuses';
import { PinnedShowAllButton } from '@/mastodon/features/account_timeline/components/pinned_statuses';
import { StatusQuoteManager } from '../components/status_quoted';

View File

@ -8,7 +8,7 @@ import { Link } from 'react-router-dom';
import { openModal } from '@/mastodon/actions/modal';
import { Button } from '@/mastodon/components/button';
import { EmptyState } from '@/mastodon/components/empty_state';
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { areCollectionsEnabled } from '@/mastodon/features/collections/utils';
import { useCurrentAccountId } from '@/mastodon/hooks/useAccountId';
import { useAppDispatch } from '@/mastodon/store';

View File

@ -6,26 +6,26 @@ import { useHistory } from 'react-router';
import { List as ImmutableList } from 'immutable';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { fetchEndorsedAccounts } from 'mastodon/actions/accounts';
import { AccountListItem } from 'mastodon/components/account_list_item';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { LoadingIndicator } from 'mastodon/components/loading_indicator';
import { RemoteHint } from 'mastodon/components/remote_hint';
import { fetchEndorsedAccounts } from '@/mastodon/actions/accounts';
import { AccountHeader } from '@/mastodon/components/account_header';
import { AccountListItem } from '@/mastodon/components/account_list_item';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { RemoteHint } from '@/mastodon/components/remote_hint';
import {
Article,
ItemList,
Scrollable,
} from 'mastodon/components/scrollable_list/components';
import type { TruncatedListItemInfo } from 'mastodon/components/truncated_list';
import { TruncatedListItems } from 'mastodon/components/truncated_list';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccount } from 'mastodon/hooks/useAccount';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import { useAppDispatch, useAppSelector } from 'mastodon/store';
} from '@/mastodon/components/scrollable_list/components';
import type { TruncatedListItemInfo } from '@/mastodon/components/truncated_list';
import { TruncatedListItems } from '@/mastodon/components/truncated_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import Column from '@/mastodon/features/ui/components/column';
import { useAccount } from '@/mastodon/hooks/useAccount';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import AddIcon from '@/material-icons/400-24px/add.svg?react';
import { useAccountCollections } from '../collections';
import { CollectionListItem } from '../collections/components/collection_list_item';

View File

@ -4,23 +4,23 @@ import { FormattedMessage } from 'react-intl';
import { List as ImmutableList, isList } from 'immutable';
import { openModal } from 'mastodon/actions/modal';
import { expandAccountMediaTimeline } from 'mastodon/actions/timelines';
import { ColumnBackButton } from 'mastodon/components/column_back_button';
import { RemoteHint } from 'mastodon/components/remote_hint';
import ScrollableList from 'mastodon/components/scrollable_list';
import { AccountHeader } from 'mastodon/features/account_timeline/components/account_header';
import { LimitedAccountHint } from 'mastodon/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import Column from 'mastodon/features/ui/components/column';
import { useAccountId } from 'mastodon/hooks/useAccountId';
import { useAccountVisibility } from 'mastodon/hooks/useAccountVisibility';
import type { MediaAttachment } from 'mastodon/models/media_attachment';
import { openModal } from '@/mastodon/actions/modal';
import { expandAccountMediaTimeline } from '@/mastodon/actions/timelines';
import { AccountHeader } from '@/mastodon/components/account_header';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { RemoteHint } from '@/mastodon/components/remote_hint';
import ScrollableList from '@/mastodon/components/scrollable_list';
import BundleColumnError from '@/mastodon/features/ui/components/bundle_column_error';
import Column from '@/mastodon/features/ui/components/column';
import { useAccountId } from '@/mastodon/hooks/useAccountId';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import type { MediaAttachment } from '@/mastodon/models/media_attachment';
import {
useAppSelector,
useAppDispatch,
createAppSelector,
} from 'mastodon/store';
} from '@/mastodon/store';
import { MediaItem } from './components/media_item';

View File

@ -1,7 +0,0 @@
import type { AccountFieldShape } from '@/mastodon/models/account';
export interface AccountField extends AccountFieldShape {
nameHasEmojis: boolean;
value_plain: string;
valueHasEmojis: boolean;
}

View File

@ -13,8 +13,8 @@ import { useOverflowButton } from '@/mastodon/hooks/useOverflow';
import { selectAccountFeaturedTags } from '@/mastodon/selectors/accounts';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
import { useAccountContext } from '../hooks/useAccountContext';
import classes from '../styles.module.scss';
export const FeaturedTags: FC<{ accountId: string }> = ({ accountId }) => {
// Fetch tags.

View File

@ -7,14 +7,13 @@ import { useParams } from 'react-router';
import Overlay from 'react-overlays/esm/Overlay';
import { AccountTabs } from '@/mastodon/components/account_header/tabs';
import { Toggle } from '@/mastodon/components/form_fields';
import { Icon } from '@/mastodon/components/icon';
import KeyboardArrowDownIcon from '@/material-icons/400-24px/keyboard_arrow_down.svg?react';
import { AccountTabs } from '../components/tabs';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
import { useAccountContext } from '../hooks/useAccountContext';
import classes from '../styles.module.scss';
export const AccountFilters: FC = () => {
const { acct } = useParams<{ acct: string }>();

View File

@ -1,68 +0,0 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import type { Relationship } from '@/mastodon/models/relationship';
export const AccountInfo: FC<{ relationship?: Relationship }> = ({
relationship,
}) => {
if (!relationship) {
return null;
}
return (
<div className='account__header__info'>
{(relationship.followed_by || relationship.requested_by) && (
<span className='relationship-tag'>
<AccountInfoFollower relationship={relationship} />
</span>
)}
{relationship.blocking && (
<span className='relationship-tag'>
<FormattedMessage id='account.blocking' defaultMessage='Blocking' />
</span>
)}
{relationship.muting && (
<span key='muting' className='relationship-tag'>
<FormattedMessage id='account.muting' defaultMessage='Muting' />
</span>
)}
{relationship.domain_blocking && (
<span key='domain_blocking' className='relationship-tag'>
<FormattedMessage
id='account.domain_blocking'
defaultMessage='Blocking domain'
/>
</span>
)}
</div>
);
};
const AccountInfoFollower: FC<{ relationship: Relationship }> = ({
relationship,
}) => {
if (
relationship.followed_by &&
(relationship.following || relationship.requested)
) {
return (
<FormattedMessage
id='account.mutual'
defaultMessage='You follow each other'
/>
);
} else if (relationship.followed_by) {
return (
<FormattedMessage id='account.follows_you' defaultMessage='Follows you' />
);
} else if (relationship.requested_by) {
return (
<FormattedMessage
id='account.requests_to_follow_you'
defaultMessage='Requests to follow you'
/>
);
}
return null;
};

View File

@ -0,0 +1,55 @@
import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { Badge } from '@/mastodon/components/badge';
import { Button } from '@/mastodon/components/button';
import { Icon } from '@/mastodon/components/icon';
import { StatusHeader } from '@/mastodon/components/status/header';
import type { StatusHeaderRenderFn } from '@/mastodon/components/status/header';
import { useAccountContext } from '../hooks/useAccountContext';
import classes from '../styles.module.scss';
export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
featured,
...args
}) => {
if (!featured) {
return <StatusHeader {...args} />;
}
return (
<StatusHeader {...args} className={classes.pinnedStatusHeader}>
<Badge
className={classes.pinnedBadge}
icon={<Icon id='pinned' icon={IconPinned} />}
label={
<FormattedMessage
id='account.timeline.pinned'
defaultMessage='Pinned'
/>
}
/>
</StatusHeader>
);
};
export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useAccountContext();
return (
<Button
onClick={onShowAllPinned}
className={classNames(classes.pinnedViewAllButton, 'focusable')}
>
<Icon id='pinned' icon={IconPinned} />
<FormattedMessage
id='account.timeline.pinned.view_all'
defaultMessage='View all pinned posts'
/>
</Button>
);
};

View File

@ -20,7 +20,7 @@ import {
createAppSelector,
} from '@/mastodon/store';
import classes from './styles.module.scss';
import classes from '../styles.module.scss';
const MAX_SUGGESTED_TAGS = 3;

View File

@ -1,4 +1,3 @@
import type { FC, ReactNode } from 'react';
import {
createContext,
useCallback,
@ -10,7 +9,7 @@ import {
import { useStorageState } from '@/mastodon/hooks/useStorage';
interface AccountTimelineContextValue {
accountId: string;
accountId: string | null;
boosts: boolean;
replies: boolean;
showAllPinned: boolean;
@ -19,13 +18,20 @@ interface AccountTimelineContextValue {
onShowAllPinned: () => void;
}
const AccountTimelineContext =
export const AccountTimelineContext =
createContext<AccountTimelineContextValue | null>(null);
export const AccountTimelineProvider: FC<{
accountId: string;
children: ReactNode;
}> = ({ accountId, children }) => {
export function useAccountContext() {
const values = useContext(AccountTimelineContext);
if (!values) {
throw new Error(
'useAccountFilters must be used within an AccountTimelineProvider',
);
}
return values;
}
export const useAccountContextValue = (accountId?: string | null) => {
const storageOptions = {
type: 'local',
prefix: 'account-filters',
@ -62,9 +68,9 @@ export const AccountTimelineProvider: FC<{
}, []);
// Memoize the context value to avoid unnecessary re-renders.
const value = useMemo(
return useMemo(
() => ({
accountId,
accountId: accountId ?? null,
boosts,
replies,
showAllPinned,
@ -82,20 +88,4 @@ export const AccountTimelineProvider: FC<{
showAllPinned,
],
);
return (
<AccountTimelineContext.Provider value={value}>
{children}
</AccountTimelineContext.Provider>
);
};
export function useAccountContext() {
const values = useContext(AccountTimelineContext);
if (!values) {
throw new Error(
'useAccountFilters must be used within an AccountTimelineProvider',
);
}
return values;
}

View File

@ -0,0 +1,58 @@
import { useEffect, useMemo } from 'react';
import { TIMELINE_PINNED_VIEW_ALL } from '@/mastodon/actions/timelines';
import {
expandTimelineByKey,
timelineKey,
} from '@/mastodon/actions/timelines_typed';
import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { useAccountContext } from './useAccountContext';
export function usePinnedStatusIds({
accountId,
tagged,
forceEmptyState = false,
}: {
accountId: string;
tagged?: string;
forceEmptyState?: boolean;
}) {
const pinnedKey = timelineKey({
type: 'account',
userId: accountId,
tagged,
pinned: true,
replies: true,
});
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(expandTimelineByKey({ key: pinnedKey }));
}, [dispatch, pinnedKey]);
const pinnedTimeline = useAppSelector((state) =>
selectTimelineByKey(state, pinnedKey),
);
const { showAllPinned } = useAccountContext();
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
const pinnedStatusIds = useMemo(() => {
if (!pinnedTimelineItems || forceEmptyState) {
return undefined;
}
if (pinnedTimelineItems.size <= 1 || showAllPinned) {
return pinnedTimelineItems;
}
return pinnedTimelineItems.slice(0, 1).push(TIMELINE_PINNED_VIEW_ALL);
}, [forceEmptyState, pinnedTimelineItems, showAllPinned]);
return {
statusIds: pinnedStatusIds,
isLoading: !!pinnedTimeline?.isLoading,
showAllPinned,
};
}

View File

@ -1,197 +0,0 @@
import PropTypes from 'prop-types';
import { FormattedMessage } from 'react-intl';
import { List as ImmutableList } from 'immutable';
import ImmutablePropTypes from 'react-immutable-proptypes';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { connect } from 'react-redux';
import BundleColumnError from 'mastodon/features/ui/components/bundle_column_error';
import { me } from 'mastodon/initial_state';
import { normalizeForLookup } from 'mastodon/reducers/accounts_map';
import { getAccountHidden } from 'mastodon/selectors/accounts';
import { lookupAccount, fetchAccount } from '../../actions/accounts';
import { expandAccountFeaturedTimeline, expandAccountTimeline, connectTimeline, disconnectTimeline } from '../../actions/timelines';
import { ColumnBackButton } from '../../components/column_back_button';
import { LoadingIndicator } from '../../components/loading_indicator';
import StatusList from '../../components/status_list';
import Column from '../ui/components/column';
import { RemoteHint } from 'mastodon/components/remote_hint';
import { AccountHeader } from './components/account_header';
import { LimitedAccountHint } from './components/limited_account_hint';
import { FeaturedCarousel } from '@/mastodon/components/featured_carousel';
const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
const accountId = id || state.accounts_map[normalizeForLookup(acct)];
if (accountId === null) {
return {
isLoading: false,
isAccount: false,
statusIds: emptyList,
};
} else if (!accountId) {
return {
isLoading: true,
statusIds: emptyList,
};
}
const path = withReplies ? `${accountId}:with_replies` : `${accountId}${tagged ? `:${tagged}` : ''}`;
return {
accountId,
isAccount: !!state.getIn(['accounts', accountId]),
statusIds: state.getIn(['timelines', `account:${path}`, 'items'], emptyList),
isLoading: state.getIn(['timelines', `account:${path}`, 'isLoading']),
hasMore: state.getIn(['timelines', `account:${path}`, 'hasMore']),
suspended: state.getIn(['accounts', accountId, 'suspended'], false),
hidden: getAccountHidden(state, accountId),
blockedBy: state.getIn(['relationships', accountId, 'blocked_by'], false),
};
};
class AccountTimeline extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.shape({
acct: PropTypes.string,
id: PropTypes.string,
tagged: PropTypes.string,
}).isRequired,
accountId: PropTypes.string,
dispatch: PropTypes.func.isRequired,
statusIds: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
withReplies: PropTypes.bool,
blockedBy: PropTypes.bool,
isAccount: PropTypes.bool,
suspended: PropTypes.bool,
hidden: PropTypes.bool,
multiColumn: PropTypes.bool,
};
_load () {
const { accountId, withReplies, params: { tagged }, dispatch } = this.props;
dispatch(fetchAccount(accountId));
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
if (accountId === me) {
dispatch(connectTimeline(`account:${me}`));
}
}
componentDidMount () {
const { params: { acct }, accountId, dispatch } = this.props;
if (accountId) {
this._load();
} else {
dispatch(lookupAccount(acct));
}
}
componentDidUpdate (prevProps) {
const { params: { acct, tagged }, accountId, withReplies, dispatch } = this.props;
if (prevProps.accountId !== accountId && accountId) {
this._load();
} else if (prevProps.params.acct !== acct) {
dispatch(lookupAccount(acct));
} else if (prevProps.params.tagged !== tagged) {
if (!withReplies) {
dispatch(expandAccountFeaturedTimeline(accountId, { tagged }));
}
dispatch(expandAccountTimeline(accountId, { withReplies, tagged }));
}
if (prevProps.accountId === me && accountId !== me) {
dispatch(disconnectTimeline({ timeline: `account:${me}` }));
}
}
componentWillUnmount () {
const { dispatch, accountId } = this.props;
if (accountId === me) {
dispatch(disconnectTimeline({ timeline: `account:${me}` }));
}
}
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged }));
};
render () {
const { accountId, statusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl, params: { tagged } } = this.props;
if (isLoading && statusIds.isEmpty()) {
return (
<Column>
<LoadingIndicator />
</Column>
);
} else if (!isLoading && !isAccount) {
return (
<BundleColumnError multiColumn={multiColumn} errorType='routing' />
);
}
let emptyMessage;
const forceEmptyState = suspended || blockedBy || hidden;
if (suspended) {
emptyMessage = <FormattedMessage id='empty_column.account_suspended' defaultMessage='Account suspended' />;
} else if (hidden) {
emptyMessage = <LimitedAccountHint accountId={accountId} />;
} else if (blockedBy) {
emptyMessage = <FormattedMessage id='empty_column.account_unavailable' defaultMessage='Profile unavailable' />;
} else if (remote && statusIds.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} url={remoteUrl} />;
} else {
emptyMessage = <FormattedMessage id='empty_column.account_timeline' defaultMessage='No posts found' />;
}
return (
<Column>
<ColumnBackButton />
<StatusList
prepend={
<>
<AccountHeader accountId={this.props.accountId} hideTabs={forceEmptyState} tagged={tagged} />
{!forceEmptyState && <FeaturedCarousel accountId={this.props.accountId} tagged={tagged} />}
</>
}
alwaysPrepend
append={<RemoteHint accountId={accountId} />}
scrollKey='account_timeline'
statusIds={forceEmptyState ? emptyList : statusIds}
isLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={this.handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
timelineId='account'
withCounters
/>
</Column>
);
}
}
export default connect(mapStateToProps)(AccountTimeline);

View File

@ -12,8 +12,10 @@ import {
expandTimelineByKey,
timelineKey,
} from '@/mastodon/actions/timelines_typed';
import { AccountHeader } from '@/mastodon/components/account_header';
import { Column } from '@/mastodon/components/column';
import { ColumnBackButton } from '@/mastodon/components/column_back_button';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { LoadingIndicator } from '@/mastodon/components/loading_indicator';
import { RemoteHint } from '@/mastodon/components/remote_hint';
import StatusList from '@/mastodon/components/status_list';
@ -26,23 +28,23 @@ import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { AccountHeader } from '../components/account_header';
import { LimitedAccountHint } from '../components/limited_account_hint';
import { AccountTimelineProvider, useAccountContext } from './context';
import { FeaturedTags } from './featured_tags';
import { AccountFilters } from './filters';
import { FeaturedTags } from './components/featured_tags';
import { AccountFilters } from './components/filters';
import { renderPinnedStatusHeader } from './components/pinned_statuses';
import { TagSuggestions } from './components/tags_suggestions';
import {
renderPinnedStatusHeader,
usePinnedStatusIds,
} from './pinned_statuses';
AccountTimelineContext,
useAccountContext,
useAccountContextValue,
} from './hooks/useAccountContext';
import { usePinnedStatusIds } from './hooks/usePinned';
import classes from './styles.module.scss';
import { TagSuggestions } from './tags_suggestions';
const emptyList = ImmutableList<string>();
const AccountTimelineV2: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const AccountTimeline: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
const accountId = useAccountId();
const accountContext = useAccountContextValue(accountId);
// Null means accountId does not exist (e.g. invalid acct). Undefined means loading.
if (accountId === null) {
@ -59,13 +61,13 @@ const AccountTimelineV2: FC<{ multiColumn: boolean }> = ({ multiColumn }) => {
// Add this key to remount the timeline when accountId changes.
return (
<AccountTimelineProvider accountId={accountId}>
<AccountTimelineContext.Provider value={accountContext}>
<InnerTimeline
accountId={accountId}
key={accountId}
multiColumn={multiColumn}
/>
</AccountTimelineProvider>
</AccountTimelineContext.Provider>
);
};
@ -183,4 +185,4 @@ const EmptyMessage: FC<{ accountId: string }> = ({ accountId }) => {
};
// eslint-disable-next-line import/no-default-export
export default AccountTimelineV2;
export default AccountTimeline;

View File

@ -2,6 +2,7 @@ import type { FC } from 'react';
import { FormattedMessage } from 'react-intl';
import type { AccountField } from '@/mastodon/components/account_header/fields';
import { Button } from '@/mastodon/components/button';
import { EmojiHTML } from '@/mastodon/components/emoji/html';
import {
@ -10,7 +11,6 @@ import {
ModalShellBody,
} from '@/mastodon/components/modal_shell';
import type { AccountField } from '../common';
import { useFieldHtml } from '../hooks/useFieldHtml';
import classes from './styles.module.scss';

View File

@ -1,110 +0,0 @@
import type { FC } from 'react';
import { useEffect, useMemo } from 'react';
import { FormattedMessage } from 'react-intl';
import classNames from 'classnames';
import IconPinned from '@/images/icons/icon_pinned.svg?react';
import { TIMELINE_PINNED_VIEW_ALL } from '@/mastodon/actions/timelines';
import {
expandTimelineByKey,
timelineKey,
} from '@/mastodon/actions/timelines_typed';
import { Badge } from '@/mastodon/components/badge';
import { Button } from '@/mastodon/components/button';
import { Icon } from '@/mastodon/components/icon';
import { StatusHeader } from '@/mastodon/components/status/header';
import type { StatusHeaderRenderFn } from '@/mastodon/components/status/header';
import { selectTimelineByKey } from '@/mastodon/selectors/timelines';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { useAccountContext } from './context';
import classes from './styles.module.scss';
export function usePinnedStatusIds({
accountId,
tagged,
forceEmptyState = false,
}: {
accountId: string;
tagged?: string;
forceEmptyState?: boolean;
}) {
const pinnedKey = timelineKey({
type: 'account',
userId: accountId,
tagged,
pinned: true,
replies: true,
});
const dispatch = useAppDispatch();
useEffect(() => {
dispatch(expandTimelineByKey({ key: pinnedKey }));
}, [dispatch, pinnedKey]);
const pinnedTimeline = useAppSelector((state) =>
selectTimelineByKey(state, pinnedKey),
);
const { showAllPinned } = useAccountContext();
const pinnedTimelineItems = pinnedTimeline?.items; // Make a const to avoid the React Compiler complaining.
const pinnedStatusIds = useMemo(() => {
if (!pinnedTimelineItems || forceEmptyState) {
return undefined;
}
if (pinnedTimelineItems.size <= 1 || showAllPinned) {
return pinnedTimelineItems;
}
return pinnedTimelineItems.slice(0, 1).push(TIMELINE_PINNED_VIEW_ALL);
}, [forceEmptyState, pinnedTimelineItems, showAllPinned]);
return {
statusIds: pinnedStatusIds,
isLoading: !!pinnedTimeline?.isLoading,
showAllPinned,
};
}
export const renderPinnedStatusHeader: StatusHeaderRenderFn = ({
featured,
...args
}) => {
if (!featured) {
return <StatusHeader {...args} />;
}
return (
<StatusHeader {...args} className={classes.pinnedStatusHeader}>
<Badge
className={classes.pinnedBadge}
icon={<Icon id='pinned' icon={IconPinned} />}
label={
<FormattedMessage
id='account.timeline.pinned'
defaultMessage='Pinned'
/>
}
/>
</StatusHeader>
);
};
export const PinnedShowAllButton: FC = () => {
const { onShowAllPinned } = useAccountContext();
return (
<Button
onClick={onShowAllPinned}
className={classNames(classes.pinnedViewAllButton, 'focusable')}
>
<Icon id='pinned' icon={IconPinned} />
<FormattedMessage
id='account.timeline.pinned.view_all'
defaultMessage='View all pinned posts'
/>
</Button>
);
};

View File

@ -1,52 +0,0 @@
import type { FC } from 'react';
import { Link } from 'react-router-dom';
import { RelativeTimestamp } from '@/mastodon/components/relative_timestamp';
import type { StatusHeaderProps } from '@/mastodon/components/status/header';
import {
StatusDisplayName,
StatusEditedAt,
StatusVisibility,
} from '@/mastodon/components/status/header';
import type { Account } from '@/mastodon/models/account';
export const AccountStatusHeader: FC<StatusHeaderProps> = ({
status,
account,
children,
avatarSize = 48,
wrapperProps,
onHeaderClick,
}) => {
const statusAccount = status.get('account') as Account | undefined;
const editedAt = status.get('edited_at') as string;
return (
/* eslint-disable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
<div
onClick={onHeaderClick}
onAuxClick={onHeaderClick}
{...wrapperProps}
className='status__info'
/* eslint-enable jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
>
<Link
to={`/@${statusAccount?.acct}/${status.get('id') as string}`}
className='status__relative-time'
>
<StatusVisibility visibility={status.get('visibility')} />
<RelativeTimestamp timestamp={status.get('created_at') as string} />
{editedAt && <StatusEditedAt editedAt={editedAt} />}
</Link>
<StatusDisplayName
statusAccount={statusAccount}
friendAccount={account}
avatarSize={avatarSize}
/>
{children}
</div>
);
};

View File

@ -2,7 +2,7 @@ import type { FC, ReactNode } from 'react';
import { FormattedMessage } from 'react-intl';
import { LimitedAccountHint } from '@/mastodon/features/account_timeline/components/limited_account_hint';
import { LimitedAccountHint } from '@/mastodon/components/limited_account_hint';
import { useAccountVisibility } from '@/mastodon/hooks/useAccountVisibility';
import type { Account } from '@/mastodon/models/account';

View File

@ -77,7 +77,7 @@ export function PinnedStatuses () {
}
export function AccountTimeline () {
return import('../../account_timeline/v2');
return import('../../account_timeline');
}
export function AccountGallery () {

View File

@ -28,12 +28,10 @@
"account.block_domain": "Block domain {domain}",
"account.block_short": "Block",
"account.blocked": "Blocked",
"account.blocking": "Blocking",
"account.cancel_follow_request": "Cancel follow",
"account.copy": "Copy link to profile",
"account.direct": "Privately mention @{name}",
"account.disable_notifications": "Stop notifying me when @{name} posts",
"account.domain_blocking": "Blocking domain",
"account.edit_note": "Edit personal note",
"account.edit_profile": "Edit profile",
"account.edit_profile_short": "Edit",
@ -111,7 +109,6 @@
"account.mute_notifications_short": "Mute notifications",
"account.mute_short": "Mute",
"account.muted": "Muted",
"account.muting": "Muting",
"account.mutual": "You follow each other",
"account.name.copy": "Copy handle",
"account.name.help.domain": "{domain} is the server that hosts the users profile and posts.",
@ -136,7 +133,6 @@
"account.remove_from_followers": "Remove {name} from followers",
"account.report": "Report @{name}",
"account.requested_follow": "{name} has requested to follow you",
"account.requests_to_follow_you": "Requests to follow you",
"account.share": "Share @{name}'s profile",
"account.show_reblogs": "Show boosts from @{name}",
"account.statuses_counter": "{count, plural, one {{counter} post} other {{counter} posts}}",