feat(shared): parseMentionUsernames + insertMentions helpers
This commit is contained in:
@@ -8,6 +8,7 @@ export * from './messages';
|
|||||||
export * from './types';
|
export * from './types';
|
||||||
export * from './userKeyMigration';
|
export * from './userKeyMigration';
|
||||||
export * from './pinnedMessages';
|
export * from './pinnedMessages';
|
||||||
|
export * from './mentions';
|
||||||
|
|
||||||
// ----- RPC wrappers ---------------------------------------------------------
|
// ----- RPC wrappers ---------------------------------------------------------
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
|
import { parseMentionUsernames } from './mentions';
|
||||||
|
|
||||||
|
describe('parseMentionUsernames', () => {
|
||||||
|
it('extracts a leading mention', () => {
|
||||||
|
expect(parseMentionUsernames('@anna hi')).toEqual(['anna']);
|
||||||
|
});
|
||||||
|
it('extracts mid-sentence', () => {
|
||||||
|
expect(parseMentionUsernames('hey @ben_c what do you think')).toEqual(['ben_c']);
|
||||||
|
});
|
||||||
|
it('lowercases usernames', () => {
|
||||||
|
expect(parseMentionUsernames('hi @Anna')).toEqual(['anna']);
|
||||||
|
});
|
||||||
|
it('deduplicates', () => {
|
||||||
|
expect(parseMentionUsernames('@xx and @xx again')).toEqual(['xx']);
|
||||||
|
});
|
||||||
|
it('ignores emails (no preceding boundary)', () => {
|
||||||
|
expect(parseMentionUsernames('mail me at foo@bar.com')).toEqual([]);
|
||||||
|
});
|
||||||
|
it('rejects 1-char names', () => {
|
||||||
|
expect(parseMentionUsernames('@a')).toEqual([]);
|
||||||
|
});
|
||||||
|
it('handles multiple in one message', () => {
|
||||||
|
expect(parseMentionUsernames('@anna, @ben and @cara')).toEqual(['anna', 'ben', 'cara']);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
import type { AppSupabaseClient } from '../supabase/client';
|
||||||
|
|
||||||
|
// `@anna_b` style — letters, digits, underscore, dot, dash, 2-32 chars.
|
||||||
|
// Conservative on purpose: false negatives (a real username we don't match)
|
||||||
|
// are recoverable (no notification fires); false positives (matching a
|
||||||
|
// non-username) just become an INSERT that the FK check rejects.
|
||||||
|
const MENTION_RE = /(?:^|[\s,;:!?(])@([a-zA-Z0-9_.-]{2,32})/g;
|
||||||
|
|
||||||
|
export function parseMentionUsernames(plaintext: string): string[] {
|
||||||
|
const out = new Set<string>();
|
||||||
|
for (const m of plaintext.matchAll(MENTION_RE)) {
|
||||||
|
if (m[1]) out.add(m[1].toLowerCase());
|
||||||
|
}
|
||||||
|
return [...out];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MentionResolver {
|
||||||
|
// Resolves an array of @usernames in this conversation to user-ids.
|
||||||
|
// Returns only memberships that exist + are accepted.
|
||||||
|
resolveUsernames(conversationId: string, usernames: string[]): Promise<Map<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeMentionResolver(client: AppSupabaseClient): MentionResolver {
|
||||||
|
return {
|
||||||
|
async resolveUsernames(conversationId, usernames) {
|
||||||
|
if (usernames.length === 0) return new Map();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const { data, error } = await (client as any)
|
||||||
|
.from('conversation_members')
|
||||||
|
.select('user_id, accepted, profiles!inner(username)')
|
||||||
|
.eq('conversation_id', conversationId)
|
||||||
|
.eq('accepted', true)
|
||||||
|
.in('profiles.username', usernames);
|
||||||
|
if (error) throw error;
|
||||||
|
const out = new Map<string, string>();
|
||||||
|
for (const row of (data ?? []) as Array<{ user_id: string; profiles: { username: string } }>) {
|
||||||
|
out.set(row.profiles.username.toLowerCase(), row.user_id);
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function insertMentions(
|
||||||
|
client: AppSupabaseClient,
|
||||||
|
messageId: string,
|
||||||
|
conversationId: string,
|
||||||
|
mentionedUserIds: string[],
|
||||||
|
): Promise<void> {
|
||||||
|
if (mentionedUserIds.length === 0) return;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const { error } = await (client as any).from('message_mentions').insert(
|
||||||
|
mentionedUserIds.map((uid) => ({
|
||||||
|
message_id: messageId,
|
||||||
|
mentioned_user_id: uid,
|
||||||
|
conversation_id: conversationId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
if (error) throw error;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user