Skip to content
Open
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: 3 additions & 0 deletions apps/obsidian/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
},
"dependencies": {
"@codemirror/view": "^6.38.8",
"@repo/database": "workspace:*",
"@repo/utils": "workspace:*",
"@supabase/supabase-js": "catalog:",
"date-fns": "^4.1.0",
"nanoid": "^4.0.2",
"react": "catalog:obsidian",
Expand Down
28 changes: 28 additions & 0 deletions apps/obsidian/scripts/compile.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable @typescript-eslint/naming-convention */
import esbuild from "esbuild";
import fs from "fs";
import path from "path";
Expand All @@ -10,6 +11,19 @@ import autoprefixer from "autoprefixer";

dotenv.config();

// For local dev: Set SUPABASE_USE_DB=local and run `pnpm run genenv` in packages/database
let envContents: (() => Record<string, string>) | null = null;
try {
const dbDotEnv = require("@repo/database/dbDotEnv");
envContents = dbDotEnv.envContents;
} catch (error) {
if ((error as Error).message.includes("Cannot find module")) {
console.error("Build the database module before compiling obsidian");
process.exit(1);
}
throw error;
}

const DEFAULT_FILES_INCLUDED = ["manifest.json"];
const isProd = process.env.NODE_ENV === "production";

Expand Down Expand Up @@ -43,6 +57,7 @@ export const args = {
"@lezer/common",
"@lezer/highlight",
"@lezer/lr",
"tslib=window.TSLib",
...builtins,
],
} as CliOpts;
Expand Down Expand Up @@ -89,6 +104,10 @@ export const compile = ({
fs.mkdirSync(outdir, { recursive: true });

const buildPromises = [] as Promise<void>[];
if (!envContents) {
throw new Error("envContents not loaded. Build the database module first.");
}
const dbEnv = envContents();
buildPromises.push(
builder({
absWorkingDir: process.cwd(),
Expand All @@ -100,6 +119,15 @@ export const compile = ({
minify: isProd,
entryNames: out,
external: external,
define: {
"process.env.SUPABASE_URL": dbEnv.SUPABASE_URL
? `"${dbEnv.SUPABASE_URL}"`
: "null",
"process.env.SUPABASE_ANON_KEY": dbEnv.SUPABASE_ANON_KEY
? `"${dbEnv.SUPABASE_ANON_KEY}"`
: "null",
"process.env.NEXT_API_ROOT": `"${dbEnv.NEXT_API_ROOT || ""}"`,
},
plugins: [
{
name: "log",
Expand Down
39 changes: 39 additions & 0 deletions apps/obsidian/src/components/GeneralSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { usePlugin } from "./PluginContext";
import { Notice, setIcon } from "obsidian";
import SuggestInput from "./SuggestInput";
import { SLACK_LOGO, WHITE_LOGO_SVG } from "~/icons";
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";

const DOCS_URL = "https://discoursegraphs.com/docs/obsidian";
const COMMUNITY_URL =
Expand Down Expand Up @@ -157,6 +158,9 @@ const GeneralSettings = () => {
const [nodeTagHotkey, setNodeTagHotkey] = useState<string>(
plugin.settings.nodeTagHotkey,
);
const [syncModeEnabled, setSyncModeEnabled] = useState<boolean>(
plugin.settings.syncModeEnabled ?? false,
);
const [hasUnsavedChanges, setHasUnsavedChanges] = useState(false);

const handleToggleChange = (newValue: boolean) => {
Expand Down Expand Up @@ -190,6 +194,11 @@ const GeneralSettings = () => {
}
}, []);

const handleSyncModeToggle = useCallback((newValue: boolean) => {
setSyncModeEnabled(newValue);
setHasUnsavedChanges(true);
}, []);

const handleSave = async () => {
const trimmedNodesFolderPath = nodesFolderPath.trim();
const trimmedCanvasFolderPath = canvasFolderPath.trim();
Expand All @@ -201,12 +210,25 @@ const GeneralSettings = () => {
plugin.settings.canvasAttachmentsFolderPath =
trimmedCanvasAttachmentsFolderPath;
plugin.settings.nodeTagHotkey = nodeTagHotkey || "";
plugin.settings.syncModeEnabled = syncModeEnabled;
setNodesFolderPath(trimmedNodesFolderPath);
setCanvasFolderPath(trimmedCanvasFolderPath);
setCanvasAttachmentsFolderPath(trimmedCanvasAttachmentsFolderPath);
await plugin.saveSettings();
new Notice("General settings saved");
setHasUnsavedChanges(false);

if (syncModeEnabled) {
try {
await initializeSupabaseSync(plugin);
new Notice("Sync mode initialized successfully");
} catch (error) {
console.error("Failed to initialize sync mode:", error);
new Notice(
`Failed to initialize sync mode: ${error instanceof Error ? error.message : String(error)}`,
);
}
}
};

return (
Expand Down Expand Up @@ -311,6 +333,23 @@ const GeneralSettings = () => {
</div>
</div>

<div className="setting-item">
<div className="setting-item-info">
<div className="setting-item-name">(BETA) Sync mode enable</div>
<div className="setting-item-description">
Enable synchronization with Discourse Graph database
</div>
</div>
<div className="setting-item-control">
<div
className={`checkbox-container ${syncModeEnabled ? "is-enabled" : ""}`}
onClick={() => handleSyncModeToggle(!syncModeEnabled)}
>
<input type="checkbox" checked={syncModeEnabled} />
</div>
</div>
</div>

<div className="setting-item">
<button
onClick={() => void handleSave()}
Expand Down
3 changes: 3 additions & 0 deletions apps/obsidian/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,9 @@ export const DEFAULT_SETTINGS: Settings = {
canvasFolderPath: "Discourse Canvas",
canvasAttachmentsFolderPath: "attachments",
nodeTagHotkey: "\\",
spacePassword: undefined,
accountLocalId: undefined,
syncModeEnabled: false,
};
export const FRONTMATTER_KEY = "tldr-dg";
export const TLDATA_DELIMITER_START =
Expand Down
13 changes: 13 additions & 0 deletions apps/obsidian/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
TFile,
MarkdownView,
WorkspaceLeaf,
Notice,
} from "obsidian";
import { EditorView } from "@codemirror/view";
import { SettingsTab } from "~/components/Settings";
Expand All @@ -22,6 +23,7 @@ import ModifyNodeModal from "~/components/ModifyNodeModal";
import { TagNodeHandler } from "~/utils/tagNodeHandler";
import { TldrawView } from "~/components/canvas/TldrawView";
import { NodeTagSuggestPopover } from "~/components/NodeTagSuggestModal";
import { initializeSupabaseSync } from "~/utils/syncDgNodesToSupabase";

export default class DiscourseGraphPlugin extends Plugin {
settings: Settings = { ...DEFAULT_SETTINGS };
Expand All @@ -32,6 +34,17 @@ export default class DiscourseGraphPlugin extends Plugin {

async onload() {
await this.loadSettings();

if (this.settings.syncModeEnabled === true) {
void initializeSupabaseSync(this).catch((error) => {
console.error("Failed to initialize Supabase sync:", error);
new Notice(
`Failed to initialize Supabase sync: ${error instanceof Error ? error.message : String(error)}`,
5000,
);
});
}

registerCommands(this);
this.addSettingTab(new SettingsTab(this.app, this));

Expand Down
3 changes: 3 additions & 0 deletions apps/obsidian/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ export type Settings = {
canvasFolderPath: string;
canvasAttachmentsFolderPath: string;
nodeTagHotkey: string;
spacePassword?: string;
accountLocalId?: string;
syncModeEnabled?: boolean;
};

export type BulkImportCandidate = {
Expand Down
126 changes: 126 additions & 0 deletions apps/obsidian/src/utils/supabaseContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import type { Enums } from "@repo/database/dbTypes";
import type { DGSupabaseClient } from "@repo/database/lib/client";
import {
fetchOrCreateSpaceDirect,
fetchOrCreatePlatformAccount,
createLoggedInClient,
} from "@repo/database/lib/contextFunctions";
import type DiscourseGraphPlugin from "~/index";

type Platform = Enums<"Platform">;

export type SupabaseContext = {
platform: Platform;
spaceId: number;
userId: number;
spacePassword: string;
};

let contextCache: SupabaseContext | null = null;

const generateAccountLocalId = (vaultName: string): string => {
const randomSuffix = Math.random()
.toString(36)
.substring(2, 8)
.toUpperCase();
return `${vaultName}-${randomSuffix}`;
};

const getOrCreateSpacePassword = async (
plugin: DiscourseGraphPlugin,
): Promise<string> => {
if (plugin.settings.spacePassword) {
return plugin.settings.spacePassword;
}
// Generate UUID using crypto.randomUUID()
const password = crypto.randomUUID();
plugin.settings.spacePassword = password;
await plugin.saveSettings();
return password;
};

const getOrCreateAccountLocalId = async (
plugin: DiscourseGraphPlugin,
vaultName: string,
): Promise<string> => {
if (plugin.settings.accountLocalId) {
return plugin.settings.accountLocalId;
}
const accountLocalId = generateAccountLocalId(vaultName);
plugin.settings.accountLocalId = accountLocalId;
await plugin.saveSettings();
return accountLocalId;
};

export const getSupabaseContext = async (
plugin: DiscourseGraphPlugin,
): Promise<SupabaseContext | null> => {
if (contextCache === null) {
try {
const vaultName = plugin.app.vault.getName() || "obsidian-vault";

const spacePassword = await getOrCreateSpacePassword(plugin);
const accountLocalId = await getOrCreateAccountLocalId(plugin, vaultName);

// Space URL format: "space" + accountLocalId
const url = `space${accountLocalId}`;
const platform: Platform = "Obsidian";

const spaceResult = await fetchOrCreateSpaceDirect({
password: spacePassword,
url,
name: vaultName,
platform,
});

if (!spaceResult.data) {
console.error("Failed to create space");
return null;
}

const spaceId = spaceResult.data.id;
const userId = await fetchOrCreatePlatformAccount({
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm following Roam pattern here to create both Anonymous User and the real user (name here is vaultName instead of "Anonymous of space [x]". But I would say we don't need this for Obsidian since each vault is a space, and is owned by only one person? hence all the handling would go through only need to go through 1 account? I can just comment out this part of the code to delete this real user creation

platform: "Obsidian",
accountLocalId,
name: vaultName,
email: undefined,
spaceId,
password: spacePassword,
});

contextCache = {
platform: "Obsidian",
spaceId,
userId,
spacePassword,
};
} catch (error) {
console.error(error);
return null;
}
}
return contextCache;
};

let loggedInClient: DGSupabaseClient | null = null;

export const getLoggedInClient = async (
plugin: DiscourseGraphPlugin,
): Promise<DGSupabaseClient | null> => {
if (loggedInClient === null) {
const context = await getSupabaseContext(plugin);
if (context === null) throw new Error("Could not create context");
loggedInClient = await createLoggedInClient(
context.platform,
context.spaceId,
context.spacePassword,
);
} else {
// renew session
const { error } = await loggedInClient.auth.getSession();
if (error) {
loggedInClient = null;
}
}
return loggedInClient;
};
15 changes: 15 additions & 0 deletions apps/obsidian/src/utils/syncDgNodesToSupabase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { getSupabaseContext } from "./supabaseContext";
import type DiscourseGraphPlugin from "~/index";

export const initializeSupabaseSync = async (
plugin: DiscourseGraphPlugin,
): Promise<void> => {
const context = await getSupabaseContext(plugin);
if (!context) {
throw new Error("Failed to initialize Supabase sync: could not create context");
}
console.log("Supabase sync initialized successfully", {
spaceId: context.spaceId,
userId: context.userId,
});
};
3 changes: 2 additions & 1 deletion packages/database/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,8 @@
"@repo/utils": "workspace:*",
"@supabase/auth-js": "catalog:",
"@supabase/functions-js": "catalog:",
"@supabase/supabase-js": "catalog:"
"@supabase/supabase-js": "catalog:",
"tslib": "2.5.1"
},
"devDependencies": {
"@cucumber/cucumber": "^12.1.0",
Expand Down
Loading