187 lines
6.4 KiB
TypeScript
187 lines
6.4 KiB
TypeScript
import { CHAT_PERMISSIONS } from "@hivegrid/chat";
|
|
import { loadChatChannelSettings, type ChatChannelSettingsRecord } from "@hivegrid/chat/channel-settings.server";
|
|
import {
|
|
archiveChatChannel as archiveManagedChatChannel,
|
|
leaveGroupChat,
|
|
removeChatChannelMember as removeManagedChatChannelMember,
|
|
setChatChannelMember,
|
|
setGroupChatPrivacy,
|
|
} from "@hivegrid/chat/lifecycle.server";
|
|
import { buildChatChannelSettingsVM, type ChatChannelSettingsVM } from "@hivegrid/view-models";
|
|
|
|
import { withTenantRLS } from "~/utils/db/.server/tenantRls.server";
|
|
import { projectDetailPath } from "~/utils/integrations/projects/projectRoutePaths";
|
|
|
|
import { requireChatPermission, type ChatRouteContext } from "../routes/chatAccess.server";
|
|
|
|
export type ChatChannelSettingsLabels = Readonly<{
|
|
directMessage: string;
|
|
unnamedGroup: string;
|
|
unnamedProject: string;
|
|
unknownProjectStage: string;
|
|
}>;
|
|
|
|
export async function loadChatChannelSettingsPage(input: {
|
|
readonly context: ChatRouteContext;
|
|
readonly tenantSlug: string;
|
|
readonly chatId: string;
|
|
readonly labels: ChatChannelSettingsLabels;
|
|
}): Promise<ChatChannelSettingsVM> {
|
|
const context = requireChatPermission(input.context, CHAT_PERMISSIONS.readOwnChannels);
|
|
const record = await loadChatChannelSettings({
|
|
tenantId: context.tenantId,
|
|
userId: context.userId,
|
|
permissions: context.permissions,
|
|
chatId: input.chatId,
|
|
});
|
|
const project = record.channel.projectId
|
|
? await loadProjectSummary(context.tenantId, input.tenantSlug, record.channel.projectId, input.labels)
|
|
: null;
|
|
|
|
return buildChatChannelSettingsVM({
|
|
currentUserId: context.userId,
|
|
channel: {
|
|
id: record.channel.id,
|
|
kind: record.channel.kind,
|
|
title: channelTitle(record, context.userId, project?.label ?? null, input.labels),
|
|
description: record.channel.description,
|
|
isPrivate: record.channel.privacy !== "public",
|
|
isArchived: record.channel.archivedAtIso !== null,
|
|
project,
|
|
},
|
|
members: record.members.map((member) => ({
|
|
user: profileInput(member),
|
|
role: member.roleInChat,
|
|
isProjectManaged: member.provenance.isProjectManaged,
|
|
isManualGuest: member.provenance.isManualMember,
|
|
manualRole: member.provenance.manualRoleInChat,
|
|
})),
|
|
tenantUsers: record.candidateUsers.map(profileInput),
|
|
permissions: {
|
|
canManageMembers: record.capabilities.canManageMembers,
|
|
canManageAdmins: record.capabilities.canManageAdmins,
|
|
canChangePrivacy: record.capabilities.canChangePrivacy,
|
|
canLeave: record.capabilities.canLeave,
|
|
canManageManualGuests: record.capabilities.canManageManualGuests,
|
|
canArchive: record.capabilities.canArchive,
|
|
},
|
|
});
|
|
}
|
|
|
|
export async function addChatChannelMember(input: MutationInput & { readonly memberUserId: string }): Promise<void> {
|
|
await setChatChannelMember({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId, memberUserId: input.memberUserId, roleInChat: "member" });
|
|
}
|
|
|
|
export async function changeChatChannelMemberRole(
|
|
input: MutationInput & { readonly memberUserId: string; readonly roleInChat: "admin" | "member" }
|
|
): Promise<void> {
|
|
await setChatChannelMember({
|
|
...repositoryContext(input.context),
|
|
chatId: input.chatId,
|
|
requestId: input.requestId,
|
|
memberUserId: input.memberUserId,
|
|
roleInChat: input.roleInChat,
|
|
});
|
|
}
|
|
|
|
export async function removeChatChannelMember(input: MutationInput & { readonly memberUserId: string }): Promise<void> {
|
|
await removeManagedChatChannelMember({
|
|
...repositoryContext(input.context),
|
|
chatId: input.chatId,
|
|
requestId: input.requestId,
|
|
memberUserId: input.memberUserId,
|
|
});
|
|
}
|
|
|
|
export async function changeChatChannelPrivacy(input: MutationInput & { readonly privacy: "public" | "private" }): Promise<void> {
|
|
await setGroupChatPrivacy({
|
|
...repositoryContext(input.context),
|
|
chatId: input.chatId,
|
|
requestId: input.requestId,
|
|
isPrivate: input.privacy === "private",
|
|
});
|
|
}
|
|
|
|
export async function leaveChatChannel(input: MutationInput): Promise<void> {
|
|
await leaveGroupChat({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId });
|
|
}
|
|
|
|
export async function archiveChatChannel(input: MutationInput): Promise<void> {
|
|
await archiveManagedChatChannel({ ...repositoryContext(input.context), chatId: input.chatId, requestId: input.requestId });
|
|
}
|
|
|
|
type MutationInput = Readonly<{
|
|
context: ChatRouteContext;
|
|
chatId: string;
|
|
requestId: string;
|
|
}>;
|
|
|
|
function repositoryContext(context: ChatRouteContext) {
|
|
return { tenantId: context.tenantId, userId: context.userId, permissions: context.permissions };
|
|
}
|
|
|
|
function profileInput(profile: {
|
|
readonly userId: string;
|
|
readonly displayName: string;
|
|
readonly email: string;
|
|
readonly avatarUrl: string | null;
|
|
}) {
|
|
return {
|
|
userId: profile.userId,
|
|
displayName: profile.displayName,
|
|
email: profile.email,
|
|
avatar: {
|
|
imageUrl: profile.avatarUrl,
|
|
fallbackLabel: fallbackLabel(profile.displayName, profile.email),
|
|
},
|
|
};
|
|
}
|
|
|
|
function fallbackLabel(displayName: string, email: string): string {
|
|
const initials = displayName
|
|
.split(/\s+/u)
|
|
.filter(Boolean)
|
|
.slice(0, 2)
|
|
.map((part) => part.slice(0, 1))
|
|
.join("")
|
|
.toLocaleUpperCase();
|
|
return initials || email.slice(0, 1).toLocaleUpperCase() || "?";
|
|
}
|
|
|
|
function channelTitle(
|
|
record: ChatChannelSettingsRecord,
|
|
currentUserId: string,
|
|
projectLabel: string | null,
|
|
labels: ChatChannelSettingsLabels
|
|
): string {
|
|
if (record.channel.kind === "group") return record.channel.name ?? labels.unnamedGroup;
|
|
if (record.channel.kind === "project") return projectLabel ?? record.channel.name ?? labels.unnamedProject;
|
|
return record.members.find((member) => member.userId !== currentUserId)?.displayName ?? labels.directMessage;
|
|
}
|
|
|
|
async function loadProjectSummary(
|
|
tenantId: string,
|
|
tenantSlug: string,
|
|
projectId: string,
|
|
labels: ChatChannelSettingsLabels
|
|
) {
|
|
const project = await withTenantRLS(tenantId, (tx) =>
|
|
tx.projekt.findFirst({
|
|
where: { tenantId, id: projectId },
|
|
select: {
|
|
id: true,
|
|
projektNummer: true,
|
|
projektname: true,
|
|
stage: { select: { name: true } },
|
|
},
|
|
})
|
|
);
|
|
return {
|
|
id: projectId,
|
|
number: project?.projektNummer ?? projectId,
|
|
label: project?.projektname?.trim() || labels.unnamedProject,
|
|
stageLabel: project?.stage.name?.trim() || labels.unknownProjectStage,
|
|
href: projectDetailPath(tenantSlug, projectId),
|
|
};
|
|
}
|