feat: Add initial frontend components and their corresponding build assets, along with generated API types and configuration.
Some checks failed
Build and Release / build-and-release (push) Failing after 7m50s
Some checks failed
Build and Release / build-and-release (push) Failing after 7m50s
This commit is contained in:
2
convex/_generated/api.d.ts
vendored
2
convex/_generated/api.d.ts
vendored
@@ -19,6 +19,7 @@ import type * as members from "../members.js";
|
||||
import type * as messages from "../messages.js";
|
||||
import type * as presence from "../presence.js";
|
||||
import type * as reactions from "../reactions.js";
|
||||
import type * as readState from "../readState.js";
|
||||
import type * as roles from "../roles.js";
|
||||
import type * as typing from "../typing.js";
|
||||
import type * as voice from "../voice.js";
|
||||
@@ -42,6 +43,7 @@ declare const fullApi: ApiFromModules<{
|
||||
messages: typeof messages;
|
||||
presence: typeof presence;
|
||||
reactions: typeof reactions;
|
||||
readState: typeof readState;
|
||||
roles: typeof roles;
|
||||
typing: typeof typing;
|
||||
voice: typeof voice;
|
||||
|
||||
@@ -7,7 +7,8 @@ type TableWithChannelIndex =
|
||||
| "channelKeys"
|
||||
| "dmParticipants"
|
||||
| "typingIndicators"
|
||||
| "voiceStates";
|
||||
| "voiceStates"
|
||||
| "channelReadState";
|
||||
|
||||
async function deleteByChannel(
|
||||
ctx: GenericMutationCtx<DataModel>,
|
||||
@@ -175,6 +176,7 @@ export const remove = mutation({
|
||||
await deleteByChannel(ctx, "dmParticipants", args.id);
|
||||
await deleteByChannel(ctx, "typingIndicators", args.id);
|
||||
await deleteByChannel(ctx, "voiceStates", args.id);
|
||||
await deleteByChannel(ctx, "channelReadState", args.id);
|
||||
|
||||
await ctx.db.delete(args.id);
|
||||
|
||||
|
||||
113
convex/readState.ts
Normal file
113
convex/readState.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { query, mutation } from "./_generated/server";
|
||||
import { v } from "convex/values";
|
||||
|
||||
// Get read state for a single channel
|
||||
export const getReadState = query({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
channelId: v.id("channels"),
|
||||
},
|
||||
returns: v.union(
|
||||
v.object({
|
||||
lastReadTimestamp: v.number(),
|
||||
}),
|
||||
v.null()
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const state = await ctx.db
|
||||
.query("channelReadState")
|
||||
.withIndex("by_user_and_channel", (q) =>
|
||||
q.eq("userId", args.userId).eq("channelId", args.channelId)
|
||||
)
|
||||
.unique();
|
||||
if (!state) return null;
|
||||
return { lastReadTimestamp: state.lastReadTimestamp };
|
||||
},
|
||||
});
|
||||
|
||||
// Mark a channel as read up to a given timestamp
|
||||
export const markRead = mutation({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
channelId: v.id("channels"),
|
||||
lastReadTimestamp: v.number(),
|
||||
},
|
||||
returns: v.null(),
|
||||
handler: async (ctx, args) => {
|
||||
const existing = await ctx.db
|
||||
.query("channelReadState")
|
||||
.withIndex("by_user_and_channel", (q) =>
|
||||
q.eq("userId", args.userId).eq("channelId", args.channelId)
|
||||
)
|
||||
.unique();
|
||||
|
||||
if (existing) {
|
||||
// Only update if timestamp is newer
|
||||
if (args.lastReadTimestamp > existing.lastReadTimestamp) {
|
||||
await ctx.db.patch(existing._id, {
|
||||
lastReadTimestamp: args.lastReadTimestamp,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await ctx.db.insert("channelReadState", {
|
||||
userId: args.userId,
|
||||
channelId: args.channelId,
|
||||
lastReadTimestamp: args.lastReadTimestamp,
|
||||
});
|
||||
}
|
||||
return null;
|
||||
},
|
||||
});
|
||||
|
||||
// Get all read states for a user (used by Sidebar)
|
||||
export const getAllReadStates = query({
|
||||
args: {
|
||||
userId: v.id("userProfiles"),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
channelId: v.id("channels"),
|
||||
lastReadTimestamp: v.number(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const states = await ctx.db
|
||||
.query("channelReadState")
|
||||
.withIndex("by_user", (q) => q.eq("userId", args.userId))
|
||||
.collect();
|
||||
return states.map((s) => ({
|
||||
channelId: s.channelId,
|
||||
lastReadTimestamp: s.lastReadTimestamp,
|
||||
}));
|
||||
},
|
||||
});
|
||||
|
||||
// Get the latest message timestamp per channel (used by Sidebar)
|
||||
export const getLatestMessageTimestamps = query({
|
||||
args: {
|
||||
channelIds: v.array(v.id("channels")),
|
||||
},
|
||||
returns: v.array(
|
||||
v.object({
|
||||
channelId: v.id("channels"),
|
||||
latestTimestamp: v.number(),
|
||||
})
|
||||
),
|
||||
handler: async (ctx, args) => {
|
||||
const results = [];
|
||||
for (const channelId of args.channelIds) {
|
||||
const latestMsg = await ctx.db
|
||||
.query("messages")
|
||||
.withIndex("by_channel", (q) => q.eq("channelId", channelId))
|
||||
.order("desc")
|
||||
.first();
|
||||
if (latestMsg) {
|
||||
results.push({
|
||||
channelId,
|
||||
latestTimestamp: latestMsg._creationTime,
|
||||
});
|
||||
}
|
||||
}
|
||||
return results;
|
||||
},
|
||||
});
|
||||
@@ -106,4 +106,13 @@ export default defineSchema({
|
||||
})
|
||||
.index("by_channel", ["channelId"])
|
||||
.index("by_user", ["userId"]),
|
||||
|
||||
channelReadState: defineTable({
|
||||
userId: v.id("userProfiles"),
|
||||
channelId: v.id("channels"),
|
||||
lastReadTimestamp: v.number(),
|
||||
})
|
||||
.index("by_user", ["userId"])
|
||||
.index("by_channel", ["channelId"])
|
||||
.index("by_user_and_channel", ["userId", "channelId"]),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user