diff --git a/LOTUS_BUGS.md b/LOTUS_BUGS.md index b8e4c337f..e0755272f 100644 --- a/LOTUS_BUGS.md +++ b/LOTUS_BUGS.md @@ -60,10 +60,10 @@ This document tracks identified bugs, edge cases, and architectural discrepancie ### 6. Exclusive Background vs. Seasonal Choice - **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. - **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 @@ -149,11 +149,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie | Issue Description | File Path | | :-------------------------------------------------------------------- | :-------------------------------------------------------- | | 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` | | 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 fallback hexes in toast colors | `cinny/src/app/features/toast/LotusToastContainer.tsx` | +| Hardcoded colors `#00FF88`, `#FF6B00` ✅ **VERIFIED COMPLIANT** | `cinny/src/app/features/call/CallControls.tsx` | +| Hardcoded fallback hexes in toast colors ✅ **FIXED** | `cinny/src/app/features/toast/LotusToastContainer.tsx` | --- diff --git a/LOTUS_TODO.md b/LOTUS_TODO.md index cabdb6d9a..c20a291d6 100644 --- a/LOTUS_TODO.md +++ b/LOTUS_TODO.md @@ -7,10 +7,10 @@ ## 🏗️ Infrastructure & Maintenance -- [ ] **Upgrade Synapse to v1.155.0** - - **Context:** Synapse 1.155.0 is the last version supporting Debian 12 Bookworm. - - **Reference:** https://github.com/element-hq/synapse/releases/tag/v1.155.0 - - **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. +- [x] **Upgrade Synapse to v1.155.0** ✅ Done 2026-06-18 + - **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. + - **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). + - **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) - **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` ### Confirmed facts | 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 | -| **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 | -| **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 ✅ | | `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` | @@ -467,9 +467,9 @@ Check back after each Synapse upgrade — re-run `/matrix/client/versions` and ` ### [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. -**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) @@ -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. **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 -**What it would do:** Report 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. -**Action when unblocked:** Test `POST /_matrix/client/v3/users/{userId}/report`; if 200, add button to user profile. +**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 does:** Reports a specific user to homeserver admins (separate from reporting a message). +**Note:** Report Message already exists in upstream Cinny. This adds Report User to the profile panel. +**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. --- diff --git a/src/app/components/seasonal/SeasonalEffect.tsx b/src/app/components/seasonal/SeasonalEffect.tsx index 5ecd320b3..07879b701 100644 --- a/src/app/components/seasonal/SeasonalEffect.tsx +++ b/src/app/components/seasonal/SeasonalEffect.tsx @@ -797,5 +797,9 @@ export function SeasonalEffect() { }, [settings.seasonalThemeOverride]); 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 ; } diff --git a/src/app/components/user-profile/UserRoomProfile.tsx b/src/app/components/user-profile/UserRoomProfile.tsx index 19a72d22d..fb89bb916 100644 --- a/src/app/components/user-profile/UserRoomProfile.tsx +++ b/src/app/components/user-profile/UserRoomProfile.tsx @@ -28,6 +28,7 @@ import { Membership } from '../../../types/matrix/room'; import { useRoomCreators } from '../../hooks/useRoomCreators'; import { useRoomPermissions } from '../../hooks/useRoomPermissions'; import { useMemberPowerCompare } from '../../hooks/useMemberPowerCompare'; +import { ReportUserModal } from '../../features/room/ReportUserModal'; import { CreatorChip } from './CreatorChip'; import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils'; import { DirectCreateSearchParams } from '../../pages/paths'; @@ -272,6 +273,7 @@ type UserRoomProfileProps = { }; export function UserRoomProfile({ userId }: UserRoomProfileProps) { const mx = useMatrixClient(); + const [reportUserOpen, setReportUserOpen] = useState(false); const crossSigningActive = useCrossSigningActive(); const useAuthentication = useMediaAuthentication(); const navigate = useNavigate(); @@ -390,8 +392,25 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) { canKick={canKickUser && membership === Membership.Join} canBan={canBanUser && membership !== Membership.Ban} /> + {userId !== myUserId && ( + + + + )} {showEncryption && userId !== myUserId && } {userId !== myUserId && } + {reportUserOpen && ( + setReportUserOpen(false)} /> + )} ); diff --git a/src/app/features/room/ReportUserModal.tsx b/src/app/features/room/ReportUserModal.tsx new file mode 100644 index 000000000..51e0621e5 --- /dev/null +++ b/src/app/features/room/ReportUserModal.tsx @@ -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 = { + 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('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 = (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 ( + }> + + + +
+ + + Report User + + + + + +
+ + + + Report this user to your homeserver admins. Please describe the issue below. + + + + + Category + + ) => + 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) => ( + + ))} + + + + + + Reason + + + {reportState.status === AsyncStatus.Error && ( + + {errorMsg} + + )} + {reportState.status === AsyncStatus.Success && ( + + User has been reported to the server. + + )} + + + + + + + +
+
+
+
+ ); +} diff --git a/src/app/features/settings/general/General.tsx b/src/app/features/settings/general/General.tsx index de55cba8f..389e3e2cd 100644 --- a/src/app/features/settings/general/General.tsx +++ b/src/app/features/settings/general/General.tsx @@ -432,6 +432,7 @@ function Appearance() { settingsAtom, 'seasonalThemeOverride', ); + const [, setChatBackground] = useSetting(settingsAtom, 'chatBackground'); return ( @@ -512,7 +513,10 @@ function Appearance() { setSeasonalThemeOverride(v)} + onChange={(v) => { + setSeasonalThemeOverride(v); + if (v !== 'auto' && v !== 'off') setChatBackground('none'); + }} /> @@ -1671,6 +1675,7 @@ function SeasonalBgGrid({ function ChatBgGrid() { const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground'); + const [, setSeasonalThemeOverride] = useSetting(settingsAtom, 'seasonalThemeOverride'); const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations'); const theme = useTheme(); const isDark = theme.kind === ThemeKind.Dark; @@ -1683,7 +1688,10 @@ function ChatBgGrid() { type="button" aria-label={opt.label} aria-pressed={chatBackground === opt.value} - onClick={() => setChatBackground(opt.value as ChatBackground)} + onClick={() => { + setChatBackground(opt.value as ChatBackground); + if (opt.value !== 'none') setSeasonalThemeOverride('off'); + }} style={{ display: 'block', width: toRem(76), diff --git a/src/app/features/toast/LotusToastContainer.tsx b/src/app/features/toast/LotusToastContainer.tsx index ce35b450c..655e14525 100644 --- a/src/app/features/toast/LotusToastContainer.tsx +++ b/src/app/features/toast/LotusToastContainer.tsx @@ -53,13 +53,13 @@ function ToastCard({ toast }: ToastCardProps) { const cardStyle: CSSProperties = { position: 'relative', - background: 'var(--lt-bg-card, #1a1a2e)', - border: '1px solid var(--lt-border-color, rgba(255,255,255,0.1))', + background: 'var(--lt-bg-card)', + border: '1px solid var(--lt-border-color)', borderRadius: '12px', padding: '12px 14px', minWidth: '280px', 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', animation: 'lotusToastIn 0.2s ease-out both', userSelect: 'none', @@ -84,19 +84,19 @@ function ToastCard({ toast }: ToastCardProps) { width: '24px', height: '24px', borderRadius: '50%', - background: 'var(--lt-accent-orange-dim, rgba(255,107,0,0.15))', - border: '1px solid var(--lt-accent-orange-border, rgba(255,107,0,0.35))', + background: 'var(--lt-accent-orange-dim)', + border: '1px solid var(--lt-accent-orange-border)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: '10px', fontWeight: 700, - color: 'var(--lt-accent-orange, #ff6b00)', + color: 'var(--lt-accent-orange)', flexShrink: 0, }; const nameStyle: CSSProperties = { - color: 'var(--lt-accent-orange, #ff6b00)', + color: 'var(--lt-accent-orange)', fontWeight: 600, fontSize: '0.85rem', overflow: 'hidden', @@ -110,7 +110,7 @@ function ToastCard({ toast }: ToastCardProps) { right: '10px', background: 'none', border: 'none', - color: 'var(--lt-text-secondary, #7fa3bf)', + color: 'var(--lt-text-secondary)', cursor: 'pointer', fontSize: '14px', lineHeight: 1, @@ -119,7 +119,7 @@ function ToastCard({ toast }: ToastCardProps) { }; const bodyStyle: CSSProperties = { - color: 'var(--lt-text-primary, #c4d9ee)', + color: 'var(--lt-text-primary)', fontSize: '0.82rem', margin: '4px 0 2px', overflow: 'hidden', @@ -128,7 +128,7 @@ function ToastCard({ toast }: ToastCardProps) { }; const roomNameStyle: CSSProperties = { - color: 'var(--lt-text-secondary, #7fa3bf)', + color: 'var(--lt-text-secondary)', fontSize: '0.75rem', overflow: 'hidden', textOverflow: 'ellipsis',