New account context menu!

Add Mention, Mute and Block
This commit is contained in:
Lim Chee Aun 2023-03-18 16:24:04 +08:00
parent 51bc920ada
commit 24fdaf78d1
5 changed files with 348 additions and 68 deletions

View file

@ -1077,8 +1077,10 @@ body:has(.status-deck) .media-post-link {
max-width: 90vw;
overflow: hidden;
}
.szh-menu__item--focusable {
background-color: transparent;
.szh-menu[aria-label='Submenu'] {
background-color: var(--bg-blur-color);
backdrop-filter: blur(4px);
box-shadow: 0 3px 24px -3px var(--drop-shadow-color);
}
.szh-menu__header {
margin: -8px 0 8px;
@ -1098,7 +1100,7 @@ body:has(.status-deck) .media-post-link {
display: flex;
gap: 8px;
align-items: center;
line-height: 1;
line-height: 1.1;
padding: 8px 16px !important;
transition: all 0.1s ease-in-out;
text-decoration: none;
@ -1106,6 +1108,9 @@ body:has(.status-deck) .media-post-link {
overflow: hidden;
text-overflow: ellipsis;
}
.szh-menu .szh-menu__item--focusable {
background-color: transparent;
}
.szh-menu .szh-menu__item span {
white-space: nowrap;
overflow: hidden;
@ -1186,6 +1191,15 @@ body:has(.status-deck) .media-post-link {
opacity: 1;
}
.szh-menu .menu-wrap {
display: flex;
flex-wrap: wrap;
}
.szh-menu .menu-wrap > * {
flex-grow: 1;
flex-basis: 50%;
}
/* GLASS MENU */
.glass-menu {

View file

@ -158,6 +158,9 @@
.account-container .actions button {
align-self: flex-end;
}
.account-container .actions .buttons {
display: flex;
}
.account-container .profile-metadata {
display: flex;

View file

@ -1,5 +1,12 @@
import './account-info.css';
import {
Menu,
MenuDivider,
MenuHeader,
MenuItem,
SubMenu,
} from '@szhsin/react-menu';
import { useEffect, useRef, useState } from 'preact/hooks';
import RelativeTime from '../components/relative-time';
@ -9,6 +16,7 @@ import enhanceContent from '../utils/enhance-content';
import handleContentLinks from '../utils/handle-content-links';
import niceDateTime from '../utils/nice-date-time';
import shortenNumber from '../utils/shorten-number';
import showToast from '../utils/show-toast';
import states, { hideAllModals } from '../utils/states';
import store from '../utils/store';
@ -17,6 +25,27 @@ import Avatar from './avatar';
import Icon from './icon';
import Link from './link';
const MUTE_DURATIONS = [
1000 * 60 * 5, // 5 minutes
1000 * 60 * 30, // 30 minutes
1000 * 60 * 60, // 1 hour
1000 * 60 * 60 * 6, // 6 hours
1000 * 60 * 60 * 24, // 1 day
1000 * 60 * 60 * 24 * 3, // 3 days
1000 * 60 * 60 * 24 * 7, // 1 week
0, // forever
];
const MUTE_DURATIONS_LABELS = {
0: 'Forever',
300_000: '5 minutes',
1_800_000: '30 minutes',
3_600_000: '1 hour',
21_600_000: '6 hours',
86_400_000: '1 day',
259_200_000: '3 days',
604_800_000: '1 week',
};
function AccountInfo({
account,
fetchAccount = () => {},
@ -360,7 +389,7 @@ function RelatedActions({ info, instance, authenticated }) {
const [relationship, setRelationship] = useState(null);
const [familiarFollowers, setFamiliarFollowers] = useState([]);
const { id, locked, lastStatusAt } = info;
const { id, acct, url, username, locked, lastStatusAt } = info;
const accountID = useRef(id);
const {
@ -445,6 +474,8 @@ function RelatedActions({ info, instance, authenticated }) {
}
}, [info, authenticated]);
const loading = relationshipUIState === 'loading';
return (
<>
{familiarFollowers?.length > 0 && (
@ -481,65 +512,277 @@ function RelatedActions({ info, instance, authenticated }) {
Last status: <RelativeTime datetime={lastStatusAt} format="micro" />
</span>
)}{' '}
{relationshipUIState !== 'loading' && relationship && (
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
disabled={relationshipUIState === 'loading'}
onClick={() => {
setRelationshipUIState('loading');
<span class="buttons">
<Menu
portal={{
target: document.body,
}}
containerProps={{
style: {
// Higher than the backdrop
zIndex: 1001,
},
}}
align="center"
position="anchor"
overflow="auto"
boundingBoxPadding="8 8 8 8"
menuButton={
<button
type="button"
title="More"
class="plain"
disabled={loading}
>
<Icon icon="more" size="l" alt="More" />
</button>
}
>
{currentAuthenticated && (
<>
<MenuItem
onClick={() => {
states.showCompose = {
draftStatus: {
status: `@${acct} `,
},
};
}}
>
<Icon icon="at" />
<span>Mention @{username}</span>
</MenuItem>
<MenuDivider />
</>
)}
<MenuItem href={url} target="_blank">
<Icon icon="external" />
<small class="menu-double-lines">{niceAccountURL(url)}</small>
</MenuItem>
<div class="menu-horizontal">
<MenuItem
onClick={() => {
// Copy url to clipboard
try {
navigator.clipboard.writeText(url);
showToast('Link copied');
} catch (e) {
console.error(e);
showToast('Unable to copy link');
}
}}
>
<Icon icon="link" />
<span>Copy</span>
</MenuItem>
{navigator?.share &&
navigator?.canShare?.({
url,
}) && (
<MenuItem
onClick={() => {
try {
navigator.share({
url,
});
} catch (e) {
console.error(e);
alert("Sharing doesn't seem to work.");
}
}}
>
<Icon icon="share" />
<span>Share</span>
</MenuItem>
)}
</div>
{!!relationship && (
<>
<MenuDivider />
{muting ? (
<MenuItem
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship =
await currentMasto.v1.accounts.unmute(id);
console.log('unmuting', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Unmuted @${username}`);
} catch (e) {
console.error(e);
setRelationshipUIState('error');
}
})();
}}
>
<Icon icon="unmute" />
<span>Unmute @{username}</span>
</MenuItem>
) : (
<SubMenu
openTrigger="clickOnly"
direction="bottom"
overflow="auto"
offsetX={-16}
label={
<>
<Icon icon="mute" />
<span class="menu-grow">Mute @{username}</span>
<span>
<Icon icon="time" />
<Icon icon="chevron-right" />
</span>
</>
}
>
<div class="menu-wrap">
{MUTE_DURATIONS.map((duration) => (
<MenuItem
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
const newRelationship =
await currentMasto.v1.accounts.mute(id, {
duration,
});
console.log('muting', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(
`Muted @${username} for ${MUTE_DURATIONS_LABELS[duration]}`,
);
} catch (e) {
console.error(e);
setRelationshipUIState('error');
showToast(`Unable to mute @${username}`);
}
})();
}}
>
{MUTE_DURATIONS_LABELS[duration]}
</MenuItem>
))}
</div>
</SubMenu>
)}
<MenuItem
onClick={() => {
if (!blocking && !confirm(`Block @${username}?`)) {
return;
}
setRelationshipUIState('loading');
(async () => {
try {
if (blocking) {
const newRelationship =
await currentMasto.v1.accounts.unblock(id);
console.log('unblocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Unblocked @${username}`);
} else {
const newRelationship =
await currentMasto.v1.accounts.block(id);
console.log('blocking', newRelationship);
setRelationship(newRelationship);
setRelationshipUIState('default');
showToast(`Blocked @${username}`);
}
} catch (e) {
console.error(e);
setRelationshipUIState('error');
if (blocking) {
showToast(`Unable to unblock @${username}`);
} else {
showToast(`Unable to block @${username}`);
}
}
})();
}}
>
{blocking ? (
<>
<Icon icon="unblock" />
<span>Unblock @{username}</span>
</>
) : (
<>
<Icon icon="block" />
<span>Block @{username}</span>
</>
)}
</MenuItem>
{/* <MenuItem>
<Icon icon="flag" />
<span>Report @{username}</span>
</MenuItem> */}
</>
)}
</Menu>
{!!relationship && (
<button
type="button"
class={`${following || requested ? 'light swap' : ''}`}
data-swap-state={following || requested ? 'danger' : ''}
disabled={loading}
onClick={() => {
setRelationshipUIState('loading');
(async () => {
try {
let newRelationship;
(async () => {
try {
let newRelationship;
if (following || requested) {
const yes = confirm(
requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`,
);
if (following || requested) {
const yes = confirm(
requested
? 'Withdraw follow request?'
: `Unfollow @${info.acct || info.username}?`,
);
if (yes) {
newRelationship = await currentMasto.v1.accounts.unfollow(
if (yes) {
newRelationship =
await currentMasto.v1.accounts.unfollow(
accountID.current,
);
}
} else {
newRelationship = await currentMasto.v1.accounts.follow(
accountID.current,
);
}
} else {
newRelationship = await currentMasto.v1.accounts.follow(
accountID.current,
);
}
if (newRelationship) setRelationship(newRelationship);
setRelationshipUIState('default');
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
{following ? (
<>
<span>Following</span>
<span>Unfollow</span>
</>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : (
'Follow'
)}
</button>
)}
if (newRelationship) setRelationship(newRelationship);
setRelationshipUIState('default');
} catch (e) {
alert(e);
setRelationshipUIState('error');
}
})();
}}
>
{following ? (
<>
<span>Following</span>
<span>Unfollow</span>
</>
) : requested ? (
<>
<span>Requested</span>
<span>Withdraw</span>
</>
) : locked ? (
<>
<Icon icon="lock" /> <span>Follow</span>
</>
) : (
'Follow'
)}
</button>
)}
</span>
</p>
</>
);
@ -561,4 +804,18 @@ function lightenRGB([r, g, b]) {
return [r, g, b, alpha];
}
function niceAccountURL(url) {
if (!url) return;
const urlObj = new URL(url);
const { host, pathname } = urlObj;
const path = pathname.replace(/\/$/, '').replace(/^\//, '');
return (
<>
<span class="more-insignificant">{host}/</span>
<wbr />
<span>{path}</span>
</>
);
}
export default AccountInfo;

View file

@ -244,12 +244,12 @@ function Compose({
textareaRef.current.value = status;
oninputTextarea();
focusTextarea();
spoilerTextRef.current.value = spoilerText;
setVisibility(visibility);
if (spoilerText) spoilerTextRef.current.value = spoilerText;
if (visibility) setVisibility(visibility);
setLanguage(language || prefs.postingDefaultLanguage || DEFAULT_LANG);
setSensitive(sensitive);
setPoll(composablePoll);
setMediaAttachments(mediaAttachments);
if (sensitive !== null) setSensitive(sensitive);
if (composablePoll) setPoll(composablePoll);
if (mediaAttachments) setMediaAttachments(mediaAttachments);
}
}, [draftStatus, editStatus, replyToStatus]);
@ -442,10 +442,6 @@ function Compose({
useEffect(() => {
const handleItems = (e) => {
if (mediaAttachments.length >= maxMediaAttachments) {
alert(`You can only attach up to ${maxMediaAttachments} files.`);
return;
}
const { items } = e.clipboardData || e.dataTransfer;
const files = [];
for (let i = 0; i < items.length; i++) {
@ -457,6 +453,10 @@ function Compose({
}
}
}
if (files.length > 0 && mediaAttachments.length >= maxMediaAttachments) {
alert(`You can only attach up to ${maxMediaAttachments} files.`);
return;
}
console.log({ files });
if (files.length > 0) {
e.preventDefault();
@ -895,7 +895,7 @@ function Compose({
? 'Edit your status'
: 'What are you doing?'
}
required={mediaAttachments.length === 0}
required={mediaAttachments?.length === 0}
disabled={uiState === 'loading'}
lang={language}
onInput={() => {
@ -906,7 +906,7 @@ function Compose({
return masto.v2.search(params);
}}
/>
{mediaAttachments.length > 0 && (
{mediaAttachments?.length > 0 && (
<div class="media-attachments">
{mediaAttachments.map((attachment, i) => {
const { id, file } = attachment;

View file

@ -66,6 +66,12 @@ const ICONS = {
translate: 'mingcute:translate-line',
play: 'mingcute:play-fill',
trash: 'mingcute:delete-2-line',
mute: 'mingcute:volume-mute-line',
unmute: 'mingcute:volume-line',
block: 'mingcute:forbid-circle-line',
unblock: ['mingcute:forbid-circle-line', '180deg'],
flag: 'mingcute:flag-4-line',
time: 'mingcute:time-line',
};
const modules = import.meta.glob('/node_modules/@iconify-icons/mingcute/*.js');