Skip to content

Commit

Permalink
Folder sharing (#39)
Browse files Browse the repository at this point in the history
* Folder sharing
  • Loading branch information
kubk authored Jan 10, 2024
1 parent 4dd1935 commit 1c64ded
Show file tree
Hide file tree
Showing 16 changed files with 191 additions and 86 deletions.
7 changes: 5 additions & 2 deletions src/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,11 @@ export const addDeckToMineRequest = (body: AddDeckToMineRequest) => {
);
};

export const getDeckAccessesOfDeckRequest = (deckId: number) => {
return request<DeckAccessesResponse>(`/deck-accesses?deck_id=${deckId}`);
export const getDeckAccessesOfDeckRequest = (
filters: { deckId: string } | { folderId: string },
) => {
const queryParams = new URLSearchParams(filters).toString();
return request<DeckAccessesResponse>(`/deck-accesses?${queryParams}`);
};

export const addDeckAccessRequest = (body: AddDeckAccessRequest) => {
Expand Down
13 changes: 10 additions & 3 deletions src/screens/app.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -88,9 +88,16 @@ export const App = observer(() => {
)}
{screenStore.screen.type === "shareDeck" && (
<PreventTelegramSwipeDownClosingIos>
<ShareDeckStoreProvider>
<ShareDeckOrFormStoreProvider type={"deck"}>
<ShareDeckScreen />
</ShareDeckStoreProvider>
</ShareDeckOrFormStoreProvider>
</PreventTelegramSwipeDownClosingIos>
)}
{screenStore.screen.type === "shareFolder" && (
<PreventTelegramSwipeDownClosingIos>
<ShareDeckOrFormStoreProvider type={"folder"}>
<ShareDeckScreen />
</ShareDeckOrFormStoreProvider>
</PreventTelegramSwipeDownClosingIos>
)}
{screenStore.screen.type === "cardQuickAddForm" && (
Expand Down
1 change: 1 addition & 0 deletions src/screens/deck-form/deck-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ export const DeckForm = observer(() => {
useTelegramProgress(() => deckFormStore.isSending);

if (!deckFormStore.form) {
console.log("no deck form");
return null;
}

Expand Down
18 changes: 12 additions & 6 deletions src/screens/deck-form/store/deck-form-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down
27 changes: 14 additions & 13 deletions src/screens/deck-review/deck-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -169,19 +169,6 @@ export const DeckPreview = observer(() => {
{t("edit")}
</ButtonSideAligned>
) : null}
{screenStore.screen.type === "deckMine" ? (
<ButtonSideAligned
icon={"mdi-delete-circle mdi-24px"}
outline
onClick={() => {
showConfirm(t("delete_deck_confirm")).then(() => {
deckListStore.removeDeck();
});
}}
>
{t("delete")}
</ButtonSideAligned>
) : null}

{deckListStore.canEditDeck && (
<ButtonSideAligned
Expand All @@ -198,6 +185,20 @@ export const DeckPreview = observer(() => {
{t("share")}
</ButtonSideAligned>
)}

{screenStore.screen.type === "deckMine" ? (
<ButtonSideAligned
icon={"mdi-delete-circle mdi-24px"}
outline
onClick={() => {
showConfirm(t("delete_deck_confirm")).then(() => {
deckListStore.removeDeck();
});
}}
>
{t("delete")}
</ButtonSideAligned>
) : null}
</div>
</div>
{deck.cardsToReview.length === 0 && (
Expand Down
19 changes: 17 additions & 2 deletions src/screens/folder-review/folder-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,22 @@ export const FolderPreview = observer(() => {
{t("edit")}
</ButtonSideAligned>
) : null}
{deckListStore.canEditFolder ? (
{deckListStore.canEditFolder && (
<ButtonSideAligned
icon={"mdi-share-circle mdi-24px"}
outline
onClick={() => {
screenStore.go({
type: "shareFolder",
folderId: folder.id,
shareId: folder.shareId,
});
}}
>
{t("share")}
</ButtonSideAligned>
)}
{deckListStore.canEditFolder && (
<ButtonSideAligned
icon={"mdi-delete-circle mdi-24px"}
outline
Expand All @@ -173,7 +188,7 @@ export const FolderPreview = observer(() => {
>
{t("delete")}
</ButtonSideAligned>
) : null}
)}
</div>
</div>
<div
Expand Down
14 changes: 14 additions & 0 deletions src/screens/share-deck/format-access-user.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export 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}`;
};
Original file line number Diff line number Diff line change
Expand Up @@ -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);
};
26 changes: 8 additions & 18 deletions src/screens/share-deck/share-deck-one-time-links.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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();
Expand All @@ -45,7 +31,11 @@ export const ShareDeckOneTimeLinks = observer(() => {
{store.deckAccesses?.state === "pending" ? <Loader /> : null}
{store.deckAccesses?.state === "fulfilled" &&
store.deckAccesses.value.accesses.length === 0 ? (
<EmptyState>{t("share_no_links")}</EmptyState>
<EmptyState>
{store.deckAccessType === "deck"
? t("share_no_links")
: t("share_no_links_for_folder")}
</EmptyState>
) : null}

{store.deckAccesses?.state === "fulfilled"
Expand All @@ -69,7 +59,7 @@ export const ShareDeckOneTimeLinks = observer(() => {
#{access.id}{" "}
<span
onClick={async () => {
const link = getDeckLink(access.share_id);
const link = getDeckOrFolderLink(access.share_id);
await copyToClipboard(link);
showAlert(t("share_link_copied"));
}}
Expand Down
2 changes: 1 addition & 1 deletion src/screens/share-deck/share-deck-settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export const ShareDeckSettings = observer(() => {
: t("share_perpetual_link");
},
() => {
store.shareDeck();
store.shareDeckOrFolder();
},
() => store.isSaveButtonVisible,
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,29 @@ 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,
} from "../../../api/api.ts";
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<DeckAccessesResponse>;
isDeckAccessesOpen = new BooleanToggle(false);
Expand All @@ -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(() => {
Expand Down
17 changes: 12 additions & 5 deletions src/screens/share-deck/store/share-deck-store-context.tsx
Original file line number Diff line number Diff line change
@@ -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<ShareDeckStore | null>(null);
const Context = createContext<ShareDeckFormStore | null>(null);

export const ShareDeckStoreProvider = (props: { children: ReactNode }) => {
type Props = {
children: ReactNode;
type: DeckAccessType;
};

export const ShareDeckOrFormStoreProvider = (props: Props) => {
const { children, type } = props;
return (
<Context.Provider value={new ShareDeckStore()}>
{props.children}
<Context.Provider value={new ShareDeckFormStore(type)}>
{children}
</Context.Provider>
);
};
Expand Down
2 changes: 1 addition & 1 deletion src/screens/user-settings/user-settings-main.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const UserSettingsMain = observer(() => {
);

useBackButton(() => {
screenStore.go({ type: "main" });
screenStore.back();
});
useTelegramProgress(() => userSettingsStore.isSending);

Expand Down
Loading

0 comments on commit 1c64ded

Please sign in to comment.