aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2025-02-14 15:53:44 -0800
committerDhravya Shah <[email protected]>2025-02-14 15:53:44 -0800
commitaf1093d4aab6d5654364e7cd9df76fd766bbee03 (patch)
treef128bdb8cac4b44ed41289dc9a158c33fe86c5ec
parenttwitter import fix (diff)
downloadsupermemory-af1093d4aab6d5654364e7cd9df76fd766bbee03.tar.xz
supermemory-af1093d4aab6d5654364e7cd9df76fd766bbee03.zip
intuitive memory movement, avoid duplicates in home
-rw-r--r--apps/backend/src/index.tsx4
-rw-r--r--apps/backend/src/routes/memories.ts32
-rw-r--r--apps/backend/src/routes/spaces.ts116
-rw-r--r--apps/web/app/components/memories/SharedCard.tsx12
-rw-r--r--apps/web/app/lib/hooks/use-spaces.tsx22
-rw-r--r--apps/web/app/routes/content.$contentid.tsx10
6 files changed, 183 insertions, 13 deletions
diff --git a/apps/backend/src/index.tsx b/apps/backend/src/index.tsx
index 1d62b543..48790e90 100644
--- a/apps/backend/src/index.tsx
+++ b/apps/backend/src/index.tsx
@@ -47,6 +47,10 @@ export const app = new Hono<{ Variables: Variables; Bindings: Env }>()
.use("/v1/*", auth)
.use("/v1/*", (c, next) => {
const user = c.get("user");
+
+ if (c.env.NODE_ENV === "development") {
+ return next();
+ }
// RATELIMITS
const rateLimitConfig = {
diff --git a/apps/backend/src/routes/memories.ts b/apps/backend/src/routes/memories.ts
index efcc6fb7..aa35877b 100644
--- a/apps/backend/src/routes/memories.ts
+++ b/apps/backend/src/routes/memories.ts
@@ -8,7 +8,7 @@ import {
spaceAccess,
contentToSpace,
} from "@supermemory/db/schema";
-import { and, database, desc, eq, or, sql } from "@supermemory/db";
+import { and, database, desc, eq, or, sql, isNull } from "@supermemory/db";
const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
.get(
@@ -124,9 +124,20 @@ const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
const [items, [{ total }]] = await Promise.all([
db
- .select()
+ .select({
+ documents: documents,
+ })
.from(documents)
- .where(eq(documents.userId, user.id))
+ .leftJoin(
+ contentToSpace,
+ eq(documents.id, contentToSpace.contentId)
+ )
+ .where(
+ and(
+ eq(documents.userId, user.id),
+ isNull(contentToSpace.contentId)
+ )
+ )
.orderBy(desc(documents.createdAt))
.limit(count)
.offset(start),
@@ -135,13 +146,22 @@ const memories = new Hono<{ Variables: Variables; Bindings: Env }>()
total: sql<number>`count(*)`.as("total"),
})
.from(documents)
- .where(eq(documents.userId, user.id)),
+ .leftJoin(
+ contentToSpace,
+ eq(documents.id, contentToSpace.contentId)
+ )
+ .where(
+ and(
+ eq(documents.userId, user.id),
+ isNull(contentToSpace.contentId)
+ )
+ ),
]);
return c.json({
items: items.map((item) => ({
- ...item,
- id: item.uuid,
+ ...item.documents,
+ id: item.documents.uuid,
})),
total,
});
diff --git a/apps/backend/src/routes/spaces.ts b/apps/backend/src/routes/spaces.ts
index 2ca2e461..bede366c 100644
--- a/apps/backend/src/routes/spaces.ts
+++ b/apps/backend/src/routes/spaces.ts
@@ -99,6 +99,7 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
const spaceId = c.req.param("spaceId");
const db = database(c.env.HYPERDRIVE.connectionString);
+
const space = await db
.select()
.from(spaces)
@@ -178,6 +179,10 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
return c.json({ error: "Unauthorized" }, 401);
}
+ if (body.spaceName.trim() === "<HOME>") {
+ return c.json({ error: "Cannot create space with name <HOME>" }, 400);
+ }
+
const db = database(c.env.HYPERDRIVE.connectionString);
const uuid = randomId();
@@ -264,6 +269,99 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
}
)
.post(
+ "/moveContent",
+ zValidator(
+ "json",
+ z.object({
+ spaceId: z.string(),
+ documentId: z.string(),
+ })
+ ),
+ async (c) => {
+ const body = c.req.valid("json");
+ const user = c.get("user");
+ const { spaceId, documentId } = body;
+
+ if (!user) {
+ return c.json({ error: "Unauthorized" }, 401);
+ }
+
+ const db = database(c.env.HYPERDRIVE.connectionString);
+
+ try {
+ await db.transaction(async (tx) => {
+ // If moving to <HOME>, just remove all space connections
+ if (spaceId === "<HOME>") {
+ const doc = await tx
+ .select()
+ .from(documents)
+ .where(eq(documents.uuid, documentId))
+ .limit(1);
+
+ if (!doc[0]) {
+ return c.json({ error: "Document not found" }, 404);
+ }
+
+ await tx
+ .delete(contentToSpace)
+ .where(eq(contentToSpace.contentId, doc[0].id));
+ return;
+ }
+
+ // Get space and document, verify space ownership
+ const results = (
+ await tx
+ .select({
+ spaceId: spaces.id,
+ documentId: documents.id,
+ ownerId: spaces.ownerId,
+ spaceName: spaces.name,
+ })
+ .from(spaces)
+ .innerJoin(
+ documents,
+ and(eq(spaces.uuid, spaceId), eq(documents.uuid, documentId))
+ )
+ .limit(1)
+ )[0];
+
+ if (!results) {
+ return c.json({ error: "Space or document not found" }, 404);
+ }
+
+ if (results.ownerId !== user.id) {
+ return c.json(
+ { error: "Not authorized to modify this space" },
+ 403
+ );
+ }
+
+ // Delete existing space relations for this document
+ await tx
+ .delete(contentToSpace)
+ .where(eq(contentToSpace.contentId, results.documentId));
+
+ // Add new space relation
+ await tx.insert(contentToSpace).values({
+ contentId: results.documentId,
+ spaceId: results.spaceId,
+ });
+ });
+
+ return c.json({ success: true, spaceId });
+ } catch (e) {
+ console.error("Failed to move content to space:", e);
+ return c.json(
+ {
+ error: "Failed to move content to space",
+ details: e instanceof Error ? e.message : "Unknown error",
+ },
+ 500
+ );
+ }
+ }
+ )
+ .post(
"/addContent",
zValidator(
"json",
@@ -285,6 +383,24 @@ const spacesRoute = new Hono<{ Variables: Variables; Bindings: Env }>()
try {
await db.transaction(async (tx) => {
+ // If adding to <HOME>, just remove all space connections
+ if (spaceId === "<HOME>") {
+ const doc = await tx
+ .select()
+ .from(documents)
+ .where(eq(documents.uuid, documentId))
+ .limit(1);
+
+ if (!doc[0]) {
+ return c.json({ error: "Document not found" }, 404);
+ }
+
+ await tx
+ .delete(contentToSpace)
+ .where(eq(contentToSpace.contentId, doc[0].id));
+ return;
+ }
+
// Get space and document, verify space ownership
const results = (
await tx
diff --git a/apps/web/app/components/memories/SharedCard.tsx b/apps/web/app/components/memories/SharedCard.tsx
index d80b848a..8e4c69b1 100644
--- a/apps/web/app/components/memories/SharedCard.tsx
+++ b/apps/web/app/components/memories/SharedCard.tsx
@@ -41,6 +41,7 @@ import { ExtraSpaceMetaData, fetchSpaces } from "~/lib/hooks/use-spaces";
import { useTextOverflow } from "~/lib/hooks/use-text-overflow";
import { Memory, WebsiteMetadata } from "~/lib/types/memory";
import { cn } from "~/lib/utils";
+import { useNavigate } from "@remix-run/react";
const { useTweet } = ReactTweet;
@@ -616,6 +617,7 @@ export function FetchAndRenderContent({ content }: { content: string }) {
function SharedCard({ data }: { data: Memory }) {
const queryClient = useQueryClient();
+ const navigate = useNavigate();
// Delete mutation
const deleteMutation = useMutation({
@@ -657,7 +659,7 @@ function SharedCard({ data }: { data: Memory }) {
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
- const response = await fetch("/backend/v1/spaces/addContent", {
+ const response = await fetch("/backend/v1/spaces/moveContent", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -668,15 +670,19 @@ function SharedCard({ data }: { data: Memory }) {
if (!response.ok) {
throw new Error("Failed to move memory");
}
- return response.json();
+ return response.json() as Promise<{ spaceId: string }>;
},
onError: (err) => {
toast.error("Failed to move memory to space");
},
- onSuccess: () => {
+ onSuccess: ({ spaceId }: { spaceId: string }) => {
toast.success("Memory moved successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
queryClient.invalidateQueries({ queryKey: ["spaces"] });
+ if (spaceId === "<HOME>") {
+ return navigate("/");
+ }
+ return navigate(`/space/${spaceId}`);
},
});
diff --git a/apps/web/app/lib/hooks/use-spaces.tsx b/apps/web/app/lib/hooks/use-spaces.tsx
index 14ffb56c..1654fd47 100644
--- a/apps/web/app/lib/hooks/use-spaces.tsx
+++ b/apps/web/app/lib/hooks/use-spaces.tsx
@@ -44,7 +44,27 @@ export async function fetchSpaces(): Promise<SpaceResponse> {
throw new Error("Failed to fetch spaces");
}
- return response.json();
+ const resp = (await response.json()) as SpaceResponse;
+
+ resp.spaces.push({
+ id: 0,
+ uuid: "<HOME>",
+ name: "Home",
+ createdAt: new Date(),
+ updatedAt: new Date(),
+ ownerId: 0,
+ isPublic: false,
+ permissions: {
+ canRead: false,
+ canEdit: false,
+ isOwner: false,
+ isPublic: false,
+ },
+ owner: null,
+ favorited: false,
+ });
+
+ return resp;
}
async function createSpace(data: {
diff --git a/apps/web/app/routes/content.$contentid.tsx b/apps/web/app/routes/content.$contentid.tsx
index ca81d039..da7e23d7 100644
--- a/apps/web/app/routes/content.$contentid.tsx
+++ b/apps/web/app/routes/content.$contentid.tsx
@@ -94,7 +94,7 @@ export default function Content() {
// Move to space mutation
const moveToSpaceMutation = useMutation({
mutationFn: async ({ spaceId, documentId }: { spaceId: string; documentId: string }) => {
- const response = await fetch("/backend/v1/spaces/addContent", {
+ const response = await fetch("/backend/v1/spaces/moveContent", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -105,12 +105,16 @@ export default function Content() {
if (!response.ok) {
throw new Error("Failed to move memory");
}
- return response.json();
+ return response.json() as Promise<{ spaceId: string }>;
},
- onSuccess: () => {
+ onSuccess: ({ spaceId }: {spaceId: string}) => {
toast.success("Memory moved successfully");
queryClient.invalidateQueries({ queryKey: ["memories"] });
queryClient.invalidateQueries({ queryKey: ["spaces"] });
+ if (spaceId === "<HOME>") {
+ return navigate("/");
+ }
+ return navigate(`/space/${spaceId}`);
},
onError: () => {
toast.error("Failed to move memory to space");