Files
HiveGrid-Wireframes/03 Quellen und Referenzen/hivegrid-cloud/apps/web/app/modules/chat/services/ChatChannelSettingsService.server.ts
T

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),
};
}