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 { Group } from 'pl-fe/normalizers/group';
import type { SelectedStatus } from 'pl-fe/selectors';
import type { Me } from 'pl-fe/types/pl-fe';
const messages = defineMessages({
adminAccount: { id: 'status.admin_account', defaultMessage: 'Moderate @{name}' },
@ -110,29 +111,358 @@ const messages = defineMessages({
hideTranslation: { id: 'status.hide_translation', defaultMessage: 'Hide translation' },
});
interface IStatusActionBar {
status: SelectedStatus;
interface IActionButton extends Pick<IStatusActionBar, 'status' | 'statusActionButtonTheme' | 'withLabels'> {
me: Me;
onOpenUnauthorizedModal: (action?: UnauthorizedModalAction) => void;
}
interface IReplyButton extends IActionButton {
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;
space?: 'sm' | 'md' | 'lg';
statusActionButtonTheme?: 'default' | 'inverse';
fromBookmarks?: boolean;
}
const StatusActionBar: React.FC<IStatusActionBar> = ({
const MenuButton: React.FC<IMenuButton> = ({
status,
withLabels = false,
statusActionButtonTheme,
withLabels,
me,
expandable,
space = 'sm',
statusActionButtonTheme = 'default',
fromBookmarks = false,
rebloggedBy,
fromBookmarks,
}) => {
const intl = useIntl();
const history = useHistory();
const dispatch = useAppDispatch();
const match = useRouteMatch<{ groupId: string }>('/groups/:groupId');
const { boostModal } = useSettings();
const { openModal } = useModalsStore();
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 { getOrCreateChatByAccountId } = useChats();
const me = useAppSelector(state => state.me);
const { groupRelationship } = useGroupRelationship(status.group_id || undefined);
const features = useFeatures();
const instance = useInstance();
const { autoTranslate, boostModal, deleteModal, knownLanguages, showWrenchButton } = useSettings();
const wrenches = showWrenchButton && status.emoji_reactions.find(emoji => emoji.name === '🔧') || undefined;
const { autoTranslate, deleteModal, knownLanguages } = useSettings();
const { translationLanguages } = useTranslationLanguages();
@ -166,76 +493,6 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
const isStaff = account ? account.is_admin || account.is_moderator : 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) => {
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) => {
if (!deleteModal) {
dispatch(deleteStatus(status.id, withRedraft));
@ -301,6 +533,15 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
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) => {
dispatch(mentionCompose(status.account));
};
@ -669,71 +910,55 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
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);
let reblogIcon = require('@tabler/icons/outline/repeat.svg');
let replyTitle;
let replyDisabled = false;
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');
}
if ((status.group as Group)?.membership_required && !groupRelationship?.member) {
replyDisabled = true;
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}
/>
return (
<DropdownMenu items={menu}>
<StatusActionButton
title={intl.formatMessage(messages.more)}
icon={require('@tabler/icons/outline/dots.svg')}
theme={statusActionButtonTheme}
/>
</DropdownMenu>
);
};
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'),
}];
interface IStatusActionBar {
status: SelectedStatus;
rebloggedBy?: Account;
withLabels?: boolean;
expandable?: boolean;
space?: 'sm' | 'md' | 'lg';
statusActionButtonTheme?: 'default' | 'inverse';
fromBookmarks?: boolean;
}
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={reblogCount + quoteCount}
text={withLabels ? intl.formatMessage(messages.reblog) : undefined}
theme={statusActionButtonTheme}
/>
);
const StatusActionBar: React.FC<IStatusActionBar> = ({
status,
withLabels = false,
expandable,
space = 'sm',
statusActionButtonTheme = 'default',
fromBookmarks = false,
rebloggedBy,
}) => {
if (!status.in_reply_to_id) {
replyTitle = intl.formatMessage(messages.reply);
} else {
replyTitle = intl.formatMessage(messages.replyAll);
const { openModal } = useModalsStore();
const me = useAppSelector(state => state.me);
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: {
[key: string]: React.ComponentProps<typeof HStack>['space'];
@ -752,92 +977,69 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
onClick={e => e.stopPropagation()}
alignItems='center'
>
{status.group ? (
<GroupPopover
group={status.group}
isEnabled={replyDisabled}
>
{replyButton}
</GroupPopover>
) : 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}
<ReplyButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
rebloggedBy={rebloggedBy}
/>
{features.statusDislikes && (
<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}
/>
)}
<ReblogButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
publicStatus={publicStatus}
/>
{me && !withLabels && features.emojiReacts && showWrenchButton && (
<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}
/>
)}
<FavouriteButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
/>
{me && !withLabels && features.emojiReacts && (
<EmojiPickerDropdown
onPickEmoji={handlePickEmoji}
theme={statusActionButtonTheme}
/>
)}
<DislikeButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
/>
{canShare && (
<StatusActionButton
title={intl.formatMessage(messages.share)}
icon={require('@tabler/icons/outline/upload.svg')}
onClick={handleShareClick}
theme={statusActionButtonTheme}
/>
)}
<WrenchButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
/>
<DropdownMenu items={menu}>
<StatusActionButton
title={intl.formatMessage(messages.more)}
icon={require('@tabler/icons/outline/dots.svg')}
theme={statusActionButtonTheme}
/>
</DropdownMenu>
<EmojiPickerButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
/>
<ShareButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
/>
<MenuButton
status={status}
statusActionButtonTheme={statusActionButtonTheme}
withLabels={withLabels}
me={me}
onOpenUnauthorizedModal={onOpenUnauthorizedModal}
expandable={expandable}
fromBookmarks={fromBookmarks}
/>
</HStack>
</HStack>
);