From 557dd37fe878b61de57ee04337a0508e932d04b5 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Mon, 2 Oct 2023 23:46:17 +0200 Subject: [PATCH] [mastodon-client] Unread notifications --- .../api/mastodon/endpoints/notifications.ts | 18 ++++++++++++++++- .../server/api/mastodon/endpoints/timeline.ts | 3 --- .../api/mastodon/helpers/notification.ts | 20 ++++++++++++++++++- .../server/api/mastodon/helpers/timeline.ts | 13 ++++++++++-- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts index 69d2976eb..4c27ef53c 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/notifications.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/notifications.ts @@ -1,11 +1,13 @@ import Router from "@koa/router"; import { convertId, IdType } from "../../index.js"; import { convertPaginationArgsIds, limitToInt, normalizeUrlQuery } from "./timeline.js"; -import { convertNotification } from "../converters.js"; +import { convertConversation, convertNotification } from "../converters.js"; import authenticate from "@/server/api/authenticate.js"; import { UserHelpers } from "@/server/api/mastodon/helpers/user.js"; import { NotificationHelpers } from "@/server/api/mastodon/helpers/notification.js"; import { NotificationConverter } from "@/server/api/mastodon/converters/notification.js"; +import { TimelineHelpers } from "@/server/api/mastodon/helpers/timeline.js"; +import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; export function setupEndpointsNotifications(router: Router): void { router.get("/v1/notifications", async (ctx) => { @@ -99,4 +101,18 @@ export function setupEndpointsNotifications(router: Router): void { ctx.body = e.response.data; } }); + + router.post("/v1/conversations/:id/read", async (ctx, reply) => { + const auth = await authenticate(ctx.headers.authorization, null); + const user = auth[0] ?? undefined; + + if (!user) { + ctx.status = 401; + return; + } + + const id = convertId(ctx.params.id, IdType.IceshrimpId); + await NotificationHelpers.markConversationAsRead(id, user); + ctx.body = {}; + }); } diff --git a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts index 35f096ec5..b6ed146b5 100644 --- a/packages/backend/src/server/api/mastodon/endpoints/timeline.ts +++ b/packages/backend/src/server/api/mastodon/endpoints/timeline.ts @@ -177,9 +177,6 @@ export function setupEndpointsTimeline(router: Router): void { }, ); router.get("/v1/conversations", async (ctx, reply) => { - const BASE_URL = `${ctx.protocol}://${ctx.hostname}`; - const accessTokens = ctx.headers.authorization; - const client = getClient(BASE_URL, accessTokens); try { const auth = await authenticate(ctx.headers.authorization, null); const user = auth[0] ?? undefined; diff --git a/packages/backend/src/server/api/mastodon/helpers/notification.ts b/packages/backend/src/server/api/mastodon/helpers/notification.ts index 112e695f3..5bed3745b 100644 --- a/packages/backend/src/server/api/mastodon/helpers/notification.ts +++ b/packages/backend/src/server/api/mastodon/helpers/notification.ts @@ -1,5 +1,5 @@ import { ILocalUser } from "@/models/entities/user.js"; -import { Notifications } from "@/models/index.js"; +import { Notes, Notifications } from "@/models/index.js"; import { PaginationHelpers } from "@/server/api/mastodon/helpers/pagination.js"; import { Notification } from "@/models/entities/notification.js"; @@ -46,6 +46,24 @@ export class NotificationHelpers { await Notifications.update({notifieeId: user.id}, {isRead: true}); } + public static async markConversationAsRead(id: string, user: ILocalUser): Promise { + const notesQuery = Notes.createQueryBuilder("note") + .select("note.id") + .andWhere("COALESCE(note.threadId, note.id) = :conversationId"); + + await Notifications.createQueryBuilder("notification") + .where(`notification."noteId" IN (${notesQuery.getQuery()})`) + .andWhere(`notification."notifieeId" = :userId`) + .andWhere(`notification."isRead" = FALSE`) + .andWhere("notification.type IN (:...types)") + .setParameter("userId", user.id) + .setParameter("conversationId", id) + .setParameter("types", ['reply', 'mention']) + .update() + .set({isRead: true}) + .execute(); + } + private static decodeTypes(types: string[]) { const result: string[] = []; if (types.includes('follow')) result.push('follow'); diff --git a/packages/backend/src/server/api/mastodon/helpers/timeline.ts b/packages/backend/src/server/api/mastodon/helpers/timeline.ts index c0b1b502a..47de37c9a 100644 --- a/packages/backend/src/server/api/mastodon/helpers/timeline.ts +++ b/packages/backend/src/server/api/mastodon/helpers/timeline.ts @@ -1,6 +1,6 @@ import { Note } from "@/models/entities/note.js"; import { ILocalUser, User } from "@/models/entities/user.js"; -import { Followings, Notes, UserListJoinings } from "@/models/index.js"; +import { Followings, Notes, Notifications, UserListJoinings } from "@/models/index.js"; import { Brackets } from "typeorm"; import { generateChannelQuery } from "@/server/api/common/generate-channel-query.js"; import { generateRepliesQuery } from "@/server/api/common/generate-replies-query.js"; @@ -187,12 +187,21 @@ export class TimelineHelpers { const userIds = unique([c.userId].concat(c.visibleUserIds).filter(p => p != user.id)); const users = userIds.map(id => UserHelpers.getUserCached(id, cache).catch(_ => null)); const accounts = Promise.all(users).then(u => UserConverter.encodeMany(u.filter(u => u) as User[], cache)); + const unread = Notifications.createQueryBuilder('notification') + .where("notification.noteId = :noteId") + .andWhere("notification.notifieeId = :userId") + .andWhere("notification.isRead = FALSE") + .andWhere("notification.type IN (:...types)") + .setParameter("noteId", c.id) + .setParameter("userId", user.id) + .setParameter("types", ['reply', 'mention']) + .getExists(); return { id: c.threadId ?? c.id, accounts: accounts.then(u => u.length > 0 ? u : UserConverter.encodeMany([user], cache)), // failsafe to prevent apps from crashing case when all participant users have been deleted last_status: NoteConverter.encode(c, user, cache), - unread: false //FIXME implement this (also the /v1/conversations/:id/read endpoint) + unread: unread } }); const res = {