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({
>
)}
- {/* */}
+
>
)}
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 && (
+
+ {CATEGORIES_INFO[selectedCategory].stampLabel ||
+ CATEGORIES_INFO[selectedCategory].label}
+ Pending review
+
+ )}
+
+
+
+ );
+}
+
+export default ReportModal;
diff --git a/src/utils/states.js b/src/utils/states.js
index 21f59e20..315be937 100644
--- a/src/utils/states.js
+++ b/src/utils/states.js
@@ -52,6 +52,7 @@ const states = proxy({
showGenericAccounts: false,
showMediaAlt: false,
showEmbedModal: false,
+ showReportModal: false,
// Shortcuts
shortcuts: [],
// Settings