1import { defineSchema, defineTable } from 'convex/server';
2import { v } from 'convex/values';
3
4export default defineSchema({
5 users: defineTable({
6 password: v.string(),
7 nickname: v.string(),
8 status: v.string(), // 'offline' | 'online' | 'away' | 'busy' | 'idle'
9 avatarUrl: v.optional(v.string()),
10 createdAt: v.number(),
11 lastSeen: v.optional(v.number()),
12 lastReadMentionTimestamp: v.optional(v.number()), // Timestamp of last read mention
13 // AIM profile
14 awayMessage: v.optional(v.string()), // Shown while status === 'away'
15 profile: v.optional(v.string()) // Free text "Get Info" profile
16 })
17 .index('by_nickname', ['nickname'])
18 .index('by_status', ['status']),
19
20 sessions: defineTable({
21 tokenHash: v.string(), // SHA-256 hash of the session token
22 userId: v.id('users'),
23 expiresAt: v.number(),
24 createdAt: v.number()
25 })
26 .index('by_tokenHash', ['tokenHash'])
27 .index('by_userId', ['userId']),
28
29 chatRooms: defineTable({
30 name: v.optional(v.string()),
31 type: v.string(), // 'direct' | 'group'
32 createdAt: v.number(),
33 createdBy: v.optional(v.id('users')),
34 topic: v.optional(v.string()),
35 // Direct rooms: exactly two members. Group rooms: undefined (open to all).
36 memberIds: v.optional(v.array(v.id('users'))),
37 // Stable key for direct rooms: sorted user ids joined by ':'
38 directKey: v.optional(v.string())
39 }).index('by_directKey', ['directKey']),
40
41 messages: defineTable({
42 chatRoomId: v.id('chatRooms'),
43 senderId: v.id('users'),
44 content: v.string(),
45 type: v.string(), // 'chat' | 'emote' | 'system' | 'auto-response'
46 timestamp: v.number(),
47 styleData: v.optional(v.string()),
48 hasFormatting: v.optional(v.boolean())
49 })
50 .index('by_chatRoom', ['chatRoomId'])
51 .index('by_timestamp', ['timestamp']),
52
53 // AIM buddy list: one row per (owner, buddy)
54 buddies: defineTable({
55 userId: v.id('users'),
56 buddyId: v.id('users'),
57 group: v.string(), // 'Buddies' | 'Family' | 'Co-Workers' | custom
58 createdAt: v.number()
59 })
60 .index('by_user', ['userId'])
61 .index('by_user_buddy', ['userId', 'buddyId']),
62
63 // Per-user read cursor per room (unread badges)
64 roomReads: defineTable({
65 userId: v.id('users'),
66 roomId: v.id('chatRooms'),
67 lastReadTimestamp: v.number()
68 })
69 .index('by_user', ['userId'])
70 .index('by_user_room', ['userId', 'roomId']),
71
72 // Typing indicators (short-lived, filtered by timestamp at read time)
73 typing: defineTable({
74 userId: v.id('users'),
75 roomId: v.id('chatRooms'),
76 updatedAt: v.number()
77 })
78 .index('by_room', ['roomId'])
79 .index('by_user_room', ['userId', 'roomId']),
80
81 userTextPreferences: defineTable({
82 userId: v.id('users'),
83 defaultStyle: v.object({
84 fontFamily: v.string(),
85 fontSize: v.number(),
86 color: v.optional(v.string()),
87 gradient: v.optional(v.array(v.string())),
88 bold: v.boolean(),
89 italic: v.boolean(),
90 underline: v.boolean(),
91 strikethrough: v.boolean()
92 }),
93 allowFormatting: v.boolean(),
94 maxMessageLength: v.number(),
95 createdAt: v.number(),
96 updatedAt: v.number()
97 }).index('by_userId', ['userId'])
98});