mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-10-31 03:12:25 -05:00 
			
		
		
		
	[feature] Add "Instance Info" settings panel section, with domain blocks + allows (#4193)
This pull request adds a new read-only, user-level "instance info" section to the settings panel, which presents api/v2/instance info in a nice readable format, and also gives the user authenticated access to the blocklist and allowlist of the domain. Closes https://codeberg.org/superseriousbusiness/gotosocial/issues/3711 Reviewed-on: https://codeberg.org/superseriousbusiness/gotosocial/pulls/4193 Co-authored-by: tobi <tobi.smethurst@protonmail.com> Co-committed-by: tobi <tobi.smethurst@protonmail.com>
This commit is contained in:
		
					parent
					
						
							
								326e04283a
							
						
					
				
			
			
				commit
				
					
						fd64a1e264
					
				
			
		
					 13 changed files with 543 additions and 40 deletions
				
			
		|  | @ -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", | ||||
|  |  | |||
|  | @ -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 }; | ||||
|  |  | |||
							
								
								
									
										53
									
								
								web/source/settings/lib/query/user/domainperms.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								web/source/settings/lib/query/user/domainperms.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -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, | ||||
| }; | ||||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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; | ||||
| } | ||||
|  |  | |||
|  | @ -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]); | ||||
| } | ||||
|  |  | |||
|  | @ -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; | ||||
|  |  | |||
|  | @ -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} | ||||
| 			/> | ||||
|  |  | |||
|  | @ -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} | ||||
|  |  | |||
							
								
								
									
										287
									
								
								web/source/settings/views/user/instance/index.tsx
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										287
									
								
								web/source/settings/views/user/instance/index.tsx
									
										
									
									
									
										Normal file
									
								
							|  | @ -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]); | ||||
| } | ||||
|  | @ -85,6 +85,11 @@ export default function UserMenu() { | |||
| 					icon="fa-plus" | ||||
| 				/> | ||||
| 			</MenuItem> | ||||
| 			<MenuItem | ||||
| 				name="Instance Info" | ||||
| 				itemUrl="instance-info" | ||||
| 				icon="fa-info" | ||||
| 			/> | ||||
| 		</MenuItem> | ||||
| 	); | ||||
| } | ||||
|  |  | |||
|  | @ -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 /> | ||||
|  |  | |||
|  | @ -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" | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue