Merge remote-tracking branch 'upstream/main'

This commit is contained in:
Natsu Kagami 2023-07-17 15:23:03 +02:00
commit a99fc68bda
Signed by: nki
GPG key ID: 55A032EB38B49ADB
11 changed files with 423 additions and 124 deletions

View file

@ -1401,7 +1401,7 @@ body > .szh-menu-container {
animation: appear-smooth 0.15s ease-in-out; animation: appear-smooth 0.15s ease-in-out;
width: 16em; width: 16em;
max-width: 90vw; max-width: 90vw;
overflow: hidden; /* overflow: hidden; */
} }
.szh-menu[aria-label='Submenu'] { .szh-menu[aria-label='Submenu'] {
background-color: var(--bg-blur-color); background-color: var(--bg-blur-color);
@ -1418,6 +1418,7 @@ body > .szh-menu-container {
text-shadow: 0 1px 0 var(--bg-color); text-shadow: 0 1px 0 var(--bg-color);
line-height: 1.2; line-height: 1.2;
/* border-bottom: 1px solid var(--outline-color); */ /* border-bottom: 1px solid var(--outline-color); */
border-radius: 8px 8px 0 0;
} }
.szh-menu__header.plain { .szh-menu__header.plain {
margin-bottom: 0; margin-bottom: 0;
@ -1426,6 +1427,28 @@ body > .szh-menu-container {
.szh-menu__header * { .szh-menu__header * {
vertical-align: middle; 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 { .szh-menu .szh-menu__item {
display: flex; display: flex;
gap: 8px; gap: 8px;
@ -1498,21 +1521,26 @@ body > .szh-menu-container {
font-size: inherit; font-size: inherit;
} }
.szh-menu .menu-horizontal { .szh-menu .menu-horizontal {
display: flex; display: grid;
/* two columns only */
grid-template-columns: repeat(2, 1fr);
} }
.szh-menu .menu-horizontal .szh-menu__item { .szh-menu .menu-horizontal > .szh-menu__item:not(:only-child):first-child,
flex: 1; .szh-menu .menu-horizontal > *:not(:only-child):first-child .szh-menu__item {
}
.szh-menu .menu-horizontal .szh-menu__item:not(:only-child):first-child {
padding-right: 4px !important; padding-right: 4px !important;
} }
.szh-menu .szh-menu
.menu-horizontal .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-left: 8px !important;
padding-right: 4px !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; padding-left: 8px !important;
} }
.szh-menu .szh-menu__item .menu-shortcut { .szh-menu .szh-menu__item .menu-shortcut {
@ -1533,6 +1561,19 @@ body > .szh-menu-container {
color: var(--red-color); color: var(--red-color);
opacity: 1; 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 { .szh-menu .menu-wrap {
display: flex; display: flex;
@ -1658,6 +1699,24 @@ meter.donut[hidden] {
margin-bottom: env(safe-area-inset-bottom); 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 */
.avatars-stack { .avatars-stack {

View file

@ -21,6 +21,7 @@ import Icon from './icon';
import Link from './link'; import Link from './link';
import ListAddEdit from './list-add-edit'; import ListAddEdit from './list-add-edit';
import Loader from './loader'; import Loader from './loader';
import MenuConfirm from './menu-confirm';
import Modal from './modal'; import Modal from './modal';
import TranslationBlock from './translation-block'; import TranslationBlock from './translation-block';
@ -734,11 +735,20 @@ function RelatedActions({ info, instance, authenticated }) {
</div> </div>
</SubMenu> </SubMenu>
)} )}
<MenuItem <MenuConfirm
onClick={() => { subMenu
if (!blocking && !confirm(`Block @${username}?`)) { confirm={!blocking}
return; confirmLabel={
<>
<Icon icon="block" />
<span>Block @{username}?</span>
</>
} }
menuItemClassName="danger"
onClick={() => {
// if (!blocking && !confirm(`Block @${username}?`)) {
// return;
// }
setRelationshipUIState('loading'); setRelationshipUIState('loading');
(async () => { (async () => {
try { try {
@ -784,7 +794,7 @@ function RelatedActions({ info, instance, authenticated }) {
<span>Block @{username}</span> <span>Block @{username}</span>
</> </>
)} )}
</MenuItem> </MenuConfirm>
{/* <MenuItem> {/* <MenuItem>
<Icon icon="flag" /> <Icon icon="flag" />
<span>Report @{username}</span> <span>Report @{username}</span>
@ -796,10 +806,17 @@ function RelatedActions({ info, instance, authenticated }) {
<Loader abrupt /> <Loader abrupt />
)} )}
{!!relationship && ( {!!relationship && (
<button <MenuConfirm
type="button" confirm={following || requested}
class={`${following || requested ? 'light swap' : ''}`} confirmLabel={
data-swap-state={following || requested ? 'danger' : ''} <span>
{requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`}
</span>
}
menuItemClassName="danger"
align="end"
disabled={loading} disabled={loading}
onClick={() => { onClick={() => {
setRelationshipUIState('loading'); setRelationshipUIState('loading');
@ -808,18 +825,17 @@ function RelatedActions({ info, instance, authenticated }) {
let newRelationship; let newRelationship;
if (following || requested) { if (following || requested) {
const yes = confirm( // const yes = confirm(
requested // requested
? 'Withdraw follow request?' // ? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`, // : `Unfollow @${info.acct || info.username}?`,
); // );
if (yes) { // if (yes) {
newRelationship = newRelationship = await currentMasto.v1.accounts.unfollow(
await currentMasto.v1.accounts.unfollow(
accountID.current, accountID.current,
); );
} // }
} else { } else {
newRelationship = await currentMasto.v1.accounts.follow( newRelationship = await currentMasto.v1.accounts.follow(
accountID.current, accountID.current,
@ -834,6 +850,12 @@ function RelatedActions({ info, instance, authenticated }) {
} }
})(); })();
}} }}
>
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
disabled={loading}
> >
{following ? ( {following ? (
<> <>
@ -853,6 +875,7 @@ function RelatedActions({ info, instance, authenticated }) {
'Follow' 'Follow'
)} )}
</button> </button>
</MenuConfirm>
)} )}
</span> </span>
</p> </p>

View file

@ -10,6 +10,7 @@ import { getCurrentAccountNS } from '../utils/store-utils';
import Icon from './icon'; import Icon from './icon';
import Loader from './loader'; import Loader from './loader';
import MenuConfirm from './menu-confirm';
function Drafts({ onClose }) { function Drafts({ onClose }) {
const { masto } = api(); const { masto } = api();
@ -89,26 +90,33 @@ function Drafts({ onClose }) {
{niceDateTime(updatedAtDate)} {niceDateTime(updatedAtDate)}
</time> </time>
</b> </b>
<button <MenuConfirm
type="button" confirmLabel={<span>Delete this draft?</span>}
class="small light" menuItemClassName="danger"
align="end"
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
onClick={() => { onClick={() => {
(async () => { (async () => {
try { try {
const yes = confirm('Delete this draft?'); // const yes = confirm('Delete this draft?');
if (yes) { // if (yes) {
await db.drafts.del(key); await db.drafts.del(key);
reload(); reload();
} // }
} catch (e) { } catch (e) {
alert('Error deleting draft! Please try again.'); alert('Error deleting draft! Please try again.');
} }
})(); })();
}} }}
>
<button
type="button"
class="small light"
disabled={uiState === 'loading'}
> >
Delete&hellip; Delete&hellip;
</button> </button>
</MenuConfirm>
</div> </div>
<button <button
type="button" type="button"
@ -145,15 +153,16 @@ function Drafts({ onClose }) {
); );
})} })}
</ul> </ul>
{drafts.length > 1 && (
<p> <p>
<button <MenuConfirm
type="button" confirmLabel={<span>Delete all drafts?</span>}
class="light danger" menuItemClassName="danger"
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
onClick={() => { onClick={() => {
(async () => { (async () => {
const yes = confirm('Delete all drafts?'); // const yes = confirm('Delete all drafts?');
if (yes) { // if (yes) {
setUIState('loading'); setUIState('loading');
try { try {
await db.drafts.delMany( await db.drafts.delMany(
@ -166,13 +175,20 @@ function Drafts({ onClose }) {
alert('Error deleting drafts! Please try again.'); alert('Error deleting drafts! Please try again.');
setUIState('error'); setUIState('error');
} }
} // }
})(); })();
}} }}
> >
Delete all drafts&hellip; <button
type="button"
class="light danger"
disabled={uiState === 'loading'}
>
Delete all&hellip;
</button> </button>
</MenuConfirm>
</p> </p>
)}
</> </>
) : ( ) : (
<p>No drafts found.</p> <p>No drafts found.</p>

View file

@ -88,6 +88,7 @@ const ICONS = {
layout4: () => import('@iconify-icons/mingcute/layout-4-line'), layout4: () => import('@iconify-icons/mingcute/layout-4-line'),
layout5: () => import('@iconify-icons/mingcute/layout-5-line'), layout5: () => import('@iconify-icons/mingcute/layout-5-line'),
announce: () => import('@iconify-icons/mingcute/announcement-line'), announce: () => import('@iconify-icons/mingcute/announcement-line'),
alert: () => import('@iconify-icons/mingcute/alert-line'),
}; };
function Icon({ function Icon({

View file

@ -3,6 +3,7 @@ import { useEffect, useRef, useState } from 'preact/hooks';
import { api } from '../utils/api'; import { api } from '../utils/api';
import Icon from './icon'; import Icon from './icon';
import MenuConfirm from './menu-confirm';
function ListAddEdit({ list, onClose }) { function ListAddEdit({ list, onClose }) {
const { masto } = api(); const { masto } = api();
@ -103,13 +104,14 @@ function ListAddEdit({ list, onClose }) {
{editMode ? 'Save' : 'Create'} {editMode ? 'Save' : 'Create'}
</button> </button>
{editMode && ( {editMode && (
<button <MenuConfirm
type="button"
class="light danger"
disabled={uiState === 'loading'} disabled={uiState === 'loading'}
align="end"
menuItemClassName="danger"
confirmLabel="Delete this list?"
onClick={() => { onClick={() => {
const yes = confirm('Delete this list?'); // const yes = confirm('Delete this list?');
if (!yes) return; // if (!yes) return;
setUiState('loading'); setUiState('loading');
(async () => { (async () => {
@ -126,9 +128,15 @@ function ListAddEdit({ list, onClose }) {
} }
})(); })();
}} }}
>
<button
type="button"
class="light danger"
disabled={uiState === 'loading'}
> >
Delete Delete
</button> </button>
</MenuConfirm>
)} )}
</div> </div>
</form> </form>

View file

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

View file

@ -28,6 +28,7 @@ import { snapshot } from 'valtio/vanilla';
import AccountBlock from '../components/account-block'; import AccountBlock from '../components/account-block';
import EmojiText from '../components/emoji-text'; import EmojiText from '../components/emoji-text';
import Loader from '../components/loader'; import Loader from '../components/loader';
import MenuConfirm from '../components/menu-confirm';
import Modal from '../components/modal'; import Modal from '../components/modal';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import Poll from '../components/poll'; 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 () => { const boostStatus = async () => {
if (!sameInstance || !authenticated) { if (!sameInstance || !authenticated) {
alert(unauthInteractionErrorMessage); alert(unauthInteractionErrorMessage);
@ -332,12 +339,8 @@ function Status({
} }
try { try {
if (!reblogged) { if (!reblogged) {
// Check if media has no descriptions
const hasNoDescriptions = mediaAttachments.some(
(attachment) => !attachment.description?.trim?.(),
);
let confirmText = 'Boost this post?'; let confirmText = 'Boost this post?';
if (hasNoDescriptions) { if (mediaNoDesc) {
confirmText += '\n\n⚠ Some media have no descriptions.'; confirmText += '\n\n⚠ Some media have no descriptions.';
} }
const yes = confirm(confirmText); const yes = confirm(confirmText);
@ -367,6 +370,34 @@ function Status({
return false; 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 () => { const favouriteStatus = async () => {
if (!sameInstance || !authenticated) { if (!sameInstance || !authenticated) {
@ -490,11 +521,27 @@ function Status({
{!isSizeLarge && sameInstance && ( {!isSizeLarge && sameInstance && (
<> <>
<div class="menu-horizontal"> <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} disabled={!canBoost}
onClick={async () => { onClick={async () => {
try { try {
const done = await boostStatus(); const done = await confirmBoostStatus();
if (!isSizeLarge && done) { if (!isSizeLarge && done) {
showToast(reblogged ? 'Unboosted' : 'Boosted'); showToast(reblogged ? 'Unboosted' : 'Boosted');
} }
@ -508,7 +555,7 @@ function Status({
}} }}
/> />
<span>{reblogged ? 'Unboost' : 'Boost…'}</span> <span>{reblogged ? 'Unboost' : 'Boost…'}</span>
</MenuItem> </MenuConfirm>
<MenuItem <MenuItem
onClick={() => { onClick={() => {
try { try {
@ -660,10 +707,18 @@ function Status({
<span>Edit</span> <span>Edit</span>
</MenuItem> </MenuItem>
{isSizeLarge && ( {isSizeLarge && (
<MenuItem <MenuConfirm
subMenu
confirmLabel={
<>
<Icon icon="trash" />
<span>Delete this post?</span>
</>
}
menuItemClassName="danger"
onClick={() => { onClick={() => {
const yes = confirm('Delete this post?'); // const yes = confirm('Delete this post?');
if (yes) { // if (yes) {
(async () => { (async () => {
try { try {
await masto.v1.statuses.remove(id); await masto.v1.statuses.remove(id);
@ -675,12 +730,12 @@ function Status({
showToast('Unable to delete'); showToast('Unable to delete');
} }
})(); })();
} // }
}} }}
> >
<Icon icon="trash" /> <Icon icon="trash" />
<span>Delete</span> <span>Delete</span>
</MenuItem> </MenuConfirm>
)} )}
</div> </div>
)} )}
@ -1157,7 +1212,7 @@ function Status({
onClick={replyStatus} onClick={replyStatus}
/> />
</div> </div>
<div class="action has-count"> {/* <div class="action has-count">
<StatusButton <StatusButton
checked={reblogged} checked={reblogged}
title={['Boost', 'Unboost']} title={['Boost', 'Unboost']}
@ -1168,7 +1223,45 @@ function Status({
onClick={boostStatus} onClick={boostStatus}
disabled={!canBoost} disabled={!canBoost}
/> />
</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> </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"> <div class="action has-count">
<StatusButton <StatusButton
checked={favourited} checked={favourited}
@ -1682,6 +1775,7 @@ function StatusButton({
title={buttonTitle} title={buttonTitle}
class={`plain ${className} ${checked ? 'checked' : ''}`} class={`plain ${className} ${checked ? 'checked' : ''}`}
onClick={(e) => { onClick={(e) => {
if (!onClick) return;
e.preventDefault(); e.preventDefault();
e.stopPropagation(); e.stopPropagation();
onClick(e); onClick(e);

View file

@ -6,6 +6,7 @@ import { useReducer, useState } from 'preact/hooks';
import Avatar from '../components/avatar'; import Avatar from '../components/avatar';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import MenuConfirm from '../components/menu-confirm';
import NameText from '../components/name-text'; import NameText from '../components/name-text';
import { api } from '../utils/api'; import { api } from '../utils/api';
import states from '../utils/states'; import states from '../utils/states';
@ -126,11 +127,19 @@ function Accounts({ onClose }) {
<span>Set as default</span> <span>Set as default</span>
</MenuItem> </MenuItem>
)} )}
<MenuItem <MenuConfirm
subMenu
confirmLabel={
<>
<Icon icon="exit" />
<span>Log out @{account.info.acct}?</span>
</>
}
disabled={!isCurrent} disabled={!isCurrent}
menuItemClassName="danger"
onClick={() => { onClick={() => {
const yes = confirm('Log out?'); // const yes = confirm('Log out?');
if (!yes) return; // if (!yes) return;
accounts.splice(i, 1); accounts.splice(i, 1);
store.local.setJSON('accounts', accounts); store.local.setJSON('accounts', accounts);
// location.reload(); // location.reload();
@ -139,7 +148,7 @@ function Accounts({ onClose }) {
> >
<Icon icon="exit" /> <Icon icon="exit" />
<span>Log out</span> <span>Log out</span>
</MenuItem> </MenuConfirm>
</Menu> </Menu>
</div> </div>
</li> </li>

View file

@ -10,6 +10,7 @@ import { useNavigate, useParams } from 'react-router-dom';
import Icon from '../components/icon'; import Icon from '../components/icon';
import Menu2 from '../components/menu2'; import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
import showToast from '../utils/show-toast'; import showToast from '../utils/show-toast';
@ -149,16 +150,19 @@ function Hashtags({ columnMode, ...props }) {
> >
{!!info && hashtags.length === 1 && ( {!!info && hashtags.length === 1 && (
<> <>
<MenuItem <MenuConfirm
subMenu
confirm={info.following}
confirmLabel={`Unfollow #${hashtag}?`}
disabled={followUIState === 'loading' || !authenticated} disabled={followUIState === 'loading' || !authenticated}
onClick={() => { onClick={() => {
setFollowUIState('loading'); setFollowUIState('loading');
if (info.following) { if (info.following) {
const yes = confirm(`Unfollow #${hashtag}?`); // const yes = confirm(`Unfollow #${hashtag}?`);
if (!yes) { // if (!yes) {
setFollowUIState('default'); // setFollowUIState('default');
return; // return;
} // }
masto.v1.tags masto.v1.tags
.unfollow(hashtag) .unfollow(hashtag)
.then(() => { .then(() => {
@ -198,7 +202,7 @@ function Hashtags({ columnMode, ...props }) {
<Icon icon="plus" /> <span>Follow</span> <Icon icon="plus" /> <span>Follow</span>
</> </>
)} )}
</MenuItem> </MenuConfirm>
<MenuDivider /> <MenuDivider />
</> </>
)} )}

View file

@ -11,6 +11,7 @@ import Icon from '../components/icon';
import Link from '../components/link'; import Link from '../components/link';
import ListAddEdit from '../components/list-add-edit'; import ListAddEdit from '../components/list-add-edit';
import Menu2 from '../components/menu2'; import Menu2 from '../components/menu2';
import MenuConfirm from '../components/menu-confirm';
import Modal from '../components/modal'; import Modal from '../components/modal';
import Timeline from '../components/timeline'; import Timeline from '../components/timeline';
import { api } from '../utils/api'; import { api } from '../utils/api';
@ -263,10 +264,11 @@ function RemoveAddButton({ account, listID }) {
const [removed, setRemoved] = useState(false); const [removed, setRemoved] = useState(false);
return ( return (
<button <MenuConfirm
type="button" confirm={!removed}
class={`light ${removed ? '' : 'danger'}`} confirmLabel={<span>Remove @{account.username} from list?</span>}
disabled={uiState === 'loading'} align="end"
menuItemClassName="danger"
onClick={() => { onClick={() => {
if (removed) { if (removed) {
setUIState('loading'); setUIState('loading');
@ -282,8 +284,8 @@ function RemoveAddButton({ account, listID }) {
} }
})(); })();
} else { } else {
const yes = confirm(`Remove ${account.username} from this list?`); // const yes = confirm(`Remove ${account.username} from this list?`);
if (!yes) return; // if (!yes) return;
setUIState('loading'); setUIState('loading');
(async () => { (async () => {
@ -299,9 +301,15 @@ function RemoveAddButton({ account, listID }) {
})(); })();
} }
}} }}
>
<button
type="button"
class={`light ${removed ? '' : 'danger'}`}
disabled={uiState === 'loading'}
> >
{removed ? 'Add' : 'Remove…'} {removed ? 'Add' : 'Remove…'}
</button> </button>
</MenuConfirm>
); );
} }

34
src/utils/toast-alert.js Normal file
View file

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