diff --git a/app/components/Chat.tsx b/app/components/Chat.tsx index 531aaab..df8e2ba 100644 --- a/app/components/Chat.tsx +++ b/app/components/Chat.tsx @@ -11,6 +11,7 @@ import Messages from "./chat/Messages"; export interface Conversation { id: string; users: { id: string; email: string }[]; + type: string; } export interface Message { @@ -51,8 +52,27 @@ export default function Chat({ user }: { user: User }) { const channelRef = useRef(null); const textareaRef = useRef(null); const bottomRef = useRef(null); + const [badWords, setBadWords] = useState([]); const creatingRef = useRef(false); + useEffect(() => { + fetch( + "https://raw.githubusercontent.com/LDNOOBW/List-of-Dirty-Naughty-Obscene-and-Otherwise-Bad-Words/refs/heads/master/en", + ) + .then((res) => res.text()) + .then((text) => { + const wordsArray = text.split("\n").filter(Boolean); + setBadWords(wordsArray); + }); + }, []); + + const sanitizeInput = (input: string) => { + if (!badWords.length) return input; + + const filter = new RegExp(`\\b(${badWords.join("|")})\\b`, "gi"); + return input.replace(filter, "*-?;[]"); + }; + useEffect(() => { if (textareaRef.current) { const el = textareaRef.current; @@ -70,7 +90,8 @@ export default function Chat({ user }: { user: User }) { ` conversation: conversations( id, - users: conversation_participants!inner(user_id, email) + users: conversation_participants!inner(user_id, email), + type ) `, ) @@ -88,9 +109,15 @@ export default function Chat({ user }: { user: User }) { id: u.user_id, email: u.email, })), + type: convo.type, }; }); - setConversations(convs); + + // making sure global conversation is always first + const sortedConvs = convs.sort((a, b) => + a.type === "global" ? -1 : b.type === "global" ? 1 : 0, + ); + setConversations(sortedConvs); } }; fetchConversations(); @@ -117,7 +144,8 @@ export default function Chat({ user }: { user: User }) { .select( ` id, - users:conversation_participants!inner(user_id) + users:conversation_participants!inner(user_id), + type `, ) .eq("id", row.conversation_id) @@ -142,6 +170,7 @@ export default function Chat({ user }: { user: User }) { id: u.user_id, email: row.email, })), + type: convo.type, }, ]); }); @@ -282,6 +311,7 @@ export default function Chat({ user }: { user: User }) { { id: user.id, email: user.email ?? "" }, { id: otherUser.user_id, email: otherUser.email ?? "" }, ], + type: "private", }, ]); @@ -295,7 +325,7 @@ export default function Chat({ user }: { user: User }) { await supabase.from("messages").insert({ conversation_id: conversationId, sender_id: user.id, - text: input.slice(0, 1000), // limit to 1000 chars + text: sanitizeInput(input.slice(0, 1000)), // limit to 1000 chars }); setTimeout(() => { diff --git a/app/components/chat/Conversations.tsx b/app/components/chat/Conversations.tsx index 2410482..9d198fb 100644 --- a/app/components/chat/Conversations.tsx +++ b/app/components/chat/Conversations.tsx @@ -30,15 +30,17 @@ export default function Conversations({ ${isActive ? "bg-indigo-500 text-white" : "bg-white/5 text-gray-300"} `} > - {otherUser?.email[0]?.toUpperCase()} + {conv.type == "global" ? "G" : otherUser?.email[0]?.toUpperCase()} - {(() => { - const name = otherUser?.email?.split("@")[0] || ""; - return name.length > 10 ? name.slice(0, 8) + "..." : name; - })()} + {conv.type == "global" + ? "Global" + : (() => { + const name = otherUser?.email?.split("@")[0] || ""; + return name.length > 10 ? name.slice(0, 8) + "..." : name; + })()} ); diff --git a/app/components/chat/Messages.tsx b/app/components/chat/Messages.tsx index e7abf7e..a11a0e0 100644 --- a/app/components/chat/Messages.tsx +++ b/app/components/chat/Messages.tsx @@ -76,8 +76,26 @@ export default function Messages({ )}
+ + { + conversations + .find( + (conv) => + conv.id === msg.conversation_id && + conv.type === "global", + ) + ?.users.find((u) => u.id === msg.sender_id) + ?.email.split("@")[0] + } +
diff --git a/app/components/landing-page/TopLeaderbord.tsx b/app/components/landing-page/TopLeaderbord.tsx index 137d8cd..da31187 100644 --- a/app/components/landing-page/TopLeaderbord.tsx +++ b/app/components/landing-page/TopLeaderbord.tsx @@ -1,7 +1,10 @@ +import { Json } from "@/app/supabase-types"; + export interface TopMember { email: string; total_seconds: number; - categories?: { name: string; total_seconds: number }[]; + categories: { name: string; total_seconds: number }[] | null; + user_id: string | null; } export default function TopLeaderboard({ diff --git a/app/page.tsx b/app/page.tsx index ca2c275..3d36006 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -11,6 +11,7 @@ import TopLeaderboard, { } from "./components/landing-page/TopLeaderbord"; import ContributeCard from "./components/landing-page/ContributeCard"; import VibeCoders from "./components/landing-page/VibeCoders"; +import { Json } from "./supabase-types"; export default async function Home() { const supabase = await createClient(); @@ -44,14 +45,14 @@ export default async function Home() { const topMembers: TopMember[] = top_members ? top_members.filter( - (u): u is { email: string; total_seconds: number; user_id: string } => + (u): u is TopMember => u.email !== null && u.total_seconds !== null && u.user_id !== null, ) : []; const losserMembers: TopMember[] = losser_members ? losser_members.filter( - (u): u is { email: string; total_seconds: number; user_id: string } => + (u): u is TopMember => u.email !== null && u.total_seconds !== null && u.user_id !== null, ) : []; diff --git a/app/supabase-types.ts b/app/supabase-types.ts index ecb18b5..1e730f2 100644 --- a/app/supabase-types.ts +++ b/app/supabase-types.ts @@ -44,14 +44,17 @@ export type Database = { Row: { created_at: string id: string + type: string } Insert: { created_at?: string id?: string + type?: string } Update: { created_at?: string id?: string + type?: string } Relationships: [] } @@ -262,6 +265,7 @@ export type Database = { } top_user_stats: { Row: { + categories: Json | null email: string | null total_seconds: number | null user_id: string | null diff --git a/supabase/migrations/20260323033205_add_type_to_conversations.sql b/supabase/migrations/20260323033205_add_type_to_conversations.sql new file mode 100644 index 0000000..15e105c --- /dev/null +++ b/supabase/migrations/20260323033205_add_type_to_conversations.sql @@ -0,0 +1 @@ +ALTER TABLE conversations ADD COLUMN type text NOT NULL DEFAULT 'private'; diff --git a/supabase/migrations/20260323033729_add_global_chat.sql b/supabase/migrations/20260323033729_add_global_chat.sql new file mode 100644 index 0000000..5638d54 --- /dev/null +++ b/supabase/migrations/20260323033729_add_global_chat.sql @@ -0,0 +1,6 @@ +-- Add a global conversation +INSERT INTO conversations (id, type) VALUES ('00000000-0000-0000-0000-000000000001', 'global'); + +INSERT INTO conversation_participants (conversation_id, user_id, email) +SELECT '00000000-0000-0000-0000-000000000001', id, email +FROM auth.users; diff --git a/supabase/migrations/20260323041448_add_type_to_conversation_participants.sql b/supabase/migrations/20260323041448_add_type_to_conversation_participants.sql new file mode 100644 index 0000000..629c317 --- /dev/null +++ b/supabase/migrations/20260323041448_add_type_to_conversation_participants.sql @@ -0,0 +1,6 @@ +ALTER TABLE conversation_participants +ADD COLUMN type text NOT NULL DEFAULT 'private'; + +UPDATE conversation_participants +SET type = 'global' +WHERE conversation_id = '00000000-0000-0000-0000-000000000001'; diff --git a/supabase/migrations/20260323041543_add_trigger_to_type.sql b/supabase/migrations/20260323041543_add_trigger_to_type.sql new file mode 100644 index 0000000..f72ab2a --- /dev/null +++ b/supabase/migrations/20260323041543_add_trigger_to_type.sql @@ -0,0 +1,12 @@ +CREATE OR REPLACE FUNCTION sync_conversation_type() +RETURNS TRIGGER AS $$ +BEGIN + NEW.type := (SELECT type FROM conversations WHERE id = NEW.conversation_id); + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER conversation_type_trigger +BEFORE INSERT ON conversation_participants +FOR EACH ROW +EXECUTE FUNCTION sync_conversation_type();