Replace (most) alert/confirms with alternative UI

Everything might break lol
pull/182/head
Lim Chee Aun 2023-07-17 21:01:00 +08:00
rodzic 10fa537a56
commit ff41cd3563
11 zmienionych plików z 423 dodań i 124 usunięć

Wyświetl plik

@ -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 {

Wyświetl plik

@ -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 }) {
</div>
</SubMenu>
)}
<MenuItem
<MenuConfirm
subMenu
confirm={!blocking}
confirmLabel={
<>
<Icon icon="block" />
<span>Block @{username}?</span>
</>
}
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 }) {
<span>Block @{username}</span>
</>
)}
</MenuItem>
</MenuConfirm>
{/* <MenuItem>
<Icon icon="flag" />
<span>Report @{username}</span>
@ -796,10 +806,17 @@ function RelatedActions({ info, instance, authenticated }) {
<Loader abrupt />
)}
{!!relationship && (
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
<MenuConfirm
confirm={following || requested}
confirmLabel={
<span>
{requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`}
</span>
}
menuItemClassName="danger"
align="end"
disabled={loading}
onClick={() => {
setRelationshipUIState('loading');
@ -808,18 +825,17 @@ function RelatedActions({ info, instance, authenticated }) {
let newRelationship;
if (following || requested) {
const yes = confirm(
requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`,
);
// const yes = confirm(
// requested
// ? 'Withdraw follow request?'
// : `Unfollow @${info.acct || info.username}?`,
// );
if (yes) {
newRelationship =
await currentMasto.v1.accounts.unfollow(
accountID.current,
);
}
// if (yes) {
newRelationship = await currentMasto.v1.accounts.unfollow(
accountID.current,
);
// }
} else {
newRelationship = await currentMasto.v1.accounts.follow(
accountID.current,
@ -835,24 +851,31 @@ function RelatedActions({ info, instance, authenticated }) {
})();
}}
>
{following ? (
<>
<span>Following</span>
<span>Unfollow</span>
</>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : (
'Follow'
)}
</button>
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
disabled={loading}
>
{following ? (
<>
<span>Following</span>
<span>Unfollow</span>
</>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : (
'Follow'
)}
</button>
</MenuConfirm>
)}
</span>
</p>

Wyświetl plik

@ -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)}
</time>
</b>
<button
type="button"
class="small light"
<MenuConfirm
confirmLabel={<span>Delete this draft?</span>}
menuItemClassName="danger"
align="end"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
try {
const yes = confirm('Delete this draft?');
if (yes) {
await db.drafts.del(key);
reload();
}
// const yes = confirm('Delete this draft?');
// if (yes) {
await db.drafts.del(key);
reload();
// }
} catch (e) {
alert('Error deleting draft! Please try again.');
}
})();
}}
>
Delete&hellip;
</button>
<button
type="button"
class="small light"
disabled={uiState === 'loading'}
>
Delete&hellip;
</button>
</MenuConfirm>
</div>
<button
type="button"
@ -145,15 +153,16 @@ function Drafts({ onClose }) {
);
})}
</ul>
<p>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
const yes = confirm('Delete all drafts?');
if (yes) {
{drafts.length > 1 && (
<p>
<MenuConfirm
confirmLabel={<span>Delete all drafts?</span>}
menuItemClassName="danger"
disabled={uiState === 'loading'}
onClick={() => {
(async () => {
// const yes = confirm('Delete all drafts?');
// if (yes) {
setUIState('loading');
try {
await db.drafts.delMany(
@ -166,13 +175,20 @@ function Drafts({ onClose }) {
alert('Error deleting drafts! Please try again.');
setUIState('error');
}
}
})();
}}
>
Delete all drafts&hellip;
</button>
</p>
// }
})();
}}
>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
>
Delete all&hellip;
</button>
</MenuConfirm>
</p>
)}
</>
) : (
<p>No drafts found.</p>

Wyświetl plik

@ -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({

Wyświetl plik

@ -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'}
</button>
{editMode && (
<button
type="button"
class="light danger"
<MenuConfirm
disabled={uiState === 'loading'}
align="end"
menuItemClassName="danger"
confirmLabel="Delete this list?"
onClick={() => {
const yes = confirm('Delete this list?');
if (!yes) return;
// const yes = confirm('Delete this list?');
// if (!yes) return;
setUiState('loading');
(async () => {
@ -127,8 +129,14 @@ function ListAddEdit({ list, onClose }) {
})();
}}
>
Delete
</button>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
>
Delete
</button>
</MenuConfirm>
)}
</div>
</form>

Wyświetl plik

@ -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 <MenuItem {...props} />;
if (onClick) {
return cloneElement(children, {
onClick,
});
}
return children;
}
const Parent = subMenu ? SubMenu : Menu;
return (
<Parent
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
gap={-8}
shift={8}
menuClassName="menu-emphasized"
{...restProps}
menuButton={subMenu ? undefined : children}
label={subMenu ? children : undefined}
>
<MenuItem className={menuItemClassName} onClick={onClick}>
{confirmLabel}
</MenuItem>
{menuFooter}
</Parent>
);
}
export default MenuConfirm;

Wyświetl plik

@ -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 && (
<>
<div class="menu-horizontal">
<MenuItem
<MenuConfirm
subMenu
confirmLabel={
<>
<Icon icon="rocket" />
<span>Unboost?</span>
</>
}
menuFooter={
mediaNoDesc &&
!reblogged && (
<div class="footer">
<Icon icon="alert" />
Some media have no descriptions.
</div>
)
}
disabled={!canBoost}
onClick={async () => {
try {
const done = await boostStatus();
const done = await confirmBoostStatus();
if (!isSizeLarge && done) {
showToast(reblogged ? 'Unboosted' : 'Boosted');
}
@ -508,7 +555,7 @@ function Status({
}}
/>
<span>{reblogged ? 'Unboost' : 'Boost…'}</span>
</MenuItem>
</MenuConfirm>
<MenuItem
onClick={() => {
try {
@ -660,27 +707,35 @@ function Status({
<span>Edit</span>
</MenuItem>
{isSizeLarge && (
<MenuItem
<MenuConfirm
subMenu
confirmLabel={
<>
<Icon icon="trash" />
<span>Delete this post?</span>
</>
}
menuItemClassName="danger"
onClick={() => {
const yes = confirm('Delete this post?');
if (yes) {
(async () => {
try {
await masto.v1.statuses.remove(id);
const cachedStatus = getStatus(id, instance);
cachedStatus._deleted = true;
showToast('Deleted');
} catch (e) {
console.error(e);
showToast('Unable to delete');
}
})();
}
// const yes = confirm('Delete this post?');
// if (yes) {
(async () => {
try {
await masto.v1.statuses.remove(id);
const cachedStatus = getStatus(id, instance);
cachedStatus._deleted = true;
showToast('Deleted');
} catch (e) {
console.error(e);
showToast('Unable to delete');
}
})();
// }
}}
>
<Icon icon="trash" />
<span>Delete</span>
</MenuItem>
</MenuConfirm>
)}
</div>
)}
@ -1157,7 +1212,7 @@ function Status({
onClick={replyStatus}
/>
</div>
<div class="action has-count">
{/* <div class="action has-count">
<StatusButton
checked={reblogged}
title={['Boost', 'Unboost']}
@ -1168,7 +1223,45 @@ function Status({
onClick={boostStatus}
disabled={!canBoost}
/>
</div>
</div> */}
<Menu
portal={{
target:
document.querySelector('.status-deck') || document.body,
}}
align="start"
gap={4}
overflow="auto"
viewScroll="close"
boundingBoxPadding="8 8 8 8"
shift={-8}
menuClassName="menu-emphasized"
menuButton={({ open }) => (
<div class="action has-count">
<StatusButton
checked={reblogged}
title={['Boost', 'Unboost']}
alt={['Boost', 'Boosted']}
class="reblog-button"
icon="rocket"
count={reblogsCount}
// onClick={boostStatus}
disabled={open || !canBoost}
/>
</div>
)}
>
<MenuItem onClick={confirmBoostStatus}>
<Icon icon="rocket" />
<span>Boost to everyone?</span>
</MenuItem>
{mediaNoDesc && (
<div class="footer">
<Icon icon="alert" />
Some media have no descriptions.
</div>
)}
</Menu>
<div class="action has-count">
<StatusButton
checked={favourited}
@ -1682,6 +1775,7 @@ function StatusButton({
title={buttonTitle}
class={`plain ${className} ${checked ? 'checked' : ''}`}
onClick={(e) => {
if (!onClick) return;
e.preventDefault();
e.stopPropagation();
onClick(e);

Wyświetl plik

@ -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 }) {
<span>Set as default</span>
</MenuItem>
)}
<MenuItem
<MenuConfirm
subMenu
confirmLabel={
<>
<Icon icon="exit" />
<span>Log out @{account.info.acct}?</span>
</>
}
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 }) {
>
<Icon icon="exit" />
<span>Log out</span>
</MenuItem>
</MenuConfirm>
</Menu>
</div>
</li>

Wyświetl plik

@ -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 && (
<>
<MenuItem
<MenuConfirm
subMenu
confirm={info.following}
confirmLabel={`Unfollow #${hashtag}?`}
disabled={followUIState === 'loading' || !authenticated}
onClick={() => {
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 }) {
<Icon icon="plus" /> <span>Follow</span>
</>
)}
</MenuItem>
</MenuConfirm>
<MenuDivider />
</>
)}

Wyświetl plik

@ -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 (
<button
type="button"
class={`light ${removed ? '' : 'danger'}`}
disabled={uiState === 'loading'}
<MenuConfirm
confirm={!removed}
confirmLabel={<span>Remove @{account.username} from list?</span>}
align="end"
menuItemClassName="danger"
onClick={() => {
if (removed) {
setUIState('loading');
@ -282,8 +284,8 @@ function RemoveAddButton({ account, listID }) {
}
})();
} else {
const yes = confirm(`Remove ${account.username} from this list?`);
if (!yes) return;
// const yes = confirm(`Remove ${account.username} from this list?`);
// if (!yes) return;
setUIState('loading');
(async () => {
@ -300,8 +302,14 @@ function RemoveAddButton({ account, listID }) {
}
}}
>
{removed ? 'Add' : 'Remove…'}
</button>
<button
type="button"
class={`light ${removed ? '' : 'danger'}`}
disabled={uiState === 'loading'}
>
{removed ? 'Add' : 'Remove…'}
</button>
</MenuConfirm>
);
}

Wyświetl plik

@ -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();
};