'use server';
/**
* User Mentions Server Actions
*
* Handles @username mentions in forum posts and bill comments.
* Creates entries in user_mentions table for notification/feed purposes.
*/
import { createAdminClient, createServerClient } from '@/lib/supabase-server';
/**
* Context types for mentions
*/
export type MentionContextType = 'forum_post' | 'forum_reply' | 'bill_comment';
/**
* Extract @username mentions from text
*
* Matches @username patterns where username:
* - Starts with a letter
* - Contains 3-30 characters (letters, numbers, underscores, hyphens)
* - Does not include a colon (to avoid @type:id entity mentions)
*/
function extractUserMentions(text: string): string[] {
// Match @username but not @type:id patterns
const mentionRegex = /@([a-z][a-z0-9_-]{2,29})(?!:)/gi;
const matches: string[] = [];
let match: RegExpExecArray | null;
while ((match = mentionRegex.exec(text)) !== null) {
const username = match[1].toLowerCase();
if (!matches.includes(username)) {
matches.push(username);
}
}
return matches;
}
/**
* Create user mention records for a post
*
* @param postId - The forum post ID where the mention occurred
* @param content - The post content to scan for @username mentions
* @param mentionerUserId - The user who wrote the post (mentioner)
* @param contextType - Type of content (forum_post, forum_reply, bill_comment)
* @returns Object with success status and count of mentions created
*/
export async function createUserMentions(
postId: string,
content: string,
mentionerUserId: string,
contextType: MentionContextType
): Promise<{ success: boolean; mentionsCreated: number; error?: string }> {
try {
const supabase = await createServerClient();
const adminClient = createAdminClient();
// Extract @username mentions from content
const usernames = extractUserMentions(content);
if (usernames.length === 0) {
return { success: true, mentionsCreated: 0 };
}
// Look up user IDs for the mentioned usernames
const { data: users, error: lookupError } = await supabase
.from('user_profiles')
.select('id, username')
.in('username', usernames);
if (lookupError) {
console.error('Error looking up mentioned users:', lookupError);
return { success: false, mentionsCreated: 0, error: lookupError.message };
}
if (!users || users.length === 0) {
return { success: true, mentionsCreated: 0 };
}
// Filter out self-mentions (user mentioning themselves)
const mentionedUsers = users.filter((u) => u.id !== mentionerUserId);
if (mentionedUsers.length === 0) {
return { success: true, mentionsCreated: 0 };
}
// Create mention records
const mentionRecords = mentionedUsers.map((user) => ({
mentioned_user_id: user.id,
mentioner_user_id: mentionerUserId,
post_id: postId,
context_type: contextType,
context_id: postId, // For forum posts, context_id = post_id
}));
// Insert mentions using admin client (bypasses RLS for insert)
const { error: insertError } = await adminClient
.from('user_mentions')
.insert(mentionRecords);
if (insertError) {
console.error('Error inserting user mentions:', insertError);
return { success: false, mentionsCreated: 0, error: insertError.message };
}
return { success: true, mentionsCreated: mentionedUsers.length };
} catch (error) {
console.error('Error in createUserMentions:', error);
return {
success: false,
mentionsCreated: 0,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Mark a mention as read
*
* @param mentionId - The user_mentions record ID
* @returns Success status
*/
export async function markMentionAsRead(
mentionId: string
): Promise<{ success: boolean; error?: string }> {
try {
const supabase = await createServerClient();
const { error } = await supabase
.from('user_mentions')
.update({ read_at: new Date().toISOString() })
.eq('id', mentionId);
if (error) {
console.error('Error marking mention as read:', error);
return { success: false, error: error.message };
}
return { success: true };
} catch (error) {
console.error('Error in markMentionAsRead:', error);
return {
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Mark all mentions as read for a user
*
* @returns Success status and count of mentions marked
*/
export async function markAllMentionsAsRead(): Promise<{
success: boolean;
count: number;
error?: string;
}> {
try {
const supabase = await createServerClient();
const { data, error } = await supabase
.from('user_mentions')
.update({ read_at: new Date().toISOString() })
.is('read_at', null)
.select('id');
if (error) {
console.error('Error marking all mentions as read:', error);
return { success: false, count: 0, error: error.message };
}
return { success: true, count: data?.length || 0 };
} catch (error) {
console.error('Error in markAllMentionsAsRead:', error);
return {
success: false,
count: 0,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}
/**
* Get unread mention count for current user
*
* @returns Count of unread mentions
*/
export async function getUnreadMentionCount(): Promise<{
success: boolean;
count: number;
error?: string;
}> {
try {
const supabase = await createServerClient();
const { count, error } = await supabase
.from('user_mentions')
.select('*', { count: 'exact', head: true })
.is('read_at', null);
if (error) {
console.error('Error getting unread mention count:', error);
return { success: false, count: 0, error: error.message };
}
return { success: true, count: count || 0 };
} catch (error) {
console.error('Error in getUnreadMentionCount:', error);
return {
success: false,
count: 0,
error: error instanceof Error ? error.message : 'Unknown error',
};
}
}