feat: MSC4260 Report User, Bug #6 mutual exclusion, TDS toast compliance
- Add ReportUserModal.tsx — category dropdown + reason input, calls
POST /_matrix/client/v3/users/{userId}/report via mx.http.authedRequest,
inline success/error feedback, auto-closes 1500ms after success
- Wire Report User button into UserRoomProfile.tsx between UserModeration
and UserDeviceSessions (hidden for own profile)
- Bug #6: enforce mutual exclusion between chat backgrounds and seasonal
themes — ChatBgGrid clears seasonal→'off' on non-'none' pick;
SeasonalBgGrid clears chatBackground→'none' on real theme pick;
SeasonalEffect guards against legacy persisted state at render time
- TDS: strip all hardcoded hex/rgba fallbacks from LotusToastContainer.tsx
(var(--lt-bg-card), --lt-accent-orange, --lt-text-primary/secondary,
--lt-accent-orange-dim/border, --lt-box-glow-orange)
- Mark Bug #6 FIXED, MSC4260 DONE, toast TDS FIXED in LOTUS_BUGS.md and
LOTUS_TODO.md; note EventReaders + CallControls already compliant
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+5
-5
@@ -60,10 +60,10 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
|||||||
### 6. Exclusive Background vs. Seasonal Choice
|
### 6. Exclusive Background vs. Seasonal Choice
|
||||||
|
|
||||||
- **File:** `cinny/src/app/state/settings.ts`
|
- **File:** `cinny/src/app/state/settings.ts`
|
||||||
- **Status:** **OPEN**
|
- **Status:** **FIXED**
|
||||||
- **Issue:** Concurrent application of both Chat Backgrounds and Seasonal Themes causes visual clutter and high GPU usage.
|
- **Issue:** Concurrent application of both Chat Backgrounds and Seasonal Themes causes visual clutter and high GPU usage.
|
||||||
- **Root Cause:** These are currently handled as independent settings in the `settingsAtom` and applied simultaneously without mutual exclusion.
|
- **Root Cause:** These are currently handled as independent settings in the `settingsAtom` and applied simultaneously without mutual exclusion.
|
||||||
- **Proposed Fix:** Introduce mutual exclusion in the settings application logic. Update the settings UI to present these as a single choice (e.g., a radio group or toggled selection) where activating one deactivates the other. Enforce this rule in `cinny/src/app/features/lotus/chatBackground.ts` and `cinny/src/app/components/seasonal/SeasonalEffect.tsx`.
|
- **Fix Applied:** Mutual exclusion enforced at two layers: (1) `General.tsx` — ChatBgGrid clears seasonalThemeOverride→'off' when any non-'none' background is picked; SeasonalBgGrid clears chatBackground→'none' when any real seasonal theme is selected. (2) `SeasonalEffect.tsx` — runtime guard returns null if `chatBackground !== 'none'`, protecting against legacy persisted state.
|
||||||
|
|
||||||
### 7. Tiny Touch Targets in Composer Toolbar
|
### 7. Tiny Touch Targets in Composer Toolbar
|
||||||
|
|
||||||
@@ -149,11 +149,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
|||||||
| Issue Description | File Path |
|
| Issue Description | File Path |
|
||||||
| :-------------------------------------------------------------------- | :-------------------------------------------------------- |
|
| :-------------------------------------------------------------------- | :-------------------------------------------------------- |
|
||||||
| Hardcoded inline style `cursor: 'pointer'` | `cinny/src/app/plugins/react-custom-html-parser.tsx` |
|
| Hardcoded inline style `cursor: 'pointer'` | `cinny/src/app/plugins/react-custom-html-parser.tsx` |
|
||||||
| Hardcoded color `#00D4FF`, `#FFB300` | `cinny/src/app/components/event-readers/EventReaders.tsx` |
|
| Hardcoded color `#00D4FF`, `#FFB300` ✅ **VERIFIED COMPLIANT** | `cinny/src/app/components/event-readers/EventReaders.tsx` |
|
||||||
| Hardcoded color `#EE1D52`, `#9146ff`, `#ff4500`, `#cb3837`, `#f48024` | `cinny/src/app/components/url-preview/UrlPreviewCard.tsx` |
|
| Hardcoded color `#EE1D52`, `#9146ff`, `#ff4500`, `#cb3837`, `#f48024` | `cinny/src/app/components/url-preview/UrlPreviewCard.tsx` |
|
||||||
| Massive number of hardcoded `backgroundColor` values | `cinny/src/app/features/lotus/chatBackground.ts` |
|
| Massive number of hardcoded `backgroundColor` values | `cinny/src/app/features/lotus/chatBackground.ts` |
|
||||||
| Hardcoded colors `#00FF88`, `#FF6B00` | `cinny/src/app/features/call/CallControls.tsx` |
|
| Hardcoded colors `#00FF88`, `#FF6B00` ✅ **VERIFIED COMPLIANT** | `cinny/src/app/features/call/CallControls.tsx` |
|
||||||
| Hardcoded fallback hexes in toast colors | `cinny/src/app/features/toast/LotusToastContainer.tsx` |
|
| Hardcoded fallback hexes in toast colors ✅ **FIXED** | `cinny/src/app/features/toast/LotusToastContainer.tsx` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
+15
-15
@@ -7,10 +7,10 @@
|
|||||||
|
|
||||||
## 🏗️ Infrastructure & Maintenance
|
## 🏗️ Infrastructure & Maintenance
|
||||||
|
|
||||||
- [ ] **Upgrade Synapse to v1.155.0**
|
- [x] **Upgrade Synapse to v1.155.0** ✅ Done 2026-06-18
|
||||||
- **Context:** Synapse 1.155.0 is the last version supporting Debian 12 Bookworm.
|
- **Context:** 1.155.0 is the last version supporting Debian 12 Bookworm. LXC 151 is already on Debian 13 Trixie — OS migration was completed prior to this upgrade.
|
||||||
- **Reference:** https://github.com/element-hq/synapse/releases/tag/v1.155.0
|
- **What changed (1.154→1.155):** No breaking changes, no config changes, no DB migrations. Bugfixes: to-device EDU size limiting, restricted room joins, sliding sync subscription response timing. Rust port of more internal classes (perf only).
|
||||||
- **Plan:** Review release notes, backup database and media store on LXC 151, perform upgrade in a staging environment if possible, then production. Prepare for OS migration to Debian 13 afterward.
|
- **MSC4452** (Preview URL capabilities) shipped in 1.154 — opt-in via `msc4452_enabled`, not enabled.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -52,18 +52,18 @@ Status: `[ ]` pending · `[~]` in progress · `[x]` completed
|
|||||||
## Server Capabilities (as of June 2026)
|
## Server Capabilities (as of June 2026)
|
||||||
|
|
||||||
- **Homeserver:** `matrix.lotusguild.org`
|
- **Homeserver:** `matrix.lotusguild.org`
|
||||||
- **Synapse version:** `1.153.0` (2026-05-19) — fully up to date
|
- **Synapse version:** `1.155.0` (2026-06-18) — fully up to date; last version for Debian 12 (LXC 151 already on Debian 13 Trixie)
|
||||||
- **Matrix spec:** up to `v1.12` formally; newer MSC features via `unstable_features`
|
- **Matrix spec:** up to `v1.12` formally; newer MSC features via `unstable_features`
|
||||||
|
|
||||||
### Confirmed facts
|
### Confirmed facts
|
||||||
|
|
||||||
| Finding | Impact |
|
| Finding | Impact |
|
||||||
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
| ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
|
||||||
| **MSC flags ON:** `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` | All safe to use now |
|
| **MSC flags ON:** `msc4140` · `msc3771` · `msc3440.stable` · `msc4133.stable` · `simplified_msc3575` · `msc4222` · `msc3266` · `msc3401_matrix_rtc` | All safe to use now |
|
||||||
| **MSC flags OFF:** `msc4306` (thread subscriptions) · `msc3882` · `msc3912` · `msc4155` | These features are BLOCKED |
|
| **MSC flags OFF:** `msc4306` (thread subscriptions) · `msc3882` · `msc3912` · `msc4155` | These features are BLOCKED |
|
||||||
| **MSC3266** room summary: returns 404 | Room Preview feature BLOCKED |
|
| **MSC3266** room summary: flag `msc3266_enabled: true` set but `GET /v1/rooms/{id}/summary` still returns 404 (M_UNRECOGNIZED) | Room Preview BLOCKED — endpoint not implemented in Synapse 1.155 |
|
||||||
| **MSC3892** relation redaction: not in flags | Reaction Redaction feature BLOCKED |
|
| **MSC3892** relation redaction: not in flags | Reaction Redaction feature BLOCKED |
|
||||||
| **MSC4260** report user: server at v1.12, endpoint may not exist | Report User feature BLOCKED |
|
| **MSC4260** report user: `POST /_matrix/client/v3/users/{userId}/report` returns **200** ✅ | **Report User UNBLOCKED** — endpoint live since Synapse 1.133; ready to build |
|
||||||
| **MSC4151** report room: HTTP 405 on GET = endpoint exists (POST only) | Report Room live ✅ |
|
| **MSC4151** report room: HTTP 405 on GET = endpoint exists (POST only) | Report Room live ✅ |
|
||||||
| `folds AvatarImage` does NOT accept children | Add frame/overlay inside `UserAvatar.tsx` itself — optional `frameName` prop |
|
| `folds AvatarImage` does NOT accept children | Add frame/overlay inside `UserAvatar.tsx` itself — optional `frameName` prop |
|
||||||
| No in-app toast system exists (was) | Built `ToastProvider` + Jotai queue; at `App.tsx:65` |
|
| No in-app toast system exists (was) | Built `ToastProvider` + Jotai queue; at `App.tsx:65` |
|
||||||
@@ -467,9 +467,9 @@ Check back after each Synapse upgrade — re-run `/matrix/client/versions` and `
|
|||||||
|
|
||||||
### [BLOCKED] · Room Preview Before Joining (MSC3266)
|
### [BLOCKED] · Room Preview Before Joining (MSC3266)
|
||||||
|
|
||||||
**Blocked by:** `GET /v1/rooms/{id}/summary` returns 404 — endpoint not available on this server
|
**Blocked by:** `GET /_matrix/client/v1/rooms/{roomId}/summary` returns `M_UNRECOGNIZED` 404 — endpoint not implemented in Synapse 1.155. Config flag `msc3266_enabled: true` is set but has no effect; Synapse appears not to have shipped a stable implementation at the v1 path. Verified 2026-06-18.
|
||||||
**What it would do:** Show room name, topic, avatar, member count before joining.
|
**What it would do:** Show room name, topic, avatar, member count before joining.
|
||||||
**Action when unblocked:** Build pre-join preview card; trigger on unjoined room navigation.
|
**Action when unblocked:** Re-test after each future Synapse upgrade.
|
||||||
|
|
||||||
### [BLOCKED] · Thread Subscriptions (MSC4306)
|
### [BLOCKED] · Thread Subscriptions (MSC4306)
|
||||||
|
|
||||||
@@ -477,12 +477,12 @@ Check back after each Synapse upgrade — re-run `/matrix/client/versions` and `
|
|||||||
**What it would do:** Follow a thread without posting; get notifications for replies.
|
**What it would do:** Follow a thread without posting; get notifications for replies.
|
||||||
**Action when unblocked:** Add "Follow thread" button in the thread panel header (depends on #P3-8 Thread Panel).
|
**Action when unblocked:** Add "Follow thread" button in the thread panel header (depends on #P3-8 Thread Panel).
|
||||||
|
|
||||||
### [BLOCKED] · Report User (MSC4260)
|
### [DONE] · Report User (MSC4260) ✅
|
||||||
|
|
||||||
**Blocked by:** Server declares only spec v1.12; MSC4260 merged in v1.14 — endpoint may not exist
|
**Previously blocked by:** Server spec v1.12, but `POST /_matrix/client/v3/users/{userId}/report` was confirmed **200** on 2026-06-18 (live since Synapse 1.133.0).
|
||||||
**What it would do:** Report a specific user to homeserver admins (separate from reporting a message).
|
**What it does:** Reports a specific user to homeserver admins (separate from reporting a message).
|
||||||
**Note:** Report Message already exists in upstream Cinny. This would add Report User to the profile panel.
|
**Note:** Report Message already exists in upstream Cinny. This adds Report User to the profile panel.
|
||||||
**Action when unblocked:** Test `POST /_matrix/client/v3/users/{userId}/report`; if 200, add button to user profile.
|
**Implemented 2026-06-18:** `ReportUserModal.tsx` added at `src/app/features/room/ReportUserModal.tsx`. Button wired into `UserRoomProfile.tsx` between UserModeration and UserDeviceSessions (hidden for own profile). Category dropdown + reason text, inline success/error feedback, auto-close 1500ms after success.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -797,5 +797,9 @@ export function SeasonalEffect() {
|
|||||||
}, [settings.seasonalThemeOverride]);
|
}, [settings.seasonalThemeOverride]);
|
||||||
|
|
||||||
if (!theme) return null;
|
if (!theme) return null;
|
||||||
|
// Suppress seasonal overlay when a chat background is active — both running simultaneously
|
||||||
|
// wastes GPU and looks cluttered. The settings UI enforces mutual exclusion on write;
|
||||||
|
// this guard covers any legacy state already persisted.
|
||||||
|
if (settings.chatBackground !== 'none') return null;
|
||||||
return <SeasonalOverlay theme={theme} reduced={reduced} />;
|
return <SeasonalOverlay theme={theme} reduced={reduced} />;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import { Membership } from '../../../types/matrix/room';
|
|||||||
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
import { useRoomCreators } from '../../hooks/useRoomCreators';
|
||||||
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
import { useRoomPermissions } from '../../hooks/useRoomPermissions';
|
||||||
import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
|
import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare';
|
||||||
|
import { ReportUserModal } from '../../features/room/ReportUserModal';
|
||||||
import { CreatorChip } from './CreatorChip';
|
import { CreatorChip } from './CreatorChip';
|
||||||
import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils';
|
import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils';
|
||||||
import { DirectCreateSearchParams } from '../../pages/paths';
|
import { DirectCreateSearchParams } from '../../pages/paths';
|
||||||
@@ -272,6 +273,7 @@ type UserRoomProfileProps = {
|
|||||||
};
|
};
|
||||||
export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
||||||
const mx = useMatrixClient();
|
const mx = useMatrixClient();
|
||||||
|
const [reportUserOpen, setReportUserOpen] = useState(false);
|
||||||
const crossSigningActive = useCrossSigningActive();
|
const crossSigningActive = useCrossSigningActive();
|
||||||
const useAuthentication = useMediaAuthentication();
|
const useAuthentication = useMediaAuthentication();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@@ -390,8 +392,25 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
|||||||
canKick={canKickUser && membership === Membership.Join}
|
canKick={canKickUser && membership === Membership.Join}
|
||||||
canBan={canBanUser && membership !== Membership.Ban}
|
canBan={canBanUser && membership !== Membership.Ban}
|
||||||
/>
|
/>
|
||||||
|
{userId !== myUserId && (
|
||||||
|
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S200}` }}>
|
||||||
|
<Button
|
||||||
|
variant="Critical"
|
||||||
|
fill="None"
|
||||||
|
size="300"
|
||||||
|
radii="300"
|
||||||
|
before={<Icon size="50" src={Icons.Warning} />}
|
||||||
|
onClick={() => setReportUserOpen(true)}
|
||||||
|
>
|
||||||
|
<Text size="B300">Report User</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
{showEncryption && userId !== myUserId && <UserDeviceSessions userId={userId} />}
|
{showEncryption && userId !== myUserId && <UserDeviceSessions userId={userId} />}
|
||||||
{userId !== myUserId && <UserPrivateNotes userId={userId} />}
|
{userId !== myUserId && <UserPrivateNotes userId={userId} />}
|
||||||
|
{reportUserOpen && (
|
||||||
|
<ReportUserModal userId={userId} onClose={() => setReportUserOpen(false)} />
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -0,0 +1,223 @@
|
|||||||
|
import React, { FormEventHandler, useCallback, useEffect, useState } from 'react';
|
||||||
|
import FocusTrap from 'focus-trap-react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Text,
|
||||||
|
Input,
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
Icon,
|
||||||
|
Icons,
|
||||||
|
Overlay,
|
||||||
|
OverlayBackdrop,
|
||||||
|
OverlayCenter,
|
||||||
|
Header,
|
||||||
|
config,
|
||||||
|
color,
|
||||||
|
Spinner,
|
||||||
|
} from 'folds';
|
||||||
|
import { Method } from 'matrix-js-sdk';
|
||||||
|
import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback';
|
||||||
|
import { useMatrixClient } from '../../hooks/useMatrixClient';
|
||||||
|
import { stopPropagation } from '../../utils/keyboard';
|
||||||
|
|
||||||
|
type ReportCategory = 'spam' | 'harassment' | 'inappropriate' | 'other';
|
||||||
|
|
||||||
|
const CATEGORY_LABELS: Record<ReportCategory, string> = {
|
||||||
|
spam: 'Spam',
|
||||||
|
harassment: 'Harassment',
|
||||||
|
inappropriate: 'Inappropriate Content',
|
||||||
|
other: 'Other',
|
||||||
|
};
|
||||||
|
|
||||||
|
type ReportUserModalProps = {
|
||||||
|
userId: string;
|
||||||
|
onClose: () => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ReportUserModal({ userId, onClose }: ReportUserModalProps) {
|
||||||
|
const mx = useMatrixClient();
|
||||||
|
const [category, setCategory] = useState<ReportCategory>('spam');
|
||||||
|
|
||||||
|
const [reportState, submitReport] = useAsyncCallback(
|
||||||
|
useCallback(
|
||||||
|
async (reason: string) => {
|
||||||
|
await mx.http.authedRequest(
|
||||||
|
Method.Post,
|
||||||
|
`/users/${encodeURIComponent(userId)}/report`,
|
||||||
|
undefined,
|
||||||
|
{ reason },
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[mx, userId],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (reportState.status === AsyncStatus.Success) {
|
||||||
|
const timer = setTimeout(onClose, 1500);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}, [reportState.status, onClose]);
|
||||||
|
|
||||||
|
const handleSubmit: FormEventHandler<HTMLFormElement> = (evt) => {
|
||||||
|
evt.preventDefault();
|
||||||
|
if (reportState.status === AsyncStatus.Loading || reportState.status === AsyncStatus.Success) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const target = evt.target as HTMLFormElement;
|
||||||
|
const reasonInput = target.elements.namedItem('reasonInput') as HTMLInputElement | null;
|
||||||
|
const reasonText = reasonInput?.value.trim() ?? '';
|
||||||
|
const fullReason = `[${CATEGORY_LABELS[category]}] ${reasonText}`;
|
||||||
|
submitReport(fullReason);
|
||||||
|
};
|
||||||
|
|
||||||
|
const reportError =
|
||||||
|
reportState.status === AsyncStatus.Error
|
||||||
|
? (reportState.error as { errcode?: string; httpStatus?: number })
|
||||||
|
: undefined;
|
||||||
|
const errcode = reportError?.errcode;
|
||||||
|
const errorMsg =
|
||||||
|
errcode === 'M_LIMIT_EXCEEDED'
|
||||||
|
? 'You are being rate limited. Please wait before reporting again.'
|
||||||
|
: errcode === 'M_FORBIDDEN'
|
||||||
|
? 'You cannot report this user.'
|
||||||
|
: errcode === 'M_UNRECOGNIZED' || reportError?.httpStatus === 404
|
||||||
|
? 'User reporting is not supported by your homeserver.'
|
||||||
|
: 'Failed to submit report. Please try again.';
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Overlay open backdrop={<OverlayBackdrop />}>
|
||||||
|
<OverlayCenter>
|
||||||
|
<FocusTrap
|
||||||
|
focusTrapOptions={{
|
||||||
|
initialFocus: false,
|
||||||
|
onDeactivate: onClose,
|
||||||
|
clickOutsideDeactivates: true,
|
||||||
|
escapeDeactivates: stopPropagation,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
as="form"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="report-user-dialog-title"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
direction="Column"
|
||||||
|
style={{
|
||||||
|
background: color.Surface.Container,
|
||||||
|
borderRadius: config.radii.R400,
|
||||||
|
boxShadow: color.Other.Shadow,
|
||||||
|
width: '100%',
|
||||||
|
maxWidth: 420,
|
||||||
|
overflow: 'hidden',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Header
|
||||||
|
style={{
|
||||||
|
padding: `0 ${config.space.S200} 0 ${config.space.S400}`,
|
||||||
|
borderBottomWidth: config.borderWidth.B300,
|
||||||
|
}}
|
||||||
|
variant="Surface"
|
||||||
|
size="500"
|
||||||
|
>
|
||||||
|
<Box grow="Yes">
|
||||||
|
<Text id="report-user-dialog-title" size="H4">
|
||||||
|
Report User
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
<IconButton size="300" onClick={onClose} radii="300" aria-label="Close">
|
||||||
|
<Icon src={Icons.Cross} />
|
||||||
|
</IconButton>
|
||||||
|
</Header>
|
||||||
|
|
||||||
|
<Box direction="Column" gap="400" style={{ padding: config.space.S400 }}>
|
||||||
|
<Text priority="400">
|
||||||
|
Report this user to your homeserver admins. Please describe the issue below.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text as="label" htmlFor="report-user-category" size="L400">
|
||||||
|
Category
|
||||||
|
</Text>
|
||||||
|
<Box
|
||||||
|
as="select"
|
||||||
|
id="report-user-category"
|
||||||
|
aria-label="Report category"
|
||||||
|
value={category}
|
||||||
|
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||||
|
setCategory(e.target.value as ReportCategory)
|
||||||
|
}
|
||||||
|
style={{
|
||||||
|
padding: `${config.space.S200} ${config.space.S300}`,
|
||||||
|
borderRadius: config.radii.R300,
|
||||||
|
border: `1px solid ${color.Surface.ContainerLine}`,
|
||||||
|
background: color.Surface.Container,
|
||||||
|
color: color.Surface.OnContainer,
|
||||||
|
fontSize: 'inherit',
|
||||||
|
fontFamily: 'inherit',
|
||||||
|
width: '100%',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{(Object.keys(CATEGORY_LABELS) as ReportCategory[]).map((key) => (
|
||||||
|
<option key={key} value={key}>
|
||||||
|
{CATEGORY_LABELS[key]}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box direction="Column" gap="100">
|
||||||
|
<Text as="label" htmlFor="report-user-reason-input" size="L400">
|
||||||
|
Reason
|
||||||
|
</Text>
|
||||||
|
<Input
|
||||||
|
id="report-user-reason-input"
|
||||||
|
name="reasonInput"
|
||||||
|
aria-label="Reason for report"
|
||||||
|
variant="Background"
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
{reportState.status === AsyncStatus.Error && (
|
||||||
|
<Text style={{ color: color.Critical.Main }} size="T300">
|
||||||
|
{errorMsg}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{reportState.status === AsyncStatus.Success && (
|
||||||
|
<Text style={{ color: color.Success.Main }} size="T300">
|
||||||
|
User has been reported to the server.
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box gap="200" justifyContent="End">
|
||||||
|
<Button type="button" variant="Secondary" fill="None" radii="300" onClick={onClose}>
|
||||||
|
<Text size="B400">Cancel</Text>
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
variant="Critical"
|
||||||
|
radii="300"
|
||||||
|
before={
|
||||||
|
reportState.status === AsyncStatus.Loading ? (
|
||||||
|
<Spinner fill="Solid" variant="Critical" size="200" />
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
|
aria-disabled={
|
||||||
|
reportState.status === AsyncStatus.Loading ||
|
||||||
|
reportState.status === AsyncStatus.Success
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<Text size="B400">
|
||||||
|
{reportState.status === AsyncStatus.Loading ? 'Reporting...' : 'Report User'}
|
||||||
|
</Text>
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
</FocusTrap>
|
||||||
|
</OverlayCenter>
|
||||||
|
</Overlay>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -432,6 +432,7 @@ function Appearance() {
|
|||||||
settingsAtom,
|
settingsAtom,
|
||||||
'seasonalThemeOverride',
|
'seasonalThemeOverride',
|
||||||
);
|
);
|
||||||
|
const [, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box direction="Column" gap="100">
|
<Box direction="Column" gap="100">
|
||||||
@@ -512,7 +513,10 @@ function Appearance() {
|
|||||||
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
|
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
|
||||||
<SeasonalBgGrid
|
<SeasonalBgGrid
|
||||||
value={seasonalThemeOverride ?? 'auto'}
|
value={seasonalThemeOverride ?? 'auto'}
|
||||||
onChange={(v) => setSeasonalThemeOverride(v)}
|
onChange={(v) => {
|
||||||
|
setSeasonalThemeOverride(v);
|
||||||
|
if (v !== 'auto' && v !== 'off') setChatBackground('none');
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</SequenceCard>
|
</SequenceCard>
|
||||||
@@ -1671,6 +1675,7 @@ function SeasonalBgGrid({
|
|||||||
|
|
||||||
function ChatBgGrid() {
|
function ChatBgGrid() {
|
||||||
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
|
||||||
|
const [, setSeasonalThemeOverride] = useSetting(settingsAtom, 'seasonalThemeOverride');
|
||||||
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations');
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isDark = theme.kind === ThemeKind.Dark;
|
const isDark = theme.kind === ThemeKind.Dark;
|
||||||
@@ -1683,7 +1688,10 @@ function ChatBgGrid() {
|
|||||||
type="button"
|
type="button"
|
||||||
aria-label={opt.label}
|
aria-label={opt.label}
|
||||||
aria-pressed={chatBackground === opt.value}
|
aria-pressed={chatBackground === opt.value}
|
||||||
onClick={() => setChatBackground(opt.value as ChatBackground)}
|
onClick={() => {
|
||||||
|
setChatBackground(opt.value as ChatBackground);
|
||||||
|
if (opt.value !== 'none') setSeasonalThemeOverride('off');
|
||||||
|
}}
|
||||||
style={{
|
style={{
|
||||||
display: 'block',
|
display: 'block',
|
||||||
width: toRem(76),
|
width: toRem(76),
|
||||||
|
|||||||
@@ -53,13 +53,13 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
|
|
||||||
const cardStyle: CSSProperties = {
|
const cardStyle: CSSProperties = {
|
||||||
position: 'relative',
|
position: 'relative',
|
||||||
background: 'var(--lt-bg-card, #1a1a2e)',
|
background: 'var(--lt-bg-card)',
|
||||||
border: '1px solid var(--lt-border-color, rgba(255,255,255,0.1))',
|
border: '1px solid var(--lt-border-color)',
|
||||||
borderRadius: '12px',
|
borderRadius: '12px',
|
||||||
padding: '12px 14px',
|
padding: '12px 14px',
|
||||||
minWidth: '280px',
|
minWidth: '280px',
|
||||||
maxWidth: '340px',
|
maxWidth: '340px',
|
||||||
boxShadow: 'var(--lt-box-glow-orange, 0 4px 16px rgba(0,0,0,0.4))',
|
boxShadow: 'var(--lt-box-glow-orange)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
animation: 'lotusToastIn 0.2s ease-out both',
|
animation: 'lotusToastIn 0.2s ease-out both',
|
||||||
userSelect: 'none',
|
userSelect: 'none',
|
||||||
@@ -84,19 +84,19 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
width: '24px',
|
width: '24px',
|
||||||
height: '24px',
|
height: '24px',
|
||||||
borderRadius: '50%',
|
borderRadius: '50%',
|
||||||
background: 'var(--lt-accent-orange-dim, rgba(255,107,0,0.15))',
|
background: 'var(--lt-accent-orange-dim)',
|
||||||
border: '1px solid var(--lt-accent-orange-border, rgba(255,107,0,0.35))',
|
border: '1px solid var(--lt-accent-orange-border)',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
justifyContent: 'center',
|
justifyContent: 'center',
|
||||||
fontSize: '10px',
|
fontSize: '10px',
|
||||||
fontWeight: 700,
|
fontWeight: 700,
|
||||||
color: 'var(--lt-accent-orange, #ff6b00)',
|
color: 'var(--lt-accent-orange)',
|
||||||
flexShrink: 0,
|
flexShrink: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nameStyle: CSSProperties = {
|
const nameStyle: CSSProperties = {
|
||||||
color: 'var(--lt-accent-orange, #ff6b00)',
|
color: 'var(--lt-accent-orange)',
|
||||||
fontWeight: 600,
|
fontWeight: 600,
|
||||||
fontSize: '0.85rem',
|
fontSize: '0.85rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -110,7 +110,7 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
right: '10px',
|
right: '10px',
|
||||||
background: 'none',
|
background: 'none',
|
||||||
border: 'none',
|
border: 'none',
|
||||||
color: 'var(--lt-text-secondary, #7fa3bf)',
|
color: 'var(--lt-text-secondary)',
|
||||||
cursor: 'pointer',
|
cursor: 'pointer',
|
||||||
fontSize: '14px',
|
fontSize: '14px',
|
||||||
lineHeight: 1,
|
lineHeight: 1,
|
||||||
@@ -119,7 +119,7 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const bodyStyle: CSSProperties = {
|
const bodyStyle: CSSProperties = {
|
||||||
color: 'var(--lt-text-primary, #c4d9ee)',
|
color: 'var(--lt-text-primary)',
|
||||||
fontSize: '0.82rem',
|
fontSize: '0.82rem',
|
||||||
margin: '4px 0 2px',
|
margin: '4px 0 2px',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
@@ -128,7 +128,7 @@ function ToastCard({ toast }: ToastCardProps) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const roomNameStyle: CSSProperties = {
|
const roomNameStyle: CSSProperties = {
|
||||||
color: 'var(--lt-text-secondary, #7fa3bf)',
|
color: 'var(--lt-text-secondary)',
|
||||||
fontSize: '0.75rem',
|
fontSize: '0.75rem',
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
textOverflow: 'ellipsis',
|
textOverflow: 'ellipsis',
|
||||||
|
|||||||
Reference in New Issue
Block a user