From 98bf2fc27c6204527d8da5bc3847bd8406f1bfe8 Mon Sep 17 00:00:00 2001
From: Renaud Chaput <renchap@gmail.com>
Date: Tue, 20 Aug 2024 00:11:58 +0200
Subject: [PATCH] Improve the list selection UI for notification requests
 (#31457)

---
 .../features/notifications/requests.jsx       | 121 +++++++-----------
 app/javascript/mastodon/locales/en.json       |  22 ++--
 .../styles/mastodon-light/variables.scss      |   1 +
 .../styles/mastodon/components.scss           |  26 ++--
 app/javascript/styles/mastodon/variables.scss |   1 +
 5 files changed, 70 insertions(+), 101 deletions(-)

diff --git a/app/javascript/mastodon/features/notifications/requests.jsx b/app/javascript/mastodon/features/notifications/requests.jsx
index f323bda4fb..f35c042ba6 100644
--- a/app/javascript/mastodon/features/notifications/requests.jsx
+++ b/app/javascript/mastodon/features/notifications/requests.jsx
@@ -7,6 +7,7 @@ import { Helmet } from 'react-helmet';
 
 import { useSelector, useDispatch } from 'react-redux';
 
+import ArrowDropDownIcon from '@/material-icons/400-24px/arrow_drop_down.svg?react';
 import InventoryIcon from '@/material-icons/400-24px/inventory_2.svg?react';
 import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react';
 import { openModal } from 'mastodon/actions/modal';
@@ -15,6 +16,7 @@ import { changeSetting } from 'mastodon/actions/settings';
 import { CheckBox } from 'mastodon/components/check_box';
 import Column from 'mastodon/components/column';
 import ColumnHeader from 'mastodon/components/column_header';
+import { Icon } from 'mastodon/components/icon';
 import ScrollableList from 'mastodon/components/scrollable_list';
 import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
 
@@ -26,16 +28,14 @@ const messages = defineMessages({
   title: { id: 'notification_requests.title', defaultMessage: 'Filtered notifications' },
   maximize: { id: 'notification_requests.maximize', defaultMessage: 'Maximize' },
   more: { id: 'status.more', defaultMessage: 'More' },
-  acceptAll: { id: 'notification_requests.accept_all', defaultMessage: 'Accept all' },
-  dismissAll: { id: 'notification_requests.dismiss_all', defaultMessage: 'Dismiss all' },
-  acceptMultiple: { id: 'notification_requests.accept_multiple', defaultMessage: '{count, plural, one {Accept # request} other {Accept # requests}}' },
-  dismissMultiple: { id: 'notification_requests.dismiss_multiple', defaultMessage: '{count, plural, one {Dismiss # request} other {Dismiss # requests}}' },
-  confirmAcceptAllTitle: { id: 'notification_requests.confirm_accept_all.title', defaultMessage: 'Accept notification requests?' },
-  confirmAcceptAllMessage: { id: 'notification_requests.confirm_accept_all.message', defaultMessage: 'You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?' },
-  confirmAcceptAllButton: { id: 'notification_requests.confirm_accept_all.button', defaultMessage: 'Accept all' },
-  confirmDismissAllTitle: { id: 'notification_requests.confirm_dismiss_all.title', defaultMessage: 'Dismiss notification requests?' },
-  confirmDismissAllMessage: { id: 'notification_requests.confirm_dismiss_all.message', defaultMessage: "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?" },
-  confirmDismissAllButton: { id: 'notification_requests.confirm_dismiss_all.button', defaultMessage: 'Dismiss all' },
+  acceptMultiple: { id: 'notification_requests.accept_multiple', defaultMessage: '{count, plural, one {Accept # request…} other {Accept # requests…}}' },
+  dismissMultiple: { id: 'notification_requests.dismiss_multiple', defaultMessage: '{count, plural, one {Dismiss # request…} other {Dismiss # requests…}}' },
+  confirmAcceptMultipleTitle: { id: 'notification_requests.confirm_accept_multiple.title', defaultMessage: 'Accept notification requests?' },
+  confirmAcceptMultipleMessage: { id: 'notification_requests.confirm_accept_multiple.message', defaultMessage: 'You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?' },
+  confirmAcceptMultipleButton: { id: 'notification_requests.confirm_accept_multiple.button', defaultMessage: '{count, plural, one {Accept request} other {Accept requests}}' },
+  confirmDismissMultipleTitle: { id: 'notification_requests.confirm_dismiss_multiple.title', defaultMessage: 'Dismiss notification requests?' },
+  confirmDismissMultipleMessage: { id: 'notification_requests.confirm_dismiss_multiple.message', defaultMessage: "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?" },
+  confirmDismissMultipleButton: { id: 'notification_requests.confirm_dismiss_multiple.button', defaultMessage: '{count, plural, one {Dismiss request} other {Dismiss requests}}' },
 });
 
 const ColumnSettings = () => {
@@ -74,45 +74,15 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
   const intl = useIntl();
   const dispatch = useDispatch();
 
-  const notificationRequests = useSelector(state => state.getIn(['notificationRequests', 'items']));
-
   const selectedCount = selectedItems.length;
 
-  const handleAcceptAll = useCallback(() => {
-    const items = notificationRequests.map(request => request.get('id')).toArray();
-    dispatch(openModal({
-      modalType: 'CONFIRM',
-      modalProps: {
-        title: intl.formatMessage(messages.confirmAcceptAllTitle),
-        message: intl.formatMessage(messages.confirmAcceptAllMessage, { count: items.length }),
-        confirm: intl.formatMessage(messages.confirmAcceptAllButton),
-        onConfirm: () =>
-          dispatch(acceptNotificationRequests(items)),
-      },
-    }));
-  }, [dispatch, intl, notificationRequests]);
-
-  const handleDismissAll = useCallback(() => {
-    const items = notificationRequests.map(request => request.get('id')).toArray();
-    dispatch(openModal({
-      modalType: 'CONFIRM',
-      modalProps: {
-        title: intl.formatMessage(messages.confirmDismissAllTitle),
-        message: intl.formatMessage(messages.confirmDismissAllMessage, { count: items.length }),
-        confirm: intl.formatMessage(messages.confirmDismissAllButton),
-        onConfirm: () =>
-          dispatch(dismissNotificationRequests(items)),
-      },
-    }));
-  }, [dispatch, intl, notificationRequests]);
-
   const handleAcceptMultiple = useCallback(() => {
     dispatch(openModal({
       modalType: 'CONFIRM',
       modalProps: {
-        title: intl.formatMessage(messages.confirmAcceptAllTitle),
-        message: intl.formatMessage(messages.confirmAcceptAllMessage, { count: selectedItems.length }),
-        confirm: intl.formatMessage(messages.confirmAcceptAllButton),
+        title: intl.formatMessage(messages.confirmAcceptMultipleTitle),
+        message: intl.formatMessage(messages.confirmAcceptMultipleMessage, { count: selectedItems.length }),
+        confirm: intl.formatMessage(messages.confirmAcceptMultipleButton, { count: selectedItems.length}),
         onConfirm: () =>
           dispatch(acceptNotificationRequests(selectedItems)),
       },
@@ -123,9 +93,9 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
     dispatch(openModal({
       modalType: 'CONFIRM',
       modalProps: {
-        title: intl.formatMessage(messages.confirmDismissAllTitle),
-        message: intl.formatMessage(messages.confirmDismissAllMessage, { count: selectedItems.length }),
-        confirm: intl.formatMessage(messages.confirmDismissAllButton),
+        title: intl.formatMessage(messages.confirmDismissMultipleTitle),
+        message: intl.formatMessage(messages.confirmDismissMultipleMessage, { count: selectedItems.length }),
+        confirm: intl.formatMessage(messages.confirmDismissMultipleButton, { count: selectedItems.length}),
         onConfirm: () =>
           dispatch(dismissNotificationRequests(selectedItems)),
       },
@@ -136,46 +106,45 @@ const SelectRow = ({selectAllChecked, toggleSelectAll, selectedItems, selectionM
     setSelectionMode((mode) => !mode);
   }, [setSelectionMode]);
 
-  const menu = selectedCount === 0 ?
-    [
-      { text: intl.formatMessage(messages.acceptAll), action: handleAcceptAll },
-      { text: intl.formatMessage(messages.dismissAll), action: handleDismissAll },
-    ] : [
-      { text: intl.formatMessage(messages.acceptMultiple, { count: selectedCount }), action: handleAcceptMultiple },
-      { text: intl.formatMessage(messages.dismissMultiple, { count: selectedCount }), action: handleDismissMultiple },
-    ];
+  const menu = [
+    { text: intl.formatMessage(messages.acceptMultiple, { count: selectedCount }), action: handleAcceptMultiple },
+    { text: intl.formatMessage(messages.dismissMultiple, { count: selectedCount }), action: handleDismissMultiple },
+  ];
+
+  const handleSelectAll = useCallback(() => {
+    setSelectionMode(true);
+    toggleSelectAll();
+  }, [setSelectionMode, toggleSelectAll]);
 
   return (
     <div className='column-header__select-row'>
-      {selectionMode && (
-        <div className='column-header__select-row__checkbox'>
-          <CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={toggleSelectAll} />
-        </div>
-      )}
-      <div className='column-header__select-row__selection-mode'>
+      <div className='column-header__select-row__checkbox'>
+        <CheckBox checked={selectAllChecked} indeterminate={selectedCount > 0 && !selectAllChecked} onChange={handleSelectAll} />
+      </div>
+      <DropdownMenuContainer
+        items={menu}
+        icons='ellipsis-h'
+        iconComponent={MoreHorizIcon}
+        direction='right'
+        title={intl.formatMessage(messages.more)}
+      >
+        <button className='dropdown-button column-header__select-row__select-menu' disabled={selectedItems.length === 0}>
+          <span className='dropdown-button__label'>
+            {selectedCount} selected
+          </span>
+          <Icon id='down' icon={ArrowDropDownIcon} />
+        </button>
+      </DropdownMenuContainer>
+      <div className='column-header__select-row__mode-button'>
         <button className='text-btn' tabIndex={0} onClick={handleToggleSelectionMode}>
           {selectionMode ? (
-            <FormattedMessage id='notification_requests.exit_selection_mode' defaultMessage='Cancel' />
+            <FormattedMessage id='notification_requests.exit_selection' defaultMessage='Done' />
           ) :
             (
-              <FormattedMessage id='notification_requests.enter_selection_mode' defaultMessage='Select' />
+              <FormattedMessage id='notification_requests.edit_selection' defaultMessage='Edit' />
             )}
         </button>
       </div>
-      {selectedCount > 0 &&
-        <div className='column-header__select-row__selected-count'>
-          {selectedCount} selected
-        </div>
-      }
-      <div className='column-header__select-row__actions'>
-        <DropdownMenuContainer
-          items={menu}
-          icons='ellipsis-h'
-          iconComponent={MoreHorizIcon}
-          direction='right'
-          title={intl.formatMessage(messages.more)}
-        />
-      </div>
     </div>
   );
 };
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index aeeaf8209c..6d2b93be57 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -518,19 +518,17 @@
   "notification.status": "{name} just posted",
   "notification.update": "{name} edited a post",
   "notification_requests.accept": "Accept",
-  "notification_requests.accept_all": "Accept all",
-  "notification_requests.accept_multiple": "{count, plural, one {Accept # request} other {Accept # requests}}",
-  "notification_requests.confirm_accept_all.button": "Accept all",
-  "notification_requests.confirm_accept_all.message": "You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?",
-  "notification_requests.confirm_accept_all.title": "Accept notification requests?",
-  "notification_requests.confirm_dismiss_all.button": "Dismiss all",
-  "notification_requests.confirm_dismiss_all.message": "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?",
-  "notification_requests.confirm_dismiss_all.title": "Dismiss notification requests?",
+  "notification_requests.accept_multiple": "{count, plural, one {Accept # request…} other {Accept # requests…}}",
+  "notification_requests.confirm_accept_multiple.button": "{count, plural, one {Accept request} other {Accept requests}}",
+  "notification_requests.confirm_accept_multiple.message": "You are about to accept {count, plural, one {one notification request} other {# notification requests}}. Are you sure you want to proceed?",
+  "notification_requests.confirm_accept_multiple.title": "Accept notification requests?",
+  "notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Dismiss request} other {Dismiss requests}}",
+  "notification_requests.confirm_dismiss_multiple.message": "You are about to dismiss {count, plural, one {one notification request} other {# notification requests}}. You won't be able to easily access {count, plural, one {it} other {them}} again. Are you sure you want to proceed?",
+  "notification_requests.confirm_dismiss_multiple.title": "Dismiss notification requests?",
   "notification_requests.dismiss": "Dismiss",
-  "notification_requests.dismiss_all": "Dismiss all",
-  "notification_requests.dismiss_multiple": "{count, plural, one {Dismiss # request} other {Dismiss # requests}}",
-  "notification_requests.enter_selection_mode": "Select",
-  "notification_requests.exit_selection_mode": "Cancel",
+  "notification_requests.dismiss_multiple": "{count, plural, one {Dismiss # request…} other {Dismiss # requests…}}",
+  "notification_requests.edit_selection": "Edit",
+  "notification_requests.exit_selection": "Done",
   "notification_requests.explainer_for_limited_account": "Notifications from this account have been filtered because the account has been limited by a moderator.",
   "notification_requests.explainer_for_limited_remote_account": "Notifications from this account have been filtered because the account or its server has been limited by a moderator.",
   "notification_requests.maximize": "Maximize",
diff --git a/app/javascript/styles/mastodon-light/variables.scss b/app/javascript/styles/mastodon-light/variables.scss
index 9d4fd60945..39809b437f 100644
--- a/app/javascript/styles/mastodon-light/variables.scss
+++ b/app/javascript/styles/mastodon-light/variables.scss
@@ -65,4 +65,5 @@ body {
   --background-color: #fff;
   --background-color-tint: rgba(255, 255, 255, 80%);
   --background-filter: blur(10px);
+  --on-surface-color: #{transparentize($ui-base-color, 0.65)};
 }
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 895762b803..c1ee4ea10d 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -4217,7 +4217,7 @@ a.status-card {
   text-decoration: none;
 
   &:hover {
-    background: lighten($ui-base-color, 2%);
+    background: var(--on-surface-color);
   }
 }
 
@@ -4346,19 +4346,18 @@ a.status-card {
     display: flex;
   }
 
-  &__selection-mode {
-    flex-grow: 1;
-
-    .text-btn:hover {
-      text-decoration: underline;
-    }
+  &__select-menu:disabled {
+    visibility: hidden;
   }
 
-  &__actions {
-    .icon-button {
-      border-radius: 4px;
-      border: 1px solid var(--background-border-color);
-      padding: 5px;
+  &__mode-button {
+    margin-left: auto;
+    color: $highlight-text-color;
+    font-weight: bold;
+    font-size: 14px;
+
+    &:hover {
+      color: lighten($highlight-text-color, 6%);
     }
   }
 }
@@ -4566,6 +4565,7 @@ a.status-card {
   padding: 0;
   font-family: inherit;
   font-size: inherit;
+  font-weight: inherit;
   color: inherit;
   border: 0;
   background: transparent;
@@ -10366,7 +10366,7 @@ noscript {
     cursor: pointer;
 
     &:hover {
-      background: lighten($ui-base-color, 1%);
+      background: var(--on-surface-color);
     }
 
     .notification-request__checkbox {
diff --git a/app/javascript/styles/mastodon/variables.scss b/app/javascript/styles/mastodon/variables.scss
index 92b4770fe3..c8271e0dcd 100644
--- a/app/javascript/styles/mastodon/variables.scss
+++ b/app/javascript/styles/mastodon/variables.scss
@@ -109,5 +109,6 @@ $font-monospace: 'mastodon-font-monospace' !default;
   --surface-background-color: #{darken($ui-base-color, 4%)};
   --surface-variant-background-color: #{$ui-base-color};
   --surface-variant-active-background-color: #{lighten($ui-base-color, 4%)};
+  --on-surface-color: #{transparentize($ui-base-color, 0.5)};
   --avatar-border-radius: 8px;
 }