This commit is contained in:
Jade Ellis 2024-12-16 01:49:00 +00:00 committed by GitHub
commit dd73331c8e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 180 additions and 82 deletions

150
web/package-lock.json generated
View file

@ -17,11 +17,13 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-intl": "^7.0.2",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
"unhomoglyph": "^1.0.6" "unhomoglyph": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.11.1",
"@swc/plugin-formatjs": "^2.0.1",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/leaflet": "^1.9.14", "@types/leaflet": "^1.9.14",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",
@ -837,6 +839,77 @@
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
} }
}, },
"node_modules/@formatjs/ecma402-abstract": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.2.5.tgz",
"integrity": "sha512-ep/5vGkyZvMSi6s8nQG8k7vTcKjuXs402fgGIWixj0AWRgKbeaZeLuYc32NIPXexgBjWepMeZGgHLuZXkuD2Gg==",
"license": "MIT",
"dependencies": {
"@formatjs/fast-memoize": "2.2.4",
"@formatjs/intl-localematcher": "0.5.8",
"tslib": "2"
}
},
"node_modules/@formatjs/fast-memoize": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.4.tgz",
"integrity": "sha512-8SzI0cBADgbLOYsoQW/IqVHljCH964CrOdESFQ07wMkRLP90+MfV7k6gZPiGD88ubqET9igJV5c292rT28B7xQ==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@formatjs/icu-messageformat-parser": {
"version": "2.9.5",
"resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.9.5.tgz",
"integrity": "sha512-mHauC9wuVXtnshAIoAYjlNrh6+OFOT6cC4fpK+AG+DHkVWwIPFVQE28hLQ/KptuvQ8VMfG/zYx6rRjtaeFPkSQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.5",
"@formatjs/icu-skeleton-parser": "1.8.9",
"tslib": "2"
}
},
"node_modules/@formatjs/icu-skeleton-parser": {
"version": "1.8.9",
"resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.9.tgz",
"integrity": "sha512-1KSSlU7ywsU5E5v7xr6VTlBzLGszMi3GOu7EVINjkfA501GN5OkeNSbd5q6ie1wIknZJGBlqkvXPYYdp3YXjpw==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.5",
"tslib": "2"
}
},
"node_modules/@formatjs/intl": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@formatjs/intl/-/intl-3.0.2.tgz",
"integrity": "sha512-yZZJDKwoyW0USqV6dnEbJohnNqPREuIFrew01Ht0IiXlfKAjuah2Q3VO6tPXEDUxDo0mhroNEk+nKV0AVLunVQ==",
"license": "MIT",
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.5",
"@formatjs/fast-memoize": "2.2.4",
"@formatjs/icu-messageformat-parser": "2.9.5",
"intl-messageformat": "10.7.8",
"tslib": "2"
},
"peerDependencies": {
"typescript": "5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/@formatjs/intl-localematcher": {
"version": "0.5.8",
"resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.8.tgz",
"integrity": "sha512-I+WDNWWJFZie+jkfkiK5Mp4hEDyRSEvmyfYadflOno/mmKJKcB17fEpEH0oJu/OWhhCJ8kJBDz2YMd/6cDl7Mg==",
"license": "MIT",
"dependencies": {
"tslib": "2"
}
},
"node_modules/@humanfs/core": { "node_modules/@humanfs/core": {
"version": "0.19.1", "version": "0.19.1",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
@ -1730,6 +1803,16 @@
"dev": true, "dev": true,
"license": "Apache-2.0" "license": "Apache-2.0"
}, },
"node_modules/@swc/plugin-formatjs": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@swc/plugin-formatjs/-/plugin-formatjs-2.0.1.tgz",
"integrity": "sha512-rVGGiKm1u9WVhopC9275TDzuBheHUxQEO7cRbqcyo4yZ5FmO5ukvM5XYkBp24k8fUScH6g3vNTAASqLN0jBL3Q==",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@swc/counter": "^0.1.3"
}
},
"node_modules/@swc/types": { "node_modules/@swc/types": {
"version": "0.1.17", "version": "0.1.17",
"resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz",
@ -1754,6 +1837,16 @@
"dev": true, "dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/hoist-non-react-statics": {
"version": "3.3.6",
"resolved": "https://registry.npmjs.org/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.6.tgz",
"integrity": "sha512-lPByRJUer/iN/xa4qpyL0qmL11DqNW81iU/IG1S3uvRUq4oKagz8VCxZjiWkumgt66YT3vOdDgZ0o32sGKtCEw==",
"license": "MIT",
"dependencies": {
"@types/react": "*",
"hoist-non-react-statics": "^3.3.0"
}
},
"node_modules/@types/json-schema": { "node_modules/@types/json-schema": {
"version": "7.0.15", "version": "7.0.15",
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
@ -1789,7 +1882,6 @@
"version": "19.0.0", "version": "19.0.0",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz",
"integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==", "integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==",
"dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"csstype": "^3.0.2" "csstype": "^3.0.2"
@ -2513,7 +2605,6 @@
"version": "3.1.3", "version": "3.1.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"dev": true,
"license": "MIT" "license": "MIT"
}, },
"node_modules/data-view-buffer": { "node_modules/data-view-buffer": {
@ -3615,6 +3706,15 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/hoist-non-react-statics": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz",
"integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==",
"license": "BSD-3-Clause",
"dependencies": {
"react-is": "^16.7.0"
}
},
"node_modules/htmlparser2": { "node_modules/htmlparser2": {
"version": "8.0.2", "version": "8.0.2",
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
@ -3687,6 +3787,18 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/intl-messageformat": {
"version": "10.7.8",
"resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.8.tgz",
"integrity": "sha512-XnFFzJnTfdaDqeiF/ZAUjpkoKEM8UKwHijQXuqpLiM42kuJCawytP/rYAMDYNNaWww/PTaI0rIoG4oUjRrRlnA==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.5",
"@formatjs/fast-memoize": "2.2.4",
"@formatjs/icu-messageformat-parser": "2.9.5",
"tslib": "2"
}
},
"node_modules/is-array-buffer": { "node_modules/is-array-buffer": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
@ -4721,6 +4833,37 @@
"react": "^19.0.0" "react": "^19.0.0"
} }
}, },
"node_modules/react-intl": {
"version": "7.0.2",
"resolved": "https://registry.npmjs.org/react-intl/-/react-intl-7.0.2.tgz",
"integrity": "sha512-6WDHf6vHgCvoJLFRhAMbLfIMaAeHnjBuJrYbV/0BS9K6lxetlDUWlCy/yKAtlZyUgxUXxMwxm1zbTJN+vHfEfQ==",
"license": "BSD-3-Clause",
"dependencies": {
"@formatjs/ecma402-abstract": "2.2.5",
"@formatjs/icu-messageformat-parser": "2.9.5",
"@formatjs/intl": "3.0.2",
"@types/hoist-non-react-statics": "3",
"@types/react": "16 || 17 || 18 || 19",
"hoist-non-react-statics": "3",
"intl-messageformat": "10.7.8",
"tslib": "2"
},
"peerDependencies": {
"react": "^16.6.0 || 17 || 18 || 19",
"typescript": "5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
"integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==",
"license": "MIT"
},
"node_modules/react-spinners": { "node_modules/react-spinners": {
"version": "0.15.0", "version": "0.15.0",
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz", "resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz",
@ -5174,7 +5317,6 @@
"version": "2.8.1", "version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD" "license": "0BSD"
}, },
"node_modules/type-check": { "node_modules/type-check": {
@ -5272,7 +5414,7 @@
"version": "5.7.2", "version": "5.7.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
"dev": true, "devOptional": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",

View file

@ -19,11 +19,13 @@
"react": "^19.0.0", "react": "^19.0.0",
"react-blurhash": "^0.3.0", "react-blurhash": "^0.3.0",
"react-dom": "^19.0.0", "react-dom": "^19.0.0",
"react-intl": "^7.0.2",
"react-spinners": "^0.15.0", "react-spinners": "^0.15.0",
"unhomoglyph": "^1.0.6" "unhomoglyph": "^1.0.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.11.1", "@eslint/js": "^9.11.1",
"@swc/plugin-formatjs": "^2.0.1",
"@types/katex": "^0.16.7", "@types/katex": "^0.16.7",
"@types/leaflet": "^1.9.14", "@types/leaflet": "^1.9.14",
"@types/react": "^19.0.0", "@types/react": "^19.0.0",

View file

@ -14,6 +14,7 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { useEffect, useLayoutEffect, useMemo } from "react" import { useEffect, useLayoutEffect, useMemo } from "react"
import { IntlProvider } from "react-intl"
import { ScaleLoader } from "react-spinners" import { ScaleLoader } from "react-spinners"
import Client from "./api/client.ts" import Client from "./api/client.ts"
import RPCClient from "./api/rpc.ts" import RPCClient from "./api/rpc.ts"
@ -84,10 +85,12 @@ function App() {
return <VerificationScreen client={client} clientState={clientState}/> return <VerificationScreen client={client} clientState={clientState}/>
} else { } else {
return <ClientContext value={client}> return <ClientContext value={client}>
<LightboxWrapper> <IntlProvider locale="en">
<MainScreen/> <LightboxWrapper>
</LightboxWrapper> <MainScreen/>
{errorOverlay} </LightboxWrapper>
{errorOverlay}
</IntlProvider>
</ClientContext> </ClientContext>
} }
} }

