feat(threads): add editing, moderation retry, and emoji reactions
Add API routes and UI for editing threads and messages Allow users to retry AI moderation for failed messages Migrate thread reactions to use native emojis Implement frontend search filtering for thread list
This commit is contained in:
@@ -987,24 +987,52 @@ CREATE INDEX IF NOT EXISTS thread_messages_user_idx
|
||||
CREATE TABLE IF NOT EXISTS thread_reactions (
|
||||
thread_id integer NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
reaction_type text NOT NULL CHECK (reaction_type IN ('thumbs-up', 'heart', 'laugh', 'fire', 'eyes')),
|
||||
reaction_type text NOT NULL CHECK (length(reaction_type) BETWEEN 1 AND 24),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (thread_id, user_id, reaction_type)
|
||||
);
|
||||
|
||||
ALTER TABLE thread_reactions
|
||||
DROP CONSTRAINT IF EXISTS thread_reactions_reaction_type_check,
|
||||
ADD CONSTRAINT thread_reactions_reaction_type_check CHECK (length(reaction_type) BETWEEN 1 AND 24);
|
||||
UPDATE thread_reactions
|
||||
SET reaction_type = CASE reaction_type
|
||||
WHEN 'thumbs-up' THEN '👍'
|
||||
WHEN 'heart' THEN '❤️'
|
||||
WHEN 'laugh' THEN '😂'
|
||||
WHEN 'fire' THEN '🔥'
|
||||
WHEN 'eyes' THEN '👀'
|
||||
ELSE reaction_type
|
||||
END
|
||||
WHERE reaction_type IN ('thumbs-up', 'heart', 'laugh', 'fire', 'eyes');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS thread_reactions_thread_idx
|
||||
ON thread_reactions(thread_id, reaction_type);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS thread_message_reactions (
|
||||
message_id integer NOT NULL REFERENCES thread_messages(id) ON DELETE CASCADE,
|
||||
user_id integer NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
reaction_type text NOT NULL CHECK (reaction_type IN ('thumbs-up', 'heart', 'laugh', 'fire', 'eyes')),
|
||||
reaction_type text NOT NULL CHECK (length(reaction_type) BETWEEN 1 AND 24),
|
||||
created_at timestamptz NOT NULL DEFAULT now(),
|
||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||
PRIMARY KEY (message_id, user_id, reaction_type)
|
||||
);
|
||||
|
||||
ALTER TABLE thread_message_reactions
|
||||
DROP CONSTRAINT IF EXISTS thread_message_reactions_reaction_type_check,
|
||||
ADD CONSTRAINT thread_message_reactions_reaction_type_check CHECK (length(reaction_type) BETWEEN 1 AND 24);
|
||||
UPDATE thread_message_reactions
|
||||
SET reaction_type = CASE reaction_type
|
||||
WHEN 'thumbs-up' THEN '👍'
|
||||
WHEN 'heart' THEN '❤️'
|
||||
WHEN 'laugh' THEN '😂'
|
||||
WHEN 'fire' THEN '🔥'
|
||||
WHEN 'eyes' THEN '👀'
|
||||
ELSE reaction_type
|
||||
END
|
||||
WHERE reaction_type IN ('thumbs-up', 'heart', 'laugh', 'fire', 'eyes');
|
||||
|
||||
CREATE INDEX IF NOT EXISTS thread_message_reactions_message_idx
|
||||
ON thread_message_reactions(message_id, reaction_type);
|
||||
|
||||
|
||||
@@ -772,12 +772,47 @@ async function updateTargetStatus(
|
||||
if (status === 'approved') {
|
||||
await applyApprovedThreadMessage(target.id);
|
||||
} else {
|
||||
const row = await queryOne<{ threadId: number }>(
|
||||
'SELECT thread_id AS "threadId" FROM thread_messages WHERE id = $1',
|
||||
const row = await queryOne<{
|
||||
threadId: number;
|
||||
body: string;
|
||||
moderationStatus: AiModerationStatus;
|
||||
moderationLanguageCode: string | null;
|
||||
moderationReason: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
author: { id: number; displayName: string } | null;
|
||||
}>(
|
||||
`
|
||||
SELECT
|
||||
tm.thread_id AS "threadId",
|
||||
tm.body,
|
||||
tm.ai_moderation_status AS "moderationStatus",
|
||||
tm.ai_moderation_language_code AS "moderationLanguageCode",
|
||||
tm.ai_moderation_reason AS "moderationReason",
|
||||
tm.created_at AS "createdAt",
|
||||
tm.updated_at AS "updatedAt",
|
||||
CASE WHEN u.id IS NULL THEN NULL ELSE json_build_object('id', u.id, 'displayName', u.display_name) END AS author
|
||||
FROM thread_messages tm
|
||||
LEFT JOIN users u ON u.id = tm.created_by_user_id
|
||||
WHERE tm.id = $1
|
||||
AND tm.deleted_at IS NULL
|
||||
`,
|
||||
[target.id]
|
||||
);
|
||||
if (row) {
|
||||
await publishThreadMessageModeration(row.threadId, null);
|
||||
await publishThreadMessageModeration(row.threadId, target.id, {
|
||||
id: target.id,
|
||||
threadId: row.threadId,
|
||||
body: row.body,
|
||||
moderationStatus: row.moderationStatus,
|
||||
moderationLanguageCode: row.moderationLanguageCode,
|
||||
moderationReason: row.moderationReason,
|
||||
createdAt: row.createdAt,
|
||||
updatedAt: row.updatedAt,
|
||||
author: row.author,
|
||||
reactionCounts: {},
|
||||
myReactions: []
|
||||
});
|
||||
}
|
||||
}
|
||||
return;
|
||||
|
||||
@@ -33,8 +33,8 @@ type ListPage<T> = {
|
||||
nextCursor: string | null;
|
||||
hasMore: boolean;
|
||||
};
|
||||
export type ThreadReactionType = 'thumbs-up' | 'heart' | 'laugh' | 'fire' | 'eyes';
|
||||
export type ThreadReactionCounts = Record<ThreadReactionType, number>;
|
||||
export type ThreadReactionType = string;
|
||||
export type ThreadReactionCounts = Record<string, number>;
|
||||
export type ThreadChannelTag = { id: number; name: string; sortOrder: number };
|
||||
export type ThreadChannel = {
|
||||
id: number;
|
||||
@@ -1154,6 +1154,12 @@ function validationError(message: string): ValidationError {
|
||||
return error;
|
||||
}
|
||||
|
||||
function forbiddenError(): ValidationError {
|
||||
const error = new Error('server.errors.permissionDenied') as ValidationError;
|
||||
error.statusCode = 403;
|
||||
return error;
|
||||
}
|
||||
|
||||
function requirePositiveInteger(value: unknown, message: string): number {
|
||||
const numberValue = Number(value);
|
||||
if (!Number.isInteger(numberValue) || numberValue <= 0) {
|
||||
@@ -8891,28 +8897,31 @@ export async function importAdminHabitatsCsv(payload: Record<string, unknown>, u
|
||||
return getAdminDataToolsSummary();
|
||||
}
|
||||
|
||||
const threadReactionTypes: ThreadReactionType[] = ['thumbs-up', 'heart', 'laugh', 'fire', 'eyes'];
|
||||
const defaultThreadLimit = 20;
|
||||
const maxThreadLimit = 50;
|
||||
const defaultThreadMessageLimit = 30;
|
||||
const maxThreadMessageLimit = 80;
|
||||
const threadEmojiReactionPattern = /(?:\p{Extended_Pictographic}|\p{Regional_Indicator})/u;
|
||||
|
||||
type ThreadCursor = { value: string; id: number };
|
||||
type ThreadMessageCursor = { createdAt: string; id: number };
|
||||
|
||||
function emptyThreadReactionCounts(): ThreadReactionCounts {
|
||||
return { 'thumbs-up': 0, heart: 0, laugh: 0, fire: 0, eyes: 0 };
|
||||
}
|
||||
|
||||
function isThreadReactionType(value: unknown): value is ThreadReactionType {
|
||||
return typeof value === 'string' && threadReactionTypes.includes(value as ThreadReactionType);
|
||||
return {};
|
||||
}
|
||||
|
||||
function cleanThreadReactionType(value: unknown): ThreadReactionType {
|
||||
if (!isThreadReactionType(value)) {
|
||||
const reactionType = typeof value === 'string' ? value.trim() : '';
|
||||
if (
|
||||
!reactionType ||
|
||||
reactionType.length > 24 ||
|
||||
/\s/.test(reactionType) ||
|
||||
/[\p{Letter}\p{Number}]/u.test(reactionType) ||
|
||||
!threadEmojiReactionPattern.test(reactionType)
|
||||
) {
|
||||
throw validationError('server.validation.reactionInvalid');
|
||||
}
|
||||
return value;
|
||||
return reactionType;
|
||||
}
|
||||
|
||||
function cleanThreadLimit(value: QueryValue, fallback = defaultThreadLimit, max = maxThreadLimit): number {
|
||||
@@ -9113,7 +9122,7 @@ async function threadReactionCounts(threadIds: number[], userId: number | null):
|
||||
);
|
||||
for (const row of countRows) {
|
||||
const item = counts.get(row.threadId);
|
||||
if (item && isThreadReactionType(row.reactionType)) {
|
||||
if (item) {
|
||||
item[row.reactionType] = row.count;
|
||||
}
|
||||
}
|
||||
@@ -9129,7 +9138,6 @@ async function threadReactionCounts(threadIds: number[], userId: number | null):
|
||||
[userId, threadIds]
|
||||
);
|
||||
for (const row of myRows) {
|
||||
if (!isThreadReactionType(row.reactionType)) continue;
|
||||
mine.set(row.threadId, [...(mine.get(row.threadId) ?? []), row.reactionType]);
|
||||
}
|
||||
}
|
||||
@@ -9157,7 +9165,7 @@ async function threadMessageReactionCounts(messageIds: number[], userId: number
|
||||
);
|
||||
for (const row of countRows) {
|
||||
const item = counts.get(row.messageId);
|
||||
if (item && isThreadReactionType(row.reactionType)) {
|
||||
if (item) {
|
||||
item[row.reactionType] = row.count;
|
||||
}
|
||||
}
|
||||
@@ -9173,7 +9181,6 @@ async function threadMessageReactionCounts(messageIds: number[], userId: number
|
||||
[userId, messageIds]
|
||||
);
|
||||
for (const row of myRows) {
|
||||
if (!isThreadReactionType(row.reactionType)) continue;
|
||||
mine.set(row.messageId, [...(mine.get(row.messageId) ?? []), row.reactionType]);
|
||||
}
|
||||
}
|
||||
@@ -9464,6 +9471,68 @@ export async function createThread(payload: Record<string, unknown>, userId: num
|
||||
return (await getThread(ids.threadId, userId)) as ThreadSummary;
|
||||
}
|
||||
|
||||
export async function updateThread(
|
||||
threadIdValue: number,
|
||||
payload: Record<string, unknown>,
|
||||
userId: number,
|
||||
canUpdateAny = false
|
||||
): Promise<ThreadSummary | null> {
|
||||
const threadId = requirePositiveInteger(threadIdValue, 'server.validation.recordInvalid');
|
||||
const title = cleanThreadTitle(payload.title);
|
||||
const tagIds = cleanThreadTagIds(payload.tagIds);
|
||||
const thread = await queryOne<{ id: number; channelId: number; createdByUserId: number }>(
|
||||
`
|
||||
SELECT id, channel_id AS "channelId", created_by_user_id AS "createdByUserId"
|
||||
FROM threads
|
||||
WHERE id = $1
|
||||
AND deleted_at IS NULL
|
||||
`,
|
||||
[threadId]
|
||||
);
|
||||
if (!thread) return null;
|
||||
if (!canUpdateAny && thread.createdByUserId !== userId) {
|
||||
throw forbiddenError();
|
||||
}
|
||||
await validateThreadTags(thread.channelId, tagIds);
|
||||
|
||||
await withTransaction(async (client) => {
|
||||
await client.query('UPDATE threads SET title = $1, updated_by_user_id = $2, updated_at = now() WHERE id = $3', [title, userId, threadId]);
|
||||
await client.query('DELETE FROM thread_tag_links WHERE thread_id = $1', [threadId]);
|
||||
for (const tagId of tagIds) {
|
||||
await client.query('INSERT INTO thread_tag_links (thread_id, tag_id) VALUES ($1, $2) ON CONFLICT DO NOTHING', [threadId, tagId]);
|
||||
}
|
||||
});
|
||||
|
||||
return getThread(threadId, userId);
|
||||
}
|
||||
|
||||
async function refreshThreadMessageAggregates(threadId: number): Promise<void> {
|
||||
await pool.query(
|
||||
`
|
||||
UPDATE threads t
|
||||
SET message_count = (
|
||||
SELECT COUNT(*)::integer
|
||||
FROM thread_messages tm
|
||||
WHERE tm.thread_id = t.id
|
||||
AND tm.deleted_at IS NULL
|
||||
AND tm.ai_moderation_status = 'approved'
|
||||
),
|
||||
last_message_id = (
|
||||
SELECT tm.id
|
||||
FROM thread_messages tm
|
||||
WHERE tm.thread_id = t.id
|
||||
AND tm.deleted_at IS NULL
|
||||
AND tm.ai_moderation_status = 'approved'
|
||||
ORDER BY tm.created_at DESC, tm.id DESC
|
||||
LIMIT 1
|
||||
),
|
||||
updated_at = now()
|
||||
WHERE t.id = $1
|
||||
`,
|
||||
[threadId]
|
||||
);
|
||||
}
|
||||
|
||||
export async function createThreadMessage(threadIdValue: number, payload: Record<string, unknown>, userId: number): Promise<ThreadMessage | null> {
|
||||
const threadId = requirePositiveInteger(threadIdValue, 'server.validation.recordInvalid');
|
||||
const body = cleanThreadMessageBody(payload.body);
|
||||
@@ -9488,6 +9557,87 @@ export async function createThreadMessage(threadIdValue: number, payload: Record
|
||||
return getThreadMessageById(result.id, userId, false);
|
||||
}
|
||||
|
||||
export async function updateThreadMessage(
|
||||
messageIdValue: number,
|
||||
payload: Record<string, unknown>,
|
||||
userId: number,
|
||||
canUpdateAny = false
|
||||
): Promise<ThreadMessage | null> {
|
||||
const messageId = requirePositiveInteger(messageIdValue, 'server.validation.recordInvalid');
|
||||
const body = cleanThreadMessageBody(payload.body);
|
||||
const message = await queryOne<{ id: number; threadId: number; languageCode: string; createdByUserId: number }>(
|
||||
`
|
||||
SELECT
|
||||
tm.id,
|
||||
tm.thread_id AS "threadId",
|
||||
t.language_code AS "languageCode",
|
||||
tm.created_by_user_id AS "createdByUserId"
|
||||
FROM thread_messages tm
|
||||
JOIN threads t ON t.id = tm.thread_id
|
||||
WHERE tm.id = $1
|
||||
AND tm.deleted_at IS NULL
|
||||
AND t.deleted_at IS NULL
|
||||
`,
|
||||
[messageId]
|
||||
);
|
||||
if (!message) return null;
|
||||
if (!canUpdateAny && message.createdByUserId !== userId) {
|
||||
throw forbiddenError();
|
||||
}
|
||||
|
||||
const result = await queryOne<{ id: number }>(
|
||||
`
|
||||
UPDATE thread_messages
|
||||
SET body = $1,
|
||||
ai_moderation_status = 'reviewing',
|
||||
ai_moderation_language_code = NULL,
|
||||
ai_moderation_reason = NULL,
|
||||
ai_moderation_content_hash = NULL,
|
||||
ai_moderation_checked_at = NULL,
|
||||
ai_moderation_retry_count = 0,
|
||||
ai_moderation_updated_at = now(),
|
||||
updated_at = now()
|
||||
WHERE id = $2
|
||||
AND deleted_at IS NULL
|
||||
RETURNING id
|
||||
`,
|
||||
[body, messageId]
|
||||
);
|
||||
if (!result) return null;
|
||||
|
||||
await refreshThreadMessageAggregates(message.threadId);
|
||||
await publishThreadMessageModeration(message.threadId, messageId, null);
|
||||
await requestAiModerationReview({ type: 'thread-message', id: messageId }, { languageCode: message.languageCode, resetRetries: true });
|
||||
return getThreadMessageById(messageId, userId, canUpdateAny);
|
||||
}
|
||||
|
||||
export async function retryThreadMessageModeration(
|
||||
messageIdValue: number,
|
||||
userId: number,
|
||||
canRetryAny = false
|
||||
): Promise<ThreadMessage | null> {
|
||||
const messageId = requirePositiveInteger(messageIdValue, 'server.validation.recordInvalid');
|
||||
const message = await queryOne<{ id: number; createdByUserId: number }>(
|
||||
`
|
||||
SELECT tm.id, tm.created_by_user_id AS "createdByUserId"
|
||||
FROM thread_messages tm
|
||||
JOIN threads t ON t.id = tm.thread_id
|
||||
WHERE tm.id = $1
|
||||
AND tm.deleted_at IS NULL
|
||||
AND t.deleted_at IS NULL
|
||||
AND tm.ai_moderation_status IN ('unreviewed', 'rejected', 'failed')
|
||||
`,
|
||||
[messageId]
|
||||
);
|
||||
if (!message) return null;
|
||||
if (!canRetryAny && message.createdByUserId !== userId) {
|
||||
throw forbiddenError();
|
||||
}
|
||||
|
||||
await requestAiModerationReview({ type: 'thread-message', id: messageId }, { incrementRetries: true });
|
||||
return getThreadMessageById(messageId, userId, canRetryAny);
|
||||
}
|
||||
|
||||
export async function markThreadRead(threadIdValue: number, userId: number): Promise<ThreadSummary | null> {
|
||||
const threadId = requirePositiveInteger(threadIdValue, 'server.validation.recordInvalid');
|
||||
const row = await queryOne<{ lastMessageId: number | null }>(
|
||||
@@ -9560,7 +9710,18 @@ export async function deleteThreadReaction(threadIdValue: number, payload: Recor
|
||||
const thread = await getThread(threadId, userId);
|
||||
if (!thread) return null;
|
||||
await pool.query('DELETE FROM thread_reactions WHERE thread_id = $1 AND user_id = $2 AND reaction_type = $3', [threadId, userId, reactionType]);
|
||||
return getThread(threadId, userId);
|
||||
const updated = await getThread(threadId, userId);
|
||||
if (updated) {
|
||||
await publishThreadReactionUpdated(userId, {
|
||||
type: 'thread.reactions.updated',
|
||||
target: 'thread',
|
||||
threadId,
|
||||
messageId: null,
|
||||
reactionCounts: updated.reactionCounts,
|
||||
myReactions: updated.myReactions
|
||||
});
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function setThreadMessageReaction(messageIdValue: number, payload: Record<string, unknown>, userId: number): Promise<ThreadMessage | null> {
|
||||
@@ -9597,7 +9758,18 @@ export async function deleteThreadMessageReaction(messageIdValue: number, payloa
|
||||
const message = await getThreadMessageById(messageId, userId);
|
||||
if (!message) return null;
|
||||
await pool.query('DELETE FROM thread_message_reactions WHERE message_id = $1 AND user_id = $2 AND reaction_type = $3', [messageId, userId, reactionType]);
|
||||
return getThreadMessageById(messageId, userId);
|
||||
const updated = await getThreadMessageById(messageId, userId);
|
||||
if (updated) {
|
||||
await publishThreadReactionUpdated(userId, {
|
||||
type: 'thread.reactions.updated',
|
||||
target: 'message',
|
||||
threadId: updated.threadId,
|
||||
messageId,
|
||||
reactionCounts: updated.reactionCounts,
|
||||
myReactions: updated.myReactions
|
||||
});
|
||||
}
|
||||
return updated;
|
||||
}
|
||||
|
||||
export async function applyApprovedThreadMessage(messageId: number): Promise<void> {
|
||||
@@ -9629,7 +9801,7 @@ export async function applyApprovedThreadMessage(messageId: number): Promise<voi
|
||||
if (message && thread) {
|
||||
await publishThreadMessageCreated(thread, message);
|
||||
} else {
|
||||
await publishThreadMessageModeration(row.threadId, message);
|
||||
await publishThreadMessageModeration(row.threadId, messageId, message);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -131,6 +131,7 @@ import {
|
||||
retryEntityDiscussionCommentModeration,
|
||||
retryLifeCommentModeration,
|
||||
retryLifePostModeration,
|
||||
retryThreadMessageModeration,
|
||||
restoreLifeComment,
|
||||
setLifePostRating,
|
||||
setLifePostReaction,
|
||||
@@ -150,7 +151,9 @@ import {
|
||||
updatePokemon,
|
||||
updateRecipe,
|
||||
updateAdminThreadChannel,
|
||||
updateThread,
|
||||
updateThreadLock,
|
||||
updateThreadMessage,
|
||||
unfollowUser,
|
||||
unfollowThread,
|
||||
wipeAdminData
|
||||
@@ -1734,6 +1737,17 @@ app.post('/api/threads', async (request, reply) => {
|
||||
return user ? reply.code(201).send(await createThread(request.body as Record<string, unknown>, user.id)) : undefined;
|
||||
});
|
||||
|
||||
app.put('/api/threads/:id', async (request, reply) => {
|
||||
const user = await requireVerifiedUser(request, reply);
|
||||
if (!user || !(await enforceUserRateLimits(request, reply, user, 'communityWrite'))) {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const canUpdateAny = userHasPermission(user, 'admin.threads.threads.lock') || userHasPermission(user, 'admin.threads.threads.delete');
|
||||
const thread = await updateThread(Number(id), request.body as Record<string, unknown>, user.id, canUpdateAny);
|
||||
return thread ? thread : notFound(reply, request);
|
||||
});
|
||||
|
||||
app.get('/api/threads/:id', async (request, reply) => {
|
||||
const { id } = request.params as { id: string };
|
||||
const user = await optionalUser(request);
|
||||
@@ -1764,6 +1778,38 @@ app.post('/api/threads/:id/messages', async (request, reply) => {
|
||||
return message ? reply.code(201).send(message) : notFound(reply, request);
|
||||
});
|
||||
|
||||
app.put('/api/thread-messages/:id', async (request, reply) => {
|
||||
const user = await requireAnyPermissionWithRateLimits(
|
||||
request,
|
||||
reply,
|
||||
['threads.messages.create', 'admin.threads.messages.delete'],
|
||||
'communityWrite'
|
||||
);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const canUpdateAny = userHasPermission(user, 'admin.threads.messages.delete');
|
||||
const message = await updateThreadMessage(Number(id), request.body as Record<string, unknown>, user.id, canUpdateAny);
|
||||
return message ? message : notFound(reply, request);
|
||||
});
|
||||
|
||||
app.post('/api/thread-messages/:id/moderation/retry', async (request, reply) => {
|
||||
const user = await requireAnyPermissionWithRateLimits(
|
||||
request,
|
||||
reply,
|
||||
['threads.messages.create', 'admin.threads.messages.delete'],
|
||||
'communityWrite'
|
||||
);
|
||||
if (!user) {
|
||||
return;
|
||||
}
|
||||
const { id } = request.params as { id: string };
|
||||
const canRetryAny = userHasPermission(user, 'admin.threads.messages.delete');
|
||||
const message = await retryThreadMessageModeration(Number(id), user.id, canRetryAny);
|
||||
return message ? message : notFound(reply, request);
|
||||
});
|
||||
|
||||
app.put('/api/threads/:id/follow', async (request, reply) => {
|
||||
const user = await requirePermissionWithRateLimits(request, reply, 'threads.follow', 'communityReaction');
|
||||
if (!user) {
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { ThreadMessage, ThreadReactionCounts, ThreadReactionType, ThreadSum
|
||||
export type ThreadWsMessage =
|
||||
| { type: 'threads.connected'; followedUnreadCount: number }
|
||||
| { type: 'thread.message.created'; threadId: number; message: ThreadMessage; thread: ThreadSummary }
|
||||
| { type: 'thread.message.moderation'; threadId: number; message: ThreadMessage | null }
|
||||
| { type: 'thread.message.moderation'; threadId: number; messageId: number; message: ThreadMessage | null }
|
||||
| {
|
||||
type: 'thread.reactions.updated';
|
||||
target: 'thread' | 'message';
|
||||
@@ -311,7 +311,7 @@ export async function applyApprovedThreadMessage(messageId: number): Promise<voi
|
||||
lastActiveAt: row.lastActiveAt,
|
||||
createdAt: row.threadCreatedAt,
|
||||
author: row.threadAuthor,
|
||||
reactionCounts: { 'thumbs-up': 0, heart: 0, laugh: 0, fire: 0, eyes: 0 },
|
||||
reactionCounts: {},
|
||||
myReactions: [],
|
||||
followed: true,
|
||||
unread: true
|
||||
@@ -326,16 +326,37 @@ export async function applyApprovedThreadMessage(messageId: number): Promise<voi
|
||||
createdAt: row.messageCreatedAt,
|
||||
updatedAt: row.messageUpdatedAt,
|
||||
author: row.messageAuthor,
|
||||
reactionCounts: { 'thumbs-up': 0, heart: 0, laugh: 0, fire: 0, eyes: 0 },
|
||||
reactionCounts: {},
|
||||
myReactions: []
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishThreadMessageModeration(threadId: number, message: ThreadMessage | null): Promise<void> {
|
||||
await publishToUsers([...new Set([...(await recipientUserIds(threadId)), ...connectedUserIds()])], {
|
||||
export async function publishThreadMessageModeration(
|
||||
threadId: number,
|
||||
messageId: number,
|
||||
message: ThreadMessage | null
|
||||
): Promise<void> {
|
||||
const publicUsers = new Set([...(await recipientUserIds(threadId)), ...connectedUserIds()]);
|
||||
if (message?.author?.id) {
|
||||
publicUsers.delete(message.author.id);
|
||||
}
|
||||
|
||||
await publishToUsers([...publicUsers], {
|
||||
type: 'thread.message.moderation',
|
||||
threadId,
|
||||
messageId,
|
||||
message: null
|
||||
});
|
||||
|
||||
if (!message?.author?.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
await publishToUsers([message.author.id], {
|
||||
type: 'thread.message.moderation',
|
||||
threadId,
|
||||
messageId,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user