@studious-lms/server 1.11.0 → 1.13.0

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.
Files changed (188) hide show
  1. package/.env.example +3 -0
  2. package/dist/index.js +40 -4
  3. package/dist/index.js.map +1 -1
  4. package/dist/lib/config/cors.d.ts.map +1 -1
  5. package/dist/lib/config/cors.js +7 -2
  6. package/dist/lib/config/cors.js.map +1 -1
  7. package/dist/lib/config/env.d.ts +8 -0
  8. package/dist/lib/config/env.d.ts.map +1 -1
  9. package/dist/lib/config/env.js +5 -2
  10. package/dist/lib/config/env.js.map +1 -1
  11. package/dist/lib/pusher.d.ts +2 -1
  12. package/dist/lib/pusher.d.ts.map +1 -1
  13. package/dist/lib/pusher.js +4 -3
  14. package/dist/lib/pusher.js.map +1 -1
  15. package/dist/models/agenda.d.ts +6 -0
  16. package/dist/models/agenda.d.ts.map +1 -1
  17. package/dist/models/announcement.d.ts +3 -0
  18. package/dist/models/announcement.d.ts.map +1 -1
  19. package/dist/models/assignment.d.ts +61 -0
  20. package/dist/models/assignment.d.ts.map +1 -1
  21. package/dist/models/auth.d.ts +6 -0
  22. package/dist/models/auth.d.ts.map +1 -1
  23. package/dist/models/class.d.ts +28 -0
  24. package/dist/models/class.d.ts.map +1 -1
  25. package/dist/models/class.js +12 -3
  26. package/dist/models/class.js.map +1 -1
  27. package/dist/models/comment.d.ts +13 -0
  28. package/dist/models/comment.d.ts.map +1 -1
  29. package/dist/models/comment.js +3 -2
  30. package/dist/models/comment.js.map +1 -1
  31. package/dist/models/conversation.d.ts +17 -0
  32. package/dist/models/conversation.d.ts.map +1 -1
  33. package/dist/models/conversation.js +13 -2
  34. package/dist/models/conversation.js.map +1 -1
  35. package/dist/models/event.d.ts +12 -0
  36. package/dist/models/event.d.ts.map +1 -1
  37. package/dist/models/file.d.ts +38 -0
  38. package/dist/models/file.d.ts.map +1 -1
  39. package/dist/models/folder.d.ts +6 -0
  40. package/dist/models/folder.d.ts.map +1 -1
  41. package/dist/models/labChat.d.ts +6 -0
  42. package/dist/models/labChat.d.ts.map +1 -1
  43. package/dist/models/newtonChat.d.ts +3 -0
  44. package/dist/models/newtonChat.d.ts.map +1 -1
  45. package/dist/models/school.d.ts +3 -0
  46. package/dist/models/school.d.ts.map +1 -1
  47. package/dist/models/section.d.ts +3 -0
  48. package/dist/models/section.d.ts.map +1 -1
  49. package/dist/models/worksheet.d.ts +62 -0
  50. package/dist/models/worksheet.d.ts.map +1 -1
  51. package/dist/models/worksheet.js +37 -7
  52. package/dist/models/worksheet.js.map +1 -1
  53. package/dist/pipelines/gradeWorksheet.d.ts.map +1 -1
  54. package/dist/pipelines/gradeWorksheet.js +59 -14
  55. package/dist/pipelines/gradeWorksheet.js.map +1 -1
  56. package/dist/routers/_app.d.ts +4641 -2285
  57. package/dist/routers/_app.d.ts.map +1 -1
  58. package/dist/routers/_app.js +6 -2
  59. package/dist/routers/_app.js.map +1 -1
  60. package/dist/routers/admin.d.ts +190 -0
  61. package/dist/routers/admin.d.ts.map +1 -0
  62. package/dist/routers/admin.js +50 -0
  63. package/dist/routers/admin.js.map +1 -0
  64. package/dist/routers/agenda.d.ts +6 -0
  65. package/dist/routers/agenda.d.ts.map +1 -1
  66. package/dist/routers/announcement.d.ts +6 -0
  67. package/dist/routers/announcement.d.ts.map +1 -1
  68. package/dist/routers/announcement.js +6 -5
  69. package/dist/routers/announcement.js.map +1 -1
  70. package/dist/routers/assignment.d.ts +58 -0
  71. package/dist/routers/assignment.d.ts.map +1 -1
  72. package/dist/routers/assignment.js +4 -4
  73. package/dist/routers/assignment.js.map +1 -1
  74. package/dist/routers/class.d.ts +54 -0
  75. package/dist/routers/class.d.ts.map +1 -1
  76. package/dist/routers/class.js +5 -2
  77. package/dist/routers/class.js.map +1 -1
  78. package/dist/routers/comment.d.ts +26 -0
  79. package/dist/routers/comment.d.ts.map +1 -1
  80. package/dist/routers/comment.js +6 -3
  81. package/dist/routers/comment.js.map +1 -1
  82. package/dist/routers/discussion.d.ts +823 -0
  83. package/dist/routers/discussion.d.ts.map +1 -0
  84. package/dist/routers/discussion.js +151 -0
  85. package/dist/routers/discussion.js.map +1 -0
  86. package/dist/routers/event.d.ts +3 -0
  87. package/dist/routers/event.d.ts.map +1 -1
  88. package/dist/routers/file.d.ts +4 -0
  89. package/dist/routers/file.d.ts.map +1 -1
  90. package/dist/routers/school.d.ts +6 -0
  91. package/dist/routers/school.d.ts.map +1 -1
  92. package/dist/routers/worksheet.d.ts +6 -0
  93. package/dist/routers/worksheet.d.ts.map +1 -1
  94. package/dist/routers/worksheet.js +4 -2
  95. package/dist/routers/worksheet.js.map +1 -1
  96. package/dist/seedDatabase.d.ts.map +1 -1
  97. package/dist/seedDatabase.js +1490 -1307
  98. package/dist/seedDatabase.js.map +1 -1
  99. package/dist/services/adminInstitution.d.ts +102 -0
  100. package/dist/services/adminInstitution.d.ts.map +1 -0
  101. package/dist/services/adminInstitution.js +117 -0
  102. package/dist/services/adminInstitution.js.map +1 -0
  103. package/dist/services/adminRunJob.d.ts +45 -0
  104. package/dist/services/adminRunJob.d.ts.map +1 -0
  105. package/dist/services/adminRunJob.js +41 -0
  106. package/dist/services/adminRunJob.js.map +1 -0
  107. package/dist/services/agenda.d.ts +6 -0
  108. package/dist/services/agenda.d.ts.map +1 -1
  109. package/dist/services/assignment.d.ts +50 -0
  110. package/dist/services/assignment.d.ts.map +1 -1
  111. package/dist/services/assignment.js +16 -9
  112. package/dist/services/assignment.js.map +1 -1
  113. package/dist/services/class.d.ts +54 -0
  114. package/dist/services/class.d.ts.map +1 -1
  115. package/dist/services/class.js +20 -3
  116. package/dist/services/class.js.map +1 -1
  117. package/dist/services/comment.d.ts +20 -0
  118. package/dist/services/comment.d.ts.map +1 -1
  119. package/dist/services/comment.js +23 -2
  120. package/dist/services/comment.js.map +1 -1
  121. package/dist/services/conversation.d.ts.map +1 -1
  122. package/dist/services/conversation.js +9 -7
  123. package/dist/services/conversation.js.map +1 -1
  124. package/dist/services/discussion.d.ts +762 -0
  125. package/dist/services/discussion.d.ts.map +1 -0
  126. package/dist/services/discussion.js +625 -0
  127. package/dist/services/discussion.js.map +1 -0
  128. package/dist/services/event.d.ts +3 -0
  129. package/dist/services/event.d.ts.map +1 -1
  130. package/dist/services/file.d.ts +8 -0
  131. package/dist/services/file.d.ts.map +1 -1
  132. package/dist/services/school.d.ts +6 -0
  133. package/dist/services/school.d.ts.map +1 -1
  134. package/dist/services/studentProgress.d.ts.map +1 -1
  135. package/dist/services/studentProgress.js +27 -5
  136. package/dist/services/studentProgress.js.map +1 -1
  137. package/dist/services/studentProgressInferenceCache.d.ts +16 -0
  138. package/dist/services/studentProgressInferenceCache.d.ts.map +1 -0
  139. package/dist/services/studentProgressInferenceCache.js +43 -0
  140. package/dist/services/studentProgressInferenceCache.js.map +1 -0
  141. package/dist/services/worksheet.d.ts +6 -0
  142. package/dist/services/worksheet.d.ts.map +1 -1
  143. package/dist/services/worksheet.js +5 -5
  144. package/dist/services/worksheet.js.map +1 -1
  145. package/dist/trpc.d.ts +12 -0
  146. package/dist/trpc.d.ts.map +1 -1
  147. package/dist/trpc.js +16 -3
  148. package/dist/trpc.js.map +1 -1
  149. package/dist/utils/adminPanelAccess.d.ts +8 -0
  150. package/dist/utils/adminPanelAccess.d.ts.map +1 -0
  151. package/dist/utils/adminPanelAccess.js +44 -0
  152. package/dist/utils/adminPanelAccess.js.map +1 -0
  153. package/package.json +1 -1
  154. package/prisma/migrations/20260510183000_add_discussions/migration.sql +151 -0
  155. package/prisma/schema.prisma +98 -0
  156. package/src/index.ts +39 -2
  157. package/src/lib/config/cors.ts +5 -0
  158. package/src/lib/config/env.ts +3 -0
  159. package/src/lib/pusher.ts +2 -1
  160. package/src/models/class.ts +10 -1
  161. package/src/models/comment.ts +1 -0
  162. package/src/models/conversation.ts +12 -0
  163. package/src/models/worksheet.ts +39 -5
  164. package/src/pipelines/gradeWorksheet.ts +66 -15
  165. package/src/routers/_app.ts +4 -0
  166. package/src/routers/admin.ts +70 -0
  167. package/src/routers/announcement.ts +4 -3
  168. package/src/routers/assignment.ts +2 -2
  169. package/src/routers/class.ts +3 -0
  170. package/src/routers/comment.ts +5 -0
  171. package/src/routers/discussion.ts +205 -0
  172. package/src/routers/worksheet.ts +2 -0
  173. package/src/seedDatabase.ts +1713 -1361
  174. package/src/services/adminInstitution.ts +140 -0
  175. package/src/services/adminRunJob.ts +46 -0
  176. package/src/services/assignment.ts +19 -8
  177. package/src/services/class.ts +28 -1
  178. package/src/services/comment.ts +22 -0
  179. package/src/services/conversation.ts +9 -4
  180. package/src/services/discussion.ts +701 -0
  181. package/src/services/studentProgress.ts +80 -3
  182. package/src/services/studentProgressInferenceCache.ts +64 -0
  183. package/src/services/worksheet.ts +5 -2
  184. package/src/trpc.ts +15 -0
  185. package/src/utils/adminPanelAccess.ts +46 -0
  186. package/tests/integration/routers/conversation.test.ts +9 -7
  187. package/tests/integration/routers/discussion.test.ts +253 -0
  188. package/tests/unit/services/discussion.test.ts +356 -0
