Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@ const nextConfig: NextConfig = {
],
},
// Node.js native modules that should not be bundled for edge/browser
serverExternalPackages: ["better-sqlite3"],
// memory-provider imports better-sqlite3 which cannot run in Cloudflare Workers
serverExternalPackages: ["better-sqlite3", "./src/db/provider/memory-provider"],
};

export default nextConfig;
Expand Down
8 changes: 4 additions & 4 deletions src/app/actions/conversations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import {
type NewChannelMember,
type NewChannelReadState,
} from "@/db/schema-conversations"
import { users } from "@/db/schema"
import { users, organizationMembers } from "@/db/schema"
import { getCurrentUser } from "@/lib/auth"
import { requirePermission } from "@/lib/permissions"
import { revalidatePath } from "next/cache"
Expand Down Expand Up @@ -170,9 +170,9 @@ export async function createChannel(data: {

// get user's organization
const orgMember = await db
.select({ organizationId: sql<string>`organization_id` })
.from(sql`organization_members`)
.where(sql`user_id = ${user.id}`)
.select({ organizationId: organizationMembers.organizationId })
.from(organizationMembers)
.where(eq(organizationMembers.userId, user.id))
.limit(1)
.then((rows) => rows[0] ?? null)

Expand Down
79 changes: 2 additions & 77 deletions src/app/dashboard/conversations/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,86 +1,11 @@
"use client"

import * as React from "react"

type ThreadMessage = {
readonly id: string
readonly channelId: string
readonly threadId: string | null
readonly content: string
readonly contentHtml: string | null
readonly editedAt: string | null
readonly deletedAt: string | null
readonly isPinned: boolean
readonly replyCount: number
readonly lastReplyAt: string | null
readonly createdAt: string
readonly user: {
readonly id: string
readonly displayName: string | null
readonly email: string
readonly avatarUrl: string | null
} | null
}

type ConversationsContextType = {
readonly threadOpen: boolean
readonly threadMessageId: string | null
readonly threadParentMessage: ThreadMessage | null
readonly openThread: (messageId: string, parentMessage: ThreadMessage) => void
readonly closeThread: () => void
}

const ConversationsContext = React.createContext<ConversationsContextType | undefined>(
undefined
)

export function useConversations() {
const context = React.useContext(ConversationsContext)
if (!context) {
throw new Error("useConversations must be used within ConversationsProvider")
}
return context
}

export type { ThreadMessage }
import { ConversationsProvider } from "@/contexts/conversations-context"

export default function ConversationsLayout({
children,
}: {
readonly children: React.ReactNode
}) {
const [threadOpen, setThreadOpen] = React.useState(false)
const [threadMessageId, setThreadMessageId] = React.useState<string | null>(null)
const [threadParentMessage, setThreadParentMessage] = React.useState<ThreadMessage | null>(null)

const openThread = React.useCallback((messageId: string, parentMessage: ThreadMessage) => {
setThreadMessageId(messageId)
setThreadParentMessage(parentMessage)
setThreadOpen(true)
}, [])

const closeThread = React.useCallback(() => {
setThreadOpen(false)
setThreadMessageId(null)
setThreadParentMessage(null)
}, [])

const value = React.useMemo(
() => ({
threadOpen,
threadMessageId,
threadParentMessage,
openThread,
closeThread,
}),
[threadOpen, threadMessageId, threadParentMessage, openThread, closeThread]
)

return (
<ConversationsContext.Provider value={value}>
<div className="flex min-h-0 flex-1 overflow-hidden">
{children}
</div>
</ConversationsContext.Provider>
)
return <ConversationsProvider>{children}</ConversationsProvider>
}
2 changes: 1 addition & 1 deletion src/components/conversations/message-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { Button } from "@/components/ui/button"
import { Textarea } from "@/components/ui/textarea"
import { MarkdownRenderer } from "@/components/ui/markdown-renderer"
import { cn } from "@/lib/utils"
import { useConversations } from "@/app/dashboard/conversations/layout"
import { useConversations } from "@/contexts/conversations-context"
import { editMessage, deleteMessage, addReaction } from "@/app/actions/chat-messages"
import { useRouter } from "next/navigation"

Expand Down
3 changes: 1 addition & 2 deletions src/components/conversations/thread-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@ import { Button } from "@/components/ui/button"
import { ScrollArea } from "@/components/ui/scroll-area"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import { useConversations } from "@/app/dashboard/conversations/layout"
import type { ThreadMessage } from "@/app/dashboard/conversations/layout"
import { useConversations, type ThreadMessage } from "@/contexts/conversations-context"
import { getThreadMessages } from "@/app/actions/chat-messages"
import { MessageItem } from "./message-item"
import { MessageComposer } from "./message-composer"
Expand Down
84 changes: 84 additions & 0 deletions src/contexts/conversations-context.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
"use client"

import * as React from "react"

export type ThreadMessage = {
readonly id: string
readonly channelId: string
readonly threadId: string | null
readonly content: string
readonly contentHtml: string | null
readonly editedAt: string | null
readonly deletedAt: string | null
readonly isPinned: boolean
readonly replyCount: number
readonly lastReplyAt: string | null
readonly createdAt: string
readonly user: {
readonly id: string
readonly displayName: string | null
readonly email: string
readonly avatarUrl: string | null
} | null
}

type ConversationsContextType = {
readonly threadOpen: boolean
readonly threadMessageId: string | null
readonly threadParentMessage: ThreadMessage | null
readonly openThread: (messageId: string, parentMessage: ThreadMessage) => void
readonly closeThread: () => void
}

const ConversationsContext = React.createContext<ConversationsContextType | undefined>(
undefined
)

export function useConversations() {
const context = React.useContext(ConversationsContext)
if (!context) {
throw new Error("useConversations must be used within ConversationsProvider")
}
return context
}

export function ConversationsProvider({
children,
}: {
readonly children: React.ReactNode
}) {
const [threadOpen, setThreadOpen] = React.useState(false)
const [threadMessageId, setThreadMessageId] = React.useState<string | null>(null)
const [threadParentMessage, setThreadParentMessage] = React.useState<ThreadMessage | null>(null)

const openThread = React.useCallback((messageId: string, parentMessage: ThreadMessage) => {
setThreadMessageId(messageId)
setThreadParentMessage(parentMessage)
setThreadOpen(true)
}, [])

const closeThread = React.useCallback(() => {
setThreadOpen(false)
setThreadMessageId(null)
setThreadParentMessage(null)
}, [])

const value = React.useMemo(
() => ({
threadOpen,
threadMessageId,
threadParentMessage,
openThread,
closeThread,
}),
[threadOpen, threadMessageId, threadParentMessage, openThread, closeThread]
)

return (
<ConversationsContext.Provider value={value}>
<div className="flex min-h-0 flex-1 overflow-hidden">
{children}
</div>
</ConversationsContext.Provider>
)
}
36 changes: 26 additions & 10 deletions src/db/provider/context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,23 @@ export interface DatabaseProviderProps {
}
}

// Lazy-loaded memory provider factory - only loaded when actually needed
// This avoids bundling better-sqlite3 (a native Node.js module) in
// environments like Cloudflare Workers where it can't run
let createMemoryProviderFn: typeof import("./memory-provider")["createMemoryProvider"] | null = null

async function getMemoryProvider(config?: MemoryProviderConfig): Promise<DatabaseProvider> {
if (!createMemoryProviderFn) {
// Construct the path dynamically to prevent static analysis by bundlers
// The path segments are concatenated at runtime
const providerDir = "."
const providerFile = "memory-provider"
const loadedModule = await import(/* webpackIgnore: true */ `${providerDir}/${providerFile}`)
createMemoryProviderFn = loadedModule.createMemoryProvider
}
return createMemoryProviderFn!(config)
}

export function DatabaseProvider({
children,
forcePlatform,
Expand Down Expand Up @@ -91,11 +108,7 @@ export function DatabaseProvider({

case "memory":
default: {
// Dynamic import to avoid bundling better-sqlite3 in browser
const { createMemoryProvider } = await import(
/* webpackIgnore: true */ "./memory-provider"
)
newProvider = createMemoryProvider(config?.memory)
newProvider = await getMemoryProvider(config?.memory)
break
}
}
Expand Down Expand Up @@ -164,12 +177,15 @@ export async function getServerDb(): Promise<DrizzleDB> {

case "memory":
default: {
// Dynamic import to avoid bundling better-sqlite3 in browser
const { createMemoryProvider } = await import(
/* webpackIgnore: true */ "./memory-provider"
// Memory provider is only for local development/testing.
// On Cloudflare Workers, detectPlatform() returns "d1", so this
// code path is never reached at runtime. However, bundlers like
// esbuild still try to resolve the import. We throw an error here
// since this code should never execute in production environments.
throw new Error(
"Memory provider not available in this environment. " +
"Ensure you have a local SQLite setup or use the D1 provider."
)
const provider = createMemoryProvider()
return provider.getDb()
}
}
}
Loading