feat: presence fix, voice ringing fix, user private notes + doc updates
- usePresenceUpdater: replace stale closure with readStatus() called at invocation time so changing custom status in Profile Settings is never silently overwritten by subsequent activity events - CallEmbedProvider: fix m.space.parent state-key lookup by switching getStateEvent → getStateEvents (plural); space channel voice rooms no longer trigger the incoming-call ring/animation - Add useUserNotes hook (io.lotus.user_notes account data, reactive via useAccountDataCallback, 500-char limit, cross-device sync) - UserRoomProfile: add UserPrivateNotes textarea with 800ms debounced auto-save, saving indicator, char counter when <100 chars remain; shown only when viewing another user's profile - LOTUS_FEATURES.md: add Private Notes section, Status Revert fix note, animation improvements subsection, Seasonal Themes section - LOTUS_BUGS.md: mark presence revert + voice ringing bugs as resolved - README.md + landing/index.html: document all new June 2026 features Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+8
-9
@@ -32,12 +32,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
|
||||
### 1. Presence Updater Reverts Status Updates
|
||||
**File:** `src/app/hooks/usePresenceUpdater.ts` (Line 20)
|
||||
**Status:** High Priority
|
||||
**Status:** ✅ RESOLVED (June 2026)
|
||||
|
||||
* **Issue:** The `storedStatus` variable is captured once when the `useEffect` starts.
|
||||
* **Impact:** If a user updates their status message in the Profile settings, the presence updater hook continues to use the *old* status message from its closure. Every time the user moves their mouse/activity, the hook sends `setOnline` with the stale status, reverting the user's change.
|
||||
* **Recommended Fix:**
|
||||
1. Read the status from `localStorage` inside the `setOnline`/`setUnavailable` functions rather than at the top of the effect.
|
||||
* **Issue:** The `storedStatus` variable was captured once when the `useEffect` started.
|
||||
* **Impact:** If a user updated their status message in Profile Settings, the hook would continue broadcasting the old message on every activity event, silently reverting the change.
|
||||
* **Fix Applied:** Replaced the single `localStorage.getItem` read with a `readStatus()` function called inside every `setOnline` and `setUnavailable` invocation, ensuring the current value is always used.
|
||||
|
||||
### 2. Audio Playback Rate Reset
|
||||
**File:** `src/app/components/message/content/AudioContent.tsx` (Line 97)
|
||||
@@ -57,11 +56,11 @@ This document tracks identified bugs, edge cases, and architectural discrepancie
|
||||
|
||||
### 4. Incorrect Ringing in Voice Rooms
|
||||
**File:** `src/app/components/CallEmbedProvider.tsx`
|
||||
**Status:** Logic Bug
|
||||
**Status:** ✅ RESOLVED (June 2026)
|
||||
|
||||
* **Issue:** Joining a static voice room (Public Space channel) triggers the "Incoming Call" ringing animation and sound. This should only occur for "StartedByUser" intents (DMs and Private Group Calls).
|
||||
* **Root Cause:** The ringing logic does not distinguish between persistent voice rooms and user-initiated calls.
|
||||
* **Recommended Fix:** Check `room.getJoinRule()` or check for the `m.space.parent` event. If the room is a public voice channel, suppress the ringing animation.
|
||||
* **Issue:** Joining a static voice room (Public Space channel) triggered the "Incoming Call" ringing animation and sound.
|
||||
* **Root Cause:** `getStateEvent(room, StateEvent.SpaceParent)` always returned `undefined` because `m.space.parent` events use the parent space's room ID as the state key, not an empty string. The ringing suppression check silently failed.
|
||||
* **Fix Applied:** Replaced `getStateEvent` with `getStateEvents` (plural), which returns all events of a given type regardless of state key. A room with any `m.space.parent` event is correctly identified as a space channel and suppresses ringing.
|
||||
|
||||
---
|
||||
|
||||
|
||||
+77
-14
@@ -10,20 +10,21 @@ Last updated: June 2026.
|
||||
1. [Branding & Identity](#branding--identity)
|
||||
2. [LotusGuild Terminal Design System (TDS) v1.2](#lotusguild-terminal-design-system-tds-v12)
|
||||
3. [Animated Chat Backgrounds (P5-4)](#animated-chat-backgrounds-p5-4)
|
||||
4. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
|
||||
5. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
|
||||
6. [Voice / Video Call Improvements](#voice--video-call-improvements)
|
||||
7. [Per-Message Read Receipts](#per-message-read-receipts)
|
||||
8. [Delivery Status Indicators](#delivery-status-indicators)
|
||||
9. [Messaging Enhancements](#messaging-enhancements)
|
||||
10. [Presence](#presence)
|
||||
11. [UX & Composer](#ux--composer)
|
||||
12. [Room Customization](#room-customization)
|
||||
13. [Moderation](#moderation)
|
||||
14. [Notifications](#notifications)
|
||||
15. [Server Integration](#server-integration)
|
||||
16. [Infrastructure](#infrastructure)
|
||||
17. [Key Custom Files](#key-custom-files)
|
||||
4. [Seasonal Theme Overlays (P5-12)](#seasonal-theme-overlays-p5-12)
|
||||
5. [Glassmorphism Sidebar (P5-3)](#glassmorphism-sidebar-p5-3)
|
||||
6. [Night Light / Blue Light Filter (P5-5)](#night-light--blue-light-filter-p5-5)
|
||||
7. [Voice / Video Call Improvements](#voice--video-call-improvements)
|
||||
8. [Per-Message Read Receipts](#per-message-read-receipts)
|
||||
9. [Delivery Status Indicators](#delivery-status-indicators)
|
||||
10. [Messaging Enhancements](#messaging-enhancements)
|
||||
11. [Presence](#presence)
|
||||
12. [UX & Composer](#ux--composer)
|
||||
13. [Room Customization](#room-customization)
|
||||
14. [Moderation](#moderation)
|
||||
15. [Notifications](#notifications)
|
||||
16. [Server Integration](#server-integration)
|
||||
17. [Infrastructure](#infrastructure)
|
||||
18. [Key Custom Files](#key-custom-files)
|
||||
|
||||
---
|
||||
|
||||
@@ -148,6 +149,16 @@ Strips all `animation` properties from the returned style object when either `pa
|
||||
|
||||
A "Pause Background Animations" toggle is exposed in **Settings → Appearance**. The preference is persisted and read by `getChatBg()` at render time.
|
||||
|
||||
### Animation Improvements (June 2026)
|
||||
|
||||
All five animated backgrounds were rewritten for smoother, more organic motion:
|
||||
|
||||
- **Digital Rain** — added a phosphor glow flicker (`animRainGlowKeyframe`, 2.1 s) layered on top of the column scroll; stripe opacity increased for better visibility
|
||||
- **Star Drift** — each of the three dot layers now moves by exactly its own tile width/height per cycle (`−130 px`, `−190 px`, `−260 px`), eliminating the visible seam on loop
|
||||
- **Grid Pulse** — independent brightness oscillation (`animGridBrightnessKeyframe`, 3.3 s) runs alongside the size breathe (4 s) at a prime period ratio so they never synchronise
|
||||
- **Aurora Flow** — four gradient layers now have individual `backgroundSize` values (`200%`, `250%`, `300%`, `220%`); the keyframe drives each layer through a distinct 5-stop path, replacing the robotic single back-and-forth
|
||||
- **Fireflies** — glow pulse (`animFirefliesGlowKeyframe`, 2.3 s `filter: brightness`) and opacity blink (`animFirefliesBlinkKeyframe`, 1.7 s) added on top of the position drift; prime periods create unsynchronised bioluminescence
|
||||
|
||||
### Files
|
||||
|
||||
- `src/app/styles/Animations.css.ts` — vanilla-extract keyframe definitions
|
||||
@@ -155,6 +166,40 @@ A "Pause Background Animations" toggle is exposed in **Settings → Appearance**
|
||||
|
||||
---
|
||||
|
||||
## Seasonal Theme Overlays (P5-12)
|
||||
|
||||
Decorative CSS-only overlays that activate automatically on holidays and events. Manually overrideable in **Settings → Appearance → Seasonal Theme**.
|
||||
|
||||
### Themes
|
||||
|
||||
| Theme | Window | Effect |
|
||||
|---|---|---|
|
||||
| 🎆 New Year | Dec 31–Jan 2 | Radial firework bursts in gold, red, cyan, purple; gold shimmer sweep |
|
||||
| 🏮 Lunar New Year | Jan 22–Feb 5 | Floating paper lanterns bobbing; silk texture; gold shimmer accent |
|
||||
| 💖 Valentine's Day | Feb 10–15 | ♥ hearts floating upward; soft pink ambient glow |
|
||||
| 🍀 St. Patrick's Day | Mar 15–18 | ☘ clovers drifting down; gold metallic shimmer top border |
|
||||
| 🃏 April Fool's | Apr 1 | Glitch overlay: RGB channel separation, hue-rotate spikes, scanline sweep, "SIGNAL LOST" watermark |
|
||||
| 🌱 Earth Day | Apr 20–23 | 🌿🍃 leaf emoji drift; sage green ambient tint; vine accent on left edge |
|
||||
| 🍂 Autumn | Sep 21–Oct 31 | Warm orange/amber leaf shapes rotating and falling |
|
||||
| 👾 Arcade Day | Sep 12 | CRT scanlines; blinking pixel corner decorations; "INSERT COIN" prompt |
|
||||
| 🚀 Deep Space Week | Oct 4–10 | Warp-speed star streaks radiating from screen centre; nebula purple/blue ambient |
|
||||
| 🎃 Halloween | Oct 15–Nov 1 | Purple and orange glowing particles; SVG spider web in top-left corner; dark purple tint |
|
||||
| ❄️ Christmas | Dec 10–Jan 2 | White dot snowfall in multiple layers at varied speeds |
|
||||
|
||||
### Implementation
|
||||
|
||||
- `SeasonalEffect` component mounted in `App.tsx` at `z-index: 9997` (below night light, above content)
|
||||
- Auto-detection via `getActiveSeason(now: Date)` — themes checked in priority order (New Year > Valentine's > … > Autumn)
|
||||
- `seasonalThemeOverride` setting: `'auto' | 'off' | <theme-name>` — persisted in `settingsAtom`
|
||||
- All particle animations gated on `prefers-reduced-motion: reduce` — ambient overlays (tints, textures, shimmer) remain active
|
||||
|
||||
### Files
|
||||
|
||||
- `src/app/components/seasonal/SeasonalEffect.tsx` — theme detection, date ranges, all overlay components
|
||||
- `src/app/components/seasonal/Seasonal.css.ts` — vanilla-extract keyframes (fall, leaf, float-up, bob, glitch, burst, warp, scanline, shimmer, etc.)
|
||||
|
||||
---
|
||||
|
||||
## Glassmorphism Sidebar (P5-3)
|
||||
|
||||
An optional frosted-glass sidebar style toggled in **Settings → Appearance**.
|
||||
@@ -538,6 +583,12 @@ Applied in:
|
||||
- `@mention` autocomplete dropdown
|
||||
- Inbox / notifications panel
|
||||
|
||||
### Status Revert Bug Fix (June 2026)
|
||||
|
||||
`usePresenceUpdater` previously captured the user's custom status message once via `localStorage.getItem` at effect initialization. When the user changed their status message in Profile Settings, subsequent automatic transitions back to `online` (e.g., returning from idle) would silently broadcast the old status message, reverting the custom status.
|
||||
|
||||
Fixed by replacing the single read with a `readStatus()` function called inside every `setOnline` and `setUnavailable` invocation, so the current localStorage value is always used.
|
||||
|
||||
### Document Title Unread Count
|
||||
|
||||
The browser tab title updates to reflect unread state:
|
||||
@@ -565,6 +616,18 @@ When a user has `m.tz` set in their profile:
|
||||
|
||||
Hook: `src/app/hooks/useLocalTime.ts`
|
||||
|
||||
### User-to-User Private Notes (P5-34)
|
||||
|
||||
A private text note on any user's profile, visible only to the logged-in user and synced across all their devices.
|
||||
|
||||
- Textarea in the user profile popout (below device sessions), shown only when viewing another user — never on your own profile
|
||||
- Auto-saves 800 ms after the last keystroke with a "Saving…" indicator
|
||||
- Character counter appears when fewer than 100 characters remain (max 500)
|
||||
- Stored in `io.lotus.user_notes` account data as `{ [userId]: string }` — deletes the key when the note is cleared
|
||||
- Reactive: updates immediately if account data arrives from another device mid-session
|
||||
|
||||
Hook: `src/app/hooks/useUserNotes.ts`
|
||||
|
||||
---
|
||||
|
||||
## UX & Composer
|
||||
|
||||
@@ -58,7 +58,8 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina
|
||||
- LotusGuild Terminal Design System (TDS) — a CRT terminal-inspired dark theme
|
||||
- TDS light mode variant for daytime use
|
||||
- 20+ static chat background patterns
|
||||
- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies
|
||||
- 5 animated chat backgrounds: Digital Rain, Star Drift, Grid Pulse, Aurora Flow, Fireflies (with improved per-layer looping, phosphor-flicker rain, fluid aurora sweep, and organic firefly bioluminescence)
|
||||
- 11 seasonal & holiday theme overlays — Halloween, Christmas, New Year, Autumn, Valentine's Day, St. Patrick's Day, Earth Day, Lunar New Year, April Fools', Deep Space, and Retro Arcade; auto-selected by date with a manual override in Settings → Appearance
|
||||
- Toggle to pause background animations
|
||||
- Glassmorphism sidebar — frosted glass effect that lets the background show through
|
||||
- Night Light / blue light filter with an adjustable intensity slider
|
||||
@@ -69,10 +70,11 @@ The Lotus Chat logo (`lotus_chat.png`) is a derivative work based on the origina
|
||||
### Presence & Profile
|
||||
|
||||
- Discord-style presence selector: Online, Idle, Do Not Disturb, Invisible, or Auto
|
||||
- Custom status message with emoji and an optional auto-clear timer
|
||||
- Custom status message with emoji and an optional auto-clear timer (changing your status is never silently overwritten by activity events)
|
||||
- Colored presence ring on member avatars (green / yellow / red)
|
||||
- Profile fields for pronouns and timezone
|
||||
- When a user's timezone is set, their current local time appears in their profile
|
||||
- Private notes on any user's profile — freeform text visible only to you, auto-saves and syncs across devices
|
||||
- Unread count shown in the browser tab title
|
||||
|
||||
### Moderation & Privacy
|
||||
|
||||
@@ -54,7 +54,7 @@ import { getChatBg } from '../features/lotus/chatBackground';
|
||||
import { useTheme, ThemeKind } from '../hooks/useTheme';
|
||||
import { useSetting } from '../state/hooks/settings';
|
||||
import { settingsAtom } from '../state/settings';
|
||||
import { getStateEvent, getMemberDisplayName } from '../utils/room';
|
||||
import { getStateEvent, getStateEvents, getMemberDisplayName } from '../utils/room';
|
||||
import { StateEvent } from '../../types/matrix/room';
|
||||
import { getPowersLevelFromMatrixEvent } from '../hooks/usePowerLevels';
|
||||
import { getRoomCreatorsForRoomId } from '../hooks/useRoomCreators';
|
||||
@@ -329,7 +329,10 @@ function IncomingCallListener({ callEmbed, joined }: IncomingCallListenerProps)
|
||||
// Space voice channels and public rooms fire room-level RTC notifications
|
||||
// whenever anyone joins — ringing every member is incorrect behaviour.
|
||||
const isDirect = directs.has(room.roomId);
|
||||
const isSpaceChild = !!getStateEvent(room, StateEvent.SpaceParent);
|
||||
// m.space.parent uses the parent space ID as the state key, so getStateEvent
|
||||
// (which defaults to stateKey='') always returns undefined. Use getStateEvents
|
||||
// (no key filter) to detect any space parent relationship.
|
||||
const isSpaceChild = getStateEvents(room, StateEvent.SpaceParent).length > 0;
|
||||
const joinRule = room.getJoinRule();
|
||||
const isPrivateGroup =
|
||||
!isSpaceChild &&
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Box, Button, config, Icon, IconButton, Icons, Spinner, Text, toRem } from 'folds';
|
||||
import React, { useCallback, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { VerificationRequest } from 'matrix-js-sdk/lib/crypto-api';
|
||||
import { AsyncState, AsyncStatus, useAsync } from '../../hooks/useAsyncCallback';
|
||||
@@ -32,6 +32,7 @@ import { CreatorChip } from './CreatorChip';
|
||||
import { getDirectCreatePath, withSearchParam } from '../../pages/pathUtils';
|
||||
import { DirectCreateSearchParams } from '../../pages/paths';
|
||||
import { useExtendedProfile } from '../../hooks/useExtendedProfile';
|
||||
import { useUserNotes, USER_NOTE_MAX_LENGTH } from '../../hooks/useUserNotes';
|
||||
|
||||
type VerifyDeviceButtonProps = {
|
||||
userId: string;
|
||||
@@ -207,6 +208,65 @@ function UserDeviceSessions({ userId }: UserDeviceSessionsProps) {
|
||||
);
|
||||
}
|
||||
|
||||
function UserPrivateNotes({ userId }: { userId: string }) {
|
||||
const { getNote, setNote } = useUserNotes();
|
||||
const [draft, setDraft] = useState(() => getNote(userId));
|
||||
const [saving, setSaving] = useState(false);
|
||||
const saveTimer = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
// Sync if account data arrives after mount
|
||||
useEffect(() => {
|
||||
setDraft(getNote(userId));
|
||||
}, [getNote, userId]);
|
||||
|
||||
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||
const val = e.target.value;
|
||||
setDraft(val);
|
||||
clearTimeout(saveTimer.current);
|
||||
saveTimer.current = setTimeout(async () => {
|
||||
setSaving(true);
|
||||
await setNote(userId, val);
|
||||
setSaving(false);
|
||||
}, 800);
|
||||
};
|
||||
|
||||
useEffect(() => () => clearTimeout(saveTimer.current), []);
|
||||
|
||||
const charsLeft = USER_NOTE_MAX_LENGTH - draft.length;
|
||||
|
||||
return (
|
||||
<Box direction="Column" gap="200">
|
||||
<Box justifyContent="SpaceBetween" alignItems="Center">
|
||||
<Text size="L400">Private Note</Text>
|
||||
<Text size="T200" style={{ opacity: 0.5 }}>
|
||||
{saving ? 'Saving…' : charsLeft < 100 ? `${charsLeft} left` : ''}
|
||||
</Text>
|
||||
</Box>
|
||||
<textarea
|
||||
value={draft}
|
||||
onChange={handleChange}
|
||||
maxLength={USER_NOTE_MAX_LENGTH}
|
||||
placeholder="Notes only visible to you…"
|
||||
rows={3}
|
||||
style={{
|
||||
width: '100%',
|
||||
resize: 'vertical',
|
||||
background: 'var(--bg-surface-variant)',
|
||||
color: 'inherit',
|
||||
border: '1px solid var(--border-interactive)',
|
||||
borderRadius: '6px',
|
||||
padding: '8px 10px',
|
||||
fontSize: '14px',
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: 1.5,
|
||||
boxSizing: 'border-box',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
type UserRoomProfileProps = {
|
||||
userId: string;
|
||||
};
|
||||
@@ -331,6 +391,7 @@ export function UserRoomProfile({ userId }: UserRoomProfileProps) {
|
||||
canBan={canBanUser && membership !== Membership.Ban}
|
||||
/>
|
||||
{showEncryption && userId !== myUserId && <UserDeviceSessions userId={userId} />}
|
||||
{userId !== myUserId && <UserPrivateNotes userId={userId} />}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -17,26 +17,34 @@ export function usePresenceUpdater() {
|
||||
|
||||
useEffect(() => {
|
||||
const userId = mx.getUserId();
|
||||
const storedStatus = userId ? (localStorage.getItem(`lotus-status-msg-${userId}`) ?? '') : '';
|
||||
|
||||
const setOnline = () =>
|
||||
mx
|
||||
// Read status from localStorage at call time so manual updates from the
|
||||
// Profile settings are never overwritten by a stale closure value.
|
||||
const readStatus = () =>
|
||||
userId ? (localStorage.getItem(`lotus-status-msg-${userId}`) ?? '') : '';
|
||||
|
||||
const setOnline = () => {
|
||||
const status = readStatus();
|
||||
return mx
|
||||
.setPresence({
|
||||
presence: 'online',
|
||||
...(storedStatus ? { status_msg: storedStatus } : {}),
|
||||
...(status ? { status_msg: status } : {}),
|
||||
})
|
||||
.catch(() => undefined);
|
||||
const setUnavailable = (statusMsg?: string) =>
|
||||
mx
|
||||
};
|
||||
const setUnavailable = (statusMsg?: string) => {
|
||||
const status = readStatus();
|
||||
return mx
|
||||
.setPresence({
|
||||
presence: 'unavailable',
|
||||
...(statusMsg
|
||||
? { status_msg: statusMsg }
|
||||
: storedStatus
|
||||
? { status_msg: storedStatus }
|
||||
: status
|
||||
? { status_msg: status }
|
||||
: {}),
|
||||
})
|
||||
.catch(() => undefined);
|
||||
};
|
||||
const setOffline = () =>
|
||||
mx.setPresence({ presence: 'offline', status_msg: '' }).catch(() => undefined);
|
||||
|
||||
|
||||
@@ -0,0 +1,56 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { MatrixClient } from 'matrix-js-sdk';
|
||||
import { useMatrixClient } from './useMatrixClient';
|
||||
import { useAccountDataCallback } from './useAccountDataCallback';
|
||||
|
||||
const NOTES_KEY = 'io.lotus.user_notes';
|
||||
export const USER_NOTE_MAX_LENGTH = 500;
|
||||
|
||||
type UserNotesContent = Record<string, string>;
|
||||
|
||||
function readNotes(mx: MatrixClient): UserNotesContent {
|
||||
return (mx.getAccountData(NOTES_KEY as any)?.getContent() as UserNotesContent | undefined) ?? {};
|
||||
}
|
||||
|
||||
export function useUserNotes(): {
|
||||
getNote: (userId: string) => string;
|
||||
setNote: (userId: string, note: string) => Promise<void>;
|
||||
} {
|
||||
const mx = useMatrixClient();
|
||||
const [notes, setNotes] = useState<UserNotesContent>(() => readNotes(mx));
|
||||
|
||||
useAccountDataCallback(
|
||||
mx,
|
||||
useCallback(
|
||||
(evt) => {
|
||||
if (evt.getType() === NOTES_KEY) {
|
||||
setNotes(evt.getContent<UserNotesContent>() ?? {});
|
||||
}
|
||||
},
|
||||
[],
|
||||
),
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setNotes(readNotes(mx));
|
||||
}, [mx]);
|
||||
|
||||
const getNote = useCallback((userId: string) => notes[userId] ?? '', [notes]);
|
||||
|
||||
const setNote = useCallback(
|
||||
async (userId: string, note: string) => {
|
||||
const current = readNotes(mx);
|
||||
const updated = { ...current };
|
||||
const trimmed = note.trim().slice(0, USER_NOTE_MAX_LENGTH);
|
||||
if (trimmed) {
|
||||
updated[userId] = trimmed;
|
||||
} else {
|
||||
delete updated[userId];
|
||||
}
|
||||
await (mx as any).setAccountData(NOTES_KEY, updated);
|
||||
},
|
||||
[mx],
|
||||
);
|
||||
|
||||
return { getNote, setNote };
|
||||
}
|
||||
Reference in New Issue
Block a user