@@ -0,0 +1,140 @@
1
+ import type { Prisma } from '@prisma/client';
2
+ import { prisma } from '../lib/prisma.js';
3
+
4
+ export async function listSchoolsForAdmin(input: {
5
+ page: number;
6
+ pageSize: number;
7
+ query?: string;
8
+ }) {
9
+ const skip = input.page * input.pageSize;
10
+ const where: Prisma.SchoolWhereInput = {};
11
+ const q = input.query?.trim();
12
+ if (q) {
13
+ where.OR = [
14
+ { name: { contains: q, mode: 'insensitive' } },
15
+ { subdomain: { contains: q, mode: 'insensitive' } },
16
+ { description: { contains: q, mode: 'insensitive' } },
17
+ ];
18
+ }
19
+
20
+ const [items, total] = await Promise.all([
21
+ prisma.school.findMany({
22
+ where,
23
+ orderBy: { createdAt: 'desc' },
24
+ skip,
25
+ take: input.pageSize,
26
+ include: {
27
+ _count: {
28
+ select: {
29
+ memberships: true,
30
+ classes: true,
31
+ departments: true,
32
+ },
33
+ },
34
+ },
35
+ }),
36
+ prisma.school.count({ where }),
37
+ ]);
38
+
39
+ return {
40
+ items,
41
+ total,
42
+ page: input.page,
43
+ pageSize: input.pageSize,
44
+ pageCount: Math.max(1, Math.ceil(total / input.pageSize)),
45
+ };
46
+ }
47
+
48
+ export async function listDepartmentsForAdmin(input: {
49
+ page: number;
50
+ pageSize: number;
51
+ schoolId?: string;
52
+ }) {
53
+ const skip = input.page * input.pageSize;
54
+ const where: Prisma.DepartmentWhereInput = {};
55
+ if (input.schoolId) where.schoolId = input.schoolId;
56
+
57
+ const [items, total] = await Promise.all([
58
+ prisma.department.findMany({
59
+ where,
60
+ orderBy: [{ schoolId: 'asc' }, { name: 'asc' }],
61
+ skip,
62
+ take: input.pageSize,
63
+ include: {
64
+ school: { select: { id: true, name: true, subdomain: true } },
65
+ _count: { select: { classes: true, members: true } },
66
+ },
67
+ }),
68
+ prisma.department.count({ where }),
69
+ ]);
70
+
71
+ return {
72
+ items,
73
+ total,
74
+ page: input.page,
75
+ pageSize: input.pageSize,
76
+ pageCount: Math.max(1, Math.ceil(total / input.pageSize)),
77
+ };
78
+ }
79
+
80
+ export async function listSchoolDevelopmentProgramsForAdmin(input: {
81
+ page: number;
82
+ pageSize: number;
83
+ status?: string;
84
+ query?: string;
85
+ }) {
86
+ const skip = input.page * input.pageSize;
87
+ const where: Prisma.SchoolDevelopementProgramWhereInput = {};
88
+ if (input.status?.trim()) where.status = input.status.trim();
89
+ const q = input.query?.trim();
90
+ if (q) {
91
+ where.OR = [
92
+ { name: { contains: q, mode: 'insensitive' } },
93
+ { city: { contains: q, mode: 'insensitive' } },
94
+ { country: { contains: q, mode: 'insensitive' } },
95
+ { contactEmail: { contains: q, mode: 'insensitive' } },
96
+ ];
97
+ }
98
+
99
+ const [items, total] = await Promise.all([
100
+ prisma.schoolDevelopementProgram.findMany({
101
+ where,
102
+ orderBy: { submittedAt: 'desc' },
103
+ skip,
104
+ take: input.pageSize,
105
+ }),
106
+ prisma.schoolDevelopementProgram.count({ where }),
107
+ ]);
108
+
109
+ return {
110
+ items,
111
+ total,
112
+ page: input.page,
113
+ pageSize: input.pageSize,
114
+ pageCount: Math.max(1, Math.ceil(total / input.pageSize)),
115
+ };
116
+ }
117
+
118
+ export async function listEarlyAccessRequestsForAdmin(input: {
119
+ page: number;
120
+ pageSize: number;
121
+ }) {
122
+ const skip = input.page * input.pageSize;
123
+
124
+ const [items, total] = await Promise.all([
125
+ prisma.earlyAccessRequest.findMany({
126
+ orderBy: { createdAt: 'desc' },
127
+ skip,
128
+ take: input.pageSize,
129
+ }),
130
+ prisma.earlyAccessRequest.count(),
131
+ ]);
132
+
133
+ return {
134
+ items,
135
+ total,
136
+ page: input.page,
137
+ pageSize: input.pageSize,
138
+ pageCount: Math.max(1, Math.ceil(total / input.pageSize)),
139
+ };
140
+ }
@@ -0,0 +1,46 @@
1
+ import type { Prisma, RunJobStatus, RunJobType } from '@prisma/client';
2
+ import { TRPCError } from '@trpc/server';
3
+ import { prisma } from '../lib/prisma.js';
4
+
5
+ export async function listRunJobsForAdmin(input: {
6
+ page: number;
7
+ pageSize: number;
8
+ status?: RunJobStatus;
9
+ type?: RunJobType;
10
+ }) {
11
+ const { page, pageSize, status, type } = input;
12
+ const skip = page * pageSize;
13
+
14
+ const where: Prisma.RunJobWhereInput = {};
15
+ if (status) where.status = status;
16
+ if (type) where.type = type;
17
+
18
+ const [items, total] = await Promise.all([
19
+ prisma.runJob.findMany({
20
+ where,
21
+ orderBy: { createdAt: 'desc' },
22
+ skip,
23
+ take: pageSize,
24
+ }),
25
+ prisma.runJob.count({ where }),
26
+ ]);
27
+
28
+ return {
29
+ items,
30
+ total,
31
+ page,
32
+ pageSize,
33
+ pageCount: Math.max(1, Math.ceil(total / pageSize)),
34
+ };
35
+ }
36
+
37
+ export async function getRunJobForAdmin(id: string) {
38
+ const job = await prisma.runJob.findUnique({ where: { id } });
39
+ if (!job) {
40
+ throw new TRPCError({
41
+ code: 'NOT_FOUND',
42
+ message: 'Run job not found',
43
+ });
44
+ }
45
+ return job;
46
+ }
@@ -26,6 +26,7 @@ import { deleteFile } from "../lib/googleCloudStorage.js";
26
26
  import { sendToMultiple } from "./notification.js";
