pl-fe: Split StatusActionBar into separate components

Signed-off-by: marcin mikołajczak <git@mkljczk.pl>
This commit is contained in:
marcin mikołajczak 2024-10-25 20:23:00 +02:00
parent 618f5f5f25
commit 492a4214a5

View file

@ -43,6 +43,7 @@ import type { UnauthorizedModalAction } from 'pl-fe/features/ui/components/modal
import type { Account } from 'pl-fe/normalizers/account'; import type { Account } from 'pl-fe/normalizers/account';
import type { Group } from 'pl-fe/normalizers/group'; import type { Group } from 'pl-fe/normalizers/group';
import type { SelectedStatus } from 'pl-fe/selectors'; import type { SelectedStatus } from 'pl-fe/selectors';
import type { Me } from 'pl-fe/types/pl-fe';
const messages = defineMessages({ const messages = defineMessages({
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' }, adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
@ -110,29 +111,358 @@ const messages = defineMessages({
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' }, hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
}); });
interface IStatusActionBar { interface IActionButton extends Pick<IStatusActionBar, 'status' | 'statusActionButtonTheme' | 'withLabels'> {
status: SelectedStatus; me: Me;
onOpenUnauthorizedModal: (action?: UnauthorizedModalAction) => void;
}
interface IReplyButton extends IActionButton {
rebloggedBy?: Account; rebloggedBy?: Account;
withLabels?: boolean; }
const ReplyButton: React.FC<IReplyButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
rebloggedBy,
}) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
let replyTitle;
let replyDisabled = false;
const replyCount = status.replies_count;
if ((status.group as Group)?.membership_required && !groupRelationship?.member) {
replyDisabled = true;
replyTitle = intl.formatMessage(messages.replies_disabled_group);
}
if (!status.in_reply_to_id) {
replyTitle = intl.formatMessage(messages.reply);
} else {
replyTitle = intl.formatMessage(messages.replyAll);
}
const handleReplyClick: React.MouseEventHandler = (e) => {
if (me) {
dispatch(replyCompose(status, rebloggedBy));
} else {
onOpenUnauthorizedModal('REPLY');
}
};
const replyButton = (
<StatusActionButton
title={replyTitle}
icon={require('@tabler/icons/outline/message-circle.svg')}
onClick={handleReplyClick}
count={replyCount}
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
disabled={replyDisabled}
theme={statusActionButtonTheme}
/>
);
return status.group ? (
<GroupPopover
group={status.group}
isEnabled={replyDisabled}
>
{replyButton}
</GroupPopover>
) : replyButton;
};
interface IReblogButton extends IActionButton {
publicStatus: boolean;
}
const ReblogButton: React.FC<IReblogButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
publicStatus,
}) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const { boostModal } = useSettings();
const { openModal } = useModalsStore();
let reblogIcon = require('@tabler/icons/outline/repeat.svg');
if (status.visibility === 'direct') {
reblogIcon = require('@tabler/icons/outline/mail.svg');
} else if (status.visibility === 'private' || status.visibility === 'mutuals_only') {
reblogIcon = require('@tabler/icons/outline/lock.svg');
}
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
if (me) {
const modalReblog = () => dispatch(toggleReblog(status));
if ((e && e.shiftKey) || !boostModal) {
modalReblog();
} else {
openModal('BOOST', { statusId: status.id, onReblog: modalReblog });
}
} else {
onOpenUnauthorizedModal('REBLOG');
}
};
const handleReblogLongPress = status.reblogs_count ? () => {
openModal('REBLOGS', { statusId: status.id });
} : undefined;
const reblogButton = (
<StatusActionButton
icon={reblogIcon}
color='success'
disabled={!publicStatus}
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)}
active={status.reblogged}
onClick={handleReblogClick}
onLongPress={handleReblogLongPress}
count={status.reblogs_count + status.quotes_count}
text={withLabels ? intl.formatMessage(messages.reblog) : undefined}
theme={statusActionButtonTheme}
/>
);
if (!features.quotePosts || !me) return reblogButton;
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(quoteCompose(status));
} else {
onOpenUnauthorizedModal('REBLOG');
}
};
const reblogMenu = [{
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog),
action: handleReblogClick,
icon: require('@tabler/icons/outline/repeat.svg'),
}, {
text: intl.formatMessage(messages.quotePost),
action: handleQuoteClick,
icon: require('@tabler/icons/outline/quote.svg'),
}];
return (
<DropdownMenu
items={reblogMenu}
disabled={!publicStatus}
onShiftClick={handleReblogClick}
>
{reblogButton}
</DropdownMenu>
);
};
const FavouriteButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
me,
withLabels,
onOpenUnauthorizedModal,
}) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const { openModal } = useModalsStore();
const handleFavouriteClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(toggleFavourite(status));
} else {
onOpenUnauthorizedModal('FAVOURITE');
}
};
const handleFavouriteLongPress = status.favourites_count ? () => {
openModal('FAVOURITES', { statusId: status.id });
} : undefined;
return (
<StatusActionButton
title={intl.formatMessage(messages.favourite)}
icon={features.statusDislikes ? require('@tabler/icons/outline/thumb-up.svg') : require('@tabler/icons/outline/heart.svg')}
color='accent'
filled
onClick={handleFavouriteClick}
onLongPress={handleFavouriteLongPress}
active={status.favourited}
count={status.favourites_count}
text={withLabels ? intl.formatMessage(messages.favourite) : undefined}
theme={statusActionButtonTheme}
/>
);
};
const DislikeButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
onOpenUnauthorizedModal,
}) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const intl = useIntl();
const { openModal } = useModalsStore();
if (!features.statusDislikes) return;
const handleDislikeClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(toggleDislike(status));
} else {
onOpenUnauthorizedModal('DISLIKE');
}
};
const handleDislikeLongPress = status.dislikes_count ? () => {
openModal('DISLIKES', { statusId: status.id });
} : undefined;
return (
<StatusActionButton
title={intl.formatMessage(messages.disfavourite)}
icon={require('@tabler/icons/outline/thumb-down.svg')}
color='accent'
filled
onClick={handleDislikeClick}
onLongPress={handleDislikeLongPress}
active={status.disliked}
count={status.dislikes_count}
text={withLabels ? intl.formatMessage(messages.disfavourite) : undefined}
theme={statusActionButtonTheme}
/>
);
};
const WrenchButton: React.FC<IActionButton> = ({
status,
statusActionButtonTheme,
withLabels,
me,
}) => {
const dispatch = useAppDispatch();
const intl = useIntl();
const features = useFeatures();
const { openModal } = useModalsStore();
const { showWrenchButton } = useSettings();
if (!me || withLabels || !features.emojiReacts || !showWrenchButton) return;
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
const handleWrenchClick: React.EventHandler<React.MouseEvent> = (e) => {
if (wrenches?.me) {
dispatch(unEmojiReact(status, '🔧'));
} else {
dispatch(emojiReact(status, '🔧'));
}
};
const handleWrenchLongPress = wrenches?.count ? () => {
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
} : undefined;
return (
<StatusActionButton
title={intl.formatMessage(messages.wrench)}
icon={require('@tabler/icons/outline/tool.svg')}
color='accent'
filled
onClick={handleWrenchClick}
onLongPress={handleWrenchLongPress}
active={wrenches?.me}
count={wrenches?.count || undefined}
theme={statusActionButtonTheme}
/>
);
};
const EmojiPickerButton: React.FC<Omit<IActionButton, 'onOpenUnauthorizedModal'>> = ({
status,
statusActionButtonTheme,
withLabels,
me,
}) => {
const dispatch = useAppDispatch();
const features = useFeatures();
const handlePickEmoji = (emoji: EmojiType) => {
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
};
return me && !withLabels && features.emojiReacts && (
<EmojiPickerDropdown
onPickEmoji={handlePickEmoji}
theme={statusActionButtonTheme}
/>
);
};
const ShareButton: React.FC<Pick<IActionButton, 'status' | 'statusActionButtonTheme'>> = ({
status,
statusActionButtonTheme,
}) => {
const intl = useIntl();
const handleShareClick = () => {
navigator.share({
text: status.search_index,
url: status.uri,
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group');
return canShare && (
<StatusActionButton
title={intl.formatMessage(messages.share)}
icon={require('@tabler/icons/outline/upload.svg')}
onClick={handleShareClick}
theme={statusActionButtonTheme}
/>
);
};
interface IMenuButton extends IActionButton {
expandable?: boolean; expandable?: boolean;
space?: 'sm' | 'md' | 'lg';
statusActionButtonTheme?: 'default' | 'inverse';
fromBookmarks?: boolean; fromBookmarks?: boolean;
} }
const StatusActionBar: React.FC<IStatusActionBar> = ({ const MenuButton: React.FC<IMenuButton> = ({
status, status,
withLabels = false, statusActionButtonTheme,
withLabels,
me,
expandable, expandable,
space = 'sm', fromBookmarks,
statusActionButtonTheme = 'default',
fromBookmarks = false,
rebloggedBy,
}) => { }) => {
const intl = useIntl(); const intl = useIntl();
const history = useHistory(); const history = useHistory();
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId'); const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { boostModal } = useSettings();
const { openModal } = useModalsStore(); const { openModal } = useModalsStore();
const { group } = useGroup((status.group as Group)?.id as string); const { group } = useGroup((status.group as Group)?.id as string);
@ -140,13 +470,10 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const blockGroupMember = useBlockGroupMember(group as Group, status.account); const blockGroupMember = useBlockGroupMember(group as Group, status.account);
const { getOrCreateChatByAccountId } = useChats(); const { getOrCreateChatByAccountId } = useChats();
const me = useAppSelector(state => state.me);
const { groupRelationship } = useGroupRelationship(status.group_id || undefined); const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
const features = useFeatures(); const features = useFeatures();
const instance = useInstance(); const instance = useInstance();
const { autoTranslate, boostModal, deleteModal, knownLanguages, showWrenchButton } = useSettings(); const { autoTranslate, deleteModal, knownLanguages } = useSettings();
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
const { translationLanguages } = useTranslationLanguages(); const { translationLanguages } = useTranslationLanguages();
@ -166,76 +493,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const isStaff = account ? account.is_admin || account.is_moderator : false; const isStaff = account ? account.is_admin || account.is_moderator : false;
const isAdmin = account ? account.is_admin : false; const isAdmin = account ? account.is_admin : false;
if (!status) {
return null;
}
const onOpenUnauthorizedModal = (action?: UnauthorizedModalAction) => {
openModal('UNAUTHORIZED', {
action,
ap_id: status.url,
});
};
const handleReplyClick: React.MouseEventHandler = (e) => {
if (me) {
dispatch(replyCompose(status, rebloggedBy));
} else {
onOpenUnauthorizedModal('REPLY');
}
};
const handleShareClick = () => {
navigator.share({
text: status.search_index,
url: status.uri,
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
const handleFavouriteClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(toggleFavourite(status));
} else {
onOpenUnauthorizedModal('FAVOURITE');
}
};
const handleFavouriteLongPress = status.favourites_count ? () => {
openModal('FAVOURITES', { statusId: status.id });
} : undefined;
const handleDislikeClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(toggleDislike(status));
} else {
onOpenUnauthorizedModal('DISLIKE');
}
};
const handleWrenchClick: React.EventHandler<React.MouseEvent> = (e) => {
if (!me) {
onOpenUnauthorizedModal('DISLIKE');
} else if (wrenches?.me) {
dispatch(unEmojiReact(status, '🔧'));
} else {
dispatch(emojiReact(status, '🔧'));
}
};
const handleDislikeLongPress = status.dislikes_count ? () => {
openModal('DISLIKES', { statusId: status.id });
} : undefined;
const handleWrenchLongPress = wrenches?.count ? () => {
openModal('REACTIONS', { statusId: status.id, reaction: wrenches.name });
} : undefined;
const handlePickEmoji = (emoji: EmojiType) => {
dispatch(emojiReact(status, emoji.custom ? emoji.id : emoji.native, emoji.custom ? emoji.imageUrl : undefined));
};
const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => { const handleBookmarkClick: React.EventHandler<React.MouseEvent> = (e) => {
dispatch(toggleBookmark(status)); dispatch(toggleBookmark(status));
}; };
@ -246,31 +503,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
}); });
}; };
const handleReblogClick: React.EventHandler<React.MouseEvent> = e => {
if (me) {
const modalReblog = () => dispatch(toggleReblog(status));
if ((e && e.shiftKey) || !boostModal) {
modalReblog();
} else {
openModal('BOOST', { statusId: status.id, onReblog: modalReblog });
}
} else {
onOpenUnauthorizedModal('REBLOG');
}
};
const handleReblogLongPress = status.reblogs_count ? () => {
openModal('REBLOGS', { statusId: status.id });
} : undefined;
const handleQuoteClick: React.EventHandler<React.MouseEvent> = (e) => {
if (me) {
dispatch(quoteCompose(status));
} else {
onOpenUnauthorizedModal('REBLOG');
}
};
const doDeleteStatus = (withRedraft = false) => { const doDeleteStatus = (withRedraft = false) => {
if (!deleteModal) { if (!deleteModal) {
dispatch(deleteStatus(status.id, withRedraft)); dispatch(deleteStatus(status.id, withRedraft));
@ -301,6 +533,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
dispatch(togglePin(status)); dispatch(togglePin(status));
}; };
const handleReblogClick: React.EventHandler<React.MouseEvent> = (e) => {
const modalReblog = () => dispatch(toggleReblog(status));
if ((e && e.shiftKey) || !boostModal) {
modalReblog();
} else {
openModal('BOOST', { statusId: status.id, onReblog: modalReblog });
}
};
const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => { const handleMentionClick: React.EventHandler<React.MouseEvent> = (e) => {
dispatch(mentionCompose(status.account)); dispatch(mentionCompose(status.account));
}; };
@ -669,71 +910,55 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const publicStatus = ['public', 'unlisted', 'group'].includes(status.visibility); const publicStatus = ['public', 'unlisted', 'group'].includes(status.visibility);
const replyCount = status.replies_count;
const reblogCount = status.reblogs_count;
const quoteCount = status.quotes_count;
const favouriteCount = status.favourites_count;
const menu = _makeMenu(publicStatus); const menu = _makeMenu(publicStatus);
let reblogIcon = require('@tabler/icons/outline/repeat.svg');
let replyTitle;
let replyDisabled = false;
if (status.visibility === 'direct') { return (
reblogIcon = require('@tabler/icons/outline/mail.svg'); <DropdownMenu items={menu}>
} else if (status.visibility === 'private' || status.visibility === 'mutuals_only') { <StatusActionButton
reblogIcon = require('@tabler/icons/outline/lock.svg'); title={intl.formatMessage(messages.more)}
} icon={require('@tabler/icons/outline/dots.svg')}
theme={statusActionButtonTheme}
if ((status.group as Group)?.membership_required && !groupRelationship?.member) { />
replyDisabled = true; </DropdownMenu>
replyTitle = intl.formatMessage(messages.replies_disabled_group);
}
const replyButton = (
<StatusActionButton
title={replyTitle}
icon={require('@tabler/icons/outline/message-circle.svg')}
onClick={handleReplyClick}
count={replyCount}
text={withLabels ? intl.formatMessage(messages.reply) : undefined}
disabled={replyDisabled}
theme={statusActionButtonTheme}
/>
); );
};
const reblogMenu = [{ interface IStatusActionBar {
text: intl.formatMessage(status.reblogged ? messages.cancel_reblog_private : messages.reblog), status: SelectedStatus;
action: handleReblogClick, rebloggedBy?: Account;
icon: require('@tabler/icons/outline/repeat.svg'), withLabels?: boolean;
}, { expandable?: boolean;
text: intl.formatMessage(messages.quotePost), space?: 'sm' | 'md' | 'lg';
action: handleQuoteClick, statusActionButtonTheme?: 'default' | 'inverse';
icon: require('@tabler/icons/outline/quote.svg'), fromBookmarks?: boolean;
}]; }
const reblogButton = ( const StatusActionBar: React.FC<IStatusActionBar> = ({
<StatusActionButton status,
icon={reblogIcon} withLabels = false,
color='success' expandable,
disabled={!publicStatus} space = 'sm',
title={!publicStatus ? intl.formatMessage(messages.cannot_reblog) : intl.formatMessage(messages.reblog)} statusActionButtonTheme = 'default',
active={status.reblogged} fromBookmarks = false,
onClick={handleReblogClick} rebloggedBy,
onLongPress={handleReblogLongPress} }) => {
count={reblogCount + quoteCount}
text={withLabels ? intl.formatMessage(messages.reblog) : undefined}
theme={statusActionButtonTheme}
/>
);
if (!status.in_reply_to_id) { const { openModal } = useModalsStore();
replyTitle = intl.formatMessage(messages.reply);
} else { const me = useAppSelector(state => state.me);
replyTitle = intl.formatMessage(messages.replyAll);
if (!status) {
return null;
} }
const canShare = ('share' in navigator) && (status.visibility === 'public' || status.visibility === 'group'); const onOpenUnauthorizedModal = (action?: UnauthorizedModalAction) => {
openModal('UNAUTHORIZED', {
action,
ap_id: status.url,
});
};
const publicStatus = ['public', 'unlisted', 'group'].includes(status.visibility);
const spacing: { const spacing: {
[key: string]: React.ComponentProps<typeof HStack>['space']; [key: string]: React.ComponentProps<typeof HStack>['space'];
@ -752,92 +977,69 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
onClick={e => e.stopPropagation()} onClick={e => e.stopPropagation()}
alignItems='center' alignItems='center'
> >
{status.group ? ( <ReplyButton
<GroupPopover status={status}
group={status.group} statusActionButtonTheme={statusActionButtonTheme}
isEnabled={replyDisabled} withLabels={withLabels}
> me={me}
{replyButton} onOpenUnauthorizedModal={onOpenUnauthorizedModal}
</GroupPopover> rebloggedBy={rebloggedBy}
) : replyButton}
{(features.quotePosts && me) ? (
<DropdownMenu
items={reblogMenu}
disabled={!publicStatus}
onShiftClick={handleReblogClick}
>
{reblogButton}
</DropdownMenu>
) : (
reblogButton
)}
<StatusActionButton
title={intl.formatMessage(messages.favourite)}
icon={features.statusDislikes ? require('@tabler/icons/outline/thumb-up.svg') : require('@tabler/icons/outline/heart.svg')}
color='accent'
filled
onClick={handleFavouriteClick}
onLongPress={handleFavouriteLongPress}
active={status.favourited}
count={favouriteCount}
text={withLabels ? intl.formatMessage(messages.favourite) : undefined}
theme={statusActionButtonTheme}
/> />
{features.statusDislikes && ( <ReblogButton
<StatusActionButton status={status}
title={intl.formatMessage(messages.disfavourite)} statusActionButtonTheme={statusActionButtonTheme}
icon={require('@tabler/icons/outline/thumb-down.svg')} withLabels={withLabels}
color='accent' me={me}
filled onOpenUnauthorizedModal={onOpenUnauthorizedModal}
onClick={handleDislikeClick} publicStatus={publicStatus}
onLongPress={handleDislikeLongPress} />
active={status.disliked}
count={status.dislikes_count}
text={withLabels ? intl.formatMessage(messages.disfavourite) : undefined}
theme={statusActionButtonTheme}
/>
)}
{me && !withLabels && features.emojiReacts && showWrenchButton && ( <FavouriteButton
<StatusActionButton status={status}
title={intl.formatMessage(messages.wrench)} statusActionButtonTheme={statusActionButtonTheme}
icon={require('@tabler/icons/outline/tool.svg')} withLabels={withLabels}
color='accent' me={me}
filled onOpenUnauthorizedModal={onOpenUnauthorizedModal}
onClick={handleWrenchClick} />
onLongPress={handleWrenchLongPress}
active={wrenches?.me}
count={wrenches?.count || undefined}
theme={statusActionButtonTheme}
/>
)}
{me && !withLabels && features.emojiReacts && ( <DislikeButton
<EmojiPickerDropdown status={status}
onPickEmoji={handlePickEmoji} statusActionButtonTheme={statusActionButtonTheme}
theme={statusActionButtonTheme} withLabels={withLabels}
/> me={me}
)} onOpenUnauthorizedModal={onOpenUnauthorizedModal}
/>
{canShare && ( <WrenchButton
<StatusActionButton status={status}
title={intl.formatMessage(messages.share)} statusActionButtonTheme={statusActionButtonTheme}
icon={require('@tabler/icons/outline/upload.svg')} withLabels={withLabels}
onClick={handleShareClick} me={me}
theme={statusActionButtonTheme} onOpenUnauthorizedModal={onOpenUnauthorizedModal}
/> />
)}
<DropdownMenu items={menu}> <EmojiPickerButton
<StatusActionButton status={status}
title={intl.formatMessage(messages.more)} statusActionButtonTheme={statusActionButtonTheme}
icon={require('@tabler/icons/outline/dots.svg')} withLabels={withLabels}
theme={statusActionButtonTheme} me={me}
/> />
</DropdownMenu>
<ShareButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
/>
<MenuButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
expandable={expandable}
fromBookmarks={fromBookmarks}
/>
</HStack> </HStack>
</HStack> </HStack>
); );