diff --git a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs index fce189fa..668f03ed 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/Renderers/NoteRenderer.cs @@ -35,6 +35,8 @@ public class NoteRenderer( var liked = data?.LikedNotes?.Contains(note.Id) ?? await db.NoteLikes.AnyAsync(p => p.Note == note && p.User == user); + var bookmarked = data?.BookmarkedNotes?.Contains(note.Id) ?? + await db.NoteBookmarks.AnyAsync(p => p.Note == note && p.User == user); var renoted = data?.Renotes?.Contains(note.Id) ?? await db.Notes.AnyAsync(p => p.Renote == note && p.User == user && p.IsPureRenote); @@ -83,8 +85,8 @@ public class NoteRenderer( FavoriteCount = note.LikeCount, IsFavorited = liked, IsRenoted = renoted, - IsBookmarked = false, //FIXME - IsMuted = null, //FIXME + IsBookmarked = bookmarked, + IsMuted = null, //FIXME IsSensitive = note.Cw != null, ContentWarning = note.Cw ?? "", Visibility = StatusEntity.EncodeVisibility(note.Visibility), @@ -139,6 +141,14 @@ public class NoteRenderer( .ToListAsync(); } + private async Task> GetBookmarkedNotes(IEnumerable notes, User? user) + { + if (user == null) return []; + return await db.NoteBookmarks.Where(p => p.User == user && notes.Contains(p.Note)) + .Select(p => p.NoteId) + .ToListAsync(); + } + private async Task> GetRenotes(IEnumerable notes, User? user) { if (user == null) return []; @@ -180,12 +190,13 @@ public class NoteRenderer( var data = new NoteRendererDto { - Accounts = accounts ?? await GetAccounts(noteList.Select(p => p.User)), - Mentions = await GetMentions(noteList), - Attachments = await GetAttachments(noteList), - LikedNotes = await GetLikedNotes(noteList, user), - Renotes = await GetRenotes(noteList, user), - Emoji = await GetEmoji(noteList) + Accounts = accounts ?? await GetAccounts(noteList.Select(p => p.User)), + Mentions = await GetMentions(noteList), + Attachments = await GetAttachments(noteList), + LikedNotes = await GetLikedNotes(noteList, user), + BookmarkedNotes = await GetBookmarkedNotes(noteList, user), + Renotes = await GetRenotes(noteList, user), + Emoji = await GetEmoji(noteList) }; return await noteList.Select(p => RenderAsync(p, user, data)).AwaitAllAsync(); @@ -197,6 +208,7 @@ public class NoteRenderer( public List? Mentions; public List? Attachments; public List? LikedNotes; + public List? BookmarkedNotes; public List? Renotes; public List? Emoji; public bool Source; diff --git a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs index 58bc63b1..515b6b69 100644 --- a/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs +++ b/Iceshrimp.Backend/Controllers/Mastodon/StatusController.cs @@ -118,6 +118,40 @@ public class StatusController( return await GetNote(id); } + [HttpPost("{id}/bookmark")] + [Authorize("write:bookmarks")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] + public async Task BookmarkNote(string id) + { + var user = HttpContext.GetUserOrFail(); + var note = await db.Notes.Where(p => p.Id == id) + .IncludeCommonProperties() + .EnsureVisibleFor(user) + .FirstOrDefaultAsync() ?? + throw GracefulException.RecordNotFound(); + + await noteSvc.BookmarkNoteAsync(note, user); + return await GetNote(id); + } + + [HttpPost("{id}/unbookmark")] + [Authorize("write:bookmarks")] + [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))] + [ProducesResponseType(StatusCodes.Status404NotFound, Type = typeof(MastodonErrorResponse))] + public async Task UnbookmarkNote(string id) + { + var user = HttpContext.GetUserOrFail(); + var note = await db.Notes.Where(p => p.Id == id) + .IncludeCommonProperties() + .EnsureVisibleFor(user) + .FirstOrDefaultAsync() ?? + throw GracefulException.RecordNotFound(); + + await noteSvc.BookmarkNoteAsync(note, user); + return await GetNote(id); + } + [HttpPost("{id}/reblog")] [Authorize("write:favourites")] [ProducesResponseType(StatusCodes.Status200OK, Type = typeof(StatusEntity))] diff --git a/Iceshrimp.Backend/Core/Services/NoteService.cs b/Iceshrimp.Backend/Core/Services/NoteService.cs index 2d1a0b57..9efd9c13 100644 --- a/Iceshrimp.Backend/Core/Services/NoteService.cs +++ b/Iceshrimp.Backend/Core/Services/NoteService.cs @@ -776,6 +776,25 @@ public class NoteService( await UnlikeNoteAsync(dbNote, actor); } + public async Task BookmarkNoteAsync(Note note, User user) + { + if (!await db.NoteBookmarks.AnyAsync(p => p.Note == note && p.User == user)) + { + var bookmark = new NoteBookmark + { + Id = IdHelpers.GenerateSlowflakeId(), CreatedAt = DateTime.UtcNow, User = user, Note = note + }; + + await db.NoteBookmarks.AddAsync(bookmark); + await db.SaveChangesAsync(); + } + } + + public async Task UnbookmarkNoteAsync(Note note, User actor) + { + await db.NoteBookmarks.Where(p => p.Note == note && p.User == actor).ExecuteDeleteAsync(); + } + public async Task UpdatePinnedNotesAsync(ASActor actor, User user) { logger.LogDebug("Updating pinned notes for user {user}", user.Id);