Files
mastodon/app/javascript/flavours/glitch/features/account_gallery/index.tsx
2026-03-27 18:21:06 +01:00

256 lines
7.2 KiB
TypeScript

import { useEffect, useCallback } from 'react';
import { FormattedMessage, useIntl, defineMessages } from 'react-intl';
import { List as ImmutableList, isList } from 'immutable';
import { isServerFeatureEnabled } from '@/flavours/glitch/utils/environment';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { openModal } from 'flavours/glitch/actions/modal';
import { expandAccountMediaTimeline } from 'flavours/glitch/actions/timelines';
import { RemoteHint } from 'flavours/glitch/components/remote_hint';
import ScrollableList from 'flavours/glitch/components/scrollable_list';
import { AccountHeader } from 'flavours/glitch/features/account_timeline/components/account_header';
import { LimitedAccountHint } from 'flavours/glitch/features/account_timeline/components/limited_account_hint';
import BundleColumnError from 'flavours/glitch/features/ui/components/bundle_column_error';
import Column from 'flavours/glitch/features/ui/components/column';
import { useAccountId } from 'flavours/glitch/hooks/useAccountId';
import { useAccountVisibility } from 'flavours/glitch/hooks/useAccountVisibility';
import type { MediaAttachment } from 'flavours/glitch/models/media_attachment';
import {
useAppSelector,
useAppDispatch,
createAppSelector,
} from 'flavours/glitch/store';
import { MediaItem } from './components/media_item';
const messages = defineMessages({
profile: { id: 'column_header.profile', defaultMessage: 'Profile' },
});
const emptyList = ImmutableList<MediaAttachment>();
const redesignEnabled = isServerFeatureEnabled('profile_redesign');
const selectGalleryTimeline = createAppSelector(
[
(_state, accountId?: string | null) => accountId,
(state) => state.timelines,
(state) => state.accounts,
(state) => state.statuses,
],
(accountId, timelines, accounts, statuses) => {
let items = emptyList;
if (!accountId) {
return {
items,
hasMore: false,
isLoading: false,
withReplies: false,
};
}
const account = accounts.get(accountId);
if (!account) {
return {
items,
hasMore: false,
isLoading: false,
withReplies: false,
};
}
const { show_media, show_media_replies } = account;
// If the account disabled showing media, don't display anything.
if (!show_media && redesignEnabled) {
return {
items,
hasMore: false,
isLoading: false,
withReplies: false,
};
}
const withReplies = show_media_replies && redesignEnabled;
const timeline = timelines.get(
`account:${accountId}:media${withReplies ? ':with_replies' : ''}`,
);
const statusIds = timeline?.get('items');
if (isList(statusIds)) {
for (const statusId of statusIds) {
const status = statuses.get(statusId);
items = items.concat(
(
status?.get('media_attachments') as ImmutableList<MediaAttachment>
).map((media) => media.set('status', status)),
);
}
}
return {
items,
hasMore: !!timeline?.get('hasMore'),
isLoading: timeline?.get('isLoading') ? true : false,
withReplies,
};
},
);
export const AccountGallery: React.FC<{
multiColumn: boolean;
}> = ({ multiColumn }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const accountId = useAccountId();
const {
isLoading,
items: attachments,
hasMore,
withReplies,
} = useAppSelector((state) => selectGalleryTimeline(state, accountId));
const { suspended, blockedBy, hidden } = useAccountVisibility(accountId);
const maxId = attachments.last()?.getIn(['status', 'id']) as
| string
| undefined;
useEffect(() => {
if (accountId) {
void dispatch(expandAccountMediaTimeline(accountId, { withReplies }));
}
}, [dispatch, accountId, withReplies]);
const handleLoadMore = useCallback(() => {
if (maxId) {
void dispatch(
expandAccountMediaTimeline(accountId, { maxId, withReplies }),
);
}
}, [maxId, dispatch, accountId, withReplies]);
const handleOpenMedia = useCallback(
(attachment: MediaAttachment) => {
const statusId = attachment.getIn(['status', 'id']);
const lang = attachment.getIn(['status', 'language']);
if (attachment.get('type') === 'video') {
dispatch(
openModal({
modalType: 'VIDEO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else if (attachment.get('type') === 'audio') {
dispatch(
openModal({
modalType: 'AUDIO',
modalProps: {
media: attachment,
statusId,
lang,
options: { autoPlay: true },
},
}),
);
} else {
const media = attachment.getIn([
'status',
'media_attachments',
]) as ImmutableList<MediaAttachment>;
const index = media.findIndex(
(x) => x.get('id') === attachment.get('id'),
);
dispatch(
openModal({
modalType: 'MEDIA',
modalProps: { media, index, statusId, lang },
}),
);
}
},
[dispatch],
);
if (accountId === null) {
return <BundleColumnError multiColumn={multiColumn} errorType='routing' />;
}
let emptyMessage;
if (accountId) {
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 (attachments.isEmpty()) {
emptyMessage = <RemoteHint accountId={accountId} />;
} else {
emptyMessage = (
<FormattedMessage
id='empty_column.account_timeline'
defaultMessage='No posts found'
/>
);
}
}
const forceEmptyState = suspended || blockedBy || hidden;
return (
<Column
icon='user-circle'
iconComponent={PersonIcon}
heading={intl.formatMessage(messages.profile)}
alwaysShowBackButton
>
<ScrollableList
className='account-gallery__container'
prepend={
accountId && (
<AccountHeader accountId={accountId} hideTabs={forceEmptyState} />
)
}
alwaysPrepend
append={accountId && <RemoteHint accountId={accountId} />}
scrollKey='account_gallery'
showLoading={isLoading}
hasMore={!forceEmptyState && hasMore}
onLoadMore={handleLoadMore}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
>
{attachments.map((attachment) => (
<MediaItem
key={attachment.get('id') as string}
attachment={attachment}
onOpenMedia={handleOpenMedia}
/>
))}
</ScrollableList>
</Column>
);
};
// eslint-disable-next-line import/no-default-export
export default AccountGallery;