fix: client perms, @everyone/@channel, docs, unit tests
- usePermissions ORs current user roles + @everyone only (not all server roles) - cache myRolesByServer; load on active server; refresh after self role edit - gate/notify @everyone and @channel; plain @username push; special mention UI - refresh FEATURE_PARITY (DMs exist; drop stale critical gaps) - README production deploy notes dumpster.service - unit tests for permission bits and broadcast mention tokens
This commit is contained in:
@@ -13,7 +13,8 @@ import Picker, { Theme } from 'emoji-picker-react';
|
||||
import { CommandDropdown } from "./CommandDropdown";
|
||||
import { findCommand, SLASH_COMMANDS } from "../lib/slashCommands";
|
||||
import { PollDisplay, CreatePollModal } from "./Poll.tsx";
|
||||
import { MentionDropdown } from "./MentionDropdown";
|
||||
import { MentionDropdown, buildMentionOptions } from "./MentionDropdown";
|
||||
import { usePermissions } from "../lib/usePermissions.ts";
|
||||
import { useReadStatesStore } from "../stores/readStates.ts";
|
||||
import { MessageSearch } from "./MessageSearch";
|
||||
import { ThreadListPanel } from "./ThreadListPanel.tsx";
|
||||
@@ -45,7 +46,7 @@ function formatTime(iso: string): string {
|
||||
}
|
||||
|
||||
function renderContent(content: string, memberUsernames: Set<string>) {
|
||||
const segments: { type: "text" | "mention"; value: string }[] = [];
|
||||
const segments: { type: "text" | "mention"; value: string; special?: boolean }[] = [];
|
||||
const mentionRe = /@([a-zA-Z0-9_.-]+)/g;
|
||||
let last = 0;
|
||||
let match: RegExpExecArray | null;
|
||||
@@ -54,8 +55,13 @@ function renderContent(content: string, memberUsernames: Set<string>) {
|
||||
segments.push({ type: "text", value: content.slice(last, match.index) });
|
||||
}
|
||||
const username = match[1];
|
||||
if (memberUsernames.has(username)) {
|
||||
segments.push({ type: "mention", value: username });
|
||||
const lower = username.toLowerCase();
|
||||
if (lower === "everyone" || lower === "channel" || lower === "here" || memberUsernames.has(username)) {
|
||||
segments.push({
|
||||
type: "mention",
|
||||
value: username,
|
||||
special: lower === "everyone" || lower === "channel" || lower === "here",
|
||||
});
|
||||
} else {
|
||||
segments.push({ type: "text", value: match[0] });
|
||||
}
|
||||
@@ -71,7 +77,10 @@ function renderContent(content: string, memberUsernames: Set<string>) {
|
||||
const nextSeg = segments[idx + 1];
|
||||
const needsSpace = !nextSeg || (nextSeg.type === "text" && !nextSeg.value.startsWith(" "));
|
||||
return (
|
||||
<span key={idx} className="text-gb-aqua">
|
||||
<span
|
||||
key={idx}
|
||||
className={seg.special ? "text-gb-orange font-bold bg-gb-orange/15 px-0.5 rounded-sm" : "text-gb-aqua"}
|
||||
>
|
||||
@{seg.value}{needsSpace ? " " : ""}
|
||||
</span>
|
||||
);
|
||||
@@ -329,6 +338,7 @@ export function ChatArea() {
|
||||
// Humans only for mentions / nickname lookup (bots live in member list separately).
|
||||
const humanMembers = useMemo(() => members.filter((m) => !m.is_bot), [members]);
|
||||
const memberUsernames = useMemo(() => new Set(humanMembers.map((m) => m.username)), [humanMembers]);
|
||||
const { canMentionEveryone } = usePermissions(activeServerId);
|
||||
const markRead = useReadStatesStore((s) => s.markRead);
|
||||
const readStates = useReadStatesStore((s) => s.states);
|
||||
|
||||
@@ -555,10 +565,7 @@ export function ChatArea() {
|
||||
if (!isDropdownOpen) return;
|
||||
|
||||
const itemCount = mq !== null
|
||||
? humanMembers.filter((m) =>
|
||||
m.username.toLowerCase().includes(mq.toLowerCase()) ||
|
||||
m.display_name?.toLowerCase().includes(mq.toLowerCase())
|
||||
).slice(0, 6).length
|
||||
? buildMentionOptions(mq, humanMembers, canMentionEveryone).length
|
||||
: cq !== null
|
||||
? SLASH_COMMANDS.filter((c) => c.name.startsWith(cq.toLowerCase())).slice(0, 8).length
|
||||
: 0;
|
||||
@@ -575,13 +582,14 @@ export function ChatArea() {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
if (mq !== null) {
|
||||
const q = mq.toLowerCase();
|
||||
const filtered = humanMembers.filter((m) =>
|
||||
m.username.toLowerCase().includes(q) ||
|
||||
m.display_name?.toLowerCase().includes(q)
|
||||
).slice(0, 6);
|
||||
if (filtered[di]) {
|
||||
handleMentionSelect(filtered[di].username);
|
||||
const options = buildMentionOptions(mq, humanMembers, canMentionEveryone);
|
||||
const selected = options[di];
|
||||
if (selected) {
|
||||
if (selected.kind === "special") {
|
||||
handleMentionSelect(selected.label);
|
||||
} else {
|
||||
handleMentionSelect(selected.member.username);
|
||||
}
|
||||
}
|
||||
} else if (cq !== null) {
|
||||
const q = cq.toLowerCase();
|
||||
@@ -620,7 +628,7 @@ export function ChatArea() {
|
||||
};
|
||||
window.addEventListener('keydown', handler);
|
||||
return () => window.removeEventListener('keydown', handler);
|
||||
}, [humanMembers, currentUser, activeChannelId, sendMessage, replyToMessage, handleMentionSelect]);
|
||||
}, [humanMembers, canMentionEveryone, currentUser, activeChannelId, sendMessage, replyToMessage, handleMentionSelect]);
|
||||
|
||||
const handleSubmit = useCallback(async () => {
|
||||
if (mentionQuery !== null || commandQuery !== null) return;
|
||||
@@ -904,7 +912,13 @@ export function ChatArea() {
|
||||
)}
|
||||
<div className="p-3 relative">
|
||||
{mentionQuery !== null && (
|
||||
<MentionDropdown query={mentionQuery} members={humanMembers} selectedIndex={dropdownIndex} onSelect={handleMentionSelect} />
|
||||
<MentionDropdown
|
||||
query={mentionQuery}
|
||||
members={humanMembers}
|
||||
selectedIndex={dropdownIndex}
|
||||
onSelect={handleMentionSelect}
|
||||
canMentionEveryone={canMentionEveryone}
|
||||
/>
|
||||
)}
|
||||
{commandQuery !== null && (
|
||||
<CommandDropdown
|
||||
|
||||
Reference in New Issue
Block a user