
Security News
OpenCode User Hit with Hostile Backlash After Reporting Free AI Loophole
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.
convex-unread-tracking
Advanced tools
Real-time unread message counter and read receipt tracker for Convex — tracks read positions, supports bulk subscriptions via groups, and provides accurate unread counts with optimistic client updates
Real-time unread message counter and read receipt tracker for Convex.
Tracks read positions per user per channel, supports bulk subscriptions via groups, and provides accurate unread counts. Built as a Convex Component.
npm install convex-unread-tracking
// convex/convex.config.ts
import { defineApp } from "convex/server";
import unreadTracking from "convex-unread-tracking/convex.config";
const app = defineApp();
app.use(unreadTracking);
export default app;
// convex/unreads.ts
import { UnreadTracking } from "convex-unread-tracking";
import { components } from "./_generated/api.js";
import { mutation, query } from "./_generated/server.js";
import { v } from "convex/values";
const unreads = new UnreadTracking(components.unreadTracking);
export const sendMessage = mutation({
args: { channelId: v.string(), userId: v.string(), text: v.string() },
handler: async (ctx, args) => {
const now = Date.now();
const msgId = await ctx.db.insert("messages", { ...args, timestamp: now });
// Register with unread tracking (same transaction)
await unreads.insertMessage(ctx, {
channelId: args.channelId,
timestamp: now,
authorId: args.userId,
});
return msgId;
},
});
export const markRead = mutation({
args: { userId: v.string(), channelId: v.string(), timestamp: v.number() },
handler: async (ctx, args) => {
await unreads.markReadUpTo(ctx, args);
},
});
export const getUnreadCount = query({
args: { userId: v.string(), channelId: v.string() },
handler: async (ctx, args) => {
return await unreads.getUnreadCount(ctx, args);
},
});
export const subscribe = mutation({
args: { userId: v.string(), channelId: v.string() },
handler: async (ctx, args) => {
await unreads.subscribe(ctx, args);
},
});
| Method | Description |
|---|---|
markReadUpTo(ctx, { userId, channelId, timestamp }) | Mark everything up to timestamp as read |
markReadRange(ctx, { userId, channelId, start, end }) | Mark a specific range as read |
markOneUnread(ctx, { userId, channelId, timestamp }) | Mark a single item as unread |
getLastRead(ctx, { userId, channelId }) | Get read position for scroll anchor |
getLastReads(ctx, { userId, channelIds }) | Batch fetch read positions |
| Method | Description |
|---|---|
insertMessage(ctx, { channelId, timestamp?, authorId? }) | Register a message for unread counting |
deleteMessage(ctx, { channelId, timestamp }) | Remove a message from tracking |
| Method | Description |
|---|---|
getUnreadCount(ctx, { userId, channelId }) | Count for a single channel |
getTotalUnreadCount(ctx, { userId }) | Aggregate across all subscriptions |
getSingleUnreads(ctx, { userId, channelIds }) | Batch fetch per-channel counts |
| Method | Description |
|---|---|
subscribe(ctx, { userId, channelId }) | Start tracking unreads |
unsubscribe(ctx, { userId, channelId }) | Stop tracking (mute) |
muteSender(ctx, { userId, targetUserId }) | Filter out a sender |
unmuteSender(ctx, { userId, targetUserId }) | Remove sender filter |
archive(ctx, { channelId }) | Stop all tracking for a channel |
| Method | Description |
|---|---|
addToGroup(ctx, { groupId, userId }) | Add user to a group |
removeFromGroup(ctx, { groupId, userId }) | Remove from group |
subscribeAll(ctx, { groupId, channelId }) | Subscribe entire group |
unsubscribeAll(ctx, { groupId, channelId }) | Unsubscribe entire group |
// convex/crons.ts
import { cronJobs } from "convex/server";
const crons = cronJobs();
crons.daily("cleanup unreads", { hourUTC: 3, minuteUTC: 0 },
internal.unreads.cleanup);
export default crons;
import { useMarkAsRead, useOptimisticUnreadCount } from "convex-unread-tracking/react";
function Channel({ channelId }) {
const { markRead, getPendingTimestamp, confirmRead } = useMarkAsRead();
// Optimistic count — shows 0 immediately after marking read
const serverCount = useQuery(api.unreads.getUnreadCount, { userId, channelId });
const count = useOptimisticUnreadCount(serverCount, getPendingTimestamp(channelId) !== null);
const handleMarkRead = async () => {
markRead(channelId, Date.now()); // optimistic
await markReadMutation({ userId, channelId, timestamp: Date.now() });
confirmRead(channelId); // confirm sync
};
}
MIT
FAQs
Real-time unread message counter and read receipt tracker for Convex — tracks read positions, supports bulk subscriptions via groups, and provides accurate unread counts with optimistic client updates
The npm package convex-unread-tracking receives a total of 2,520 weekly downloads. As such, convex-unread-tracking popularity was classified as popular.
We found that convex-unread-tracking demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.

Security News
An OpenCode user faced harassment after reporting a free AI usage loophole, drawing a bitter backlash in a GitHub thread that has since been deleted.

Threat Research
/Security News
319 browser extensions installed by 13.4 million users hijack searches, capture queries, and collect referral payments from Yahoo and Bing.

Threat Research
Malicious Firefox extensions mimic Phantom, Jupiter, and Bitget wallets to steal recovery phrases and private keys, with one fetching its payload remotely.