mirror of
https://github.com/versia-pub/server.git
synced 2026-03-13 05:49:16 +01:00
feat(api): ✨ Add Emoji Reactions
This commit is contained in:
parent
70974d3c35
commit
9722b94eae
17 changed files with 755 additions and 7 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import type { Status } from "@versia/client/schemas";
|
||||
import { db, Instance } from "@versia/kit/db";
|
||||
import type { NoteReactionWithAccounts, Status } from "@versia/client/schemas";
|
||||
import { db, Instance, type Reaction } from "@versia/kit/db";
|
||||
import {
|
||||
EmojiToNote,
|
||||
Likes,
|
||||
|
|
@ -54,6 +54,7 @@ type NoteTypeWithRelations = NoteType & {
|
|||
reblogged: boolean;
|
||||
muted: boolean;
|
||||
liked: boolean;
|
||||
reactions: Omit<typeof Reaction.$type, "note" | "author">[];
|
||||
};
|
||||
|
||||
export type NoteTypeWithoutRecursiveRelations = Omit<
|
||||
|
|
@ -698,7 +699,13 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
edited_at: data.updatedAt
|
||||
? new Date(data.updatedAt).toISOString()
|
||||
: null,
|
||||
reactions: [],
|
||||
reactions: this.getReactions(userFetching ?? undefined).map(
|
||||
// Remove account_ids
|
||||
(r) => ({
|
||||
...r,
|
||||
account_ids: undefined,
|
||||
}),
|
||||
),
|
||||
text: data.contentSource,
|
||||
};
|
||||
}
|
||||
|
|
@ -915,4 +922,67 @@ export class Note extends BaseInterface<typeof Notes, NoteTypeWithRelations> {
|
|||
|
||||
return viewableDescendants;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get reactions for this note grouped by emoji name
|
||||
* @param user - The user requesting reactions (to determine 'me' field)
|
||||
* @returns Array of reactions grouped by emoji name with counts and account IDs
|
||||
*/
|
||||
public getReactions(
|
||||
user?: User,
|
||||
): z.infer<typeof NoteReactionWithAccounts>[] {
|
||||
// Group reactions by emoji name (either emojiText for Unicode or formatted shortcode for custom)
|
||||
const groupedReactions = new Map<
|
||||
string,
|
||||
{
|
||||
count: number;
|
||||
me: boolean;
|
||||
account_ids: string[];
|
||||
}
|
||||
>();
|
||||
|
||||
for (const reaction of this.data.reactions) {
|
||||
let emojiName: string;
|
||||
|
||||
// Determine emoji name based on type
|
||||
if (reaction.emojiText) {
|
||||
emojiName = reaction.emojiText;
|
||||
} else if (reaction.emoji) {
|
||||
emojiName = `:${reaction.emoji.shortcode}:`;
|
||||
} else {
|
||||
continue; // Skip invalid reactions
|
||||
}
|
||||
|
||||
// Initialize group if it doesn't exist
|
||||
if (!groupedReactions.has(emojiName)) {
|
||||
groupedReactions.set(emojiName, {
|
||||
count: 0,
|
||||
me: false,
|
||||
account_ids: [],
|
||||
});
|
||||
}
|
||||
|
||||
const group = groupedReactions.get(emojiName);
|
||||
|
||||
if (!group) {
|
||||
continue;
|
||||
}
|
||||
|
||||
group.count += 1;
|
||||
group.account_ids.push(reaction.authorId);
|
||||
|
||||
// Check if current user reacted with this emoji
|
||||
if (user && reaction.authorId === user.id) {
|
||||
group.me = true;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert map to array format
|
||||
return Array.from(groupedReactions.entries()).map(([name, data]) => ({
|
||||
name,
|
||||
count: data.count,
|
||||
me: data.me,
|
||||
account_ids: data.account_ids,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,11 +2,13 @@ import { db, Emoji, Instance, type Note, User } from "@versia/kit/db";
|
|||
import { type Notes, Reactions, type Users } from "@versia/kit/tables";
|
||||
import { randomUUIDv7 } from "bun";
|
||||
import {
|
||||
and,
|
||||
desc,
|
||||
eq,
|
||||
type InferInsertModel,
|
||||
type InferSelectModel,
|
||||
inArray,
|
||||
isNull,
|
||||
type SQL,
|
||||
} from "drizzle-orm";
|
||||
import { config } from "~/config.ts";
|
||||
|
|
@ -156,6 +158,32 @@ export class Reaction extends BaseInterface<typeof Reactions, ReactionType> {
|
|||
return this.data.id;
|
||||
}
|
||||
|
||||
public static fromEmoji(
|
||||
emoji: Emoji | string,
|
||||
author: User,
|
||||
note: Note,
|
||||
): Promise<Reaction | null> {
|
||||
if (emoji instanceof Emoji) {
|
||||
return Reaction.fromSql(
|
||||
and(
|
||||
eq(Reactions.authorId, author.id),
|
||||
eq(Reactions.noteId, note.id),
|
||||
isNull(Reactions.emojiText),
|
||||
eq(Reactions.emojiId, emoji.id),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Reaction.fromSql(
|
||||
and(
|
||||
eq(Reactions.authorId, author.id),
|
||||
eq(Reactions.noteId, note.id),
|
||||
eq(Reactions.emojiText, emoji),
|
||||
isNull(Reactions.emojiId),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
public getUri(baseUrl: URL): URL {
|
||||
return this.data.uri
|
||||
? new URL(this.data.uri)
|
||||
|
|
|
|||
|
|
@ -6,7 +6,13 @@ import type {
|
|||
Source,
|
||||
Status as StatusSchema,
|
||||
} from "@versia/client/schemas";
|
||||
import { db, Media, Notification, PushSubscription } from "@versia/kit/db";
|
||||
import {
|
||||
db,
|
||||
Media,
|
||||
Notification,
|
||||
PushSubscription,
|
||||
Reaction,
|
||||
} from "@versia/kit/db";
|
||||
import {
|
||||
EmojiToUser,
|
||||
Likes,
|
||||
|
|
@ -732,6 +738,48 @@ export class User extends BaseInterface<typeof Users, UserWithRelations> {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an emoji reaction to a note
|
||||
* @param note - The note to react to
|
||||
* @param emoji - The emoji to react with (Emoji object for custom emojis, or Unicode emoji)
|
||||
* @returns The created reaction
|
||||
*/
|
||||
public async react(note: Note, emoji: Emoji | string): Promise<void> {
|
||||
const existingReaction = await Reaction.fromEmoji(emoji, this, note);
|
||||
|
||||
if (existingReaction) {
|
||||
return; // Reaction already exists, don't create duplicate
|
||||
}
|
||||
|
||||
// Create the reaction
|
||||
await Reaction.insert({
|
||||
id: randomUUIDv7(),
|
||||
authorId: this.id,
|
||||
noteId: note.id,
|
||||
emojiText: emoji instanceof Emoji ? null : emoji,
|
||||
emojiId: emoji instanceof Emoji ? emoji.id : null,
|
||||
});
|
||||
|
||||
// TODO: Handle federation and notifications
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove an emoji reaction from a note
|
||||
* @param note - The note to remove reaction from
|
||||
* @param emoji - The emoji to remove reaction for (Emoji object for custom emojis, or Unicode emoji)
|
||||
*/
|
||||
public async unreact(note: Note, emoji: Emoji | string): Promise<void> {
|
||||
const reactionToDelete = await Reaction.fromEmoji(emoji, this, note);
|
||||
|
||||
if (!reactionToDelete) {
|
||||
return; // Reaction doesn't exist, nothing to delete
|
||||
}
|
||||
|
||||
await reactionToDelete.delete();
|
||||
|
||||
// TODO: Handle federation and notifications
|
||||
}
|
||||
|
||||
public async notify(
|
||||
type: "mention" | "follow_request" | "follow" | "favourite" | "reblog",
|
||||
relatedUser: User,
|
||||
|
|
|
|||
|
|
@ -40,6 +40,16 @@ export const findManyNotes = async (
|
|||
media: true,
|
||||
},
|
||||
},
|
||||
reactions: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
|
|
@ -71,6 +81,16 @@ export const findManyNotes = async (
|
|||
media: true,
|
||||
},
|
||||
},
|
||||
reactions: {
|
||||
with: {
|
||||
emoji: {
|
||||
with: {
|
||||
instance: true,
|
||||
media: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
emojis: {
|
||||
with: {
|
||||
emoji: {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue