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:
2026-07-15 20:56:53 -04:00
parent a277c78e2c
commit 3c7b8278ce
13 changed files with 470 additions and 176 deletions
+2 -1
View File
@@ -55,7 +55,8 @@ export const PERMISSION_LABELS: Record<PermissionKey, string> = {
};
export function hasPermission(perms: number, flag: number): boolean {
return (perms & flag) !== 0;
// Match backend permissions.Has: all required bits must be present.
return (perms & flag) === flag;
}
export interface ChannelOverride {
+57 -9
View File
@@ -27,10 +27,13 @@ export interface UpdateRoleData {
interface RoleState {
roles: Role[];
/** Assigned roles for the current user, keyed by server id. Does not include @everyone. */
myRolesByServer: Record<string, Role[]>;
loading: boolean;
error: string | null;
fetchRoles: (serverId: string) => Promise<void>;
fetchMyRoles: (serverId: string, userId: string) => Promise<void>;
createRole: (serverId: string, data: CreateRoleData) => Promise<Role>;
updateRole: (serverId: string, roleId: string, data: UpdateRoleData) => Promise<Role>;
deleteRole: (serverId: string, roleId: string) => Promise<void>;
@@ -40,6 +43,7 @@ interface RoleState {
export const useRoleStore = create<RoleState>((set) => ({
roles: [],
myRolesByServer: {},
loading: false,
error: null,
@@ -47,7 +51,14 @@ export const useRoleStore = create<RoleState>((set) => ({
set({ loading: true, error: null });
try {
const roles = await api.get<Role[]>(`/servers/${serverId}/roles`);
set({ roles: roles.sort((a, b) => b.position - a.position), loading: false });
set((state) => ({
// Keep roles from other servers if mixed; replace same-server entries.
roles: [
...state.roles.filter((r) => r.server_id !== serverId),
...(Array.isArray(roles) ? roles : []),
].sort((a, b) => b.position - a.position),
loading: false,
}));
} catch (error) {
set({
loading: false,
@@ -56,6 +67,25 @@ export const useRoleStore = create<RoleState>((set) => ({
}
},
fetchMyRoles: async (serverId, userId) => {
try {
const roles = await api.get<Role[]>(`/servers/${serverId}/members/${userId}/roles`);
set((state) => ({
myRolesByServer: {
...state.myRolesByServer,
[serverId]: Array.isArray(roles) ? roles : [],
},
}));
} catch {
set((state) => ({
myRolesByServer: {
...state.myRolesByServer,
[serverId]: [],
},
}));
}
},
createRole: async (serverId, data) => {
set({ loading: true, error: null });
try {
@@ -78,10 +108,19 @@ export const useRoleStore = create<RoleState>((set) => ({
set({ loading: true, error: null });
try {
const role = await api.patch<Role>(`/servers/${serverId}/roles/${roleId}`, data);
set((state) => ({
roles: state.roles.map((r) => (r.id === roleId ? role : r)).sort((a, b) => b.position - a.position),
loading: false,
}));
set((state) => {
const myRoles = state.myRolesByServer[serverId];
const nextMy = myRoles
? myRoles.map((r) => (r.id === roleId ? role : r))
: myRoles;
return {
roles: state.roles.map((r) => (r.id === roleId ? role : r)).sort((a, b) => b.position - a.position),
myRolesByServer: nextMy
? { ...state.myRolesByServer, [serverId]: nextMy }
: state.myRolesByServer,
loading: false,
};
});
return role;
} catch (error) {
set({
@@ -96,10 +135,19 @@ export const useRoleStore = create<RoleState>((set) => ({
set({ loading: true, error: null });
try {
await api.delete(`/servers/${serverId}/roles/${roleId}`);
set((state) => ({
roles: state.roles.filter((r) => r.id !== roleId),
loading: false,
}));
set((state) => {
const myRoles = state.myRolesByServer[serverId];
return {
roles: state.roles.filter((r) => r.id !== roleId),
myRolesByServer: myRoles
? {
...state.myRolesByServer,
[serverId]: myRoles.filter((r) => r.id !== roleId),
}
: state.myRolesByServer,
loading: false,
};
});
} catch (error) {
set({
loading: false,