From ff41cd3563dd2425981ff4154caa03d6bb23a662 Mon Sep 17 00:00:00 2001 From: Lim Chee Aun Date: Mon, 17 Jul 2023 21:01:00 +0800 Subject: [PATCH] Replace (most) alert/confirms with alternative UI Everything might break lol --- src/app.css | 75 ++++++++++++++-- src/components/account-info.jsx | 99 +++++++++++++-------- src/components/drafts.jsx | 68 ++++++++------ src/components/icon.jsx | 1 + src/components/list-add-edit.jsx | 22 +++-- src/components/menu-confirm.jsx | 43 +++++++++ src/components/status.jsx | 146 +++++++++++++++++++++++++------ src/pages/accounts.jsx | 17 +++- src/pages/hashtag.jsx | 18 ++-- src/pages/list.jsx | 24 +++-- src/utils/toast-alert.js | 34 +++++++ 11 files changed, 423 insertions(+), 124 deletions(-) create mode 100644 src/components/menu-confirm.jsx create mode 100644 src/utils/toast-alert.js diff --git a/src/app.css b/src/app.css index 9f70cbe..1e744d1 100644 --- a/src/app.css +++ b/src/app.css @@ -1401,7 +1401,7 @@ body > .szh-menu-container { animation: appear-smooth 0.15s ease-in-out; width: 16em; max-width: 90vw; - overflow: hidden; + /* overflow: hidden; */ } .szh-menu[aria-label='Submenu'] { background-color: var(--bg-blur-color); @@ -1418,6 +1418,7 @@ body > .szh-menu-container { text-shadow: 0 1px 0 var(--bg-color); line-height: 1.2; /* border-bottom: 1px solid var(--outline-color); */ + border-radius: 8px 8px 0 0; } .szh-menu__header.plain { margin-bottom: 0; @@ -1426,6 +1427,28 @@ body > .szh-menu-container { .szh-menu__header * { vertical-align: middle; } +.szh-menu.menu-emphasized { + border-color: var(--outline-hover-color); + box-shadow: 0 3px 16px -3px var(--drop-shadow-color), + 0 3px 32px var(--drop-shadow-color), 0 3px 48px var(--drop-shadow-color); + background-color: var(--bg-color); + animation-duration: 0.3s; + animation-timing-function: ease-in-out; + width: auto; +} +.szh-menu .footer { + margin: 8px 0 -8px; + padding: 8px 16px; + color: var(--text-insignificant-color); + font-size: 90%; + background-color: var(--bg-faded-color); + text-shadow: 0 1px 0 var(--bg-color); + line-height: 1.2; + display: flex; + gap: 8px; + align-items: center; + border-radius: 0 0 8px 8px; +} .szh-menu .szh-menu__item { display: flex; gap: 8px; @@ -1498,21 +1521,26 @@ body > .szh-menu-container { font-size: inherit; } .szh-menu .menu-horizontal { - display: flex; + display: grid; + /* two columns only */ + grid-template-columns: repeat(2, 1fr); } -.szh-menu .menu-horizontal .szh-menu__item { - flex: 1; -} -.szh-menu .menu-horizontal .szh-menu__item:not(:only-child):first-child { +.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child, +.szh-menu .menu-horizontal > *:not(:only-child):first-child .szh-menu__item { padding-right: 4px !important; } .szh-menu .menu-horizontal - .szh-menu__item:not(:only-child):not(:first-child):not(:last-child) { + > .szh-menu__item:not(:only-child):not(:first-child):not(:last-child), +.szh-menu + .menu-horizontal + > *:not(:only-child):not(:first-child):not(:last-child) + .szh-menu__item { padding-left: 8px !important; padding-right: 4px !important; } -.szh-menu .menu-horizontal .szh-menu__item:not(:only-child):last-child { +.szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):last-child, +.szh-menu .menu-horizontal > *:not(:only-child):last-child .szh-menu__item { padding-left: 8px !important; } .szh-menu .szh-menu__item .menu-shortcut { @@ -1533,6 +1561,19 @@ body > .szh-menu-container { color: var(--red-color); opacity: 1; } +.szh-menu + .szh-menu__item:not(.szh-menu__item--disabled):not( + .szh-menu__item--hover + ).danger { + color: var(--red-color); +} +.szh-menu + .szh-menu__item:not(.szh-menu__item--disabled):not( + .szh-menu__item--hover + ).danger + .icon { + opacity: 1; +} .szh-menu .menu-wrap { display: flex; @@ -1658,6 +1699,24 @@ meter.donut[hidden] { margin-bottom: env(safe-area-inset-bottom); } +/* TOAST - ALERT */ + +:root .toastify.alert { + z-index: 1001; + box-shadow: 0 8px 32px var(--text-insignificant-color); + background-color: var(--bg-color); + color: var(--text-color); + cursor: pointer; + pointer-events: auto; + padding: 16px 32px; + font-size: max(calc(16px * 1.1), var(--text-size)); + text-align: center; + line-height: 1.25; +} +:root .toastify.alert:is(:hover, :active) { + background-color: var(--bg-faded-color); +} + /* AVATARS STACK */ .avatars-stack { diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index d21a567..9df1f25 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -21,6 +21,7 @@ import Icon from './icon'; import Link from './link'; import ListAddEdit from './list-add-edit'; import Loader from './loader'; +import MenuConfirm from './menu-confirm'; import Modal from './modal'; import TranslationBlock from './translation-block'; @@ -734,11 +735,20 @@ function RelatedActions({ info, instance, authenticated }) { )} - + + Block @{username}? + + } + menuItemClassName="danger" onClick={() => { - if (!blocking && !confirm(`Block @${username}?`)) { - return; - } + // if (!blocking && !confirm(`Block @${username}?`)) { + // return; + // } setRelationshipUIState('loading'); (async () => { try { @@ -784,7 +794,7 @@ function RelatedActions({ info, instance, authenticated }) { Block @{username}… )} - + {/* Report @{username}… @@ -796,10 +806,17 @@ function RelatedActions({ info, instance, authenticated }) { )} {!!relationship && ( - + + )}

diff --git a/src/components/drafts.jsx b/src/components/drafts.jsx index f47217d..be783f1 100644 --- a/src/components/drafts.jsx +++ b/src/components/drafts.jsx @@ -10,6 +10,7 @@ import { getCurrentAccountNS } from '../utils/store-utils'; import Icon from './icon'; import Loader from './loader'; +import MenuConfirm from './menu-confirm'; function Drafts({ onClose }) { const { masto } = api(); @@ -89,26 +90,33 @@ function Drafts({ onClose }) { {niceDateTime(updatedAtDate)} - + + -

+ // } + })(); + }} + > + + +

+ )} ) : (

No drafts found.

diff --git a/src/components/icon.jsx b/src/components/icon.jsx index 5c42a02..a130562 100644 --- a/src/components/icon.jsx +++ b/src/components/icon.jsx @@ -87,6 +87,7 @@ const ICONS = { layout4: () => import('@iconify-icons/mingcute/layout-4-line'), layout5: () => import('@iconify-icons/mingcute/layout-5-line'), announce: () => import('@iconify-icons/mingcute/announcement-line'), + alert: () => import('@iconify-icons/mingcute/alert-line'), }; function Icon({ diff --git a/src/components/list-add-edit.jsx b/src/components/list-add-edit.jsx index 606062b..4b19be0 100644 --- a/src/components/list-add-edit.jsx +++ b/src/components/list-add-edit.jsx @@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'preact/hooks'; import { api } from '../utils/api'; import Icon from './icon'; +import MenuConfirm from './menu-confirm'; function ListAddEdit({ list, onClose }) { const { masto } = api(); @@ -103,13 +104,14 @@ function ListAddEdit({ list, onClose }) { {editMode ? 'Save' : 'Create'} {editMode && ( - + + )} diff --git a/src/components/menu-confirm.jsx b/src/components/menu-confirm.jsx new file mode 100644 index 0000000..14e6774 --- /dev/null +++ b/src/components/menu-confirm.jsx @@ -0,0 +1,43 @@ +import { Menu, MenuItem, SubMenu } from '@szhsin/react-menu'; +import { cloneElement } from 'preact'; + +function MenuConfirm({ + subMenu = false, + confirm = true, + confirmLabel, + menuItemClassName, + menuFooter, + ...props +}) { + const { children, onClick, ...restProps } = props; + if (!confirm) { + if (subMenu) return ; + if (onClick) { + return cloneElement(children, { + onClick, + }); + } + return children; + } + const Parent = subMenu ? SubMenu : Menu; + return ( + + + {confirmLabel} + + {menuFooter} + + ); +} + +export default MenuConfirm; diff --git a/src/components/status.jsx b/src/components/status.jsx index f77bf69..e926fb3 100644 --- a/src/components/status.jsx +++ b/src/components/status.jsx @@ -28,6 +28,7 @@ import { snapshot } from 'valtio/vanilla'; import AccountBlock from '../components/account-block'; import EmojiText from '../components/emoji-text'; import Loader from '../components/loader'; +import MenuConfirm from '../components/menu-confirm'; import Modal from '../components/modal'; import NameText from '../components/name-text'; import Poll from '../components/poll'; @@ -325,6 +326,12 @@ function Status({ }; }; + // Check if media has no descriptions + const mediaNoDesc = useMemo(() => { + return mediaAttachments.some( + (attachment) => !attachment.description?.trim?.(), + ); + }, [mediaAttachments]); const boostStatus = async () => { if (!sameInstance || !authenticated) { alert(unauthInteractionErrorMessage); @@ -332,12 +339,8 @@ function Status({ } try { if (!reblogged) { - // Check if media has no descriptions - const hasNoDescriptions = mediaAttachments.some( - (attachment) => !attachment.description?.trim?.(), - ); let confirmText = 'Boost this post?'; - if (hasNoDescriptions) { + if (mediaNoDesc) { confirmText += '\n\n⚠️ Some media have no descriptions.'; } const yes = confirm(confirmText); @@ -367,6 +370,34 @@ function Status({ return false; } }; + const confirmBoostStatus = async () => { + if (!sameInstance || !authenticated) { + alert(unauthInteractionErrorMessage); + return false; + } + try { + // Optimistic + states.statuses[sKey] = { + ...status, + reblogged: !reblogged, + reblogsCount: reblogsCount + (reblogged ? -1 : 1), + }; + if (reblogged) { + const newStatus = await masto.v1.statuses.unreblog(id); + saveStatus(newStatus, instance); + return true; + } else { + const newStatus = await masto.v1.statuses.reblog(id); + saveStatus(newStatus, instance); + return true; + } + } catch (e) { + console.error(e); + // Revert optimistism + states.statuses[sKey] = status; + return false; + } + }; const favouriteStatus = async () => { if (!sameInstance || !authenticated) { @@ -490,11 +521,27 @@ function Status({ {!isSizeLarge && sameInstance && ( <> )} @@ -1157,7 +1212,7 @@ function Status({ onClick={replyStatus} /> -
+ {/*
-
+
*/} + ( +
+ +
+ )} + > + + + Boost to everyone? + + {mediaNoDesc && ( + + )} +
{ + if (!onClick) return; e.preventDefault(); e.stopPropagation(); onClick(e); diff --git a/src/pages/accounts.jsx b/src/pages/accounts.jsx index b576207..c7910b5 100644 --- a/src/pages/accounts.jsx +++ b/src/pages/accounts.jsx @@ -6,6 +6,7 @@ import { useReducer, useState } from 'preact/hooks'; import Avatar from '../components/avatar'; import Icon from '../components/icon'; import Link from '../components/link'; +import MenuConfirm from '../components/menu-confirm'; import NameText from '../components/name-text'; import { api } from '../utils/api'; import states from '../utils/states'; @@ -126,11 +127,19 @@ function Accounts({ onClose }) { Set as default )} - + + Log out @{account.info.acct}? + + } disabled={!isCurrent} + menuItemClassName="danger" onClick={() => { - const yes = confirm('Log out?'); - if (!yes) return; + // const yes = confirm('Log out?'); + // if (!yes) return; accounts.splice(i, 1); store.local.setJSON('accounts', accounts); // location.reload(); @@ -139,7 +148,7 @@ function Accounts({ onClose }) { > Log out… - +
diff --git a/src/pages/hashtag.jsx b/src/pages/hashtag.jsx index b81a674..d9a9156 100644 --- a/src/pages/hashtag.jsx +++ b/src/pages/hashtag.jsx @@ -10,6 +10,7 @@ import { useNavigate, useParams } from 'react-router-dom'; import Icon from '../components/icon'; import Menu2 from '../components/menu2'; +import MenuConfirm from '../components/menu-confirm'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; import showToast from '../utils/show-toast'; @@ -149,16 +150,19 @@ function Hashtags({ columnMode, ...props }) { > {!!info && hashtags.length === 1 && ( <> - { setFollowUIState('loading'); if (info.following) { - const yes = confirm(`Unfollow #${hashtag}?`); - if (!yes) { - setFollowUIState('default'); - return; - } + // const yes = confirm(`Unfollow #${hashtag}?`); + // if (!yes) { + // setFollowUIState('default'); + // return; + // } masto.v1.tags .unfollow(hashtag) .then(() => { @@ -198,7 +202,7 @@ function Hashtags({ columnMode, ...props }) { Follow )} - + )} diff --git a/src/pages/list.jsx b/src/pages/list.jsx index 3c36028..dcfa71e 100644 --- a/src/pages/list.jsx +++ b/src/pages/list.jsx @@ -11,6 +11,7 @@ import Icon from '../components/icon'; import Link from '../components/link'; import ListAddEdit from '../components/list-add-edit'; import Menu2 from '../components/menu2'; +import MenuConfirm from '../components/menu-confirm'; import Modal from '../components/modal'; import Timeline from '../components/timeline'; import { api } from '../utils/api'; @@ -263,10 +264,11 @@ function RemoveAddButton({ account, listID }) { const [removed, setRemoved] = useState(false); return ( - + + ); } diff --git a/src/utils/toast-alert.js b/src/utils/toast-alert.js new file mode 100644 index 0000000..d401477 --- /dev/null +++ b/src/utils/toast-alert.js @@ -0,0 +1,34 @@ +// Replace alert() with toastify-js +import Toastify from 'toastify-js'; + +const nativeAlert = window.alert; +if (!window.__nativeAlert) window.__nativeAlert = nativeAlert; + +window.alert = function (message) { + console.debug( + 'ALERT: This is a custom alert() function. Native alert() is still available as window.__nativeAlert()', + ); + // If Error object, show the message + if (message instanceof Error && message?.message) { + message = message.message; + } + // If not string, stringify it + if (typeof message !== 'string') { + message = JSON.stringify(message); + } + + const toast = Toastify({ + text: message, + className: 'alert', + gravity: 'top', + position: 'center', + duration: 10_000, + offset: { + y: 48, + }, + onClick: () => { + toast.hideToast(); + }, + }); + toast.showToast(); +};