From 24a99900b1bb0b7b30f10a780eec54e1ae94c2a0 Mon Sep 17 00:00:00 2001 From: Gabriel Garcia Date: Mon, 6 Oct 2025 20:03:49 -0400 Subject: [PATCH] fix: role logic, autofocus - MessageInput: autofocus on reply - usePermission: new hook to start working out permissions and actions - fix all build blocking errors --- .../src/components/layout/ChannelSidebar.tsx | 6 +- .../src/components/layout/MemberList.tsx | 242 +++++++++++------- .../components/message/MessageComponent.tsx | 11 +- .../src/components/message/MessageInput.tsx | 21 +- concord-client/src/hooks/usePermissions.ts | 108 ++++++++ concord-client/src/pages/ChatPage.tsx | 11 +- 6 files changed, 274 insertions(+), 125 deletions(-) create mode 100644 concord-client/src/hooks/usePermissions.ts diff --git a/concord-client/src/components/layout/ChannelSidebar.tsx b/concord-client/src/components/layout/ChannelSidebar.tsx index 3968960..31e6986 100644 --- a/concord-client/src/components/layout/ChannelSidebar.tsx +++ b/concord-client/src/components/layout/ChannelSidebar.tsx @@ -73,11 +73,11 @@ const ChannelSidebar: React.FC = () => { {/* Bottom Actions */}
-
+
- + +
+
+ {isOwner && ( + + )} + {/* Display Shield for Admins and Mods, not for Members */} + {!isOwner && effectiveMemberRolePriority > 1 && ( + + )} + + {member.nickname || member.username} + +
+ {member.bio && ( +
+ {member.bio} +
+ )} +
+
+ ); }; const MemberList: React.FC = () => { - const { instanceId } = useParams(); + const { instanceId } = useParams<{ instanceId: string }>(); const { data: members, isLoading } = useInstanceMembers(instanceId); const { user: currentUser } = useAuthStore(); - const currentUserRole = React.useMemo(() => { - if (!currentUser || !instanceId) return "member"; - if (currentUser.admin) return "admin"; + const currentUserRoleInfo = React.useMemo(() => { + if (!currentUser || !instanceId) { + return { role: "member", priority: 1, name: "Member", color: null }; + } - const userRole = currentUser.roles.find( - (role) => role.instanceId === instanceId, - ); - return userRole?.role || "member"; + // If the current user is a global admin, they are effectively an admin of any instance. + if (currentUser.admin) { + return { role: "admin", priority: 3, name: "Admin", color: "#ff6b6b" }; + } + + const role = getUserRoleForInstance(currentUser.roles, instanceId); + return { ...getRoleInfo(role), role: role }; }, [currentUser, instanceId]); if (!instanceId) { @@ -140,17 +166,26 @@ const MemberList: React.FC = () => { if (!members || members.length === 0) { return (
-
No members
+
No members found
); } - // Group members by role + // Group members by their role for the current instance. const groupedMembers = members.reduce( (acc, member) => { - const userRole = - member.roles.find((r) => r.instanceId === instanceId)?.role || "member"; - const roleInfo = getRoleInfo(userRole); + // Determine the effective role for this instance. + let effectiveRoleName = getUserRoleForInstance( + member.roles as Role[], + instanceId, + ); + + // Global admin is instance admin + if (member.admin && effectiveRoleName !== "admin") { + effectiveRoleName = "admin"; + } + + const roleInfo = getRoleInfo(effectiveRoleName); if (!acc[roleInfo.name]) { acc[roleInfo.name] = []; @@ -161,11 +196,11 @@ const MemberList: React.FC = () => { {} as Record, ); - // Sort role groups by priority (admin > mod > member) - const sortedRoleGroups = Object.entries(groupedMembers).sort( - ([roleNameA], [roleNameB]) => { - const priorityA = getRoleInfo(roleNameA.toLowerCase())?.priority || 1; - const priorityB = getRoleInfo(roleNameB.toLowerCase())?.priority || 1; + // Get all unique role names present and sort them by priority. + const sortedRoleNames = Object.keys(groupedMembers).sort( + (roleNameA, roleNameB) => { + const priorityA = getRoleInfo(roleNameA).priority; + const priorityB = getRoleInfo(roleNameB).priority; return priorityB - priorityA; }, ); @@ -174,40 +209,53 @@ const MemberList: React.FC = () => {
{/* Header */}
- -

- {members.length} Members +

+ +

+ Members +

+
+

+ {members.length}

{/* Member List */}
- {sortedRoleGroups.map(([roleName, roleMembers]) => ( -
- {/* Role Header */} -
-

- {roleName} — {roleMembers.length} -

-
+ {sortedRoleNames.map((roleName) => { + const roleMembers = groupedMembers[roleName]; + // Sort members within each role group alphabetically by username. + const sortedMembers = roleMembers.sort((a, b) => + (a.nickname || a.username).localeCompare( + b.nickname || b.username, + ), + ); - {/* Role Members */} -
- {roleMembers - .sort((a, b) => a.username.localeCompare(b.username)) - .map((member) => ( + return ( +
+ {/* Role Header */} +
+

+ {roleName} — {roleMembers.length} +

+
+ + {/* Role Members */} +
+ {sortedMembers.map((member) => ( ))} +
-
- ))} + ); + })}
diff --git a/concord-client/src/components/message/MessageComponent.tsx b/concord-client/src/components/message/MessageComponent.tsx index b0b2e57..54ca920 100644 --- a/concord-client/src/components/message/MessageComponent.tsx +++ b/concord-client/src/components/message/MessageComponent.tsx @@ -16,7 +16,7 @@ import { useState } from "react"; interface MessageUser { id: string; username?: string; - nickName?: string | null; + nickname?: string | null; picture?: string | null; } @@ -64,8 +64,8 @@ export const MessageComponent: React.FC = ({ const { mode } = useTheme(); // Get username with fallback - const username = user.username || user.userName || "Unknown User"; - const displayName = user.nickname || user.nickName || username; + const username = user.username || user.username || "Unknown User"; + const displayName = user.nickname || user.nickname || username; const isDeleted = message.deleted; @@ -109,10 +109,7 @@ export const MessageComponent: React.FC = ({
- {replyToUser.nickname || - replyToUser.nickName || - replyToUser.username || - replyToUser.userName} + {replyToUser.nickname || replyToUser.username} {replyTo.text.replace(/```[\s\S]*?```/g, "[code]")} diff --git a/concord-client/src/components/message/MessageInput.tsx b/concord-client/src/components/message/MessageInput.tsx index 02a2660..734b1be 100644 --- a/concord-client/src/components/message/MessageInput.tsx +++ b/concord-client/src/components/message/MessageInput.tsx @@ -1,12 +1,14 @@ +// src/components/message/MessageInput.tsx + import { Message } from "@/lib/api-client"; -import { useState, useRef, useEffect } from "react"; +import { useState, useRef, useEffect } from "react"; // Keep useRef import { useSendMessage } from "@/hooks/useMessages"; import { Button } from "@/components/ui/button"; interface MessageUser { id: string; username?: string; - nickName?: string | null; + nickname?: string | null; picture?: string | null; } interface MessageInputProps { @@ -15,7 +17,6 @@ interface MessageInputProps { replyingTo?: Message | null; onCancelReply?: () => void; replyingToUser: MessageUser | null; - messageInputRef: React.RefObject; } export const MessageInput: React.FC = ({ @@ -24,22 +25,26 @@ export const MessageInput: React.FC = ({ replyingTo, onCancelReply, replyingToUser, - messageInputRef, }) => { const [content, setContent] = useState(""); - const textareaRef = messageInputRef; + const textareaRef = useRef(null); const formRef = useRef(null); // Use the API hook for sending messages const sendMessageMutation = useSendMessage(); - // Auto-resize textarea + // Auto-resize textarea and focus when replying useEffect(() => { if (textareaRef.current) { textareaRef.current.style.height = "auto"; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; + + // Focus the input when a reply is initiated + if (replyingTo) { + textareaRef.current.focus(); + } } - }, [content]); + }, [content, replyingTo]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -73,7 +78,7 @@ export const MessageInput: React.FC = ({
- {replyingToUser.nickname || replyingToUser.userName} + {replyingToUser.nickname || replyingToUser.username}
)}