diff options
Diffstat (limited to 'web/source')
| -rw-r--r-- | web/source/package.json | 2 | ||||
| -rw-r--r-- | web/source/settings/lib/query/gts-api.ts | 16 | ||||
| -rw-r--r-- | web/source/settings/lib/query/user/domainperms.ts | 53 | ||||
| -rw-r--r-- | web/source/settings/lib/types/domain-permission.ts | 1 | ||||
| -rw-r--r-- | web/source/settings/lib/types/instance.ts | 112 | ||||
| -rw-r--r-- | web/source/settings/lib/util/index.ts | 42 | ||||
| -rw-r--r-- | web/source/settings/style.css | 44 | ||||
| -rw-r--r-- | web/source/settings/views/moderation/domain-permissions/detail.tsx | 4 | ||||
| -rw-r--r-- | web/source/settings/views/moderation/domain-permissions/drafts/new.tsx | 4 | ||||
| -rw-r--r-- | web/source/settings/views/user/instance/index.tsx | 287 | ||||
| -rw-r--r-- | web/source/settings/views/user/menu.tsx | 5 | ||||
| -rw-r--r-- | web/source/settings/views/user/router.tsx | 3 | ||||
| -rw-r--r-- | web/source/yarn.lock | 10 |
13 files changed, 543 insertions, 40 deletions
diff --git a/web/source/package.json b/web/source/package.json index 3cb70e9a6..80dbb114e 100644 --- a/web/source/package.json +++ b/web/source/package.json @@ -16,6 +16,7 @@ "blurhash": "^2.0.5", "get-by-dot": "^1.0.2", "html-to-text": "^9.0.5", + "humanize-duration": "^3.32.2", "is-valid-domain": "^0.1.6", "js-file-download": "^0.4.12", "langs": "^2.0.0", @@ -48,6 +49,7 @@ "@browserify/uglifyify": "^6.0.0", "@joepie91/eslint-config": "^1.1.1", "@types/html-to-text": "^9.0.4", + "@types/humanize-duration": "^3.27.4", "@types/is-valid-domain": "^0.0.2", "@types/papaparse": "^5.3.9", "@types/parse-link-header": "^2.0.3", diff --git a/web/source/settings/lib/query/gts-api.ts b/web/source/settings/lib/query/gts-api.ts index 9d38e435d..33429d8a8 100644 --- a/web/source/settings/lib/query/gts-api.ts +++ b/web/source/settings/lib/query/gts-api.ts @@ -26,7 +26,7 @@ import type { import { serialize as serializeForm } from "object-to-formdata"; import type { FetchBaseQueryMeta } from "@reduxjs/toolkit/dist/query/fetchBaseQuery"; import type { RootState } from '../../redux/store'; -import { InstanceV1 } from '../types/instance'; +import { InstanceV1, InstanceV2 } from '../types/instance'; /** * GTSFetchArgs extends standard FetchArgs used by @@ -186,6 +186,11 @@ export const gtsApi = createApi({ query: () => ({ url: `/api/v1/instance` }) + }), + instanceV2: build.query<InstanceV2, void>({ + query: () => ({ + url: `/api/v2/instance` + }) }) }) }); @@ -193,8 +198,13 @@ export const gtsApi = createApi({ /** * Query /api/v1/instance to retrieve basic instance information. * This endpoint does not require authentication/authorization. - * TODO: move this to ./instance. */ const useInstanceV1Query = gtsApi.useInstanceV1Query; -export { useInstanceV1Query }; +/** + * Query /api/v2/instance to retrieve basic instance information. + * This endpoint does not require authentication/authorization. + */ +const useInstanceV2Query = gtsApi.useInstanceV2Query; + +export { useInstanceV1Query, useInstanceV2Query }; diff --git a/web/source/settings/lib/query/user/domainperms.ts b/web/source/settings/lib/query/user/domainperms.ts new file mode 100644 index 000000000..3d8e77bfe --- /dev/null +++ b/web/source/settings/lib/query/user/domainperms.ts @@ -0,0 +1,53 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import { gtsApi } from "../gts-api"; + +import type { DomainPerm } from "../../types/domain-permission"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + instanceDomainBlocks: build.query<DomainPerm[], void>({ + query: () => ({ + url: `/api/v1/instance/domain_blocks` + }), + }), + + instanceDomainAllows: build.query<DomainPerm[], void>({ + query: () => ({ + url: `/api/v1/instance/domain_allows` + }) + }), + }), +}); + +/** + * Get user-level view of all explicitly blocked domains. + */ +const useInstanceDomainBlocksQuery = extended.useInstanceDomainBlocksQuery; + +/** + * Get user-level view of all explicitly allowed domains. + */ +const useInstanceDomainAllowsQuery = extended.useInstanceDomainAllowsQuery; + +export { + useInstanceDomainBlocksQuery, + useInstanceDomainAllowsQuery, +}; diff --git a/web/source/settings/lib/types/domain-permission.ts b/web/source/settings/lib/types/domain-permission.ts index 27c4b56c9..3e947db61 100644 --- a/web/source/settings/lib/types/domain-permission.ts +++ b/web/source/settings/lib/types/domain-permission.ts @@ -33,6 +33,7 @@ export interface DomainPerm { obfuscate?: boolean; private_comment?: string; public_comment?: string; + comment?: string; created_at?: string; created_by?: string; subscription_id?: string; diff --git a/web/source/settings/lib/types/instance.ts b/web/source/settings/lib/types/instance.ts index 9abdc6a96..87d129d92 100644 --- a/web/source/settings/lib/types/instance.ts +++ b/web/source/settings/lib/types/instance.ts @@ -17,36 +17,52 @@ along with this program. If not, see <http://www.gnu.org/licenses/>. */ +import { Account } from "./account"; + export interface InstanceV1 { - uri: string; - account_domain: string; - title: string; - description: string; + uri: string; + account_domain: string; + title: string; + description: string; description_text?: string; - short_description: string; + short_description: string; short_description_text?: string; - custom_css: string; - email: string; - version: string; - debug?: boolean; - languages: any[]; // TODO: define this - registrations: boolean; - approval_required: boolean; - invites_enabled: boolean; - configuration: InstanceConfiguration; - urls: InstanceUrls; - stats: InstanceStats; - thumbnail: string; - contact_account: Object; // TODO: define this. - max_toot_chars: number; - rules: any[]; // TODO: define this - terms?: string; + custom_css: string; + email: string; + version: string; + debug?: boolean; + languages: string[]; + registrations: boolean; + approval_required: boolean; + invites_enabled: boolean; + configuration: InstanceV1Configuration; + urls: InstanceV1Urls; + stats: InstanceStats; + thumbnail: string; + contact_account: Account; + max_toot_chars: number; + rules: any[]; // TODO: define this + terms?: string; terms_text?: string; } -export interface InstanceConfiguration { +export interface InstanceV2 { + domain: string; + account_domain: string; + title: string; + version: string; + debug: boolean; + source_url: string; + description: string; + custom_css: string; + thumbnail: InstanceV2Thumbnail; + languages: string[]; + configuration: InstanceV2Configuration; +} + +export interface InstanceV1Configuration { statuses: InstanceStatuses; - media_attachments: InstanceMediaAttachments; + media_attachments: InstanceV1MediaAttachments; polls: InstancePolls; accounts: InstanceAccounts; emojis: InstanceEmojis; @@ -63,15 +79,6 @@ export interface InstanceEmojis { emoji_size_limit: number; } -export interface InstanceMediaAttachments { - supported_mime_types: string[]; - image_size_limit: number; - image_matrix_limit: number; - video_size_limit: number; - video_frame_rate_limit: number; - video_matrix_limit: number; -} - export interface InstancePolls { max_options: number; max_characters_per_option: number; @@ -92,7 +99,46 @@ export interface InstanceStats { user_count: number; } -export interface InstanceUrls { +export interface InstanceV1Urls { streaming_api: string; } +export interface InstanceV1MediaAttachments { + supported_mime_types: string[]; + image_size_limit: number; + image_matrix_limit: number; + video_size_limit: number; + video_frame_rate_limit: number; + video_matrix_limit: number; +} + +export interface InstanceV2Configuration { + urls: InstanceV2URLs; + accounts: InstanceAccounts; + statuses: InstanceStatuses; + media_attachments: InstanceV2MediaAttachments; + polls: InstancePolls; + translation: InstanceV2Translation; + emojis: InstanceEmojis; +} + +export interface InstanceV2MediaAttachments extends InstanceV1MediaAttachments { + description_limit: number; +} + +export interface InstanceV2Thumbnail { + url: string; + thumbnail_type?: string; + static_url?: string; + thumbnail_static_type?: string; + thumbnail_description?: string; + blurhash?: string; +} + +export interface InstanceV2Translation { + enabled: boolean; +} + +export interface InstanceV2URLs { + streaming: string; +} diff --git a/web/source/settings/lib/util/index.ts b/web/source/settings/lib/util/index.ts index 8bcf5ab5d..46b35fd70 100644 --- a/web/source/settings/lib/util/index.ts +++ b/web/source/settings/lib/util/index.ts @@ -22,6 +22,8 @@ import { useMemo } from "react"; import { AdminAccount } from "../types/account"; import { store } from "../../redux/store"; +import humanizeDuration from "humanize-duration"; + export function yesOrNo(b: boolean): string { return b ? "yes" : "no"; } @@ -54,3 +56,43 @@ export function useCapitalize(i?: string): string { return i.charAt(0).toUpperCase() + i.slice(1); }, [i]); } + +/** + * Return human-readable string representation of given bytes. + * + * Adapted from https://stackoverflow.com/a/14919494. + */ +export function useHumanReadableBytes(bytes: number): string { + return useMemo(() => { + const thresh = 1024; + const digitPrecision = 2; + const r = 10**digitPrecision; + const units = ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB']; + + if (Math.abs(bytes) < thresh) { + return bytes + ' B'; + } + + let u = -1; + let threshed = bytes; + do { threshed /= thresh; ++u; + } while (Math.round(Math.abs(threshed) * r) / r >= thresh && u < units.length - 1); + + return threshed.toFixed(digitPrecision) + ' ' + units[u]; + }, [bytes]); +} + +/** + * Return human-readable string representation of given time in seconds. + */ +export function useHumanReadableDuration(seconds: number): string { + return useMemo(() => { + if (seconds % 2629746 === 0) { + const n = seconds / 2629746; + return n + " month" + (n !== 1 ? "s" : ""); + } + + const ms = seconds*1000; + return humanizeDuration(ms); + }, [seconds]); +} diff --git a/web/source/settings/style.css b/web/source/settings/style.css index 67937bd9e..742407ea3 100644 --- a/web/source/settings/style.css +++ b/web/source/settings/style.css @@ -1549,6 +1549,50 @@ button.tab-button { } } +.instance-info-view { + .info-list .info-list-entry { + /* + Some of the labels are quite + long so ensure there's enough + gap when they're wrapped. + */ + gap: 1rem; + } + + /* + Make sure ellipsis works + properly for v. long domains. + */ + .list.domain-perm-list > .entry > .domain { + display: inline-block; + font-weight: bold; + } + + /* + Make sure we can break. + */ + .list.domain-perm-list > .entry > .public_comment { + word-wrap: anywhere; + } + + /* + Disable the hover effects as + these entries aren't clickable. + */ + .list.domain-perm-list > .entry:hover { + background: $list-entry-bg; + } + .list.domain-perm-list > .entry:nth-child(2n):hover { + background: $list-entry-alternate-bg; + } + .list.domain-perm-list > .entry { + &:active, &:focus, &:hover, &:target { + border-color: $gray1; + border-top-color: transparent; + } + } +} + .instance-rules { list-style-position: inside; margin: 0; diff --git a/web/source/settings/views/moderation/domain-permissions/detail.tsx b/web/source/settings/views/moderation/domain-permissions/detail.tsx index e8ef487e3..35be0e16d 100644 --- a/web/source/settings/views/moderation/domain-permissions/detail.tsx +++ b/web/source/settings/views/moderation/domain-permissions/detail.tsx @@ -307,14 +307,14 @@ function CreateOrUpdateDomainPerm({ <TextArea field={form.privateComment} - label="Private comment" + label="Private comment (shown to admins only)" autoCapitalize="sentences" rows={3} /> <TextArea field={form.publicComment} - label="Public comment" + label="Public comment (shown to members of this instance via the instance info page, and on the web if enabled)" autoCapitalize="sentences" rows={3} /> diff --git a/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx b/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx index c78f8192a..f9dc2d387 100644 --- a/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx +++ b/web/source/settings/views/moderation/domain-permissions/drafts/new.tsx @@ -86,7 +86,7 @@ export default function DomainPermissionDraftNew() { <TextArea field={form.private_comment} - label={"Private comment"} + label={"Private comment (will be shown to admins only)"} placeholder="This domain is like unto a clown car full of clowns, I suggest we block it forthwith." autoCapitalize="sentences" rows={3} @@ -94,7 +94,7 @@ export default function DomainPermissionDraftNew() { <TextArea field={form.public_comment} - label={"Public comment"} + label={"Public comment (will be shown to members of this instance via the instance info page, and on the web if enabled)"} placeholder="Bad posters" autoCapitalize="sentences" rows={3} diff --git a/web/source/settings/views/user/instance/index.tsx b/web/source/settings/views/user/instance/index.tsx new file mode 100644 index 000000000..0e0643bd7 --- /dev/null +++ b/web/source/settings/views/user/instance/index.tsx @@ -0,0 +1,287 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see <http://www.gnu.org/licenses/>. +*/ + +import React, { useMemo } from "react"; +import { useInstanceV2Query } from "../../../lib/query/gts-api"; +import Loading from "../../../components/loading"; +import { InstanceV2 } from "../../../lib/types/instance"; +import { useHumanReadableBytes, useHumanReadableDuration, yesOrNo } from "../../../lib/util"; +import { HighlightedCode } from "../../../components/highlightedcode"; +import { useInstanceDomainAllowsQuery, useInstanceDomainBlocksQuery } from "../../../lib/query/user/domainperms"; + +export default function InstanceInfo() { + // Load instance v2 data. + const { + data, + isFetching, + isLoading, + } = useInstanceV2Query(); + + if (isFetching || isLoading) { + return <Loading />; + } + + if (data === undefined) { + throw "could not fetch instance v2"; + } + + return ( + <div className="instance-info-view"> + <div className="form-section-docs"> + <h1>Instance Info</h1> + <p> + On this page you can see information about this instance, and view domain blocks + and domain allows that have been created by the admin(s) of the instance. + </p> + </div> + <Instance instance={data} /> + <Allowlist /> + <Blocklist /> + </div> + ); +} + +function Instance({ instance }: { instance: InstanceV2 }) { + const emojiSizeLimit = useHumanReadableBytes(instance.configuration.emojis.emoji_size_limit); + const accountsCustomCSS = yesOrNo(instance.configuration.accounts.allow_custom_css); + const imageSizeLimit = useHumanReadableBytes(instance.configuration.media_attachments.image_size_limit); + const videoSizeLimit = useHumanReadableBytes(instance.configuration.media_attachments.video_size_limit); + const pollMinExpiry = useHumanReadableDuration(instance.configuration.polls.min_expiration); + const pollMaxExpiry = useHumanReadableDuration(instance.configuration.polls.max_expiration); + + return ( + <> + <dl className="info-list"> + <div className="info-list-entry"> + <dt>Software version:</dt> + <dd> + <a + href={instance.source_url} + target="_blank" + rel="noreferrer" + > + {instance.version} + </a> + </dd> + </div> + + <div className="info-list-entry"> + <dt>Streaming URL:</dt> + <dd className="monospace">{instance.configuration.urls.streaming}</dd> + </div> + + <div className="info-list-entry"> + <dt>Emoji size limit:</dt> + <dd>{emojiSizeLimit}</dd> + </div> + + <div className="info-list-entry"> + <dt>Accounts custom CSS:</dt> + <dd>{accountsCustomCSS}</dd> + </div> + + <div className="info-list-entry"> + <dt>Accounts max featured tags:</dt> + <dd>{instance.configuration.accounts.max_featured_tags}</dd> + </div> + + <div className="info-list-entry"> + <dt>Accounts max profile fields:</dt> + <dd>{instance.configuration.accounts.max_profile_fields}</dd> + </div> + + <div className="info-list-entry"> + <dt>Posts max characters:</dt> + <dd>{instance.configuration.statuses.max_characters}</dd> + </div> + + <div className="info-list-entry"> + <dt>Posts max attachments:</dt> + <dd>{instance.configuration.statuses.max_media_attachments}</dd> + </div> + + <div className="info-list-entry"> + <dt>Posts supported types:</dt> + <dd className="monospace"> + { useJoinWithNewlines(instance.configuration.statuses.supported_mime_types) } + </dd> + </div> + + <div className="info-list-entry"> + <dt>Polls max options:</dt> + <dd>{instance.configuration.polls.max_options}</dd> + </div> + + <div className="info-list-entry"> + <dt>Polls max characters per option:</dt> + <dd>{instance.configuration.polls.max_characters_per_option}</dd> + </div> + + <div className="info-list-entry"> + <dt>Polls min expiration:</dt> + <dd>{pollMinExpiry}</dd> + </div> + + <div className="info-list-entry"> + <dt>Polls max expiration:</dt> + <dd>{pollMaxExpiry}</dd> + </div> + + <div className="info-list-entry"> + <dt>Media max description characters:</dt> + <dd>{instance.configuration.media_attachments.description_limit}</dd> + </div> + + <div className="info-list-entry"> + <dt>Media max image size:</dt> + <dd>{imageSizeLimit}</dd> + </div> + + <div className="info-list-entry"> + <dt>Media max video size:</dt> + <dd>{videoSizeLimit}</dd> + </div> + + <div className="info-list-entry"> + <dt>Media supported types:</dt> + <dd className="monospace"> + { useJoinWithNewlines(instance.configuration.media_attachments.supported_mime_types) } + </dd> + </div> + </dl> + + { instance.custom_css && + <> + <div className="form-section-docs"> + <h3>Custom CSS</h3> + <p>The following custom CSS has been set by the admin(s) of this instance, and will be loaded on each web page:</p> + </div> + <HighlightedCode code={instance.custom_css} lang="css" /> + </> + } + </> + ); +} + +function Allowlist() { + // Load allows. + const { + data, + isFetching, + isLoading, + } = useInstanceDomainAllowsQuery(); + + if (isFetching || isLoading) { + return <Loading />; + } + + if (data === undefined) { + throw "could not fetch domain allows"; + } + + return ( + <> + <div className="form-section-docs"> + <h3>Domain Allows</h3> + <p> + The following list of domains has been explicitly allowed by the administrator(s) of this instance. + <br/>This extends to subdomains, so an allowlist entry for domain 'example.com' includes domain 'social.example.com' etc as well. + </p> + </div> + { data.length !== 0 + ? <div className="list domain-perm-list"> + <div className="header entry"> + <div className="domain">Domain</div> + <div className="public_comment">Public comment</div> + </div> + { data.map(e => { + return ( + <div className="entry" id={e.domain} key={e.domain}> + <div className="domain text-cutoff">{e.domain}</div> + <div className="public_comment">{e.comment}</div> + </div> + ); + }) } + </div> + : <b>No explicit allows.</b> + } + </> + ); +} + +function Blocklist() { + // Load blocks. + const { + data, + isFetching, + isLoading, + } = useInstanceDomainBlocksQuery(); + + if (isFetching || isLoading) { + return <Loading />; + } + + if (data === undefined) { + throw "could not fetch domain blocks"; + } + + return ( + <> + <div className="form-section-docs"> + <h3>Domain Blocks</h3> + <p> + The following list of domains has been blocked by the administrator(s) of this instance. + <br/>All past, present, and future accounts at blocked domains are forbidden from interacting with this instance or accounts on this instance. + <br/>No data will be sent to the server at the remote domain, and no data will be received from it. + <br/>This extends to subdomains, so a blocklist entry for domain 'example.com' includes domain 'social.example.com' etc as well. + </p> + </div> + { data.length !== 0 + ? <div className="list domain-perm-list"> + <div className="header entry"> + <div className="domain">Domain</div> + <div className="public_comment">Public comment</div> + </div> + { data.map(e => { + return ( + <div className="entry" id={e.domain} key={e.domain}> + <div className="domain text-cutoff">{e.domain}</div> + <div className="public_comment">{e.comment}</div> + </div> + ); + }) } + </div> + : <b>No domain blocks.</b> + } + </> + ); +} + +function useJoinWithNewlines(a: string[]) { + return useMemo(() => { + const l = a.length; + return a.map((v, i) => { + const e = <span key={v}>{v}</span>; + if (i+1 !== l) { + return [e, <br key={v + "br"} />]; + } + return [e]; + }).flat(); + }, [a]); +} diff --git a/web/source/settings/views/user/menu.tsx b/web/source/settings/views/user/menu.tsx index 4127aa8f0..ae214b6f2 100644 --- a/web/source/settings/views/user/menu.tsx +++ b/web/source/settings/views/user/menu.tsx @@ -85,6 +85,11 @@ export default function UserMenu() { icon="fa-plus" /> </MenuItem> + <MenuItem + name="Instance Info" + itemUrl="instance-info" + icon="fa-info" + /> </MenuItem> ); } diff --git a/web/source/settings/views/user/router.tsx b/web/source/settings/views/user/router.tsx index 62eaf0f36..ea84464bf 100644 --- a/web/source/settings/views/user/router.tsx +++ b/web/source/settings/views/user/router.tsx @@ -33,6 +33,7 @@ import NewApp from "./applications/new"; import AppDetail from "./applications/detail"; import { AppTokenCallback } from "./applications/callback"; import Migration from "./migration"; +import InstanceInfo from "./instance"; /** * - /settings/user/profile @@ -43,6 +44,7 @@ import Migration from "./migration"; * - /settings/user/tokens * - /settings/user/interaction_requests * - /settings/user/applications + * - /settings/user/instance-info */ export default function UserRouter() { const baseUrl = useBaseUrl(); @@ -59,6 +61,7 @@ export default function UserRouter() { <Route path="/migration" component={Migration} /> <Route path="/export-import" component={ExportImport} /> <Route path="/tokens" component={Tokens} /> + <Route path="/instance-info" component={InstanceInfo} /> </Switch> <InteractionRequestsRouter /> <ApplicationsRouter /> diff --git a/web/source/yarn.lock b/web/source/yarn.lock index 88882f370..d378ffd60 100644 --- a/web/source/yarn.lock +++ b/web/source/yarn.lock @@ -1573,6 +1573,11 @@ dependencies: "@types/node" "*" +"@types/humanize-duration@^3.27.4": + version "3.27.4" + resolved "https://registry.yarnpkg.com/@types/humanize-duration/-/humanize-duration-3.27.4.tgz#51d6d278213374735440bc3749de920935e9127e" + integrity sha512-yaf7kan2Sq0goxpbcwTQ+8E9RP6HutFBPv74T/IA/ojcHKhuKVlk2YFYyHhWZeLvZPzzLE3aatuQB4h0iqyyUA== + "@types/is-valid-domain@^0.0.2": version "0.0.2" resolved "https://registry.yarnpkg.com/@types/is-valid-domain/-/is-valid-domain-0.0.2.tgz#78b236f05da281213481c4af0a7ce452d4ff810a" @@ -4339,6 +4344,11 @@ https-browserify@^1.0.0: resolved "https://registry.yarnpkg.com/https-browserify/-/https-browserify-1.0.0.tgz#ec06c10e0a34c0f2faf199f7fd7fc78fffd03c73" integrity sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg== +humanize-duration@^3.32.2: + version "3.32.2" + resolved "https://registry.yarnpkg.com/humanize-duration/-/humanize-duration-3.32.2.tgz#c80287a1b89f1aa7c7fe8fae33417a302b77b427" + integrity sha512-jcTwWYeCJf4dN5GJnjBmHd42bNyK94lY49QTkrsAQrMTUoIYLevvDpmQtg5uv8ZrdIRIbzdasmSNZ278HHUPEg== + iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" |