27
27
  import { enqueueRunJob } from "../workers/queues.js";
28
28
  import { logger } from "../utils/logger.js";
29
+ import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
29
30
 
30
31
  export async function assignmentExists(id: string) {
31
32
  const assignment = await findAssignmentById(id);
@@ -424,12 +425,13 @@ export async function updateAssignmentRecord(
424
425
  });
425
426
  }
426
427
 
427
- const submissionData =
428
- input.studentIds?.length
429
- ? input.studentIds.map((studentId) => ({ student: { connect: { id: studentId } } }))
430
- : classData?.students.map((student) => ({
431
- student: { connect: { id: student.id } },
432
- }));
428
+ const submissionData = input.studentIds
429
+ ? (input.studentIds.length > 0
430
+ ? input.studentIds.map((studentId) => ({ student: { connect: { id: studentId } } }))
431
+ : classData?.students.map((student) => ({
432
+ student: { connect: { id: student.id } },
433
+ })))
434
+ : undefined;
433
435
 
434
436
  if (input.removedAttachments?.length) {
435
437
  const filesToDelete = assignment.attachments.filter((f) =>
@@ -480,7 +482,7 @@ export async function updateAssignmentRecord(
480
482
  connect: input.studentIds.map((id) => ({ id })),
481
483
  },
482
484
  }),
