main f6d8405

usersInternal.ts RawHistoryFolder

1import { internalQuery, internalMutation } from './_generated/server';
2import { v } from 'convex/values';
3
4// Get user by nickname (internal)
5export const getByNickname = internalQuery({
6	args: { nickname: v.string() },
7	handler: async (ctx, args) => {
8		return await ctx.db
9			.query('users')
10			.withIndex('by_nickname', (q) => q.eq('nickname', args.nickname))
11			.first();
12	}
13});
14
15// Get user by ID (internal)
16export const getById = internalQuery({
17	args: { id: v.id('users') },
18	handler: async (ctx, args) => {
19		return await ctx.db.get(args.id);
20	}
21});
22
23// Get all users (internal)
24export const list = internalQuery({
25	args: {},
26	handler: async (ctx) => {
27		return await ctx.db.query('users').collect();
28	}
29});
30
31// Create a new user (internal)
32export const create = internalMutation({
33	args: {
34		nickname: v.string(),
35		password: v.string()
36	},
37	handler: async (ctx, args) => {
38		// Check if nickname already exists
39		const existing = await ctx.db
40			.query('users')
41			.withIndex('by_nickname', (q) => q.eq('nickname', args.nickname))
42			.first();
43
44		if (existing) {
45			throw new Error('Username already taken');
46		}
47
48		const userId = await ctx.db.insert('users', {
49			nickname: args.nickname,
50			password: args.password,
51			status: 'offline',
52			createdAt: Date.now()
53		});
54
55		return userId;
56	}
57});
58
59// Update user status by ID (internal)
60export const updateStatus = internalMutation({
61	args: {
62		id: v.id('users'),
63		status: v.string()
64	},
65	handler: async (ctx, args) => {
66		const validStatuses = ['online', 'away', 'busy', 'offline', 'idle'];
67		if (!validStatuses.includes(args.status)) {
68			throw new Error('Invalid status');
69		}
70
71		const user = await ctx.db.get(args.id);
72		if (!user) {
73			throw new Error('User not found');
74		}
75
76		// An away message keeps the user away: presence heartbeats only refresh lastSeen.
77		const keepAway = user.awayMessage && args.status !== 'offline';
78		await ctx.db.patch(args.id, {
79			status: keepAway ? 'away' : args.status,
80			lastSeen: Date.now()
81		});
82
83		return await ctx.db.get(args.id);
84	}
85});
86
87// Set all users offline (internal) - used on server startup
88export const setAllOffline = internalMutation({
89	args: {},
90	handler: async (ctx) => {
91		const users = await ctx.db.query('users').collect();
92		for (const user of users) {
93			await ctx.db.patch(user._id, { status: 'offline' });
94		}
95		return { updated: users.length };
96	}
97});
98
99// Update last read mention timestamp (internal)
100export const updateLastReadMentionTimestamp = internalMutation({
101	args: {
102		id: v.id('users'),
103		timestamp: v.number()
104	},
105	handler: async (ctx, args) => {
106		const user = await ctx.db.get(args.id);
107		if (!user) {
108			throw new Error('User not found');
109		}
110
111		const currentTimestamp = user.lastReadMentionTimestamp ?? 0;
112
113		// Only update if the new timestamp is greater (more recent)
114		if (args.timestamp > currentTimestamp) {
115			await ctx.db.patch(args.id, {
116				lastReadMentionTimestamp: args.timestamp
117			});
118		}
119
120		return { success: true };
121	}
122});
123
124// Cleanup stale users who haven't sent a heartbeat in 2 minutes
125export const cleanupStaleUsers = internalMutation({
126	args: {},
127	handler: async (ctx) => {
128		const ONLINE_TIMEOUT_MS = 2 * 60 * 1000; // 2 minutes
129		const timeoutThreshold = Date.now() - ONLINE_TIMEOUT_MS;
130
131		const nonOfflineStatuses = ['online', 'away', 'busy', 'idle'];
132		let updated = 0;
133
134		for (const status of nonOfflineStatuses) {
135			const users = await ctx.db
136				.query('users')
137				.withIndex('by_status', (q) => q.eq('status', status))
138				.collect();
139
140			for (const user of users) {
141				const lastSeen = user.lastSeen ?? 0;
142				if (lastSeen < timeoutThreshold) {
143					await ctx.db.patch(user._id, { status: 'offline' });
144					updated++;
145				}
146			}
147		}
148
149		return { updated };
150	}
151});

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 3

Tags 8

↑↓ moveenter openesc close