diff --git a/package.json b/package.json
index 8d0a7d17..573c338c 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"@babel/preset-typescript": "^7.21.5",
"@babel/runtime": "^7.21.5",
"@emoji-mart/data": "^1.1.0",
+ "@shortcm/qr-image": "^9.0.2",
"autosize": "^6.0.1",
"babel-loader": "^9.1.2",
"babel-plugin-inferno": "^6.6.0",
@@ -69,7 +70,7 @@
"inferno-router": "^8.2.2",
"inferno-server": "^8.2.2",
"jwt-decode": "^3.1.2",
- "lemmy-js-client": "^0.19.0-rc.12",
+ "lemmy-js-client": "^0.19.0-rc.13",
"lodash.isequal": "^4.5.0",
"markdown-it": "^13.0.1",
"markdown-it-container": "^3.0.0",
diff --git a/src/assets/css/main.css b/src/assets/css/main.css
index f1e0c4d6..bad10ec5 100644
--- a/src/assets/css/main.css
+++ b/src/assets/css/main.css
@@ -448,3 +448,7 @@ br.big {
.skip-link:focus {
top: 0;
}
+
+.totp-link {
+ width: fit-content;
+}
diff --git a/src/shared/components/comment/comment-report.tsx b/src/shared/components/comment/comment-report.tsx
index ff740b59..d5d0bba7 100644
--- a/src/shared/components/comment/comment-report.tsx
+++ b/src/shared/components/comment/comment-report.tsx
@@ -10,6 +10,7 @@ import { I18NextService } from "../../services";
import { Icon, Spinner } from "../common/icon";
import { PersonListing } from "../person/person-listing";
import { CommentNode } from "./comment-node";
+import { EMPTY_REQUEST } from "../../services/HttpService";
interface CommentReportProps {
report: CommentReportView;
@@ -97,8 +98,8 @@ export class CommentReport extends Component<
onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
- onCreateComment={() => Promise.resolve({ state: "empty" })}
- onEditComment={() => Promise.resolve({ state: "empty" })}
+ onCreateComment={() => Promise.resolve(EMPTY_REQUEST)}
+ onEditComment={() => Promise.resolve(EMPTY_REQUEST)}
/>
{I18NextService.i18n.t("reporter")}:{" "}
diff --git a/src/shared/components/common/totp-modal.tsx b/src/shared/components/common/totp-modal.tsx
new file mode 100644
index 00000000..0d8dc870
--- /dev/null
+++ b/src/shared/components/common/totp-modal.tsx
@@ -0,0 +1,268 @@
+import {
+ Component,
+ MouseEventHandler,
+ RefObject,
+ createRef,
+ linkEvent,
+} from "inferno";
+import { I18NextService } from "../../services";
+import { toast } from "../../toast";
+import type { Modal } from "bootstrap";
+
+interface TotpModalProps {
+ /**Takes totp as param, returns whether submit was successful*/
+ onSubmit: (totp: string) => Promise
;
+ onClose: MouseEventHandler;
+ type: "login" | "remove" | "generate";
+ secretUrl?: string;
+ show?: boolean;
+}
+
+interface TotpModalState {
+ totp: string;
+ qrCode?: string;
+}
+
+const TOTP_LENGTH = 6;
+
+async function handleSubmit(modal: TotpModal, totp: string) {
+ const successful = await modal.props.onSubmit(totp);
+
+ if (!successful) {
+ modal.setState({ totp: "" });
+ modal.inputRefs[0]?.focus();
+ }
+}
+
+function handleInput(
+ { modal, i }: { modal: TotpModal; i: number },
+ event: any,
+) {
+ if (isNaN(event.target.value)) {
+ event.preventDefault();
+ return;
+ }
+
+ modal.setState(prev => ({ ...prev, totp: prev.totp + event.target.value }));
+ modal.inputRefs[i + 1]?.focus();
+
+ const { totp } = modal.state;
+ if (totp.length >= TOTP_LENGTH) {
+ handleSubmit(modal, totp);
+ }
+}
+
+function handleKeyUp(
+ { modal, i }: { modal: TotpModal; i: number },
+ event: any,
+) {
+ if (event.key === "Backspace" && i > 0) {
+ event.preventDefault();
+
+ modal.setState(prev => ({
+ ...prev,
+ totp: prev.totp.slice(0, prev.totp.length - 1),
+ }));
+ modal.inputRefs[i - 1]?.focus();
+ }
+}
+
+function handlePaste(modal: TotpModal, event: any) {
+ event.preventDefault();
+ const text: string = event.clipboardData.getData("text");
+
+ if (text.length > TOTP_LENGTH || isNaN(Number(text))) {
+ toast(I18NextService.i18n.t("invalid_totp_code"), "danger");
+ modal.setState({ totp: "" });
+ } else {
+ modal.setState({ totp: text });
+ handleSubmit(modal, text);
+ }
+}
+
+export default class TotpModal extends Component<
+ TotpModalProps,
+ TotpModalState
+> {
+ private readonly modalDivRef: RefObject;
+ inputRefs: (HTMLInputElement | null)[] = [];
+ modal: Modal;
+ state: TotpModalState = {
+ totp: "",
+ };
+
+ constructor(props: TotpModalProps, context: any) {
+ super(props, context);
+
+ this.modalDivRef = createRef();
+
+ this.clearTotp = this.clearTotp.bind(this);
+ this.handleShow = this.handleShow.bind(this);
+ }
+
+ async componentDidMount() {
+ this.modalDivRef.current?.addEventListener(
+ "shown.bs.modal",
+ this.handleShow,
+ );
+
+ this.modalDivRef.current?.addEventListener(
+ "hidden.bs.modal",
+ this.clearTotp,
+ );
+
+ const Modal = (await import("bootstrap/js/dist/modal")).default;
+ this.modal = new Modal(this.modalDivRef.current!);
+
+ if (this.props.show) {
+ this.modal.show();
+ }
+ }
+
+ componentWillUnmount() {
+ this.modalDivRef.current?.removeEventListener(
+ "shown.bs.modal",
+ this.handleShow,
+ );
+
+ this.modalDivRef.current?.removeEventListener(
+ "hidden.bs.modal",
+ this.clearTotp,
+ );
+
+ this.modal.dispose();
+ }
+
+ componentDidUpdate({ show: prevShow }: TotpModalProps) {
+ if (!!prevShow !== !!this.props.show) {
+ if (this.props.show) {
+ this.modal.show();
+ } else {
+ this.modal.hide();
+ }
+ }
+ }
+
+ render() {
+ const { type, secretUrl, onClose } = this.props;
+ const { totp } = this.state;
+
+ return (
+
+
+
+
+
+ {I18NextService.i18n.t(
+ type === "generate"
+ ? "enable_totp"
+ : type === "remove"
+ ? "disable_totp"
+ : "enter_totp_code",
+ )}
+
+
+
+
+ {type === "generate" && (
+
+ )}
+
+
+
+
+
+
+ );
+ }
+
+ clearTotp() {
+ this.setState({ totp: "" });
+ }
+
+ async handleShow() {
+ this.inputRefs[0]?.focus();
+
+ if (this.props.type === "generate") {
+ const { getSVG } = await import("@shortcm/qr-image/lib/svg");
+
+ this.setState({
+ qrCode: URL.createObjectURL(
+ new Blob([(await getSVG(this.props.secretUrl!)).buffer], {
+ type: "image/svg+xml",
+ }),
+ ),
+ });
+ }
+ }
+}
diff --git a/src/shared/components/community/communities.tsx b/src/shared/components/community/communities.tsx
index b36e1c8a..ca015e2d 100644
--- a/src/shared/components/community/communities.tsx
+++ b/src/shared/components/community/communities.tsx
@@ -18,7 +18,12 @@ import {
} from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import { ListingTypeSelect } from "../common/listing-type-select";
@@ -64,7 +69,7 @@ function getCommunitiesQueryParams() {
export class Communities extends Component {
private isoData = setIsoData(this.context);
state: CommunitiesState = {
- listCommunitiesResponse: { state: "empty" },
+ listCommunitiesResponse: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
searchText: "",
isIsomorphic: false,
@@ -333,7 +338,7 @@ export class Communities extends Component {
}
async refetch() {
- this.setState({ listCommunitiesResponse: { state: "loading" } });
+ this.setState({ listCommunitiesResponse: LOADING_REQUEST });
const { listingType, sort, page } = getCommunitiesQueryParams();
diff --git a/src/shared/components/community/community.tsx b/src/shared/components/community/community.tsx
index 6b70b550..59d32e8d 100644
--- a/src/shared/components/community/community.tsx
+++ b/src/shared/components/community/community.tsx
@@ -83,7 +83,12 @@ import {
InitialFetchRequest,
} from "../../interfaces";
import { FirstLoadService, I18NextService, UserService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { setupTippy } from "../../tippy";
import { toast } from "../../toast";
import { CommentNodes } from "../comment/comment-nodes";
@@ -146,9 +151,9 @@ export class Community extends Component<
> {
private isoData = setIsoData(this.context);
state: State = {
- communityRes: { state: "empty" },
- postsRes: { state: "empty" },
- commentsRes: { state: "empty" },
+ communityRes: EMPTY_REQUEST,
+ postsRes: EMPTY_REQUEST,
+ commentsRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
showSidebarMobile: false,
finished: new Map(),
@@ -212,7 +217,7 @@ export class Community extends Component<
}
async fetchCommunity() {
- this.setState({ communityRes: { state: "loading" } });
+ this.setState({ communityRes: LOADING_REQUEST });
this.setState({
communityRes: await HttpService.client.getCommunity({
name: this.props.match.params.name,
@@ -248,10 +253,8 @@ export class Community extends Component<
const page = getPageFromString(urlPage);
- let postsResponse: RequestState = { state: "empty" };
- let commentsResponse: RequestState = {
- state: "empty",
- };
+ let postsResponse: RequestState = EMPTY_REQUEST;
+ let commentsResponse: RequestState = EMPTY_REQUEST;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
@@ -585,7 +588,7 @@ export class Community extends Component<
const { name } = this.props.match.params;
if (dataType === DataType.Post) {
- this.setState({ postsRes: { state: "loading" } });
+ this.setState({ postsRes: LOADING_REQUEST });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
@@ -597,7 +600,7 @@ export class Community extends Component<
}),
});
} else {
- this.setState({ commentsRes: { state: "loading" } });
+ this.setState({ commentsRes: LOADING_REQUEST });
this.setState({
commentsRes: await HttpService.client.getComments({
page,
diff --git a/src/shared/components/home/admin-settings.tsx b/src/shared/components/home/admin-settings.tsx
index c86fd01f..a799b455 100644
--- a/src/shared/components/home/admin-settings.tsx
+++ b/src/shared/components/home/admin-settings.tsx
@@ -16,7 +16,12 @@ import {
import { InitialFetchRequest } from "../../interfaces";
import { removeFromEmojiDataModel, updateEmojiDataModel } from "../../markdown";
import { FirstLoadService, I18NextService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@@ -50,9 +55,9 @@ export class AdminSettings extends Component {
siteRes: this.isoData.site_res,
banned: [],
currentTab: "site",
- bannedRes: { state: "empty" },
- instancesRes: { state: "empty" },
- leaveAdminTeamRes: { state: "empty" },
+ bannedRes: EMPTY_REQUEST,
+ instancesRes: EMPTY_REQUEST,
+ leaveAdminTeamRes: EMPTY_REQUEST,
loading: false,
themeList: [],
isIsomorphic: false,
@@ -231,8 +236,8 @@ export class AdminSettings extends Component {
async fetchData() {
this.setState({
- bannedRes: { state: "loading" },
- instancesRes: { state: "loading" },
+ bannedRes: LOADING_REQUEST,
+ instancesRes: LOADING_REQUEST,
themeList: [],
});
@@ -333,7 +338,7 @@ export class AdminSettings extends Component {
}
async handleLeaveAdminTeam(i: AdminSettings) {
- i.setState({ leaveAdminTeamRes: { state: "loading" } });
+ i.setState({ leaveAdminTeamRes: LOADING_REQUEST });
this.setState({
leaveAdminTeamRes: await HttpService.client.leaveAdmin(),
});
diff --git a/src/shared/components/home/home.tsx b/src/shared/components/home/home.tsx
index 97dd89ca..fc141d99 100644
--- a/src/shared/components/home/home.tsx
+++ b/src/shared/components/home/home.tsx
@@ -85,7 +85,12 @@ import {
I18NextService,
UserService,
} from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { setupTippy } from "../../tippy";
import { toast } from "../../toast";
import { CommentNodes } from "../comment/comment-nodes";
@@ -221,9 +226,9 @@ const LinkButton = ({
export class Home extends Component {
private isoData = setIsoData(this.context);
state: HomeState = {
- postsRes: { state: "empty" },
- commentsRes: { state: "empty" },
- trendingCommunitiesRes: { state: "empty" },
+ postsRes: EMPTY_REQUEST,
+ commentsRes: EMPTY_REQUEST,
+ trendingCommunitiesRes: EMPTY_REQUEST,
scrolled: true,
siteRes: this.isoData.site_res,
showSubscribedMobile: false,
@@ -321,10 +326,8 @@ export class Home extends Component {
const page = urlPage ? Number(urlPage) : 1;
- let postsRes: RequestState = { state: "empty" };
- let commentsRes: RequestState = {
- state: "empty",
- };
+ let postsRes: RequestState = EMPTY_REQUEST;
+ let commentsRes: RequestState = EMPTY_REQUEST;
if (dataType === DataType.Post) {
const getPostsForm: GetPosts = {
@@ -790,7 +793,7 @@ export class Home extends Component {
}
async fetchTrendingCommunities() {
- this.setState({ trendingCommunitiesRes: { state: "loading" } });
+ this.setState({ trendingCommunitiesRes: LOADING_REQUEST });
this.setState({
trendingCommunitiesRes: await HttpService.client.listCommunities({
type_: "Local",
@@ -814,7 +817,7 @@ export class Home extends Component {
behavior: "instant",
});
} else {
- this.setState({ postsRes: { state: "loading" } });
+ this.setState({ postsRes: LOADING_REQUEST });
this.setState({
postsRes: await HttpService.client.getPosts({
page,
@@ -828,7 +831,7 @@ export class Home extends Component {
HomeCacheService.postsRes = this.state.postsRes;
}
} else {
- this.setState({ commentsRes: { state: "loading" } });
+ this.setState({ commentsRes: LOADING_REQUEST });
this.setState({
commentsRes: await HttpService.client.getComments({
page,
diff --git a/src/shared/components/home/instances.tsx b/src/shared/components/home/instances.tsx
index 4edc6056..40ab420a 100644
--- a/src/shared/components/home/instances.tsx
+++ b/src/shared/components/home/instances.tsx
@@ -10,7 +10,12 @@ import classNames from "classnames";
import { relTags } from "../../config";
import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import Tabs from "../common/tabs";
@@ -28,7 +33,7 @@ interface InstancesState {
export class Instances extends Component {
private isoData = setIsoData(this.context);
state: InstancesState = {
- instancesRes: { state: "empty" },
+ instancesRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
isIsomorphic: false,
};
@@ -54,7 +59,7 @@ export class Instances extends Component {
async fetchInstances() {
this.setState({
- instancesRes: { state: "loading" },
+ instancesRes: LOADING_REQUEST,
});
this.setState({
diff --git a/src/shared/components/home/login.tsx b/src/shared/components/home/login.tsx
index 3fa10a4e..c1b22b1a 100644
--- a/src/shared/components/home/login.tsx
+++ b/src/shared/components/home/login.tsx
@@ -5,11 +5,17 @@ import { Component, linkEvent } from "inferno";
import { RouteComponentProps } from "inferno-router/dist/Route";
import { GetSiteResponse, LoginResponse } from "lemmy-js-client";
import { I18NextService, UserService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
import PasswordInput from "../common/password-input";
+import TotpModal from "../common/totp-modal";
interface LoginProps {
prev?: string;
@@ -25,57 +31,57 @@ const getLoginQueryParams = () =>
interface State {
loginRes: RequestState;
form: {
- username_or_email?: string;
- password?: string;
- totp_2fa_token?: string;
+ username_or_email: string;
+ password: string;
};
- showTotp: boolean;
siteRes: GetSiteResponse;
+ show2faModal: boolean;
+}
+
+async function handleLoginSuccess(i: Login, loginRes: LoginResponse) {
+ UserService.Instance.login({
+ res: loginRes,
+ });
+ const site = await HttpService.client.getSite();
+
+ if (site.state === "success") {
+ UserService.Instance.myUserInfo = site.data.my_user;
+ }
+
+ const { prev } = getLoginQueryParams();
+
+ prev
+ ? i.props.history.replace(prev)
+ : i.props.history.action === "PUSH"
+ ? i.props.history.back()
+ : i.props.history.replace("/");
}
async function handleLoginSubmit(i: Login, event: any) {
event.preventDefault();
- const { password, totp_2fa_token, username_or_email } = i.state.form;
+ const { password, username_or_email } = i.state.form;
if (username_or_email && password) {
- i.setState({ loginRes: { state: "loading" } });
+ i.setState({ loginRes: LOADING_REQUEST });
const loginRes = await HttpService.client.login({
username_or_email,
password,
- totp_2fa_token,
});
switch (loginRes.state) {
case "failed": {
if (loginRes.msg === "missing_totp_token") {
- i.setState({ showTotp: true });
- toast(I18NextService.i18n.t("enter_two_factor_code"), "info");
+ i.setState({ show2faModal: true });
} else {
toast(I18NextService.i18n.t(loginRes.msg), "danger");
}
- i.setState({ loginRes: { state: "failed", msg: loginRes.msg } });
+ i.setState({ loginRes });
break;
}
case "success": {
- UserService.Instance.login({
- res: loginRes.data,
- });
- const site = await HttpService.client.getSite();
-
- if (site.state === "success") {
- UserService.Instance.myUserInfo = site.data.my_user;
- }
-
- const { prev } = getLoginQueryParams();
-
- prev
- ? i.props.history.replace(prev)
- : i.props.history.action === "PUSH"
- ? i.props.history.back()
- : i.props.history.replace("/");
-
+ handleLoginSuccess(i, loginRes.data);
break;
}
}
@@ -88,14 +94,14 @@ function handleLoginUsernameChange(i: Login, event: any) {
);
}
-function handleLoginTotpChange(i: Login, event: any) {
- i.setState(prevState => (prevState.form.totp_2fa_token = event.target.value));
-}
-
function handleLoginPasswordChange(i: Login, event: any) {
i.setState(prevState => (prevState.form.password = event.target.value));
}
+function handleClose2faModal(i: Login) {
+ i.setState({ show2faModal: false });
+}
+
export class Login extends Component<
RouteComponentProps>,
State
@@ -103,14 +109,19 @@ export class Login extends Component<
private isoData = setIsoData(this.context);
state: State = {
- loginRes: { state: "empty" },
- form: {},
- showTotp: false,
+ loginRes: EMPTY_REQUEST,
+ form: {
+ username_or_email: "",
+ password: "",
+ },
siteRes: this.isoData.site_res,
+ show2faModal: false,
};
constructor(props: any, context: any) {
super(props, context);
+
+ this.handleSubmitTotp = this.handleSubmitTotp.bind(this);
}
componentDidMount() {
@@ -137,6 +148,12 @@ export class Login extends Component<
title={this.documentTitle}
path={this.context.router.route.match.url}
/>
+
@@ -144,6 +161,24 @@ export class Login extends Component<
);
}
+ async handleSubmitTotp(totp: string) {
+ const loginRes = await HttpService.client.login({
+ password: this.state.form.password,
+ username_or_email: this.state.form.username_or_email,
+ totp_2fa_token: totp,
+ });
+
+ const successful = loginRes.state === "success";
+ if (successful) {
+ this.setState({ show2faModal: false });
+ handleLoginSuccess(this, loginRes.data);
+ } else {
+ toast(I18NextService.i18n.t("incorrect_totp_code"), "danger");
+ }
+
+ return successful;
+ }
+
loginForm() {
return (
@@ -178,28 +213,6 @@ export class Login extends Component<
showForgotLink
/>
- {this.state.showTotp && (
-
-
-
-
-
-
- )}
);
+async function handleGenerateTotp(i: Settings) {
+ i.setState({ generateTotpRes: LOADING_REQUEST });
+
+ const generateTotpRes = await HttpService.client.generateTotpSecret();
+
+ if (generateTotpRes.state === "failed") {
+ toast(generateTotpRes.msg, "danger");
+ } else {
+ i.setState({ show2faModal: true });
+ }
+
+ i.setState({
+ generateTotpRes,
+ });
+}
+
+function handleShowTotpModal(i: Settings) {
+ i.setState({ show2faModal: true });
+}
+
+function handleClose2faModal(i: Settings) {
+ i.setState({ show2faModal: false });
+}
+
export class Settings extends Component
{
private isoData = setIsoData(this.context);
state: SettingsState = {
- saveRes: { state: "empty" },
- deleteAccountRes: { state: "empty" },
- changePasswordRes: { state: "empty" },
- instancesRes: { state: "empty" },
+ saveRes: EMPTY_REQUEST,
+ deleteAccountRes: EMPTY_REQUEST,
+ changePasswordRes: EMPTY_REQUEST,
+ instancesRes: EMPTY_REQUEST,
saveUserSettingsForm: {},
changePasswordForm: {},
deleteAccountShowConfirm: false,
@@ -170,6 +204,9 @@ export class Settings extends Component {
searchPersonOptions: [],
searchInstanceOptions: [],
isIsomorphic: false,
+ generateTotpRes: EMPTY_REQUEST,
+ updateTotpRes: EMPTY_REQUEST,
+ show2faModal: false,
};
constructor(props: any, context: any) {
@@ -193,6 +230,10 @@ export class Settings extends Component {
this.handleBlockCommunity = this.handleBlockCommunity.bind(this);
this.handleBlockInstance = this.handleBlockInstance.bind(this);
+ this.handleToggle2fa = this.handleToggle2fa.bind(this);
+ this.handleEnable2fa = this.handleEnable2fa.bind(this);
+ this.handleDisable2fa = this.handleDisable2fa.bind(this);
+
const mui = UserService.Instance.myUserInfo;
if (mui) {
const {
@@ -271,7 +312,7 @@ export class Settings extends Component {
if (!this.state.isIsomorphic) {
this.setState({
- instancesRes: { state: "loading" },
+ instancesRes: LOADING_REQUEST,
});
this.setState({
@@ -1001,57 +1042,89 @@ export class Settings extends Component {
}
totpSection() {
- const totpUrl =
- UserService.Instance.myUserInfo?.local_user_view.local_user.totp_2fa_url;
+ const totpEnabled =
+ !!UserService.Instance.myUserInfo?.local_user_view.local_user
+ .totp_2fa_enabled;
+ const { generateTotpRes } = this.state;
return (
<>
- {!totpUrl && (
-
-
-
-
-
-
- )}
-
- {totpUrl && (
- <>
-
-
-
-
-
-
-
- >
+
+ {totpEnabled ? (
+
+ ) : (
+
)}
>
);
}
+ async handleToggle2fa(totp: string, enabled: boolean) {
+ this.setState({ updateTotpRes: LOADING_REQUEST });
+
+ const updateTotpRes = await HttpService.client.updateTotp({
+ enabled,
+ totp_token: totp,
+ });
+
+ this.setState({ updateTotpRes });
+
+ const successful = updateTotpRes.state === "success";
+ if (successful) {
+ this.setState({ show2faModal: false });
+
+ const siteRes = await HttpService.client.getSite();
+ UserService.Instance.myUserInfo!.local_user_view.local_user.totp_2fa_enabled =
+ enabled;
+
+ if (siteRes.state === "success") {
+ this.setState({ siteRes: siteRes.data });
+ }
+
+ toast(
+ I18NextService.i18n.t(
+ enabled ? "enable_totp_success" : "disable_totp_success",
+ ),
+ );
+ } else {
+ toast(I18NextService.i18n.t("incorrect_totp_code"), "danger");
+ }
+
+ return successful;
+ }
+
+ handleEnable2fa(totp: string) {
+ return this.handleToggle2fa(totp, true);
+ }
+
+ handleDisable2fa(totp: string) {
+ return this.handleToggle2fa(totp, false);
+ }
+
handlePersonSearch = debounce(async (text: string) => {
this.setState({ searchPersonLoading: true });
@@ -1248,19 +1321,12 @@ export class Settings extends Component {
);
}
- handleGenerateTotp(i: Settings, event: any) {
- // Coerce false to undefined here, so it won't generate it.
- const checked: boolean | undefined = event.target.checked || undefined;
- if (checked) {
- toast(I18NextService.i18n.t("two_factor_setup_instructions"));
- }
- i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
- }
+ async handleGenerateTotp(i: Settings) {
+ i.setState({ generateTotpRes: LOADING_REQUEST });
- handleRemoveTotp(i: Settings, event: any) {
- // Coerce true to undefined here, so it won't generate it.
- const checked: boolean | undefined = !event.target.checked && undefined;
- i.setState(s => ((s.saveUserSettingsForm.generate_totp_2fa = checked), s));
+ i.setState({
+ generateTotpRes: await HttpService.client.generateTotpSecret(),
+ });
}
handleSendNotificationsToEmailChange(i: Settings, event: any) {
@@ -1365,7 +1431,7 @@ export class Settings extends Component {
async handleSaveSettingsSubmit(i: Settings, event: any) {
event.preventDefault();
- i.setState({ saveRes: { state: "loading" } });
+ i.setState({ saveRes: LOADING_REQUEST });
const saveRes = await HttpService.client.saveUserSettings({
...i.state.saveUserSettingsForm,
@@ -1400,7 +1466,7 @@ export class Settings extends Component {
i.state.changePasswordForm;
if (new_password && old_password && new_password_verify) {
- i.setState({ changePasswordRes: { state: "loading" } });
+ i.setState({ changePasswordRes: LOADING_REQUEST });
const changePasswordRes = await HttpService.client.changePassword({
new_password,
new_password_verify,
@@ -1431,7 +1497,7 @@ export class Settings extends Component {
event.preventDefault();
const password = i.state.deleteAccountForm.password;
if (password) {
- i.setState({ deleteAccountRes: { state: "loading" } });
+ i.setState({ deleteAccountRes: LOADING_REQUEST });
const deleteAccountRes = await HttpService.client.deleteAccount({
password,
// TODO: promt user weather he wants the content to be deleted
diff --git a/src/shared/components/person/verify-email.tsx b/src/shared/components/person/verify-email.tsx
index b736146e..35bece5f 100644
--- a/src/shared/components/person/verify-email.tsx
+++ b/src/shared/components/person/verify-email.tsx
@@ -2,7 +2,12 @@ import { setIsoData } from "@utils/app";
import { Component } from "inferno";
import { GetSiteResponse, VerifyEmailResponse } from "lemmy-js-client";
import { I18NextService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@@ -16,7 +21,7 @@ export class VerifyEmail extends Component {
private isoData = setIsoData(this.context);
state: State = {
- verifyRes: { state: "empty" },
+ verifyRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
};
@@ -26,7 +31,7 @@ export class VerifyEmail extends Component {
async verify() {
this.setState({
- verifyRes: { state: "loading" },
+ verifyRes: LOADING_REQUEST,
});
this.setState({
diff --git a/src/shared/components/post/create-post.tsx b/src/shared/components/post/create-post.tsx
index b447cf14..bd5b3edf 100644
--- a/src/shared/components/post/create-post.tsx
+++ b/src/shared/components/post/create-post.tsx
@@ -14,6 +14,7 @@ import {
import { InitialFetchRequest, PostFormParams } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
import {
+ EMPTY_REQUEST,
HttpService,
RequestState,
WrappedLemmyHttp,
@@ -57,7 +58,7 @@ export class CreatePost extends Component<
state: CreatePostState = {
siteRes: this.isoData.site_res,
loading: true,
- initialCommunitiesRes: { state: "empty" },
+ initialCommunitiesRes: EMPTY_REQUEST,
isIsomorphic: false,
};
@@ -242,7 +243,7 @@ export class CreatePost extends Component<
>): Promise {
const data: CreatePostData = {
initialCommunitiesRes: await fetchCommunitiesForOptions(client),
- communityResponse: { state: "empty" },
+ communityResponse: EMPTY_REQUEST,
};
if (communityId) {
diff --git a/src/shared/components/post/post-form.tsx b/src/shared/components/post/post-form.tsx
index 406d86ea..970bf93c 100644
--- a/src/shared/components/post/post-form.tsx
+++ b/src/shared/components/post/post-form.tsx
@@ -29,7 +29,12 @@ import {
} from "../../config";
import { PostFormParams } from "../../interfaces";
import { I18NextService, UserService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { setupTippy } from "../../tippy";
import { toast } from "../../toast";
import { Icon, Spinner } from "../common/icon";
@@ -116,7 +121,7 @@ function copySuggestedTitle(d: { i: PostForm; suggestedTitle?: string }) {
d.i.setState(
s => ((s.form.name = sTitle?.substring(0, MAX_POST_TITLE_LENGTH)), s),
);
- d.i.setState({ suggestedPostsRes: { state: "empty" } });
+ d.i.setState({ suggestedPostsRes: EMPTY_REQUEST });
setTimeout(() => {
const textarea: any = document.getElementById("post-title");
autosize.update(textarea);
@@ -215,8 +220,8 @@ function handleImageDelete(i: PostForm) {
export class PostForm extends Component {
state: PostFormState = {
- suggestedPostsRes: { state: "empty" },
- metadataRes: { state: "empty" },
+ suggestedPostsRes: EMPTY_REQUEST,
+ metadataRes: EMPTY_REQUEST,
form: {},
loading: false,
imageLoading: false,
@@ -648,7 +653,7 @@ export class PostForm extends Component {
async fetchPageTitle() {
const url = this.state.form.url;
if (url && validURL(url)) {
- this.setState({ metadataRes: { state: "loading" } });
+ this.setState({ metadataRes: LOADING_REQUEST });
this.setState({
metadataRes: await HttpService.client.getSiteMetadata({ url }),
});
@@ -658,7 +663,7 @@ export class PostForm extends Component {
async fetchSimilarPosts() {
const q = this.state.form.name;
if (q && q !== "") {
- this.setState({ suggestedPostsRes: { state: "loading" } });
+ this.setState({ suggestedPostsRes: LOADING_REQUEST });
this.setState({
suggestedPostsRes: await HttpService.client.search({
q,
diff --git a/src/shared/components/post/post.tsx b/src/shared/components/post/post.tsx
index 25514208..8f9bd59e 100644
--- a/src/shared/components/post/post.tsx
+++ b/src/shared/components/post/post.tsx
@@ -83,7 +83,12 @@ import {
InitialFetchRequest,
} from "../../interfaces";
import { FirstLoadService, I18NextService, UserService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { setupTippy } from "../../tippy";
import { toast } from "../../toast";
import { CommentForm } from "../comment/comment-form";
@@ -120,8 +125,8 @@ export class Post extends Component {
private isoData = setIsoData(this.context);
private commentScrollDebounced: () => void;
state: PostState = {
- postRes: { state: "empty" },
- commentsRes: { state: "empty" },
+ postRes: EMPTY_REQUEST,
+ commentsRes: EMPTY_REQUEST,
postId: getIdFromProps(this.props),
commentId: getCommentIdFromProps(this.props),
commentSort: "Hot",
@@ -196,8 +201,8 @@ export class Post extends Component {
async fetchPost() {
this.setState({
- postRes: { state: "loading" },
- commentsRes: { state: "loading" },
+ postRes: LOADING_REQUEST,
+ commentsRes: LOADING_REQUEST,
});
const [postRes, commentsRes] = await Promise.all([
@@ -697,8 +702,8 @@ export class Post extends Component {
i.setState({
commentSort: event.target.value as CommentSortType,
commentViewType: CommentViewType.Tree,
- commentsRes: { state: "loading" },
- postRes: { state: "loading" },
+ commentsRes: LOADING_REQUEST,
+ postRes: LOADING_REQUEST,
});
await i.fetchPost();
}
diff --git a/src/shared/components/private_message/create-private-message.tsx b/src/shared/components/private_message/create-private-message.tsx
index cbe25d79..7c04771f 100644
--- a/src/shared/components/private_message/create-private-message.tsx
+++ b/src/shared/components/private_message/create-private-message.tsx
@@ -9,7 +9,12 @@ import {
} from "lemmy-js-client";
import { InitialFetchRequest } from "../../interfaces";
import { FirstLoadService, I18NextService } from "../../services";
-import { HttpService, RequestState } from "../../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../../services/HttpService";
import { toast } from "../../toast";
import { HtmlTags } from "../common/html-tags";
import { Spinner } from "../common/icon";
@@ -33,7 +38,7 @@ export class CreatePrivateMessage extends Component<
private isoData = setIsoData(this.context);
state: CreatePrivateMessageState = {
siteRes: this.isoData.site_res,
- recipientRes: { state: "empty" },
+ recipientRes: EMPTY_REQUEST,
recipientId: getRecipientIdFromProps(this.props),
isIsomorphic: false,
};
@@ -78,7 +83,7 @@ export class CreatePrivateMessage extends Component<
async fetchPersonDetails() {
this.setState({
- recipientRes: { state: "loading" },
+ recipientRes: LOADING_REQUEST,
});
this.setState({
diff --git a/src/shared/components/remote-fetch.tsx b/src/shared/components/remote-fetch.tsx
index 8f7a3f4b..38b58720 100644
--- a/src/shared/components/remote-fetch.tsx
+++ b/src/shared/components/remote-fetch.tsx
@@ -5,7 +5,11 @@ import { Component, linkEvent } from "inferno";
import { CommunityView, ResolveObjectResponse } from "lemmy-js-client";
import { InitialFetchRequest } from "../interfaces";
import { FirstLoadService, HttpService, I18NextService } from "../services";
-import { RequestState } from "../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ LOADING_REQUEST,
+ RequestState,
+} from "../services/HttpService";
import { HtmlTags } from "./common/html-tags";
import { Spinner } from "./common/icon";
import { LoadingEllipses } from "./common/loading-ellipses";
@@ -76,7 +80,7 @@ const handleUnfollow = (i: RemoteFetch) => handleToggleFollow(i, false);
export class RemoteFetch extends Component {
private isoData = setIsoData(this.context);
state: RemoteFetchState = {
- resolveObjectRes: { state: "empty" },
+ resolveObjectRes: EMPTY_REQUEST,
isIsomorphic: false,
followCommunityLoading: false,
};
@@ -100,7 +104,7 @@ export class RemoteFetch extends Component {
const { uri } = getRemoteFetchQueryParams();
if (uri) {
- this.setState({ resolveObjectRes: { state: "loading" } });
+ this.setState({ resolveObjectRes: LOADING_REQUEST });
this.setState({
resolveObjectRes: await HttpService.client.resolveObject({
q: uriToQuery(uri),
@@ -208,7 +212,7 @@ export class RemoteFetch extends Component {
}: InitialFetchRequest<
QueryParams
>): Promise {
- const data: RemoteFetchData = { resolveObjectRes: { state: "empty" } };
+ const data: RemoteFetchData = { resolveObjectRes: EMPTY_REQUEST };
if (uri && auth) {
data.resolveObjectRes = await client.resolveObject({
diff --git a/src/shared/components/search.tsx b/src/shared/components/search.tsx
index 9041d6fc..08d70727 100644
--- a/src/shared/components/search.tsx
+++ b/src/shared/components/search.tsx
@@ -48,7 +48,12 @@ import {
import { fetchLimit } from "../config";
import { CommentViewType, InitialFetchRequest } from "../interfaces";
import { FirstLoadService, I18NextService } from "../services";
-import { HttpService, RequestState } from "../services/HttpService";
+import {
+ EMPTY_REQUEST,
+ HttpService,
+ LOADING_REQUEST,
+ RequestState,
+} from "../services/HttpService";
import { CommentNodes } from "./comment/comment-nodes";
import { HtmlTags } from "./common/html-tags";
import { Spinner } from "./common/icon";
@@ -239,14 +244,14 @@ export class Search extends Component {
private isoData = setIsoData(this.context);
state: SearchState = {
- resolveObjectRes: { state: "empty" },
- creatorDetailsRes: { state: "empty" },
- communitiesRes: { state: "empty" },
- communityRes: { state: "empty" },
+ resolveObjectRes: EMPTY_REQUEST,
+ creatorDetailsRes: EMPTY_REQUEST,
+ communitiesRes: EMPTY_REQUEST,
+ communityRes: EMPTY_REQUEST,
siteRes: this.isoData.site_res,
creatorSearchOptions: [],
communitySearchOptions: [],
- searchRes: { state: "empty" },
+ searchRes: EMPTY_REQUEST,
searchCreatorLoading: false,
searchCommunitiesLoading: false,
isIsomorphic: false,
@@ -343,7 +348,7 @@ export class Search extends Component {
}
async fetchCommunities() {
- this.setState({ communitiesRes: { state: "loading" } });
+ this.setState({ communitiesRes: LOADING_REQUEST });
this.setState({
communitiesRes: await HttpService.client.listCommunities({
type_: defaultListingType,
@@ -362,12 +367,9 @@ export class Search extends Component {
query: { communityId, creatorId, q, type, sort, listingType, page },
}: InitialFetchRequest>): Promise {
const community_id = getIdFromString(communityId);
- let communityResponse: RequestState = {
- state: "empty",
- };
- let listCommunitiesResponse: RequestState = {
- state: "empty",
- };
+ let communityResponse: RequestState = EMPTY_REQUEST;
+ let listCommunitiesResponse: RequestState =
+ EMPTY_REQUEST;
if (community_id) {
const getCommunityForm: GetCommunity = {
id: community_id,
@@ -387,9 +389,8 @@ export class Search extends Component {
}
const creator_id = getIdFromString(creatorId);
- let creatorDetailsResponse: RequestState = {
- state: "empty",
- };
+ let creatorDetailsResponse: RequestState =
+ EMPTY_REQUEST;
if (creator_id) {
const getCreatorForm: GetPersonDetails = {
person_id: creator_id,
@@ -400,10 +401,9 @@ export class Search extends Component {
const query = getSearchQueryFromQuery(q);
- let searchResponse: RequestState = { state: "empty" };
- let resolveObjectResponse: RequestState = {
- state: "empty",
- };
+ let searchResponse: RequestState = EMPTY_REQUEST;
+ let resolveObjectResponse: RequestState =
+ EMPTY_REQUEST;
if (query) {
const form: SearchForm = {
@@ -430,7 +430,7 @@ export class Search extends Component {
// If we return this object with a state of failed, the catch-all-handler will redirect
// to an error page, so we ignore it by covering up the error with the empty state.
if (resolveObjectResponse.state === "failed") {
- resolveObjectResponse = { state: "empty" };
+ resolveObjectResponse = EMPTY_REQUEST;
}
}
}
@@ -744,8 +744,8 @@ export class Search extends Component {
onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
- onCreateComment={() => Promise.resolve({ state: "empty" })}
- onEditComment={() => Promise.resolve({ state: "empty" })}
+ onCreateComment={() => Promise.resolve(EMPTY_REQUEST)}
+ onEditComment={() => Promise.resolve(EMPTY_REQUEST)}
/>
)}
{i.type_ === "communities" && (
@@ -805,8 +805,8 @@ export class Search extends Component {
onPersonMentionRead={() => {}}
onBanPersonFromCommunity={() => {}}
onBanPerson={() => {}}
- onCreateComment={() => Promise.resolve({ state: "empty" })}
- onEditComment={() => Promise.resolve({ state: "empty" })}
+ onCreateComment={() => Promise.resolve(EMPTY_REQUEST)}
+ onEditComment={() => Promise.resolve(EMPTY_REQUEST)}
/>
);
}
@@ -948,7 +948,7 @@ export class Search extends Component {
getSearchQueryParams();
if (q) {
- this.setState({ searchRes: { state: "loading" } });
+ this.setState({ searchRes: LOADING_REQUEST });
this.setState({
searchRes: await HttpService.client.search({
q,
@@ -965,7 +965,7 @@ export class Search extends Component {
restoreScrollPosition(this.context);
if (myAuth()) {
- this.setState({ resolveObjectRes: { state: "loading" } });
+ this.setState({ resolveObjectRes: LOADING_REQUEST });
this.setState({
resolveObjectRes: await HttpService.silent_client.resolveObject({
q,
diff --git a/src/shared/services/HomeCacheService.ts b/src/shared/services/HomeCacheService.ts
index 9f33dc4e..3b767361 100644
--- a/src/shared/services/HomeCacheService.ts
+++ b/src/shared/services/HomeCacheService.ts
@@ -1,5 +1,5 @@
import { GetPostsResponse } from "lemmy-js-client";
-import { RequestState } from "./HttpService.js";
+import { EMPTY_REQUEST, RequestState } from "./HttpService";
/**
* Service to cache home post listings and restore home state when user uses the browser back buttons.
@@ -8,7 +8,7 @@ export class HomeCacheService {
static #_instance: HomeCacheService;
historyIdx = 0;
scrollY = 0;
- posts: RequestState = { state: "empty" };
+ posts: RequestState = EMPTY_REQUEST;
get active() {
return (
diff --git a/src/shared/services/HttpService.ts b/src/shared/services/HttpService.ts
index 8ba9ac6e..6e0313e9 100644
--- a/src/shared/services/HttpService.ts
+++ b/src/shared/services/HttpService.ts
@@ -3,13 +3,17 @@ import { LemmyHttp } from "lemmy-js-client";
import { toast } from "../toast";
import { I18NextService } from "./I18NextService";
-export type EmptyRequestState = {
- state: "empty";
-};
+export const EMPTY_REQUEST = {
+ state: "empty",
+} as const;
-type LoadingRequestState = {
- state: "loading";
-};
+export type EmptyRequestState = typeof EMPTY_REQUEST;
+
+export const LOADING_REQUEST = {
+ state: "loading",
+} as const;
+
+type LoadingRequestState = typeof LOADING_REQUEST;
export type FailedRequestState = {
state: "failed";
diff --git a/yarn.lock b/yarn.lock
index 5df2a0da..b10a5f07 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1613,6 +1613,20 @@
"@nodelib/fs.scandir" "2.1.5"
fastq "^1.6.0"
+"@pdf-lib/standard-fonts@^1.0.0":
+ version "1.0.0"
+ resolved "https://registry.yarnpkg.com/@pdf-lib/standard-fonts/-/standard-fonts-1.0.0.tgz#8ba691c4421f71662ed07c9a0294b44528af2d7f"
+ integrity sha512-hU30BK9IUN/su0Mn9VdlVKsWBS6GyhVfqjwl1FjZN4TxP6cCw0jP2w7V3Hf5uX7M0AZJ16vey9yE0ny7Sa59ZA==
+ dependencies:
+ pako "^1.0.6"
+
+"@pdf-lib/upng@^1.0.1":
+ version "1.0.1"
+ resolved "https://registry.yarnpkg.com/@pdf-lib/upng/-/upng-1.0.1.tgz#7dc9c636271aca007a9df4deaf2dd7e7960280cb"
+ integrity sha512-dQK2FUMQtowVP00mtIksrlZhdFXQZPC+taih1q4CvPZ5vqdxR/LKBaFg0oAfzd1GlHZXXSPdQfzQnt+ViGvEIQ==
+ dependencies:
+ pako "^1.0.10"
+
"@pkgjs/parseargs@^0.11.0":
version "0.11.0"
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
@@ -1685,6 +1699,16 @@
domhandler "^5.0.3"
selderee "^0.11.0"
+"@shortcm/qr-image@^9.0.2":
+ version "9.0.2"
+ resolved "https://registry.yarnpkg.com/@shortcm/qr-image/-/qr-image-9.0.2.tgz#a24ed06026466974badb7fc7fc863d704d496bbe"
+ integrity sha512-/hz2NqFlT0Xmd5FDiYSsb/lDucZbByWeFUiEz1ekFnz6MHtdpv03mSMSsLm+LF8n/LgumjBcKci3gG2TMirIJA==
+ dependencies:
+ color-string "^1.9.1"
+ js-base64 "^3.7.5"
+ pdf-lib "^1.17.1"
+ sharp "^0.32.5"
+
"@surma/rollup-plugin-off-main-thread@^2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053"
@@ -3210,7 +3234,7 @@ color-name@^1.0.0, color-name@~1.1.4:
resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2"
integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
-color-string@^1.9.0:
+color-string@^1.9.0, color-string@^1.9.1:
version "1.9.1"
resolved "https://registry.yarnpkg.com/color-string/-/color-string-1.9.1.tgz#4467f9146f036f855b764dfb5bf8582bf342c7a4"
integrity sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==
@@ -5874,6 +5898,11 @@ jest-worker@^27.4.5:
merge-stream "^2.0.0"
supports-color "^8.0.0"
+js-base64@^3.7.5:
+ version "3.7.5"
+ resolved "https://registry.yarnpkg.com/js-base64/-/js-base64-3.7.5.tgz#21e24cf6b886f76d6f5f165bfcd69cc55b9e3fca"
+ integrity sha512-3MEt5DTINKqfScXKfJFrRbxkrnk2AxPWGBL/ycjz4dK8iqiSJ06UxD8jh8xuh6p10TX4t2+7FsBYVxxQbMg+qA==
+
"js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0"
resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
@@ -6035,10 +6064,10 @@ leac@^0.6.0:
resolved "https://registry.yarnpkg.com/leac/-/leac-0.6.0.tgz#dcf136e382e666bd2475f44a1096061b70dc0912"
integrity sha512-y+SqErxb8h7nE/fiEX07jsbuhrpO9lL8eca7/Y1nuWV2moNlXhyd59iDGcRf6moVyDMbmTNzL40SUyrFU/yDpg==
-lemmy-js-client@^0.19.0-rc.12:
- version "0.19.0-rc.12"
- resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.12.tgz#e3bd4e21b1966d583ab790ef70ece8394b012b48"
- integrity sha512-1iu2fW9vlb3TrI+QR/ODP3+5pWZB0rUqL1wH09IzomDXohCqoQvfmXpwArmgF4Eq8GZgjkcfeMDC2gMrfw/i7Q==
+lemmy-js-client@^0.19.0-rc.13:
+ version "0.19.0-rc.13"
+ resolved "https://registry.yarnpkg.com/lemmy-js-client/-/lemmy-js-client-0.19.0-rc.13.tgz#e0e15ba6fe3a08cb85130eea7eec4bd2773999f9"
+ integrity sha512-JP9oEh1+Wfttqx5O5EMAVIR/hFVS66iVKmEo8/Uxw8fJfyUeQo7BhKvG8LTYegBE39Womgyu3KxXb7Jy9DRI5A==
dependencies:
cross-fetch "^3.1.5"
form-data "^4.0.0"
@@ -7451,6 +7480,11 @@ pacote@^8.1.6:
unique-filename "^1.1.0"
which "^1.3.0"
+pako@^1.0.10, pako@^1.0.11, pako@^1.0.6:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+ integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
parallel-transform@^1.1.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/parallel-transform/-/parallel-transform-1.2.0.tgz#9049ca37d6cb2182c3b1d2c720be94d14a5814fc"
@@ -7570,6 +7604,16 @@ path-type@^4.0.0:
resolved "https://registry.yarnpkg.com/path-type/-/path-type-4.0.0.tgz#84ed01c0a7ba380afe09d90a8c180dcd9d03043b"
integrity sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==
+pdf-lib@^1.17.1:
+ version "1.17.1"
+ resolved "https://registry.yarnpkg.com/pdf-lib/-/pdf-lib-1.17.1.tgz#9e7dd21261a0c1fb17992580885b39e7d08f451f"
+ integrity sha512-V/mpyJAoTsN4cnP31vc0wfNA1+p20evqqnap0KLoRUN0Yk/p3wN52DOEsL4oBFcLdb76hlpKPtzJIgo67j/XLw==
+ dependencies:
+ "@pdf-lib/standard-fonts" "^1.0.0"
+ "@pdf-lib/upng" "^1.0.1"
+ pako "^1.0.11"
+ tslib "^1.11.1"
+
peberminta@^0.9.0:
version "0.9.0"
resolved "https://registry.yarnpkg.com/peberminta/-/peberminta-0.9.0.tgz#8ec9bc0eb84b7d368126e71ce9033501dca2a352"
@@ -8570,6 +8614,20 @@ sharp@^0.32.4:
tar-fs "^3.0.4"
tunnel-agent "^0.6.0"
+sharp@^0.32.5:
+ version "0.32.6"
+ resolved "https://registry.yarnpkg.com/sharp/-/sharp-0.32.6.tgz#6ad30c0b7cd910df65d5f355f774aa4fce45732a"
+ integrity sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==
+ dependencies:
+ color "^4.2.3"
+ detect-libc "^2.0.2"
+ node-addon-api "^6.1.0"
+ prebuild-install "^7.1.1"
+ semver "^7.5.4"
+ simple-get "^4.0.1"
+ tar-fs "^3.0.4"
+ tunnel-agent "^0.6.0"
+
shebang-command@^1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-1.2.0.tgz#44aac65b695b03398968c39f363fee5deafdf1ea"
@@ -9386,6 +9444,11 @@ ts-api-utils@^1.0.1:
resolved "https://registry.yarnpkg.com/ts-api-utils/-/ts-api-utils-1.0.1.tgz#8144e811d44c749cd65b2da305a032510774452d"
integrity sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==
+tslib@^1.11.1:
+ version "1.14.1"
+ resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00"
+ integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==
+
tslib@^2.1.0, tslib@^2.5.0, tslib@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.6.0.tgz#b295854684dbda164e181d259a22cd779dcd7bc3"