mirror of
https://github.com/cheeaun/phanpy.git
synced 2025-01-23 09:06:23 +01:00
Report post/profile
This commit is contained in:
parent
c595b0ee31
commit
a3236ea0f0
7 changed files with 535 additions and 7 deletions
|
@ -73,7 +73,7 @@ export const ICONS = {
|
|||
() => import('@iconify-icons/mingcute/forbid-circle-line'),
|
||||
'180deg',
|
||||
],
|
||||
flag: () => import('@iconify-icons/mingcute/flag-4-line'),
|
||||
flag: () => import('@iconify-icons/mingcute/flag-1-line'),
|
||||
time: () => import('@iconify-icons/mingcute/time-line'),
|
||||
refresh: () => import('@iconify-icons/mingcute/refresh-2-line'),
|
||||
emoji2: () => import('@iconify-icons/mingcute/emoji-2-line'),
|
||||
|
|
|
@ -1238,10 +1238,17 @@ function RelatedActions({
|
|||
</>
|
||||
)}
|
||||
</MenuConfirm>
|
||||
{/* <MenuItem>
|
||||
<Icon icon="flag" />
|
||||
<span>Report @{username}…</span>
|
||||
</MenuItem> */}
|
||||
<MenuItem
|
||||
className="danger"
|
||||
onClick={() => {
|
||||
states.showReportModal = {
|
||||
account: currentInfo || info,
|
||||
};
|
||||
}}
|
||||
>
|
||||
<Icon icon="flag" />
|
||||
<span>Report @{username}…</span>
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
</Menu2>
|
||||
|
|
|
@ -56,8 +56,18 @@ function Modal({ children, onClose, onClick, class: className }) {
|
|||
}}
|
||||
tabIndex="-1"
|
||||
onFocus={(e) => {
|
||||
if (e.target === e.currentTarget) {
|
||||
modalRef.current?.querySelector?.('[tabindex="-1"]')?.focus?.();
|
||||
try {
|
||||
if (e.target === e.currentTarget) {
|
||||
const focusElement =
|
||||
modalRef.current?.querySelector('[tabindex="-1"]');
|
||||
const isFocusable =
|
||||
getComputedStyle(focusElement)?.pointerEvents !== 'none';
|
||||
if (focusElement && isFocusable) {
|
||||
focusElement.focus();
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
|
|
@ -15,6 +15,7 @@ import GenericAccounts from './generic-accounts';
|
|||
import MediaAltModal from './media-alt-modal';
|
||||
import MediaModal from './media-modal';
|
||||
import Modal from './modal';
|
||||
import ReportModal from './report-modal';
|
||||
import ShortcutsSettings from './shortcuts-settings';
|
||||
|
||||
subscribe(states, (changes) => {
|
||||
|
@ -218,6 +219,21 @@ export default function Modals() {
|
|||
/>
|
||||
</Modal>
|
||||
)}
|
||||
{!!snapStates.showReportModal && (
|
||||
<Modal
|
||||
onClose={() => {
|
||||
states.showReportModal = false;
|
||||
}}
|
||||
>
|
||||
<ReportModal
|
||||
account={snapStates.showReportModal.account}
|
||||
post={snapStates.showReportModal.post}
|
||||
onClose={() => {
|
||||
states.showReportModal = false;
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
196
src/components/report-modal.css
Normal file
196
src/components/report-modal.css
Normal file
|
@ -0,0 +1,196 @@
|
|||
.report-modal-container {
|
||||
width: 100%;
|
||||
max-height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-width: 40em;
|
||||
background-color: var(--bg-color);
|
||||
box-shadow: 0 16px 32px -8px var(--drop-shadow-color);
|
||||
overflow-y: auto;
|
||||
animation: slide-up-smooth 0.3s ease-in-out;
|
||||
position: relative;
|
||||
|
||||
@media (min-width: 40em) {
|
||||
max-height: calc(100% - 32px);
|
||||
}
|
||||
|
||||
h1 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.top-controls {
|
||||
position: sticky;
|
||||
top: var(--sai-top, 0);
|
||||
z-index: 1;
|
||||
background-color: var(--bg-blur-color);
|
||||
backdrop-filter: blur(16px);
|
||||
padding: 16px;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
justify-content: space-between;
|
||||
pointer-events: auto;
|
||||
align-items: center;
|
||||
|
||||
h1 {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
}
|
||||
|
||||
main {
|
||||
padding: 0 16px 16px;
|
||||
/* display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px; */
|
||||
}
|
||||
|
||||
form {
|
||||
/* display: flex; */
|
||||
/* flex-direction: column; */
|
||||
/* gap: 16px; */
|
||||
text-wrap: pretty;
|
||||
|
||||
input {
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-preview {
|
||||
background-color: var(--bg-color);
|
||||
border-radius: 8px;
|
||||
border: 2px dashed var(--red-color);
|
||||
box-shadow: inset 0 0 16px -4px var(--red-bg-color);
|
||||
overflow: auto;
|
||||
max-height: 33vh;
|
||||
|
||||
.status {
|
||||
font-size: 90%;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-drag: none;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
|
||||
.account-block {
|
||||
margin: 16px;
|
||||
user-select: none;
|
||||
pointer-events: none;
|
||||
-webkit-touch-callout: none;
|
||||
-webkit-user-drag: none;
|
||||
filter: grayscale(0.5);
|
||||
}
|
||||
}
|
||||
|
||||
.rubber-stamp {
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
position: absolute;
|
||||
right: 32px;
|
||||
margin-top: -48px;
|
||||
animation: rubber-stamp 0.3s ease-in both;
|
||||
position: absolute;
|
||||
font-weight: bold;
|
||||
color: var(--red-color);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: -0.5px;
|
||||
font-size: 2em;
|
||||
line-height: 1;
|
||||
padding: 0.1em;
|
||||
border: 0.15em solid var(--red-color);
|
||||
border-radius: 0.3em;
|
||||
background-color: var(--bg-blur-color);
|
||||
text-align: center;
|
||||
/* Noise pattern - https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ */
|
||||
mask-image: repeating-conic-gradient(
|
||||
#000 0 0.01%,
|
||||
rgba(0, 0, 0, 0.45) 0 0.02%
|
||||
);
|
||||
|
||||
small {
|
||||
display: block;
|
||||
font-size: 11px;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
margin-block: 0.5em;
|
||||
}
|
||||
|
||||
section {
|
||||
label {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&:has(:checked) {
|
||||
.insignificant {
|
||||
color: var(--text-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
> label:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.report-categories {
|
||||
label {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.report-rules {
|
||||
margin-left: 1.75em;
|
||||
}
|
||||
}
|
||||
|
||||
.report-comment {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: flex-start;
|
||||
margin-top: 2em;
|
||||
flex-wrap: wrap;
|
||||
|
||||
p {
|
||||
margin: 0;
|
||||
padding: 8px 0 0;
|
||||
flex-shrink: 0;
|
||||
|
||||
label {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
textarea {
|
||||
flex-grow: 1;
|
||||
resize: vertical;
|
||||
}
|
||||
}
|
||||
|
||||
footer {
|
||||
margin-top: 2em;
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
align-items: center;
|
||||
|
||||
button {
|
||||
border-radius: 8px !important;
|
||||
align-self: stretch;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes rubber-stamp {
|
||||
0% {
|
||||
transform: rotate(-20deg) scale(5);
|
||||
opacity: 0;
|
||||
}
|
||||
100% {
|
||||
transform: rotate(-20deg) scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
298
src/components/report-modal.jsx
Normal file
298
src/components/report-modal.jsx
Normal file
|
@ -0,0 +1,298 @@
|
|||
import './report-modal.css';
|
||||
|
||||
import { Fragment } from 'preact';
|
||||
import { useMemo, useRef, useState } from 'preact/hooks';
|
||||
|
||||
import { api } from '../utils/api';
|
||||
import showToast from '../utils/show-toast';
|
||||
import { getCurrentInstance } from '../utils/store-utils';
|
||||
|
||||
import AccountBlock from './account-block';
|
||||
import Icon from './icon';
|
||||
import Loader from './loader';
|
||||
import Status from './status';
|
||||
|
||||
// NOTE: `dislike` hidden for now, it's actually not used for reporting
|
||||
// Mastodon shows another screen for unfollowing, muting or blocking instead or reporting
|
||||
|
||||
const CATEGORIES = [, /*'dislike'*/ 'spam', 'legal', 'violation', 'other'];
|
||||
// `violation` will be set if there are `rule_ids[]`
|
||||
|
||||
const CATEGORIES_INFO = {
|
||||
// dislike: {
|
||||
// label: 'Dislike',
|
||||
// description: 'Not something you want to see',
|
||||
// },
|
||||
spam: {
|
||||
label: 'Spam',
|
||||
description: 'Malicious links, fake engagement, or repetitive replies',
|
||||
},
|
||||
legal: {
|
||||
label: 'Illegal',
|
||||
description: "Violates the law of your or the server's country",
|
||||
},
|
||||
violation: {
|
||||
label: 'Server rule violation',
|
||||
description: 'Breaks specific server rules',
|
||||
stampLabel: 'Violation',
|
||||
},
|
||||
other: {
|
||||
label: 'Other',
|
||||
description: "Issue doesn't fit other categories",
|
||||
excludeStamp: true,
|
||||
},
|
||||
};
|
||||
|
||||
function ReportModal({ account, post, onClose }) {
|
||||
const { masto } = api();
|
||||
const [uiState, setUIState] = useState('default');
|
||||
const [username, domain] = account.acct.split('@');
|
||||
|
||||
const [rules, currentDomain] = useMemo(() => {
|
||||
const { rules, domain } = getCurrentInstance();
|
||||
return [rules || [], domain];
|
||||
});
|
||||
|
||||
const [selectedCategory, setSelectedCategory] = useState(null);
|
||||
const [showRules, setShowRules] = useState(false);
|
||||
|
||||
const rulesRef = useRef(null);
|
||||
const [hasRules, setHasRules] = useState(false);
|
||||
|
||||
return (
|
||||
<div class="report-modal-container">
|
||||
<div class="top-controls">
|
||||
<h1>{post ? 'Report Post' : `Report @${username}`}</h1>
|
||||
<button
|
||||
type="button"
|
||||
class="plain4 small"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={() => onClose()}
|
||||
>
|
||||
<Icon icon="x" size="xl" />
|
||||
</button>
|
||||
</div>
|
||||
<main>
|
||||
<div class="report-preview">
|
||||
{post ? (
|
||||
<Status status={post} size="s" previewMode />
|
||||
) : (
|
||||
<AccountBlock
|
||||
account={account}
|
||||
avatarSize="xxl"
|
||||
useAvatarStatic
|
||||
showStats
|
||||
showActivity
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!!selectedCategory &&
|
||||
!CATEGORIES_INFO[selectedCategory].excludeStamp && (
|
||||
<span
|
||||
class="rubber-stamp"
|
||||
key={selectedCategory}
|
||||
aria-hidden="true"
|
||||
>
|
||||
{CATEGORIES_INFO[selectedCategory].stampLabel ||
|
||||
CATEGORIES_INFO[selectedCategory].label}
|
||||
<small>Pending review</small>
|
||||
</span>
|
||||
)}
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const formData = new FormData(e.target);
|
||||
const entries = Object.fromEntries(formData.entries());
|
||||
console.log('ENTRIES', entries);
|
||||
|
||||
let { category, comment, forward } = entries;
|
||||
if (!comment) comment = undefined;
|
||||
if (forward === 'on') forward = true;
|
||||
const ruleIds =
|
||||
category === 'violation'
|
||||
? Object.entries(entries)
|
||||
.filter(([key]) => key.startsWith('rule_ids'))
|
||||
.map(([key, value]) => value)
|
||||
: undefined;
|
||||
|
||||
const params = {
|
||||
category,
|
||||
comment,
|
||||
forward,
|
||||
ruleIds,
|
||||
};
|
||||
console.log('PARAMS', params);
|
||||
|
||||
setUIState('loading');
|
||||
(async () => {
|
||||
try {
|
||||
await masto.v1.reports.create({
|
||||
accountId: account.id,
|
||||
statusIds: post?.id ? [post.id] : undefined,
|
||||
category,
|
||||
comment,
|
||||
ruleIds,
|
||||
forward,
|
||||
});
|
||||
setUIState('success');
|
||||
showToast(post ? 'Post reported' : 'Profile reported');
|
||||
onClose();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setUIState('error');
|
||||
showToast(
|
||||
error?.message ||
|
||||
(post
|
||||
? 'Unable to report post'
|
||||
: 'Unable to report profile'),
|
||||
);
|
||||
}
|
||||
})();
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
{post
|
||||
? `What's the issue with this post?`
|
||||
: `What's the issue with this profile?`}
|
||||
</p>
|
||||
<section class="report-categories">
|
||||
{CATEGORIES.map((category) =>
|
||||
category === 'violation' && !rules?.length ? null : (
|
||||
<Fragment key={category}>
|
||||
<label class="report-category">
|
||||
<input
|
||||
type="radio"
|
||||
name="category"
|
||||
value={category}
|
||||
required
|
||||
disabled={uiState === 'loading'}
|
||||
onChange={(e) => {
|
||||
setSelectedCategory(e.target.value);
|
||||
setShowRules(e.target.value === 'violation');
|
||||
}}
|
||||
/>
|
||||
<span>
|
||||
{CATEGORIES_INFO[category].label}
|
||||
<small class="ib insignificant">
|
||||
{CATEGORIES_INFO[category].description}
|
||||
</small>
|
||||
</span>
|
||||
</label>
|
||||
{category === 'violation' && !!rules?.length && (
|
||||
<div
|
||||
class="shazam-container no-animation"
|
||||
hidden={!showRules}
|
||||
>
|
||||
<div class="shazam-container-inner">
|
||||
<div class="report-rules" ref={rulesRef}>
|
||||
{rules.map((rule, i) => (
|
||||
<label class="report-rule" key={rule.id}>
|
||||
<input
|
||||
type="checkbox"
|
||||
name={`rule_ids[${i}]`}
|
||||
value={rule.id}
|
||||
required={showRules && !hasRules}
|
||||
disabled={uiState === 'loading'}
|
||||
onChange={(e) => {
|
||||
const { checked } = e.target;
|
||||
if (checked) {
|
||||
setHasRules(true);
|
||||
} else {
|
||||
const checkedInputs =
|
||||
rulesRef.current.querySelectorAll(
|
||||
'input:checked',
|
||||
);
|
||||
if (!checkedInputs.length) {
|
||||
setHasRules(false);
|
||||
}
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<span>{rule.text}</span>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Fragment>
|
||||
),
|
||||
)}
|
||||
</section>
|
||||
<section class="report-comment">
|
||||
<p>
|
||||
<label for="report-comment">Additional info</label>
|
||||
</p>
|
||||
<textarea
|
||||
maxlength="1000"
|
||||
rows="1"
|
||||
name="comment"
|
||||
id="report-comment"
|
||||
disabled={uiState === 'loading'}
|
||||
/>
|
||||
</section>
|
||||
<section>
|
||||
{domain !== currentDomain && (
|
||||
<p>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
switch
|
||||
name="forward"
|
||||
disabled={uiState === 'loading'}
|
||||
/>{' '}
|
||||
<span>
|
||||
Forward to <i>{domain}</i>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
)}
|
||||
</section>
|
||||
<footer>
|
||||
<button type="submit" disabled={uiState === 'loading'}>
|
||||
Send Report
|
||||
</button>{' '}
|
||||
<button
|
||||
type="submit"
|
||||
class="plain2"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await masto.v1.accounts.$select(account.id).mute(); // Infinite duration
|
||||
showToast(`Muted ${username}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(`Unable to mute ${username}`);
|
||||
}
|
||||
// onSubmit will still run
|
||||
}}
|
||||
>
|
||||
Send Report <small class="ib">+ Mute profile</small>
|
||||
</button>{' '}
|
||||
<button
|
||||
type="submit"
|
||||
class="plain2"
|
||||
disabled={uiState === 'loading'}
|
||||
onClick={async () => {
|
||||
try {
|
||||
await masto.v1.accounts.$select(account.id).block();
|
||||
showToast(`Blocked ${username}`);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
showToast(`Unable to block ${username}`);
|
||||
}
|
||||
// onSubmit will still run
|
||||
}}
|
||||
>
|
||||
Send Report <small class="ib">+ Block profile</small>
|
||||
</button>
|
||||
<Loader hidden={uiState !== 'loading'} />
|
||||
</footer>
|
||||
</form>
|
||||
</main>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default ReportModal;
|
|
@ -52,6 +52,7 @@ const states = proxy({
|
|||
showGenericAccounts: false,
|
||||
showMediaAlt: false,
|
||||
showEmbedModal: false,
|
||||
showReportModal: false,
|
||||
// Shortcuts
|
||||
shortcuts: [],
|
||||
// Settings
|
||||
|
|
Loading…
Reference in a new issue