mirror of
https://github.com/cheeaun/phanpy.git
synced 2024-11-22 17:25:40 +03:00
New account context menu!
Add Mention, Mute and Block
This commit is contained in:
parent
51bc920ada
commit
24fdaf78d1
5 changed files with 348 additions and 68 deletions
20
src/app.css
20
src/app.css
|
@ -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 {
|
||||
|
|
|
@ -158,6 +158,9 @@
|
|||
.account-container .actions button {
|
||||
align-self: flex-end;
|
||||
}
|
||||
.account-container .actions .buttons {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.account-container .profile-metadata {
|
||||
display: flex;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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');
|
||||
|
|
Loading…
Reference in a new issue