diff --git a/docs/core-checklist-status.md b/docs/core-checklist-status.md index 30334b8..a142513 100644 --- a/docs/core-checklist-status.md +++ b/docs/core-checklist-status.md @@ -31,7 +31,7 @@ Legend: 22. Text Formatting - `PARTIAL` (bold/italic/underline/spoiler/mono/links + strikethrough + quote/code block; toolbar still evolving) 23. Groups - `PARTIAL` (create/add/remove/invite link; join-by-invite and invite permissions covered by integration tests; members API now returns profile fields (`username/name/avatar_url`) for richer moderation UI; advanced moderation still partial) 24. Roles - `DONE` (owner/admin/member) -25. Admin Rights - `PARTIAL` (delete/pin/edit info + explicit ban APIs for groups/channels including ban list endpoint; web Chat Info now shows `Banned users` with `Unban` action for owner/admin; integration tests cover channel member read-only, channel admin full-delete, channel message delete-for-all permissions, group profile edit permissions, owner-only role management rules, and admin-visible/member-forbidden ban-list access; remaining UX moderation tools limited) +25. Admin Rights - `PARTIAL` (delete/pin/edit info + explicit ban APIs for groups/channels including ban list endpoint; web Chat Info now shows searchable `Banned users` with right-click `Unban` action for owner/admin, plus member search and invite-link copy/regenerate actions; integration tests cover channel member read-only, channel admin full-delete, channel message delete-for-all permissions, group profile edit permissions, owner-only role management rules, and admin-visible/member-forbidden ban-list access; remaining UX moderation tools limited) 26. Channels - `PARTIAL` (create/post/edit/delete/subscribe/unsubscribe; integration tests now also cover invite-link permissions (member forbidden, admin allowed); UX edge-cases still polishing) 27. Channel Types - `DONE` (public/private) 28. Notifications - `PARTIAL` (browser notifications + mute/settings; chat mute is propagated in chat list payload, honored by web realtime notifications with mention override, and mute toggle now syncs instantly in chat store; backend now emits `chat_updated` after notification mute/unmute for cross-tab consistency; no mobile push infra) diff --git a/web/src/components/ChatInfoPanel.tsx b/web/src/components/ChatInfoPanel.tsx index e02c104..7e09b53 100644 --- a/web/src/components/ChatInfoPanel.tsx +++ b/web/src/components/ChatInfoPanel.tsx @@ -51,6 +51,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { const [chatAvatarUploading, setChatAvatarUploading] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [searchResults, setSearchResults] = useState([]); + const [memberFilter, setMemberFilter] = useState(""); + const [bannedFilter, setBannedFilter] = useState(""); const [muted, setMuted] = useState(false); const [savingMute, setSavingMute] = useState(false); const [counterpartBlocked, setCounterpartBlocked] = useState(false); @@ -61,6 +63,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { const [linkItems, setLinkItems] = useState>([]); const [attachmentCtx, setAttachmentCtx] = useState<{ x: number; y: number; url: string; messageId: number } | null>(null); const [memberCtx, setMemberCtx] = useState<{ x: number; y: number; member: ChatMember } | null>(null); + const [banCtx, setBanCtx] = useState<{ x: number; y: number; ban: ChatBan } | null>(null); const [bans, setBans] = useState([]); const [bannedUsers, setBannedUsers] = useState>({}); const [attachmentsTab, setAttachmentsTab] = useState<"all" | "photos" | "videos" | "audio" | "links" | "voice">("all"); @@ -99,6 +102,34 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { [attachments] ); const allAttachmentItems = useMemo(() => [...attachments].sort((a, b) => b.id - a.id), [attachments]); + const filteredMembers = useMemo(() => { + const query = memberFilter.trim().toLowerCase(); + if (!query) { + return members; + } + return members.filter((member) => { + const user = memberUsers[member.user_id]; + return ( + (user?.name ?? "").toLowerCase().includes(query) || + (user?.username ?? "").toLowerCase().includes(query) || + String(member.user_id).includes(query) + ); + }); + }, [memberFilter, members, memberUsers]); + const filteredBans = useMemo(() => { + const query = bannedFilter.trim().toLowerCase(); + if (!query) { + return bans; + } + return bans.filter((ban) => { + const user = bannedUsers[ban.user_id]; + return ( + (user?.name ?? "").toLowerCase().includes(query) || + (user?.username ?? "").toLowerCase().includes(query) || + String(ban.user_id).includes(query) + ); + }); + }, [bannedFilter, bans, bannedUsers]); async function refreshMembers(targetChatId: number): Promise { const nextMembers = await listChatMembers(targetChatId); @@ -239,6 +270,8 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { setBannedUsers({}); setSearchQuery(""); setSearchResults([]); + setMemberFilter(""); + setBannedFilter(""); }, [chatId, open]); useEffect(() => { @@ -279,6 +312,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { onClick={() => { setAttachmentCtx(null); setMemberCtx(null); + setBanCtx(null); onClose(); }} > @@ -287,6 +321,7 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { onClick={(e) => { setAttachmentCtx(null); setMemberCtx(null); + setBanCtx(null); e.stopPropagation(); }} > @@ -439,7 +474,42 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { > Create invite link - {inviteLink ?

{inviteLink}

: null} + {inviteLink ? ( + <> +

{inviteLink}

+
+ + +
+ + ) : null} ) : null} @@ -447,8 +517,14 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { {showMembersSection ? (

Members ({members.length})

+ setMemberFilter(e.target.value)} + placeholder="Search members" + value={memberFilter} + />
- {members.map((member) => { + {filteredMembers.map((member) => { const user = memberUsers[member.user_id]; const isSelf = member.user_id === me?.id; const canOpenMemberMenu = @@ -550,33 +626,37 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) { {showMembersSection && canManageMembers ? (

Banned users ({bans.length})

+ setBannedFilter(e.target.value)} + placeholder="Search banned users" + value={bannedFilter} + /> {bans.length === 0 ?

No banned users

: null}
- {bans.map((ban) => { + {filteredBans.map((ban) => { const user = bannedUsers[ban.user_id]; return ( -
+ -
+ Banned + ); })}
@@ -925,6 +1005,39 @@ export function ChatInfoPanel({ chatId, open, onClose }: Props) {
) : null} + {banCtx ? ( +
event.stopPropagation()} + > + + +
+ ) : null} {mediaViewer ? (