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" && ( +
+ + {I18NextService.i18n.t("totp_link")} + +
+ + {I18NextService.i18n.t("totp_qr_segue")} + + {I18NextService.i18n.t("totp_qr")} +
+
+ )} +
+ +
+ {Array.from(Array(TOTP_LENGTH).keys()).map(i => ( + { + this.inputRefs[i] = element; + }} + /> + ))} +
+
+
+
+ +
+
+
+
+ ); + } + + 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} /> +
{this.loginForm()}
@@ -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"