Skip to content

Commit 780687d

Browse files
u
1 parent 5308dbe commit 780687d

6 files changed

Lines changed: 26 additions & 20 deletions

File tree

‎.gitignore‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,4 @@ next-env.d.ts
4646
/.omc
4747
/dist
4848
!.env.example
49+
/idea.md

‎next.config.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ const securityHeaders = [
3535
const nextConfig: NextConfig = {
3636
output: "standalone",
3737
poweredByHeader: false,
38+
// Allow testing the dev server via 127.0.0.1 in addition to localhost
39+
allowedDevOrigins: ["127.0.0.1"],
3840
reactStrictMode: true,
3941
serverExternalPackages: ["imapflow", "mailparser", "nodemailer", "sanitize-html", "postgres"],
4042
images: {

‎src/app/(marketing)/layout.tsx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import type { Viewport } from "next"
22
import { redirect } from "next/navigation"
3-
import { getSettings } from "@/server/settings"
3+
import { getSettings, isSetupComplete } from "@/server/settings"
44
import { SiteChrome } from "@/components/marketing/site-chrome"
55
import { APP_ENTRY } from "@/content/marketing/site"
66
import "@/components/marketing/marketing.css"
@@ -13,6 +13,8 @@ export const viewport: Viewport = { themeColor: "#FAF9F5" }
1313
* Legal pages live in the (legal) group so they stay reachable either way.
1414
*/
1515
export default async function MarketingLayout({ children }: { children: React.ReactNode }) {
16+
// A fresh instance goes straight to the first-run wizard
17+
if (!(await isSetupComplete())) redirect("/setup")
1618
const general = await getSettings("general")
1719
if (general.marketingSite === false) redirect(APP_ENTRY)
1820
return <SiteChrome variant="full">{children}</SiteChrome>

‎src/components/inbox/composer/editor.tsx‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -179,33 +179,35 @@ export function EditorToolbar({ editor, className }: { editor: Editor | null; cl
179179
}
180180
: null,
181181
})
182-
if (!editor || !state) return null
182+
if (!editor) return null
183+
// useEditorState only reports after the first transaction of a freshly mounted editor.
184+
const active = state ?? { bold: false, italic: false, underline: false, strike: false, bullet: false, ordered: false, quote: false, code: false }
183185
const chain = () => editor.chain().focus()
184186
return (
185187
<div className={cn("flex flex-wrap items-center gap-0.5", className)} role="toolbar" aria-label="Formatting">
186-
<ToolbarButton label="Bold" active={state.bold} onClick={() => chain().toggleBold().run()}>
188+
<ToolbarButton label="Bold" active={active.bold} onClick={() => chain().toggleBold().run()}>
187189
<Bold />
188190
</ToolbarButton>
189-
<ToolbarButton label="Italic" active={state.italic} onClick={() => chain().toggleItalic().run()}>
191+
<ToolbarButton label="Italic" active={active.italic} onClick={() => chain().toggleItalic().run()}>
190192
<Italic />
191193
</ToolbarButton>
192-
<ToolbarButton label="Underline" active={state.underline} onClick={() => chain().toggleUnderline().run()}>
194+
<ToolbarButton label="Underline" active={active.underline} onClick={() => chain().toggleUnderline().run()}>
193195
<Underline />
194196
</ToolbarButton>
195-
<ToolbarButton label="Strikethrough" active={state.strike} onClick={() => chain().toggleStrike().run()}>
197+
<ToolbarButton label="Strikethrough" active={active.strike} onClick={() => chain().toggleStrike().run()}>
196198
<Strikethrough />
197199
</ToolbarButton>
198200
<span className="mx-1 h-4 w-px bg-border" aria-hidden />
199-
<ToolbarButton label="Bulleted list" active={state.bullet} onClick={() => chain().toggleBulletList().run()}>
201+
<ToolbarButton label="Bulleted list" active={active.bullet} onClick={() => chain().toggleBulletList().run()}>
200202
<List />
201203
</ToolbarButton>
202-
<ToolbarButton label="Numbered list" active={state.ordered} onClick={() => chain().toggleOrderedList().run()}>
204+
<ToolbarButton label="Numbered list" active={active.ordered} onClick={() => chain().toggleOrderedList().run()}>
203205
<ListOrdered />
204206
</ToolbarButton>
205-
<ToolbarButton label="Quote" active={state.quote} onClick={() => chain().toggleBlockquote().run()}>
207+
<ToolbarButton label="Quote" active={active.quote} onClick={() => chain().toggleBlockquote().run()}>
206208
<Quote />
207209
</ToolbarButton>
208-
<ToolbarButton label="Code" active={state.code} onClick={() => chain().toggleCode().run()}>
210+
<ToolbarButton label="Code" active={active.code} onClick={() => chain().toggleCode().run()}>
209211
<Code />
210212
</ToolbarButton>
211213
<LinkButton editor={editor} />

‎src/server/conversations/queries.ts‎

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -130,15 +130,14 @@ function encodeCursor(row: ListRow) {
130130
return Buffer.from(`${row.cursorTs}|${row.id}`).toString("base64url")
131131
}
132132

133+
/** Postgres timestamptz text, e.g. "2026-09-27 10:00:00.123456+02" (parsed by Postgres itself). */
134+
const CURSOR_TS_RE = /^\d{4}-\d\d-\d\d[ T]\d\d:\d\d:\d\d(\.\d{1,6})?([+-]\d\d(:?\d\d)?|Z)?$/
135+
const CURSOR_ID_RE = /^[0-9a-f-]{36}$/i
136+
133137
function decodeCursor(cursor: string): { ts: string; id: string } {
134-
try {
135-
const [ts, id] = Buffer.from(cursor, "base64url").toString("utf8").split("|")
136-
// Postgres timestamptz text, e.g. "2026-09-27 10:00:00.123456+00" (parsed by Postgres itself).
137-
if (!ts || !id || !/^[0-9a-f-]{36}$/i.test(id) || !/^\d{4}-\d\d-\d\d[ T]\d\d:\d\d:\d\d(\.\d{1,6})?([+-]\d\d(:?\d\d)?|Z)?$/.test(ts)) throw new Error()
138-
return { ts, id }
139-
} catch {
140-
throw new ApiError(400, "Invalid cursor", "invalid_cursor")
141-
}
138+
const [ts, id] = Buffer.from(cursor, "base64url").toString("utf8").split("|")
139+
if (!ts || !id || !CURSOR_ID_RE.test(id) || !CURSOR_TS_RE.test(ts)) throw new ApiError(400, "Invalid cursor", "invalid_cursor")
140+
return { ts, id }
142141
}
143142

144143
export async function listConversations(

‎src/server/conversations/scope.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export async function loadScope(ctx: Ctx): Promise<InboxScope> {
4040

4141
/**
4242
* Visibility of conversations for inbox queries: the foundation rules plus
43-
* "internal chats are only visible to their current members" (leaving or
44-
* being removed from a chat revokes access even for its creator/followers).
43+
* "internal chats are only visible to their current members": leaving or
44+
* being removed from a chat revokes access, also for its creator/followers.
4545
*/
4646
export function visibleWhere(ctx: Ctx, scope: InboxScope) {
4747
const c = schema.conversations

0 commit comments

Comments
 (0)