483
- ...(submissionData && {
485
+ ...(input.studentIds && submissionData && {
484
486
  submissions: {
485
487
  deleteMany: {},
486
488
  create: submissionData,
@@ -892,7 +894,7 @@ export async function updateSubmissionAsTeacherRecord(
892
894
  );
893
895
  }
894
896
 
895
- return prisma.submission.update({
897
+ const updated = await prisma.submission.update({
896
898
  where: { id: submissionId },
897
899
  data: {
898
900
  ...(gradeReceived !== undefined && { gradeReceived }),
@@ -906,6 +908,15 @@ export async function updateSubmissionAsTeacherRecord(
906
908
  },
907
909
  include: submissionTeacherInclude,
908
910
  });
911
+
912
+ if (gradeReceived !== undefined) {
913
+ invalidateStudentProgressInferenceCache(
914
+ submission.assignment.classId,
915
+ submission.studentId,
916
+ );
917
+ }
918
+
919
+ return updated;
909
920
  }
910
921
 
911
922
  export async function attachAssignmentToEventRecord(
@@ -26,6 +26,7 @@ import {
26
26
  createClassByImport,
27
27
  } from "../models/class.js";
28
28
  import { Class, Folder } from "@prisma/client";
29
+ import { invalidateStudentProgressInferenceCache } from "./studentProgressInferenceCache.js";
29
30
 
30
31
  async function requireActiveSchoolMembers(schoolId: string, userIds: string[]) {
31
32
  const uniqueUserIds = [...new Set(userIds)];
@@ -74,6 +75,9 @@ export async function getAllClasses(userId: string) {
74
75
  schoolId: cls.schoolId ?? null,
75
76
  school: cls.school ?? null,
76
77
  department: cls.department ?? null,
78
+ discussionsEnabled: cls.discussionsEnabled,
79
+ studentsCanCreateDiscussions: cls.studentsCanCreateDiscussions,
80
+ studentsCanReply: cls.studentsCanReply,
77
81
  })).filter((cls) => cls.members.some((member) => member.id === userId)),
78
82
  studentInClass: studentClasses.map((cls) => ({
79
83
  id: cls.id,
@@ -81,12 +85,24 @@ export async function getAllClasses(userId: string) {
81
85
  section: cls.section,
82
86
  subject: cls.subject,
83
87
  dueToday: cls.assignments,
84
- assignments: cls.assignments,
88
+ assignments: cls.assignments.map((assignment) => ({
89
+ ...assignment,
90
+ late: new Date(assignment.dueDate) < new Date(),
91
+ submitted: assignment.submissions.find(
92
+ (s) => s.studentId === userId,
93
+ )?.submitted ?? false,
94
+ returned: assignment.submissions.find(
95
+ (s) => s.studentId === userId,
96
+ )?.returned ?? false,
97
+ })),
85
98
  members: [...cls.students, ...cls.teachers],
86
99
  color: cls.color,
87
100
  schoolId: cls.schoolId ?? null,
88
101
  school: cls.school ?? null,
89
102
  department: cls.department ?? null,
103
+ discussionsEnabled: cls.discussionsEnabled,
104
+ studentsCanCreateDiscussions: cls.studentsCanCreateDiscussions,
105
+ studentsCanReply: cls.studentsCanReply,
90
106
  })),
91
107
  };
92
108
  }
@@ -171,6 +187,9 @@ export async function updateClass(
171
187
  subject?: string;
172
188
  color?: string;
173
189
  departmentId?: string | null;
190
+ discussionsEnabled?: boolean;
191
+ studentsCanCreateDiscussions?: boolean;
192
+ studentsCanReply?: boolean;
174
193
  }
175
194
  ) {
176
195
  if (data.departmentId !== undefined) {
@@ -209,6 +228,9 @@ export async function updateClass(
209
228
  subject: true,
210
229
  color: true,
211
230
  departmentId: true,
231
+ discussionsEnabled: true,
232
+ studentsCanCreateDiscussions: true,
233
+ studentsCanReply: true,
212
234
  },
213
235
  });
214
236
  return { updatedClass };
@@ -498,10 +520,15 @@ export async function updateGrade(
498
520
  title: true,
499
521
  maxGrade: true,
500
522
  weight: true,
523
+ classId: true,
501
524
  },
502
525
  },
503
526
  },
504
527
  });