View file

@ -14,14 +14,14 @@
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { Fragment, JSX } from "react" import { Fragment, JSX } from "react"
import { FormattedList } from "react-intl"
import { ACLEventContent } from "@/api/types" import { ACLEventContent } from "@/api/types"
import { listDiff } from "@/util/diff.ts" import { listDiff } from "@/util/diff.ts"
import { humanJoinReact, joinReact } from "@/util/reactjoin.tsx"
import { ensureArray, ensureStringArray } from "@/util/validation.ts" import { ensureArray, ensureStringArray } from "@/util/validation.ts"
import EventContentProps from "./props.ts" import EventContentProps from "./props.ts"
function joinServers(arr: string[]): JSX.Element[] { function joinServers(arr: string[]): JSX.Element {
return humanJoinReact(arr.map(item => <code className="server-name">{item}</code>)) return <FormattedList type="conjunction" value={(arr.map(item => <code className="server-name">{item}</code>))}/>
} }
function makeACLChangeString( function makeACLChangeString(
@ -47,7 +47,7 @@ function makeACLChangeString(
<>Participating from a server using an IP literal hostname is now {newAllowIP ? "allowed" : "banned"}.</>, <>Participating from a server using an IP literal hostname is now {newAllowIP ? "allowed" : "banned"}.</>,
) )
} }
return joinReact(parts) return <FormattedList type="conjunction" value={parts}/>
} }
const ACLBody = ({ event, sender }: EventContentProps) => { const ACLBody = ({ event, sender }: EventContentProps) => {
@ -68,9 +68,9 @@ const ACLBody = ({ event, sender }: EventContentProps) => {
} }
let changeString = makeACLChangeString(addedAllow, removedAllow, addedDeny, removedDeny, prevAllowIP, newAllowIP) let changeString = makeACLChangeString(addedAllow, removedAllow, addedDeny, removedDeny, prevAllowIP, newAllowIP)
if (ensureArray(content.allow).length === 0) { if (ensureArray(content.allow).length === 0) {
changeString = [<Fragment key="yay"> changeString = <Fragment key="yay">
🎉 All servers are banned from participating! This room can no longer be used. 🎉 All servers are banned from participating! This room can no longer be used.
</Fragment>] </Fragment>
} }
return <div className="acl-body"> return <div className="acl-body">
{sender?.content.displayname ?? event.sender} changed the server ACLs: {changeString} {sender?.content.displayname ?? event.sender} changed the server ACLs: {changeString}

View file

@ -13,30 +13,35 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { IntlShape, useIntl } from "react-intl"
import { PinnedEventsContent } from "@/api/types" import { PinnedEventsContent } from "@/api/types"
import { listDiff } from "@/util/diff.ts" import { listDiff } from "@/util/diff.ts"
import { humanJoin } from "@/util/join.ts"
import EventContentProps from "./props.ts" import EventContentProps from "./props.ts"
function renderPinChanges(content: PinnedEventsContent, prevContent?: PinnedEventsContent): string { function renderPinChanges(intl: IntlShape, content: PinnedEventsContent, prevContent?: PinnedEventsContent): string {
const list = (items: ReadonlyArray<string>) => intl.formatList(items, { type: "conjunction" })
const [added, removed] = listDiff(content.pinned ?? [], prevContent?.pinned ?? []) const [added, removed] = listDiff(content.pinned ?? [], prevContent?.pinned ?? [])
if (added.length) { if (added.length || removed.length) {
if (removed.length) { const items = []
return `pinned ${humanJoin(added)} and unpinned ${humanJoin(removed)}` if (added.length) {
items.push(`pinned ${list(added)}`)
} }
return `pinned ${humanJoin(added)}` if (removed.length) {
} else if (removed.length) { items.push(`unpinned ${list(removed)}`)
return `unpinned ${humanJoin(removed)}` }
return list(items)
} else { } else {
return "sent a no-op pin event" return "sent a no-op pin event"
} }
} }
const PinnedEventsBody = ({ event, sender }: EventContentProps) => { const PinnedEventsBody = ({ event, sender }: EventContentProps) => {
const intl = useIntl()
const content = event.content as PinnedEventsContent const content = event.content as PinnedEventsContent
const prevContent = event.unsigned.prev_content as PinnedEventsContent | undefined const prevContent = event.unsigned.prev_content as PinnedEventsContent | undefined
return <div className="pinned-events-body"> return <div className="pinned-events-body">
{sender?.content.displayname ?? event.sender} {renderPinChanges(content, prevContent)} {sender?.content.displayname ?? event.sender} {renderPinChanges(intl, content, prevContent)}
</div> </div>
} }

View file

@ -13,9 +13,9 @@
// //
// You should have received a copy of the GNU Affero General Public License // You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
import { FormattedList } from "react-intl"
import { PowerLevelEventContent } from "@/api/types" import { PowerLevelEventContent } from "@/api/types"
import { objectDiff } from "@/util/diff.ts" import { objectDiff } from "@/util/diff.ts"
import { humanJoin } from "@/util/join.ts"
import EventContentProps from "./props.ts" import EventContentProps from "./props.ts"
function intDiff(messageParts: TemplateStringsArray, oldVal: number, newVal: number): string | null { function intDiff(messageParts: TemplateStringsArray, oldVal: number, newVal: number): string | null {
@ -68,7 +68,8 @@ const PowerLevelBody = ({ event, sender }: EventContentProps) => {
const content = event.content as PowerLevelEventContent const content = event.content as PowerLevelEventContent
const prevContent = event.unsigned.prev_content as PowerLevelEventContent | undefined const prevContent = event.unsigned.prev_content as PowerLevelEventContent | undefined
return <div className="power-level-body"> return <div className="power-level-body">
{sender?.content.displayname ?? event.sender} {humanJoin(renderPowerLevels(content, prevContent))} {sender?.content.displayname ?? event.sender}
<FormattedList type="conjunction" value={renderPowerLevels(content, prevContent)}/>
</div> </div>
} }

View file

@ -1,27 +0,0 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// 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 <https://www.gnu.org/licenses/>.
export function humanJoin(arr: string[], sep: string = ", ", lastSep: string = " and "): string {
if (arr.length === 0) {
return ""
}
if (arr.length === 1) {
return arr[0]
}
if (arr.length === 2) {
return arr.join(lastSep)
}
return arr.slice(0, -1).join(sep) + lastSep + arr[arr.length - 1]
}

View file

@ -1,30 +0,0 @@
// gomuks - A Matrix client written in Go.
// Copyright (C) 2024 Tulir Asokan
//
// 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 <https://www.gnu.org/licenses/>.
import { Fragment, JSX } from "react"
export function humanJoinReact(
arr: (string | JSX.Element)[],
sep: string | JSX.Element = ", ",
lastSep: string | JSX.Element = " and ",
): JSX.Element[] {
return arr.map((elem, idx) =>
<Fragment key={idx}>
{elem}
{idx < arr.length - 1 ? (idx === arr.length - 2 ? lastSep : sep) : null}
</Fragment>)
}
export const joinReact = (arr: (string | JSX.Element)[]) => humanJoinReact(arr, " ", " ")

View file

@ -7,6 +7,7 @@ const splitDeps = ["katex", "leaflet", "monaco-editor"]
export default defineConfig({ export default defineConfig({
base: "./", base: "./",
build: { build: {
target: ["esnext", "firefox128", "chrome131", "safari18"], target: ["esnext", "firefox128", "chrome131", "safari18"],
chunkSizeWarningLimit: 3500, chunkSizeWarningLimit: 3500,
rollupOptions: { rollupOptions: {
@ -36,6 +37,7 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
"@": "/src", "@": "/src",
"@formatjs/icu-messageformat-parser": "@formatjs/icu-messageformat-parser/no-parser",
}, },
}, },
server: { server: {