@studious-lms/server 1.4.3 → 1.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/settings.json +3 -0
- package/dist/index.js +4 -18
- package/dist/index.js.map +1 -1
- package/dist/models/conversation.d.ts +22 -0
- package/dist/models/conversation.d.ts.map +1 -1
- package/dist/models/conversation.js +13 -2
- package/dist/models/conversation.js.map +1 -1
- package/dist/models/labChat.d.ts +3 -0
- package/dist/models/labChat.d.ts.map +1 -1
- package/dist/models/message.d.ts +2 -0
- package/dist/models/message.d.ts.map +1 -1
- package/dist/models/newtonChat.d.ts +1 -0
- package/dist/models/newtonChat.d.ts.map +1 -1
- package/dist/models/worksheet.d.ts +1 -0
- package/dist/models/worksheet.d.ts.map +1 -1
- package/dist/models/worksheet.js +2 -2
- package/dist/models/worksheet.js.map +1 -1
- package/dist/pipelines/pdfToWorksheet.d.ts +12 -0
- package/dist/pipelines/pdfToWorksheet.d.ts.map +1 -0
- package/dist/pipelines/pdfToWorksheet.js +155 -0
- package/dist/pipelines/pdfToWorksheet.js.map +1 -0
- package/dist/routers/_app.d.ts +252 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/_app.js +4 -2
- package/dist/routers/_app.js.map +1 -1
- package/dist/routers/conversation.d.ts +14 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +6 -3
- package/dist/routers/conversation.js.map +1 -1
- package/dist/routers/labChat.d.ts +2 -0
- package/dist/routers/labChat.d.ts.map +1 -1
- package/dist/routers/search.d.ts +41 -0
- package/dist/routers/search.d.ts.map +1 -0
- package/dist/routers/search.js +15 -0
- package/dist/routers/search.js.map +1 -0
- package/dist/routers/worksheet.d.ts +71 -0
- package/dist/routers/worksheet.d.ts.map +1 -1
- package/dist/routers/worksheet.js +110 -3
- package/dist/routers/worksheet.js.map +1 -1
- package/dist/services/conversation.d.ts +8 -0
- package/dist/services/conversation.d.ts.map +1 -1
- package/dist/services/conversation.js +24 -6
- package/dist/services/conversation.js.map +1 -1
- package/dist/services/labChat.d.ts +2 -0
- package/dist/services/labChat.d.ts.map +1 -1
- package/dist/services/message.d.ts.map +1 -1
- package/dist/services/message.js +9 -2
- package/dist/services/message.js.map +1 -1
- package/dist/services/search.d.ts +21 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +352 -0
- package/dist/services/search.js.map +1 -0
- package/dist/services/worksheet.d.ts +21 -0
- package/dist/services/worksheet.d.ts.map +1 -1
- package/dist/services/worksheet.js +50 -2
- package/dist/services/worksheet.js.map +1 -1
- package/dist/utils/inference.d.ts +2 -1
- package/dist/utils/inference.d.ts.map +1 -1
- package/dist/utils/inference.js +53 -5
- package/dist/utils/inference.js.map +1 -1
- package/package.json +1 -1
- package/prisma/migrations/20260417083000_add_conversation_member_hidden_at/migration.sql +2 -0
- package/prisma/schema.prisma +1 -0
- package/src/index.ts +4 -19
- package/src/models/conversation.ts +12 -0
- package/src/models/worksheet.ts +1 -0
- package/src/pipelines/pdfToWorksheet.ts +177 -0
- package/src/routers/_app.ts +2 -0
- package/src/routers/conversation.ts +7 -0
- package/src/routers/search.ts +14 -0
- package/src/routers/worksheet.ts +143 -0
- package/src/services/conversation.ts +27 -3
- package/src/services/message.ts +8 -0
- package/src/services/search.ts +397 -0
- package/src/services/worksheet.ts +61 -0
- package/src/utils/inference.ts +77 -7
- package/tests/pipelines/pdfToWorksheet.test.ts +176 -0
- package/tests/routers/conversation.test.ts +114 -2
- package/tests/routers/search.test.ts +109 -0
- package/tests/utils/inference.test.ts +37 -0
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PDF URL → worksheet: send a remote PDF URL to structured inference,
|
|
3
|
+
* then persist a Worksheet and WorksheetQuestion rows.
|
|
4
|
+
*/
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { ResponseInput } from "openai/resources/responses/responses.js";
|
|
7
|
+
import { prisma } from "../lib/prisma.js";
|
|
8
|
+
import { inference } from "../utils/inference.js";
|
|
9
|
+
import { logger } from "../utils/logger.js";
|
|
10
|
+
|
|
11
|
+
/** Restrict remote fetches to Google-hosted object URLs. */
|
|
12
|
+
function isProbablyGoogleCloudUrl(url: string): boolean {
|
|
13
|
+
try {
|
|
14
|
+
const host = new URL(url).hostname;
|
|
15
|
+
return (
|
|
16
|
+
host.endsWith("storage.googleapis.com") ||
|
|
17
|
+
host.endsWith("googleusercontent.com") ||
|
|
18
|
+
host.includes("storage.cloud.google.com")
|
|
19
|
+
);
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function getFilenameFromUrl(url: string): string {
|
|
26
|
+
try {
|
|
27
|
+
const parsed = new URL(url);
|
|
28
|
+
const lastSegment = parsed.pathname.split("/").filter(Boolean).pop();
|
|
29
|
+
return lastSegment && lastSegment.includes(".") ? lastSegment : "document.pdf";
|
|
30
|
+
} catch {
|
|
31
|
+
return "document.pdf";
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function sanitizeUrlForLogs(url: string): string {
|
|
36
|
+
try {
|
|
37
|
+
const parsed = new URL(url);
|
|
38
|
+
return `${parsed.origin}${parsed.pathname}`;
|
|
39
|
+
} catch {
|
|
40
|
+
return 'invalid-url';
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** LLM output shape (validated by inference). */
|
|
45
|
+
const worksheetDraftSchema = z.object({
|
|
46
|
+
title: z.string(),
|
|
47
|
+
questions: z
|
|
48
|
+
.array(
|
|
49
|
+
z.object({
|
|
50
|
+
type: z.enum([
|
|
51
|
+
"MULTIPLE_CHOICE",
|
|
52
|
+
"TRUE_FALSE",
|
|
53
|
+
"SHORT_ANSWER",
|
|
54
|
+
"LONG_ANSWER",
|
|
55
|
+
"MATH_EXPRESSION",
|
|
56
|
+
"ESSAY",
|
|
57
|
+
]),
|
|
58
|
+
question: z.string(),
|
|
59
|
+
answer: z.string(),
|
|
60
|
+
options: z
|
|
61
|
+
.array(
|
|
62
|
+
z.object({
|
|
63
|
+
id: z.string(),
|
|
64
|
+
text: z.string(),
|
|
65
|
+
isCorrect: z.boolean(),
|
|
66
|
+
})
|
|
67
|
+
)
|
|
68
|
+
.optional()
|
|
69
|
+
.default([]),
|
|
70
|
+
markScheme: z
|
|
71
|
+
.array(
|
|
72
|
+
z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
points: z.number().transform((value) => Math.round(value)),
|
|
75
|
+
description: z.string(),
|
|
76
|
+
})
|
|
77
|
+
)
|
|
78
|
+
.optional()
|
|
79
|
+
.default([]),
|
|
80
|
+
points: z.number().optional().default(0),
|
|
81
|
+
order: z.number().transform((value) => Math.round(value)),
|
|
82
|
+
})
|
|
83
|
+
)
|
|
84
|
+
.default([]),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
type WorksheetDraft = z.infer<typeof worksheetDraftSchema>;
|
|
88
|
+
|
|
89
|
+
const schemaInstructions = `Return ONLY valid JSON matching this schema:
|
|
90
|
+
{
|
|
91
|
+
"title": string,
|
|
92
|
+
"questions": [
|
|
93
|
+
{
|
|
94
|
+
"type": "MULTIPLE_CHOICE" | "TRUE_FALSE" | "SHORT_ANSWER" | "LONG_ANSWER" | "MATH_EXPRESSION" | "ESSAY",
|
|
95
|
+
"question": string,
|
|
96
|
+
"answer": string,
|
|
97
|
+
"options": [ { "id": string, "text": string, "isCorrect": boolean } ],
|
|
98
|
+
"markScheme": [ { "id": string, "points": number, "description": string } ],
|
|
99
|
+
"points": number,
|
|
100
|
+
"order": number
|
|
101
|
+
}
|
|
102
|
+
]
|
|
103
|
+
}
|
|
104
|
+
Rules: "questions" is an array ([] if none). Non-MCQ "options" can be []. Use "order" 1,2,3,… in reading order. No markdown or extra keys.
|
|
105
|
+
If no answer is provided for a multiple-choice or true/false question, determine the answer from the options.`;
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Sends a PDF URL to the model for remote retrieval, extracts a worksheet via
|
|
109
|
+
* structured inference, and inserts worksheet + questions in one DB transaction.
|
|
110
|
+
*/
|
|
111
|
+
export async function pdfToWorksheet(gcsUrl: string, classId: string) {
|
|
112
|
+
const sanitizedUrl = sanitizeUrlForLogs(gcsUrl);
|
|
113
|
+
|
|
114
|
+
if (!isProbablyGoogleCloudUrl(gcsUrl)) {
|
|
115
|
+
logger.warn("pdfToWorksheet: rejected unsupported PDF URL", {
|
|
116
|
+
sanitizedUrl,
|
|
117
|
+
});
|
|
118
|
+
throw new Error("Only Google Cloud Storage URLs are supported");
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const filename = getFilenameFromUrl(gcsUrl);
|
|
122
|
+
const prompt = `Read the attached PDF and convert it into a single worksheet JSON object.\n\n${schemaInstructions}`;
|
|
123
|
+
|
|
124
|
+
// Force Responses API input types so we can pass a remote PDF via `file_url`.
|
|
125
|
+
const input: ResponseInput = [
|
|
126
|
+
{
|
|
127
|
+
role: "user",
|
|
128
|
+
content: [
|
|
129
|
+
{ type: "input_text", text: prompt },
|
|
130
|
+
{ type: "input_file", file_url: gcsUrl },
|
|
131
|
+
],
|
|
132
|
+
},
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
const draft = await inference<WorksheetDraft>(
|
|
136
|
+
input,
|
|
137
|
+
worksheetDraftSchema,
|
|
138
|
+
{
|
|
139
|
+
reason: 'pdf_to_worksheet_draft',
|
|
140
|
+
metadata: {
|
|
141
|
+
classId,
|
|
142
|
+
filename,
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if (!draft) {
|
|
148
|
+
logger.warn("pdfToWorksheet: no model output", { filename });
|
|
149
|
+
return null;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
return prisma.$transaction(async (tx) => {
|
|
153
|
+
const worksheet = await tx.worksheet.create({
|
|
154
|
+
data: { classId, name: draft.title },
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
if (draft.questions.length > 0) {
|
|
158
|
+
await tx.worksheetQuestion.createMany({
|
|
159
|
+
data: draft.questions.map((question) => ({
|
|
160
|
+
worksheetId: worksheet.id,
|
|
161
|
+
type: question.type,
|
|
162
|
+
question: question.question,
|
|
163
|
+
answer: question.answer,
|
|
164
|
+
points: Math.round(question.points ?? 0),
|
|
165
|
+
options: question.options ?? [],
|
|
166
|
+
markScheme: (question.markScheme ?? []).map((item) => ({
|
|
167
|
+
...item,
|
|
168
|
+
points: Math.round(item.points),
|
|
169
|
+
})),
|
|
170
|
+
order: Math.round(question.order),
|
|
171
|
+
})),
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return worksheet;
|
|
176
|
+
});
|
|
177
|
+
}
|
package/src/routers/_app.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { marketingRouter } from "./marketing.js";
|
|
|
21
21
|
import { worksheetRouter } from "./worksheet.js";
|
|
22
22
|
import { commentRouter } from "./comment.js";
|
|
23
23
|
import { studentProgressRouter } from "./studentProgress.js";
|
|
24
|
+
import { searchRouter } from "./search.js";
|
|
24
25
|
|
|
25
26
|
export const appRouter = createTRPCRouter({
|
|
26
27
|
class: classRouter,
|
|
@@ -43,6 +44,7 @@ export const appRouter = createTRPCRouter({
|
|
|
43
44
|
worksheet: worksheetRouter,
|
|
44
45
|
comment: commentRouter,
|
|
45
46
|
studentProgress: studentProgressRouter,
|
|
47
|
+
search: searchRouter,
|
|
46
48
|
});
|
|
47
49
|
|
|
48
50
|
// Export type router type definition
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createConversationRecord,
|
|
6
6
|
getConversation,
|
|
7
7
|
addMemberToConversation,
|
|
8
|
+
hideConversation,
|
|
8
9
|
removeMemberFromConversation,
|
|
9
10
|
} from "../services/conversation.js";
|
|
10
11
|
|
|
@@ -36,6 +37,12 @@ export const conversationRouter = createTRPCRouter({
|
|
|
36
37
|
getConversation(ctx.user!.id, input.conversationId)
|
|
37
38
|
),
|
|
38
39
|
|
|
40
|
+
hide: protectedProcedure
|
|
41
|
+
.input(z.object({ conversationId: z.string() }))
|
|
42
|
+
.mutation(({ ctx, input }) =>
|
|
43
|
+
hideConversation(ctx.user!.id, input.conversationId)
|
|
44
|
+
),
|
|
45
|
+
|
|
39
46
|
addMember: protectedProcedure
|
|
40
47
|
.input(
|
|
41
48
|
z.object({
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
|
+
import { searchGlobalClasses } from "../services/search.js";
|
|
4
|
+
|
|
5
|
+
export const searchRouter = createTRPCRouter({
|
|
6
|
+
global: protectedProcedure
|
|
7
|
+
.input(
|
|
8
|
+
z.object({
|
|
9
|
+
query: z.string().max(80).optional(),
|
|
10
|
+
limit: z.number().int().min(1).max(30).optional(),
|
|
11
|
+
})
|
|
12
|
+
)
|
|
13
|
+
.query(({ ctx, input }) => searchGlobalClasses(ctx.user!.id, input)),
|
|
14
|
+
});
|
package/src/routers/worksheet.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { TRPCError } from "@trpc/server";
|
|
2
|
+
import { prisma } from "../lib/prisma.js";
|
|
3
|
+
import {
|
|
4
|
+
confirmDirectUpload,
|
|
5
|
+
createDirectUploadFiles,
|
|
6
|
+
updateUploadProgress,
|
|
7
|
+
} from "../lib/fileUpload.js";
|
|
2
8
|
import {
|
|
3
9
|
createTRPCRouter,
|
|
4
10
|
protectedClassMemberProcedure,
|
|
@@ -13,6 +19,8 @@ import {
|
|
|
13
19
|
updateWorksheetRecord,
|
|
14
20
|
deleteWorksheetRecord,
|
|
15
21
|
createWorksheetRecord,
|
|
22
|
+
createWorksheetFromPdf,
|
|
23
|
+
createWorksheetFromUploadedPdf,
|
|
16
24
|
addQuestionToWorksheet,
|
|
17
25
|
reorderWorksheetQuestions,
|
|
18
26
|
updateWorksheetQuestionRecord,
|
|
@@ -25,6 +33,40 @@ import {
|
|
|
25
33
|
addCommentToResponse,
|
|
26
34
|
} from "../services/worksheet.js";
|
|
27
35
|
|
|
36
|
+
const directFileSchema = z.object({
|
|
37
|
+
name: z.string(),
|
|
38
|
+
type: z.string(),
|
|
39
|
+
size: z.number(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const pdfFileSchema = directFileSchema.superRefine((file, ctx) => {
|
|
43
|
+
const isPdf =
|
|
44
|
+
file.type === "application/pdf" && file.name.toLowerCase().endsWith(".pdf");
|
|
45
|
+
if (!isPdf) {
|
|
46
|
+
ctx.addIssue({
|
|
47
|
+
code: z.ZodIssueCode.custom,
|
|
48
|
+
message: "Only PDF files are supported",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
const getWorksheetUploadUrlsSchema = z.object({
|
|
54
|
+
classId: z.string(),
|
|
55
|
+
files: z.array(pdfFileSchema),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
const confirmWorksheetUploadSchema = z.object({
|
|
59
|
+
classId: z.string(),
|
|
60
|
+
fileId: z.string(),
|
|
61
|
+
uploadSuccess: z.boolean(),
|
|
62
|
+
errorMessage: z.string().optional(),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
const updateUploadProgressSchema = z.object({
|
|
66
|
+
fileId: z.string(),
|
|
67
|
+
progress: z.number().min(0).max(100),
|
|
68
|
+
});
|
|
69
|
+
|
|
28
70
|
export const worksheetRouter = createTRPCRouter({
|
|
29
71
|
getWorksheet: protectedProcedure
|
|
30
72
|
.input(z.object({ worksheetId: z.string() }))
|
|
@@ -66,6 +108,75 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
66
108
|
createWorksheetRecord(input.classId, input.name)
|
|
67
109
|
),
|
|
68
110
|
|
|
111
|
+
createFromPdf: protectedTeacherProcedure
|
|
112
|
+
.input(z.object({ classId: z.string(), url: z.string().url() }))
|
|
113
|
+
.mutation(({ input }) => createWorksheetFromPdf(input.classId, input.url)),
|
|
114
|
+
|
|
115
|
+
getWorksheetUploadUrls: protectedTeacherProcedure
|
|
116
|
+
.input(getWorksheetUploadUrlsSchema)
|
|
117
|
+
.mutation(async ({ ctx, input }) => {
|
|
118
|
+
if (!ctx.user) {
|
|
119
|
+
throw new TRPCError({
|
|
120
|
+
code: "UNAUTHORIZED",
|
|
121
|
+
message: "You must be logged in to upload files",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const uploadFiles = await createDirectUploadFiles(
|
|
126
|
+
input.files,
|
|
127
|
+
ctx.user.id
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
success: true,
|
|
132
|
+
uploadFiles,
|
|
133
|
+
};
|
|
134
|
+
}),
|
|
135
|
+
|
|
136
|
+
confirmWorksheetUpload: protectedTeacherProcedure
|
|
137
|
+
.input(confirmWorksheetUploadSchema)
|
|
138
|
+
.mutation(async ({ ctx, input }) => {
|
|
139
|
+
if (!ctx.user) {
|
|
140
|
+
throw new TRPCError({
|
|
141
|
+
code: "UNAUTHORIZED",
|
|
142
|
+
message: "You must be logged in",
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const file = await prisma.file.findFirst({
|
|
147
|
+
where: {
|
|
148
|
+
id: input.fileId,
|
|
149
|
+
userId: ctx.user.id,
|
|
150
|
+
},
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
if (!file) {
|
|
154
|
+
throw new TRPCError({
|
|
155
|
+
code: "NOT_FOUND",
|
|
156
|
+
message: "File not found or you don't have permission",
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
await confirmDirectUpload(
|
|
161
|
+
input.fileId,
|
|
162
|
+
input.uploadSuccess,
|
|
163
|
+
input.errorMessage
|
|
164
|
+
);
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
success: true,
|
|
168
|
+
message: input.uploadSuccess
|
|
169
|
+
? "Upload confirmed successfully"
|
|
170
|
+
: "Upload failed",
|
|
171
|
+
};
|
|
172
|
+
}),
|
|
173
|
+
|
|
174
|
+
createFromUploadedPdf: protectedTeacherProcedure
|
|
175
|
+
.input(z.object({ classId: z.string(), fileId: z.string() }))
|
|
176
|
+
.mutation(({ ctx, input }) =>
|
|
177
|
+
createWorksheetFromUploadedPdf(input.classId, input.fileId, ctx.user!.id)
|
|
178
|
+
),
|
|
179
|
+
|
|
69
180
|
addQuestion: protectedProcedure
|
|
70
181
|
.input(
|
|
71
182
|
z.object({
|
|
@@ -261,4 +372,36 @@ export const worksheetRouter = createTRPCRouter({
|
|
|
261
372
|
ctx.user!.id
|
|
262
373
|
)
|
|
263
374
|
),
|
|
375
|
+
|
|
376
|
+
updateUploadProgress: protectedProcedure
|
|
377
|
+
.input(updateUploadProgressSchema)
|
|
378
|
+
.mutation(async ({ ctx, input }) => {
|
|
379
|
+
if (!ctx.user) {
|
|
380
|
+
throw new TRPCError({
|
|
381
|
+
code: "UNAUTHORIZED",
|
|
382
|
+
message: "You must be logged in",
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const file = await prisma.file.findFirst({
|
|
387
|
+
where: {
|
|
388
|
+
id: input.fileId,
|
|
389
|
+
userId: ctx.user.id,
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
if (!file) {
|
|
394
|
+
throw new TRPCError({
|
|
395
|
+
code: "NOT_FOUND",
|
|
396
|
+
message: "File not found or you don't have permission",
|
|
397
|
+
});
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
await updateUploadProgress(input.fileId, input.progress);
|
|
401
|
+
|
|
402
|
+
return {
|
|
403
|
+
success: true,
|
|
404
|
+
progress: input.progress,
|
|
405
|
+
};
|
|
406
|
+
}),
|
|
264
407
|
});
|
|
@@ -13,15 +13,25 @@ import {
|
|
|
13
13
|
createConversation,
|
|
14
14
|
findConversationByIdAndMember,
|
|
15
15
|
createConversationMember,
|
|
16
|
+
hideConversationMember,
|
|
16
17
|
deleteConversationMember,
|
|
17
18
|
} from "../models/conversation.js";
|
|
18
19
|
|
|
19
20
|
/** List user's conversations with unread and mention counts. */
|
|
20
21
|
export async function listConversations(userId: string) {
|
|
21
22
|
const conversations = await findConversationsByUserId(userId);
|
|
23
|
+
const visibleConversations = conversations.filter((conversation) => {
|
|
24
|
+
const userMembership = conversation.members.find((member) => member.userId === userId);
|
|
25
|
+
|
|
26
|
+
if (!userMembership?.hiddenAt) {
|
|
27
|
+
return true;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return conversation.updatedAt > userMembership.hiddenAt;
|
|
31
|
+
});
|
|
22
32
|
|
|
23
33
|
const conversationsWithUnread = await Promise.all(
|
|
24
|
-
|
|
34
|
+
visibleConversations.map(async (conversation) => {
|
|
25
35
|
const userMembership = conversation.members.find(
|
|
26
36
|
(m) => m.userId === userId
|
|
27
37
|
);
|
|
@@ -65,6 +75,20 @@ export async function listConversations(userId: string) {
|
|
|
65
75
|
return conversationsWithUnread;
|
|
66
76
|
}
|
|
67
77
|
|
|
78
|
+
export async function hideConversation(userId: string, conversationId: string) {
|
|
79
|
+
const conversation = await findConversationByIdAndMember(conversationId, userId);
|
|
80
|
+
if (!conversation) {
|
|
81
|
+
throw new TRPCError({
|
|
82
|
+
code: "FORBIDDEN",
|
|
83
|
+
message: "Not a member of this conversation",
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
await hideConversationMember(userId, conversationId);
|
|
88
|
+
|
|
89
|
+
return { success: true };
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
/** Create a DM or group conversation with the given members. */
|
|
69
93
|
export async function createConversationRecord(
|
|
70
94
|
userId: string,
|
|
@@ -87,11 +111,11 @@ export async function createConversationRecord(
|
|
|
87
111
|
}
|
|
88
112
|
|
|
89
113
|
if (type === "DM") {
|
|
90
|
-
const targetUser = await
|
|
114
|
+
const [targetUser] = await findUsersByIdsOrUsernames([memberIds[0]]);
|
|
91
115
|
if (!targetUser) {
|
|
92
116
|
throw new TRPCError({
|
|
93
117
|
code: "BAD_REQUEST",
|
|
94
|
-
message: `User
|
|
118
|
+
message: `User not found`,
|
|
95
119
|
});
|
|
96
120
|
}
|
|
97
121
|
|
package/src/services/message.ts
CHANGED
|
@@ -259,6 +259,14 @@ export async function sendMessage(
|
|
|
259
259
|
});
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
await tx.conversationMember.updateMany({
|
|
263
|
+
where: {
|
|
264
|
+
conversationId,
|
|
265
|
+
hiddenAt: { not: null },
|
|
266
|
+
},
|
|
267
|
+
data: { hiddenAt: null },
|
|
268
|
+
});
|
|
269
|
+
|
|
262
270
|
await tx.conversation.update({
|
|
263
271
|
where: { id: conversationId },
|
|
264
272
|
data: { updatedAt: new Date() },
|