@studious-lms/server 1.1.9 → 1.1.11
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/dist/index.js +68 -0
- package/dist/routers/_app.d.ts +138 -0
- package/dist/routers/_app.d.ts.map +1 -1
- package/dist/routers/assignment.d.ts +10 -0
- package/dist/routers/assignment.d.ts.map +1 -1
- package/dist/routers/assignment.js +21 -0
- package/dist/routers/attendance.d.ts +15 -0
- package/dist/routers/attendance.d.ts.map +1 -1
- package/dist/routers/attendance.js +32 -0
- package/dist/routers/class.d.ts +10 -0
- package/dist/routers/class.d.ts.map +1 -1
- package/dist/routers/class.js +14 -0
- package/dist/routers/conversation.d.ts.map +1 -1
- package/dist/routers/conversation.js +10 -4
- package/dist/routers/message.d.ts +34 -0
- package/dist/routers/message.d.ts.map +1 -1
- package/dist/routers/message.js +208 -2
- package/dist/routers/user.d.ts.map +1 -1
- package/dist/routers/user.js +5 -4
- package/package.json +1 -1
- package/src/index.ts +79 -0
- package/src/routers/assignment.ts +21 -0
- package/src/routers/attendance.ts +32 -0
- package/src/routers/class.ts +14 -0
- package/src/routers/conversation.ts +11 -4
- package/src/routers/message.ts +233 -2
- package/src/routers/user.ts +5 -3
|
@@ -76,6 +76,40 @@ export declare const messageRouter: import("@trpc/server").TRPCBuiltRouter<{
|
|
|
76
76
|
};
|
|
77
77
|
meta: object;
|
|
78
78
|
}>;
|
|
79
|
+
update: import("@trpc/server").TRPCMutationProcedure<{
|
|
80
|
+
input: {
|
|
81
|
+
content: string;
|
|
82
|
+
messageId: string;
|
|
83
|
+
mentionedUserIds?: string[] | undefined;
|
|
84
|
+
};
|
|
85
|
+
output: {
|
|
86
|
+
id: string;
|
|
87
|
+
content: string;
|
|
88
|
+
senderId: string;
|
|
89
|
+
conversationId: string;
|
|
90
|
+
createdAt: Date;
|
|
91
|
+
sender: {
|
|
92
|
+
id: string;
|
|
93
|
+
username: string;
|
|
94
|
+
profile: {
|
|
95
|
+
displayName: string | null;
|
|
96
|
+
profilePicture: string | null;
|
|
97
|
+
} | null;
|
|
98
|
+
};
|
|
99
|
+
mentionedUserIds: string[];
|
|
100
|
+
};
|
|
101
|
+
meta: object;
|
|
102
|
+
}>;
|
|
103
|
+
delete: import("@trpc/server").TRPCMutationProcedure<{
|
|
104
|
+
input: {
|
|
105
|
+
messageId: string;
|
|
106
|
+
};
|
|
107
|
+
output: {
|
|
108
|
+
success: boolean;
|
|
109
|
+
messageId: string;
|
|
110
|
+
};
|
|
111
|
+
meta: object;
|
|
112
|
+
}>;
|
|
79
113
|
markAsRead: import("@trpc/server").TRPCMutationProcedure<{
|
|
80
114
|
input: {
|
|
81
115
|
conversationId: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa
|
|
1
|
+
{"version":3,"file":"message.d.ts","sourceRoot":"","sources":["../../src/routers/message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAMxB,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6kBxB,CAAC"}
|
package/dist/routers/message.js
CHANGED
|
@@ -192,6 +192,207 @@ export const messageRouter = createTRPCRouter({
|
|
|
192
192
|
mentionedUserIds,
|
|
193
193
|
};
|
|
194
194
|
}),
|
|
195
|
+
update: protectedProcedure
|
|
196
|
+
.input(z.object({
|
|
197
|
+
messageId: z.string(),
|
|
198
|
+
content: z.string().min(1).max(4000),
|
|
199
|
+
mentionedUserIds: z.array(z.string()).optional(),
|
|
200
|
+
}))
|
|
201
|
+
.mutation(async ({ input, ctx }) => {
|
|
202
|
+
const userId = ctx.user.id;
|
|
203
|
+
const { messageId, content, mentionedUserIds = [] } = input;
|
|
204
|
+
// Get the existing message and verify user is the sender
|
|
205
|
+
const existingMessage = await prisma.message.findUnique({
|
|
206
|
+
where: { id: messageId },
|
|
207
|
+
include: {
|
|
208
|
+
sender: {
|
|
209
|
+
select: {
|
|
210
|
+
id: true,
|
|
211
|
+
username: true,
|
|
212
|
+
profile: {
|
|
213
|
+
select: {
|
|
214
|
+
displayName: true,
|
|
215
|
+
profilePicture: true,
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
if (!existingMessage) {
|
|
223
|
+
throw new TRPCError({
|
|
224
|
+
code: 'NOT_FOUND',
|
|
225
|
+
message: 'Message not found',
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
if (existingMessage.senderId !== userId) {
|
|
229
|
+
throw new TRPCError({
|
|
230
|
+
code: 'FORBIDDEN',
|
|
231
|
+
message: 'Not the sender of this message',
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
// Verify user is still a member of the conversation
|
|
235
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
236
|
+
where: {
|
|
237
|
+
conversationId: existingMessage.conversationId,
|
|
238
|
+
userId,
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
if (!membership) {
|
|
242
|
+
throw new TRPCError({
|
|
243
|
+
code: 'FORBIDDEN',
|
|
244
|
+
message: 'Not a member of this conversation',
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
// Verify mentioned users are members of the conversation
|
|
248
|
+
if (mentionedUserIds.length > 0) {
|
|
249
|
+
const mentionedMemberships = await prisma.conversationMember.findMany({
|
|
250
|
+
where: {
|
|
251
|
+
conversationId: existingMessage.conversationId,
|
|
252
|
+
userId: { in: mentionedUserIds },
|
|
253
|
+
},
|
|
254
|
+
});
|
|
255
|
+
if (mentionedMemberships.length !== mentionedUserIds.length) {
|
|
256
|
+
throw new TRPCError({
|
|
257
|
+
code: 'BAD_REQUEST',
|
|
258
|
+
message: 'Some mentioned users are not members of this conversation',
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
// Update message and mentions in transaction
|
|
263
|
+
const updatedMessage = await prisma.$transaction(async (tx) => {
|
|
264
|
+
// Update the message content
|
|
265
|
+
const message = await tx.message.update({
|
|
266
|
+
where: { id: messageId },
|
|
267
|
+
data: { content },
|
|
268
|
+
include: {
|
|
269
|
+
sender: {
|
|
270
|
+
select: {
|
|
271
|
+
id: true,
|
|
272
|
+
username: true,
|
|
273
|
+
profile: {
|
|
274
|
+
select: {
|
|
275
|
+
displayName: true,
|
|
276
|
+
profilePicture: true,
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
});
|
|
283
|
+
// Delete existing mentions
|
|
284
|
+
await tx.mention.deleteMany({
|
|
285
|
+
where: { messageId },
|
|
286
|
+
});
|
|
287
|
+
// Create new mentions if any
|
|
288
|
+
if (mentionedUserIds.length > 0) {
|
|
289
|
+
await tx.mention.createMany({
|
|
290
|
+
data: mentionedUserIds.map((mentionedUserId) => ({
|
|
291
|
+
messageId,
|
|
292
|
+
userId: mentionedUserId,
|
|
293
|
+
})),
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
return message;
|
|
297
|
+
});
|
|
298
|
+
// Broadcast message update to Pusher
|
|
299
|
+
try {
|
|
300
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-updated', {
|
|
301
|
+
id: updatedMessage.id,
|
|
302
|
+
content: updatedMessage.content,
|
|
303
|
+
senderId: updatedMessage.senderId,
|
|
304
|
+
conversationId: updatedMessage.conversationId,
|
|
305
|
+
createdAt: updatedMessage.createdAt,
|
|
306
|
+
sender: updatedMessage.sender,
|
|
307
|
+
mentionedUserIds,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
catch (error) {
|
|
311
|
+
console.error('Failed to broadcast message update:', error);
|
|
312
|
+
// Don't fail the request if Pusher fails
|
|
313
|
+
}
|
|
314
|
+
return {
|
|
315
|
+
id: updatedMessage.id,
|
|
316
|
+
content: updatedMessage.content,
|
|
317
|
+
senderId: updatedMessage.senderId,
|
|
318
|
+
conversationId: updatedMessage.conversationId,
|
|
319
|
+
createdAt: updatedMessage.createdAt,
|
|
320
|
+
sender: updatedMessage.sender,
|
|
321
|
+
mentionedUserIds,
|
|
322
|
+
};
|
|
323
|
+
}),
|
|
324
|
+
delete: protectedProcedure
|
|
325
|
+
.input(z.object({
|
|
326
|
+
messageId: z.string(),
|
|
327
|
+
}))
|
|
328
|
+
.mutation(async ({ input, ctx }) => {
|
|
329
|
+
const userId = ctx.user.id;
|
|
330
|
+
const { messageId } = input;
|
|
331
|
+
// Get the message and verify user is the sender
|
|
332
|
+
const existingMessage = await prisma.message.findUnique({
|
|
333
|
+
where: { id: messageId },
|
|
334
|
+
include: {
|
|
335
|
+
sender: {
|
|
336
|
+
select: {
|
|
337
|
+
id: true,
|
|
338
|
+
username: true,
|
|
339
|
+
},
|
|
340
|
+
},
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
if (!existingMessage) {
|
|
344
|
+
throw new TRPCError({
|
|
345
|
+
code: 'NOT_FOUND',
|
|
346
|
+
message: 'Message not found',
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
if (existingMessage.senderId !== userId) {
|
|
350
|
+
throw new TRPCError({
|
|
351
|
+
code: 'FORBIDDEN',
|
|
352
|
+
message: 'Not the sender of this message',
|
|
353
|
+
});
|
|
354
|
+
}
|
|
355
|
+
// Verify user is still a member of the conversation
|
|
356
|
+
const membership = await prisma.conversationMember.findFirst({
|
|
357
|
+
where: {
|
|
358
|
+
conversationId: existingMessage.conversationId,
|
|
359
|
+
userId,
|
|
360
|
+
},
|
|
361
|
+
});
|
|
362
|
+
if (!membership) {
|
|
363
|
+
throw new TRPCError({
|
|
364
|
+
code: 'FORBIDDEN',
|
|
365
|
+
message: 'Not a member of this conversation',
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
// Delete message and all related mentions in transaction
|
|
369
|
+
await prisma.$transaction(async (tx) => {
|
|
370
|
+
// Delete mentions first (due to foreign key constraint)
|
|
371
|
+
await tx.mention.deleteMany({
|
|
372
|
+
where: { messageId },
|
|
373
|
+
});
|
|
374
|
+
// Delete the message
|
|
375
|
+
await tx.message.delete({
|
|
376
|
+
where: { id: messageId },
|
|
377
|
+
});
|
|
378
|
+
});
|
|
379
|
+
// Broadcast message deletion to Pusher
|
|
380
|
+
try {
|
|
381
|
+
await pusher.trigger(`conversation-${existingMessage.conversationId}`, 'message-deleted', {
|
|
382
|
+
messageId,
|
|
383
|
+
conversationId: existingMessage.conversationId,
|
|
384
|
+
senderId: existingMessage.senderId,
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
catch (error) {
|
|
388
|
+
console.error('Failed to broadcast message deletion:', error);
|
|
389
|
+
// Don't fail the request if Pusher fails
|
|
390
|
+
}
|
|
391
|
+
return {
|
|
392
|
+
success: true,
|
|
393
|
+
messageId,
|
|
394
|
+
};
|
|
395
|
+
}),
|
|
195
396
|
markAsRead: protectedProcedure
|
|
196
397
|
.input(z.object({
|
|
197
398
|
conversationId: z.string(),
|
|
@@ -305,14 +506,19 @@ export const messageRouter = createTRPCRouter({
|
|
|
305
506
|
},
|
|
306
507
|
});
|
|
307
508
|
// Count unread mentions
|
|
509
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
510
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
511
|
+
const mentionCutoffTime = membership.lastViewedMentionAt && membership.lastViewedAt
|
|
512
|
+
? (membership.lastViewedMentionAt > membership.lastViewedAt ? membership.lastViewedMentionAt : membership.lastViewedAt)
|
|
513
|
+
: (membership.lastViewedMentionAt || membership.lastViewedAt);
|
|
308
514
|
const unreadMentionCount = await prisma.mention.count({
|
|
309
515
|
where: {
|
|
310
516
|
userId,
|
|
311
517
|
message: {
|
|
312
518
|
conversationId,
|
|
313
519
|
senderId: { not: userId },
|
|
314
|
-
...(
|
|
315
|
-
createdAt: { gt:
|
|
520
|
+
...(mentionCutoffTime && {
|
|
521
|
+
createdAt: { gt: mentionCutoffTime }
|
|
316
522
|
}),
|
|
317
523
|
},
|
|
318
524
|
},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
1
|
+
{"version":3,"file":"user.d.ts","sourceRoot":"","sources":["../../src/routers/user.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AA2DxB,eAAO,MAAM,UAAU;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAuMrB,CAAC"}
|
package/dist/routers/user.js
CHANGED
|
@@ -2,7 +2,6 @@ import { z } from "zod";
|
|
|
2
2
|
import { createTRPCRouter, protectedProcedure } from "../trpc.js";
|
|
3
3
|
import { TRPCError } from "@trpc/server";
|
|
4
4
|
import { prisma } from "../lib/prisma.js";
|
|
5
|
-
import { getSignedUrl } from "../lib/googleCloudStorage.js";
|
|
6
5
|
import { logger } from "../utils/logger.js";
|
|
7
6
|
// Helper function to convert file path to backend proxy URL
|
|
8
7
|
function getFileUrl(filePath) {
|
|
@@ -203,13 +202,15 @@ export const userRouter = createTRPCRouter({
|
|
|
203
202
|
const fileExtension = input.fileName.split('.').pop();
|
|
204
203
|
const uniqueFilename = `${ctx.user.id}-${Date.now()}.${fileExtension}`;
|
|
205
204
|
const filePath = `users/${ctx.user.id}/profile/${uniqueFilename}`;
|
|
206
|
-
// Generate
|
|
207
|
-
const
|
|
205
|
+
// Generate backend proxy upload URL instead of direct GCS signed URL
|
|
206
|
+
const backendUrl = process.env.BACKEND_URL || 'http://localhost:3001';
|
|
207
|
+
const uploadUrl = `${backendUrl}/api/upload/${encodeURIComponent(filePath)}`;
|
|
208
208
|
logger.info('Generated upload URL', {
|
|
209
209
|
userId: ctx.user.id,
|
|
210
210
|
filePath,
|
|
211
211
|
fileName: uniqueFilename,
|
|
212
|
-
fileType: input.fileType
|
|
212
|
+
fileType: input.fileType,
|
|
213
|
+
uploadUrl
|
|
213
214
|
});
|
|
214
215
|
return {
|
|
215
216
|
uploadUrl,
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
|
@@ -22,6 +22,8 @@ app.use(cors({
|
|
|
22
22
|
'http://localhost:3001', // Server port
|
|
23
23
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
24
24
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
25
|
+
'https://www.studious.sh', // Production frontend
|
|
26
|
+
'https://studious.sh', // Production frontend (without www)
|
|
25
27
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
26
28
|
],
|
|
27
29
|
credentials: true,
|
|
@@ -37,6 +39,8 @@ app.options('*', (req, res) => {
|
|
|
37
39
|
'http://localhost:3001',
|
|
38
40
|
'http://127.0.0.1:3000',
|
|
39
41
|
'http://127.0.0.1:3001',
|
|
42
|
+
'https://www.studious.sh', // Production frontend
|
|
43
|
+
'https://studious.sh', // Production frontend (without www)
|
|
40
44
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
41
45
|
];
|
|
42
46
|
|
|
@@ -92,6 +96,8 @@ const io = new Server(httpServer, {
|
|
|
92
96
|
'http://localhost:3001', // Server port
|
|
93
97
|
'http://127.0.0.1:3000', // Alternative localhost
|
|
94
98
|
'http://127.0.0.1:3001', // Alternative localhost
|
|
99
|
+
'https://www.studious.sh', // Production frontend
|
|
100
|
+
'https://studious.sh', // Production frontend (without www)
|
|
95
101
|
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
96
102
|
],
|
|
97
103
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
@@ -158,6 +164,79 @@ app.get('/api/files/:filePath', async (req, res) => {
|
|
|
158
164
|
}
|
|
159
165
|
});
|
|
160
166
|
|
|
167
|
+
// File upload endpoint for secure file uploads (supports both POST and PUT)
|
|
168
|
+
app.post('/api/upload/:filePath', async (req, res) => {
|
|
169
|
+
handleFileUpload(req, res);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
app.put('/api/upload/:filePath', async (req, res) => {
|
|
173
|
+
handleFileUpload(req, res);
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
function handleFileUpload(req: any, res: any) {
|
|
177
|
+
try {
|
|
178
|
+
const filePath = decodeURIComponent(req.params.filePath);
|
|
179
|
+
console.log('File upload request:', { filePath, originalPath: req.params.filePath, method: req.method });
|
|
180
|
+
|
|
181
|
+
// Set CORS headers for upload endpoint
|
|
182
|
+
const origin = req.headers.origin;
|
|
183
|
+
const allowedOrigins = [
|
|
184
|
+
'http://localhost:3000',
|
|
185
|
+
'http://localhost:3001',
|
|
186
|
+
'http://127.0.0.1:3000',
|
|
187
|
+
'http://127.0.0.1:3001',
|
|
188
|
+
'https://www.studious.sh', // Production frontend
|
|
189
|
+
'https://studious.sh', // Production frontend (without www)
|
|
190
|
+
process.env.NEXT_PUBLIC_APP_URL || 'http://localhost:3000'
|
|
191
|
+
];
|
|
192
|
+
|
|
193
|
+
if (origin && allowedOrigins.includes(origin)) {
|
|
194
|
+
res.header('Access-Control-Allow-Origin', origin);
|
|
195
|
+
} else {
|
|
196
|
+
res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
res.header('Access-Control-Allow-Credentials', 'true');
|
|
200
|
+
|
|
201
|
+
// Get content type from headers
|
|
202
|
+
const contentType = req.headers['content-type'] || 'application/octet-stream';
|
|
203
|
+
|
|
204
|
+
// Create a new file in the bucket
|
|
205
|
+
const file = bucket.file(filePath);
|
|
206
|
+
|
|
207
|
+
// Create a write stream to Google Cloud Storage
|
|
208
|
+
const writeStream = file.createWriteStream({
|
|
209
|
+
metadata: {
|
|
210
|
+
contentType,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
// Handle stream events
|
|
215
|
+
writeStream.on('error', (error) => {
|
|
216
|
+
console.error('Error uploading file:', error);
|
|
217
|
+
if (!res.headersSent) {
|
|
218
|
+
res.status(500).json({ error: 'Error uploading file' });
|
|
219
|
+
}
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
writeStream.on('finish', () => {
|
|
223
|
+
console.log('File uploaded successfully:', filePath);
|
|
224
|
+
res.status(200).json({
|
|
225
|
+
success: true,
|
|
226
|
+
filePath,
|
|
227
|
+
message: 'File uploaded successfully'
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
// Pipe the request body to the write stream
|
|
232
|
+
req.pipe(writeStream);
|
|
233
|
+
|
|
234
|
+
} catch (error) {
|
|
235
|
+
console.error('Error handling file upload:', error);
|
|
236
|
+
res.status(500).json({ error: 'Internal server error' });
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
161
240
|
// Create caller
|
|
162
241
|
const createCaller = createCallerFactory(appRouter);
|
|
163
242
|
|
|
@@ -1093,6 +1093,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1093
1093
|
select: {
|
|
1094
1094
|
id: true,
|
|
1095
1095
|
username: true,
|
|
1096
|
+
profile: {
|
|
1097
|
+
select: {
|
|
1098
|
+
displayName: true,
|
|
1099
|
+
profilePicture: true,
|
|
1100
|
+
profilePictureThumbnail: true,
|
|
1101
|
+
},
|
|
1102
|
+
},
|
|
1096
1103
|
},
|
|
1097
1104
|
},
|
|
1098
1105
|
assignment: {
|
|
@@ -1195,6 +1202,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1195
1202
|
select: {
|
|
1196
1203
|
id: true,
|
|
1197
1204
|
username: true,
|
|
1205
|
+
profile: {
|
|
1206
|
+
select: {
|
|
1207
|
+
displayName: true,
|
|
1208
|
+
profilePicture: true,
|
|
1209
|
+
profilePictureThumbnail: true,
|
|
1210
|
+
},
|
|
1211
|
+
},
|
|
1198
1212
|
},
|
|
1199
1213
|
},
|
|
1200
1214
|
assignment: {
|
|
@@ -1311,6 +1325,13 @@ export const assignmentRouter = createTRPCRouter({
|
|
|
1311
1325
|
select: {
|
|
1312
1326
|
id: true,
|
|
1313
1327
|
username: true,
|
|
1328
|
+
profile: {
|
|
1329
|
+
select: {
|
|
1330
|
+
displayName: true,
|
|
1331
|
+
profilePicture: true,
|
|
1332
|
+
profilePictureThumbnail: true,
|
|
1333
|
+
},
|
|
1334
|
+
},
|
|
1314
1335
|
},
|
|
1315
1336
|
},
|
|
1316
1337
|
assignment: {
|
|
@@ -49,6 +49,17 @@ export const attendanceRouter = createTRPCRouter({
|
|
|
49
49
|
students: {
|
|
50
50
|
select: {
|
|
51
51
|
id: true,
|
|
52
|
+
username: true,
|
|
53
|
+
profile: {
|
|
54
|
+
select: {
|
|
55
|
+
displayName: true,
|
|
56
|
+
profilePicture: true,
|
|
57
|
+
profilePictureThumbnail: true,
|
|
58
|
+
bio: true,
|
|
59
|
+
location: true,
|
|
60
|
+
website: true,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
52
63
|
},
|
|
53
64
|
},
|
|
54
65
|
},
|
|
@@ -117,18 +128,39 @@ export const attendanceRouter = createTRPCRouter({
|
|
|
117
128
|
select: {
|
|
118
129
|
id: true,
|
|
119
130
|
username: true,
|
|
131
|
+
profile: {
|
|
132
|
+
select: {
|
|
133
|
+
displayName: true,
|
|
134
|
+
profilePicture: true,
|
|
135
|
+
profilePictureThumbnail: true,
|
|
136
|
+
},
|
|
137
|
+
},
|
|
120
138
|
},
|
|
121
139
|
},
|
|
122
140
|
late: {
|
|
123
141
|
select: {
|
|
124
142
|
id: true,
|
|
125
143
|
username: true,
|
|
144
|
+
profile: {
|
|
145
|
+
select: {
|
|
146
|
+
displayName: true,
|
|
147
|
+
profilePicture: true,
|
|
148
|
+
profilePictureThumbnail: true,
|
|
149
|
+
},
|
|
150
|
+
},
|
|
126
151
|
},
|
|
127
152
|
},
|
|
128
153
|
absent: {
|
|
129
154
|
select: {
|
|
130
155
|
id: true,
|
|
131
156
|
username: true,
|
|
157
|
+
profile: {
|
|
158
|
+
select: {
|
|
159
|
+
displayName: true,
|
|
160
|
+
profilePicture: true,
|
|
161
|
+
profilePictureThumbnail: true,
|
|
162
|
+
},
|
|
163
|
+
},
|
|
132
164
|
},
|
|
133
165
|
},
|
|
134
166
|
},
|
package/src/routers/class.ts
CHANGED
|
@@ -104,6 +104,13 @@ export const classRouter = createTRPCRouter({
|
|
|
104
104
|
select: {
|
|
105
105
|
id: true,
|
|
106
106
|
username: true,
|
|
107
|
+
profile: {
|
|
108
|
+
select: {
|
|
109
|
+
displayName: true,
|
|
110
|
+
profilePicture: true,
|
|
111
|
+
profilePictureThumbnail: true,
|
|
112
|
+
},
|
|
113
|
+
},
|
|
107
114
|
},
|
|
108
115
|
},
|
|
109
116
|
announcements: {
|
|
@@ -118,6 +125,13 @@ export const classRouter = createTRPCRouter({
|
|
|
118
125
|
select: {
|
|
119
126
|
id: true,
|
|
120
127
|
username: true,
|
|
128
|
+
profile: {
|
|
129
|
+
select: {
|
|
130
|
+
displayName: true,
|
|
131
|
+
profilePicture: true,
|
|
132
|
+
profilePictureThumbnail: true,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
121
135
|
},
|
|
122
136
|
},
|
|
123
137
|
},
|
|
@@ -76,14 +76,20 @@ export const conversationRouter = createTRPCRouter({
|
|
|
76
76
|
});
|
|
77
77
|
|
|
78
78
|
// Count unread mentions
|
|
79
|
+
// Use the later of lastViewedAt or lastViewedMentionAt
|
|
80
|
+
// This means if user viewed conversation after mention, mention is considered read
|
|
81
|
+
const mentionCutoffTime = lastViewedMentionAt && lastViewedAt
|
|
82
|
+
? (lastViewedMentionAt > lastViewedAt ? lastViewedMentionAt : lastViewedAt)
|
|
83
|
+
: (lastViewedMentionAt || lastViewedAt);
|
|
84
|
+
|
|
79
85
|
const unreadMentionCount = await prisma.mention.count({
|
|
80
86
|
where: {
|
|
81
87
|
userId,
|
|
82
88
|
message: {
|
|
83
89
|
conversationId: conversation.id,
|
|
84
90
|
senderId: { not: userId },
|
|
85
|
-
...(
|
|
86
|
-
createdAt: { gt:
|
|
91
|
+
...(mentionCutoffTime && {
|
|
92
|
+
createdAt: { gt: mentionCutoffTime }
|
|
87
93
|
}),
|
|
88
94
|
},
|
|
89
95
|
},
|
|
@@ -181,12 +187,13 @@ export const conversationRouter = createTRPCRouter({
|
|
|
181
187
|
// Verify all members exist
|
|
182
188
|
const members = await prisma.user.findMany({
|
|
183
189
|
where: {
|
|
184
|
-
|
|
190
|
+
username: {
|
|
185
191
|
in: memberIds,
|
|
186
192
|
},
|
|
187
193
|
},
|
|
188
194
|
select: {
|
|
189
195
|
id: true,
|
|
196
|
+
username: true,
|
|
190
197
|
},
|
|
191
198
|
});
|
|
192
199
|
|
|
@@ -209,7 +216,7 @@ export const conversationRouter = createTRPCRouter({
|
|
|
209
216
|
role: type === 'GROUP' ? 'ADMIN' : 'MEMBER',
|
|
210
217
|
},
|
|
211
218
|
...memberIds.map((memberId) => ({
|
|
212
|
-
userId: memberId,
|
|
219
|
+
userId: members.find((member) => member.username === memberId)!.id,
|
|
213
220
|
role: 'MEMBER' as const,
|
|
214
221
|
})),
|
|
215
222
|
],
|