diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index f63a3ebf39..c3c41f3c5d 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
-# using RuboCop version 1.75.1.
+# using RuboCop version 1.75.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
@@ -58,12 +58,6 @@ Style/FormatStringToken:
Style/GuardClause:
Enabled: false
-# This cop supports unsafe autocorrection (--autocorrect-all).
-Style/HashTransformValues:
- Exclude:
- - 'app/serializers/rest/web_push_subscription_serializer.rb'
- - 'app/services/import_service.rb'
-
# Configuration parameters: AllowedMethods.
# AllowedMethods: respond_to_missing?
Style/OptionalBooleanParameter:
diff --git a/Gemfile.lock b/Gemfile.lock
index 86cfaa3132..1ed4b71318 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -201,7 +201,7 @@ GEM
domain_name (0.6.20240107)
doorkeeper (5.8.2)
railties (>= 5)
- dotenv (3.1.7)
+ dotenv (3.1.8)
drb (2.2.1)
elasticsearch (7.17.11)
elasticsearch-api (= 7.17.11)
diff --git a/app/javascript/mastodon/components/account.tsx b/app/javascript/mastodon/components/account.tsx
index 55f1e6fb91..f6241504f6 100644
--- a/app/javascript/mastodon/components/account.tsx
+++ b/app/javascript/mastodon/components/account.tsx
@@ -1,4 +1,4 @@
-import { useCallback } from 'react';
+import { useCallback, useMemo } from 'react';
import { defineMessages, useIntl, FormattedMessage } from 'react-intl';
@@ -12,6 +12,7 @@ import {
muteAccount,
unmuteAccount,
} from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
import { initMuteModal } from 'mastodon/actions/mutes';
import { Avatar } from 'mastodon/components/avatar';
import { Button } from 'mastodon/components/button';
@@ -23,7 +24,7 @@ import { RelativeTimestamp } from 'mastodon/components/relative_timestamp';
import { ShortNumber } from 'mastodon/components/short_number';
import { Skeleton } from 'mastodon/components/skeleton';
import { VerifiedBadge } from 'mastodon/components/verified_badge';
-import { me } from 'mastodon/initial_state';
+import type { MenuItem } from 'mastodon/models/dropdown_menu';
import { useAppSelector, useAppDispatch } from 'mastodon/store';
const messages = defineMessages({
@@ -46,6 +47,14 @@ const messages = defineMessages({
mute: { id: 'account.mute_short', defaultMessage: 'Mute' },
block: { id: 'account.block_short', defaultMessage: 'Block' },
more: { id: 'status.more', defaultMessage: 'More' },
+ addToLists: {
+ id: 'account.add_or_remove_from_list',
+ defaultMessage: 'Add or Remove from lists',
+ },
+ openOriginalPage: {
+ id: 'account.open_original_page',
+ defaultMessage: 'Open original page',
+ },
});
export const Account: React.FC<{
@@ -60,6 +69,7 @@ export const Account: React.FC<{
const account = useAppSelector((state) => state.accounts.get(id));
const relationship = useAppSelector((state) => state.relationships.get(id));
const dispatch = useAppDispatch();
+ const accountUrl = account?.url;
const handleBlock = useCallback(() => {
if (relationship?.blocking) {
@@ -77,13 +87,62 @@ export const Account: React.FC<{
}
}, [dispatch, id, account, relationship]);
- const handleMuteNotifications = useCallback(() => {
- dispatch(muteAccount(id, true));
- }, [dispatch, id]);
+ const menu = useMemo(() => {
+ let arr: MenuItem[] = [];
- const handleUnmuteNotifications = useCallback(() => {
- dispatch(muteAccount(id, false));
- }, [dispatch, id]);
+ if (defaultAction === 'mute') {
+ const handleMuteNotifications = () => {
+ dispatch(muteAccount(id, true));
+ };
+
+ const handleUnmuteNotifications = () => {
+ dispatch(muteAccount(id, false));
+ };
+
+ arr = [
+ {
+ text: intl.formatMessage(
+ relationship?.muting_notifications
+ ? messages.unmute_notifications
+ : messages.mute_notifications,
+ ),
+ action: relationship?.muting_notifications
+ ? handleUnmuteNotifications
+ : handleMuteNotifications,
+ },
+ ];
+ } else if (defaultAction !== 'block') {
+ const handleAddToLists = () => {
+ dispatch(
+ openModal({
+ modalType: 'LIST_ADDER',
+ modalProps: {
+ accountId: id,
+ },
+ }),
+ );
+ };
+
+ arr = [
+ {
+ text: intl.formatMessage(messages.addToLists),
+ action: handleAddToLists,
+ },
+ ];
+
+ if (accountUrl) {
+ arr.unshift(
+ {
+ text: intl.formatMessage(messages.openOriginalPage),
+ href: accountUrl,
+ },
+ null,
+ );
+ }
+ }
+
+ return arr;
+ }, [dispatch, intl, id, accountUrl, relationship, defaultAction]);
if (hidden) {
return (
@@ -94,68 +153,42 @@ export const Account: React.FC<{
);
}
- let buttons;
+ let button: React.ReactNode, dropdown: React.ReactNode;
- if (account && account.id !== me && relationship) {
- const { requested, blocking, muting } = relationship;
-
- if (requested) {
- buttons = ;
- } else if (blocking) {
- buttons = (
-
- );
- } else if (muting) {
- const menu = [
- {
- text: intl.formatMessage(
- relationship.muting_notifications
- ? messages.unmute_notifications
- : messages.mute_notifications,
- ),
- action: relationship.muting_notifications
- ? handleUnmuteNotifications
- : handleMuteNotifications,
- },
- ];
-
- buttons = (
- <>
-
-
-
- >
- );
- } else if (defaultAction === 'mute') {
- buttons = (
-
- );
- } else if (defaultAction === 'block') {
- buttons = (
-
- );
- } else {
- buttons = ;
- }
- } else {
- buttons = ;
+ if (menu.length > 0) {
+ dropdown = (
+
+ );
}
- let muteTimeRemaining;
+ if (defaultAction === 'block') {
+ button = (
+
+ );
+ } else if (defaultAction === 'mute') {
+ button = (
+
+ );
+ } else {
+ button = ;
+ }
+
+ let muteTimeRemaining: React.ReactNode;
if (account?.mute_expires_at) {
muteTimeRemaining = (
@@ -165,7 +198,7 @@ export const Account: React.FC<{
);
}
- let verification;
+ let verification: React.ReactNode;
const firstVerifiedField = account?.fields.find((item) => !!item.verified_at);
@@ -211,7 +244,12 @@ export const Account: React.FC<{
- {!minimal &&
{buttons}
}
+ {!minimal && (
+
+ {dropdown}
+ {button}
+
+ )}
{account &&
diff --git a/app/javascript/mastodon/components/dropdown_menu.tsx b/app/javascript/mastodon/components/dropdown_menu.tsx
index a5d2deaae1..886d517fa9 100644
--- a/app/javascript/mastodon/components/dropdown_menu.tsx
+++ b/app/javascript/mastodon/components/dropdown_menu.tsx
@@ -71,6 +71,8 @@ type RenderItemFn- = (
},
) => React.ReactNode;
+type ItemClickFn
- = (item: Item, index: number) => void;
+
type RenderHeaderFn
- = (items: Item[]) => React.ReactNode;
interface DropdownMenuProps
- {
@@ -81,10 +83,10 @@ interface DropdownMenuProps
- {
openedViaKeyboard: boolean;
renderItem?: RenderItemFn
- ;
renderHeader?: RenderHeaderFn
- ;
- onItemClick: (e: React.MouseEvent | React.KeyboardEvent) => void;
+ onItemClick?: ItemClickFn
- ;
}
-const DropdownMenu =
- ({
+export const DropdownMenu =
- ({
items,
loading,
scrollable,
@@ -176,20 +178,35 @@ const DropdownMenu =
- ({
[],
);
+ const handleItemClick = useCallback(
+ (e: React.MouseEvent | React.KeyboardEvent) => {
+ const i = Number(e.currentTarget.getAttribute('data-index'));
+ const item = items?.[i];
+
+ onClose();
+
+ if (!item) {
+ return;
+ }
+
+ if (typeof onItemClick === 'function') {
+ e.preventDefault();
+ onItemClick(item, i);
+ } else if (isActionItem(item)) {
+ e.preventDefault();
+ item.action();
+ }
+ },
+ [onClose, onItemClick, items],
+ );
+
const handleItemKeyUp = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter' || e.key === ' ') {
- onItemClick(e);
+ handleItemClick(e);
}
},
- [onItemClick],
- );
-
- const handleClick = useCallback(
- (e: React.MouseEvent | React.KeyboardEvent) => {
- onItemClick(e);
- },
- [onItemClick],
+ [handleItemClick],
);
const nativeRenderItem = (option: Item, i: number) => {
@@ -209,7 +226,7 @@ const DropdownMenu =
- ({
element = (