diff --git a/src/components/ICONS.jsx b/src/components/ICONS.jsx index 04afb189..bb654b7d 100644 --- a/src/components/ICONS.jsx +++ b/src/components/ICONS.jsx @@ -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'), diff --git a/src/components/account-info.jsx b/src/components/account-info.jsx index 15d303d1..0f5fd5b0 100644 --- a/src/components/account-info.jsx +++ b/src/components/account-info.jsx @@ -1238,10 +1238,17 @@ function RelatedActions({ )} - {/* - - Report @{username}… - */} + { + states.showReportModal = { + account: currentInfo || info, + }; + }} + > + + Report @{username}… + )} diff --git a/src/components/modal.jsx b/src/components/modal.jsx index 4149046e..427b7123 100644 --- a/src/components/modal.jsx +++ b/src/components/modal.jsx @@ -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); } }} > diff --git a/src/components/modals.jsx b/src/components/modals.jsx index 75de5abb..8b3ebf7d 100644 --- a/src/components/modals.jsx +++ b/src/components/modals.jsx @@ -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() { /> )} + {!!snapStates.showReportModal && ( + { + states.showReportModal = false; + }} + > + { + states.showReportModal = false; + }} + /> + + )} ); } diff --git a/src/components/report-modal.css b/src/components/report-modal.css new file mode 100644 index 00000000..8306de99 --- /dev/null +++ b/src/components/report-modal.css @@ -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; + } +} diff --git a/src/components/report-modal.jsx b/src/components/report-modal.jsx new file mode 100644 index 00000000..f79ef558 --- /dev/null +++ b/src/components/report-modal.jsx @@ -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 ( +
+
+

{post ? 'Report Post' : `Report @${username}`}

+ +
+
+
+ {post ? ( + + ) : ( + + )} +
+ {!!selectedCategory && + !CATEGORIES_INFO[selectedCategory].excludeStamp && ( + + )} +
{ + 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'), + ); + } + })(); + }} + > +

+ {post + ? `What's the issue with this post?` + : `What's the issue with this profile?`} +

+
+ {CATEGORIES.map((category) => + category === 'violation' && !rules?.length ? null : ( + + + {category === 'violation' && !!rules?.length && ( + + )} + + ), + )} +
+
+

+ +

+