From 98bd664e7e9bed0a824ee065255708224f6b8963 Mon Sep 17 00:00:00 2001 From: Egor Gorbachev <7gorbachevm@gmail.com> Date: Wed, 10 Jan 2024 18:46:05 +0700 Subject: [PATCH] Folder sharing (#39) * Folder sharing --- functions/add-deck-access.ts | 32 +++++--- functions/db/custom-types.ts | 3 + functions/db/databaseTypes.ts | 24 +++++- .../db/deck-access/create-deck-access-db.ts | 15 +++- .../get-deck-access-by-share-id-db.ts | 6 +- .../get-last-deck-accesses-for-deck-db.ts | 13 +++- .../db/deck/get-deck-by-id-and-author-id.ts | 2 +- .../get-deck-with-cards-by-share-id-db.ts | 11 ++- .../db/deck/get-many-decks-with-cards-db.ts | 26 +++++++ .../db/deck/get-my-decks-with-cards-db.ts | 24 +----- functions/db/deck/is-user-deck-exists.ts | 8 +- functions/db/deck/remove-deck-from-mine-db.ts | 25 ++++++- functions/db/folder/add-folder-to-mine-db.tsx | 37 +++++++++ .../folder/get-folder-by-id-and-author-id.ts | 2 +- .../db/folder/get-folder-with-deck-ids.ts | 37 +++++++++ .../get-folder-with-decks-with-cards-db.ts | 32 ++++++++ ...tsx => get-many-folders-with-decks-db.tsx} | 6 +- functions/deck-accesses.ts | 24 ++++-- functions/delete-folder.ts | 24 ++++-- functions/get-shared-deck.test.ts | 4 + functions/get-shared-deck.ts | 75 +++++++++++++++---- functions/my-info.ts | 6 +- functions/upsert-deck.ts | 6 +- functions/upsert-folder.ts | 17 +++-- src/api/api.ts | 7 +- src/screens/app.tsx | 13 +++- src/screens/deck-form/deck-form.tsx | 1 + .../deck-form/store/deck-form-store.ts | 18 +++-- src/screens/deck-review/deck-preview.tsx | 27 +++---- src/screens/folder-review/folder-preview.tsx | 19 ++++- src/screens/share-deck/format-access-user.ts | 14 ++++ ... redirect-user-to-deck-or-folder-link.tsx} | 6 +- .../share-deck/share-deck-one-time-links.tsx | 26 ++----- .../share-deck/share-deck-settings.tsx | 2 +- ...deck-store.ts => share-deck-form-store.ts} | 44 +++++++---- .../store/share-deck-store-context.tsx | 17 +++-- .../user-settings/user-settings-main.tsx | 2 +- src/store/deck-list-store.ts | 69 ++++++++++++----- src/store/screen-store.ts | 1 + src/translations/t.ts | 11 +++ 40 files changed, 557 insertions(+), 179 deletions(-) create mode 100644 functions/db/custom-types.ts create mode 100644 functions/db/deck/get-many-decks-with-cards-db.ts create mode 100644 functions/db/folder/add-folder-to-mine-db.tsx create mode 100644 functions/db/folder/get-folder-with-deck-ids.ts create mode 100644 functions/db/folder/get-folder-with-decks-with-cards-db.ts rename functions/db/folder/{get-folders-with-decks-db.tsx => get-many-folders-with-decks-db.tsx} (85%) create mode 100644 src/screens/share-deck/format-access-user.ts rename src/screens/share-deck/{redirect-user-to-deck-link.tsx => redirect-user-to-deck-or-folder-link.tsx} (69%) rename src/screens/share-deck/store/{share-deck-store.ts => share-deck-form-store.ts} (63%) diff --git a/functions/add-deck-access.ts b/functions/add-deck-access.ts index 403539cd..1f595146 100644 --- a/functions/add-deck-access.ts +++ b/functions/add-deck-access.ts @@ -8,10 +8,13 @@ import { envSchema } from "./env/env-schema.ts"; import { createForbiddenRequestResponse } from "./lib/json-response/create-forbidden-request-response.ts"; import { createJsonResponse } from "./lib/json-response/create-json-response.ts"; import { createDeckAccessDb } from "./db/deck-access/create-deck-access-db.ts"; +import { getFolderByIdAndAuthorId } from "./db/folder/get-folder-by-id-and-author-id.ts"; const requestSchema = z.object({ - deckId: z.number(), + deckId: z.number().nullable(), + folderId: z.number().nullable(), durationDays: z.number().nullable(), + type: z.enum(["deck", "folder"]), }); const responseSchema = z.object({ @@ -30,20 +33,29 @@ export const onRequestPost = handleError(async ({ request, env }) => { } const envSafe = envSchema.parse(env); - const canEdit = await getDeckByIdAndAuthorId( - envSafe, - input.data.deckId, - user, - ); - if (!canEdit) { - return createForbiddenRequestResponse(); + if (input.data.deckId) { + if (!(await getDeckByIdAndAuthorId(envSafe, input.data.deckId, user))) { + return createForbiddenRequestResponse(); + } + } + if (input.data.folderId) { + if (!(await getFolderByIdAndAuthorId(envSafe, input.data.folderId, user))) { + return createForbiddenRequestResponse(); + } + } + + // Only 1 option from deckId or folderId should be provided + if ( + (input.data.folderId && input.data.deckId) || + (!input.data.folderId && !input.data.deckId) + ) { + return createBadRequestResponse(); } const createDeckAccessResult = await createDeckAccessDb( envSafe, user.id, - input.data.deckId, - input.data.durationDays, + input.data, ); return createJsonResponse( diff --git a/functions/db/custom-types.ts b/functions/db/custom-types.ts new file mode 100644 index 00000000..e823d0fa --- /dev/null +++ b/functions/db/custom-types.ts @@ -0,0 +1,3 @@ +import { Database } from "./databaseTypes.ts"; + +export type DeckAccessType = Database["public"]["Enums"]["deck_access_type"]; diff --git a/functions/db/databaseTypes.ts b/functions/db/databaseTypes.ts index 96af94bd..2bcc8bbd 100644 --- a/functions/db/databaseTypes.ts +++ b/functions/db/databaseTypes.ts @@ -108,33 +108,39 @@ export interface Database { Row: { author_id: number created_at: string - deck_id: number + deck_id: number | null duration_days: number | null + folder_id: number | null id: number processed_at: string | null share_id: string + type: Database["public"]["Enums"]["deck_access_type"] usage_started_at: string | null used_by: number | null } Insert: { author_id: number created_at?: string - deck_id: number + deck_id?: number | null duration_days?: number | null + folder_id?: number | null id?: number processed_at?: string | null share_id: string + type?: Database["public"]["Enums"]["deck_access_type"] usage_started_at?: string | null used_by?: number | null } Update: { author_id?: number created_at?: string - deck_id?: number + deck_id?: number | null duration_days?: number | null + folder_id?: number | null id?: number processed_at?: string | null share_id?: string + type?: Database["public"]["Enums"]["deck_access_type"] usage_started_at?: string | null used_by?: number | null } @@ -151,6 +157,12 @@ export interface Database { referencedRelation: "deck" referencedColumns: ["id"] }, + { + foreignKeyName: "deck_access_folder_id_fkey" + columns: ["folder_id"] + referencedRelation: "folder" + referencedColumns: ["id"] + }, { foreignKeyName: "deck_access_used_by_fkey" columns: ["used_by"] @@ -254,6 +266,7 @@ export interface Database { created_at: string description: string | null id: number + share_id: string title: string } Insert: { @@ -261,6 +274,7 @@ export interface Database { created_at?: string description?: string | null id?: number + share_id: string title: string } Update: { @@ -268,6 +282,7 @@ export interface Database { created_at?: string description?: string | null id?: number + share_id?: string title?: string } Relationships: [ @@ -476,6 +491,7 @@ export interface Database { folder_id: number folder_title: string folder_description: string + folder_share_id: string folder_author_id: number deck_id: number }[] @@ -547,7 +563,7 @@ export interface Database { } } Enums: { - [_ in never]: never + deck_access_type: "deck" | "folder" } CompositeTypes: { [_ in never]: never diff --git a/functions/db/deck-access/create-deck-access-db.ts b/functions/db/deck-access/create-deck-access-db.ts index 6c191a00..1fba6b2d 100644 --- a/functions/db/deck-access/create-deck-access-db.ts +++ b/functions/db/deck-access/create-deck-access-db.ts @@ -2,22 +2,29 @@ import { EnvSafe } from "../../env/env-schema.ts"; import { shortUniqueId } from "../../lib/short-unique-id/short-unique-id.ts"; import { DatabaseException } from "../database-exception.ts"; import { getDatabase } from "../get-database.ts"; +import { DeckAccessType } from "../custom-types.ts"; export const createDeckAccessDb = async ( envSafe: EnvSafe, userId: number, - deckId: number, - durationDays: number | null, + body: { + deckId: number | null; + folderId: number | null; + durationDays: number | null; + type: DeckAccessType; + }, ) => { const db = getDatabase(envSafe); const createDeckAccessResult = await db .from("deck_access") .insert({ - deck_id: deckId, + deck_id: body.deckId, + folder_id: body.folderId, author_id: userId, share_id: shortUniqueId(), - duration_days: durationDays, + duration_days: body.durationDays, + type: body.type, }) .select() .single(); diff --git a/functions/db/deck-access/get-deck-access-by-share-id-db.ts b/functions/db/deck-access/get-deck-access-by-share-id-db.ts index 758a0729..c65d3192 100644 --- a/functions/db/deck-access/get-deck-access-by-share-id-db.ts +++ b/functions/db/deck-access/get-deck-access-by-share-id-db.ts @@ -4,10 +4,12 @@ import { EnvSafe } from "../../env/env-schema.ts"; import { z } from "zod"; const resultSchema = z.object({ - deck_id: z.number(), + deck_id: z.number().nullable(), + folder_id: z.number().nullable(), author_id: z.number(), used_by: z.number().nullable(), processed_at: z.string().nullable(), + type: z.enum(["folder", "deck"]), }); type GetDeckAccessByShareIdDbResultType = z.infer; @@ -20,7 +22,7 @@ export const getDeckAccessByShareIdDb = async ( const oneTimeShareLinkResult = await db .from("deck_access") - .select("deck_id, author_id, used_by, processed_at") + .select("deck_id, author_id, used_by, processed_at, folder_id, type") .eq("share_id", shareId) .maybeSingle(); diff --git a/functions/db/deck-access/get-last-deck-accesses-for-deck-db.ts b/functions/db/deck-access/get-last-deck-accesses-for-deck-db.ts index c14bbfb1..748fc0f8 100644 --- a/functions/db/deck-access/get-last-deck-accesses-for-deck-db.ts +++ b/functions/db/deck-access/get-last-deck-accesses-for-deck-db.ts @@ -25,19 +25,26 @@ export type DeckAccessesForDeckTypeDb = z.infer; export const getLastDeckAccessesForDeckDb = async ( envSafe: EnvSafe, - deckId: number, + filters: { deckId: number } | { folderId: number }, ): Promise => { const db = getDatabase(envSafe); - const { data, error } = await db + const query = db .from("deck_access") .select( "deck_id, author_id, used_by, share_id, id, created_at, duration_days, user:used_by (id, username, first_name, last_name)", ) - .eq("deck_id", deckId) .order("created_at", { ascending: false }) .limit(100); + if ("deckId" in filters) { + query.eq("deck_id", filters.deckId); + } + if ("folderId" in filters) { + query.eq("folder_id", filters.folderId); + } + + const { data, error } = await query; if (error) { throw new DatabaseException(error); } diff --git a/functions/db/deck/get-deck-by-id-and-author-id.ts b/functions/db/deck/get-deck-by-id-and-author-id.ts index 2d34dedf..5b63c8a0 100644 --- a/functions/db/deck/get-deck-by-id-and-author-id.ts +++ b/functions/db/deck/get-deck-by-id-and-author-id.ts @@ -14,7 +14,7 @@ export const getDeckByIdAndAuthorId = async ( query = query.eq("author_id", user.id); } - const canEditDeckResult = await query.single(); + const canEditDeckResult = await query.maybeSingle(); if (canEditDeckResult.error) { throw new DatabaseException(canEditDeckResult.error); } diff --git a/functions/db/deck/get-deck-with-cards-by-share-id-db.ts b/functions/db/deck/get-deck-with-cards-by-share-id-db.ts index 9aeb3b1a..ff0b6c2d 100644 --- a/functions/db/deck/get-deck-with-cards-by-share-id-db.ts +++ b/functions/db/deck/get-deck-with-cards-by-share-id-db.ts @@ -1,12 +1,15 @@ import { DatabaseException } from "../database-exception.ts"; -import { deckWithCardsSchema } from "./decks-with-cards-schema.ts"; +import { + DeckWithCardsDbType, + deckWithCardsSchema, +} from "./decks-with-cards-schema.ts"; import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; export const getDeckWithCardsByShareIdDb = async ( env: EnvSafe, shareId: string, -) => { +): Promise => { const db = getDatabase(env); const { data, error } = await db @@ -14,11 +17,11 @@ export const getDeckWithCardsByShareIdDb = async ( .select("*, deck_card!deck_card_deck_id_fkey(*)") .eq("share_id", shareId) .limit(1) - .single(); + .maybeSingle(); if (error) { throw new DatabaseException(error); } - return deckWithCardsSchema.parse(data); + return data ? deckWithCardsSchema.parse(data) : null; }; diff --git a/functions/db/deck/get-many-decks-with-cards-db.ts b/functions/db/deck/get-many-decks-with-cards-db.ts new file mode 100644 index 00000000..2226ab89 --- /dev/null +++ b/functions/db/deck/get-many-decks-with-cards-db.ts @@ -0,0 +1,26 @@ +import { DatabaseException } from "../database-exception.ts"; +import { + decksWithCardsSchema, + DeckWithCardsDbType, +} from "./decks-with-cards-schema.ts"; +import { getDatabase } from "../get-database.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; + +export const getManyDecksWithCardsDb = async ( + env: EnvSafe, + deckIds: number[], +): Promise => { + const db = getDatabase(env); + + const { data, error } = await db + .from("deck") + .select("*, deck_card!deck_card_deck_id_fkey(*)") + .in("id", deckIds) + .order("id", { ascending: false }); + + if (error) { + throw new DatabaseException(error); + } + + return decksWithCardsSchema.parse(data); +}; diff --git a/functions/db/deck/get-my-decks-with-cards-db.ts b/functions/db/deck/get-my-decks-with-cards-db.ts index 3846e287..9b7edee6 100644 --- a/functions/db/deck/get-my-decks-with-cards-db.ts +++ b/functions/db/deck/get-my-decks-with-cards-db.ts @@ -1,11 +1,9 @@ import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; -import { - decksWithCardsSchema, - DeckWithCardsDbType, -} from "./decks-with-cards-schema.ts"; +import { DeckWithCardsDbType } from "./decks-with-cards-schema.ts"; import { z } from "zod"; +import { getManyDecksWithCardsDb } from "./get-many-decks-with-cards-db.ts"; export const getMyDecksWithCardsDb = async ( env: EnvSafe, @@ -22,23 +20,9 @@ export const getMyDecksWithCardsDb = async ( } const deckIds = z - .array( - z.object({ - id: z.number(), - }), - ) + .array(z.object({ id: z.number() })) .transform((list) => list.map((item) => item.id)) .parse(getUserDeckIdsResult.data); - const { data, error } = await db - .from("deck") - .select("*, deck_card!deck_card_deck_id_fkey(*)") - .in("id", deckIds) - .order("id", { ascending: false }); - - if (error) { - throw new DatabaseException(error); - } - - return decksWithCardsSchema.parse(data); + return getManyDecksWithCardsDb(env, deckIds); }; diff --git a/functions/db/deck/is-user-deck-exists.ts b/functions/db/deck/is-user-deck-exists.ts index b8693889..dfe14008 100644 --- a/functions/db/deck/is-user-deck-exists.ts +++ b/functions/db/deck/is-user-deck-exists.ts @@ -8,11 +8,15 @@ export const isUserDeckExists = async ( ) => { const db = getDatabase(envSafe); - const userDeck = await db.from("user_deck").select().match(body); + const userDeck = await db + .from("user_deck") + .select() + .match(body) + .maybeSingle(); if (userDeck.error) { throw new DatabaseException(userDeck.error); } - return userDeck.data.length ? userDeck.data[0] : null; + return userDeck.data ? userDeck.data : null; }; diff --git a/functions/db/deck/remove-deck-from-mine-db.ts b/functions/db/deck/remove-deck-from-mine-db.ts index 207211ea..d5e9f5cc 100644 --- a/functions/db/deck/remove-deck-from-mine-db.ts +++ b/functions/db/deck/remove-deck-from-mine-db.ts @@ -1,6 +1,7 @@ import { EnvSafe } from "../../env/env-schema.ts"; import { getDatabase } from "../get-database.ts"; import { DatabaseException } from "../database-exception.ts"; +import { getDeckByIdAndAuthorId } from "./get-deck-by-id-and-author-id.ts"; export const removeDeckFromMineDb = async ( env: EnvSafe, @@ -8,10 +9,28 @@ export const removeDeckFromMineDb = async ( ): Promise => { const db = getDatabase(env); - const { error } = await db.from("user_deck").delete().match(body); + const deleteFromUserDeckResult = await db + .from("user_deck") + .delete() + .match(body); + if (deleteFromUserDeckResult.error) { + throw new DatabaseException(deleteFromUserDeckResult.error); + } - if (error) { - throw new DatabaseException(error); + const userDeckResult = await getDeckByIdAndAuthorId(env, body.deck_id, { + id: body.user_id, + is_admin: false, + }); + if (userDeckResult) { + const deleteFromDeckFolderResult = await db + .from("deck_folder") + .delete() + .match({ + deck_id: body.deck_id, + }); + if (deleteFromDeckFolderResult.error) { + throw new DatabaseException(deleteFromDeckFolderResult.error); + } } return null; diff --git a/functions/db/folder/add-folder-to-mine-db.tsx b/functions/db/folder/add-folder-to-mine-db.tsx new file mode 100644 index 00000000..76188e32 --- /dev/null +++ b/functions/db/folder/add-folder-to-mine-db.tsx @@ -0,0 +1,37 @@ +import { EnvSafe } from "../../env/env-schema.ts"; +import { getDatabase } from "../get-database.ts"; +import { DatabaseException } from "../database-exception.ts"; +import { getFolderWithDeckIds } from "./get-folder-with-deck-ids.ts"; + +export const addFolderToMine = async ( + env: EnvSafe, + body: { user_id: number; folder_id: number }, +) => { + const folderWithDeckIdsResult = await getFolderWithDeckIds(env, { + id: body.folder_id, + }); + + const db = getDatabase(env); + const connectUserToFolder = await db + .from("user_folder") + .insert(body) + .single(); + + if (connectUserToFolder.error) { + throw new DatabaseException(connectUserToFolder.error); + } + + const connectDecksToUser = await db + .from("user_deck") + .upsert( + folderWithDeckIdsResult.decks.map((deck) => ({ + user_id: body.user_id, + deck_id: deck.id, + })), + ) + .select(); + + if (connectDecksToUser.error) { + throw new DatabaseException(connectDecksToUser.error); + } +}; diff --git a/functions/db/folder/get-folder-by-id-and-author-id.ts b/functions/db/folder/get-folder-by-id-and-author-id.ts index 7b271e71..e4462d91 100644 --- a/functions/db/folder/get-folder-by-id-and-author-id.ts +++ b/functions/db/folder/get-folder-by-id-and-author-id.ts @@ -14,7 +14,7 @@ export const getFolderByIdAndAuthorId = async ( query = query.eq("author_id", user.id); } - const canEditResult = await query.single(); + const canEditResult = await query.maybeSingle(); if (canEditResult.error) { throw new DatabaseException(canEditResult.error); } diff --git a/functions/db/folder/get-folder-with-deck-ids.ts b/functions/db/folder/get-folder-with-deck-ids.ts new file mode 100644 index 00000000..7347e979 --- /dev/null +++ b/functions/db/folder/get-folder-with-deck-ids.ts @@ -0,0 +1,37 @@ +import { EnvSafe } from "../../env/env-schema.ts"; +import { DatabaseException } from "../database-exception.ts"; +import { getDatabase } from "../get-database.ts"; +import { z } from "zod"; + +const folderSchema = z.object({ + id: z.number(), + title: z.string(), + author_id: z.number(), + description: z.string().nullable(), + share_id: z.string(), + decks: z.array( + z.object({ + id: z.number(), + }), + ), +}); + +export type FolderWithDeckIdsDbType = z.infer; + +export const getFolderWithDeckIds = async ( + env: EnvSafe, + match: { id: number } | { share_id: string }, +): Promise => { + const db = getDatabase(env); + const folderWithDecksResult = await db + .from("folder") + .select("*, decks:deck(id)") + .match(match) + .single(); + + if (folderWithDecksResult.error) { + throw new DatabaseException(folderWithDecksResult.error); + } + + return folderSchema.parse(folderWithDecksResult.data); +}; diff --git a/functions/db/folder/get-folder-with-decks-with-cards-db.ts b/functions/db/folder/get-folder-with-decks-with-cards-db.ts new file mode 100644 index 00000000..56d10852 --- /dev/null +++ b/functions/db/folder/get-folder-with-decks-with-cards-db.ts @@ -0,0 +1,32 @@ +import { getManyDecksWithCardsDb } from "../deck/get-many-decks-with-cards-db.ts"; +import { EnvSafe } from "../../env/env-schema.ts"; +import { + FolderWithDeckIdsDbType, + getFolderWithDeckIds, +} from "./get-folder-with-deck-ids.ts"; +import { DeckWithCardsDbType } from "../deck/decks-with-cards-schema.ts"; + +export type FolderWithDecksWithCards = Omit< + FolderWithDeckIdsDbType, + "decks" +> & { + decks: DeckWithCardsDbType[]; +}; + +export const getFolderWithDecksWithCardsDb = async ( + envSafe: EnvSafe, + folderMatch: { id: number } | { share_id: string }, +): Promise => { + const folderWithDeckIdsResult = await getFolderWithDeckIds( + envSafe, + folderMatch, + ); + + return { + ...folderWithDeckIdsResult, + decks: await getManyDecksWithCardsDb( + envSafe, + folderWithDeckIdsResult.decks.map((deck) => deck.id), + ), + }; +}; diff --git a/functions/db/folder/get-folders-with-decks-db.tsx b/functions/db/folder/get-many-folders-with-decks-db.tsx similarity index 85% rename from functions/db/folder/get-folders-with-decks-db.tsx rename to functions/db/folder/get-many-folders-with-decks-db.tsx index 57d5b3e9..b812f086 100644 --- a/functions/db/folder/get-folders-with-decks-db.tsx +++ b/functions/db/folder/get-many-folders-with-decks-db.tsx @@ -8,12 +8,16 @@ const userFoldersSchema = z.object({ folder_title: z.string(), folder_description: z.string().nullable(), folder_author_id: z.number(), + folder_share_id: z.string(), deck_id: z.number().nullable(), }); export type UserFoldersDbType = z.infer; -export const getFoldersWithDecksDb = async (env: EnvSafe, userId: number) => { +export const getManyFoldersWithDecksDb = async ( + env: EnvSafe, + userId: number, +) => { const db = getDatabase(env); const result = await db.rpc("get_folder_with_decks", { diff --git a/functions/deck-accesses.ts b/functions/deck-accesses.ts index 60b29a2e..8e456e4a 100644 --- a/functions/deck-accesses.ts +++ b/functions/deck-accesses.ts @@ -9,6 +9,20 @@ import { getLastDeckAccessesForDeckDb, } from "./db/deck-access/get-last-deck-accesses-for-deck-db.ts"; +const getFilters = (urlString: string) => { + const url = new URL(urlString); + const deckId = url.searchParams.get("deckId"); + const folderId = url.searchParams.get("folderId"); + + if (deckId) { + return { deckId: parseInt(deckId) }; + } + if (folderId) { + return { folderId: parseInt(folderId) }; + } + return null; +}; + export type DeckAccessesResponse = { accesses: DeckAccessesForDeckTypeDb; }; @@ -17,15 +31,11 @@ export const onRequest = handleError(async ({ request, env }) => { const user = await getUser(request, env); if (!user) return createAuthFailedResponse(); - const url = new URL(request.url); - const deckId = url.searchParams.get("deck_id"); - if (!deckId) { - return createBadRequestResponse(); - } + const filters = getFilters(request.url); + if (!filters) return createBadRequestResponse(); const envSafe = envSchema.parse(env); - - const data = await getLastDeckAccessesForDeckDb(envSafe, Number(deckId)); + const data = await getLastDeckAccessesForDeckDb(envSafe, filters); return createJsonResponse({ accesses: data, diff --git a/functions/delete-folder.ts b/functions/delete-folder.ts index 49991cce..d554b8cc 100644 --- a/functions/delete-folder.ts +++ b/functions/delete-folder.ts @@ -6,6 +6,7 @@ import { createBadRequestResponse } from "./lib/json-response/create-bad-request import { getFolderByIdAndAuthorId } from "./db/folder/get-folder-by-id-and-author-id.ts"; import { createJsonResponse } from "./lib/json-response/create-json-response.ts"; import { deleteFolderById } from "./db/folder/delete-folder-by-id.ts"; +import { getDatabase } from "./db/get-database.ts"; export const onRequestPost = handleError(async ({ request, env }) => { const user = await getUser(request, env); @@ -20,16 +21,29 @@ export const onRequestPost = handleError(async ({ request, env }) => { return createBadRequestResponse(); } - const canEdit = await getFolderByIdAndAuthorId( + const folderToDelete = await getFolderByIdAndAuthorId( envSafe, parseInt(folderId), - user, + // Ignore user.is_admin to avoid accidentally deleting other user's folder + { ...user, is_admin: false }, ); - if (!canEdit) { - return createBadRequestResponse(); + if (folderToDelete) { + await deleteFolderById(envSafe, parseInt(folderId)); + return createJsonResponse(null); } - await deleteFolderById(envSafe, parseInt(folderId)); + const db = getDatabase(envSafe); + + const deleteUserFolderResult = await db + .from("user_folder") + .delete() + .match({ + user_id: user.id, + folder_id: parseInt(folderId), + }); + if (deleteUserFolderResult.error) { + return createBadRequestResponse(); + } return createJsonResponse(null); }); diff --git a/functions/get-shared-deck.test.ts b/functions/get-shared-deck.test.ts index 70337dd5..c4f28336 100644 --- a/functions/get-shared-deck.test.ts +++ b/functions/get-shared-deck.test.ts @@ -84,6 +84,7 @@ describe("get shared deck", () => { author_id: 1, used_by: null, deck_id: 1, + type: "deck", }); getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser1); @@ -116,6 +117,7 @@ describe("get shared deck", () => { author_id: 2, used_by: 2, deck_id: 2, + type: "deck", }); getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser2); @@ -131,6 +133,7 @@ describe("get shared deck", () => { author_id: 2, used_by: null, deck_id: 2, + type: "deck", }); getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser2); @@ -149,6 +152,7 @@ describe("get shared deck", () => { used_by: null, deck_id: 2, processed_at: new Date().toISOString(), + type: "deck", }); getDeckWithCardsByIdMock.mockReturnValue(mockDeckOfUser2); diff --git a/functions/get-shared-deck.ts b/functions/get-shared-deck.ts index 294848ba..a32048ae 100644 --- a/functions/get-shared-deck.ts +++ b/functions/get-shared-deck.ts @@ -13,10 +13,16 @@ import { startUsingDeckAccessDb } from "./db/deck-access/start-using-deck-access import { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts"; import { getDeckWithCardsByShareIdDb } from "./db/deck/get-deck-with-cards-by-share-id-db.ts"; import { addDeckToMineDb } from "./db/deck/add-deck-to-mine-db.ts"; +import { assert } from "./lib/typescript/assert.ts"; +import { addFolderToMine } from "./db/folder/add-folder-to-mine-db.tsx"; +import { + FolderWithDecksWithCards, + getFolderWithDecksWithCardsDb, +} from "./db/folder/get-folder-with-decks-with-cards-db.ts"; -export type GetSharedDeckResponse = { - deck: DeckWithCardsDbType; -}; +export type GetSharedDeckResponse = + | { deck: DeckWithCardsDbType } + | { folder: FolderWithDecksWithCards }; export const onRequest = handleError(async ({ env, request }) => { const user = await getUser(request, env); @@ -45,27 +51,68 @@ export const onRequest = handleError(async ({ env, request }) => { } } else { await startUsingDeckAccessDb(envSafe, user.id, shareId); - await addDeckToMineDb(envSafe, { - user_id: user.id, - deck_id: deckAccessResult.deck_id, - }); + + if (deckAccessResult.type === "deck") { + assert( + deckAccessResult.deck_id, + "deck_id is null when the type is deck", + ); + await addDeckToMineDb(envSafe, { + user_id: user.id, + deck_id: deckAccessResult.deck_id, + }); + } + if (deckAccessResult.type === "folder") { + assert( + deckAccessResult.folder_id, + "folder_id is null when the type is folder", + ); + await addFolderToMine(envSafe, { + user_id: user.id, + folder_id: deckAccessResult.folder_id, + }); + } } } - const deckId = deckAccessResult.deck_id; - const stableShareLinkResult = await getDeckWithCardsById(envSafe, deckId); + if (deckAccessResult.type === "deck") { + const deckId = deckAccessResult.deck_id; + assert(deckId, "deck_id is null when the type is deck"); + const deck = await getDeckWithCardsById(envSafe, deckId); - return createJsonResponse({ - deck: deckWithCardsSchema.parse(stableShareLinkResult), - }); + return createJsonResponse({ + deck: deckWithCardsSchema.parse(deck), + }); + } + if (deckAccessResult.type === "folder") { + const folderId = deckAccessResult.folder_id; + assert(folderId, "folder_id is null when the type is folder"); + const folder = await getFolderWithDecksWithCardsDb(envSafe, { + id: folderId, + }); + + return createJsonResponse({ + folder, + }); + } + assert(false, `Unknown deck access type: ${deckAccessResult.type}`); } else { - const stableShareLinkResult = await getDeckWithCardsByShareIdDb( + const deckStableShareLinkResult = await getDeckWithCardsByShareIdDb( envSafe, shareId, ); + if (deckStableShareLinkResult) { + return createJsonResponse({ + deck: deckWithCardsSchema.parse(deckStableShareLinkResult), + }); + } + + const folder = await getFolderWithDecksWithCardsDb(envSafe, { + share_id: shareId, + }); return createJsonResponse({ - deck: deckWithCardsSchema.parse(stableShareLinkResult), + folder, }); } }); diff --git a/functions/my-info.ts b/functions/my-info.ts index f8180730..1a985853 100644 --- a/functions/my-info.ts +++ b/functions/my-info.ts @@ -12,9 +12,9 @@ import { } from "./db/deck/get-cards-to-review-db.ts"; import { getUnAddedPublicDecksDb } from "./db/deck/get-un-added-public-decks-db.ts"; import { - getFoldersWithDecksDb, + getManyFoldersWithDecksDb, UserFoldersDbType, -} from "./db/folder/get-folders-with-decks-db.tsx"; +} from "./db/folder/get-many-folders-with-decks-db.tsx"; export type MyInfoResponse = { user: UserDbType; @@ -33,7 +33,7 @@ export const onRequest = handleError(async ({ request, env }) => { await getUnAddedPublicDecksDb(envSafe, user.id), await getMyDecksWithCardsDb(envSafe, user.id), await getCardsToReviewDb(envSafe, user.id), - await getFoldersWithDecksDb(envSafe, user.id), + await getManyFoldersWithDecksDb(envSafe, user.id), ]); return createJsonResponse({ diff --git a/functions/upsert-deck.ts b/functions/upsert-deck.ts index 9634a6f5..36ddd59d 100644 --- a/functions/upsert-deck.ts +++ b/functions/upsert-deck.ts @@ -18,9 +18,9 @@ import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts"; import { Database } from "./db/databaseTypes.ts"; import { getDeckWithCardsById } from "./db/deck/get-deck-with-cards-by-id-db.ts"; import { - getFoldersWithDecksDb, + getManyFoldersWithDecksDb, UserFoldersDbType, -} from "./db/folder/get-folders-with-decks-db.tsx"; +} from "./db/folder/get-many-folders-with-decks-db.tsx"; import { CardToReviewDbType, getCardsToReviewDb, @@ -147,7 +147,7 @@ export const onRequestPost = handleError(async ({ request, env }) => { const [deck, folders, cardsToReview] = await Promise.all([ getDeckWithCardsById(envSafe, upsertedDeck.id), - getFoldersWithDecksDb(envSafe, user.id), + getManyFoldersWithDecksDb(envSafe, user.id), getCardsToReviewDb(envSafe, user.id), ]); diff --git a/functions/upsert-folder.ts b/functions/upsert-folder.ts index 668f9a30..c2aabb62 100644 --- a/functions/upsert-folder.ts +++ b/functions/upsert-folder.ts @@ -8,10 +8,11 @@ import { envSchema } from "./env/env-schema.ts"; import { DatabaseException } from "./db/database-exception.ts"; import { createJsonResponse } from "./lib/json-response/create-json-response.ts"; import { - getFoldersWithDecksDb, + getManyFoldersWithDecksDb, UserFoldersDbType, -} from "./db/folder/get-folders-with-decks-db.tsx"; +} from "./db/folder/get-many-folders-with-decks-db.tsx"; import { getFolderByIdAndAuthorId } from "./db/folder/get-folder-by-id-and-author-id.ts"; +import { shortUniqueId } from "./lib/short-unique-id/short-unique-id.ts"; const requestSchema = z.object({ id: z.number().optional(), @@ -24,7 +25,7 @@ export type AddFolderRequest = z.infer; export type AddFolderResponse = { folder: { id: number; - } + }; folders: UserFoldersDbType[]; }; @@ -40,9 +41,11 @@ export const onRequestPost = handleError(async ({ request, env }) => { const envSafe = envSchema.parse(env); const { data } = input; + let databaseFolder: { share_id: string } | null = null; + if (data.id) { - const canEdit = await getFolderByIdAndAuthorId(envSafe, data.id, user); - if (!canEdit) { + databaseFolder = await getFolderByIdAndAuthorId(envSafe, data.id, user); + if (!databaseFolder) { return createBadRequestResponse(); } } @@ -56,6 +59,8 @@ export const onRequestPost = handleError(async ({ request, env }) => { title: data.title, description: data.description, author_id: user.id, + share_id: + data.id && databaseFolder ? databaseFolder.share_id : shortUniqueId(), }) .select() .single(); @@ -87,6 +92,6 @@ export const onRequestPost = handleError(async ({ request, env }) => { return createJsonResponse({ folder: upsertFolderResult.data, - folders: await getFoldersWithDecksDb(envSafe, user.id), + folders: await getManyFoldersWithDecksDb(envSafe, user.id), }); }); diff --git a/src/api/api.ts b/src/api/api.ts index f584bbe8..64e8bf19 100644 --- a/src/api/api.ts +++ b/src/api/api.ts @@ -58,8 +58,11 @@ export const addDeckToMineRequest = (body: AddDeckToMineRequest) => { ); }; -export const getDeckAccessesOfDeckRequest = (deckId: number) => { - return request(`/deck-accesses?deck_id=${deckId}`); +export const getDeckAccessesOfDeckRequest = ( + filters: { deckId: string } | { folderId: string }, +) => { + const queryParams = new URLSearchParams(filters).toString(); + return request(`/deck-accesses?${queryParams}`); }; export const addDeckAccessRequest = (body: AddDeckAccessRequest) => { diff --git a/src/screens/app.tsx b/src/screens/app.tsx index c649016a..e64f2471 100644 --- a/src/screens/app.tsx +++ b/src/screens/app.tsx @@ -22,7 +22,7 @@ import { DeckOrFolderChoose } from "./deck-or-folder-choose/deck-or-folder-choos import { FolderForm } from "./folder-form/folder-form.tsx"; import { DeckCatalogStoreContextProvider } from "./deck-catalog/store/deck-catalog-store-context.tsx"; import { ShareDeckScreen } from "./share-deck/share-deck-screen.tsx"; -import { ShareDeckStoreProvider } from "./share-deck/store/share-deck-store-context.tsx"; +import { ShareDeckOrFormStoreProvider } from "./share-deck/store/share-deck-store-context.tsx"; import { FolderFormStoreProvider } from "./folder-form/store/folder-form-store-context.tsx"; import { FolderScreen } from "./folder-review/folder-screen.tsx"; import { useSettingsButton } from "../lib/telegram/use-settings-button.ts"; @@ -88,9 +88,16 @@ export const App = observer(() => { )} {screenStore.screen.type === "shareDeck" && ( - + - + + + )} + {screenStore.screen.type === "shareFolder" && ( + + + + )} {screenStore.screen.type === "cardQuickAddForm" && ( diff --git a/src/screens/deck-form/deck-form.tsx b/src/screens/deck-form/deck-form.tsx index 37a55f51..9f994627 100644 --- a/src/screens/deck-form/deck-form.tsx +++ b/src/screens/deck-form/deck-form.tsx @@ -49,6 +49,7 @@ export const DeckForm = observer(() => { useTelegramProgress(() => deckFormStore.isSending); if (!deckFormStore.form) { + console.log("no deck form"); return null; } diff --git a/src/screens/deck-form/store/deck-form-store.ts b/src/screens/deck-form/store/deck-form-store.ts index 315986ea..f137e79f 100644 --- a/src/screens/deck-form/store/deck-form-store.ts +++ b/src/screens/deck-form/store/deck-form-store.ts @@ -133,10 +133,11 @@ export class DeckFormStore { get isDeckSaveButtonVisible() { return Boolean( - (this.form?.description.isTouched || - this.form?.title.isTouched || - this.form?.speakingCardsField.isTouched || - this.form?.speakingCardsLocale.isTouched) && + this.form && + (this.form.description.isTouched || + this.form.title.isTouched || + this.form.speakingCardsField.isTouched || + this.form.speakingCardsLocale.isTouched) && this.form?.cards.length > 0, ); } @@ -215,7 +216,9 @@ export class DeckFormStore { } toggleIsSpeakingCardEnabled() { - if (!this.form) return; + if (!this.form) { + return; + } const { speakingCardsLocale, speakingCardsField } = this.form; if (speakingCardsLocale.value && speakingCardsField.value) { @@ -341,11 +344,14 @@ export class DeckFormStore { }) .then( action(({ deck, folders, cardsToReview }) => { + const redirectToEdit = !this.form?.id; this.form = createUpdateForm(deck.id, deck); deckListStore.replaceDeck(deck, true); deckListStore.updateFolders(folders); deckListStore.updateCardsToReview(cardsToReview); - screenStore.go({ type: "deckForm", deckId: deck.id }); + if (redirectToEdit) { + screenStore.go({ type: "deckForm", deckId: deck.id }); + } }), ) .finally( diff --git a/src/screens/deck-review/deck-preview.tsx b/src/screens/deck-review/deck-preview.tsx index ad4140e4..4a3ff83b 100644 --- a/src/screens/deck-review/deck-preview.tsx +++ b/src/screens/deck-review/deck-preview.tsx @@ -169,19 +169,6 @@ export const DeckPreview = observer(() => { {t("edit")} ) : null} - {screenStore.screen.type === "deckMine" ? ( - { - showConfirm(t("delete_deck_confirm")).then(() => { - deckListStore.removeDeck(); - }); - }} - > - {t("delete")} - - ) : null} {deckListStore.canEditDeck && ( { {t("share")} )} + + {screenStore.screen.type === "deckMine" ? ( + { + showConfirm(t("delete_deck_confirm")).then(() => { + deckListStore.removeDeck(); + }); + }} + > + {t("delete")} + + ) : null} {deck.cardsToReview.length === 0 && ( diff --git a/src/screens/folder-review/folder-preview.tsx b/src/screens/folder-review/folder-preview.tsx index 8ded29d7..2bccc065 100644 --- a/src/screens/folder-review/folder-preview.tsx +++ b/src/screens/folder-review/folder-preview.tsx @@ -161,7 +161,22 @@ export const FolderPreview = observer(() => { {t("edit")} ) : null} - {deckListStore.canEditFolder ? ( + {deckListStore.canEditFolder && ( + { + screenStore.go({ + type: "shareFolder", + folderId: folder.id, + shareId: folder.shareId, + }); + }} + > + {t("share")} + + )} + {deckListStore.canEditFolder && ( { > {t("delete")} - ) : null} + )}
{ + if (user.username) { + return `@${user.username}`; + } + if (user.first_name || user.last_name) { + return `${user.first_name ?? ""} ${user.last_name ?? ""}`; + } + return `#${user.id}`; +}; diff --git a/src/screens/share-deck/redirect-user-to-deck-link.tsx b/src/screens/share-deck/redirect-user-to-deck-or-folder-link.tsx similarity index 69% rename from src/screens/share-deck/redirect-user-to-deck-link.tsx rename to src/screens/share-deck/redirect-user-to-deck-or-folder-link.tsx index 8c4e09e0..cf703c37 100644 --- a/src/screens/share-deck/redirect-user-to-deck-link.tsx +++ b/src/screens/share-deck/redirect-user-to-deck-or-folder-link.tsx @@ -2,14 +2,14 @@ import { assert } from "../../lib/typescript/assert.ts"; import { trimEnd } from "../../lib/string/trim.ts"; import WebApp from "@twa-dev/sdk"; -export const getDeckLink = (shareId: string) => { +export const getDeckOrFolderLink = (shareId: string) => { const botUrl = import.meta.env.VITE_BOT_APP_URL; assert(botUrl, "Bot URL is not set"); return `${trimEnd(botUrl, "/")}?startapp=${shareId}`; }; -export const redirectUserToDeckLink = (shareId: string) => { - const botUrlWithDeckId = getDeckLink(shareId); +export const redirectUserToDeckOrFolderLink = (shareId: string) => { + const botUrlWithDeckId = getDeckOrFolderLink(shareId); const shareUrl = `https://t.me/share/url?text=&url=${botUrlWithDeckId}`; WebApp.openTelegramLink(shareUrl); }; diff --git a/src/screens/share-deck/share-deck-one-time-links.tsx b/src/screens/share-deck/share-deck-one-time-links.tsx index a25ed6e3..65c827cf 100644 --- a/src/screens/share-deck/share-deck-one-time-links.tsx +++ b/src/screens/share-deck/share-deck-one-time-links.tsx @@ -4,7 +4,7 @@ import { t } from "../../translations/t.ts"; import React from "react"; import { useBackButton } from "../../lib/telegram/use-back-button.tsx"; import { useMount } from "../../lib/react/use-mount.ts"; -import { getDeckLink } from "./redirect-user-to-deck-link.tsx"; +import { getDeckOrFolderLink } from "./redirect-user-to-deck-or-folder-link.tsx"; import { copyToClipboard } from "../../lib/copy-to-clipboard/copy-to-clipboard.ts"; import { showAlert } from "../../lib/telegram/show-alert.ts"; import { theme } from "../../ui/theme.tsx"; @@ -13,21 +13,7 @@ import { useShareDeckStore } from "./store/share-deck-store-context.tsx"; import { Screen } from "../shared/screen.tsx"; import { Loader } from "../../ui/loader.tsx"; import { EmptyState } from "../../ui/empty-state.tsx"; - -const formatAccessUser = (user: { - id: number; - username: string | null; - first_name: string | null; - last_name: string | null; -}) => { - if (user.username) { - return `@${user.username}`; - } - if (user.first_name || user.last_name) { - return `${user.first_name ?? ""} ${user.last_name ?? ""}`; - } - return `#${user.id}`; -}; +import { formatAccessUser } from "./format-access-user.ts"; export const ShareDeckOneTimeLinks = observer(() => { const store = useShareDeckStore(); @@ -45,7 +31,11 @@ export const ShareDeckOneTimeLinks = observer(() => { {store.deckAccesses?.state === "pending" ? : null} {store.deckAccesses?.state === "fulfilled" && store.deckAccesses.value.accesses.length === 0 ? ( - {t("share_no_links")} + + {store.deckAccessType === "deck" + ? t("share_no_links") + : t("share_no_links_for_folder")} + ) : null} {store.deckAccesses?.state === "fulfilled" @@ -69,7 +59,7 @@ export const ShareDeckOneTimeLinks = observer(() => { #{access.id}{" "} { - const link = getDeckLink(access.share_id); + const link = getDeckOrFolderLink(access.share_id); await copyToClipboard(link); showAlert(t("share_link_copied")); }} diff --git a/src/screens/share-deck/share-deck-settings.tsx b/src/screens/share-deck/share-deck-settings.tsx index 5dcdb841..9917ce3d 100644 --- a/src/screens/share-deck/share-deck-settings.tsx +++ b/src/screens/share-deck/share-deck-settings.tsx @@ -27,7 +27,7 @@ export const ShareDeckSettings = observer(() => { : t("share_perpetual_link"); }, () => { - store.shareDeck(); + store.shareDeckOrFolder(); }, () => store.isSaveButtonVisible, ); diff --git a/src/screens/share-deck/store/share-deck-store.ts b/src/screens/share-deck/store/share-deck-form-store.ts similarity index 63% rename from src/screens/share-deck/store/share-deck-store.ts rename to src/screens/share-deck/store/share-deck-form-store.ts index adfba20c..1bd43f63 100644 --- a/src/screens/share-deck/store/share-deck-store.ts +++ b/src/screens/share-deck/store/share-deck-form-store.ts @@ -5,7 +5,7 @@ import { t } from "../../../translations/t.ts"; import { action, makeAutoObservable } from "mobx"; import { isFormValid } from "../../../lib/mobx-form/form-has-error.ts"; import { screenStore } from "../../../store/screen-store.ts"; -import { redirectUserToDeckLink } from "../redirect-user-to-deck-link.tsx"; +import { redirectUserToDeckOrFolderLink } from "../redirect-user-to-deck-or-folder-link.tsx"; import { addDeckAccessRequest, getDeckAccessesOfDeckRequest, @@ -13,8 +13,21 @@ import { import { persistableField } from "../../../lib/mobx-form/persistable-field.ts"; import { fromPromise, IPromiseBasedObservable } from "mobx-utils"; import { DeckAccessesResponse } from "../../../../functions/deck-accesses.ts"; +import { DeckAccessType } from "../../../../functions/db/custom-types.ts"; -export class ShareDeckStore { +const getRequestFiltersForScreen = () => { + const screen = screenStore.screen; + switch (screen.type) { + case "shareDeck": + return { deckId: screen.deckId.toString() }; + case "shareFolder": + return { folderId: screen.folderId.toString() }; + default: + assert(false, `Invalid screen type: ${screen.type}`); + } +}; + +export class ShareDeckFormStore { isSending = false; deckAccesses?: IPromiseBasedObservable; isDeckAccessesOpen = new BooleanToggle(false); @@ -40,42 +53,45 @@ export class ShareDeckStore { ), }; - constructor() { + constructor(public deckAccessType: DeckAccessType) { makeAutoObservable(this, {}, { autoBind: true }); } load() { - const screen = screenStore.screen; - assert(screen.type === "shareDeck", "Screen is not shareDeck"); - const { deckId } = screen; - - this.deckAccesses = fromPromise(getDeckAccessesOfDeckRequest(deckId)); + this.deckAccesses = fromPromise( + getDeckAccessesOfDeckRequest(getRequestFiltersForScreen()), + ); } get isSaveButtonVisible() { return Boolean(this.form && isFormValid(this.form)); } - async shareDeck() { + async shareDeckOrFolder() { const screen = screenStore.screen; - assert(screen.type === "shareDeck", "Screen is not shareDeck"); - const { deckId, shareId } = screen; + assert( + screen.type === "shareDeck" || screen.type === "shareFolder", + "Screen type is not shareDeck or shareFolder", + ); + const { shareId } = screen; if (!this.form.isOneTime.value) { - redirectUserToDeckLink(shareId); + redirectUserToDeckOrFolderLink(shareId); return; } this.isSending = true; addDeckAccessRequest({ - deckId, + deckId: screen.type === "shareDeck" ? screen.deckId : null, + folderId: screen.type === "shareFolder" ? screen.folderId : null, + type: this.deckAccessType, durationDays: this.form.isAccessDuration.value ? Number(this.form.accessDurationLimitDays.value) : null, }) .then((result) => { - redirectUserToDeckLink(result.share_id); + redirectUserToDeckOrFolderLink(result.share_id); }) .finally( action(() => { diff --git a/src/screens/share-deck/store/share-deck-store-context.tsx b/src/screens/share-deck/store/share-deck-store-context.tsx index 9549366a..ae7871de 100644 --- a/src/screens/share-deck/store/share-deck-store-context.tsx +++ b/src/screens/share-deck/store/share-deck-store-context.tsx @@ -1,13 +1,20 @@ import { createContext, ReactNode, useContext } from "react"; -import { ShareDeckStore } from "./share-deck-store.ts"; +import { ShareDeckFormStore } from "./share-deck-form-store.ts"; import { assert } from "../../../lib/typescript/assert.ts"; +import { DeckAccessType } from "../../../../functions/db/custom-types.ts"; -const Context = createContext(null); +const Context = createContext(null); -export const ShareDeckStoreProvider = (props: { children: ReactNode }) => { +type Props = { + children: ReactNode; + type: DeckAccessType; +}; + +export const ShareDeckOrFormStoreProvider = (props: Props) => { + const { children, type } = props; return ( - - {props.children} + + {children} ); }; diff --git a/src/screens/user-settings/user-settings-main.tsx b/src/screens/user-settings/user-settings-main.tsx index f7c992cb..0f9c4418 100644 --- a/src/screens/user-settings/user-settings-main.tsx +++ b/src/screens/user-settings/user-settings-main.tsx @@ -34,7 +34,7 @@ export const UserSettingsMain = observer(() => { ); useBackButton(() => { - screenStore.go({ type: "main" }); + screenStore.back(); }); useTelegramProgress(() => userSettingsStore.isSending); diff --git a/src/store/deck-list-store.ts b/src/store/deck-list-store.ts index bc3b2792..e66c7386 100644 --- a/src/store/deck-list-store.ts +++ b/src/store/deck-list-store.ts @@ -18,7 +18,7 @@ import { assert } from "../lib/typescript/assert.ts"; import { ReviewStore } from "../screens/deck-review/store/review-store.ts"; import { reportHandledError } from "../lib/rollbar/rollbar.tsx"; import { BooleanToggle } from "../lib/mobx-form/boolean-toggle.ts"; -import { UserFoldersDbType } from "../../functions/db/folder/get-folders-with-decks-db.tsx"; +import { UserFoldersDbType } from "../../functions/db/folder/get-many-folders-with-decks-db.tsx"; import { userStore } from "./user-store.ts"; export enum StartParamType { @@ -46,6 +46,7 @@ export type DeckListItem = { type: "folder"; decks: DeckWithCardsWithReviewType[]; authorId: number; + shareId: string; } ); @@ -126,26 +127,55 @@ export class DeckListStore { getSharedDeckRequest(startParam) .then( - action(({ deck }) => { - assert(this.myInfo); - if (this.myInfo.myDecks.find((myDeck) => myDeck.id === deck.id)) { - screenStore.go({ type: "deckMine", deckId: deck.id }); - return; + action((sharedDeckResponse) => { + if ("deck" in sharedDeckResponse) { + const deck = sharedDeckResponse.deck; + assert(this.myInfo); + if (this.myInfo.myDecks.find((myDeck) => myDeck.id === deck.id)) { + screenStore.go({ type: "deckMine", deckId: deck.id }); + return; + } + + if ( + this.publicDecks.find((publicDeck) => publicDeck.id === deck.id) + ) { + this.replaceDeck(deck); + screenStore.go({ + type: "deckPublic", + deckId: deck.id, + }); + return; + } + + this.myInfo.publicDecks.push(deck); + screenStore.go({ type: "deckPublic", deckId: deck.id }); } - if ( - this.publicDecks.find((publicDeck) => publicDeck.id === deck.id) - ) { - this.replaceDeck(deck); - screenStore.go({ - type: "deckPublic", - deckId: deck.id, - }); - return; + if ("folder" in sharedDeckResponse) { + const folder = sharedDeckResponse.folder; + assert(this.myInfo); + if ( + this.myInfo.folders.find( + (myFolder) => myFolder.folder_id === folder.id, + ) + ) { + screenStore.go({ type: "folderPreview", folderId: folder.id }); + return; + } + + for (const deck of folder.decks) { + this.myInfo.folders.push({ + deck_id: deck.id, + folder_id: folder.id, + folder_author_id: folder.author_id, + folder_description: folder.description, + folder_share_id: folder.share_id, + folder_title: folder.title, + }); + this.myInfo.myDecks.push(deck); + } + screenStore.go({ type: "folderPreview", folderId: folder.id }); } - - this.myInfo.publicDecks.push(deck); - screenStore.go({ type: "deckPublic", deckId: deck.id }); }), ) .catch((e) => { @@ -384,6 +414,7 @@ export class DeckListStore { folderName: string; folderDescription: string | null; folderAuthorId: number; + folderShareId: string; decks: DeckWithCardsWithReviewType[]; } >(); @@ -393,6 +424,7 @@ export class DeckListStore { folderName: folder.folder_title, folderDescription: folder.folder_description, folderAuthorId: folder.folder_author_id, + folderShareId: folder.folder_share_id, decks: [], }; const deck = myDecks.find((deck) => deck.id === folder.deck_id); @@ -411,6 +443,7 @@ export class DeckListStore { ), type: "folder", name: mapItem.folderName, + shareId: mapItem.folderShareId, description: mapItem.folderDescription, authorId: mapItem.folderAuthorId, })); diff --git a/src/store/screen-store.ts b/src/store/screen-store.ts index 0eea959d..8207d89e 100644 --- a/src/store/screen-store.ts +++ b/src/store/screen-store.ts @@ -14,6 +14,7 @@ type Route = | { type: "cardQuickAddForm"; deckId: number } | { type: "deckCatalog" } | { type: "shareDeck"; deckId: number; shareId: string } + | { type: "shareFolder"; folderId: number; shareId: string } | { type: "userSettings" }; export type RouteType = Route["type"]; diff --git a/src/translations/t.ts b/src/translations/t.ts index 78fe541c..f6b251be 100644 --- a/src/translations/t.ts +++ b/src/translations/t.ts @@ -106,6 +106,7 @@ const en = { share_perpetual_link: "Share perpetual link", share_one_time_link: "Share one-time link", share_deck_settings: "Share deck", + share_folder_settings: "Share folder", share_one_time_access_link: "One-time access link", share_one_time_access_link_description: "The link is only available for one user. After the first use, the link will be invalid", @@ -122,6 +123,8 @@ const en = { share_access_duration_no_limit: "No limit", share_deck_access_created_at: "Created at", share_no_links: "You haven't created any one-time links for this deck", + share_no_links_for_folder: + "You haven't created any one-time links for this folder", go_back: "Go back", delete_folder_confirm: "Do you want to delete the folder? Deleting folder won't remove decks inside the folder", @@ -130,6 +133,9 @@ const en = { type Translation = typeof en; const ru: Translation = { + share_folder_settings: "Настройки шеринга папки", + share_no_links_for_folder: + "Вы еще не создали одноразовых ссылок для этой папки", choose_what_to_create: "Выберите что создать", card_preview: "Предпросмотр карточки", deck: "Колода", @@ -256,6 +262,9 @@ const ru: Translation = { const es: Translation = { card_preview: "Vista previa de la tarjeta", + share_no_links_for_folder: + "No has creado ningún enlace de un solo uso para esta carpeta", + share_folder_settings: "Compartir carpeta", review_folder: "Repasar carpeta", folder_description: "Una colección de mazos", add_deck_short: "Mazo", @@ -384,6 +393,8 @@ const es: Translation = { }; const ptBr: Translation = { + share_folder_settings: "Compartilhar pasta", + share_no_links_for_folder: "Você ainda não criou nenhum link para esta pasta", card_preview: "Visualização do cartão", review_folder: "Revisar pasta", add_deck_short: "Baralho",