mirror of
https://github.com/tulir/gomuks.git
synced 2025-04-19 18:13:41 -05:00
web/intl: replace homegrown join functions with Intl
This commit is contained in:
parent
1056a319bd
commit
74a86d1342
8 changed files with 166 additions and 82 deletions
139
web/package-lock.json
generated
139
web/package-lock.json
generated
|
@ -17,6 +17,7 @@
|
|||
"react": "^19.0.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-intl": "^7.0.2",
|
||||
"react-spinners": "^0.15.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
|
@ -837,6 +838,77 @@
|
|||
"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": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz",
|
||||
|
@ -1754,6 +1826,16 @@
|
|||
"dev": true,
|
||||
"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": {
|
||||
"version": "7.0.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz",
|
||||
|
@ -1789,7 +1871,6 @@
|
|||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.0.0.tgz",
|
||||
"integrity": "sha512-MY3oPudxvMYyesqs/kW1Bh8y9VqSmf+tzqw3ae8a9DZW68pUe3zAdHeI1jc6iAysuRdACnVknHP8AhwD4/dxtg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"csstype": "^3.0.2"
|
||||
|
@ -2513,7 +2594,6 @@
|
|||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
|
||||
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/data-view-buffer": {
|
||||
|
@ -3615,6 +3695,15 @@
|
|||
"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": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz",
|
||||
|
@ -3687,6 +3776,18 @@
|
|||
"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": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
|
||||
|
@ -4721,6 +4822,37 @@
|
|||
"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": {
|
||||
"version": "0.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-spinners/-/react-spinners-0.15.0.tgz",
|
||||
|
@ -5174,7 +5306,6 @@
|
|||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
|
@ -5272,7 +5403,7 @@
|
|||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz",
|
||||
"integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==",
|
||||
"dev": true,
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0",
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
|
|
|
@ -19,6 +19,7 @@
|
|||
"react": "^19.0.0",
|
||||
"react-blurhash": "^0.3.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"react-intl": "^7.0.2",
|
||||
"react-spinners": "^0.15.0",
|
||||
"unhomoglyph": "^1.0.6"
|
||||
},
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
// 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 { useEffect, useLayoutEffect, useMemo } from "react"
|
||||
import { IntlProvider } from "react-intl"
|
||||
import { ScaleLoader } from "react-spinners"
|
||||
import Client from "./api/client.ts"
|
||||
import RPCClient from "./api/rpc.ts"
|
||||
|
@ -84,10 +85,12 @@ function App() {
|
|||
return <VerificationScreen client={client} clientState={clientState}/>
|
||||
} else {
|
||||
return <ClientContext value={client}>
|
||||
<IntlProvider locale="en">
|
||||
<LightboxWrapper>
|
||||
<MainScreen/>
|
||||
</LightboxWrapper>
|
||||
{errorOverlay}
|
||||
</IntlProvider>
|
||||
</ClientContext>
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,14 +14,14 @@
|
|||
// 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"
|
||||
import { FormattedList } from "react-intl"
|
||||
import { ACLEventContent } from "@/api/types"
|
||||
import { listDiff } from "@/util/diff.ts"
|
||||
import { humanJoinReact, joinReact } from "@/util/reactjoin.tsx"
|
||||
import { ensureArray, ensureStringArray } from "@/util/validation.ts"
|
||||
import EventContentProps from "./props.ts"
|
||||
|
||||
function joinServers(arr: string[]): JSX.Element[] {
|
||||
return humanJoinReact(arr.map(item => <code className="server-name">{item}</code>))
|
||||
function joinServers(arr: string[]): JSX.Element {
|
||||
return <FormattedList type="conjunction" value={(arr.map(item => <code className="server-name">{item}</code>))}/>
|
||||
}
|
||||
|
||||
function makeACLChangeString(
|
||||
|
@ -47,7 +47,7 @@ function makeACLChangeString(
|
|||
<>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) => {
|
||||
|
@ -68,9 +68,9 @@ const ACLBody = ({ event, sender }: EventContentProps) => {
|
|||
}
|
||||
let changeString = makeACLChangeString(addedAllow, removedAllow, addedDeny, removedDeny, prevAllowIP, newAllowIP)
|
||||
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.
|
||||
</Fragment>]
|
||||
</Fragment>
|
||||
}
|
||||
return <div className="acl-body">
|
||||
{sender?.content.displayname ?? event.sender} changed the server ACLs: {changeString}
|
||||
|
|
|
@ -13,30 +13,35 @@
|
|||
//
|
||||
// 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 { IntlShape, useIntl } from "react-intl"
|
||||
import { PinnedEventsContent } from "@/api/types"
|
||||
import { listDiff } from "@/util/diff.ts"
|
||||
import { humanJoin } from "@/util/join.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 ?? [])
|
||||
if (added.length || removed.length) {
|
||||
const items = []
|
||||
if (added.length) {
|
||||
if (removed.length) {
|
||||
return `pinned ${humanJoin(added)} and unpinned ${humanJoin(removed)}`
|
||||
items.push(`pinned ${list(added)}`)
|
||||
}
|
||||
return `pinned ${humanJoin(added)}`
|
||||
} else if (removed.length) {
|
||||
return `unpinned ${humanJoin(removed)}`
|
||||
if (removed.length) {
|
||||
items.push(`unpinned ${list(removed)}`)
|
||||
}
|
||||
return list(items)
|
||||
} else {
|
||||
return "sent a no-op pin event"
|
||||
}
|
||||
}
|
||||
|
||||
const PinnedEventsBody = ({ event, sender }: EventContentProps) => {
|
||||
|
||||
const intl = useIntl()
|
||||
const content = event.content as PinnedEventsContent
|
||||
const prevContent = event.unsigned.prev_content as PinnedEventsContent | undefined
|
||||
return <div className="pinned-events-body">
|
||||
{sender?.content.displayname ?? event.sender} {renderPinChanges(content, prevContent)}
|
||||
{sender?.content.displayname ?? event.sender} {renderPinChanges(intl, content, prevContent)}
|
||||
</div>
|
||||
}
|
||||
|
||||
|
|
|
@ -13,9 +13,9 @@
|
|||
//
|
||||
// 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 { FormattedList } from "react-intl"
|
||||
import { PowerLevelEventContent } from "@/api/types"
|
||||
import { objectDiff } from "@/util/diff.ts"
|
||||
import { humanJoin } from "@/util/join.ts"
|
||||
import EventContentProps from "./props.ts"
|
||||
|
||||
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 prevContent = event.unsigned.prev_content as PowerLevelEventContent | undefined
|
||||
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>
|
||||
}
|
||||
|
||||
|
|
|
@ -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]
|
||||
}
|
|
@ -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, " ", " ")
|
Loading…
Add table
Reference in a new issue