528
+ invalidateStudentProgressInferenceCache(
529
+ updatedSubmission.assignment.classId,
530
+ updatedSubmission.studentId,
531
+ );
505
532
  return updatedSubmission;
506
533
  }
507
534
 
@@ -158,6 +158,28 @@ export async function deleteComment(userId: string, id: string) {
158
158
  return { success: true };
159
159
  }
160
160
 
161
+ /** Pin or unpin a comment. Only teachers can pin comments. */
162
+ export async function pinComment(userId: string, commentId: string, isPinned: boolean) {
163
+ const comment = await requireCommentAccess(commentId, userId);
164
+ if (comment.parentCommentId) {
165
+ throw new TRPCError({
166
+ code: "BAD_REQUEST",
167
+ message: "Only top-level comments can be pinned",
168
+ });
169
+ }
170
+ if (!isClassTeacher(comment, userId)) {
171
+ throw new TRPCError({
172
+ code: "FORBIDDEN",
173
+ message: "Only teachers can pin comments",
174
+ });
175
+ }
176
+ const updated = await prisma.comment.update({
177
+ where: { id: commentId },
178
+ data: { isPinned },
179
+ });
180
+ return updated;
181
+ }
182
+
161
183
  /** Add or update a reaction (emoji) on a comment. */
162
184
  export async function addReaction(
163
185
  userId: string,
@@ -15,6 +15,7 @@ import {
15
15
  findConversationByIdAndMember,
16
16
  createConversationMember,
17
17
  hideConversationMember,
18
+ unhideConversationMember,
18
19
  deleteConversationMember,
19
20
  } from "../models/conversation.js";
20
21
 
@@ -160,10 +161,14 @@ export async function createConversationRecord(
160
161
  });
161
162
 
162
163
  if (existingDM) {
163
- throw new TRPCError({
164
- code: "BAD_REQUEST",
165
- message: `A conversation with ${targetUser.username} already exists`,
166
- });
164
+ const userMembership = existingDM.members.find(
165
+ (m) => m.userId === userId
166
+ );
167
+ if (userMembership) {
168
+ await unhideConversationMember(userId, existingDM.id);
169
+ }
170
+ const conversation = await findConversationByIdAndMember(existingDM.id, userId);
171
+ return conversation!;
167
172
  }
168
173
  }
169
174