1
0
Fork 0

Report post/profile

This commit is contained in:
Lim Chee Aun 2024-02-26 13:59:26 +08:00
parent c595b0ee31
commit a3236ea0f0
7 changed files with 535 additions and 7 deletions

View file

@ -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'),

View file

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

View file

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

View file

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

View 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;
}
}

View 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} &nbsp;
<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;

View file

@ -52,6 +52,7 @@ const states = proxy({
showGenericAccounts: false,
showMediaAlt: false,
showEmbedModal: false,
showReportModal: false,
// Shortcuts
shortcuts: [],
// Settings