fix(chat): limit visitors to the last 50 General messages on the server and make history paging members-only
- commit
- c7334b4bb7a1d1106680e4c13f3aa00dbdab8556
- author
- Kit · · 2026-09-29 07:11 UTC
- parent
- dcb7d15
- signature
- SSH Unverified
SHA256:H91DFuEG4jpk5edzLA4aUKkpv4zv9Q339ipRmkph6TkThe key is unknown
4 files · +61 -48
M src/convex/queries.ts +29 -36
| @@ -1,4 +1,5 @@ | |||
| 1 | 1 | import { query } from './_generated/server'; | |
| 2 | + | import type { QueryCtx } from './_generated/server'; | |
| 2 | 3 | import { authQuery } from './auth'; | |
| 3 | 4 | import { v } from 'convex/values'; | |
| 4 | 5 | import { toPublicUser, assertRoomAccess } from './aim'; | |
| @@ -26,9 +27,22 @@ function computeEffectiveStatus( | |||
| 26 | 27 | ||
| 27 | 28 | // ============ PUBLIC QUERIES (no auth required) ============ | |
| 28 | 29 | ||
| 29 | - | // Public query: Get messages for a chat room (real-time subscription) | |
| 30 | - | // Max 100 messages for public/unauthenticated users | |
| 31 | - | const PUBLIC_MESSAGE_LIMIT = 100; | |
| 30 | + | // Visitors (not signed in) only see the latest messages of the default room. | |
| 31 | + | // Keep in sync with PUBLIC_PREVIEW_MESSAGES in src/lib/validation/message.ts. | |
| 32 | + | const PUBLIC_MESSAGE_LIMIT = 50; | |
| 33 | + | const MAX_AUTH_MESSAGE_LIMIT = 200; | |
| 34 | + | ||
| 35 | + | // Default room: "General", or the first group room when it does not exist | |
| 36 | + | async function findDefaultRoom(ctx: QueryCtx) { | |
| 37 | + | const rooms = await ctx.db.query('chatRooms').collect(); | |
| 38 | + | return ( | |
| 39 | + | rooms.find((r) => r.name === 'General' && r.type === 'group') ?? | |
| 40 | + | rooms.find((r) => r.type === 'group') ?? | |
| 41 | + | null | |
| 42 | + | ); | |
| 43 | + | } | |
| 44 | + | ||
| 45 | + | // Public query: latest messages of the default room (real-time subscription) | |
| 32 | 46 | ||
| 33 | 47 | export const getMessagesPublic = query({ | |
| 34 | 48 | args: { | |
| @@ -36,9 +50,9 @@ export const getMessagesPublic = query({ | |||
| 36 | 50 | limit: v.optional(v.number()) | |
| 37 | 51 | }, | |
| 38 | 52 | handler: async (ctx, args) => { | |
| 39 | - | // Direct (IM) rooms are never public | |
| 40 | - | const room = await ctx.db.get(args.roomId); | |
| 41 | - | if (!room || room.type === 'direct') return []; | |
| 53 | + | // Only the default room is readable without an account | |
| 54 | + | const defaultRoom = await findDefaultRoom(ctx); | |
| 55 | + | if (!defaultRoom || defaultRoom._id !== args.roomId) return []; | |
| 42 | 56 | ||
| 43 | 57 | // Enforce max limit and clamp to a safe positive integer | |
| 44 | 58 | const rawLimit = args.limit ?? PUBLIC_MESSAGE_LIMIT; | |
| @@ -80,20 +94,20 @@ export const getMessagesPublic = query({ | |||
| 80 | 94 | } | |
| 81 | 95 | }); | |
| 82 | 96 | ||
| 83 | - | // Public query: Get paginated messages for a chat room (older history) | |
| 84 | - | export const getMessagesPublicPage = query({ | |
| 97 | + | // Authenticated query: older history of a room (pagination is members-only) | |
| 98 | + | export const getMessagesPage = authQuery({ | |
| 85 | 99 | args: { | |
| 86 | 100 | roomId: v.id('chatRooms'), | |
| 87 | 101 | limit: v.optional(v.number()), | |
| 88 | 102 | beforeTimestamp: v.optional(v.number()) | |
| 89 | 103 | }, | |
| 90 | 104 | handler: async (ctx, args) => { | |
| 91 | - | // Direct (IM) rooms are never public | |
| 92 | 105 | const room = await ctx.db.get(args.roomId); | |
| 93 | - | if (!room || room.type === 'direct') return { messages: [], hasMore: false }; | |
| 106 | + | if (!room) return { messages: [], hasMore: false }; | |
| 107 | + | await assertRoomAccess(ctx, room, ctx.user._id); | |
| 94 | 108 | ||
| 95 | 109 | const rawLimit = args.limit ?? 50; | |
| 96 | - | const limit = Math.max(1, Math.min(PUBLIC_MESSAGE_LIMIT, Math.floor(rawLimit))); | |
| 110 | + | const limit = Math.max(1, Math.min(MAX_AUTH_MESSAGE_LIMIT, Math.floor(rawLimit))); | |
| 97 | 111 | ||
| 98 | 112 | let query = ctx.db | |
| 99 | 113 | .query('messages') | |
| @@ -139,30 +153,10 @@ export const getUsersPublic = query({ | |||
| 139 | 153 | export const getDefaultRoomPublic = query({ | |
| 140 | 154 | args: {}, | |
| 141 | 155 | handler: async (ctx) => { | |
| 142 | - | const rooms = await ctx.db.query('chatRooms').collect(); | |
| 143 | - | const defaultRoom = rooms.find((r) => r.name === 'General' && r.type === 'group'); | |
| 144 | - | ||
| 145 | - | if (defaultRoom) { | |
| 146 | - | return { | |
| 147 | - | id: defaultRoom._id, | |
| 148 | - | name: defaultRoom.name, | |
| 149 | - | type: defaultRoom.type, | |
| 150 | - | createdAt: defaultRoom.createdAt | |
| 151 | - | }; | |
| 152 | - | } | |
| 153 | - | ||
| 154 | - | // Return first group room if General doesn't exist | |
| 155 | - | const firstGroup = rooms.find((r) => r.type === 'group'); | |
| 156 | - | if (firstGroup) { | |
| 157 | - | return { | |
| 158 | - | id: firstGroup._id, | |
| 159 | - | name: firstGroup.name, | |
| 160 | - | type: firstGroup.type, | |
| 161 | - | createdAt: firstGroup.createdAt | |
| 162 | - | }; | |
| 163 | - | } | |
| 164 | - | ||
| 165 | - | return null; | |
| 156 | + | const room = await findDefaultRoom(ctx); | |
| 157 | + | return room | |
| 158 | + | ? { id: room._id, name: room.name, type: room.type, createdAt: room.createdAt } | |
| 159 | + | : null; | |
| 166 | 160 | } | |
| 167 | 161 | }); | |
| 168 | 162 | ||
| @@ -179,7 +173,6 @@ export const getMessages = authQuery({ | |||
| 179 | 173 | if (!room) return []; | |
| 180 | 174 | await assertRoomAccess(ctx, room, ctx.user._id); | |
| 181 | 175 | ||
| 182 | - | const MAX_AUTH_MESSAGE_LIMIT = 200; | |
| 183 | 176 | const rawLimit = args.limit ?? 100; | |
| 184 | 177 | const limit = Math.max(1, Math.min(MAX_AUTH_MESSAGE_LIMIT, Math.floor(rawLimit))); | |
| 185 | 178 | const query = ctx.db | |
| @@ -1,4 +1,5 @@ | |||||
| 1 | import { query } from './_generated/server'; | 1 | import { query } from './_generated/server'; | ||
| 2 | + | import type { QueryCtx } from './_generated/server'; | |||
| 2 | import { authQuery } from './auth'; | 3 | import { authQuery } from './auth'; | ||
| 3 | import { v } from 'convex/values'; | 4 | import { v } from 'convex/values'; | ||
| 4 | import { toPublicUser, assertRoomAccess } from './aim'; | 5 | import { toPublicUser, assertRoomAccess } from './aim'; | ||
| @@ -26,9 +27,22 @@ function computeEffectiveStatus( | |||||
| 26 | 27 | ||||
| 27 | // ============ PUBLIC QUERIES (no auth required) ============ | 28 | // ============ PUBLIC QUERIES (no auth required) ============ | ||
| 28 | 29 | ||||
| 29 | - | // Public query: Get messages for a chat room (real-time subscription) | 30 | + | // Visitors (not signed in) only see the latest messages of the default room. |
| 30 | - | // Max 100 messages for public/unauthenticated users | 31 | + | // Keep in sync with PUBLIC_PREVIEW_MESSAGES in src/lib/validation/message.ts. |
| 31 | - | const PUBLIC_MESSAGE_LIMIT = 100; | 32 | + | const PUBLIC_MESSAGE_LIMIT = 50; |
| 33 | + | const MAX_AUTH_MESSAGE_LIMIT = 200; | |||
| 34 | + | ||||
| 35 | + | // Default room: "General", or the first group room when it does not exist | |||
| 36 | + | async function findDefaultRoom(ctx: QueryCtx) { | |||
| 37 | + | const rooms = await ctx.db.query('chatRooms').collect(); | |||
| 38 | + | return ( | |||
| 39 | + | rooms.find((r) => r.name === 'General' && r.type === 'group') ?? | |||
| 40 | + | rooms.find((r) => r.type === 'group') ?? | |||
| 41 | + | null | |||
| 42 | + | ); | |||
| 43 | + | } | |||
| 44 | + | ||||
| 45 | + | // Public query: latest messages of the default room (real-time subscription) | |||
| 32 | 46 | ||||
| 33 | export const getMessagesPublic = query({ | 47 | export const getMessagesPublic = query({ | ||
| 34 | args: { | 48 | args: { | ||
| @@ -36,9 +50,9 @@ export const getMessagesPublic = query({ | |||||
| 36 | limit: v.optional(v.number()) | 50 | limit: v.optional(v.number()) | ||
| 37 | }, | 51 | }, | ||
| 38 | handler: async (ctx, args) => { | 52 | handler: async (ctx, args) => { | ||
| 39 | - | // Direct (IM) rooms are never public | 53 | + | // Only the default room is readable without an account |
| 40 | - | const room = await ctx.db.get(args.roomId); | 54 | + | const defaultRoom = await findDefaultRoom(ctx); |
| 41 | - | if (!room || room.type === 'direct') return []; | 55 | + | if (!defaultRoom || defaultRoom._id !== args.roomId) return []; |
| 42 | 56 | ||||
| 43 | // Enforce max limit and clamp to a safe positive integer | 57 | // Enforce max limit and clamp to a safe positive integer | ||
| 44 | const rawLimit = args.limit ?? PUBLIC_MESSAGE_LIMIT; | 58 | const rawLimit = args.limit ?? PUBLIC_MESSAGE_LIMIT; | ||
| @@ -80,20 +94,20 @@ export const getMessagesPublic = query({ | |||||
| 80 | } | 94 | } | ||
| 81 | }); | 95 | }); | ||
| 82 | 96 | ||||
| 83 | - | // Public query: Get paginated messages for a chat room (older history) | 97 | + | // Authenticated query: older history of a room (pagination is members-only) |
| 84 | - | export const getMessagesPublicPage = query({ | 98 | + | export const getMessagesPage = authQuery({ |
| 85 | args: { | 99 | args: { | ||
| 86 | roomId: v.id('chatRooms'), | 100 | roomId: v.id('chatRooms'), | ||
| 87 | limit: v.optional(v.number()), | 101 | limit: v.optional(v.number()), | ||
| 88 | beforeTimestamp: v.optional(v.number()) | 102 | beforeTimestamp: v.optional(v.number()) | ||
| 89 | }, | 103 | }, | ||
| 90 | handler: async (ctx, args) => { | 104 | handler: async (ctx, args) => { | ||
| 91 | - | // Direct (IM) rooms are never public | |||
| 92 | const room = await ctx.db.get(args.roomId); | 105 | const room = await ctx.db.get(args.roomId); | ||
| 93 | - | if (!room || room.type === 'direct') return { messages: [], hasMore: false }; | 106 | + | if (!room) return { messages: [], hasMore: false }; |
| 107 | + | await assertRoomAccess(ctx, room, ctx.user._id); | |||
| 94 | 108 | ||||
| 95 | const rawLimit = args.limit ?? 50; | 109 | const rawLimit = args.limit ?? 50; | ||
| 96 | - | const limit = Math.max(1, Math.min(PUBLIC_MESSAGE_LIMIT, Math.floor(rawLimit))); | 110 | + | const limit = Math.max(1, Math.min(MAX_AUTH_MESSAGE_LIMIT, Math.floor(rawLimit))); |
| 97 | 111 | ||||
| 98 | let query = ctx.db | 112 | let query = ctx.db | ||
| 99 | .query('messages') | 113 | .query('messages') | ||
| @@ -139,30 +153,10 @@ export const getUsersPublic = query({ | |||||
| 139 | export const getDefaultRoomPublic = query({ | 153 | export const getDefaultRoomPublic = query({ | ||
| 140 | args: {}, | 154 | args: {}, | ||
| 141 | handler: async (ctx) => { | 155 | handler: async (ctx) => { | ||
| 142 | - | const rooms = await ctx.db.query('chatRooms').collect(); | 156 | + | const room = await findDefaultRoom(ctx); |
| 143 | - | const defaultRoom = rooms.find((r) => r.name === 'General' && r.type === 'group'); | 157 | + | return room |
| 144 | - | 158 | + | ? { id: room._id, name: room.name, type: room.type, createdAt: room.createdAt } | |
| 145 | - | if (defaultRoom) { | 159 | + | : null; |
| 146 | - | return { | |||
| 147 | - | id: defaultRoom._id, | |||
| 148 | - | name: defaultRoom.name, | |||
| 149 | - | type: defaultRoom.type, | |||
| 150 | - | createdAt: defaultRoom.createdAt | |||
| 151 | - | }; | |||
| 152 | - | } | |||
| 153 | - | ||||
| 154 | - | // Return first group room if General doesn't exist | |||
| 155 | - | const firstGroup = rooms.find((r) => r.type === 'group'); | |||
| 156 | - | if (firstGroup) { | |||
| 157 | - | return { | |||
| 158 | - | id: firstGroup._id, | |||
| 159 | - | name: firstGroup.name, | |||
| 160 | - | type: firstGroup.type, | |||
| 161 | - | createdAt: firstGroup.createdAt | |||
| 162 | - | }; | |||
| 163 | - | } | |||
| 164 | - | ||||
| 165 | - | return null; | |||
| 166 | } | 160 | } | ||
| 167 | }); | 161 | }); | ||
| 168 | 162 | ||||
| @@ -179,7 +173,6 @@ export const getMessages = authQuery({ | |||||
| 179 | if (!room) return []; | 173 | if (!room) return []; | ||
| 180 | await assertRoomAccess(ctx, room, ctx.user._id); | 174 | await assertRoomAccess(ctx, room, ctx.user._id); | ||
| 181 | 175 | ||||
| 182 | - | const MAX_AUTH_MESSAGE_LIMIT = 200; | |||
| 183 | const rawLimit = args.limit ?? 100; | 176 | const rawLimit = args.limit ?? 100; | ||
| 184 | const limit = Math.max(1, Math.min(MAX_AUTH_MESSAGE_LIMIT, Math.floor(rawLimit))); | 177 | const limit = Math.max(1, Math.min(MAX_AUTH_MESSAGE_LIMIT, Math.floor(rawLimit))); | ||
| 185 | const query = ctx.db | 178 | const query = ctx.db | ||
M src/lib/components/chat-room.svelte +23 -10
| @@ -13,7 +13,8 @@ | |||
| 13 | 13 | import Tooltip from './ui/tooltip.svelte'; | |
| 14 | 14 | import LoadingDots from './ui/loading-dots.svelte'; | |
| 15 | 15 | import { desktop } from '$lib/states/desktop.svelte'; | |
| 16 | - | import { useAimClient } from '$lib/aim/client'; | |
| 16 | + | import { authQuery, tokenHash, useAimClient } from '$lib/aim/client'; | |
| 17 | + | import { PUBLIC_PREVIEW_MESSAGES } from '$lib/validation/message'; | |
| 17 | 18 | import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim'; | |
| 18 | 19 | import FormattedMessage from './formatted-message.svelte'; | |
| 19 | 20 | import TextFormattingToolbar from './text-formatting-toolbar.svelte'; | |
| @@ -62,10 +63,17 @@ | |||
| 62 | 63 | (requestedRoomId ?? defaultRoomQuery?.data?.id) as Id<'chatRooms'> | undefined | |
| 63 | 64 | ); | |
| 64 | 65 | ||
| 65 | - | // Subscribe to messages - skipped until the room ID is available | |
| 66 | - | const messagesQuery = isClient | |
| 67 | - | ? useQuery(convexApi.queries.getMessagesPublic, () => (roomId ? { roomId } : 'skip')) | |
| 66 | + | // Subscribe to messages - skipped until the room ID is available. | |
| 67 | + | // Members get the full live feed; visitors only the public preview of the default room. | |
| 68 | + | const memberMessagesQuery = authQuery(convexApi.queries.getMessages, () => | |
| 69 | + | roomId ? { roomId } : 'skip' | |
| 70 | + | ); | |
| 71 | + | const publicMessagesQuery = isClient | |
| 72 | + | ? useQuery(convexApi.queries.getMessagesPublic, () => | |
| 73 | + | roomId && !tokenHash() ? { roomId } : 'skip' | |
| 74 | + | ) | |
| 68 | 75 | : null; | |
| 76 | + | const messagesQuery = $derived(tokenHash() ? memberMessagesQuery : publicMessagesQuery); | |
| 69 | 77 | ||
| 70 | 78 | const convexClient = isClient ? useConvexClient() : null; | |
| 71 | 79 | ||
| @@ -270,11 +278,15 @@ | |||
| 270 | 278 | // Visible messages (limit for non-logged-in users) | |
| 271 | 279 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | |
| 272 | 280 | const isLoggedIn = Boolean(currentUser); | |
| 273 | - | return isLoggedIn ? allMessages : allMessages.slice(Math.max(0, allMessages.length - 50)); | |
| 281 | + | return isLoggedIn | |
| 282 | + | ? allMessages | |
| 283 | + | : allMessages.slice(Math.max(0, allMessages.length - PUBLIC_PREVIEW_MESSAGES)); | |
| 274 | 284 | }); | |
| 275 | 285 | ||
| 276 | 286 | // Show registration prompt for non-logged-in users | |
| 277 | - | const showRegistrationPrompt = $derived(!currentUser && allMessages.length > 50); | |
| 287 | + | const showRegistrationPrompt = $derived( | |
| 288 | + | !currentUser && allMessages.length >= PUBLIC_PREVIEW_MESSAGES | |
| 289 | + | ); | |
| 278 | 290 | ||
| 279 | 291 | // Character counter | |
| 280 | 292 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | |
| @@ -960,18 +972,19 @@ | |||
| 960 | 972 | hasMoreMessages && | |
| 961 | 973 | roomId && | |
| 962 | 974 | chatArea && | |
| 963 | - | currentUser && | |
| 964 | 975 | convexClient | |
| 965 | 976 | ) { | |
| 977 | + | const hash = tokenHash(); | |
| 978 | + | if (!hash) return; | |
| 966 | 979 | isLoadingMore = true; | |
| 967 | 980 | const prevScrollHeight = chatArea.scrollHeight; | |
| 968 | 981 | const prevScrollTop = chatArea.scrollTop; | |
| 969 | 982 | ||
| 970 | 983 | try { | |
| 971 | - | const fetchLimit = currentUser ? 100 : 50; | |
| 972 | - | const data = await convexClient.query(convexApi.queries.getMessagesPublicPage, { | |
| 984 | + | const data = await convexClient.query(convexApi.queries.getMessagesPage, { | |
| 985 | + | tokenHash: hash, | |
| 973 | 986 | roomId, | |
| 974 | - | limit: fetchLimit, | |
| 987 | + | limit: 100, | |
| 975 | 988 | beforeTimestamp: oldestMessageTimestamp ?? undefined | |
| 976 | 989 | }); | |
| 977 | 990 | ||
| @@ -13,7 +13,8 @@ | |||||
| 13 | import Tooltip from './ui/tooltip.svelte'; | 13 | import Tooltip from './ui/tooltip.svelte'; | ||
| 14 | import LoadingDots from './ui/loading-dots.svelte'; | 14 | import LoadingDots from './ui/loading-dots.svelte'; | ||
| 15 | import { desktop } from '$lib/states/desktop.svelte'; | 15 | import { desktop } from '$lib/states/desktop.svelte'; | ||
| 16 | - | import { useAimClient } from '$lib/aim/client'; | 16 | + | import { authQuery, tokenHash, useAimClient } from '$lib/aim/client'; |
| 17 | + | import { PUBLIC_PREVIEW_MESSAGES } from '$lib/validation/message'; | |||
| 17 | import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim'; | 18 | import { STATUS_LABELS_FR, type AimStatus } from '$lib/types/aim'; | ||
| 18 | import FormattedMessage from './formatted-message.svelte'; | 19 | import FormattedMessage from './formatted-message.svelte'; | ||
| 19 | import TextFormattingToolbar from './text-formatting-toolbar.svelte'; | 20 | import TextFormattingToolbar from './text-formatting-toolbar.svelte'; | ||
| @@ -62,10 +63,17 @@ | |||||
| 62 | (requestedRoomId ?? defaultRoomQuery?.data?.id) as Id<'chatRooms'> | undefined | 63 | (requestedRoomId ?? defaultRoomQuery?.data?.id) as Id<'chatRooms'> | undefined | ||
| 63 | ); | 64 | ); | ||
| 64 | 65 | ||||
| 65 | - | // Subscribe to messages - skipped until the room ID is available | 66 | + | // Subscribe to messages - skipped until the room ID is available. |
| 66 | - | const messagesQuery = isClient | 67 | + | // Members get the full live feed; visitors only the public preview of the default room. |
| 67 | - | ? useQuery(convexApi.queries.getMessagesPublic, () => (roomId ? { roomId } : 'skip')) | 68 | + | const memberMessagesQuery = authQuery(convexApi.queries.getMessages, () => |
| 69 | + | roomId ? { roomId } : 'skip' | |||
| 70 | + | ); | |||
| 71 | + | const publicMessagesQuery = isClient | |||
| 72 | + | ? useQuery(convexApi.queries.getMessagesPublic, () => | |||
| 73 | + | roomId && !tokenHash() ? { roomId } : 'skip' | |||
| 74 | + | ) | |||
| 68 | : null; | 75 | : null; | ||
| 76 | + | const messagesQuery = $derived(tokenHash() ? memberMessagesQuery : publicMessagesQuery); | |||
| 69 | 77 | ||||
| 70 | const convexClient = isClient ? useConvexClient() : null; | 78 | const convexClient = isClient ? useConvexClient() : null; | ||
| 71 | 79 | ||||
| @@ -270,11 +278,15 @@ | |||||
| 270 | // Visible messages (limit for non-logged-in users) | 278 | // Visible messages (limit for non-logged-in users) | ||
| 271 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | 279 | const visibleMessages = $derived.by<EnrichedMessage[]>(() => { | ||
| 272 | const isLoggedIn = Boolean(currentUser); | 280 | const isLoggedIn = Boolean(currentUser); | ||
| 273 | - | return isLoggedIn ? allMessages : allMessages.slice(Math.max(0, allMessages.length - 50)); | 281 | + | return isLoggedIn |
| 282 | + | ? allMessages | |||
| 283 | + | : allMessages.slice(Math.max(0, allMessages.length - PUBLIC_PREVIEW_MESSAGES)); | |||
| 274 | }); | 284 | }); | ||
| 275 | 285 | ||||
| 276 | // Show registration prompt for non-logged-in users | 286 | // Show registration prompt for non-logged-in users | ||
| 277 | - | const showRegistrationPrompt = $derived(!currentUser && allMessages.length > 50); | 287 | + | const showRegistrationPrompt = $derived( |
| 288 | + | !currentUser && allMessages.length >= PUBLIC_PREVIEW_MESSAGES | |||
| 289 | + | ); | |||
| 278 | 290 | ||||
| 279 | // Character counter | 291 | // Character counter | ||
| 280 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | 292 | const CHAR_WARNING_THRESHOLD = Math.floor(MAX_MESSAGE_LENGTH * 0.8); | ||
| @@ -960,18 +972,19 @@ | |||||
| 960 | hasMoreMessages && | 972 | hasMoreMessages && | ||
| 961 | roomId && | 973 | roomId && | ||
| 962 | chatArea && | 974 | chatArea && | ||
| 963 | - | currentUser && | |||
| 964 | convexClient | 975 | convexClient | ||
| 965 | ) { | 976 | ) { | ||
| 977 | + | const hash = tokenHash(); | |||
| 978 | + | if (!hash) return; | |||
| 966 | isLoadingMore = true; | 979 | isLoadingMore = true; | ||
| 967 | const prevScrollHeight = chatArea.scrollHeight; | 980 | const prevScrollHeight = chatArea.scrollHeight; | ||
| 968 | const prevScrollTop = chatArea.scrollTop; | 981 | const prevScrollTop = chatArea.scrollTop; | ||
| 969 | 982 | ||||
| 970 | try { | 983 | try { | ||
| 971 | - | const fetchLimit = currentUser ? 100 : 50; | 984 | + | const data = await convexClient.query(convexApi.queries.getMessagesPage, { |
| 972 | - | const data = await convexClient.query(convexApi.queries.getMessagesPublicPage, { | 985 | + | tokenHash: hash, |
| 973 | roomId, | 986 | roomId, | ||
| 974 | - | limit: fetchLimit, | 987 | + | limit: 100, |
| 975 | beforeTimestamp: oldestMessageTimestamp ?? undefined | 988 | beforeTimestamp: oldestMessageTimestamp ?? undefined | ||
| 976 | }); | 989 | }); | ||
| 977 | 990 | ||||
M src/lib/validation/message.ts +3 -0
| @@ -3,6 +3,9 @@ import { z } from 'zod'; | |||
| 3 | 3 | // Message constraints | |
| 4 | 4 | export const MAX_MESSAGE_LENGTH = 500; | |
| 5 | 5 | export const MIN_MESSAGE_LENGTH = 1; | |
| 6 | + | // Visitors see this many latest messages of the default room before the registration prompt. | |
| 7 | + | // Keep in sync with PUBLIC_MESSAGE_LIMIT in src/convex/queries.ts. | |
| 8 | + | export const PUBLIC_PREVIEW_MESSAGES = 50; | |
| 6 | 9 | ||
| 7 | 10 | // Message content schema | |
| 8 | 11 | export const messageContentSchema = z | |
| @@ -3,6 +3,9 @@ import { z } from 'zod'; | |||||
| 3 | // Message constraints | 3 | // Message constraints | ||
| 4 | export const MAX_MESSAGE_LENGTH = 500; | 4 | export const MAX_MESSAGE_LENGTH = 500; | ||
| 5 | export const MIN_MESSAGE_LENGTH = 1; | 5 | export const MIN_MESSAGE_LENGTH = 1; | ||
| 6 | + | // Visitors see this many latest messages of the default room before the registration prompt. | |||
| 7 | + | // Keep in sync with PUBLIC_MESSAGE_LIMIT in src/convex/queries.ts. | |||
| 8 | + | export const PUBLIC_PREVIEW_MESSAGES = 50; | |||
| 6 | 9 | ||||
| 7 | // Message content schema | 10 | // Message content schema | ||
| 8 | export const messageContentSchema = z | 11 | export const messageContentSchema = z | ||
M src/routes/api/chat/messages/+server.ts +6 -2
| @@ -5,7 +5,7 @@ import type { SendMessageResponse, GetMessagesResponse } from '$lib/types/payloa | |||
| 5 | 5 | import { error, isHttpError } from '@sveltejs/kit'; | |
| 6 | 6 | import { createLogger } from '$lib/utils/logger.server'; | |
| 7 | 7 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | |
| 8 | - | import { sendMessageSchema } from '$lib/validation/message'; | |
| 8 | + | import { sendMessageSchema, PUBLIC_PREVIEW_MESSAGES } from '$lib/validation/message'; | |
| 9 | 9 | import type { RequestHandler } from './$types'; | |
| 10 | 10 | ||
| 11 | 11 | const log = createLogger('chat-server'); | |
| @@ -70,6 +70,10 @@ export const GET: RequestHandler = async ({ url, locals }) => { | |||
| 70 | 70 | throw error(403, 'Not a member of this conversation'); | |
| 71 | 71 | } | |
| 72 | 72 | } | |
| 73 | + | // Visitors only get a preview of the default room | |
| 74 | + | if (isPublic && roomId !== getDefaultChatRoomId()) { | |
| 75 | + | throw error(403, 'Sign in to read this room'); | |
| 76 | + | } | |
| 73 | 77 | ||
| 74 | 78 | log.debug('Fetching messages from Convex', { | |
| 75 | 79 | beforeTimestamp, | |
| @@ -90,7 +94,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { | |||
| 90 | 94 | }); | |
| 91 | 95 | } | |
| 92 | 96 | ||
| 93 | - | const fetchLimit = 100; | |
| 97 | + | const fetchLimit = isPublic ? PUBLIC_PREVIEW_MESSAGES : 100; | |
| 94 | 98 | ||
| 95 | 99 | const result = await messages.getByRoom( | |
| 96 | 100 | roomId, | |
| @@ -5,7 +5,7 @@ import type { SendMessageResponse, GetMessagesResponse } from '$lib/types/payloa | |||||
| 5 | import { error, isHttpError } from '@sveltejs/kit'; | 5 | import { error, isHttpError } from '@sveltejs/kit'; | ||
| 6 | import { createLogger } from '$lib/utils/logger.server'; | 6 | import { createLogger } from '$lib/utils/logger.server'; | ||
| 7 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | 7 | import { sanitizeStyleData } from '$lib/validation/text-formatting'; | ||
| 8 | - | import { sendMessageSchema } from '$lib/validation/message'; | 8 | + | import { sendMessageSchema, PUBLIC_PREVIEW_MESSAGES } from '$lib/validation/message'; |
| 9 | import type { RequestHandler } from './$types'; | 9 | import type { RequestHandler } from './$types'; | ||
| 10 | 10 | ||||
| 11 | const log = createLogger('chat-server'); | 11 | const log = createLogger('chat-server'); | ||
| @@ -70,6 +70,10 @@ export const GET: RequestHandler = async ({ url, locals }) => { | |||||
| 70 | throw error(403, 'Not a member of this conversation'); | 70 | throw error(403, 'Not a member of this conversation'); | ||
| 71 | } | 71 | } | ||
| 72 | } | 72 | } | ||
| 73 | + | // Visitors only get a preview of the default room | |||
| 74 | + | if (isPublic && roomId !== getDefaultChatRoomId()) { | |||
| 75 | + | throw error(403, 'Sign in to read this room'); | |||
| 76 | + | } | |||
| 73 | 77 | ||||
| 74 | log.debug('Fetching messages from Convex', { | 78 | log.debug('Fetching messages from Convex', { | ||
| 75 | beforeTimestamp, | 79 | beforeTimestamp, | ||
| @@ -90,7 +94,7 @@ export const GET: RequestHandler = async ({ url, locals }) => { | |||||
| 90 | }); | 94 | }); | ||
| 91 | } | 95 | } | ||
| 92 | 96 | ||||
| 93 | - | const fetchLimit = 100; | 97 | + | const fetchLimit = isPublic ? PUBLIC_PREVIEW_MESSAGES : 100; |
| 94 | 98 | ||||
| 95 | const result = await messages.getByRoom( | 99 | const result = await messages.getByRoom( | ||
| 96 | roomId, | 100 | roomId, | ||