diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index f188010a0..000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:solid/typescript" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "plugins": [ - "@typescript-eslint", - "solid" - ], - "rules": { - "@typescript-eslint/no-non-null-asserted-optional-chain": "off", - "brace-style": [ - "error", "stroustrup" - ], - "quotes": [ - "error", "double" - ], - "indent": [ - "error", 2, { "SwitchCase": 1 } - ], - "semi": [ - "error", "always" - ], - "comma-dangle": [ - "error", "never" - ] - } -} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 000000000..cf524d7db --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,19 @@ +# Pull Request Template + +## What does this PR do? +- Example: "Adds new strings for X file" + +## Screenshots + + +## Did you test your code? + + +## Additional context + + +## Checklist +- [ ] Changes are clear, concise, and easy to review +- [ ] Code has been tested and works as intended +- [ ] Text/content changes support internationalization (i18n) +- [ ] Any new user-facing strings are properly localized diff --git a/.github/workflows/pull-request-check.yml b/.github/workflows/pull-request-check.yml new file mode 100644 index 000000000..edbd18b99 --- /dev/null +++ b/.github/workflows/pull-request-check.yml @@ -0,0 +1,60 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: pull-request-check + +# This workflow can be used for pushing to the production server and will only be ran when code is pushed to the main branch + +on: + pull_request: + branches: [main] + +concurrency: + # github.workflow: name of the workflow + # github.event.pull_request.number || github.ref: pull request number or branch name if not a pull request + group: ${{ github.workflow }}-${{ github.ref }} + + # Cancel in-progress runs when a new workflow with the same group name is triggered + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + container: node:24 + steps: + - uses: actions/checkout@v2 + - name: Install Node.js dependencies + run: | + npm i -g pnpm + pnpm i + + - name: Get short SHA + id: slug + run: | + git config --global --add safe.directory /__w/nerimity-web/nerimity-web + echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT + + - name: Build App + run: pnpm run build + env: + VITE_SERVER_URL: "https://nerimity.com" + VITE_APP_URL: "https://latest.nerimity.com" + VITE_MOBILE_WIDTH: 850 + VITE_TURNSTILE_SITEKEY: "0x4AAAAAAABO1ilip_YaVHJk" + VITE_APP_VERSION: ${{ steps.slug.outputs.sha_short }} + VITE_EMOJI_URL: "https://nerimity.com/twemojis/" + VITE_NERIMITY_CDN: "https://cdn.nerimity.com/" + VITE_GOOGLE_CLIENT_ID: "833085928210-2ksk1asgbmqvsg6jb3es4asnmug2a4iu.apps.googleusercontent.com" + VITE_GOOGLE_API_KEY: "AIzaSyAPeozJV7itoZk9Fk1VYbFCDMMXB-gU38M" + + - name: Zip The Build + uses: vimtor/action-zip@v1 + with: + files: dist/ + dest: dangerous-chat-client.zip + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: dangerous-client-build + path: dangerous-chat-client.zip + retention-days: 5 diff --git a/.github/workflows/push-build.yml b/.github/workflows/push-build.yml index 3bbddc3f4..3c4afca84 100644 --- a/.github/workflows/push-build.yml +++ b/.github/workflows/push-build.yml @@ -20,9 +20,15 @@ concurrency: jobs: build: runs-on: ubuntu-latest - container: node:20 + container: node:24 steps: - uses: actions/checkout@v2 + + - name: Get the date + id: get_date + run: | + echo "RELEASE_TIMESTAMP=$(date +%s%3N)" >> $GITHUB_OUTPUT + - name: Install Node.js dependencies run: | npm i -g pnpm @@ -41,6 +47,7 @@ jobs: VITE_APP_URL: "https://latest.nerimity.com" VITE_MOBILE_WIDTH: 850 VITE_TURNSTILE_SITEKEY: "0x4AAAAAAABO1ilip_YaVHJk" + VITE_RELEASE_TIMESTAMP: ${{ steps.get_date.outputs.RELEASE_TIMESTAMP }} VITE_APP_VERSION: ${{ steps.slug.outputs.sha_short }} VITE_EMOJI_URL: "https://nerimity.com/twemojis/" VITE_NERIMITY_CDN: "https://cdn.nerimity.com/" @@ -70,5 +77,6 @@ jobs: password: ${{ secrets.PASS }} port: ${{ secrets.PORT }} script: | + find /var/www/latest.nerimity.com/dist/assets -type f -mtime +7 -delete unzip -o -DD /var/www/latest-chat-client.zip -d /var/www/latest.nerimity.com/dist rm -rf /var/www/latest-chat-client.zip diff --git a/.github/workflows/tag-release.yml b/.github/workflows/tag-release.yml index 21f0a1f89..bf0cb8227 100644 --- a/.github/workflows/tag-release.yml +++ b/.github/workflows/tag-release.yml @@ -21,13 +21,15 @@ concurrency: jobs: build: runs-on: ubuntu-latest - container: node:20 + container: node:24 steps: - uses: actions/checkout@v2 - name: Get the version id: get_version - run: echo "VERSION=$(echo $GITHUB_REF | cut -d / -f 3)" >> $GITHUB_OUTPUT + run: | + echo "VERSION=$(echo $GITHUB_REF | cut -d / -f 3)" >> $GITHUB_OUTPUT + echo "RELEASE_TIMESTAMP=$(date +%s%3N)" >> $GITHUB_OUTPUT - name: Install Node.js dependencies run: | @@ -39,6 +41,7 @@ jobs: env: VITE_SERVER_URL: "https://nerimity.com" VITE_APP_URL: "https://nerimity.com" + VITE_RELEASE_TIMESTAMP: ${{ steps.get_version.outputs.RELEASE_TIMESTAMP }} VITE_APP_VERSION: ${{ steps.get_version.outputs.VERSION }} VITE_MOBILE_WIDTH: 850 VITE_TURNSTILE_SITEKEY: "0x4AAAAAAABO1ilip_YaVHJk" @@ -70,6 +73,7 @@ jobs: password: ${{ secrets.PASS }} port: ${{ secrets.PORT }} script: | + find /var/www/nerimity.com/dist/assets -type f -mtime +7 -delete unzip -o -DD /var/www/chat-client-${{ steps.get_version.outputs.VERSION }}.zip -d /var/www/nerimity.com/dist rm -rf /var/www/chat-client-${{ steps.get_version.outputs.VERSION }}.zip diff --git a/.gitignore b/.gitignore index 662a9bd3d..9924712a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ node_modules dist .env -dev-dist \ No newline at end of file +dev-dist +.DS_Store diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 000000000..8d28f3420 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,3 @@ +src/locales +src/emoji +src/highlight-js-parser/*.json \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 000000000..ea898c110 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": true, + "singleQuote": false, + "jsxSingleQuote": false, + "trailingComma": "none", + "tabWidth": 2, + "useTabs": false +} diff --git a/.vscode/settings.json b/.vscode/settings.json index 2479efe1d..911783713 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,23 +1,23 @@ { "workbench.colorCustomizations": { - "activityBar.activeBackground": "#2a8d44", - "activityBar.activeBorder": "#5333ac", - "activityBar.background": "#2a8d44", - "activityBar.foreground": "#e7e7e7", - "activityBar.inactiveForeground": "#e7e7e799", - "activityBarBadge.background": "#5333ac", - "activityBarBadge.foreground": "#e7e7e7", - "sash.hoverBorder": "#2a8d44", - "statusBar.background": "#1e6631", - "statusBar.foreground": "#e7e7e7", - "statusBarItem.hoverBackground": "#2a8d44", - "statusBarItem.remoteBackground": "#1e6631", - "statusBarItem.remoteForeground": "#e7e7e7", - "titleBar.activeBackground": "#1e6631", - "titleBar.activeForeground": "#e7e7e7", - "titleBar.inactiveBackground": "#1e663199", - "titleBar.inactiveForeground": "#e7e7e799", - "commandCenter.border": "#e7e7e799" + // "activityBar.activeBackground": "#2a8d44", + // "activityBar.activeBorder": "#5333ac", + // "activityBar.background": "#2a8d44", + // "activityBar.foreground": "#e7e7e7", + // "activityBar.inactiveForeground": "#e7e7e799", + // "activityBarBadge.background": "#5333ac", + // "activityBarBadge.foreground": "#e7e7e7", + // "sash.hoverBorder": "#2a8d44", + // "statusBar.background": "#1e6631", + // "statusBar.foreground": "#e7e7e7", + // "statusBarItem.hoverBackground": "#2a8d44", + // "statusBarItem.remoteBackground": "#1e6631", + // "statusBarItem.remoteForeground": "#e7e7e7", + // "titleBar.activeBackground": "#1e6631", + // "titleBar.activeForeground": "#e7e7e7", + // "titleBar.inactiveBackground": "#1e663199", + // "titleBar.inactiveForeground": "#e7e7e799", + // "commandCenter.border": "#e7e7e799" }, "peacock.color": "#1e6631", "editor.tabSize": 2, @@ -30,14 +30,21 @@ "typescriptreact": "html", }, "cSpell.words": [ + "autorenew", "Coloris", "gapi", + "GIFS", "hljs", + "KLIPY", "mbarzda", "Nerimity", + "repost", + "Reposted", + "reposts", "Shortcode", "Shortcodes", "solidjs", + "twttr", "unfollow", "webp" ], diff --git a/README.md b/README.md index ddd88c6e5..cb6b7f68a 100644 --- a/README.md +++ b/README.md @@ -2,61 +2,89 @@

- - - -

+ + + + +

# Nerimity Web (SolidJS) + [![Nerimity](https://raw.githubusercontent.com/Nerimity/assets/main/src/nerimity-badge-88x31.png)](https://nerimity.com) [![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/Y8Y1FN57Z) Chat App made using SolidJS. +## 🗂️ Repos -## Repos - Nerimity Web - Frontend (You Are Here) -- [Nerimity Server - Backend](https://github.com/Supertigerr/chat-server) +- [Nerimity Server - Backend](https://github.com/Nerimity/nerimity-server) -## Setup +## 🔧 Setup -* Fork the repo -* duplicate and rename `example.env` to `.env` -* Run `pnpm i` and `pnpm run dev` -* Go to http://local.nerimity.com:3000 +- Fork the repo +- duplicate and rename `example.env` to `.env` +- Run `pnpm i` and `pnpm run dev` +- Go to http://local.nerimity.com:3000 -## Features Checklist: +## ✨ Features -### Planned Features: -- [ ] Explore Themes +### App: -### Completed Features: -- [x] Explore Servers -- [x] Notification Sounds +- [x] Upload Files - [x] Re-organize Servers -- [x] Emojis -- [x] Markdown -- [x] Join/Leave/Kick/Ban Messages -- [x] Edit Messages -- [x] Kick/Ban Users From Servers +- [x] App Settings +- [x] Changelog +- [x] Notification Sounds +- [x] Mute Notifications +- [x] Push Notifications +- [x] Desktop App + +### User: + - [x] Login & Register +- [x] Update Account - [x] Friends System +- [x] Block Users - [x] DM & Server Channels - [x] DM & Server Notifications - [x] User Presence (Online, Offline, etc...) -- [x] Delete Server -- [x] Delete Server Channels -- [x] Update Server Channels -- [x] Create New Server Channels -- [x] Update Account -- [x] Changelog -- [x] Block Users -- [x] Mentions -- [x] Custom Emojis -- [x] Mute Notifications -- [x] Desktop App -- [x] App Settings - [x] Delete Account + +### Messaging: + +- [x] Mentions +- [x] Edit Messages +- [x] Markdown - [x] Embeds -- [x] Upload Files -- [x] Push Notifications +- [x] Emoji +- [x] Custom Emoji + +### Servers: + +- [x] Kick/Ban Users From Servers +- [x] Join/Leave/Kick/Ban Messages +- [x] Create New Server Channels +- [x] Update Server Channels +- [x] Delete Server Channels +- [x] Delete Server + + +### Explore: + +- [x] Explore Servers +- [x] Explore Bots +- [x] Explore Themes + +## 🌍 Translations + +We use **Weblate** for managing all translations. + +If you would like to help translate this project, please visit our Weblate page: https://hosted.weblate.org/projects/nerimity/ + +[![Translation status](https://hosted.weblate.org/widget/nerimity/nerimity-web/multi-auto.svg)](https://hosted.weblate.org/engage/nerimity/) + +## 🤝 Contributions + +- **Focus:** Each PR must contain small, easy to understand changes. +- **Large Features:** For any significant changes, please DM me first or open an issue. diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 000000000..1380d8acf --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,55 @@ +import typescriptEslint from "@typescript-eslint/eslint-plugin"; +import solid from "eslint-plugin-solid/configs/typescript"; +import globals from "globals"; +import tsParser from "@typescript-eslint/parser"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import js from "@eslint/js"; +import { FlatCompat } from "@eslint/eslintrc"; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +export default [ + ...compat.extends( + "eslint:recommended", + "plugin:@typescript-eslint/recommended" + ), + { + plugins: { + "@typescript-eslint": typescriptEslint + }, + + languageOptions: { + globals: { + ...globals.browser + }, + + parser: tsParser, + ecmaVersion: "latest", + sourceType: "module" + }, + + rules: { + "@typescript-eslint/no-non-null-asserted-optional-chain": "off", + "@typescript-eslint/no-unused-vars": [ + "warn", + { + enableAutofixRemoval: { imports: true } + } + ], + "brace-style": "off", + quotes: ["error", "double", { avoidEscape: true }], + indent: "off", + + semi: ["error", "always"], + "comma-dangle": "off" + } + }, + solid +]; diff --git a/example.env b/example.env index fb5002e86..05213cad8 100644 --- a/example.env +++ b/example.env @@ -1,4 +1,5 @@ VITE_SERVER_URL="https://nerimity.com" +VITE_WS_URL="https://nerimity.com" VITE_APP_URL="http://local.nerimity.com" VITE_MOBILE_WIDTH=850 VITE_DEV_MODE="true" diff --git a/index.html b/index.html index ea03ab0ed..6502f11be 100644 --- a/index.html +++ b/index.html @@ -1,4 +1,4 @@ - + @@ -15,24 +15,20 @@ + - - + @@ -40,17 +36,4 @@ - diff --git a/package.json b/package.json index f6090c36c..a2e4c2334 100644 --- a/package.json +++ b/package.json @@ -11,69 +11,89 @@ }, "license": "MIT", "devDependencies": { - "@maxim_mazurok/gapi.client.drive-v3": "^0.0.20230927", - "@types/chroma-js": "^2.4.4", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "^9.39.4", + "@maxim_mazurok/gapi.client.drive-v3": "^0.2.20260311", + "@types/chroma-js": "^3.1.2", "@types/croppie": "^2.6.4", - "@types/gapi": "^0.0.45", - "@types/markdown-it": "^13.0.9", - "@types/markdown-it-emoji": "^2.0.5", - "@types/node": "^20.16.5", - "@types/simple-peer": "^9.11.8", + "@types/gapi": "^0.0.47", + "@types/markdown-it": "^14.1.2", + "@types/markdown-it-emoji": "^3.0.1", + "@types/node": "^22.19.19", + "@types/simple-peer": "^9.11.9", "@types/uzip": "^0.20201231.2", "@types/voice-activity-detection": "^0.0.5", - "@typescript-eslint/eslint-plugin": "^7.18.0", - "@typescript-eslint/parser": "^7.18.0", - "eslint": "^9.10.0", - "eslint-plugin-solid": "^0.14.3", + "@typescript-eslint/eslint-plugin": "^8.59.4", + "@typescript-eslint/parser": "^8.59.4", + "eslint": "^9.39.4", + "eslint-plugin-solid": "^0.14.5", "events": "^3.3.0", "global": "^4.4.0", + "globals": "^15.15.0", + "js-sha256": "^0.11.1", + "magic-string": "^0.30.21", "process": "^0.11.10", - "typescript": "^5.5.4", - "vite": "^5.4.3", - "vite-plugin-solid": "^2.10.2" + "sass-embedded": "^1.99.0", + "typescript": "^6.0.3", + "vite": "^8.0.14", + "vite-plugin-solid": "^2.11.12" }, "dependencies": { - "@codemirror/autocomplete": "^6.18.0", - "@codemirror/commands": "^6.6.1", - "@codemirror/lang-css": "^6.3.0", - "@codemirror/language": "^6.10.2", - "@codemirror/state": "^6.4.1", - "@codemirror/view": "^6.33.0", - "@material-symbols/font-400": "^0.21.3", - "@mbarzda/solid-i18next": "^1.4.1", - "@melloware/coloris": "^0.24.0", - "@nerimity/nevula": "^0.14.0", - "@nerimity/solid-emoji-picker": "^0.4.8", + "@codemirror/autocomplete": "^6.20.2", + "@codemirror/commands": "^6.10.3", + "@codemirror/lang-css": "^6.3.1", + "@codemirror/language": "^6.12.3", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.43.0", + "@fontsource/dancing-script": "^5.2.8", + "@fontsource/finger-paint": "^5.2.8", + "@fontsource/grandstander": "^5.2.7", + "@fontsource/ibm-plex-mono": "^5.2.7", + "@fontsource/indie-flower": "^5.2.7", + "@fontsource/inter": "^5.2.8", + "@fontsource/mochiy-pop-one": "^5.2.8", + "@fontsource/pixelify-sans": "^5.2.7", + "@fontsource/roboto-slab": "^5.2.8", + "@fontsource/sora": "^5.2.8", + "@formatjs/intl-durationformat": "^0.10.12", + "@jridgewell/trace-mapping": "^0.3.31", + "@melloware/coloris": "^0.25.0", + "@nerimity/i18lite": "^1.2.0", + "@nerimity/nevula": "^0.15.0", + "@nerimity/solid-emoji-picker": "^0.4.9", + "@nerimity/solid-i18lite": "^1.8.1", "@nerimity/solid-opus-media-recorder": "^1.0.1", "@nerimity/solid-turnstile": "^1.1.0", - "@solid-primitives/context": "^0.2.3", - "@solid-primitives/keyed": "^1.2.2", + "@sentry/solid": "^10.53.1", + "@solid-primitives/context": "^0.3.2", + "@solid-primitives/keyed": "^1.5.3", "@solidjs/meta": "^0.29.4", - "@thaunknown/simple-peer": "^10.0.10", - "autoprefixer": "^10.4.20", - "chroma-js": "^3.1.1", + "@thaunknown/simple-peer": "^10.1.0", + "autoprefixer": "^10.5.0", + "chroma-js": "^3.2.0", "croppie": "^2.6.5", - "eventemitter3": "^5.0.1", - "highlight.js": "^11.10.0", + "eruda": "^3.4.3", + "eventemitter3": "^5.0.4", + "fzstd": "^0.1.1", + "highlight.js": "^11.11.1", "html-parse-string": "^0.0.9", - "i18next": "^23.14.0", - "idb-keyval": "^6.2.1", - "konva": "^9.3.14", - "markdown-it": "^13.0.2", - "markdown-it-emoji": "^2.0.2", - "match-sorter": "^6.3.4", - "sass": "^1.78.0", - "socket.io-client": "^4.7.5", - "solid-codemirror": "^2.3.1", - "solid-js": "^1.8.22", - "solid-navigator": "^0.3.14", - "solid-sortablejs": "^2.1.2", + "idb-keyval": "^6.2.4", + "konva": "^10.3.0", + "markdown-it": "^14.1.1", + "markdown-it-emoji": "^3.0.0", + "match-sorter": "^8.3.0", + "postcss-nested": "^7.0.2", + "socket.io-client": "^4.8.3", + "solid-codemirror": "^2.3.3", + "solid-js": "^1.9.13", + "solid-navigator": "^0.4.1", + "solid-sortablejs": "^2.1.8", "solid-styled-components": "^0.28.5", + "temporal-polyfill": "^0.3.2", "thememirror": "^2.0.1", - "twemoji": "^14.0.2", "uzip": "^0.20201231.0", "voice-activity-detection": "^0.0.5", - "zoomist": "^2.1.1" + "zoomist": "^2.2.0" }, "engines": { "npm": "please-use-pnpm", diff --git a/patches/sortablejs.patch b/patches/sortablejs.patch new file mode 100644 index 000000000..ff292328d --- /dev/null +++ b/patches/sortablejs.patch @@ -0,0 +1,48 @@ +diff --git a/modular/sortable.esm.js b/modular/sortable.esm.js +index 824d48148fc5b4694d10f953dcc34f1f56b4de54..d8519b78421f33b9df1b42ac5e8a6d184cfb2a2c 100644 +--- a/modular/sortable.esm.js ++++ b/modular/sortable.esm.js +@@ -896,7 +896,6 @@ var dragEl, + activeGroup, + putSortable, + awaitingDragStarted = false, +- ignoreNextClick = false, + sortables = [], + tapEvt, + touchEvt, +@@ -1028,18 +1027,7 @@ var documentExists = typeof document !== 'undefined', + } + }; + +-// #1184 fix - Prevent click event on fallback if dragged but item not changed position +-if (documentExists && !ChromeForAndroid) { +- document.addEventListener('click', function (evt) { +- if (ignoreNextClick) { +- evt.preventDefault(); +- evt.stopPropagation && evt.stopPropagation(); +- evt.stopImmediatePropagation && evt.stopImmediatePropagation(); +- ignoreNextClick = false; +- return false; +- } +- }, true); +-} ++ + var nearestEmptyInsertDetectEvent = function nearestEmptyInsertDetectEvent(evt) { + if (dragEl) { + evt = evt.touches ? evt.touches[0] : evt; +@@ -1611,7 +1599,6 @@ Sortable.prototype = /** @lends Sortable.prototype */{ + + // Set proper drop events + if (fallback) { +- ignoreNextClick = true; + _this._loopId = setInterval(_this._emulateDragOver, 50); + } else { + // Undo what was set in _prepareDragStart before drag started +@@ -1756,7 +1743,6 @@ Sortable.prototype = /** @lends Sortable.prototype */{ + if (dragEl.contains(evt.target) || target.animated && target.animatingX && target.animatingY || _this._ignoreWhileAnimating === target) { + return completed(false); + } +- ignoreNextClick = false; + if (activeSortable && !options.disabled && (isOwner ? canSort || (revert = parentEl !== rootEl) // Reverting item into the original list + : putSortable === this || (this.lastPutMode = activeGroup.checkPull(this, activeSortable, dragEl, evt)) && group.checkPut(this, activeSortable, dragEl, evt))) { + vertical = this._getDirection(evt, target) === 'vertical'; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 8e8c041d2..4490120e2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,124 +4,169 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +patchedDependencies: + sortablejs: 2cd8457c177f4d9fbcc7edf39464d4e76e261c69f33a5431ca6af76bdfd29786 + importers: .: dependencies: '@codemirror/autocomplete': - specifier: ^6.18.0 - version: 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1) + specifier: ^6.20.2 + version: 6.20.2 '@codemirror/commands': - specifier: ^6.6.1 - version: 6.6.1 + specifier: ^6.10.3 + version: 6.10.3 '@codemirror/lang-css': - specifier: ^6.3.0 - version: 6.3.0(@codemirror/view@6.33.0) + specifier: ^6.3.1 + version: 6.3.1 '@codemirror/language': - specifier: ^6.10.2 - version: 6.10.2 + specifier: ^6.12.3 + version: 6.12.3 '@codemirror/state': - specifier: ^6.4.1 - version: 6.4.1 + specifier: ^6.6.0 + version: 6.6.0 '@codemirror/view': - specifier: ^6.33.0 - version: 6.33.0 - '@material-symbols/font-400': - specifier: ^0.21.3 - version: 0.21.3 - '@mbarzda/solid-i18next': - specifier: ^1.4.1 - version: 1.4.1(html-parse-string@0.0.9)(i18next@23.14.0)(solid-js@1.8.22) + specifier: ^6.43.0 + version: 6.43.0 + '@fontsource/dancing-script': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/finger-paint': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/grandstander': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/ibm-plex-mono': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/indie-flower': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/inter': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/mochiy-pop-one': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/pixelify-sans': + specifier: ^5.2.7 + version: 5.2.7 + '@fontsource/roboto-slab': + specifier: ^5.2.8 + version: 5.2.8 + '@fontsource/sora': + specifier: ^5.2.8 + version: 5.2.8 + '@formatjs/intl-durationformat': + specifier: ^0.10.12 + version: 0.10.12 + '@jridgewell/trace-mapping': + specifier: ^0.3.31 + version: 0.3.31 '@melloware/coloris': - specifier: ^0.24.0 - version: 0.24.0 + specifier: ^0.25.0 + version: 0.25.0 + '@nerimity/i18lite': + specifier: ^1.2.0 + version: 1.2.0(html-parse-string@0.0.9)(solid-js@1.9.13) '@nerimity/nevula': - specifier: ^0.14.0 - version: 0.14.0 + specifier: ^0.15.0 + version: 0.15.0 '@nerimity/solid-emoji-picker': - specifier: ^0.4.8 - version: 0.4.8(solid-js@1.8.22) + specifier: ^0.4.9 + version: 0.4.9(solid-js@1.9.13) + '@nerimity/solid-i18lite': + specifier: ^1.8.1 + version: 1.8.1(@nerimity/i18lite@1.2.0(html-parse-string@0.0.9)(solid-js@1.9.13))(html-parse-string@0.0.9)(solid-js@1.9.13) '@nerimity/solid-opus-media-recorder': specifier: ^1.0.1 - version: 1.0.1(solid-js@1.8.22) + version: 1.0.1(solid-js@1.9.13) '@nerimity/solid-turnstile': specifier: ^1.1.0 - version: 1.1.0(solid-js@1.8.22) + version: 1.1.0(solid-js@1.9.13) + '@sentry/solid': + specifier: ^10.53.1 + version: 10.53.1(solid-js@1.9.13) '@solid-primitives/context': - specifier: ^0.2.3 - version: 0.2.3(solid-js@1.8.22) + specifier: ^0.3.2 + version: 0.3.2(solid-js@1.9.13) '@solid-primitives/keyed': - specifier: ^1.2.2 - version: 1.2.2(solid-js@1.8.22) + specifier: ^1.5.3 + version: 1.5.3(solid-js@1.9.13) '@solidjs/meta': specifier: ^0.29.4 - version: 0.29.4(solid-js@1.8.22) + version: 0.29.4(solid-js@1.9.13) '@thaunknown/simple-peer': - specifier: ^10.0.10 - version: 10.0.10 + specifier: ^10.1.0 + version: 10.1.0 autoprefixer: - specifier: ^10.4.20 - version: 10.4.20(postcss@8.4.45) + specifier: ^10.5.0 + version: 10.5.0(postcss@8.5.15) chroma-js: - specifier: ^3.1.1 - version: 3.1.1 + specifier: ^3.2.0 + version: 3.2.0 croppie: specifier: ^2.6.5 version: 2.6.5 + eruda: + specifier: ^3.4.3 + version: 3.4.3 eventemitter3: - specifier: ^5.0.1 - version: 5.0.1 + specifier: ^5.0.4 + version: 5.0.4 + fzstd: + specifier: ^0.1.1 + version: 0.1.1 highlight.js: - specifier: ^11.10.0 - version: 11.10.0 + specifier: ^11.11.1 + version: 11.11.1 html-parse-string: specifier: ^0.0.9 version: 0.0.9 - i18next: - specifier: ^23.14.0 - version: 23.14.0 idb-keyval: - specifier: ^6.2.1 - version: 6.2.1 + specifier: ^6.2.4 + version: 6.2.4 konva: - specifier: ^9.3.14 - version: 9.3.14 + specifier: ^10.3.0 + version: 10.3.0 markdown-it: - specifier: ^13.0.2 - version: 13.0.2 + specifier: ^14.1.1 + version: 14.1.1 markdown-it-emoji: - specifier: ^2.0.2 - version: 2.0.2 + specifier: ^3.0.0 + version: 3.0.0 match-sorter: - specifier: ^6.3.4 - version: 6.3.4 - sass: - specifier: ^1.78.0 - version: 1.78.0 + specifier: ^8.3.0 + version: 8.3.0 + postcss-nested: + specifier: ^7.0.2 + version: 7.0.2(postcss@8.5.15) socket.io-client: - specifier: ^4.7.5 - version: 4.7.5 + specifier: ^4.8.3 + version: 4.8.3 solid-codemirror: - specifier: ^2.3.1 - version: 2.3.1(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(solid-js@1.8.22) + specifier: ^2.3.3 + version: 2.3.3(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(solid-js@1.9.13) solid-js: - specifier: ^1.8.22 - version: 1.8.22 + specifier: ^1.9.13 + version: 1.9.13 solid-navigator: - specifier: ^0.3.14 - version: 0.3.14(solid-js@1.8.22) + specifier: ^0.4.1 + version: 0.4.1(solid-js@1.9.13) solid-sortablejs: - specifier: ^2.1.2 - version: 2.1.2(solid-js@1.8.22) + specifier: ^2.1.8 + version: 2.1.8(solid-js@1.9.13) solid-styled-components: specifier: ^0.28.5 - version: 0.28.5(solid-js@1.8.22) + version: 0.28.5(solid-js@1.9.13) + temporal-polyfill: + specifier: ^0.3.2 + version: 0.3.2 thememirror: specifier: ^2.0.1 - version: 2.0.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0) - twemoji: - specifier: ^14.0.2 - version: 14.0.2 + version: 2.0.1(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0) uzip: specifier: ^0.20201231.0 version: 0.20201231.0 @@ -129,33 +174,39 @@ importers: specifier: ^0.0.5 version: 0.0.5 zoomist: - specifier: ^2.1.1 - version: 2.1.1 + specifier: ^2.2.0 + version: 2.2.0 devDependencies: + '@eslint/eslintrc': + specifier: ^3.3.5 + version: 3.3.5 + '@eslint/js': + specifier: ^9.39.4 + version: 9.39.4 '@maxim_mazurok/gapi.client.drive-v3': - specifier: ^0.0.20230927 - version: 0.0.20230927 + specifier: ^0.2.20260311 + version: 0.2.20260311 '@types/chroma-js': - specifier: ^2.4.4 - version: 2.4.4 + specifier: ^3.1.2 + version: 3.1.2 '@types/croppie': specifier: ^2.6.4 version: 2.6.4 '@types/gapi': - specifier: ^0.0.45 - version: 0.0.45 + specifier: ^0.0.47 + version: 0.0.47 '@types/markdown-it': - specifier: ^13.0.9 - version: 13.0.9 + specifier: ^14.1.2 + version: 14.1.2 '@types/markdown-it-emoji': - specifier: ^2.0.5 - version: 2.0.5 + specifier: ^3.0.1 + version: 3.0.1 '@types/node': - specifier: ^20.16.5 - version: 20.16.5 + specifier: ^22.19.19 + version: 22.19.19 '@types/simple-peer': - specifier: ^9.11.8 - version: 9.11.8 + specifier: ^9.11.9 + version: 9.11.9 '@types/uzip': specifier: ^0.20201231.2 version: 0.20201231.2 @@ -163,455 +214,342 @@ importers: specifier: ^0.0.5 version: 0.0.5 '@typescript-eslint/eslint-plugin': - specifier: ^7.18.0 - version: 7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0)(typescript@5.5.4))(eslint@9.10.0)(typescript@5.5.4) + specifier: ^8.59.4 + version: 8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3) '@typescript-eslint/parser': - specifier: ^7.18.0 - version: 7.18.0(eslint@9.10.0)(typescript@5.5.4) + specifier: ^8.59.4 + version: 8.59.4(eslint@9.39.4)(typescript@6.0.3) eslint: - specifier: ^9.10.0 - version: 9.10.0 + specifier: ^9.39.4 + version: 9.39.4 eslint-plugin-solid: - specifier: ^0.14.3 - version: 0.14.3(eslint@9.10.0)(typescript@5.5.4) + specifier: ^0.14.5 + version: 0.14.5(eslint@9.39.4)(typescript@6.0.3) events: specifier: ^3.3.0 version: 3.3.0 global: specifier: ^4.4.0 version: 4.4.0 + globals: + specifier: ^15.15.0 + version: 15.15.0 + js-sha256: + specifier: ^0.11.1 + version: 0.11.1 + magic-string: + specifier: ^0.30.21 + version: 0.30.21 process: specifier: ^0.11.10 version: 0.11.10 + sass-embedded: + specifier: ^1.99.0 + version: 1.99.0 typescript: - specifier: ^5.5.4 - version: 5.5.4 + specifier: ^6.0.3 + version: 6.0.3 vite: - specifier: ^5.4.3 - version: 5.4.3(@types/node@20.16.5)(sass@1.78.0) + specifier: ^8.0.14 + version: 8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0) vite-plugin-solid: - specifier: ^2.10.2 - version: 2.10.2(solid-js@1.8.22)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)) + specifier: ^2.11.12 + version: 2.11.12(solid-js@1.9.13)(vite@8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0)) packages: - '@ampproject/remapping@2.3.0': - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@babel/code-frame@7.29.0': + resolution: {integrity: sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==} + engines: {node: '>=6.9.0'} - '@babel/code-frame@7.24.7': - resolution: {integrity: sha512-BcYH1CVJBO9tvyIZ2jVeXgSIMvGZ2FDRvDdOIVQyuklNKSsx+eppDEBq/g47Ayw+RqNFE+URvOShmf+f/qwAlA==} + '@babel/compat-data@7.29.3': + resolution: {integrity: sha512-LIVqM46zQWZhj17qA8wb4nW/ixr2y1Nw+r1etiAWgRM6U1IqP+LNhL1yg440jYZR72jCWcWbLWzIosH+uP1fqg==} engines: {node: '>=6.9.0'} - '@babel/compat-data@7.25.4': - resolution: {integrity: sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==} + '@babel/core@7.29.0': + resolution: {integrity: sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==} engines: {node: '>=6.9.0'} - '@babel/core@7.25.2': - resolution: {integrity: sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==} + '@babel/generator@7.29.1': + resolution: {integrity: sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==} engines: {node: '>=6.9.0'} - '@babel/generator@7.25.6': - resolution: {integrity: sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==} + '@babel/helper-compilation-targets@7.28.6': + resolution: {integrity: sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==} engines: {node: '>=6.9.0'} - '@babel/helper-compilation-targets@7.25.2': - resolution: {integrity: sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==} + '@babel/helper-globals@7.28.0': + resolution: {integrity: sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==} engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.18.6': resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} engines: {node: '>=6.9.0'} - '@babel/helper-module-imports@7.24.7': - resolution: {integrity: sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==} + '@babel/helper-module-imports@7.28.6': + resolution: {integrity: sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==} engines: {node: '>=6.9.0'} - '@babel/helper-module-transforms@7.25.2': - resolution: {integrity: sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==} + '@babel/helper-module-transforms@7.28.6': + resolution: {integrity: sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 - '@babel/helper-plugin-utils@7.24.8': - resolution: {integrity: sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==} - engines: {node: '>=6.9.0'} - - '@babel/helper-simple-access@7.24.7': - resolution: {integrity: sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==} + '@babel/helper-plugin-utils@7.28.6': + resolution: {integrity: sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==} engines: {node: '>=6.9.0'} - '@babel/helper-string-parser@7.24.8': - resolution: {integrity: sha512-pO9KhhRcuUyGnJWwyEgnRJTSIZHiT+vMD0kPeD+so0l7mxkMT19g3pjY9GTnHySck/hDzq+dtW/4VgnMkippsQ==} + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-identifier@7.24.7': - resolution: {integrity: sha512-rR+PBcQ1SMQDDyF6X0wxtG8QyLCgUB0eRAGguqRLfkCA87l7yAP7ehq8SNj96OOGTO8OBV70KhuFYcIkHXOg0w==} + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} engines: {node: '>=6.9.0'} - '@babel/helper-validator-option@7.24.8': - resolution: {integrity: sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==} + '@babel/helper-validator-option@7.27.1': + resolution: {integrity: sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==} engines: {node: '>=6.9.0'} - '@babel/helpers@7.25.6': - resolution: {integrity: sha512-Xg0tn4HcfTijTwfDwYlvVCl43V6h4KyVVX2aEm4qdO/PC6L2YvzLHFdmxhoeSA3eslcE6+ZVXHgWwopXYLNq4Q==} + '@babel/helpers@7.29.2': + resolution: {integrity: sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==} engines: {node: '>=6.9.0'} - '@babel/highlight@7.24.7': - resolution: {integrity: sha512-EStJpq4OuY8xYfhGVXngigBJRWxftKX9ksiGDnmlY3o7B/V7KIAc9X4oiK87uPJSc/vs5L869bem5fhZa8caZw==} - engines: {node: '>=6.9.0'} - - '@babel/parser@7.25.6': - resolution: {integrity: sha512-trGdfBdbD0l1ZPmcJ83eNxB9rbEax4ALFTF7fN386TMYbeCQbyme5cOEXQhbGXKebwGaB/J52w1mrklMcbgy6Q==} + '@babel/parser@7.29.3': + resolution: {integrity: sha512-b3ctpQwp+PROvU/cttc4OYl4MzfJUWy6FZg+PMXfzmt/+39iHVF0sDfqay8TQM3JA2EUOyKcFZt75jWriQijsA==} engines: {node: '>=6.0.0'} hasBin: true - '@babel/plugin-syntax-jsx@7.24.7': - resolution: {integrity: sha512-6ddciUPe/mpMnOKv/U+RSd2vvVy+Yw/JfBB0ZHYjEZt9NLHmCUylNYlsbqCCS1Bffjlb0fCwC9Vqz+sBz6PsiQ==} + '@babel/plugin-syntax-jsx@7.28.6': + resolution: {integrity: sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==} engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0-0 - '@babel/runtime@7.25.6': - resolution: {integrity: sha512-VBj9MYyDb9tuLq7yzqjgzt6Q+IBQLrGZfdjOekyEirZPHxXWoTSGUTMrpsfi58Up73d13NfYLv8HT9vmznjzhQ==} + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} engines: {node: '>=6.9.0'} - '@babel/template@7.25.0': - resolution: {integrity: sha512-aOOgh1/5XzKvg1jvVz7AVrx2piJ2XBi227DHmbY6y+bM9H2FlN+IfecYu4Xl0cNiiVejlsCri89LUsbj8vJD9Q==} + '@babel/template@7.28.6': + resolution: {integrity: sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==} engines: {node: '>=6.9.0'} - '@babel/traverse@7.25.6': - resolution: {integrity: sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==} + '@babel/traverse@7.29.0': + resolution: {integrity: sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==} engines: {node: '>=6.9.0'} - '@babel/types@7.25.6': - resolution: {integrity: sha512-/l42B1qxpG6RdfYf343Uw1vmDjeNhneUXtzhojE7pDgfpEypmRhI6j1kr17XCVv4Cgl9HdAiQY2x0GwKm7rWCw==} + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} engines: {node: '>=6.9.0'} - '@changesets/apply-release-plan@7.0.5': - resolution: {integrity: sha512-1cWCk+ZshEkSVEZrm2fSj1Gz8sYvxgUL4Q78+1ZZqeqfuevPTPk033/yUZ3df8BKMohkqqHfzj0HOOrG0KtXTw==} - - '@changesets/assemble-release-plan@6.0.4': - resolution: {integrity: sha512-nqICnvmrwWj4w2x0fOhVj2QEGdlUuwVAwESrUo5HLzWMI1rE5SWfsr9ln+rDqWB6RQ2ZyaMZHUcU7/IRaUJS+Q==} - - '@changesets/changelog-git@0.2.0': - resolution: {integrity: sha512-bHOx97iFI4OClIT35Lok3sJAwM31VbUM++gnMBV16fdbtBhgYu4dxsphBF/0AZZsyAHMrnM0yFcj5gZM1py6uQ==} - - '@changesets/cli@2.27.8': - resolution: {integrity: sha512-gZNyh+LdSsI82wBSHLQ3QN5J30P4uHKJ4fXgoGwQxfXwYFTJzDdvIJasZn8rYQtmKhyQuiBj4SSnLuKlxKWq4w==} - hasBin: true + '@bufbuild/protobuf@2.12.0': + resolution: {integrity: sha512-B/XlCaFIP8LOwzo+bz5uFzATYokcwCKQcghqnlfwSmM5eX/qTkvDBnDPs+gXtX/RyjxJ4DRikECcPJbyALA8FA==} - '@changesets/config@3.0.3': - resolution: {integrity: sha512-vqgQZMyIcuIpw9nqFIpTSNyc/wgm/Lu1zKN5vECy74u95Qx/Wa9g27HdgO4NkVAaq+BGA8wUc/qvbvVNs93n6A==} + '@codemirror/autocomplete@6.20.2': + resolution: {integrity: sha512-G5FPkgIiLjOgZMjqVjvuKQ1rGPtHogLldJr33eFJdVLtmwY+giGrlv/ewljLz6b9BSQLkjxuwBc6g6omDM+YxQ==} - '@changesets/errors@0.2.0': - resolution: {integrity: sha512-6BLOQUscTpZeGljvyQXlWOItQyU71kCdGz7Pi8H8zdw6BI0g3m43iL4xKUVPWtG+qrrL9DTjpdn8eYuCQSRpow==} + '@codemirror/commands@6.10.3': + resolution: {integrity: sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==} - '@changesets/get-dependents-graph@2.1.2': - resolution: {integrity: sha512-sgcHRkiBY9i4zWYBwlVyAjEM9sAzs4wYVwJUdnbDLnVG3QwAaia1Mk5P8M7kraTOZN+vBET7n8KyB0YXCbFRLQ==} + '@codemirror/lang-css@6.3.1': + resolution: {integrity: sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==} - '@changesets/get-release-plan@4.0.4': - resolution: {integrity: sha512-SicG/S67JmPTrdcc9Vpu0wSQt7IiuN0dc8iR5VScnnTVPfIaLvKmEGRvIaF0kcn8u5ZqLbormZNTO77bCEvyWw==} + '@codemirror/language@6.12.3': + resolution: {integrity: sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==} - '@changesets/get-version-range-type@0.4.0': - resolution: {integrity: sha512-hwawtob9DryoGTpixy1D3ZXbGgJu1Rhr+ySH2PvTLHvkZuQ7sRT4oQwMh0hbqZH1weAooedEjRsbrWcGLCeyVQ==} + '@codemirror/state@6.6.0': + resolution: {integrity: sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==} - '@changesets/git@3.0.1': - resolution: {integrity: sha512-pdgHcYBLCPcLd82aRcuO0kxCDbw/yISlOtkmwmE8Odo1L6hSiZrBOsRl84eYG7DRCab/iHnOkWqExqc4wxk2LQ==} + '@codemirror/view@6.43.0': + resolution: {integrity: sha512-V7ZCLQO3Jus9hzh2jVCCPW3mO4IBMr43O37PqSUYautJSnnJF41YlgLw21x0fLJTYvJ+Vkm6Gp+qKGH9pltgXA==} - '@changesets/logger@0.1.1': - resolution: {integrity: sha512-OQtR36ZlnuTxKqoW4Sv6x5YIhOmClRd5pWsjZsddYxpWs517R0HkyiefQPIytCVh4ZcC5x9XaG8KTdd5iRQUfg==} + '@emnapi/core@1.10.0': + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} - '@changesets/parse@0.4.0': - resolution: {integrity: sha512-TS/9KG2CdGXS27S+QxbZXgr8uPsP4yNJYb4BC2/NeFUj80Rni3TeD2qwWmabymxmrLo7JEsytXH1FbpKTbvivw==} + '@emnapi/runtime@1.10.0': + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} - '@changesets/pre@2.0.1': - resolution: {integrity: sha512-vvBJ/If4jKM4tPz9JdY2kGOgWmCowUYOi5Ycv8dyLnEE8FgpYYUo1mgJZxcdtGGP3aG8rAQulGLyyXGSLkIMTQ==} + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} - '@changesets/read@0.6.1': - resolution: {integrity: sha512-jYMbyXQk3nwP25nRzQQGa1nKLY0KfoOV7VLgwucI0bUO8t8ZLCr6LZmgjXsiKuRDc+5A6doKPr9w2d+FEJ55zQ==} - - '@changesets/should-skip-package@0.1.1': - resolution: {integrity: sha512-H9LjLbF6mMHLtJIc/eHR9Na+MifJ3VxtgP/Y+XLn4BF7tDTEN1HNYtH6QMcjP1uxp9sjaFYmW8xqloaCi/ckTg==} - - '@changesets/types@4.1.0': - resolution: {integrity: sha512-LDQvVDv5Kb50ny2s25Fhm3d9QSZimsoUGBsUioj6MC3qbMUCuC8GPIvk/M6IvXx3lYhAs0lwWUQLb+VIEUCECw==} - - '@changesets/types@6.0.0': - resolution: {integrity: sha512-b1UkfNulgKoWfqyHtzKS5fOZYSJO+77adgL7DLRDr+/7jhChN+QcHnbjiQVOz/U+Ts3PGNySq7diAItzDgugfQ==} - - '@changesets/write@0.3.2': - resolution: {integrity: sha512-kDxDrPNpUgsjDbWBvUo27PzKX4gqeKOlhibaOXDJA6kuBisGqNHv/HwGJrAu8U/dSf8ZEFIeHIPtvSlZI1kULw==} - - '@codemirror/autocomplete@6.18.0': - resolution: {integrity: sha512-5DbOvBbY4qW5l57cjDsmmpDh3/TeK1vXfTHa+BUMrRzdWdcxKZ4U4V7vQaTtOpApNU4kLS4FQ6cINtLg245LXA==} + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: - '@codemirror/language': ^6.0.0 - '@codemirror/state': ^6.0.0 - '@codemirror/view': ^6.0.0 - '@lezer/common': ^1.0.0 - - '@codemirror/commands@6.6.1': - resolution: {integrity: sha512-iBfKbyIoXS1FGdsKcZmnrxmbc8VcbMrSgD7AVrsnX+WyAYjmUDWvE93dt5D874qS4CCVu4O1JpbagHdXbbLiOw==} - - '@codemirror/lang-css@6.3.0': - resolution: {integrity: sha512-CyR4rUNG9OYcXDZwMPvJdtb6PHbBDKUc/6Na2BIwZ6dKab1JQqKa4di+RNRY9Myn7JB81vayKwJeQ7jEdmNVDA==} - - '@codemirror/language@6.10.2': - resolution: {integrity: sha512-kgbTYTo0Au6dCSc/TFy7fK3fpJmgHDv1sG1KNQKJXVi+xBTEeBPY/M30YXiU6mMXeH+YIDLsbrT4ZwNRdtF+SA==} - - '@codemirror/state@6.4.1': - resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} - - '@codemirror/view@6.33.0': - resolution: {integrity: sha512-AroaR3BvnjRW8fiZBalAaK+ZzB5usGgI014YKElYZvQdNH5ZIidHlO+cyf/2rWzyBFRkvG6VhiXeAEbC53P2YQ==} - - '@esbuild/aix-ppc64@0.21.5': - resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - - '@esbuild/android-arm64@0.21.5': - resolution: {integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - - '@esbuild/android-arm@0.21.5': - resolution: {integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - - '@esbuild/android-x64@0.21.5': - resolution: {integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - - '@esbuild/darwin-arm64@0.21.5': - resolution: {integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 - '@esbuild/darwin-x64@0.21.5': - resolution: {integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} - '@esbuild/freebsd-arm64@0.21.5': - resolution: {integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/freebsd-x64@0.21.5': - resolution: {integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-arm64@0.21.5': - resolution: {integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-arm@0.21.5': - resolution: {integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-ia32@0.21.5': - resolution: {integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-loong64@0.21.5': - resolution: {integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-mips64el@0.21.5': - resolution: {integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@esbuild/linux-ppc64@0.21.5': - resolution: {integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] + '@fontsource/dancing-script@5.2.8': + resolution: {integrity: sha512-zHVVgIQ5/rAIM0VVp1QMsqvJm9INHO5/C82E9rHT6NlabjaPTFYn8wl9lT4vPWizfB0TublGQM1999kfdA+XFA==} - '@esbuild/linux-riscv64@0.21.5': - resolution: {integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] + '@fontsource/finger-paint@5.2.8': + resolution: {integrity: sha512-RUN6NFBR3HJsBRDK0+E3SdcvQMxK4N5mIsm5TzD6thVmnuaPx3MuVOHRBooqT/aEYovBErpdiBG2JzI8bl+OKA==} - '@esbuild/linux-s390x@0.21.5': - resolution: {integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] + '@fontsource/grandstander@5.2.7': + resolution: {integrity: sha512-SBO1jNrp3dBswtIy5LDg4nd8yC9oS1Thq9IvbigYxcWv411UdhiNtWRG44awoPuH9g9hs8KuCQKZ/ZBjEPIWUw==} - '@esbuild/linux-x64@0.21.5': - resolution: {integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] + '@fontsource/ibm-plex-mono@5.2.7': + resolution: {integrity: sha512-MKAb8qV+CaiMQn2B0dIi1OV3565NYzp3WN5b4oT6LTkk+F0jR6j0ZN+5BKJiIhffDC3rtBULsYZE65+0018z9w==} - '@esbuild/netbsd-x64@0.21.5': - resolution: {integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] + '@fontsource/indie-flower@5.2.7': + resolution: {integrity: sha512-vu9yEMW3Be2TXRkw2NYMLK1C4KQOUme3SUtqSha/wGzvBgzc2llT/lQ3bzZZ4aoCeF4x9ghGV+iwW+4hVr+Yhg==} - '@esbuild/openbsd-x64@0.21.5': - resolution: {integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] + '@fontsource/inter@5.2.8': + resolution: {integrity: sha512-P6r5WnJoKiNVV+zvW2xM13gNdFhAEpQ9dQJHt3naLvfg+LkF2ldgSLiF4T41lf1SQCM9QmkqPTn4TH568IRagg==} - '@esbuild/sunos-x64@0.21.5': - resolution: {integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] + '@fontsource/mochiy-pop-one@5.2.8': + resolution: {integrity: sha512-WNPX7iIlifcXKDd27o2+YeUkS2BwLIU3LupW3Sz7RLHxxl/mE3ucNuAJesJ+aL/BNLI62+J7w+e3RMoGNlzd8w==} - '@esbuild/win32-arm64@0.21.5': - resolution: {integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] + '@fontsource/pixelify-sans@5.2.7': + resolution: {integrity: sha512-F/UuV2M9poAj/BJ5/6u95mOy6ptp8/+dpfnxh4TFzKeAB+vdanAjQ8fLJcS0q+WHYgesRRxPwNFr2Pqm18CVGg==} - '@esbuild/win32-ia32@0.21.5': - resolution: {integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] + '@fontsource/roboto-slab@5.2.8': + resolution: {integrity: sha512-8+iMCsoUZsDwQUe5omwCp7JPNTVdyAgay5AdhmnFZPEVIVabujrmYaFkSuZ1+GUemPEWlzEQ6aQkg2mPL84SAA==} - '@esbuild/win32-x64@0.21.5': - resolution: {integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] + '@fontsource/sora@5.2.8': + resolution: {integrity: sha512-1G6iTXUx8rcCKzi3mjaTQ1DE8PQz0OmW3Qnku+64S+bqRr1o/gGeiw8fxIQhhBU9ZP8ZofIqai7o00DNOPnlDw==} - '@eslint-community/eslint-utils@4.4.0': - resolution: {integrity: sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==} - engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - peerDependencies: - eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + '@formatjs/bigdecimal@0.2.5': + resolution: {integrity: sha512-2XTKNrZRaCUyXK2976wfutqxMBuPO/S/zbJnQdysLI2Zy5mWPVNVEkE6tsTcSVWSE7DgO88t8DtBy+uf3I8bxg==} - '@eslint-community/regexpp@4.11.0': - resolution: {integrity: sha512-G/M/tIiMrTAxEWRfLfQJMmGNX28IxBg4PBz8XqQhqUHLFI6TL2htpIB1iQCj144V5ee/JaKyT9/WZ0MGZWfA7A==} - engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + '@formatjs/fast-memoize@3.1.5': + resolution: {integrity: sha512-KLi3fan6WnCHmigd9pmEEN8Hid0v4wiFBW576M/d07KMWYecf1CvyMI3n34vCmHT4AoVqG2n702kiHbXjzZX2A==} - '@eslint/config-array@0.18.0': - resolution: {integrity: sha512-fTxvnS1sRMu3+JjXwJG0j/i4RT9u4qJ+lqS/yCGap4lH4zZGzQ7tu+xZqQmcMZq5OBZDL4QRxQzRjkWcGt8IVw==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@formatjs/intl-durationformat@0.10.12': + resolution: {integrity: sha512-k9LXUGERtAKoav1my1JIfMZY++ifg/2+7zAjFCUu+tPP2PEfN2N0shdZ9hddHMbaD0icr2iAVT8zleLNf70W8A==} - '@eslint/eslintrc@3.1.0': - resolution: {integrity: sha512-4Bfj15dVJdoy3RfZmmo86RK1Fwzn6SstsvK9JS+BaVKqC6QQQQyXekNaC+g+LKNgkQ+2VhGAzm6hO40AhMR3zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@formatjs/intl-localematcher@0.8.8': + resolution: {integrity: sha512-pBr2hVKWvkHVnfXegW+53NT9U2uaVQCc+EgzLPCCwXqBA3nvM5fPbK9IcJlNjV+NMKGyZ2F3ZSG78iGdxAAqbA==} - '@eslint/js@9.10.0': - resolution: {integrity: sha512-fuXtbiP5GWIn8Fz+LWoOMVf/Jxm+aajZYkhi6CuEm4SxymFM+eUWzbO9qXT+L0iCkL5+KGYMCSGxo686H19S1g==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} - '@eslint/object-schema@2.1.4': - resolution: {integrity: sha512-BsWiH1yFGjXXS2yvrf5LyuoSIIbPrGUWob917o+BTKuZ7qJdxX8aJLRxs1fS9n6r7vESrq1OUqb68dANcFXuQQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} - '@eslint/plugin-kit@0.1.0': - resolution: {integrity: sha512-autAXT203ixhqei9xt+qkYOvY8l6LAFIdT2UXc/RPNeUVfqRF1BV94GTJyVPFKT8nFM6MyVJhjLj9E8JWvf5zQ==} - engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} engines: {node: '>=12.22'} - '@humanwhocodes/retry@0.3.0': - resolution: {integrity: sha512-d2CGZR2o7fS6sWB7DG/3a95bGKQyHMACZ5aW8qGkkqQpUoZV6C0X7Pc7l4ZNMZkfNBf4VWNe9E1jRsf0G146Ew==} + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} - '@jridgewell/gen-mapping@0.3.5': - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} engines: {node: '>=6.0.0'} - '@jridgewell/set-array@1.2.1': - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/sourcemap-codec@1.5.0': - resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@lezer/common@1.5.2': + resolution: {integrity: sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==} - '@lezer/common@1.2.1': - resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + '@lezer/css@1.3.3': + resolution: {integrity: sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==} - '@lezer/css@1.1.9': - resolution: {integrity: sha512-TYwgljcDv+YrV0MZFFvYFQHCfGgbPMR6nuqLabBdmZoFH3EP1gvw8t0vae326Ne3PszQkbXfVBjCnf3ZVCr0bA==} + '@lezer/highlight@1.2.3': + resolution: {integrity: sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==} - '@lezer/highlight@1.2.1': - resolution: {integrity: sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==} + '@lezer/lr@1.4.10': + resolution: {integrity: sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==} - '@lezer/lr@1.4.2': - resolution: {integrity: sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==} + '@marijn/find-cluster-break@1.0.2': + resolution: {integrity: sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==} - '@manypkg/find-root@1.1.0': - resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} + '@maxim_mazurok/gapi.client.discovery-v1@0.5.20200806': + resolution: {integrity: sha512-oVq9hnnI5VhAtsx55iJbPz8NRfJtWFpI1kINKeuygzCvsx90b1GQDeN3MDUvhADXiQ7+Izs316cqBnJjoDxCow==} - '@manypkg/get-packages@1.1.3': - resolution: {integrity: sha512-fo+QhuU3qE/2TQMQmbVMqaQ6EWbMhi4ABWP+O4AM1NqPBuy0OrApV5LO6BrrgnhtAHS2NH6RrVk9OL181tTi8A==} + '@maxim_mazurok/gapi.client.drive-v3@0.2.20260311': + resolution: {integrity: sha512-2SVn8bIFZB9pq1JjqBNY/Agebv8gjHdr5k+ippSVsz07eWpl7d0Vxj4huX1O60ev0NDqrIx6a7w6MFFUIKlN6w==} - '@material-symbols/font-400@0.21.3': - resolution: {integrity: sha512-ed9aUM5AUBLsFTV988Zq3EYUXftNnv0/GC9QoCV+J808kGIDs0d5rX87pPQGbof3HgL5B4Sk+Iq7bzm+WXjNhQ==} + '@melloware/coloris@0.25.0': + resolution: {integrity: sha512-RBWVFLjWbup7GRkOXb9g3+ZtR9AevFtJinrRz2cYPLjZ3TCkNRGMWuNbmQWbZ5cF3VU7aQDZwUsYgIY/bGrh2g==} - '@maxim_mazurok/gapi.client.discovery-v1@0.1.20200806': - resolution: {integrity: sha512-Wl6UfmZVDdWbY3PUu8E2ULk9RPLjnMqp/iOA4tcK8Ne+U/GmlnWP/e34IaZNGArfl7iXJNOG+/3Rj9L9jQyF9Q==} + '@minht11/solid-virtual-container@0.2.1': + resolution: {integrity: sha512-HvQWx1uE5NWwx9WsN4waFtmyOjhZKMA/3vBf+j3zGsRfi556LCUk4oOmqZcOvIB5nEpHezvuZ8oUlwxigdO3Xg==} + peerDependencies: + solid-js: '>= 1.0.0' - '@maxim_mazurok/gapi.client.drive-v3@0.0.20230927': - resolution: {integrity: sha512-PoX5cY8umIODYif0vO+eA+Dgz2jmJ3rHRZSVoacrzoNFF0LjHt+NGx0TWNTsMNxuZsDP3TQA9ABhSk/+arOZHw==} + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 - '@mbarzda/solid-i18next@1.4.1': - resolution: {integrity: sha512-QC/mrD9aMibcjuSNpdr9uky+JNupVedOB2CLNYJrFrZCMyY8+HXK3q1Lda+P2WoNBWToS+6BCwbB1D9sKBCXSQ==} + '@nerimity/i18lite@1.2.0': + resolution: {integrity: sha512-M0wreW0wMqz2/vYD27v/E7PD+6F55n+EWVmgb4xXlziayhPbaKZTQJWCFSWxltUSWFbsI5AtDX+67eVbN7GLeQ==} peerDependencies: html-parse-string: <=1.x - i18next: '>=23.x' solid-js: '>=1.8.x' peerDependenciesMeta: html-parse-string: optional: true - '@melloware/coloris@0.24.0': - resolution: {integrity: sha512-9RGKHqZJsUSsxb/0xaBCK5OKywobiK/xRtV8f4KQDmviqmVfkMLR3kK4DRuTTLSFdSOqkV0OQ/Niitu+rlXXYw==} + '@nerimity/nevula@0.15.0': + resolution: {integrity: sha512-1fztSBR0Uxw5L1EUfTXT+fZNJO0P1ZKHR916PRV1LYHRg8iYsWRU4+CCRwJ153NNaD7iF4AiXdqK/s/nYXLgcw==} - '@minht11/solid-virtual-container@0.2.1': - resolution: {integrity: sha512-HvQWx1uE5NWwx9WsN4waFtmyOjhZKMA/3vBf+j3zGsRfi556LCUk4oOmqZcOvIB5nEpHezvuZ8oUlwxigdO3Xg==} + '@nerimity/solid-emoji-picker@0.4.9': + resolution: {integrity: sha512-TwBeqqIrzXaKCaY+OKGX3DmPY64bnb+UITOT+UX8P66iy2v/HD0pFO+SAW4ofPkHjsveZ9TV4U2/QVnmgMGUsQ==} peerDependencies: - solid-js: '>= 1.0.0' - - '@nerimity/nevula@0.14.0': - resolution: {integrity: sha512-LPoswLxCMuxdB6Fe4wyVW3RqhnCTzf/PUaqWm39rweVgPg0SharUYyLkJ02rYPSQkyMFmJtnrjuK7TH4xyGtFA==} + solid-js: ^1.6.0 - '@nerimity/solid-emoji-picker@0.4.8': - resolution: {integrity: sha512-dolM0ncAry0M77K9xlULC+AgHKPwAxARlEM2mTgz4huORupGzajhK3jiOxMPtH1j0M6MREY+B9NQ3YHxFDga0A==} + '@nerimity/solid-i18lite@1.8.1': + resolution: {integrity: sha512-iH+oXTR6qV/TEBsq9zK45E5IDbR+kHT2SUfyfuyTi3yKGdK1fBAV3Zzz9uOS7AjEsNhTabSKUuhVj63W7IttOw==} peerDependencies: - solid-js: ^1.6.0 + '@nerimity/i18lite': <=1.x + html-parse-string: <=1.x + solid-js: '>=1.8.x' + peerDependenciesMeta: + html-parse-string: + optional: true '@nerimity/solid-opus-media-recorder@1.0.1': resolution: {integrity: sha512-bcf3KtT2FmYIm5JEFGovlFGp3t7rYwkI4Mm04t9/QkdAjDyLDkxE1bd/84cXvZedGrNXKN3tVzZ3iXNRWbeauQ==} @@ -624,108 +562,242 @@ packages: peerDependencies: solid-js: '>=1.0.0' - '@nodelib/fs.scandir@2.1.5': - resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} - engines: {node: '>= 8'} + '@oxc-project/types@0.132.0': + resolution: {integrity: sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==} - '@nodelib/fs.stat@2.0.5': - resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} - engines: {node: '>= 8'} + '@parcel/watcher-android-arm64@2.5.6': + resolution: {integrity: sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] - '@nodelib/fs.walk@1.2.8': - resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} - engines: {node: '>= 8'} + '@parcel/watcher-darwin-arm64@2.5.6': + resolution: {integrity: sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.6': + resolution: {integrity: sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.6': + resolution: {integrity: sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] - '@rollup/rollup-android-arm-eabi@4.21.2': - resolution: {integrity: sha512-fSuPrt0ZO8uXeS+xP3b+yYTCBUd05MoSp2N/MFOgjhhUhMmchXlpTQrTpI8T+YAwAQuK7MafsCOxW7VrPMrJcg==} + '@parcel/watcher-linux-arm-glibc@2.5.6': + resolution: {integrity: sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==} + engines: {node: '>= 10.0.0'} cpu: [arm] - os: [android] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm-musl@2.5.6': + resolution: {integrity: sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + libc: [musl] - '@rollup/rollup-android-arm64@4.21.2': - resolution: {integrity: sha512-xGU5ZQmPlsjQS6tzTTGwMsnKUtu0WVbl0hYpTPauvbRAnmIvpInhJtgjj3mcuJpEiuUw4v1s4BimkdfDWlh7gA==} + '@parcel/watcher-linux-arm64-glibc@2.5.6': + resolution: {integrity: sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-arm64-musl@2.5.6': + resolution: {integrity: sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@parcel/watcher-linux-x64-glibc@2.5.6': + resolution: {integrity: sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@parcel/watcher-linux-x64-musl@2.5.6': + resolution: {integrity: sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@parcel/watcher-win32-arm64@2.5.6': + resolution: {integrity: sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.6': + resolution: {integrity: sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.6': + resolution: {integrity: sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.6': + resolution: {integrity: sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==} + engines: {node: '>= 10.0.0'} + + '@rolldown/binding-android-arm64@1.0.2': + resolution: {integrity: sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [android] - '@rollup/rollup-darwin-arm64@4.21.2': - resolution: {integrity: sha512-99AhQ3/ZMxU7jw34Sq8brzXqWH/bMnf7ZVhvLk9QU2cOepbQSVTns6qoErJmSiAvU3InRqC2RRZ5ovh1KN0d0Q==} + '@rolldown/binding-darwin-arm64@1.0.2': + resolution: {integrity: sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [darwin] - '@rollup/rollup-darwin-x64@4.21.2': - resolution: {integrity: sha512-ZbRaUvw2iN/y37x6dY50D8m2BnDbBjlnMPotDi/qITMJ4sIxNY33HArjikDyakhSv0+ybdUxhWxE6kTI4oX26w==} + '@rolldown/binding-darwin-x64@1.0.2': + resolution: {integrity: sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [darwin] - '@rollup/rollup-linux-arm-gnueabihf@4.21.2': - resolution: {integrity: sha512-ztRJJMiE8nnU1YFcdbd9BcH6bGWG1z+jP+IPW2oDUAPxPjo9dverIOyXz76m6IPA6udEL12reYeLojzW2cYL7w==} - cpu: [arm] - os: [linux] + '@rolldown/binding-freebsd-x64@1.0.2': + resolution: {integrity: sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] - '@rollup/rollup-linux-arm-musleabihf@4.21.2': - resolution: {integrity: sha512-flOcGHDZajGKYpLV0JNc0VFH361M7rnV1ee+NTeC/BQQ1/0pllYcFmxpagltANYt8FYf9+kL6RSk80Ziwyhr7w==} + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + resolution: {integrity: sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm] os: [linux] - '@rollup/rollup-linux-arm64-gnu@4.21.2': - resolution: {integrity: sha512-69CF19Kp3TdMopyteO/LJbWufOzqqXzkrv4L2sP8kfMaAQ6iwky7NoXTp7bD6/irKgknDKM0P9E/1l5XxVQAhw==} + '@rolldown/binding-linux-arm64-gnu@1.0.2': + resolution: {integrity: sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-arm64-musl@4.21.2': - resolution: {integrity: sha512-48pD/fJkTiHAZTnZwR0VzHrao70/4MlzJrq0ZsILjLW/Ab/1XlVUStYyGt7tdyIiVSlGZbnliqmult/QGA2O2w==} + '@rolldown/binding-linux-arm64-musl@1.0.2': + resolution: {integrity: sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] os: [linux] + libc: [musl] - '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': - resolution: {integrity: sha512-cZdyuInj0ofc7mAQpKcPR2a2iu4YM4FQfuUzCVA2u4HI95lCwzjoPtdWjdpDKyHxI0UO82bLDoOaLfpZ/wviyQ==} + '@rolldown/binding-linux-ppc64-gnu@1.0.2': + resolution: {integrity: sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [ppc64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-riscv64-gnu@4.21.2': - resolution: {integrity: sha512-RL56JMT6NwQ0lXIQmMIWr1SW28z4E4pOhRRNqwWZeXpRlykRIlEpSWdsgNWJbYBEWD84eocjSGDu/XxbYeCmwg==} - cpu: [riscv64] - os: [linux] - - '@rollup/rollup-linux-s390x-gnu@4.21.2': - resolution: {integrity: sha512-PMxkrWS9z38bCr3rWvDFVGD6sFeZJw4iQlhrup7ReGmfn7Oukrr/zweLhYX6v2/8J6Cep9IEA/SmjXjCmSbrMQ==} + '@rolldown/binding-linux-s390x-gnu@1.0.2': + resolution: {integrity: sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [s390x] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-gnu@4.21.2': - resolution: {integrity: sha512-B90tYAUoLhU22olrafY3JQCFLnT3NglazdwkHyxNDYF/zAxJt5fJUB/yBoWFoIQ7SQj+KLe3iL4BhOMa9fzgpw==} + '@rolldown/binding-linux-x64-gnu@1.0.2': + resolution: {integrity: sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [glibc] - '@rollup/rollup-linux-x64-musl@4.21.2': - resolution: {integrity: sha512-7twFizNXudESmC9oneLGIUmoHiiLppz/Xs5uJQ4ShvE6234K0VB1/aJYU3f/4g7PhssLGKBVCC37uRkkOi8wjg==} + '@rolldown/binding-linux-x64-musl@1.0.2': + resolution: {integrity: sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [linux] + libc: [musl] - '@rollup/rollup-win32-arm64-msvc@4.21.2': - resolution: {integrity: sha512-9rRero0E7qTeYf6+rFh3AErTNU1VCQg2mn7CQcI44vNUWM9Ze7MSRS/9RFuSsox+vstRt97+x3sOhEey024FRQ==} + '@rolldown/binding-openharmony-arm64@1.0.2': + resolution: {integrity: sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [arm64] - os: [win32] + os: [openharmony] - '@rollup/rollup-win32-ia32-msvc@4.21.2': - resolution: {integrity: sha512-5rA4vjlqgrpbFVVHX3qkrCo/fZTj1q0Xxpg+Z7yIo3J2AilW7t2+n6Q8Jrx+4MrYpAnjttTYF8rr7bP46BPzRw==} - cpu: [ia32] + '@rolldown/binding-wasm32-wasi@1.0.2': + resolution: {integrity: sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.2': + resolution: {integrity: sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] os: [win32] - '@rollup/rollup-win32-x64-msvc@4.21.2': - resolution: {integrity: sha512-6UUxd0+SKomjdzuAcp+HAmxw1FlGBnl1v2yEPSabtx4lBfdXHDVsW7+lQkgz9cNFJGY3AWR7+V8P5BqkD9L9nA==} + '@rolldown/binding-win32-x64-msvc@1.0.2': + resolution: {integrity: sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==} + engines: {node: ^20.19.0 || >=22.12.0} cpu: [x64] os: [win32] + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + + '@sentry-internal/browser-utils@10.53.1': + resolution: {integrity: sha512-X4d6y8sBMjmNhcDW4eMBU3ASsNIMz8dqaFkhyIMN/dkYr/yZKnbRZPaVuVUGvHKjnlficPpIH0/HK9KBjrYxPw==} + engines: {node: '>=18'} + + '@sentry-internal/feedback@10.53.1': + resolution: {integrity: sha512-vVpTI/aEYN5d9IgZeYJWMqVaN0+iFgidSrYNAsZTh1US5sJUzF/wrl+68KdpmCtFROrN3jiAn1oPSwL5CKvEJA==} + engines: {node: '>=18'} + + '@sentry-internal/replay-canvas@10.53.1': + resolution: {integrity: sha512-aueLaf/2prExwA76BGU5/bOXCKWqtt6jQXWA6WJQNrmKpPEtZJB4ypnpsou0McXQCF8tur2Y8U0TEkwQP13yJQ==} + engines: {node: '>=18'} + + '@sentry-internal/replay@10.53.1': + resolution: {integrity: sha512-wZNzTBYkgGUPWMuUQv7L64+OJmoCnz7GQNiTrTFK6EVAjJXFBCSsPp/nhif0bLhbk8+0g4xz633uOhpXuQbFdw==} + engines: {node: '>=18'} + + '@sentry/browser@10.53.1': + resolution: {integrity: sha512-zXF373hzUOGzUOrqd8xb1U3LQi5uYC3mwv+z5OMKUUinQlu30tTWBs7ypy6YTchtix9QlYaHWlayUF8vBZ5UjA==} + engines: {node: '>=18'} + + '@sentry/core@10.53.1': + resolution: {integrity: sha512-XG4ezlkyuAPjBC5+9kXC94rXXuqYTw9NRhfaDHssbTFaGnqBR8vQX2UUgZfY7ucbeelRDGfBu1sywoU+mB04uA==} + engines: {node: '>=18'} + + '@sentry/solid@10.53.1': + resolution: {integrity: sha512-QfvAIFd/fc6zb8hPN8z8H56tWWPihUFchhBSEfJ0m37S2ESnsMRQ+VgNjLga1CnqXBiNmZJE6tx2PQx/To1Kbg==} + engines: {node: '>=18'} + peerDependencies: + '@solidjs/router': ^0.13.4 || ^0.14.0 || ^0.15.0 + '@tanstack/solid-router': ^1.132.27 + solid-js: ^1.8.4 + peerDependenciesMeta: + '@solidjs/router': + optional: true + '@tanstack/solid-router': + optional: true + '@socket.io/component-emitter@3.1.2': resolution: {integrity: sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==} - '@solid-primitives/context@0.2.3': - resolution: {integrity: sha512-6/e8qu9qJf48FJ+sxc/B782NdgFw5TvI8+r6U0gHizumfZcWZg8FAJqvRZAiwlygkUNiTQOGTeO10LVbMm0kvg==} + '@solid-primitives/context@0.3.2': + resolution: {integrity: sha512-6fvTtpK17PFHnUf/UOc1TzBjd+kLFjtA62aRFEm1kDP9ufTo7FYW2kUzQAWbfbRHi30yjBJtopbR8qd6nShwyg==} peerDependencies: solid-js: ^1.6.12 - '@solid-primitives/keyed@1.2.2': - resolution: {integrity: sha512-oBziY40JK4XmJ57XGkFl8j0GtEarSu0hhjdkUQgqL/U0QQE3TZrRo9uhgH7I6VGJKBKG7SAraTPE6S5lVLM1ow==} + '@solid-primitives/keyed@1.5.3': + resolution: {integrity: sha512-zNadtyYBhJSOjXtogkGHmRxjGdz9KHc8sGGVAGlUABkE8BED2tbIZoxkwSqzOwde8OcUEH0bb5DLZUWIMvyBSA==} peerDependencies: solid-js: ^1.6.12 @@ -734,29 +806,32 @@ packages: peerDependencies: solid-js: '>=1.8.4' - '@thaunknown/simple-peer@10.0.10': - resolution: {integrity: sha512-RtoYQChP5clkbh+crUhv0LD/fdzgNO/Ah/SBdcSqla6xY6GK50ukNhq39H4vzAKoYqvDu01Hc+JSD9DxCdoBOw==} + '@thaunknown/simple-peer@10.1.0': + resolution: {integrity: sha512-xNM49v0rBbjIKrS9XNwXW3FFuGvsPGadFRWbBdLAY87pEJeo7V0dxyX6GBHP8UVlefffRedCLsjYXb6i8W9Ofg==} + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} '@types/babel__core@7.20.5': resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - '@types/babel__generator@7.6.8': - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__generator@7.27.0': + resolution: {integrity: sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==} '@types/babel__template@7.4.4': resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} - '@types/babel__traverse@7.20.6': - resolution: {integrity: sha512-r1bzfrm0tomOI8g1SzvCaQHo6Lcv6zu0EA+W2kHrt8dyrHQxGzBBL4kdkzIS+jBMV+EYcMAEAqXqYaLJq5rOZg==} + '@types/babel__traverse@7.28.0': + resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} - '@types/chroma-js@2.4.4': - resolution: {integrity: sha512-/DTccpHTaKomqussrn+ciEvfW4k6NAHzNzs/sts1TCqg333qNxOhy8TNIoQCmbGG3Tl8KdEhkGAssb1n3mTXiQ==} + '@types/chroma-js@3.1.2': + resolution: {integrity: sha512-YBTQqArPN8A0niHXCwrO1z5x++a+6l0mLBykncUpr23oIPW7L4h39s6gokdK/bDrPmSh8+TjMmrhBPnyiaWPmQ==} '@types/croppie@2.6.4': resolution: {integrity: sha512-rxKLA5S+QarlaMVlsMqhn2fMMC5XlvogFzTYdMlkeupPgxT1mWaheucdZNzkUJACn61+JjN/eJYt5dS9GMoeXw==} - '@types/estree@1.0.5': - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/gapi.client.discovery-v1@0.0.4': resolution: {integrity: sha512-uevhRumNE65F5mf2gABLaReOmbFSXONuzFZjNR3dYv6BmkHg+wciubHrfBAsp3554zNo3Dcg6dUAlwMqQfpwjQ==} @@ -764,35 +839,29 @@ packages: '@types/gapi.client@1.0.8': resolution: {integrity: sha512-qJQUmmumbYym3Amax0S8CVzuSngcXsC1fJdwRS2zeW5lM63zXkw4wJFP+bG0jzgi0R6EsJKoHnGNVTDbOyG1ng==} - '@types/gapi@0.0.45': - resolution: {integrity: sha512-xc8X9FrdZcWoOd4tMhLenYbnmaxCgy2FBt+SOI7xv9Efy/7Jk2V9gBZiufFBLIfDUUvpWY7DN3x4lrdL1I9y0Q==} - - '@types/linkify-it@3.0.5': - resolution: {integrity: sha512-yg6E+u0/+Zjva+buc3EIb+29XEg4wltq7cSmd4Uc2EE/1nUVmxyzpX6gUXD0V8jIrG0r7YeOGVIbYRkxeooCtw==} + '@types/gapi@0.0.47': + resolution: {integrity: sha512-/ZsLuq6BffMgbKMtZyDZ8vwQvTyKhKQ1G2K6VyWCgtHHhfSSXbk4+4JwImZiTjWNXfI2q1ZStAwFFHSkNoTkHA==} - '@types/markdown-it-emoji@2.0.5': - resolution: {integrity: sha512-iJLsmCNpSWKtV6Ia3mLSjcXJPEt7ubGG342z+hGvYx++TpM19oTUrJcI7XjbOqRQ+W2UQ323E7B0eCLwlgT/9g==} + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} - '@types/markdown-it@13.0.9': - resolution: {integrity: sha512-1XPwR0+MgXLWfTn9gCsZ55AHOKW1WN+P9vr0PaQh5aerR9LLQXUbjfEAFhjmEmyoYFWAyuN2Mqkn40MZ4ukjBw==} + '@types/linkify-it@5.0.0': + resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} - '@types/mdurl@1.0.5': - resolution: {integrity: sha512-6L6VymKTzYSrEf4Nev4Xa1LCHKrlTlYCBMTlQKFuddo1CvQcE52I0mwfOJayueUC7MJuXOeHTcIU683lzd0cUA==} + '@types/markdown-it-emoji@3.0.1': + resolution: {integrity: sha512-cz1j8R35XivBqq9mwnsrP2fsz2yicLhB8+PDtuVkKOExwEdsVBNI+ROL3sbhtR5occRZ66vT0QnwFZCqdjf3pA==} - '@types/node@12.20.55': - resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} + '@types/markdown-it@14.1.2': + resolution: {integrity: sha512-promo4eFwuiW+TfGxhi+0x3czqTYJkG8qB17ZUJiVF10Xm7NLVRSLUsfRTU/6h1e24VvRnXCx+hG7li58lkzog==} - '@types/node@20.16.5': - resolution: {integrity: sha512-VwYCweNo3ERajwy0IUlqqcyZ8/A7Zwa9ZP3MnENWcB11AejO+tLy3pu850goUW2FC/IJMdZUfKpX/yxL1gymCA==} + '@types/mdurl@2.0.0': + resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/semver@7.5.8': - resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} + '@types/node@22.19.19': + resolution: {integrity: sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==} - '@types/simple-peer@9.11.8': - resolution: {integrity: sha512-rvqefdp2rvIA6wiomMgKWd2UZNPe6LM2EV5AuY3CPQJF+8TbdrL5TjYdMf0VAjGczzlkH4l1NjDkihwbj3Xodw==} - - '@types/sortablejs@1.15.8': - resolution: {integrity: sha512-b79830lW+RZfwaztgs1aVPgbasJ8e7AXtZYHTELNXZPsERt4ymJdjV4OccDbHQAvHrCcFpbF78jkm0R6h/pZVg==} + '@types/simple-peer@9.11.9': + resolution: {integrity: sha512-6Gdl7TSS5oh9nuwKD4Pl8cSmaxWycYeZz9HLnJBNvIwWjZuGVsmHe9RwW3+9RxfhC1aIR9Z83DvaJoMw6rhkbg==} '@types/uzip@0.20201231.2': resolution: {integrity: sha512-l8n/uaJDoxv2hMMWKCaq6l/umbCZjq6CMr8z2wuteB7lKoIwHrIlMZ3AwAuc+olmOK1AobfvK1PPfr5wl7lkWQ==} @@ -800,89 +869,63 @@ packages: '@types/voice-activity-detection@0.0.5': resolution: {integrity: sha512-sdk0AnjVO3GOx7UbQanOzz9+g0L99w2Kdo6Jg1BWqt7zNoQ9WQejltukUgvB/XzGEgifhL8uZEZz9opnsYBvmA==} - '@typescript-eslint/eslint-plugin@7.18.0': - resolution: {integrity: sha512-94EQTWZ40mzBc42ATNIBimBEDltSJ9RQHCC8vc/PDbxi4k8dVwUAv4o98dk50M1zB+JGFxp43FP7f8+FP8R6Sw==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/eslint-plugin@8.59.4': + resolution: {integrity: sha512-PegsU+XfyJJNjd4+u/k6f9yTyp0lEXXiPopUNobZcIAUJFGICFLN+sP0Rb3JehVmiij1Ph0dFGYqODoRo/2+6A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - '@typescript-eslint/parser': ^7.0.0 - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + '@typescript-eslint/parser': ^8.59.4 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/parser@7.18.0': - resolution: {integrity: sha512-4Z+L8I2OqhZV8qA132M4wNL30ypZGYOQVBfMgxDH/K5UX0PNqTu1c6za9ST5r9+tavvHiTWmBnKzpCJ/GlVFtg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/parser@8.59.4': + resolution: {integrity: sha512-zORHqO/tuhxY1zWuTvMUqddRxpiFJ72xVfcNoWpqdLjs6lfPbuQBJuW4pk+49/uBMy7Ssr4bzgjiKmmDB1UbZQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/scope-manager@7.18.0': - resolution: {integrity: sha512-jjhdIE/FPF2B7Z1uzc6i3oWKbGcHb87Qw7AWj6jmEqNOfDFbJWtjt/XfwCpvNkpGWlcJaog5vTR+VV8+w9JflA==} - engines: {node: ^18.18.0 || >=20.0.0} - - '@typescript-eslint/scope-manager@8.4.0': - resolution: {integrity: sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==} + '@typescript-eslint/project-service@8.59.4': + resolution: {integrity: sha512-Ly00Vu4oAacfDeHp2Zg85ioNG6l8HG+tN1D7J+xTHSxu9y0awYKJ2zH1rFBn8ZSfuGK+7FxK3Cgl3uAz0aZZLg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - - '@typescript-eslint/type-utils@7.18.0': - resolution: {integrity: sha512-XL0FJXuCLaDuX2sYqZUUSOJ2sG5/i1AAze+axqmLnSkNEVMVYLF+cbwlB2w8D1tinFuSikHmFta+P+HOofrLeA==} - engines: {node: ^18.18.0 || >=20.0.0} peerDependencies: - eslint: ^8.56.0 - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true - - '@typescript-eslint/types@7.18.0': - resolution: {integrity: sha512-iZqi+Ds1y4EDYUtlOOC+aUmxnE9xS/yCigkjA7XpTKV6nCBd3Hp/PRGGmdwnfkV2ThMyYldP1wRpm/id99spTQ==} - engines: {node: ^18.18.0 || >=20.0.0} + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/types@8.4.0': - resolution: {integrity: sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==} + '@typescript-eslint/scope-manager@8.59.4': + resolution: {integrity: sha512-mUeR/3H1WrTAddJrwut8OoPjfauaztMQmRwV5fQTUyNVJCLiUXXe4lGEyYIL2oFDpP7UtgbGJXCt72wT0z2S3Q==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/typescript-estree@7.18.0': - resolution: {integrity: sha512-aP1v/BSPnnyhMHts8cf1qQ6Q1IFwwRvAQGRvBFkWlo3/lH29OXA3Pts+c10nxRxIBrDnoMqzhgdwVe5f2D6OzA==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/tsconfig-utils@8.59.4': + resolution: {integrity: sha512-DLCpnKgD4alVxTBSKulK+gU1KCqOgUXfDRDXh2mZgzokQKa/70ax93I2uVO3m/LLvIAtWZIFoiifudmIqAxpMA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/typescript-estree@8.4.0': - resolution: {integrity: sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==} + '@typescript-eslint/type-utils@8.59.4': + resolution: {integrity: sha512-uonTuPAAKr9XaBGqJ3LjYTh72zy5DyGesljO9gtmk/eFW0W1fRHjnwVYKB35Lm8d5Q5CluEW3gPHjTvZTmgrfA==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - typescript: '*' - peerDependenciesMeta: - typescript: - optional: true + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/utils@7.18.0': - resolution: {integrity: sha512-kK0/rNa2j74XuHVcoCZxdFBMF+aq/vH83CXAOHieC+2Gis4mF8jJXT5eAfyD3K0sAxtPuwxaIOIOvhwzVDt/kw==} - engines: {node: ^18.18.0 || >=20.0.0} - peerDependencies: - eslint: ^8.56.0 + '@typescript-eslint/types@8.59.4': + resolution: {integrity: sha512-F1o7WJcCq+bc8dwcO/YsSEOudAH8RDtaOhM6wcAQhcUsFhnWQl81JKy48q1hoxAU0qrzM89+31GYh1515Zde3Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - '@typescript-eslint/utils@8.4.0': - resolution: {integrity: sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==} + '@typescript-eslint/typescript-estree@8.59.4': + resolution: {integrity: sha512-F+RuOmcDXo4+TPdfd/TCLS3m2nw8gE9XXyZLrA3JBfaA5tz9TtdkyD3YJFmPxulyc2cKbEok/CvFE3MgSLWnag==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: - eslint: ^8.57.0 || ^9.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@7.18.0': - resolution: {integrity: sha512-cDF0/Gf81QpY3xYyJKDV14Zwdmid5+uuENhjH2EqFaF0ni+yAyq/LzMaIJdhNJXZI7uLzwIlA+V7oWoyn6Curg==} - engines: {node: ^18.18.0 || >=20.0.0} + '@typescript-eslint/utils@8.59.4': + resolution: {integrity: sha512-cYXeNAUsG4lJo5dbc1FcKm+JwIWrj1/UpTORsC6tGMjEZ81DYcvIr9/ueikhMa/Y/gDQYGp+YX9/xQrXje5BJw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' - '@typescript-eslint/visitor-keys@8.4.0': - resolution: {integrity: sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==} + '@typescript-eslint/visitor-keys@8.59.4': + resolution: {integrity: sha512-U3gxVaDVnuZKhSspW/MzMxE1kq7zOdc072FcSNoqA1I9p8HyKbBFfEHoWckBAMgNMph4MamwS5iTVzFmrnt8TQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} acorn-jsx@5.3.2: @@ -890,75 +933,70 @@ packages: peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 - acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} analyser-frequency-average@1.0.0: resolution: {integrity: sha512-Y8HRgDfMWpefR286IAT7w9WsZ2r2dLOAkUNz8SQgsTAM0GsM9SAAqr1psqOr1scN76cL0pfuNZoQTnuvdoM0RA==} - ansi-colors@4.1.3: - resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==} - engines: {node: '>=6'} - - ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - - ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} - ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} - anymatch@3.1.3: - resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} - engines: {node: '>= 8'} - - argparse@1.0.10: - resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} - argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} - array-union@2.1.0: - resolution: {integrity: sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==} - engines: {node: '>=8'} - audio-frequency-to-index@2.0.0: resolution: {integrity: sha512-7zqlDEAsEkPB0ORRhjBlsK7KBZQtdgLLQcmemFD2V2KHPH4flqzDOheWl+U69K0P/LA7J/H5YBNzNWaoS/7WAQ==} - autoprefixer@10.4.20: - resolution: {integrity: sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==} + autoprefixer@10.5.0: + resolution: {integrity: sha512-FMhOoZV4+qR6aTUALKX2rEqGG+oyATvwBt9IIzVR5rMa2HRWPkxf+P+PAJLD1I/H5/II+HuZcBJYEFBpq39ong==} engines: {node: ^10 || ^12 || >=14} hasBin: true peerDependencies: postcss: ^8.1.0 - b4a@1.6.6: - resolution: {integrity: sha512-5Tk1HLk6b6ctmjIkAcU/Ujv/1WqiDl0F0JdRCR80VsOcUlHcu7pWeWRlOqQLHfDEsVx9YH/aif5AG4ehoCtTmg==} + b4a@1.8.1: + resolution: {integrity: sha512-aiqre1Nr0B/6DgE2N5vwTc+2/oQZ4Wh1t4NznYY4E00y8LCt6NqdRv81so00oo27D8MVKTpUa/MwUUtBLXCoDw==} + peerDependencies: + react-native-b4a: '*' + peerDependenciesMeta: + react-native-b4a: + optional: true - babel-plugin-jsx-dom-expressions@0.38.5: - resolution: {integrity: sha512-JfjHYKOKGwoiOYQ56Oo8gbZPb9wNMpPuEEUhSCjMpnuHM9K21HFIUBm83TZPB40Av4caCIW4Tfjzpkp/MtFpMw==} + babel-plugin-jsx-dom-expressions@0.40.7: + resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} peerDependencies: '@babel/core': ^7.20.12 - babel-preset-solid@1.8.22: - resolution: {integrity: sha512-nKwisb//lZsiRF2NErlRP64zVTJqa1OSZiDnSl0YbcTiCZoMt52CY2Pg+9fsYAPtjYMT7RHBmzU41pxK6hFOcg==} + babel-preset-solid@1.9.12: + resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} peerDependencies: '@babel/core': ^7.0.0 + solid-js: ^1.9.12 + peerDependenciesMeta: + solid-js: + optional: true balanced-match@1.0.2: resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - bare-events@2.4.2: - resolution: {integrity: sha512-qMKFd2qG/36aA4GwvKq8MxnPgCQAmBWmSyLWsJcbn8v03wvIPQ/hG1Ms8bPzndZxMDoHpxez5VOS+gC9Yi24/Q==} + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + bare-events@2.8.3: + resolution: {integrity: sha512-HdUm8EMQBLaJvGUdidNNbqpA1kYkwNcb+MYxkxCLAPJGQzlv9J0C24h8V65Z4c5GLd/JEALDvpFCQgpLJqc0zw==} + peerDependencies: + bare-abort-controller: '*' + peerDependenciesMeta: + bare-abort-controller: + optional: true base64-arraybuffer@1.0.2: resolution: {integrity: sha512-I3yl4r9QB5ZRY3XuJVEPfc2XhZO6YweFPI+UovAzn+8/hb3oJ6lnysaFcjVpkCPfVWFUDvoZ8kmVDP7WyRtYtQ==} @@ -967,29 +1005,23 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - better-path-resolve@1.0.0: - resolution: {integrity: sha512-pbnl5XzGBdrFU/wT4jqmJVPn2B6UHPBOhzMQkY/SPUPB6QtUXtmBHBIwCbXJol93mOpGMnQyP/+BB19q04xj7g==} - engines: {node: '>=4'} - - binary-extensions@2.3.0: - resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} - engines: {node: '>=8'} + baseline-browser-mapping@2.10.31: + resolution: {integrity: sha512-MujYO3eP72uvmSE0i4wltsodRfIpZATP3jvzRNRGGxgzId7aVocVJJV3nf01qnzzKFGxQVC9bpWxl5cjxTr/7Q==} + engines: {node: '>=6.0.0'} + hasBin: true bl@4.1.0: resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} - brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} - brace-expansion@2.0.1: - resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} - braces@3.0.3: - resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} - engines: {node: '>=8'} - - browserslist@4.23.3: - resolution: {integrity: sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==} + browserslist@4.28.2: + resolution: {integrity: sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true @@ -1000,71 +1032,62 @@ packages: resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} engines: {node: '>=6'} - caniuse-lite@1.0.30001658: - resolution: {integrity: sha512-N2YVqWbJELVdrnsW5p+apoQyYt51aBMSsBZki1XZEfeBCexcM/sf4xiAHcXQBkuOwJBXtWF7aW1sYX6tKebPHw==} - - chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + caniuse-lite@1.0.30001793: + resolution: {integrity: sha512-iwSsYWaCOoh26cV8NwNRViHlrfUvYsHDfRVcbtmw0Kg6PJIZZXwMkj1442FYLBGkeUf1juAsU3DTfxW579mrPA==} chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} - chardet@0.7.0: - resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} - - chokidar@3.6.0: - resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} - engines: {node: '>= 8.10.0'} + chokidar@4.0.3: + resolution: {integrity: sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==} + engines: {node: '>= 14.16.0'} chownr@1.1.4: resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} - chroma-js@3.1.1: - resolution: {integrity: sha512-CGr6w73Gi86142RWqZ1RjED/CyduYw2vMTikQZUvr2jGIihnZlMo/Kzm9rYHWDP2pJc6eebwc8CkX0iteBon+A==} - - ci-info@3.9.0: - resolution: {integrity: sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==} - engines: {node: '>=8'} + chroma-js@3.2.0: + resolution: {integrity: sha512-os/OippSlX1RlWWr+QDPcGUZs0uoqr32urfxESG9U93lhUfbnlyckte84Q8P1UQY/qth983AS1JONKmLS4T0nw==} clamp@1.0.1: resolution: {integrity: sha512-kgMuFyE78OC6Dyu3Dy7vcx4uy97EIbVxJB/B0eJ3bUNAkwdNcxYzgKltnyADiYwsR7SEqkkUPsEUT//OVS6XMA==} - color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - color-convert@2.0.1: resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} engines: {node: '>=7.0.0'} - color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + colorjs.io@0.5.2: + resolution: {integrity: sha512-twmVoizEW7ylZSN32OgKdXRmo1qg+wT5/6C3xu5b9QsWzSFAhHLn2xd8ro0diCsKfCj1RdaTP/nrcW+vAoQPIw==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + crelt@1.0.6: + resolution: {integrity: sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==} + croppie@2.6.5: resolution: {integrity: sha512-IlChnVUGG5T3w2gRZIaQgBtlvyuYnlUWs2YZIXXR3H9KrlO1PtBT3j+ykxvy9eZIWhk+V5SpBmhCQz5UXKrEKQ==} - cross-spawn@5.1.0: - resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} - - cross-spawn@7.0.3: - resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} - csstype@3.1.3: - resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - debug@4.3.7: - resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} peerDependencies: supports-color: '*' @@ -1086,82 +1109,73 @@ packages: detect-browser@4.8.0: resolution: {integrity: sha512-f4h2dFgzHUIpjpBLjhnDIteXv8VQiUm8XzAuzQtYUqECX/eKh67ykuiVoyb7Db7a0PUSmJa3OGXStG0CbQFUVw==} - detect-indent@6.1.0: - resolution: {integrity: sha512-reYkTUJAZb9gUuZ2RvVCNhVHdg62RHnJ7WJl8ftMi4diZ6NWlciOzQN88pUhSELEwflJht4oQDv0F0BMlwaYtA==} - engines: {node: '>=8'} - - detect-libc@2.0.3: - resolution: {integrity: sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==} - engines: {node: '>=8'} - - dir-glob@3.0.1: - resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} engines: {node: '>=8'} dom-walk@0.1.2: resolution: {integrity: sha512-6QvTW9mrGeIegrFXdtQi9pk7O/nSK6lSdXW2eqUspN5LWD7UTji2Fqw5V2YLjBpHEoU9Xl/eUWNpDeZvoyOv2w==} - electron-to-chromium@1.5.18: - resolution: {integrity: sha512-1OfuVACu+zKlmjsNdcJuVQuVE61sZOLbNM4JAQ1Rvh6EOj0/EUKhMJjRH73InPlXSh8HIJk1cVZ8pyOV/FMdUQ==} + electron-to-chromium@1.5.360: + resolution: {integrity: sha512-GkcBt6YYAw9SxFWn+xVar4cLVGlXVuswwtRLBozi2zp0GjXs4ZnOrqV4zbXzg35n7w81hCkyJNYicgXlVHAmBA==} - end-of-stream@1.4.4: - resolution: {integrity: sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - engine.io-client@6.5.4: - resolution: {integrity: sha512-GeZeeRjpD2qf49cZQ0Wvh/8NJNfeXkXXcoGh+F77oEAgo9gUHwT1fCRxSNU+YEEaysOJTnsFHmM5oAcPy4ntvQ==} + engine.io-client@6.6.5: + resolution: {integrity: sha512-QCwxUDULPlXv8F6tqMMKx5dNkTe6OaBYRMPYeXKBlyOoKvAmE0ac6pW7fFhSscJ/5SI7666/U/B+MElbsrJlIg==} engine.io-parser@5.2.3: resolution: {integrity: sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==} engines: {node: '>=10.0.0'} - enquirer@2.4.1: - resolution: {integrity: sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==} - engines: {node: '>=8.6'} + entities@4.5.0: + resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} + engines: {node: '>=0.12'} - entities@3.0.1: - resolution: {integrity: sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==} + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} engines: {node: '>=0.12'} err-code@3.0.1: resolution: {integrity: sha512-GiaH0KJUewYok+eeY05IIgjtAe4Yltygk9Wqp1V5yVWLdhf0hYZchRjNIT9bb0mSwRcIusT3cx7PJUf3zEIfUA==} - esbuild@0.21.5: - resolution: {integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==} - engines: {node: '>=12'} - hasBin: true + eruda@3.4.3: + resolution: {integrity: sha512-J2TsF4dXSspOXev5bJ6mljv0dRrxj21wklrDzbvPmYaEmVoC+2psylyRi70nUPFh1mTQfIBsSusUtAMZtUN+/w==} escalade@3.2.0: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} - escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - escape-string-regexp@4.0.0: resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} engines: {node: '>=10'} - eslint-plugin-solid@0.14.3: - resolution: {integrity: sha512-eDeyPrijSjVGeyb4oKoyidgLlMDZwAg/YdxiY9QvGXl2kLgpcHvLpgpaGK4KJ8xSsg0ym3B2dPRBAIlT7iUrEQ==} + eslint-plugin-solid@0.14.5: + resolution: {integrity: sha512-nfuYK09ah5aJG/oEN6P1qziy1zLgW4PDWe75VNPi4CEFYk1x2AEqwFeQfEPR7gNn0F2jOeqKhx2E+5oNCOBYWQ==} engines: {node: '>=18.0.0'} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 + typescript: '>=4.8.4' - eslint-scope@8.0.2: - resolution: {integrity: sha512-6E4xmrTw5wtxnLA5wYL3WDfhZ/1bUBGOXV0zQvVRDOtrR8D0p6W7fs3JweNYhwRYeGvd/1CKX2se0/2s7Q/nJA==} + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} - eslint-visitor-keys@4.0.0: - resolution: {integrity: sha512-OtIRv/2GyiF6o/d8K7MYKKbXrOUBIK6SfkIRM4Z0dY3w+LiQ0vy3F57m0Z71bjbyeiWFiHJ8brqnmE6H6/jEuw==} + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - eslint@9.10.0: - resolution: {integrity: sha512-Y4D0IgtBZfOcOUAIQTSXBKoNGfY0REGqHJG6+Q81vNippW5YlKjHFj4soMxamKK1NXHUWuBZTLdU3Km+L/pcHw==} + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} hasBin: true peerDependencies: @@ -1170,17 +1184,12 @@ packages: jiti: optional: true - espree@10.1.0: - resolution: {integrity: sha512-M1M6CpiE6ffoigIOWYO9UDP8TMUw9kqb21tf+08IgDYjCsOvCuDt4jQcZmoYxx+w7zlKw9/N0KXfto+I8/FrXA==} + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - esquery@1.6.0: - resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} engines: {node: '>=0.10'} esrecurse@4.3.0: @@ -1199,8 +1208,11 @@ packages: resolution: {integrity: sha512-HK5GhnEAkm7fLy249GtF7DIuYmjLm85Ft6ssj7DhVl8Tx/z9+v0W6aiIVUdT4AXWGYy5Fc+s6gqBI49Bf0LejQ==} engines: {node: '>=4'} - eventemitter3@5.0.1: - resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + events-universal@1.0.1: + resolution: {integrity: sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==} events@3.3.0: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} @@ -1210,44 +1222,31 @@ packages: resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} engines: {node: '>=6'} - extendable-error@0.1.7: - resolution: {integrity: sha512-UOiS2in6/Q0FK0R0q6UY9vYpQ21mr/Qn1KOnte7vsACuNJf514WvCCUHSRCPcgjPT2bAhNIJdlE6bVap1GKmeg==} - - external-editor@3.1.0: - resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} - engines: {node: '>=4'} - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-fifo@1.3.2: resolution: {integrity: sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==} - fast-glob@3.3.2: - resolution: {integrity: sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==} - engines: {node: '>=8.6.0'} - fast-json-stable-stringify@2.1.0: resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} - fastq@1.17.1: - resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true file-entry-cache@8.0.0: resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} engines: {node: '>=16.0.0'} - fill-range@7.1.1: - resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} - engines: {node: '>=8'} - - find-up@4.1.0: - resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} - engines: {node: '>=8'} - find-up@5.0.0: resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} engines: {node: '>=10'} @@ -1256,28 +1255,23 @@ packages: resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} engines: {node: '>=16'} - flatted@3.3.1: - resolution: {integrity: sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==} + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} - fraction.js@4.3.7: - resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==} + fraction.js@5.3.4: + resolution: {integrity: sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==} fs-constants@1.0.0: resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} - fs-extra@7.0.1: - resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} - engines: {node: '>=6 <7 || >=8'} - - fs-extra@8.1.0: - resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} - engines: {node: '>=6 <7 || >=8'} - fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] + fzstd@0.1.1: + resolution: {integrity: sha512-dkuVSOKKwh3eas5VkJy1AW1vFpet8TA/fGmVA5krThl8YcOVE/8ZIoEA1+U1vEn5ckxxhLirSdY837azmbaNHA==} + gensync@1.0.0-beta.2: resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} engines: {node: '>=6.9.0'} @@ -1285,10 +1279,6 @@ packages: github-from-package@0.0.0: resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} - glob-parent@5.1.2: - resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} - engines: {node: '>= 6'} - glob-parent@6.0.2: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} @@ -1296,39 +1286,25 @@ packages: global@4.4.0: resolution: {integrity: sha512-wv/LAoHdRE3BeTGz53FAamhGlPLhlssK45usmGFThIi4XqnBmjKQ16u+RNbP7WvigRZDxUsM0J3gcQ5yicaL0w==} - globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} - globby@11.1.0: - resolution: {integrity: sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==} - engines: {node: '>=10'} + globals@15.15.0: + resolution: {integrity: sha512-7ACyT3wmyp3I61S4fG682L0VA2RGD9otkqGJIwNUMF1SWUombIIk+af1unuDYgMm082aHYwD+mzJvv9Iu8dsgg==} + engines: {node: '>=18'} - goober@2.1.14: - resolution: {integrity: sha512-4UpC0NdGyAFqLNPnhCT2iHpza2q+RAY3GV85a/mRPdzyPQMsj0KmMMuetdIkzWRbJ+Hgau1EZztq8ImmiMGhsg==} + goober@2.1.19: + resolution: {integrity: sha512-U7veizMqxyKlM58+Z5j2ngJBH/r9siDmxpvNxSw0PylF6WQvrASJEZrxh1hidRBJc2jqoBVSyOban5u8m+6Rxg==} peerDependencies: csstype: ^3.0.10 - graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - - graphemer@1.4.0: - resolution: {integrity: sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==} - - has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} - highlight.js@11.10.0: - resolution: {integrity: sha512-SYVnVFswQER+zu1laSya563s+F8VDGt7o35d4utbamowvUNLLMovFqwCLSocpZTz3MgaSRA1IbqRWZv97dtErQ==} + highlight.js@11.11.1: + resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} html-entities@2.3.3: @@ -1341,18 +1317,8 @@ packages: resolution: {integrity: sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==} engines: {node: '>=8'} - human-id@1.0.2: - resolution: {integrity: sha512-UNopramDEhHJD+VR+ehk8rOslwSfByxPIZyJRfV739NDhN5LF1fa1MqnzKm2lGTQRjNrjK19Q5fhkgIfjlVUKw==} - - i18next@23.14.0: - resolution: {integrity: sha512-Y5GL4OdA8IU2geRrt2+Uc1iIhsjICdHZzT9tNwQ3TVqdNzgxHToGCKf/TPRP80vTCAP6svg2WbbJL+Gx5MFQVA==} - - iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} - - idb-keyval@6.2.1: - resolution: {integrity: sha512-8Sb3veuYCyrZL+VBt9LJfZjLUPWVvqn8tG28VqYNFCo43KHcKuq+b4EiXGeuaLAQWL2YmyDgMp2aSpH9JHsEQg==} + idb-keyval@6.2.4: + resolution: {integrity: sha512-D/NzHWUmYJGXi++z67aMSrnisb9A3621CyRK5G89JyTlN13C8xf0g04DLxUKMufPem3e3L2JAXR6Z00OWy183Q==} ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -1361,11 +1327,15 @@ packages: resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} engines: {node: '>= 4'} - immutable@4.3.7: - resolution: {integrity: sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==} + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + immutable@5.1.5: + resolution: {integrity: sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==} - import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} imurmurhash@0.1.4: @@ -1378,12 +1348,8 @@ packages: ini@1.3.8: resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} - inline-style-parser@0.2.3: - resolution: {integrity: sha512-qlD8YNDqyTKTyuITrDOffsl6Tdhv+UC4hcdAVuQsK4IMQ99nSgd1MIA/Q+jQYoh9r3hVUXhYh7urSRmXPkW04g==} - - is-binary-path@2.1.0: - resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} - engines: {node: '>=8'} + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} @@ -1397,43 +1363,26 @@ packages: resolution: {integrity: sha512-S+OpgB5i7wzIue/YSE5hg0e5ZYfG3hhpNh9KGl6ayJ38p7ED6wxQLd1TV91xHpcTvw90KMJ9EwN3F/iNflHBVg==} engines: {node: '>=8'} - is-number@7.0.0: - resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} - engines: {node: '>=0.12.0'} - - is-path-inside@3.0.3: - resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} - engines: {node: '>=8'} - - is-subdir@1.2.0: - resolution: {integrity: sha512-2AT6j+gXe/1ueqbW6fLZJiIw3F8iXGJtt0yDrZaBhAZEG1raiTxKWU+IPqMCzQAXOUCKdA4UDMgacKH25XG2Cw==} - engines: {node: '>=4'} - is-what@4.1.16: resolution: {integrity: sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==} engines: {node: '>=12.13'} - is-windows@1.0.2: - resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==} - engines: {node: '>=0.10.0'} - isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + js-sha256@0.11.1: + resolution: {integrity: sha512-o6WSo/LUvY2uC4j7mO50a2ms7E/EAdbP0swigLV+nzHKTTaYnaLIWJ02VdXrsJX0vGedDESQnLsOekr94ryfjg==} + js-tokens@4.0.0: resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} - js-yaml@3.14.1: - resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} hasBin: true - js-yaml@4.1.0: - resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} - hasBin: true - - jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: @@ -1450,12 +1399,6 @@ packages: engines: {node: '>=6'} hasBin: true - jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} - - jsonfile@5.0.0: - resolution: {integrity: sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==} - kebab-case@1.0.2: resolution: {integrity: sha512-7n6wXq4gNgBELfDCpzKc+mRrZFs7D+wgfF5WRFLNAr4DA/qtr9Js8uOAVAfHhuLMfAcQ0pRKqbpjx+TcJVdE1Q==} @@ -1465,19 +1408,89 @@ packages: known-css-properties@0.30.0: resolution: {integrity: sha512-VSWXYUnsPu9+WYKkfmJyLKtIvaRJi1kXUqVmBACORXZQxT5oZDsoZ2vQP+bQFDnWtpI/4eq3MLoRMjI2fnLzTQ==} - konva@9.3.14: - resolution: {integrity: sha512-Gmm5lyikGYJyogKQA7Fy6dKkfNh350V6DwfZkid0RVrGYP2cfCsxuMxgF5etKeCv7NjXYpJxKqi1dYkIkX/dcA==} + konva@10.3.0: + resolution: {integrity: sha512-gt19K2gzY4lHbnkvsku7eSmB+A9PTS2jG4F9coBMsdjM1UKfJNxJbDbXVpeCW1wjEGRwBD3nBamcHnqJhAeKlg==} levn@0.4.1: resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} engines: {node: '>= 0.8.0'} - linkify-it@4.0.1: - resolution: {integrity: sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] - locate-path@5.0.0: - resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} - engines: {node: '>=8'} + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + linkify-it@5.0.0: + resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} locate-path@6.0.0: resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} @@ -1486,53 +1499,46 @@ packages: lodash.merge@4.6.2: resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} - lodash.startcase@4.4.0: - resolution: {integrity: sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==} - - lru-cache@4.1.5: - resolution: {integrity: sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==} - lru-cache@5.1.1: resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} - markdown-it-emoji@2.0.2: - resolution: {integrity: sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-it-emoji@3.0.0: + resolution: {integrity: sha512-+rUD93bXHubA4arpEZO3q80so0qgoFJEKRkRbjKX8RTdca89v2kfyF+xR3i2sQTwql9tpPZPOQN5B+PunspXRg==} - markdown-it@13.0.2: - resolution: {integrity: sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==} + markdown-it@14.1.1: + resolution: {integrity: sha512-BuU2qnTti9YKgK5N+IeMubp14ZUKUUw7yeJbkjtosvHiP0AZ5c8IAgEMk79D0eC8F23r4Ac/q8cAIFdm2FtyoA==} hasBin: true - match-sorter@6.3.4: - resolution: {integrity: sha512-jfZW7cWS5y/1xswZo8VBOdudUiSd9nifYRWphc9M5D/ee4w4AoXLgBEdRbgVaxbMuagBPeUC5y2Hi8DO6o9aDg==} + match-sorter@6.4.0: + resolution: {integrity: sha512-d4664ahzdL1QTTvmK1iI0JsrxWeJ6gn33qkYtnPg3mcn+naBLtXSgSPOe+X2vUgtgGwaAk3eiaj7gwKjjMAq+Q==} + deprecated: This was arguably a breaking change. Not in API, but more results can be returned. Upgrade to the next major when you are ready for that + + match-sorter@8.3.0: + resolution: {integrity: sha512-8Py1GbZi5zsclYSFcPAH4H5xfTbeD0bOREA7qP/t8bW4MbOSlPl8sbqHOedEV7O+Bxyvxm6xs/v6BXJGe+JDNA==} - mdurl@1.0.1: - resolution: {integrity: sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==} + mdurl@2.0.0: + resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} merge-anything@5.1.7: resolution: {integrity: sha512-eRtbOb1N5iyH0tkQDAoQ4Ipsp/5qSR79Dzrz8hEPxRX10RWWR/iQXdoKmBSRCThY1Fh5EhISDtpSc93fpxUniQ==} engines: {node: '>=12.13'} - merge2@1.4.1: - resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} - engines: {node: '>= 8'} - - micromatch@4.0.8: - resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} - engines: {node: '>=8.6'} - mimic-response@3.1.0: resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} engines: {node: '>=10'} - min-document@2.19.0: - resolution: {integrity: sha512-9Wy1B3m3f66bPPmU5hdA4DR4PB2OfDU/+GS3yAB7IQozE3tqXaVv2zOjgla7MEGSRv95+ILmOuvhLkOK6wJtCQ==} + min-document@2.19.2: + resolution: {integrity: sha512-8S5I8db/uZN8r9HSLFVWPdJCvYOejMcEC82VIzNUc6Zkklf/d1gg2psfE79/vyhWOj4+J8MtwmoOz3TmvaGu5A==} - minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} - minimatch@9.0.5: - resolution: {integrity: sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==} - engines: {node: '>=16 || 14 >=14.17'} + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimist@1.2.8: resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} @@ -1540,50 +1546,33 @@ packages: mkdirp-classic@0.5.3: resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - napi-build-utils@1.0.2: - resolution: {integrity: sha512-ONmRUqK7zj7DWX0D9ADe03wbwOBZxNAfF20PlGfCWQcD3+/MakShIHrMqx9YwPTfxDdF1zLeL+RGZiR9kGMLdg==} + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} natural-compare@1.4.0: resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} - node-abi@3.67.0: - resolution: {integrity: sha512-bLn/fU/ALVBE9wj+p4Y21ZJWYFjUXLXPi/IewyLZkx3ApxKDNBWCKdReeKOtD8dWpOdDCeMyLh6ZewzcLsG2Nw==} + node-abi@3.92.0: + resolution: {integrity: sha512-KdHvFWZjEKDf0cakgFjebl371GPsISX2oZHcuyKqM7DtogIsHrqKeLTo8wBHxaXRAQlY2PsPlZmfo+9ZCxEREQ==} engines: {node: '>=10'} - node-datachannel@0.10.1: - resolution: {integrity: sha512-rhxb1iQgbFLY6HMt3W6Xcs8Q1k4jIMgI7KduXcYvIn2UMKYK6e/eegya2caF/+XYAqTeo1743gOr11CXvJ/DJA==} - engines: {node: '>=16.0.0'} - - node-domexception@1.0.0: - resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} - engines: {node: '>=10.5.0'} - - node-domexception@2.0.1: - resolution: {integrity: sha512-M85rnSC7WQ7wnfQTARPT4LrK7nwCHLdDFOCcItZMhTQjyCebJH8GciKqYJNgaOFZs9nFmTmd/VMyi3OW5jA47w==} - engines: {node: '>=16'} + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} - node-releases@2.0.18: - resolution: {integrity: sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==} + node-datachannel@0.32.3: + resolution: {integrity: sha512-Aok1ZhLsll472lRefgWYuWJ0070jh0ecHravTdRyZEmoESumebMEQV8Y+poBwSW2ZbEwAokAOGsK5Cu8pDDT2g==} + engines: {node: '>=18.20.0'} - normalize-path@3.0.0: - resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} - engines: {node: '>=0.10.0'} - - normalize-range@0.1.2: - resolution: {integrity: sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==} - engines: {node: '>=0.10.0'} + node-releases@2.0.44: + resolution: {integrity: sha512-5WUyunoPMsvvEhS8AxHtRzP+oA8UCkJ7YRxatWKjngndhDGLiqEVAQKWjFAiAiuL8zMRGzGSJxFnLetoa43qGQ==} once@1.4.0: resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} @@ -1595,48 +1584,21 @@ packages: opus-media-recorder@0.8.0: resolution: {integrity: sha512-AIvJMpnJqZ18dFAU7Amtt5cZZp8oPzDoAOtobdTcLzwVNm/j815+GJmBupBzBZGBa4L940TEulm7Uu4tGOYDGQ==} - os-tmpdir@1.0.2: - resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} - engines: {node: '>=0.10.0'} - - outdent@0.5.0: - resolution: {integrity: sha512-/jHxFIzoMXdqPzTaCpFzAAWhpkSjZPF4Vsn6jAfNpmbH/ymsmd7Qc6VE9BGn0L6YMj6uwpQLxCECpus4ukKS9Q==} - - p-filter@2.1.0: - resolution: {integrity: sha512-ZBxxZ5sL2HghephhpGAQdoskxplTwr7ICaehZwLIlfL6acuVgZPm8yBNuRAFBGEqtD/hmUeq9eqLg2ys9Xr/yw==} - engines: {node: '>=8'} - - p-limit@2.3.0: - resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} - engines: {node: '>=6'} - p-limit@3.1.0: resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} engines: {node: '>=10'} - p-locate@4.1.0: - resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} - engines: {node: '>=8'} - p-locate@5.0.0: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} - p-map@2.1.0: - resolution: {integrity: sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==} - engines: {node: '>=6'} - - p-try@2.2.0: - resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} - engines: {node: '>=6'} - - package-manager-detector@0.2.0: - resolution: {integrity: sha512-E385OSk9qDcXhcM9LNSe4sdhx8a9mAPrZ4sMLW+tmxl5ZuGtPUcdFu+MPP2jbgiWAZ6Pfe5soGFMd+0Db5Vrog==} - parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -1645,80 +1607,66 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} - path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} - picocolors@1.1.0: - resolution: {integrity: sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} - picomatch@2.3.1: - resolution: {integrity: sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==} - engines: {node: '>=8.6'} + postcss-nested@7.0.2: + resolution: {integrity: sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==} + engines: {node: '>=18.0'} + peerDependencies: + postcss: ^8.2.14 - pify@4.0.1: - resolution: {integrity: sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==} - engines: {node: '>=6'} + postcss-selector-parser@7.1.1: + resolution: {integrity: sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==} + engines: {node: '>=4'} postcss-value-parser@4.2.0: resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} - postcss@8.4.45: - resolution: {integrity: sha512-7KTLTdzdZZYscUc65XmjFiB73vBhBfbPztCYdUNvlaso9PrzjzcmjqBPR0lNGkcVlcO4BjiO5rK/qNz+XAen1Q==} + postcss@8.5.15: + resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} - prebuild-install@7.1.2: - resolution: {integrity: sha512-UnNke3IQb6sgarcZIDU3gbMeTp/9SSU1DAIkil7PrqG1vZlBtY5msYccSKSHDqa3hNg436IXK+SNImReuA1wEQ==} + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. hasBin: true prelude-ls@1.2.1: resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} engines: {node: '>= 0.8.0'} - prettier@2.8.8: - resolution: {integrity: sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q==} - engines: {node: '>=10.13.0'} - hasBin: true - process@0.11.10: resolution: {integrity: sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==} engines: {node: '>= 0.6.0'} - pseudomap@1.0.2: - resolution: {integrity: sha512-b/YwNhb8lk1Zz2+bXXpS/LK9OisiZZ1SNsSLxN1x2OXVEhW2Ckr/7mWE5vrC1ZTiJlD9g19jWszTmJsB+oEpFQ==} + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - pump@3.0.0: - resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} + punycode.js@2.3.1: + resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} + engines: {node: '>=6'} punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} - queue-microtask@1.2.3: - resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} - - queue-tick@1.0.1: - resolution: {integrity: sha512-kJt5qhMxoszgU/62PLP1CJytzd2NKetjSRnyuj31fDd3Rlcz3fzlFdFLD1SItunPwyqEOkca6GbV612BWfaBag==} - rc@1.2.8: resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} hasBin: true - read-yaml-file@1.1.0: - resolution: {integrity: sha512-VIMnQi/Z4HT2Fxuwg5KrY174U1VdUIASQVWXXyqtNRtxSr9IYkn1rsI6Tb6HsrHCmB7gVpNwX6JxPTHcH6IoTA==} - engines: {node: '>=6'} - readable-stream@3.6.2: resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} engines: {node: '>= 6'} - readdirp@3.6.0: - resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} - engines: {node: '>=8.10.0'} - - regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} + readdirp@4.1.2: + resolution: {integrity: sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==} + engines: {node: '>= 14.18.0'} remove-accents@0.5.0: resolution: {integrity: sha512-8g3/Otx1eJaVD12e31UbJj1YzdtVvzH85HV7t+9MJYk/u3XmkOUJ5Ys9wQrf9PCPK8+xn4ymzqYCiZl6QWKn+A==} @@ -1727,30 +1675,136 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} - resolve-from@5.0.0: - resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} - engines: {node: '>=8'} - - reusify@1.0.4: - resolution: {integrity: sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==} - engines: {iojs: '>=1.0.0', node: '>=0.10.0'} - - rollup@4.21.2: - resolution: {integrity: sha512-e3TapAgYf9xjdLvKQCkQTnbTKd4a6jwlpQSJJFokHGaX2IVjoEqkIIhiQfqsi0cdwlOD+tQGuOd5AJkc5RngBw==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} + rolldown@1.0.2: + resolution: {integrity: sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true - run-parallel@1.2.0: - resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rxjs@7.8.2: + resolution: {integrity: sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==} safe-buffer@5.2.1: resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sass-embedded-all-unknown@1.99.0: + resolution: {integrity: sha512-qPIRG8Uhjo6/OKyAKixTnwMliTz+t9K6Duk0mx5z+K7n0Ts38NSJz2sjDnc7cA/8V9Lb3q09H38dZ1CLwD+ssw==} + cpu: ['!arm', '!arm64', '!riscv64', '!x64'] + + sass-embedded-android-arm64@1.99.0: + resolution: {integrity: sha512-fNHhdnP23yqqieCbAdym4N47AleSwjbNt6OYIYx4DdACGdtERjQB4iOX/TaKsW034MupfF7SjnAAK8w7Ptldtg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [android] + + sass-embedded-android-arm@1.99.0: + resolution: {integrity: sha512-EHvJ0C7/VuP78Qr6f8gIUVUmCqIorEQpw2yp3cs3SMg02ZuumlhjXvkTcFBxHmFdFR23vTNk1WnhY6QSeV1nFQ==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [android] + + sass-embedded-android-riscv64@1.99.0: + resolution: {integrity: sha512-4zqDFRvgGDTL5vTHuIhRxUpXFoh0Cy7Gm5Ywk19ASd8Settmd14YdPRZPmMxfgS1GH292PofV1fq1ifiSEJWBw==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [android] + + sass-embedded-android-x64@1.99.0: + resolution: {integrity: sha512-Uk53k/dGYt04RjOL4gFjZ0Z9DH9DKh8IA8WsXUkNqsxerAygoy3zqRBS2zngfE9K2jiOM87q+1R1p87ory9oQQ==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [android] + + sass-embedded-darwin-arm64@1.99.0: + resolution: {integrity: sha512-u61/7U3IGLqoO6gL+AHeiAtlTPFwJK1+964U8gp45ZN0hzh1yrARf5O1mivXv8NnNgJvbG2wWJbiNZP0lG/lTg==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [darwin] + + sass-embedded-darwin-x64@1.99.0: + resolution: {integrity: sha512-j/kkk/NcXdIameLezSfXjgCiBkVcA+G60AXrX768/3g0miK1g7M9dj7xOhCb1i7/wQeiEI3rw2LLuO63xRIn4A==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [darwin] + + sass-embedded-linux-arm64@1.99.0: + resolution: {integrity: sha512-btNcFpItcB56L40n8hDeL7sRSMLDXQ56nB5h2deddJx1n60rpKSElJmkaDGHtpkrY+CTtDRV0FZDjHeTJddYew==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: glibc + + sass-embedded-linux-arm@1.99.0: + resolution: {integrity: sha512-d4IjJZrX2+AwB2YCy1JySwdptJECNP/WfAQLUl8txI3ka8/d3TUI155GtelnoZUkio211PwIeFvvAeZ9RXPQnw==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: glibc + + sass-embedded-linux-musl-arm64@1.99.0: + resolution: {integrity: sha512-Hi2bt/IrM5P4FBKz6EcHAlniwfpoz9mnTdvSd58y+avA3SANM76upIkAdSayA8ZGwyL3gZokru1AKDPF9lJDNw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-arm@1.99.0: + resolution: {integrity: sha512-2gvHOupgIw3ytatXT4nFUow71LFbuOZPEwG+HUzcNQDH8ue4Ez8cr03vsv5MDv3lIjOKcXwDvWD980t18MwkoQ==} + engines: {node: '>=14.0.0'} + cpu: [arm] + os: [linux] + libc: musl + + sass-embedded-linux-musl-riscv64@1.99.0: + resolution: {integrity: sha512-mKqGvVaJ9rHMqyZsF0kikQe4NO0f4osb67+X6nLhBiVDKvyazQHJ3zJQreNefIE36yL2sjHIclSB//MprzaQDg==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: musl + + sass-embedded-linux-musl-x64@1.99.0: + resolution: {integrity: sha512-huhgOMmOc30r7CH7qbRbT9LerSEGSnWuS4CYNOskr9BvNeQp4dIneFufNRGZ7hkOAxUM8DglxIZJN/cyAT95Ew==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: musl + + sass-embedded-linux-riscv64@1.99.0: + resolution: {integrity: sha512-mevFPIFAVhrH90THifxLfOntFmHtcEKOcdWnep2gJ0X4DVva4AiVIRlQe/7w9JFx5+gnDRE1oaJJkzuFUuYZsA==} + engines: {node: '>=14.0.0'} + cpu: [riscv64] + os: [linux] + libc: glibc + + sass-embedded-linux-x64@1.99.0: + resolution: {integrity: sha512-9k7IkULqIZdCIVt4Mboryt6vN8Mjmm3EhI1P3mClU5y5i3wLK5ExC3cbVWk047KsID/fvB1RLslqghXJx5BoxA==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [linux] + libc: glibc + + sass-embedded-unknown-all@1.99.0: + resolution: {integrity: sha512-P7MxiUtL/XzGo3PX0CaB8lNNEFLQWKikPA8pbKytx9ZCLZSDkt2NJcdAbblB/sqMs4AV3EK2NadV8rI/diq3xg==} + os: ['!android', '!darwin', '!linux', '!win32'] + + sass-embedded-win32-arm64@1.99.0: + resolution: {integrity: sha512-8whpsW7S+uO8QApKfQuc36m3P9EISzbVZOgC79goob4qGy09u8Gz/rYvw8h1prJDSjltpHGhOzBE6LDz7WvzVw==} + engines: {node: '>=14.0.0'} + cpu: [arm64] + os: [win32] + + sass-embedded-win32-x64@1.99.0: + resolution: {integrity: sha512-ipuOv1R2K4MHeuCEAZGpuUbAgma4gb0sdacyrTjJtMOy/OY9UvWfVlwErdB09KIkp4fPDpQJDJfvYN6bC8jeNg==} + engines: {node: '>=14.0.0'} + cpu: [x64] + os: [win32] + + sass-embedded@1.99.0: + resolution: {integrity: sha512-gF/juR1aX02lZHkvwxdF80SapkQeg2fetoDF6gIQkNbSw5YEUFspMkyGTjPjgZSgIHuZpy+Wz4PlebKnLXMjdg==} + engines: {node: '>=16.0.0'} + hasBin: true - sass@1.78.0: - resolution: {integrity: sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==} + sass@1.99.0: + resolution: {integrity: sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==} engines: {node: '>=14.0.0'} hasBin: true @@ -1758,71 +1812,56 @@ packages: resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true - semver@7.6.3: - resolution: {integrity: sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==} + semver@7.8.0: + resolution: {integrity: sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==} engines: {node: '>=10'} hasBin: true - seroval-plugins@1.1.1: - resolution: {integrity: sha512-qNSy1+nUj7hsCOon7AO4wdAIo9P0jrzAMp18XhiOzA6/uO5TKtP7ScozVJ8T293oRIvi5wyCHSM4TrJo/c/GJA==} + seroval-plugins@1.5.4: + resolution: {integrity: sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 - seroval@1.1.1: - resolution: {integrity: sha512-rqEO6FZk8mv7Hyv4UCj3FD3b6Waqft605TLfsCe/BiaylRpyyMC0b+uA5TJKawX3KzMrdi3wsLbCaLplrQmBvQ==} + seroval@1.5.4: + resolution: {integrity: sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==} engines: {node: '>=10'} - shebang-command@1.2.0: - resolution: {integrity: sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==} - engines: {node: '>=0.10.0'} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} - shebang-regex@1.0.0: - resolution: {integrity: sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==} - engines: {node: '>=0.10.0'} - shebang-regex@3.0.0: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} - signal-exit@3.0.7: - resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} - simple-concat@1.0.1: resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} simple-get@4.0.1: resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} - slash@3.0.0: - resolution: {integrity: sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==} - engines: {node: '>=8'} - - socket.io-client@4.7.5: - resolution: {integrity: sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==} + socket.io-client@4.8.3: + resolution: {integrity: sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==} engines: {node: '>=10.0.0'} - socket.io-parser@4.2.4: - resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + socket.io-parser@4.2.6: + resolution: {integrity: sha512-asJqbVBDsBCJx0pTqw3WfesSY0iRX+2xzWEWzrpcH7L6fLzrhyF8WPI8UaeM4YCuDfpwA/cgsdugMsmtz8EJeg==} engines: {node: '>=10.0.0'} - solid-codemirror@2.3.1: - resolution: {integrity: sha512-9J5k8Vl9IG7Z7Gc4i0lfackWOmm1g6Obbk7OQHvw2A7WqcP/Ax7i/kfDDVlofjVKPE/9MZSsP94PvroabpJoAw==} + solid-codemirror@2.3.3: + resolution: {integrity: sha512-Qu8VxPc6fv/ZcDZzYWM6Oznp8z4A1gqsSdhyYR3QfjOFVmG3feT0nUfZ5ecV2Oov0XB1jLvFOowvgtZ/7rczzw==} engines: {node: '>=14'} peerDependencies: - '@codemirror/state': ^6.2.0 - '@codemirror/view': ^6.12.0 + '@codemirror/state': ^6.6.0 + '@codemirror/view': ^6.40.0 solid-js: ^1.7.0 - solid-js@1.8.22: - resolution: {integrity: sha512-VBzN5j+9Y4rqIKEnK301aBk+S7fvFSTs9ljg+YEdFxjNjH0hkjXPiQRcws9tE5fUzMznSS6KToL5hwMfHDgpLA==} + solid-js@1.9.13: + resolution: {integrity: sha512-6hJeJMOcEX8ktqjpDoJZEmld3ijvcvWBDtiXBm7f4332SiFN66QeAQI1REQshvyUoISsSeJ4PHDauKYbwao9JQ==} - solid-navigator@0.3.14: - resolution: {integrity: sha512-5t9MIeyLhQnoy0CCSXx1RHCsKO3nf4To7asL6kJcfWjmLNXyoTJfV28PQIDy5/7nNCdbRhZqhIaLIp+6kzuviw==} + solid-navigator@0.4.1: + resolution: {integrity: sha512-pVYgOKhvryncME+xHQcrhluC+feO2jFBboKqrUB4urzg3Ow+NUFN4LjFjfFNvbG/CLZrcMVMS9K5ZBv/wT7JUg==} engines: {node: '>=18', pnpm: '>=8.6.0'} peerDependencies: solid-js: ^1.6.0 @@ -1832,43 +1871,30 @@ packages: peerDependencies: solid-js: ^1.3 - solid-sortablejs@2.1.2: - resolution: {integrity: sha512-+4PFGsITCAMahYf13SlvkTxqJev34Cu0PdzgAWQr3rIR+yTozQRkyRMZfPz9znpmsF4jIkMPcQIMTghMFE+hfA==} + solid-sortablejs@2.1.8: + resolution: {integrity: sha512-GwuCPh9EP+JJaAldySGQ+yTyrA7f/SMCSIFsIvmEe3A3eOfqDipOXleloVxCQMJRRao8hBkxkJEWxVFQqyMIZA==} + engines: {node: '>=18', pnpm: '>=9.0.0'} peerDependencies: - solid-js: '>=1.0.0' + solid-js: ^1.6.0 solid-styled-components@0.28.5: resolution: {integrity: sha512-vwTcdp76wZNnESIzB6rRZ3U55NgcSAQXCiiRIiEFhxTFqT0bEh/warNT1qaRZu4OkAzrBkViOngF35ktI8sc4A==} peerDependencies: solid-js: ^1.4.4 - sortablejs@1.15.3: - resolution: {integrity: sha512-zdK3/kwwAK1cJgy1rwl1YtNTbRmc8qW/+vgXf75A7NHag5of4pyI6uK86ktmQETyWRH7IGaE73uZOOBcGxgqZg==} + sortablejs@1.15.7: + resolution: {integrity: sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A==} - source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} - spawndamnit@2.0.0: - resolution: {integrity: sha512-j4JKEcncSjFlqIwU5L/rp2N5SIPsdxaRsIv678+TZxZ0SRDJTm8JrxJMjE/XuiEZNEir3S8l0Fa3Ke339WI4qA==} - - sprintf-js@1.0.3: - resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - - streamx@2.20.0: - resolution: {integrity: sha512-ZGd1LhDeGFucr1CUCTBOS58ZhEendd0ttpGT3usTvosS4ntIwKN9LJFp+OeCSprsCPL14BXVRZlHGRY1V9PVzQ==} + streamx@2.25.0: + resolution: {integrity: sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==} string_decoder@1.3.0: resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} - strip-ansi@6.0.1: - resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} - engines: {node: '>=8'} - - strip-bom@3.0.0: - resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} - engines: {node: '>=4'} - strip-json-comments@2.0.1: resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} engines: {node: '>=0.10.0'} @@ -1877,36 +1903,43 @@ packages: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} - style-mod@4.1.2: - resolution: {integrity: sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==} + style-mod@4.1.3: + resolution: {integrity: sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==} - style-to-object@1.0.7: - resolution: {integrity: sha512-uSjr59G5u6fbxUfKbb8GcqMGT3Xs9v5IbPkjb0S16GyOeBLAzSRK0CixBv5YrYvzO6TDLzIS6QCn78tkqWngPw==} - - supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} - tar-fs@2.1.1: - resolution: {integrity: sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==} + supports-color@8.1.1: + resolution: {integrity: sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==} + engines: {node: '>=10'} + + sync-child-process@1.0.2: + resolution: {integrity: sha512-8lD+t2KrrScJ/7KXCSyfhT3/hRq78rC0wBFqNJXv3mZyn6hW2ypM05JmlSvtqRbeq6jqA94oHbxAr2vYsJ8vDA==} + engines: {node: '>=16.0.0'} + + sync-message-port@1.2.0: + resolution: {integrity: sha512-gAQ9qrUN/UCypHtGFbbe7Rc/f9bzO88IwrG8TDo/aMKAApKyD6E3W4Cm0EfhfBb6Z6SKt59tTCTfD+n1xmAvMg==} + engines: {node: '>=16.0.0'} + + tar-fs@2.1.4: + resolution: {integrity: sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==} tar-stream@2.2.0: resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} engines: {node: '>=6'} - term-size@2.2.1: - resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} - engines: {node: '>=8'} + temporal-polyfill@0.3.2: + resolution: {integrity: sha512-TzHthD/heRK947GNiSu3Y5gSPpeUDH34+LESnfsq8bqpFhsB79HFBX8+Z834IVX68P3EUyRPZK5bL/1fh437Eg==} - text-decoder@1.1.1: - resolution: {integrity: sha512-8zll7REEv4GDD3x4/0pW+ppIxSNs7H1J10IKFZsuOMscumCdM2a+toDGLPA3T+1+fLBql4zbt5z83GEQGGV5VA==} + temporal-spec@0.3.1: + resolution: {integrity: sha512-B4TUhezh9knfSIMwt7RVggApDRJZo73uZdj8AacL2mZ8RP5KtLianh2MXxL06GN9ESYiIsiuoLQhgVfwe55Yhw==} - text-table@0.2.0: - resolution: {integrity: sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==} + text-decoder@1.2.7: + resolution: {integrity: sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==} thememirror@2.0.1: resolution: {integrity: sha512-d5i6FVvWWPkwrm4cHLI3t9AT1OrkAt7Ig8dtdYSofgF7C/eiyNuq6zQzSTusWTde3jpW9WLvA9J/fzNKMUsd0w==} @@ -1915,57 +1948,42 @@ packages: '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 - tmp@0.0.33: - resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} - engines: {node: '>=0.6.0'} - - to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - - to-regex-range@5.0.1: - resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} - engines: {node: '>=8.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} - ts-api-utils@1.3.0: - resolution: {integrity: sha512-UQMIo7pb8WRomKR1/+MFVLTroIvDVtMX3K6OUir8ynLyzB8Jeriont2bTAtmNPa1ekAgN7YPDyf6V+ygrdU+eQ==} - engines: {node: '>=16'} + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} peerDependencies: - typescript: '>=4.2.0' + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} - twemoji-parser@14.0.0: - resolution: {integrity: sha512-9DUOTGLOWs0pFWnh1p6NF+C3CkQ96PWmEFwhOVmT3WbecRC+68AIqpsnJXygfkFcp4aXbOp8Dwbhh/HQgvoRxA==} - - twemoji@14.0.2: - resolution: {integrity: sha512-BzOoXIe1QVdmsUmZ54xbEH+8AgtOKUiG53zO5vVP2iUu6h5u9lN15NcuS6te4OY96qx0H7JK9vjjl9WQbkTRuA==} - type-check@0.4.0: resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} engines: {node: '>= 0.8.0'} - typescript@5.5.4: - resolution: {integrity: sha512-Mtq29sKDAEYP7aljRgtPOpTvOfbwRWlS6dPRzwjdE+C0R4brX/GUyhHSecbHMFLNBLcJIPt9nl9yG5TZ1weH+Q==} + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} engines: {node: '>=14.17'} hasBin: true - uc.micro@1.0.6: - resolution: {integrity: sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==} - - uint8-util@2.2.5: - resolution: {integrity: sha512-/QxVQD7CttWpVUKVPz9znO+3Dd4BdTSnFQ7pv/4drVhC9m4BaL2LFHTkJn6EsYoxT79VDq/2Gg8L0H22PrzyMw==} + uc.micro@2.1.0: + resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} - undici-types@6.19.8: - resolution: {integrity: sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==} + uint8-util@2.2.6: + resolution: {integrity: sha512-r+ZjS8CzPhtPF771ROOadUoqC40OVdiMKBI8lTfJQWb4W7+73sMBwMYmai/uvNcmZ7tBJJyZSad03yMWIt3RQg==} - universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} - update-browserslist-db@1.1.0: - resolution: {integrity: sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' @@ -1979,38 +1997,46 @@ packages: uzip@0.20201231.0: resolution: {integrity: sha512-OZeJfZP+R0z9D6TmBgLq2LHzSSptGMGDGigGiEe0pr8UBe/7fdflgHlHBNDASTXB5jnFuxHpNaJywSg8YFeGng==} - validate-html-nesting@1.2.2: - resolution: {integrity: sha512-hGdgQozCsQJMyfK5urgFcWEqsSSrK63Awe0t/IMR0bZ0QMtnuaiHzThW81guu3qx9abLi99NEuiaN6P9gVYsNg==} + varint@6.0.0: + resolution: {integrity: sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==} - vite-plugin-solid@2.10.2: - resolution: {integrity: sha512-AOEtwMe2baBSXMXdo+BUwECC8IFHcKS6WQV/1NEd+Q7vHPap5fmIhLcAzr+DUJ04/KHx/1UBU0l1/GWP+rMAPQ==} + vite-plugin-solid@2.11.12: + resolution: {integrity: sha512-FgjPcx2OwX9h6f28jli7A4bG7PP3te8uyakE5iqsmpq3Jqi1TWLgSroC9N6cMfGRU2zXsl4Q6ISvTr2VL0QHpA==} peerDependencies: '@testing-library/jest-dom': ^5.16.6 || ^5.17.0 || ^6.* solid-js: ^1.7.2 - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: '@testing-library/jest-dom': optional: true - vite@5.4.3: - resolution: {integrity: sha512-IH+nl64eq9lJjFqU+/yrRnrHPVTlgy42/+IzbOdaFDVlyLgI/wDlf+FCobXLX1cT0X5+7LMyH1mIy2xJdLfo8Q==} - engines: {node: ^18.0.0 || >=20.0.0} + vite@8.0.14: + resolution: {integrity: sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - sass-embedded: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.18 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 peerDependenciesMeta: '@types/node': optional: true - less: + '@vitejs/devtools': + optional: true + esbuild: optional: true - lightningcss: + jiti: + optional: true + less: optional: true sass: optional: true @@ -2022,11 +2048,15 @@ packages: optional: true terser: optional: true + tsx: + optional: true + yaml: + optional: true - vitefu@0.2.5: - resolution: {integrity: sha512-SgHtMLoqaeeGnd2evZ849ZbACbnwQCIwRH57t18FxcXoZop0uQu0uzlIhJBlF/eWVzuce0sHeqPcDo+evVcg8Q==} + vitefu@1.1.3: + resolution: {integrity: sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==} peerDependencies: - vite: ^3.0.0 || ^4.0.0 || ^5.0.0 + vite: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 peerDependenciesMeta: vite: optional: true @@ -2037,14 +2067,10 @@ packages: w3c-keyname@2.2.8: resolution: {integrity: sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==} - webrtc-polyfill@1.1.8: - resolution: {integrity: sha512-ms2rE5MEg1KXQX45sjl2QaIIevhpPogqoFz7Z1MAJYxWUuxFfI3L0SoiifrTNrWJiJiuFn/Dsf5OIGUWJFdU5g==} + webrtc-polyfill@1.2.0: + resolution: {integrity: sha512-epaVJbKzWOY5Wf3k7DoZLNgHP/5IoALBvjvlZQgX+9vFnf9UfCHv+rc+r/vJ7jxQUwH3cIYx9blHfyWWxGbw1g==} engines: {node: '>=16.0.0'} - which@1.3.1: - resolution: {integrity: sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==} - hasBin: true - which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -2057,8 +2083,8 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - ws@8.17.1: - resolution: {integrity: sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -2069,13 +2095,10 @@ packages: utf-8-validate: optional: true - xmlhttprequest-ssl@2.0.0: - resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + xmlhttprequest-ssl@2.1.2: + resolution: {integrity: sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==} engines: {node: '>=0.4.0'} - yallist@2.1.2: - resolution: {integrity: sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==} - yallist@3.1.1: resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} @@ -2083,806 +2106,709 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} - zoomist@2.1.1: - resolution: {integrity: sha512-QSvR5LbeyG7zgixfDsS5OAXX0fBuc+3FGk9AM3N/USzgppHGVEEYQfufAqwGqQtIZJLsG1R2Rg8edtz3W3kFPw==} + zoomist@2.2.0: + resolution: {integrity: sha512-+TkJ5Kw8ybTOjXy5bNhUxYlOshcwPlL2fU6t11jZur+4A5GR7wT7i8+FghFh1QqFaiBH34USR7Ak2/acNGI+0A==} snapshots: - '@ampproject/remapping@2.3.0': - dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - - '@babel/code-frame@7.24.7': - dependencies: - '@babel/highlight': 7.24.7 - picocolors: 1.1.0 - - '@babel/compat-data@7.25.4': {} - - '@babel/core@7.25.2': + '@babel/code-frame@7.29.0': dependencies: - '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.6 - '@babel/helper-compilation-targets': 7.25.2 - '@babel/helper-module-transforms': 7.25.2(@babel/core@7.25.2) - '@babel/helpers': 7.25.6 - '@babel/parser': 7.25.6 - '@babel/template': 7.25.0 - '@babel/traverse': 7.25.6 - '@babel/types': 7.25.6 + '@babel/helper-validator-identifier': 7.28.5 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.3': {} + + '@babel/core@7.29.0': + dependencies: + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-compilation-targets': 7.28.6 + '@babel/helper-module-transforms': 7.28.6(@babel/core@7.29.0) + '@babel/helpers': 7.29.2 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 + '@jridgewell/remapping': 2.3.5 convert-source-map: 2.0.0 - debug: 4.3.7 + debug: 4.4.3 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - '@babel/generator@7.25.6': + '@babel/generator@7.29.1': dependencies: - '@babel/types': 7.25.6 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 - jsesc: 2.5.2 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 - '@babel/helper-compilation-targets@7.25.2': + '@babel/helper-compilation-targets@7.28.6': dependencies: - '@babel/compat-data': 7.25.4 - '@babel/helper-validator-option': 7.24.8 - browserslist: 4.23.3 + '@babel/compat-data': 7.29.3 + '@babel/helper-validator-option': 7.27.1 + browserslist: 4.28.2 lru-cache: 5.1.1 semver: 6.3.1 - '@babel/helper-module-imports@7.18.6': - dependencies: - '@babel/types': 7.25.6 + '@babel/helper-globals@7.28.0': {} - '@babel/helper-module-imports@7.24.7': + '@babel/helper-module-imports@7.18.6': dependencies: - '@babel/traverse': 7.25.6 - '@babel/types': 7.25.6 - transitivePeerDependencies: - - supports-color + '@babel/types': 7.29.0 - '@babel/helper-module-transforms@7.25.2(@babel/core@7.25.2)': + '@babel/helper-module-imports@7.28.6': dependencies: - '@babel/core': 7.25.2 - '@babel/helper-module-imports': 7.24.7 - '@babel/helper-simple-access': 7.24.7 - '@babel/helper-validator-identifier': 7.24.7 - '@babel/traverse': 7.25.6 + '@babel/traverse': 7.29.0 + '@babel/types': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-plugin-utils@7.24.8': {} - - '@babel/helper-simple-access@7.24.7': + '@babel/helper-module-transforms@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/traverse': 7.25.6 - '@babel/types': 7.25.6 + '@babel/core': 7.29.0 + '@babel/helper-module-imports': 7.28.6 + '@babel/helper-validator-identifier': 7.28.5 + '@babel/traverse': 7.29.0 transitivePeerDependencies: - supports-color - '@babel/helper-string-parser@7.24.8': {} + '@babel/helper-plugin-utils@7.28.6': {} - '@babel/helper-validator-identifier@7.24.7': {} + '@babel/helper-string-parser@7.27.1': {} - '@babel/helper-validator-option@7.24.8': {} + '@babel/helper-validator-identifier@7.28.5': {} - '@babel/helpers@7.25.6': - dependencies: - '@babel/template': 7.25.0 - '@babel/types': 7.25.6 + '@babel/helper-validator-option@7.27.1': {} - '@babel/highlight@7.24.7': + '@babel/helpers@7.29.2': dependencies: - '@babel/helper-validator-identifier': 7.24.7 - chalk: 2.4.2 - js-tokens: 4.0.0 - picocolors: 1.1.0 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 - '@babel/parser@7.25.6': + '@babel/parser@7.29.3': dependencies: - '@babel/types': 7.25.6 + '@babel/types': 7.29.0 - '@babel/plugin-syntax-jsx@7.24.7(@babel/core@7.25.2)': + '@babel/plugin-syntax-jsx@7.28.6(@babel/core@7.29.0)': dependencies: - '@babel/core': 7.25.2 - '@babel/helper-plugin-utils': 7.24.8 + '@babel/core': 7.29.0 + '@babel/helper-plugin-utils': 7.28.6 - '@babel/runtime@7.25.6': - dependencies: - regenerator-runtime: 0.14.1 + '@babel/runtime@7.29.2': {} - '@babel/template@7.25.0': + '@babel/template@7.28.6': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/parser': 7.25.6 - '@babel/types': 7.25.6 + '@babel/code-frame': 7.29.0 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 - '@babel/traverse@7.25.6': + '@babel/traverse@7.29.0': dependencies: - '@babel/code-frame': 7.24.7 - '@babel/generator': 7.25.6 - '@babel/parser': 7.25.6 - '@babel/template': 7.25.0 - '@babel/types': 7.25.6 - debug: 4.3.7 - globals: 11.12.0 + '@babel/code-frame': 7.29.0 + '@babel/generator': 7.29.1 + '@babel/helper-globals': 7.28.0 + '@babel/parser': 7.29.3 + '@babel/template': 7.28.6 + '@babel/types': 7.29.0 + debug: 4.4.3 transitivePeerDependencies: - supports-color - '@babel/types@7.25.6': - dependencies: - '@babel/helper-string-parser': 7.24.8 - '@babel/helper-validator-identifier': 7.24.7 - to-fast-properties: 2.0.0 - - '@changesets/apply-release-plan@7.0.5': - dependencies: - '@changesets/config': 3.0.3 - '@changesets/get-version-range-type': 0.4.0 - '@changesets/git': 3.0.1 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - detect-indent: 6.1.0 - fs-extra: 7.0.1 - lodash.startcase: 4.4.0 - outdent: 0.5.0 - prettier: 2.8.8 - resolve-from: 5.0.0 - semver: 7.6.3 - - '@changesets/assemble-release-plan@6.0.4': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - semver: 7.6.3 - - '@changesets/changelog-git@0.2.0': - dependencies: - '@changesets/types': 6.0.0 - - '@changesets/cli@2.27.8': - dependencies: - '@changesets/apply-release-plan': 7.0.5 - '@changesets/assemble-release-plan': 6.0.4 - '@changesets/changelog-git': 0.2.0 - '@changesets/config': 3.0.3 - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 - '@changesets/get-release-plan': 4.0.4 - '@changesets/git': 3.0.1 - '@changesets/logger': 0.1.1 - '@changesets/pre': 2.0.1 - '@changesets/read': 0.6.1 - '@changesets/should-skip-package': 0.1.1 - '@changesets/types': 6.0.0 - '@changesets/write': 0.3.2 - '@manypkg/get-packages': 1.1.3 - '@types/semver': 7.5.8 - ansi-colors: 4.1.3 - ci-info: 3.9.0 - enquirer: 2.4.1 - external-editor: 3.1.0 - fs-extra: 7.0.1 - mri: 1.2.0 - outdent: 0.5.0 - p-limit: 2.3.0 - package-manager-detector: 0.2.0 - picocolors: 1.1.0 - resolve-from: 5.0.0 - semver: 7.6.3 - spawndamnit: 2.0.0 - term-size: 2.2.1 - - '@changesets/config@3.0.3': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/get-dependents-graph': 2.1.2 - '@changesets/logger': 0.1.1 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - micromatch: 4.0.8 - - '@changesets/errors@0.2.0': - dependencies: - extendable-error: 0.1.7 - - '@changesets/get-dependents-graph@2.1.2': - dependencies: - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - picocolors: 1.1.0 - semver: 7.6.3 - - '@changesets/get-release-plan@4.0.4': - dependencies: - '@changesets/assemble-release-plan': 6.0.4 - '@changesets/config': 3.0.3 - '@changesets/pre': 2.0.1 - '@changesets/read': 0.6.1 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/get-version-range-type@0.4.0': {} - - '@changesets/git@3.0.1': - dependencies: - '@changesets/errors': 0.2.0 - '@manypkg/get-packages': 1.1.3 - is-subdir: 1.2.0 - micromatch: 4.0.8 - spawndamnit: 2.0.0 - - '@changesets/logger@0.1.1': - dependencies: - picocolors: 1.1.0 - - '@changesets/parse@0.4.0': - dependencies: - '@changesets/types': 6.0.0 - js-yaml: 3.14.1 - - '@changesets/pre@2.0.1': - dependencies: - '@changesets/errors': 0.2.0 - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - fs-extra: 7.0.1 - - '@changesets/read@0.6.1': - dependencies: - '@changesets/git': 3.0.1 - '@changesets/logger': 0.1.1 - '@changesets/parse': 0.4.0 - '@changesets/types': 6.0.0 - fs-extra: 7.0.1 - p-filter: 2.1.0 - picocolors: 1.1.0 - - '@changesets/should-skip-package@0.1.1': - dependencies: - '@changesets/types': 6.0.0 - '@manypkg/get-packages': 1.1.3 - - '@changesets/types@4.1.0': {} - - '@changesets/types@6.0.0': {} - - '@changesets/write@0.3.2': + '@babel/types@7.29.0': dependencies: - '@changesets/types': 6.0.0 - fs-extra: 7.0.1 - human-id: 1.0.2 - prettier: 2.8.8 + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bufbuild/protobuf@2.12.0': {} - '@codemirror/autocomplete@6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1)': + '@codemirror/autocomplete@6.20.2': dependencies: - '@codemirror/language': 6.10.2 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.33.0 - '@lezer/common': 1.2.1 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 - '@codemirror/commands@6.6.1': + '@codemirror/commands@6.10.3': dependencies: - '@codemirror/language': 6.10.2 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.33.0 - '@lezer/common': 1.2.1 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 - '@codemirror/lang-css@6.3.0(@codemirror/view@6.33.0)': + '@codemirror/lang-css@6.3.1': dependencies: - '@codemirror/autocomplete': 6.18.0(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(@lezer/common@1.2.1) - '@codemirror/language': 6.10.2 - '@codemirror/state': 6.4.1 - '@lezer/common': 1.2.1 - '@lezer/css': 1.1.9 - transitivePeerDependencies: - - '@codemirror/view' + '@codemirror/autocomplete': 6.20.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@lezer/common': 1.5.2 + '@lezer/css': 1.3.3 - '@codemirror/language@6.10.2': + '@codemirror/language@6.12.3': dependencies: - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.33.0 - '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 - style-mod: 4.1.2 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 + style-mod: 4.1.3 - '@codemirror/state@6.4.1': {} + '@codemirror/state@6.6.0': + dependencies: + '@marijn/find-cluster-break': 1.0.2 - '@codemirror/view@6.33.0': + '@codemirror/view@6.43.0': dependencies: - '@codemirror/state': 6.4.1 - style-mod: 4.1.2 + '@codemirror/state': 6.6.0 + crelt: 1.0.6 + style-mod: 4.1.3 w3c-keyname: 2.2.8 - '@esbuild/aix-ppc64@0.21.5': - optional: true - - '@esbuild/android-arm64@0.21.5': + '@emnapi/core@1.10.0': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 optional: true - '@esbuild/android-arm@0.21.5': + '@emnapi/runtime@1.10.0': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/android-x64@0.21.5': + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 optional: true - '@esbuild/darwin-arm64@0.21.5': - optional: true + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4)': + dependencies: + eslint: 9.39.4 + eslint-visitor-keys: 3.4.3 - '@esbuild/darwin-x64@0.21.5': - optional: true + '@eslint-community/regexpp@4.12.2': {} - '@esbuild/freebsd-arm64@0.21.5': - optional: true + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color - '@esbuild/freebsd-x64@0.21.5': - optional: true + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 - '@esbuild/linux-arm64@0.21.5': - optional: true + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 - '@esbuild/linux-arm@0.21.5': - optional: true + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color - '@esbuild/linux-ia32@0.21.5': - optional: true + '@eslint/js@9.39.4': {} - '@esbuild/linux-loong64@0.21.5': - optional: true + '@eslint/object-schema@2.1.7': {} - '@esbuild/linux-mips64el@0.21.5': - optional: true + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 - '@esbuild/linux-ppc64@0.21.5': - optional: true + '@fontsource/dancing-script@5.2.8': {} - '@esbuild/linux-riscv64@0.21.5': - optional: true + '@fontsource/finger-paint@5.2.8': {} - '@esbuild/linux-s390x@0.21.5': - optional: true + '@fontsource/grandstander@5.2.7': {} - '@esbuild/linux-x64@0.21.5': - optional: true + '@fontsource/ibm-plex-mono@5.2.7': {} - '@esbuild/netbsd-x64@0.21.5': - optional: true + '@fontsource/indie-flower@5.2.7': {} - '@esbuild/openbsd-x64@0.21.5': - optional: true + '@fontsource/inter@5.2.8': {} - '@esbuild/sunos-x64@0.21.5': - optional: true + '@fontsource/mochiy-pop-one@5.2.8': {} - '@esbuild/win32-arm64@0.21.5': - optional: true + '@fontsource/pixelify-sans@5.2.7': {} - '@esbuild/win32-ia32@0.21.5': - optional: true + '@fontsource/roboto-slab@5.2.8': {} - '@esbuild/win32-x64@0.21.5': - optional: true + '@fontsource/sora@5.2.8': {} - '@eslint-community/eslint-utils@4.4.0(eslint@9.10.0)': - dependencies: - eslint: 9.10.0 - eslint-visitor-keys: 3.4.3 + '@formatjs/bigdecimal@0.2.5': {} - '@eslint-community/regexpp@4.11.0': {} + '@formatjs/fast-memoize@3.1.5': {} - '@eslint/config-array@0.18.0': + '@formatjs/intl-durationformat@0.10.12': dependencies: - '@eslint/object-schema': 2.1.4 - debug: 4.3.7 - minimatch: 3.1.2 - transitivePeerDependencies: - - supports-color + '@formatjs/bigdecimal': 0.2.5 + '@formatjs/intl-localematcher': 0.8.8 - '@eslint/eslintrc@3.1.0': + '@formatjs/intl-localematcher@0.8.8': dependencies: - ajv: 6.12.6 - debug: 4.3.7 - espree: 10.1.0 - globals: 14.0.0 - ignore: 5.3.2 - import-fresh: 3.3.0 - js-yaml: 4.1.0 - minimatch: 3.1.2 - strip-json-comments: 3.1.1 - transitivePeerDependencies: - - supports-color + '@formatjs/fast-memoize': 3.1.5 - '@eslint/js@9.10.0': {} - - '@eslint/object-schema@2.1.4': {} + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 - '@eslint/plugin-kit@0.1.0': + '@humanfs/node@0.16.8': dependencies: - levn: 0.4.1 + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} '@humanwhocodes/module-importer@1.0.1': {} - '@humanwhocodes/retry@0.3.0': {} + '@humanwhocodes/retry@0.4.3': {} - '@jridgewell/gen-mapping@0.3.5': + '@jridgewell/gen-mapping@0.3.13': dependencies: - '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.5.0 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/resolve-uri@3.1.2': {} + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 - '@jridgewell/set-array@1.2.1': {} + '@jridgewell/resolve-uri@3.1.2': {} - '@jridgewell/sourcemap-codec@1.5.0': {} + '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.5.0 - - '@lezer/common@1.2.1': {} - - '@lezer/css@1.1.9': - dependencies: - '@lezer/common': 1.2.1 - '@lezer/highlight': 1.2.1 - '@lezer/lr': 1.4.2 + '@jridgewell/sourcemap-codec': 1.5.5 - '@lezer/highlight@1.2.1': - dependencies: - '@lezer/common': 1.2.1 + '@lezer/common@1.5.2': {} - '@lezer/lr@1.4.2': + '@lezer/css@1.3.3': dependencies: - '@lezer/common': 1.2.1 + '@lezer/common': 1.5.2 + '@lezer/highlight': 1.2.3 + '@lezer/lr': 1.4.10 - '@manypkg/find-root@1.1.0': + '@lezer/highlight@1.2.3': dependencies: - '@babel/runtime': 7.25.6 - '@types/node': 12.20.55 - find-up: 4.1.0 - fs-extra: 8.1.0 + '@lezer/common': 1.5.2 - '@manypkg/get-packages@1.1.3': + '@lezer/lr@1.4.10': dependencies: - '@babel/runtime': 7.25.6 - '@changesets/types': 4.1.0 - '@manypkg/find-root': 1.1.0 - fs-extra: 8.1.0 - globby: 11.1.0 - read-yaml-file: 1.1.0 + '@lezer/common': 1.5.2 - '@material-symbols/font-400@0.21.3': {} + '@marijn/find-cluster-break@1.0.2': {} - '@maxim_mazurok/gapi.client.discovery-v1@0.1.20200806': + '@maxim_mazurok/gapi.client.discovery-v1@0.5.20200806': dependencies: '@types/gapi.client': 1.0.8 '@types/gapi.client.discovery-v1': 0.0.4 - '@maxim_mazurok/gapi.client.drive-v3@0.0.20230927': + '@maxim_mazurok/gapi.client.drive-v3@0.2.20260311': dependencies: '@types/gapi.client': 1.0.8 '@types/gapi.client.discovery-v1': 0.0.4 - '@mbarzda/solid-i18next@1.4.1(html-parse-string@0.0.9)(i18next@23.14.0)(solid-js@1.8.22)': + '@melloware/coloris@0.25.0': {} + + '@minht11/solid-virtual-container@0.2.1(solid-js@1.9.13)': + dependencies: + solid-js: 1.9.13 + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)': dependencies: - i18next: 23.14.0 - solid-js: 1.8.22 + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@nerimity/i18lite@1.2.0(html-parse-string@0.0.9)(solid-js@1.9.13)': + dependencies: + solid-js: 1.9.13 optionalDependencies: html-parse-string: 0.0.9 - '@melloware/coloris@0.24.0': {} + '@nerimity/nevula@0.15.0': {} - '@minht11/solid-virtual-container@0.2.1(solid-js@1.8.22)': + '@nerimity/solid-emoji-picker@0.4.9(solid-js@1.9.13)': dependencies: - solid-js: 1.8.22 - - '@nerimity/nevula@0.14.0': {} + '@minht11/solid-virtual-container': 0.2.1(solid-js@1.9.13) + match-sorter: 6.4.0 + solid-js: 1.9.13 + solid-styled-components: 0.28.5(solid-js@1.9.13) - '@nerimity/solid-emoji-picker@0.4.8(solid-js@1.8.22)': + '@nerimity/solid-i18lite@1.8.1(@nerimity/i18lite@1.2.0(html-parse-string@0.0.9)(solid-js@1.9.13))(html-parse-string@0.0.9)(solid-js@1.9.13)': dependencies: - '@minht11/solid-virtual-container': 0.2.1(solid-js@1.8.22) - match-sorter: 6.3.4 - solid-js: 1.8.22 - solid-styled-components: 0.28.5(solid-js@1.8.22) + '@nerimity/i18lite': 1.2.0(html-parse-string@0.0.9)(solid-js@1.9.13) + solid-js: 1.9.13 + optionalDependencies: + html-parse-string: 0.0.9 - '@nerimity/solid-opus-media-recorder@1.0.1(solid-js@1.8.22)': + '@nerimity/solid-opus-media-recorder@1.0.1(solid-js@1.9.13)': dependencies: opus-media-recorder: 0.8.0 - solid-js: 1.8.22 + solid-js: 1.9.13 - '@nerimity/solid-turnstile@1.1.0(solid-js@1.8.22)': + '@nerimity/solid-turnstile@1.1.0(solid-js@1.9.13)': dependencies: - solid-js: 1.8.22 + solid-js: 1.9.13 - '@nodelib/fs.scandir@2.1.5': - dependencies: - '@nodelib/fs.stat': 2.0.5 - run-parallel: 1.2.0 + '@oxc-project/types@0.132.0': {} + + '@parcel/watcher-android-arm64@2.5.6': + optional: true - '@nodelib/fs.stat@2.0.5': {} + '@parcel/watcher-darwin-arm64@2.5.6': + optional: true - '@nodelib/fs.walk@1.2.8': - dependencies: - '@nodelib/fs.scandir': 2.1.5 - fastq: 1.17.1 + '@parcel/watcher-darwin-x64@2.5.6': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.6': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.6': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.6': + optional: true - '@rollup/rollup-android-arm-eabi@4.21.2': + '@parcel/watcher-linux-arm64-musl@2.5.6': optional: true - '@rollup/rollup-android-arm64@4.21.2': + '@parcel/watcher-linux-x64-glibc@2.5.6': optional: true - '@rollup/rollup-darwin-arm64@4.21.2': + '@parcel/watcher-linux-x64-musl@2.5.6': optional: true - '@rollup/rollup-darwin-x64@4.21.2': + '@parcel/watcher-win32-arm64@2.5.6': optional: true - '@rollup/rollup-linux-arm-gnueabihf@4.21.2': + '@parcel/watcher-win32-ia32@2.5.6': optional: true - '@rollup/rollup-linux-arm-musleabihf@4.21.2': + '@parcel/watcher-win32-x64@2.5.6': optional: true - '@rollup/rollup-linux-arm64-gnu@4.21.2': + '@parcel/watcher@2.5.6': + dependencies: + detect-libc: 2.1.2 + is-glob: 4.0.3 + node-addon-api: 7.1.1 + picomatch: 4.0.4 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.6 + '@parcel/watcher-darwin-arm64': 2.5.6 + '@parcel/watcher-darwin-x64': 2.5.6 + '@parcel/watcher-freebsd-x64': 2.5.6 + '@parcel/watcher-linux-arm-glibc': 2.5.6 + '@parcel/watcher-linux-arm-musl': 2.5.6 + '@parcel/watcher-linux-arm64-glibc': 2.5.6 + '@parcel/watcher-linux-arm64-musl': 2.5.6 + '@parcel/watcher-linux-x64-glibc': 2.5.6 + '@parcel/watcher-linux-x64-musl': 2.5.6 + '@parcel/watcher-win32-arm64': 2.5.6 + '@parcel/watcher-win32-ia32': 2.5.6 + '@parcel/watcher-win32-x64': 2.5.6 + optional: true + + '@rolldown/binding-android-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.2': + optional: true + + '@rolldown/binding-darwin-x64@1.0.2': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.2': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.2': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.2': optional: true - '@rollup/rollup-linux-arm64-musl@4.21.2': + '@rolldown/binding-linux-arm64-musl@1.0.2': optional: true - '@rollup/rollup-linux-powerpc64le-gnu@4.21.2': + '@rolldown/binding-linux-ppc64-gnu@1.0.2': optional: true - '@rollup/rollup-linux-riscv64-gnu@4.21.2': + '@rolldown/binding-linux-s390x-gnu@1.0.2': optional: true - '@rollup/rollup-linux-s390x-gnu@4.21.2': + '@rolldown/binding-linux-x64-gnu@1.0.2': optional: true - '@rollup/rollup-linux-x64-gnu@4.21.2': + '@rolldown/binding-linux-x64-musl@1.0.2': optional: true - '@rollup/rollup-linux-x64-musl@4.21.2': + '@rolldown/binding-openharmony-arm64@1.0.2': optional: true - '@rollup/rollup-win32-arm64-msvc@4.21.2': + '@rolldown/binding-wasm32-wasi@1.0.2': + dependencies: + '@emnapi/core': 1.10.0 + '@emnapi/runtime': 1.10.0 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) optional: true - '@rollup/rollup-win32-ia32-msvc@4.21.2': + '@rolldown/binding-win32-arm64-msvc@1.0.2': optional: true - '@rollup/rollup-win32-x64-msvc@4.21.2': + '@rolldown/binding-win32-x64-msvc@1.0.2': optional: true + '@rolldown/pluginutils@1.0.1': {} + + '@sentry-internal/browser-utils@10.53.1': + dependencies: + '@sentry/core': 10.53.1 + + '@sentry-internal/feedback@10.53.1': + dependencies: + '@sentry/core': 10.53.1 + + '@sentry-internal/replay-canvas@10.53.1': + dependencies: + '@sentry-internal/replay': 10.53.1 + '@sentry/core': 10.53.1 + + '@sentry-internal/replay@10.53.1': + dependencies: + '@sentry-internal/browser-utils': 10.53.1 + '@sentry/core': 10.53.1 + + '@sentry/browser@10.53.1': + dependencies: + '@sentry-internal/browser-utils': 10.53.1 + '@sentry-internal/feedback': 10.53.1 + '@sentry-internal/replay': 10.53.1 + '@sentry-internal/replay-canvas': 10.53.1 + '@sentry/core': 10.53.1 + + '@sentry/core@10.53.1': {} + + '@sentry/solid@10.53.1(solid-js@1.9.13)': + dependencies: + '@sentry/browser': 10.53.1 + '@sentry/core': 10.53.1 + solid-js: 1.9.13 + '@socket.io/component-emitter@3.1.2': {} - '@solid-primitives/context@0.2.3(solid-js@1.8.22)': + '@solid-primitives/context@0.3.2(solid-js@1.9.13)': dependencies: - solid-js: 1.8.22 + solid-js: 1.9.13 - '@solid-primitives/keyed@1.2.2(solid-js@1.8.22)': + '@solid-primitives/keyed@1.5.3(solid-js@1.9.13)': dependencies: - solid-js: 1.8.22 + solid-js: 1.9.13 - '@solidjs/meta@0.29.4(solid-js@1.8.22)': + '@solidjs/meta@0.29.4(solid-js@1.9.13)': dependencies: - solid-js: 1.8.22 + solid-js: 1.9.13 - '@thaunknown/simple-peer@10.0.10': + '@thaunknown/simple-peer@10.1.0': dependencies: - debug: 4.3.7 + debug: 4.4.3 err-code: 3.0.1 - streamx: 2.20.0 - uint8-util: 2.2.5 - webrtc-polyfill: 1.1.8 + streamx: 2.25.0 + uint8-util: 2.2.6 + webrtc-polyfill: 1.2.0 transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a - supports-color + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + '@types/babel__core@7.20.5': dependencies: - '@babel/parser': 7.25.6 - '@babel/types': 7.25.6 - '@types/babel__generator': 7.6.8 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 + '@types/babel__generator': 7.27.0 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.6 + '@types/babel__traverse': 7.28.0 - '@types/babel__generator@7.6.8': + '@types/babel__generator@7.27.0': dependencies: - '@babel/types': 7.25.6 + '@babel/types': 7.29.0 '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.25.6 - '@babel/types': 7.25.6 + '@babel/parser': 7.29.3 + '@babel/types': 7.29.0 - '@types/babel__traverse@7.20.6': + '@types/babel__traverse@7.28.0': dependencies: - '@babel/types': 7.25.6 + '@babel/types': 7.29.0 - '@types/chroma-js@2.4.4': {} + '@types/chroma-js@3.1.2': {} '@types/croppie@2.6.4': {} - '@types/estree@1.0.5': {} + '@types/estree@1.0.9': {} '@types/gapi.client.discovery-v1@0.0.4': dependencies: - '@maxim_mazurok/gapi.client.discovery-v1': 0.1.20200806 + '@maxim_mazurok/gapi.client.discovery-v1': 0.5.20200806 '@types/gapi.client@1.0.8': {} - '@types/gapi@0.0.45': {} + '@types/gapi@0.0.47': {} - '@types/linkify-it@3.0.5': {} + '@types/json-schema@7.0.15': {} - '@types/markdown-it-emoji@2.0.5': - dependencies: - '@types/markdown-it': 13.0.9 + '@types/linkify-it@5.0.0': {} - '@types/markdown-it@13.0.9': + '@types/markdown-it-emoji@3.0.1': dependencies: - '@types/linkify-it': 3.0.5 - '@types/mdurl': 1.0.5 - - '@types/mdurl@1.0.5': {} - - '@types/node@12.20.55': {} + '@types/markdown-it': 14.1.2 - '@types/node@20.16.5': + '@types/markdown-it@14.1.2': dependencies: - undici-types: 6.19.8 + '@types/linkify-it': 5.0.0 + '@types/mdurl': 2.0.0 - '@types/semver@7.5.8': {} + '@types/mdurl@2.0.0': {} - '@types/simple-peer@9.11.8': + '@types/node@22.19.19': dependencies: - '@types/node': 20.16.5 + undici-types: 6.21.0 - '@types/sortablejs@1.15.8': {} + '@types/simple-peer@9.11.9': + dependencies: + '@types/node': 22.19.19 '@types/uzip@0.20201231.2': {} '@types/voice-activity-detection@0.0.5': {} - '@typescript-eslint/eslint-plugin@7.18.0(@typescript-eslint/parser@7.18.0(eslint@9.10.0)(typescript@5.5.4))(eslint@9.10.0)(typescript@5.5.4)': + '@typescript-eslint/eslint-plugin@8.59.4(@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3))(eslint@9.39.4)(typescript@6.0.3)': dependencies: - '@eslint-community/regexpp': 4.11.0 - '@typescript-eslint/parser': 7.18.0(eslint@9.10.0)(typescript@5.5.4) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/type-utils': 7.18.0(eslint@9.10.0)(typescript@5.5.4) - '@typescript-eslint/utils': 7.18.0(eslint@9.10.0)(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 7.18.0 - eslint: 9.10.0 - graphemer: 1.4.0 - ignore: 5.3.2 + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/type-utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + eslint: 9.39.4 + ignore: 7.0.5 natural-compare: 1.4.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/parser@7.18.0(eslint@9.10.0)(typescript@5.5.4)': + '@typescript-eslint/parser@8.59.4(eslint@9.39.4)(typescript@6.0.3)': dependencies: - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7 - eslint: 9.10.0 - optionalDependencies: - typescript: 5.5.4 + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + eslint: 9.39.4 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/scope-manager@7.18.0': + '@typescript-eslint/project-service@8.59.4(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - - '@typescript-eslint/scope-manager@8.4.0': - dependencies: - '@typescript-eslint/types': 8.4.0 - '@typescript-eslint/visitor-keys': 8.4.0 - - '@typescript-eslint/type-utils@7.18.0(eslint@9.10.0)(typescript@5.5.4)': - dependencies: - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) - '@typescript-eslint/utils': 7.18.0(eslint@9.10.0)(typescript@5.5.4) - debug: 4.3.7 - eslint: 9.10.0 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + debug: 4.4.3 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/types@7.18.0': {} - - '@typescript-eslint/types@8.4.0': {} + '@typescript-eslint/scope-manager@8.59.4': + dependencies: + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 - '@typescript-eslint/typescript-estree@7.18.0(typescript@5.5.4)': + '@typescript-eslint/tsconfig-utils@8.59.4(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/visitor-keys': 7.18.0 - debug: 4.3.7 - globby: 11.1.0 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 - transitivePeerDependencies: - - supports-color + typescript: 6.0.3 - '@typescript-eslint/typescript-estree@8.4.0(typescript@5.5.4)': + '@typescript-eslint/type-utils@8.59.4(eslint@9.39.4)(typescript@6.0.3)': dependencies: - '@typescript-eslint/types': 8.4.0 - '@typescript-eslint/visitor-keys': 8.4.0 - debug: 4.3.7 - fast-glob: 3.3.2 - is-glob: 4.0.3 - minimatch: 9.0.5 - semver: 7.6.3 - ts-api-utils: 1.3.0(typescript@5.5.4) - optionalDependencies: - typescript: 5.5.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - '@typescript-eslint/utils@7.18.0(eslint@9.10.0)(typescript@5.5.4)': + '@typescript-eslint/types@8.59.4': {} + + '@typescript-eslint/typescript-estree@8.59.4(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0) - '@typescript-eslint/scope-manager': 7.18.0 - '@typescript-eslint/types': 7.18.0 - '@typescript-eslint/typescript-estree': 7.18.0(typescript@5.5.4) - eslint: 9.10.0 + '@typescript-eslint/project-service': 8.59.4(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.4(typescript@6.0.3) + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/visitor-keys': 8.59.4 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.8.0 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 transitivePeerDependencies: - supports-color - - typescript - '@typescript-eslint/utils@8.4.0(eslint@9.10.0)(typescript@5.5.4)': + '@typescript-eslint/utils@8.59.4(eslint@9.39.4)(typescript@6.0.3)': dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0) - '@typescript-eslint/scope-manager': 8.4.0 - '@typescript-eslint/types': 8.4.0 - '@typescript-eslint/typescript-estree': 8.4.0(typescript@5.5.4) - eslint: 9.10.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@typescript-eslint/scope-manager': 8.59.4 + '@typescript-eslint/types': 8.59.4 + '@typescript-eslint/typescript-estree': 8.59.4(typescript@6.0.3) + eslint: 9.39.4 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - - typescript - - '@typescript-eslint/visitor-keys@7.18.0': - dependencies: - '@typescript-eslint/types': 7.18.0 - eslint-visitor-keys: 3.4.3 - '@typescript-eslint/visitor-keys@8.4.0': + '@typescript-eslint/visitor-keys@8.59.4': dependencies: - '@typescript-eslint/types': 8.4.0 - eslint-visitor-keys: 3.4.3 + '@typescript-eslint/types': 8.59.4 + eslint-visitor-keys: 5.0.1 - acorn-jsx@5.3.2(acorn@8.12.1): + acorn-jsx@5.3.2(acorn@8.16.0): dependencies: - acorn: 8.12.1 + acorn: 8.16.0 - acorn@8.12.1: {} + acorn@8.16.0: {} - ajv@6.12.6: + ajv@6.15.0: dependencies: fast-deep-equal: 3.1.3 fast-json-stable-stringify: 2.1.0 @@ -2893,75 +2819,54 @@ snapshots: dependencies: audio-frequency-to-index: 2.0.0 - ansi-colors@4.1.3: {} - - ansi-regex@5.0.1: {} - - ansi-styles@3.2.1: - dependencies: - color-convert: 1.9.3 - ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 - anymatch@3.1.3: - dependencies: - normalize-path: 3.0.0 - picomatch: 2.3.1 - - argparse@1.0.10: - dependencies: - sprintf-js: 1.0.3 - argparse@2.0.1: {} - array-union@2.1.0: {} - audio-frequency-to-index@2.0.0: dependencies: clamp: 1.0.1 - autoprefixer@10.4.20(postcss@8.4.45): + autoprefixer@10.5.0(postcss@8.5.15): dependencies: - browserslist: 4.23.3 - caniuse-lite: 1.0.30001658 - fraction.js: 4.3.7 - normalize-range: 0.1.2 - picocolors: 1.1.0 - postcss: 8.4.45 + browserslist: 4.28.2 + caniuse-lite: 1.0.30001793 + fraction.js: 5.3.4 + picocolors: 1.1.1 + postcss: 8.5.15 postcss-value-parser: 4.2.0 - b4a@1.6.6: {} + b4a@1.8.1: {} - babel-plugin-jsx-dom-expressions@0.38.5(@babel/core@7.25.2): + babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.29.0): dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.29.0 '@babel/helper-module-imports': 7.18.6 - '@babel/plugin-syntax-jsx': 7.24.7(@babel/core@7.25.2) - '@babel/types': 7.25.6 + '@babel/plugin-syntax-jsx': 7.28.6(@babel/core@7.29.0) + '@babel/types': 7.29.0 html-entities: 2.3.3 - validate-html-nesting: 1.2.2 + parse5: 7.3.0 - babel-preset-solid@1.8.22(@babel/core@7.25.2): + babel-preset-solid@1.9.12(@babel/core@7.29.0)(solid-js@1.9.13): dependencies: - '@babel/core': 7.25.2 - babel-plugin-jsx-dom-expressions: 0.38.5(@babel/core@7.25.2) + '@babel/core': 7.29.0 + babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.29.0) + optionalDependencies: + solid-js: 1.9.13 balanced-match@1.0.2: {} - bare-events@2.4.2: - optional: true + balanced-match@4.0.4: {} + + bare-events@2.8.3: {} base64-arraybuffer@1.0.2: {} base64-js@1.5.1: {} - better-path-resolve@1.0.0: - dependencies: - is-windows: 1.0.2 - - binary-extensions@2.3.0: {} + baseline-browser-mapping@2.10.31: {} bl@4.1.0: dependencies: @@ -2969,25 +2874,22 @@ snapshots: inherits: 2.0.4 readable-stream: 3.6.2 - brace-expansion@1.1.11: + brace-expansion@1.1.14: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - brace-expansion@2.0.1: + brace-expansion@5.0.6: dependencies: - balanced-match: 1.0.2 - - braces@3.0.3: - dependencies: - fill-range: 7.1.1 + balanced-match: 4.0.4 - browserslist@4.23.3: + browserslist@4.28.2: dependencies: - caniuse-lite: 1.0.30001658 - electron-to-chromium: 1.5.18 - node-releases: 2.0.18 - update-browserslist-db: 1.1.0(browserslist@4.23.3) + baseline-browser-mapping: 2.10.31 + caniuse-lite: 1.0.30001793 + electron-to-chromium: 1.5.360 + node-releases: 2.0.44 + update-browserslist-db: 1.2.3(browserslist@4.28.2) buffer@5.7.1: dependencies: @@ -2996,74 +2898,51 @@ snapshots: callsites@3.1.0: {} - caniuse-lite@1.0.30001658: {} - - chalk@2.4.2: - dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 + caniuse-lite@1.0.30001793: {} chalk@4.1.2: dependencies: ansi-styles: 4.3.0 supports-color: 7.2.0 - chardet@0.7.0: {} - - chokidar@3.6.0: + chokidar@4.0.3: dependencies: - anymatch: 3.1.3 - braces: 3.0.3 - glob-parent: 5.1.2 - is-binary-path: 2.1.0 - is-glob: 4.0.3 - normalize-path: 3.0.0 - readdirp: 3.6.0 - optionalDependencies: - fsevents: 2.3.3 + readdirp: 4.1.2 + optional: true chownr@1.1.4: {} - chroma-js@3.1.1: {} - - ci-info@3.9.0: {} + chroma-js@3.2.0: {} clamp@1.0.1: {} - color-convert@1.9.3: - dependencies: - color-name: 1.1.3 - color-convert@2.0.1: dependencies: color-name: 1.1.4 - color-name@1.1.3: {} - color-name@1.1.4: {} + colorjs.io@0.5.2: {} + concat-map@0.0.1: {} convert-source-map@2.0.0: {} - croppie@2.6.5: {} + crelt@1.0.6: {} - cross-spawn@5.1.0: - dependencies: - lru-cache: 4.1.5 - shebang-command: 1.2.0 - which: 1.3.1 + croppie@2.6.5: {} - cross-spawn@7.0.3: + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 shebang-command: 2.0.0 which: 2.0.2 - csstype@3.1.3: {} + cssesc@3.0.0: {} - debug@4.3.7: + csstype@3.2.3: {} + + debug@4.4.3: dependencies: ms: 2.1.3 @@ -3077,29 +2956,23 @@ snapshots: detect-browser@4.8.0: {} - detect-indent@6.1.0: {} - - detect-libc@2.0.3: {} - - dir-glob@3.0.1: - dependencies: - path-type: 4.0.0 + detect-libc@2.1.2: {} dom-walk@0.1.2: {} - electron-to-chromium@1.5.18: {} + electron-to-chromium@1.5.360: {} - end-of-stream@1.4.4: + end-of-stream@1.4.5: dependencies: once: 1.4.0 - engine.io-client@6.5.4: + engine.io-client@6.6.5: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 engine.io-parser: 5.2.3 - ws: 8.17.1 - xmlhttprequest-ssl: 2.0.0 + ws: 8.20.1 + xmlhttprequest-ssl: 2.1.2 transitivePeerDependencies: - bufferutil - supports-color @@ -3107,89 +2980,65 @@ snapshots: engine.io-parser@5.2.3: {} - enquirer@2.4.1: - dependencies: - ansi-colors: 4.1.3 - strip-ansi: 6.0.1 + entities@4.5.0: {} - entities@3.0.1: {} + entities@6.0.1: {} err-code@3.0.1: {} - esbuild@0.21.5: - optionalDependencies: - '@esbuild/aix-ppc64': 0.21.5 - '@esbuild/android-arm': 0.21.5 - '@esbuild/android-arm64': 0.21.5 - '@esbuild/android-x64': 0.21.5 - '@esbuild/darwin-arm64': 0.21.5 - '@esbuild/darwin-x64': 0.21.5 - '@esbuild/freebsd-arm64': 0.21.5 - '@esbuild/freebsd-x64': 0.21.5 - '@esbuild/linux-arm': 0.21.5 - '@esbuild/linux-arm64': 0.21.5 - '@esbuild/linux-ia32': 0.21.5 - '@esbuild/linux-loong64': 0.21.5 - '@esbuild/linux-mips64el': 0.21.5 - '@esbuild/linux-ppc64': 0.21.5 - '@esbuild/linux-riscv64': 0.21.5 - '@esbuild/linux-s390x': 0.21.5 - '@esbuild/linux-x64': 0.21.5 - '@esbuild/netbsd-x64': 0.21.5 - '@esbuild/openbsd-x64': 0.21.5 - '@esbuild/sunos-x64': 0.21.5 - '@esbuild/win32-arm64': 0.21.5 - '@esbuild/win32-ia32': 0.21.5 - '@esbuild/win32-x64': 0.21.5 + eruda@3.4.3: {} escalade@3.2.0: {} - escape-string-regexp@1.0.5: {} - escape-string-regexp@4.0.0: {} - eslint-plugin-solid@0.14.3(eslint@9.10.0)(typescript@5.5.4): + eslint-plugin-solid@0.14.5(eslint@9.39.4)(typescript@6.0.3): dependencies: - '@typescript-eslint/utils': 8.4.0(eslint@9.10.0)(typescript@5.5.4) - eslint: 9.10.0 + '@typescript-eslint/utils': 8.59.4(eslint@9.39.4)(typescript@6.0.3) + eslint: 9.39.4 estraverse: 5.3.0 is-html: 2.0.0 kebab-case: 1.0.2 known-css-properties: 0.30.0 - style-to-object: 1.0.7 + style-to-object: 1.0.14 + typescript: 6.0.3 transitivePeerDependencies: - supports-color - - typescript - eslint-scope@8.0.2: + eslint-scope@8.4.0: dependencies: esrecurse: 4.3.0 estraverse: 5.3.0 eslint-visitor-keys@3.4.3: {} - eslint-visitor-keys@4.0.0: {} + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} - eslint@9.10.0: + eslint@9.39.4: dependencies: - '@eslint-community/eslint-utils': 4.4.0(eslint@9.10.0) - '@eslint-community/regexpp': 4.11.0 - '@eslint/config-array': 0.18.0 - '@eslint/eslintrc': 3.1.0 - '@eslint/js': 9.10.0 - '@eslint/plugin-kit': 0.1.0 + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 '@humanwhocodes/module-importer': 1.0.1 - '@humanwhocodes/retry': 0.3.0 - '@nodelib/fs.walk': 1.2.8 - ajv: 6.12.6 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.9 + ajv: 6.15.0 chalk: 4.1.2 - cross-spawn: 7.0.3 - debug: 4.3.7 + cross-spawn: 7.0.6 + debug: 4.4.3 escape-string-regexp: 4.0.0 - eslint-scope: 8.0.2 - eslint-visitor-keys: 4.0.0 - espree: 10.1.0 - esquery: 1.6.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 file-entry-cache: 8.0.0 @@ -3198,26 +3047,21 @@ snapshots: ignore: 5.3.2 imurmurhash: 0.1.4 is-glob: 4.0.3 - is-path-inside: 3.0.3 json-stable-stringify-without-jsonify: 1.0.1 lodash.merge: 4.6.2 - minimatch: 3.1.2 + minimatch: 3.1.5 natural-compare: 1.4.0 optionator: 0.9.4 - strip-ansi: 6.0.1 - text-table: 0.2.0 transitivePeerDependencies: - supports-color - espree@10.1.0: + espree@10.4.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - eslint-visitor-keys: 4.0.0 + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 - esprima@4.0.1: {} - - esquery@1.6.0: + esquery@1.7.0: dependencies: estraverse: 5.3.0 @@ -3231,53 +3075,34 @@ snapshots: event-target-shim@3.0.2: {} - eventemitter3@5.0.1: {} + eventemitter3@5.0.4: {} + + events-universal@1.0.1: + dependencies: + bare-events: 2.8.3 + transitivePeerDependencies: + - bare-abort-controller events@3.3.0: {} expand-template@2.0.3: {} - extendable-error@0.1.7: {} - - external-editor@3.1.0: - dependencies: - chardet: 0.7.0 - iconv-lite: 0.4.24 - tmp: 0.0.33 - fast-deep-equal@3.1.3: {} fast-fifo@1.3.2: {} - fast-glob@3.3.2: - dependencies: - '@nodelib/fs.stat': 2.0.5 - '@nodelib/fs.walk': 1.2.8 - glob-parent: 5.1.2 - merge2: 1.4.1 - micromatch: 4.0.8 - fast-json-stable-stringify@2.1.0: {} fast-levenshtein@2.0.6: {} - fastq@1.17.1: - dependencies: - reusify: 1.0.4 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 file-entry-cache@8.0.0: dependencies: flat-cache: 4.0.1 - fill-range@7.1.1: - dependencies: - to-regex-range: 5.0.1 - - find-up@4.1.0: - dependencies: - locate-path: 5.0.0 - path-exists: 4.0.0 - find-up@5.0.0: dependencies: locate-path: 6.0.0 @@ -3285,73 +3110,44 @@ snapshots: flat-cache@4.0.1: dependencies: - flatted: 3.3.1 + flatted: 3.4.2 keyv: 4.5.4 - flatted@3.3.1: {} + flatted@3.4.2: {} - fraction.js@4.3.7: {} + fraction.js@5.3.4: {} fs-constants@1.0.0: {} - fs-extra@7.0.1: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - - fs-extra@8.1.0: - dependencies: - graceful-fs: 4.2.11 - jsonfile: 4.0.0 - universalify: 0.1.2 - fsevents@2.3.3: optional: true + fzstd@0.1.1: {} + gensync@1.0.0-beta.2: {} github-from-package@0.0.0: {} - glob-parent@5.1.2: - dependencies: - is-glob: 4.0.3 - glob-parent@6.0.2: dependencies: is-glob: 4.0.3 global@4.4.0: dependencies: - min-document: 2.19.0 + min-document: 2.19.2 process: 0.11.10 - globals@11.12.0: {} - globals@14.0.0: {} - globby@11.1.0: - dependencies: - array-union: 2.1.0 - dir-glob: 3.0.1 - fast-glob: 3.3.2 - ignore: 5.3.2 - merge2: 1.4.1 - slash: 3.0.0 + globals@15.15.0: {} - goober@2.1.14(csstype@3.1.3): + goober@2.1.19(csstype@3.2.3): dependencies: - csstype: 3.1.3 - - graceful-fs@4.2.11: {} - - graphemer@1.4.0: {} - - has-flag@3.0.0: {} + csstype: 3.2.3 has-flag@4.0.0: {} - highlight.js@11.10.0: {} + highlight.js@11.11.1: {} html-entities@2.3.3: {} @@ -3359,25 +3155,17 @@ snapshots: html-tags@3.3.1: {} - human-id@1.0.2: {} - - i18next@23.14.0: - dependencies: - '@babel/runtime': 7.25.6 - - iconv-lite@0.4.24: - dependencies: - safer-buffer: 2.1.2 - - idb-keyval@6.2.1: {} + idb-keyval@6.2.4: {} ieee754@1.2.1: {} ignore@5.3.2: {} - immutable@4.3.7: {} + ignore@7.0.5: {} + + immutable@5.1.5: {} - import-fresh@3.3.0: + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 @@ -3388,11 +3176,7 @@ snapshots: ini@1.3.8: {} - inline-style-parser@0.2.3: {} - - is-binary-path@2.1.0: - dependencies: - binary-extensions: 2.3.0 + inline-style-parser@0.2.7: {} is-extglob@2.1.1: {} @@ -3404,32 +3188,19 @@ snapshots: dependencies: html-tags: 3.3.1 - is-number@7.0.0: {} - - is-path-inside@3.0.3: {} - - is-subdir@1.2.0: - dependencies: - better-path-resolve: 1.0.0 - is-what@4.1.16: {} - is-windows@1.0.2: {} - isexe@2.0.0: {} - js-tokens@4.0.0: {} + js-sha256@0.11.1: {} - js-yaml@3.14.1: - dependencies: - argparse: 1.0.10 - esprima: 4.0.1 + js-tokens@4.0.0: {} - js-yaml@4.1.0: + js-yaml@4.1.1: dependencies: argparse: 2.0.1 - jsesc@2.5.2: {} + jsesc@3.1.0: {} json-buffer@3.0.1: {} @@ -3439,16 +3210,6 @@ snapshots: json5@2.2.3: {} - jsonfile@4.0.0: - optionalDependencies: - graceful-fs: 4.2.11 - - jsonfile@5.0.0: - dependencies: - universalify: 0.1.2 - optionalDependencies: - graceful-fs: 4.2.11 - kebab-case@1.0.2: {} keyv@4.5.4: @@ -3457,20 +3218,65 @@ snapshots: known-css-properties@0.30.0: {} - konva@9.3.14: {} + konva@10.3.0: {} levn@0.4.1: dependencies: prelude-ls: 1.2.1 type-check: 0.4.0 - linkify-it@4.0.1: - dependencies: - uc.micro: 1.0.6 + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true - locate-path@5.0.0: + lightningcss@1.32.0: dependencies: - p-locate: 4.1.0 + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + linkify-it@5.0.0: + dependencies: + uc.micro: 2.1.0 locate-path@6.0.0: dependencies: @@ -3478,91 +3284,79 @@ snapshots: lodash.merge@4.6.2: {} - lodash.startcase@4.4.0: {} - - lru-cache@4.1.5: - dependencies: - pseudomap: 1.0.2 - yallist: 2.1.2 - lru-cache@5.1.1: dependencies: yallist: 3.1.1 - markdown-it-emoji@2.0.2: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-it-emoji@3.0.0: {} - markdown-it@13.0.2: + markdown-it@14.1.1: dependencies: argparse: 2.0.1 - entities: 3.0.1 - linkify-it: 4.0.1 - mdurl: 1.0.1 - uc.micro: 1.0.6 + entities: 4.5.0 + linkify-it: 5.0.0 + mdurl: 2.0.0 + punycode.js: 2.3.1 + uc.micro: 2.1.0 - match-sorter@6.3.4: + match-sorter@6.4.0: dependencies: - '@babel/runtime': 7.25.6 + '@babel/runtime': 7.29.2 remove-accents: 0.5.0 - mdurl@1.0.1: {} - - merge-anything@5.1.7: + match-sorter@8.3.0: dependencies: - is-what: 4.1.16 + '@babel/runtime': 7.29.2 + remove-accents: 0.5.0 - merge2@1.4.1: {} + mdurl@2.0.0: {} - micromatch@4.0.8: + merge-anything@5.1.7: dependencies: - braces: 3.0.3 - picomatch: 2.3.1 + is-what: 4.1.16 mimic-response@3.1.0: {} - min-document@2.19.0: + min-document@2.19.2: dependencies: dom-walk: 0.1.2 - minimatch@3.1.2: + minimatch@10.2.5: dependencies: - brace-expansion: 1.1.11 + brace-expansion: 5.0.6 - minimatch@9.0.5: + minimatch@3.1.5: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 1.1.14 minimist@1.2.8: {} mkdirp-classic@0.5.3: {} - mri@1.2.0: {} - ms@2.1.3: {} - nanoid@3.3.7: {} + nanoid@3.3.12: {} - napi-build-utils@1.0.2: {} + napi-build-utils@2.0.0: {} natural-compare@1.4.0: {} - node-abi@3.67.0: + node-abi@3.92.0: dependencies: - semver: 7.6.3 + semver: 7.8.0 - node-datachannel@0.10.1: - dependencies: - node-domexception: 2.0.1 - prebuild-install: 7.1.2 - - node-domexception@1.0.0: {} - - node-domexception@2.0.1: {} - - node-releases@2.0.18: {} + node-addon-api@7.1.1: + optional: true - normalize-path@3.0.0: {} + node-datachannel@0.32.3: + dependencies: + prebuild-install: 7.1.3 - normalize-range@0.1.2: {} + node-releases@2.0.44: {} once@1.4.0: dependencies: @@ -3582,93 +3376,75 @@ snapshots: detect-browser: 4.8.0 event-target-shim: 3.0.2 - os-tmpdir@1.0.2: {} - - outdent@0.5.0: {} - - p-filter@2.1.0: - dependencies: - p-map: 2.1.0 - - p-limit@2.3.0: - dependencies: - p-try: 2.2.0 - p-limit@3.1.0: dependencies: yocto-queue: 0.1.0 - p-locate@4.1.0: - dependencies: - p-limit: 2.3.0 - p-locate@5.0.0: dependencies: p-limit: 3.1.0 - p-map@2.1.0: {} - - p-try@2.2.0: {} - - package-manager-detector@0.2.0: {} - parent-module@1.0.1: dependencies: callsites: 3.1.0 + parse5@7.3.0: + dependencies: + entities: 6.0.1 + path-exists@4.0.0: {} path-key@3.1.1: {} - path-type@4.0.0: {} + picocolors@1.1.1: {} - picocolors@1.1.0: {} + picomatch@4.0.4: {} - picomatch@2.3.1: {} + postcss-nested@7.0.2(postcss@8.5.15): + dependencies: + postcss: 8.5.15 + postcss-selector-parser: 7.1.1 - pify@4.0.1: {} + postcss-selector-parser@7.1.1: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 postcss-value-parser@4.2.0: {} - postcss@8.4.45: + postcss@8.5.15: dependencies: - nanoid: 3.3.7 - picocolors: 1.1.0 - source-map-js: 1.2.0 + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 - prebuild-install@7.1.2: + prebuild-install@7.1.3: dependencies: - detect-libc: 2.0.3 + detect-libc: 2.1.2 expand-template: 2.0.3 github-from-package: 0.0.0 minimist: 1.2.8 mkdirp-classic: 0.5.3 - napi-build-utils: 1.0.2 - node-abi: 3.67.0 - pump: 3.0.0 + napi-build-utils: 2.0.0 + node-abi: 3.92.0 + pump: 3.0.4 rc: 1.2.8 simple-get: 4.0.1 - tar-fs: 2.1.1 + tar-fs: 2.1.4 tunnel-agent: 0.6.0 prelude-ls@1.2.1: {} - prettier@2.8.8: {} - process@0.11.10: {} - pseudomap@1.0.2: {} - - pump@3.0.0: + pump@3.0.4: dependencies: - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 once: 1.4.0 - punycode@2.3.1: {} - - queue-microtask@1.2.3: {} + punycode.js@2.3.1: {} - queue-tick@1.0.1: {} + punycode@2.3.1: {} rc@1.2.8: dependencies: @@ -3677,93 +3453,158 @@ snapshots: minimist: 1.2.8 strip-json-comments: 2.0.1 - read-yaml-file@1.1.0: - dependencies: - graceful-fs: 4.2.11 - js-yaml: 3.14.1 - pify: 4.0.1 - strip-bom: 3.0.0 - readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - readdirp@3.6.0: - dependencies: - picomatch: 2.3.1 - - regenerator-runtime@0.14.1: {} + readdirp@4.1.2: + optional: true remove-accents@0.5.0: {} resolve-from@4.0.0: {} - resolve-from@5.0.0: {} - - reusify@1.0.4: {} - - rollup@4.21.2: + rolldown@1.0.2: dependencies: - '@types/estree': 1.0.5 + '@oxc-project/types': 0.132.0 + '@rolldown/pluginutils': 1.0.1 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.21.2 - '@rollup/rollup-android-arm64': 4.21.2 - '@rollup/rollup-darwin-arm64': 4.21.2 - '@rollup/rollup-darwin-x64': 4.21.2 - '@rollup/rollup-linux-arm-gnueabihf': 4.21.2 - '@rollup/rollup-linux-arm-musleabihf': 4.21.2 - '@rollup/rollup-linux-arm64-gnu': 4.21.2 - '@rollup/rollup-linux-arm64-musl': 4.21.2 - '@rollup/rollup-linux-powerpc64le-gnu': 4.21.2 - '@rollup/rollup-linux-riscv64-gnu': 4.21.2 - '@rollup/rollup-linux-s390x-gnu': 4.21.2 - '@rollup/rollup-linux-x64-gnu': 4.21.2 - '@rollup/rollup-linux-x64-musl': 4.21.2 - '@rollup/rollup-win32-arm64-msvc': 4.21.2 - '@rollup/rollup-win32-ia32-msvc': 4.21.2 - '@rollup/rollup-win32-x64-msvc': 4.21.2 - fsevents: 2.3.3 + '@rolldown/binding-android-arm64': 1.0.2 + '@rolldown/binding-darwin-arm64': 1.0.2 + '@rolldown/binding-darwin-x64': 1.0.2 + '@rolldown/binding-freebsd-x64': 1.0.2 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.2 + '@rolldown/binding-linux-arm64-gnu': 1.0.2 + '@rolldown/binding-linux-arm64-musl': 1.0.2 + '@rolldown/binding-linux-ppc64-gnu': 1.0.2 + '@rolldown/binding-linux-s390x-gnu': 1.0.2 + '@rolldown/binding-linux-x64-gnu': 1.0.2 + '@rolldown/binding-linux-x64-musl': 1.0.2 + '@rolldown/binding-openharmony-arm64': 1.0.2 + '@rolldown/binding-wasm32-wasi': 1.0.2 + '@rolldown/binding-win32-arm64-msvc': 1.0.2 + '@rolldown/binding-win32-x64-msvc': 1.0.2 + + rxjs@7.8.2: + dependencies: + tslib: 2.8.1 + + safe-buffer@5.2.1: {} - run-parallel@1.2.0: + sass-embedded-all-unknown@1.99.0: dependencies: - queue-microtask: 1.2.3 + sass: 1.99.0 + optional: true - safe-buffer@5.2.1: {} + sass-embedded-android-arm64@1.99.0: + optional: true - safer-buffer@2.1.2: {} + sass-embedded-android-arm@1.99.0: + optional: true + + sass-embedded-android-riscv64@1.99.0: + optional: true + + sass-embedded-android-x64@1.99.0: + optional: true + + sass-embedded-darwin-arm64@1.99.0: + optional: true + + sass-embedded-darwin-x64@1.99.0: + optional: true + + sass-embedded-linux-arm64@1.99.0: + optional: true + + sass-embedded-linux-arm@1.99.0: + optional: true + + sass-embedded-linux-musl-arm64@1.99.0: + optional: true + + sass-embedded-linux-musl-arm@1.99.0: + optional: true + + sass-embedded-linux-musl-riscv64@1.99.0: + optional: true + + sass-embedded-linux-musl-x64@1.99.0: + optional: true + + sass-embedded-linux-riscv64@1.99.0: + optional: true + + sass-embedded-linux-x64@1.99.0: + optional: true - sass@1.78.0: + sass-embedded-unknown-all@1.99.0: dependencies: - chokidar: 3.6.0 - immutable: 4.3.7 - source-map-js: 1.2.0 + sass: 1.99.0 + optional: true - semver@6.3.1: {} + sass-embedded-win32-arm64@1.99.0: + optional: true - semver@7.6.3: {} + sass-embedded-win32-x64@1.99.0: + optional: true - seroval-plugins@1.1.1(seroval@1.1.1): + sass-embedded@1.99.0: dependencies: - seroval: 1.1.1 + '@bufbuild/protobuf': 2.12.0 + colorjs.io: 0.5.2 + immutable: 5.1.5 + rxjs: 7.8.2 + supports-color: 8.1.1 + sync-child-process: 1.0.2 + varint: 6.0.0 + optionalDependencies: + sass-embedded-all-unknown: 1.99.0 + sass-embedded-android-arm: 1.99.0 + sass-embedded-android-arm64: 1.99.0 + sass-embedded-android-riscv64: 1.99.0 + sass-embedded-android-x64: 1.99.0 + sass-embedded-darwin-arm64: 1.99.0 + sass-embedded-darwin-x64: 1.99.0 + sass-embedded-linux-arm: 1.99.0 + sass-embedded-linux-arm64: 1.99.0 + sass-embedded-linux-musl-arm: 1.99.0 + sass-embedded-linux-musl-arm64: 1.99.0 + sass-embedded-linux-musl-riscv64: 1.99.0 + sass-embedded-linux-musl-x64: 1.99.0 + sass-embedded-linux-riscv64: 1.99.0 + sass-embedded-linux-x64: 1.99.0 + sass-embedded-unknown-all: 1.99.0 + sass-embedded-win32-arm64: 1.99.0 + sass-embedded-win32-x64: 1.99.0 + + sass@1.99.0: + dependencies: + chokidar: 4.0.3 + immutable: 5.1.5 + source-map-js: 1.2.1 + optionalDependencies: + '@parcel/watcher': 2.5.6 + optional: true - seroval@1.1.1: {} + semver@6.3.1: {} + + semver@7.8.0: {} - shebang-command@1.2.0: + seroval-plugins@1.5.4(seroval@1.5.4): dependencies: - shebang-regex: 1.0.0 + seroval: 1.5.4 + + seroval@1.5.4: {} shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 - shebang-regex@1.0.0: {} - shebang-regex@3.0.0: {} - signal-exit@3.0.7: {} - simple-concat@1.0.1: {} simple-get@4.0.1: @@ -3772,188 +3613,168 @@ snapshots: once: 1.4.0 simple-concat: 1.0.1 - slash@3.0.0: {} - - socket.io-client@4.7.5: + socket.io-client@4.8.3: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 - engine.io-client: 6.5.4 - socket.io-parser: 4.2.4 + debug: 4.4.3 + engine.io-client: 6.6.5 + socket.io-parser: 4.2.6 transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - socket.io-parser@4.2.4: + socket.io-parser@4.2.6: dependencies: '@socket.io/component-emitter': 3.1.2 - debug: 4.3.7 + debug: 4.4.3 transitivePeerDependencies: - supports-color - solid-codemirror@2.3.1(@codemirror/state@6.4.1)(@codemirror/view@6.33.0)(solid-js@1.8.22): + solid-codemirror@2.3.3(@codemirror/state@6.6.0)(@codemirror/view@6.43.0)(solid-js@1.9.13): dependencies: - '@changesets/cli': 2.27.8 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.33.0 - solid-js: 1.8.22 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 + solid-js: 1.9.13 - solid-js@1.8.22: + solid-js@1.9.13: dependencies: - csstype: 3.1.3 - seroval: 1.1.1 - seroval-plugins: 1.1.1(seroval@1.1.1) + csstype: 3.2.3 + seroval: 1.5.4 + seroval-plugins: 1.5.4(seroval@1.5.4) - solid-navigator@0.3.14(solid-js@1.8.22): + solid-navigator@0.4.1(solid-js@1.9.13): dependencies: - solid-js: 1.8.22 + solid-js: 1.9.13 - solid-refresh@0.6.3(solid-js@1.8.22): + solid-refresh@0.6.3(solid-js@1.9.13): dependencies: - '@babel/generator': 7.25.6 - '@babel/helper-module-imports': 7.24.7 - '@babel/types': 7.25.6 - solid-js: 1.8.22 + '@babel/generator': 7.29.1 + '@babel/helper-module-imports': 7.28.6 + '@babel/types': 7.29.0 + solid-js: 1.9.13 transitivePeerDependencies: - supports-color - solid-sortablejs@2.1.2(solid-js@1.8.22): + solid-sortablejs@2.1.8(solid-js@1.9.13): dependencies: - '@types/sortablejs': 1.15.8 - solid-js: 1.8.22 - sortablejs: 1.15.3 + solid-js: 1.9.13 + sortablejs: 1.15.7(patch_hash=2cd8457c177f4d9fbcc7edf39464d4e76e261c69f33a5431ca6af76bdfd29786) - solid-styled-components@0.28.5(solid-js@1.8.22): + solid-styled-components@0.28.5(solid-js@1.9.13): dependencies: - csstype: 3.1.3 - goober: 2.1.14(csstype@3.1.3) - solid-js: 1.8.22 - - sortablejs@1.15.3: {} - - source-map-js@1.2.0: {} + csstype: 3.2.3 + goober: 2.1.19(csstype@3.2.3) + solid-js: 1.9.13 - spawndamnit@2.0.0: - dependencies: - cross-spawn: 5.1.0 - signal-exit: 3.0.7 + sortablejs@1.15.7(patch_hash=2cd8457c177f4d9fbcc7edf39464d4e76e261c69f33a5431ca6af76bdfd29786): {} - sprintf-js@1.0.3: {} + source-map-js@1.2.1: {} - streamx@2.20.0: + streamx@2.25.0: dependencies: + events-universal: 1.0.1 fast-fifo: 1.3.2 - queue-tick: 1.0.1 - text-decoder: 1.1.1 - optionalDependencies: - bare-events: 2.4.2 + text-decoder: 1.2.7 + transitivePeerDependencies: + - bare-abort-controller + - react-native-b4a string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - strip-ansi@6.0.1: - dependencies: - ansi-regex: 5.0.1 - - strip-bom@3.0.0: {} - strip-json-comments@2.0.1: {} strip-json-comments@3.1.1: {} - style-mod@4.1.2: {} + style-mod@4.1.3: {} - style-to-object@1.0.7: + style-to-object@1.0.14: dependencies: - inline-style-parser: 0.2.3 + inline-style-parser: 0.2.7 - supports-color@5.5.0: + supports-color@7.2.0: dependencies: - has-flag: 3.0.0 + has-flag: 4.0.0 - supports-color@7.2.0: + supports-color@8.1.1: dependencies: has-flag: 4.0.0 - tar-fs@2.1.1: + sync-child-process@1.0.2: + dependencies: + sync-message-port: 1.2.0 + + sync-message-port@1.2.0: {} + + tar-fs@2.1.4: dependencies: chownr: 1.1.4 mkdirp-classic: 0.5.3 - pump: 3.0.0 + pump: 3.0.4 tar-stream: 2.2.0 tar-stream@2.2.0: dependencies: bl: 4.1.0 - end-of-stream: 1.4.4 + end-of-stream: 1.4.5 fs-constants: 1.0.0 inherits: 2.0.4 readable-stream: 3.6.2 - term-size@2.2.1: {} - - text-decoder@1.1.1: + temporal-polyfill@0.3.2: dependencies: - b4a: 1.6.6 + temporal-spec: 0.3.1 - text-table@0.2.0: {} + temporal-spec@0.3.1: {} - thememirror@2.0.1(@codemirror/language@6.10.2)(@codemirror/state@6.4.1)(@codemirror/view@6.33.0): + text-decoder@1.2.7: dependencies: - '@codemirror/language': 6.10.2 - '@codemirror/state': 6.4.1 - '@codemirror/view': 6.33.0 + b4a: 1.8.1 + transitivePeerDependencies: + - react-native-b4a - tmp@0.0.33: + thememirror@2.0.1(@codemirror/language@6.12.3)(@codemirror/state@6.6.0)(@codemirror/view@6.43.0): dependencies: - os-tmpdir: 1.0.2 + '@codemirror/language': 6.12.3 + '@codemirror/state': 6.6.0 + '@codemirror/view': 6.43.0 - to-fast-properties@2.0.0: {} - - to-regex-range@5.0.1: + tinyglobby@0.2.16: dependencies: - is-number: 7.0.0 + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 - ts-api-utils@1.3.0(typescript@5.5.4): + ts-api-utils@2.5.0(typescript@6.0.3): dependencies: - typescript: 5.5.4 + typescript: 6.0.3 + + tslib@2.8.1: {} tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 - twemoji-parser@14.0.0: {} - - twemoji@14.0.2: - dependencies: - fs-extra: 8.1.0 - jsonfile: 5.0.0 - twemoji-parser: 14.0.0 - universalify: 0.1.2 - type-check@0.4.0: dependencies: prelude-ls: 1.2.1 - typescript@5.5.4: {} + typescript@6.0.3: {} - uc.micro@1.0.6: {} + uc.micro@2.1.0: {} - uint8-util@2.2.5: + uint8-util@2.2.6: dependencies: base64-arraybuffer: 1.0.2 - undici-types@6.19.8: {} - - universalify@0.1.2: {} + undici-types@6.21.0: {} - update-browserslist-db@1.1.0(browserslist@4.23.3): + update-browserslist-db@1.2.3(browserslist@4.28.2): dependencies: - browserslist: 4.23.3 + browserslist: 4.28.2 escalade: 3.2.0 - picocolors: 1.1.0 + picocolors: 1.1.1 uri-js@4.4.1: dependencies: @@ -3963,34 +3784,37 @@ snapshots: uzip@0.20201231.0: {} - validate-html-nesting@1.2.2: {} + varint@6.0.0: {} - vite-plugin-solid@2.10.2(solid-js@1.8.22)(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)): + vite-plugin-solid@2.11.12(solid-js@1.9.13)(vite@8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0)): dependencies: - '@babel/core': 7.25.2 + '@babel/core': 7.29.0 '@types/babel__core': 7.20.5 - babel-preset-solid: 1.8.22(@babel/core@7.25.2) + babel-preset-solid: 1.9.12(@babel/core@7.29.0)(solid-js@1.9.13) merge-anything: 5.1.7 - solid-js: 1.8.22 - solid-refresh: 0.6.3(solid-js@1.8.22) - vite: 5.4.3(@types/node@20.16.5)(sass@1.78.0) - vitefu: 0.2.5(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)) + solid-js: 1.9.13 + solid-refresh: 0.6.3(solid-js@1.9.13) + vite: 8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0) + vitefu: 1.1.3(vite@8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0)) transitivePeerDependencies: - supports-color - vite@5.4.3(@types/node@20.16.5)(sass@1.78.0): + vite@8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0): dependencies: - esbuild: 0.21.5 - postcss: 8.4.45 - rollup: 4.21.2 + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.15 + rolldown: 1.0.2 + tinyglobby: 0.2.16 optionalDependencies: - '@types/node': 20.16.5 + '@types/node': 22.19.19 fsevents: 2.3.3 - sass: 1.78.0 + sass: 1.99.0 + sass-embedded: 1.99.0 - vitefu@0.2.5(vite@5.4.3(@types/node@20.16.5)(sass@1.78.0)): + vitefu@1.1.3(vite@8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0)): optionalDependencies: - vite: 5.4.3(@types/node@20.16.5)(sass@1.78.0) + vite: 8.0.14(@types/node@22.19.19)(sass-embedded@1.99.0)(sass@1.99.0) voice-activity-detection@0.0.5: dependencies: @@ -3998,14 +3822,9 @@ snapshots: w3c-keyname@2.2.8: {} - webrtc-polyfill@1.1.8: + webrtc-polyfill@1.2.0: dependencies: - node-datachannel: 0.10.1 - node-domexception: 1.0.0 - - which@1.3.1: - dependencies: - isexe: 2.0.0 + node-datachannel: 0.32.3 which@2.0.2: dependencies: @@ -4015,14 +3834,12 @@ snapshots: wrappy@1.0.2: {} - ws@8.17.1: {} - - xmlhttprequest-ssl@2.0.0: {} + ws@8.20.1: {} - yallist@2.1.2: {} + xmlhttprequest-ssl@2.1.2: {} yallist@3.1.1: {} yocto-queue@0.1.0: {} - zoomist@2.1.1: {} + zoomist@2.2.0: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 000000000..d666b2c2b --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,7 @@ +allowBuilds: + node-datachannel: true +minimumReleaseAgeExclude: + - idb-keyval@6.2.4 + - vite@8.0.14 +patchedDependencies: + sortablejs: patches/sortablejs.patch diff --git a/project.inlang/settings.json b/project.inlang/settings.json deleted file mode 100644 index 4f1d2a818..000000000 --- a/project.inlang/settings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "sourceLanguageTag": "en-gb", - "languageTags": ["en-gb", "af-za", "pt-br", "zn-hant", "nl-nl", "fr-FR", "de-de", "hu-hu", "pl-pl", "ro-ro", "es-es", "tr-tr"], - "modules": [ - "https://cdn.jsdelivr.net/npm/@inlang/plugin-i18next@latest/dist/index.js" - ], - "plugin.inlang.i18next": { - "pathPattern": "../src/locales/list/{languageTag}.json" - } -} \ No newline at end of file diff --git a/public/assets/Drive.svg b/public/assets/Drive.svg new file mode 100644 index 000000000..a8cefd5b2 --- /dev/null +++ b/public/assets/Drive.svg @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/public/assets/Google.svg b/public/assets/Google.svg index 4cf163bfe..9d25a230a 100644 --- a/public/assets/Google.svg +++ b/public/assets/Google.svg @@ -1 +1,104 @@ - \ No newline at end of file + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/assets/apple.svg b/public/assets/apple.svg new file mode 100644 index 000000000..6eab46465 --- /dev/null +++ b/public/assets/apple.svg @@ -0,0 +1,4 @@ + + + + diff --git a/public/assets/bluesky.svg b/public/assets/bluesky.svg new file mode 100644 index 000000000..e731338dc --- /dev/null +++ b/public/assets/bluesky.svg @@ -0,0 +1 @@ +Bluesky butterfly logo \ No newline at end of file diff --git a/public/assets/boosty.jpg b/public/assets/boosty.jpg new file mode 100644 index 000000000..70bcae898 Binary files /dev/null and b/public/assets/boosty.jpg differ diff --git a/public/assets/discord.svg b/public/assets/discord.svg new file mode 100644 index 000000000..bd25430eb --- /dev/null +++ b/public/assets/discord.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/emojiSprites-17.png b/public/assets/emojiSprites-17.png new file mode 100644 index 000000000..59e4be3cc Binary files /dev/null and b/public/assets/emojiSprites-17.png differ diff --git a/public/assets/emojiSprites.png b/public/assets/emojiSprites.png deleted file mode 100644 index af60f50ae..000000000 Binary files a/public/assets/emojiSprites.png and /dev/null differ diff --git a/public/assets/itchio.svg b/public/assets/itchio.svg new file mode 100644 index 000000000..0426462d1 --- /dev/null +++ b/public/assets/itchio.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/klipy-light.png b/public/assets/klipy-light.png new file mode 100644 index 000000000..9bafc2569 Binary files /dev/null and b/public/assets/klipy-light.png differ diff --git a/public/assets/klipy-powered-by.png b/public/assets/klipy-powered-by.png new file mode 100644 index 000000000..1b110c064 Binary files /dev/null and b/public/assets/klipy-powered-by.png differ diff --git a/public/assets/kofi.svg b/public/assets/kofi.svg new file mode 100644 index 000000000..ade749d4d --- /dev/null +++ b/public/assets/kofi.svg @@ -0,0 +1,13 @@ + + + + + + + + + + + + + diff --git a/public/assets/linux.svg b/public/assets/linux.svg new file mode 100644 index 000000000..efaea19e8 --- /dev/null +++ b/public/assets/linux.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/logo-monochrome.svg b/public/assets/logo-monochrome.svg new file mode 100644 index 000000000..b8e67596d --- /dev/null +++ b/public/assets/logo-monochrome.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/assets/logo.png b/public/assets/logo.png index 03f2a2121..cd494a958 100644 Binary files a/public/assets/logo.png and b/public/assets/logo.png differ diff --git a/public/assets/mastodon.svg b/public/assets/mastodon.svg new file mode 100644 index 000000000..39a116b22 --- /dev/null +++ b/public/assets/mastodon.svg @@ -0,0 +1,3 @@ + + + diff --git a/public/assets/producthunt.svg b/public/assets/producthunt.svg new file mode 100644 index 000000000..7c0b94cbd --- /dev/null +++ b/public/assets/producthunt.svg @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/assets/profile.png b/public/assets/profile.png deleted file mode 100644 index e6ae04e44..000000000 Binary files a/public/assets/profile.png and /dev/null differ diff --git a/public/assets/reddit.svg b/public/assets/reddit.svg new file mode 100644 index 000000000..108e3598d --- /dev/null +++ b/public/assets/reddit.svg @@ -0,0 +1,40 @@ + + + + + + + + diff --git a/public/assets/sounds/default-call-join.mp3 b/public/assets/sounds/default-call-join.mp3 new file mode 100644 index 000000000..da65193c6 Binary files /dev/null and b/public/assets/sounds/default-call-join.mp3 differ diff --git a/public/assets/sounds/default-call-leave.mp3 b/public/assets/sounds/default-call-leave.mp3 new file mode 100644 index 000000000..a2471f4b7 Binary files /dev/null and b/public/assets/sounds/default-call-leave.mp3 differ diff --git a/public/assets/threads.svg b/public/assets/threads.svg new file mode 100644 index 000000000..809c12d8d --- /dev/null +++ b/public/assets/threads.svg @@ -0,0 +1,21 @@ + + + + + + + + diff --git a/public/assets/twitter.svg b/public/assets/twitter.svg new file mode 100644 index 000000000..43950cee2 --- /dev/null +++ b/public/assets/twitter.svg @@ -0,0 +1,37 @@ + + + + diff --git a/public/assets/youtube.svg b/public/assets/youtube.svg new file mode 100644 index 000000000..012bc5631 --- /dev/null +++ b/public/assets/youtube.svg @@ -0,0 +1,23 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/borders/admin-left-wing.png b/public/borders/admin-left-wing.png deleted file mode 100644 index 7fe85eba6..000000000 Binary files a/public/borders/admin-left-wing.png and /dev/null differ diff --git a/public/borders/admin-left-wing.webp b/public/borders/admin-left-wing.webp new file mode 100644 index 000000000..39bd43ce1 Binary files /dev/null and b/public/borders/admin-left-wing.webp differ diff --git a/public/borders/admin-right-wing.png b/public/borders/admin-right-wing.png deleted file mode 100644 index 53b15108a..000000000 Binary files a/public/borders/admin-right-wing.png and /dev/null differ diff --git a/public/borders/admin-right-wing.webp b/public/borders/admin-right-wing.webp new file mode 100644 index 000000000..bb254193f Binary files /dev/null and b/public/borders/admin-right-wing.webp differ diff --git a/public/borders/admin.png b/public/borders/admin.png deleted file mode 100644 index 9e7b85d79..000000000 Binary files a/public/borders/admin.png and /dev/null differ diff --git a/public/borders/admin.webp b/public/borders/admin.webp new file mode 100644 index 000000000..d38354678 Binary files /dev/null and b/public/borders/admin.webp differ diff --git a/public/borders/bunny-ears-black.webp b/public/borders/bunny-ears-black.webp new file mode 100644 index 000000000..04f5caa99 Binary files /dev/null and b/public/borders/bunny-ears-black.webp differ diff --git a/public/borders/bunny-ears-maid.webp b/public/borders/bunny-ears-maid.webp new file mode 100644 index 000000000..c5d958142 Binary files /dev/null and b/public/borders/bunny-ears-maid.webp differ diff --git a/public/borders/cat-ears-blue.webp b/public/borders/cat-ears-blue.webp new file mode 100644 index 000000000..405f29320 Binary files /dev/null and b/public/borders/cat-ears-blue.webp differ diff --git a/public/borders/cat-ears-maid.webp b/public/borders/cat-ears-maid.webp new file mode 100644 index 000000000..337b55136 Binary files /dev/null and b/public/borders/cat-ears-maid.webp differ diff --git a/public/borders/cat-ears-purple.webp b/public/borders/cat-ears-purple.webp new file mode 100644 index 000000000..9fe20054b Binary files /dev/null and b/public/borders/cat-ears-purple.webp differ diff --git a/public/borders/cat-ears-white.webp b/public/borders/cat-ears-white.webp new file mode 100644 index 000000000..ee6c4a7e8 Binary files /dev/null and b/public/borders/cat-ears-white.webp differ diff --git a/public/borders/deer-ears-horns-dark.webp b/public/borders/deer-ears-horns-dark.webp new file mode 100644 index 000000000..5d2b70e11 Binary files /dev/null and b/public/borders/deer-ears-horns-dark.webp differ diff --git a/public/borders/deer-ears-horns.webp b/public/borders/deer-ears-horns.webp new file mode 100644 index 000000000..6cf0934e7 Binary files /dev/null and b/public/borders/deer-ears-horns.webp differ diff --git a/public/borders/deer-ears-white.webp b/public/borders/deer-ears-white.webp new file mode 100644 index 000000000..a31f629ae Binary files /dev/null and b/public/borders/deer-ears-white.webp differ diff --git a/public/borders/dog-ears-brown.webp b/public/borders/dog-ears-brown.webp new file mode 100644 index 000000000..f2f8ec606 Binary files /dev/null and b/public/borders/dog-ears-brown.webp differ diff --git a/public/borders/dog-ears-shiba.webp b/public/borders/dog-ears-shiba.webp new file mode 100644 index 000000000..07c046995 Binary files /dev/null and b/public/borders/dog-ears-shiba.webp differ diff --git a/public/borders/dog-tail-shiba.webp b/public/borders/dog-tail-shiba.webp new file mode 100644 index 000000000..f514985d8 Binary files /dev/null and b/public/borders/dog-tail-shiba.webp differ diff --git a/public/borders/emo-supporter-left-wing.webp b/public/borders/emo-supporter-left-wing.webp new file mode 100644 index 000000000..c52a0524b Binary files /dev/null and b/public/borders/emo-supporter-left-wing.webp differ diff --git a/public/borders/emo-supporter-right-wing.webp b/public/borders/emo-supporter-right-wing.webp new file mode 100644 index 000000000..79d126ece Binary files /dev/null and b/public/borders/emo-supporter-right-wing.webp differ diff --git a/public/borders/emo-supporter.webp b/public/borders/emo-supporter.webp new file mode 100644 index 000000000..ecd3e79a3 Binary files /dev/null and b/public/borders/emo-supporter.webp differ diff --git a/public/borders/founder-left-wing.png b/public/borders/founder-left-wing.png deleted file mode 100644 index 8842210a2..000000000 Binary files a/public/borders/founder-left-wing.png and /dev/null differ diff --git a/public/borders/founder-left-wing.webp b/public/borders/founder-left-wing.webp new file mode 100644 index 000000000..6698214af Binary files /dev/null and b/public/borders/founder-left-wing.webp differ diff --git a/public/borders/founder-right-wing.png b/public/borders/founder-right-wing.png deleted file mode 100644 index 15f795097..000000000 Binary files a/public/borders/founder-right-wing.png and /dev/null differ diff --git a/public/borders/founder-right-wing.webp b/public/borders/founder-right-wing.webp new file mode 100644 index 000000000..a66af0221 Binary files /dev/null and b/public/borders/founder-right-wing.webp differ diff --git a/public/borders/founder.png b/public/borders/founder.png deleted file mode 100644 index 8b044b4b3..000000000 Binary files a/public/borders/founder.png and /dev/null differ diff --git a/public/borders/founder.webp b/public/borders/founder.webp new file mode 100644 index 000000000..dd18fe312 Binary files /dev/null and b/public/borders/founder.webp differ diff --git a/public/borders/fox-ears-brown.webp b/public/borders/fox-ears-brown.webp new file mode 100644 index 000000000..0fe691bc8 Binary files /dev/null and b/public/borders/fox-ears-brown.webp differ diff --git a/public/borders/fox-ears-gold.webp b/public/borders/fox-ears-gold.webp new file mode 100644 index 000000000..e2ed66c86 Binary files /dev/null and b/public/borders/fox-ears-gold.webp differ diff --git a/public/borders/goat-ears-white.webp b/public/borders/goat-ears-white.webp new file mode 100644 index 000000000..3a8ec2f8c Binary files /dev/null and b/public/borders/goat-ears-white.webp differ diff --git a/public/borders/goat-horns.webp b/public/borders/goat-horns.webp new file mode 100644 index 000000000..1761d3c55 Binary files /dev/null and b/public/borders/goat-horns.webp differ diff --git a/public/borders/mod-left-wing.webp b/public/borders/mod-left-wing.webp new file mode 100644 index 000000000..e2cb94413 Binary files /dev/null and b/public/borders/mod-left-wing.webp differ diff --git a/public/borders/mod-right-wing.webp b/public/borders/mod-right-wing.webp new file mode 100644 index 000000000..1bdc55b91 Binary files /dev/null and b/public/borders/mod-right-wing.webp differ diff --git a/public/borders/mod.webp b/public/borders/mod.webp new file mode 100644 index 000000000..13b7165df Binary files /dev/null and b/public/borders/mod.webp differ diff --git a/public/borders/palestine.png b/public/borders/palestine.png deleted file mode 100644 index d4273dadc..000000000 Binary files a/public/borders/palestine.png and /dev/null differ diff --git a/public/borders/palestine.webp b/public/borders/palestine.webp new file mode 100644 index 000000000..0dfd53f14 Binary files /dev/null and b/public/borders/palestine.webp differ diff --git a/public/borders/supporter-left-wing.png b/public/borders/supporter-left-wing.png deleted file mode 100644 index c324618f7..000000000 Binary files a/public/borders/supporter-left-wing.png and /dev/null differ diff --git a/public/borders/supporter-left-wing.webp b/public/borders/supporter-left-wing.webp new file mode 100644 index 000000000..7317d52bb Binary files /dev/null and b/public/borders/supporter-left-wing.webp differ diff --git a/public/borders/supporter-right-wing.png b/public/borders/supporter-right-wing.png deleted file mode 100644 index ce555e975..000000000 Binary files a/public/borders/supporter-right-wing.png and /dev/null differ diff --git a/public/borders/supporter-right-wing.webp b/public/borders/supporter-right-wing.webp new file mode 100644 index 000000000..4662c984e Binary files /dev/null and b/public/borders/supporter-right-wing.webp differ diff --git a/public/borders/supporter.png b/public/borders/supporter.png deleted file mode 100644 index 6a0a38fd3..000000000 Binary files a/public/borders/supporter.png and /dev/null differ diff --git a/public/borders/supporter.webp b/public/borders/supporter.webp new file mode 100644 index 000000000..558e0ac49 Binary files /dev/null and b/public/borders/supporter.webp differ diff --git a/public/borders/wolf-ears.webp b/public/borders/wolf-ears.webp new file mode 100644 index 000000000..69e1e4b25 Binary files /dev/null and b/public/borders/wolf-ears.webp differ diff --git a/public/favicon-alert.ico b/public/favicon-alert.ico index 3a867bd5a..740ec64db 100644 Binary files a/public/favicon-alert.ico and b/public/favicon-alert.ico differ diff --git a/public/favicon.ico b/public/favicon.ico index 271b8a861..6f35b4f04 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ diff --git a/public/fonts/inter-v18-latin-300.woff2 b/public/fonts/inter-v18-latin-300.woff2 deleted file mode 100644 index 794bde5ab..000000000 Binary files a/public/fonts/inter-v18-latin-300.woff2 and /dev/null differ diff --git a/public/fonts/inter-v18-latin-300italic.woff2 b/public/fonts/inter-v18-latin-300italic.woff2 deleted file mode 100644 index 34af554af..000000000 Binary files a/public/fonts/inter-v18-latin-300italic.woff2 and /dev/null differ diff --git a/public/fonts/inter-v18-latin-700.woff2 b/public/fonts/inter-v18-latin-700.woff2 deleted file mode 100644 index 12b51d770..000000000 Binary files a/public/fonts/inter-v18-latin-700.woff2 and /dev/null differ diff --git a/public/fonts/inter-v18-latin-700italic.woff2 b/public/fonts/inter-v18-latin-700italic.woff2 deleted file mode 100644 index 461728072..000000000 Binary files a/public/fonts/inter-v18-latin-700italic.woff2 and /dev/null differ diff --git a/public/fonts/inter-v18-latin-italic.woff2 b/public/fonts/inter-v18-latin-italic.woff2 deleted file mode 100644 index ec07ef7dd..000000000 Binary files a/public/fonts/inter-v18-latin-italic.woff2 and /dev/null differ diff --git a/public/fonts/inter-v18-latin-regular.woff2 b/public/fonts/inter-v18-latin-regular.woff2 deleted file mode 100644 index 33002f128..000000000 Binary files a/public/fonts/inter-v18-latin-regular.woff2 and /dev/null differ diff --git a/public/icon512_maskable.png b/public/icon512_maskable.png new file mode 100644 index 000000000..d5b7a5b49 Binary files /dev/null and b/public/icon512_maskable.png differ diff --git a/public/icon512_rounded.png b/public/icon512_rounded.png new file mode 100644 index 000000000..0231bf3c4 Binary files /dev/null and b/public/icon512_rounded.png differ diff --git a/public/manifest.webmanifest b/public/manifest.webmanifest index 2eb8e2e2b..15fe37d4a 100644 --- a/public/manifest.webmanifest +++ b/public/manifest.webmanifest @@ -9,13 +9,15 @@ "display": "standalone", "icons": [ { - "src": "pwa-192x192.png", - "sizes": "192x192", + "purpose": "maskable", + "sizes": "512x512", + "src": "icon512_maskable.png", "type": "image/png" }, { - "src": "/pwa-512x512.png", + "purpose": "any", "sizes": "512x512", + "src": "icon512_rounded.png", "type": "image/png" } ] diff --git a/public/pwa-192x192.png b/public/pwa-192x192.png deleted file mode 100644 index 125ed1143..000000000 Binary files a/public/pwa-192x192.png and /dev/null differ diff --git a/public/pwa-512x512.png b/public/pwa-512x512.png deleted file mode 100644 index 29c053bd9..000000000 Binary files a/public/pwa-512x512.png and /dev/null differ diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 000000000..faf794c28 --- /dev/null +++ b/public/sw.js @@ -0,0 +1,38 @@ +const CACHE_NAME = "nerimity-assets"; + +self.addEventListener("install", () => { + console.log("Service Worker installing..."); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + console.log("Service Worker activate"); + event.waitUntil(self.clients.claim()); +}); + +self.addEventListener("fetch", async (event) => { + + if (event.request.method !== "GET") return; + + const url = new URL(event.request.url); + const isSameOrigin = url.origin === location.origin; + const isAssetsRequest = isSameOrigin && url.pathname.startsWith("/assets/"); + + if (!isAssetsRequest) { + return; + } + event.respondWith( + (async () => { + + const cache = await caches.open(CACHE_NAME); + const cached = await cache.match(event.request); + if (cached) return cached; + + const response = await fetch(event.request); + if (response && response.ok && response.status === 200) { + cache.put(event.request, response.clone()); + } + return response; + })() + ); +}); diff --git a/readme-assets/Dashboard.png b/readme-assets/Dashboard.png new file mode 100644 index 000000000..6bb3e7875 Binary files /dev/null and b/readme-assets/Dashboard.png differ diff --git a/readme-assets/Profile.png b/readme-assets/Profile.png new file mode 100644 index 000000000..b1c53a2ce Binary files /dev/null and b/readme-assets/Profile.png differ diff --git a/readme-assets/Server.png b/readme-assets/Server.png new file mode 100644 index 000000000..37e5dbd2f Binary files /dev/null and b/readme-assets/Server.png differ diff --git a/readme-assets/ServerSettings.png b/readme-assets/ServerSettings.png new file mode 100644 index 000000000..9ab5698dd Binary files /dev/null and b/readme-assets/ServerSettings.png differ diff --git a/src/App.tsx b/src/App.tsx index a8413335f..e9f7f9ac3 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -1,7 +1,14 @@ -import { createMemo, lazy, onCleanup, onMount, Show } from "solid-js"; +import { + createMemo, + createSignal, + lazy, + onCleanup, + onMount, + Show +} from "solid-js"; import { getCurrentLanguage, getLanguage } from "./locales/languages"; -import { useTransContext } from "@mbarzda/solid-i18next"; +import { useTransContext } from "@nerimity/solid-i18lite"; import { electronWindowAPI, spellcheckSuggestions } from "./common/Electron"; import { ElectronTitleBar } from "./components/ElectronTitleBar"; import { useMatch } from "solid-navigator"; @@ -9,16 +16,36 @@ import { useReactNativeEvent } from "./common/ReactNative"; import { registerFCM } from "./chat-api/services/UserService"; import { useCustomPortal } from "./components/ui/custom-portal/CustomPortal"; import ContextMenu, { - ContextMenuItem, + ContextMenuItem } from "./components/ui/context-menu/ContextMenu"; import { Delay } from "./common/Delay"; const ConnectingStatusHeader = lazy( () => import("@/components/connecting-status-header/ConnectingStatusHeader") ); + +const InAppNotificationPreviews = lazy( + () => import("@/components/in-app-notification-previews") +); + export default function App() { const [, actions] = useTransContext(); const isAppPage = useMatch(() => "/app/*"); + const [customTitlebarDisabled, setCustomTitlebarDisabled] = + createSignal(false); + + onMount(() => { + if (electronWindowAPI()?.isElectron) { + electronWindowAPI() + ?.getCustomTitlebarDisabled() + .then((disabled) => { + setCustomTitlebarDisabled(disabled); + if (!disabled) { + document.body.classList.add("electron-custom-titlebar"); + } + }); + } + }); useElectronContextMenu(); @@ -33,6 +60,11 @@ export default function App() { const setLanguage = async () => { const key = getCurrentLanguage(); if (!key) return; + + // Set language attribute without changing layout direction + const langKey = key.replace("_", "-"); + document.documentElement.setAttribute("lang", langKey || "en"); + if (key === "en_gb") return; const language = await getLanguage(key); if (!language) return; @@ -42,11 +74,12 @@ export default function App() { return ( <> - + + ); @@ -90,8 +123,8 @@ const InputContextMenu = (props: { id: suggestion, label: suggestion, icon: "spellcheck", - onClick: () => electronWindowAPI()?.replaceMisspelling(suggestion), - })), + onClick: () => electronWindowAPI()?.replaceMisspelling(suggestion) + })) ]; if (items.length) { @@ -100,36 +133,37 @@ const InputContextMenu = (props: { const selection = getSelection(); const highlighted = selection?.toString(); + const [t] = useTransContext(); if (highlighted) { items.push({ id: "copy", - label: "Copy", + label: t("inputFieldActions.copy"), icon: "content_copy", onClick: () => { props.input.focus(); electronWindowAPI()?.clipboardCopy(highlighted); - }, + } }); items.push({ id: "cut", - label: "Cut", + label: t("inputFieldActions.cut"), icon: "content_cut", onClick: () => { props.input.focus(); electronWindowAPI()?.clipboardCut(); - }, + } }); } items.push({ id: "paste", - label: "Paste", + label: t("inputFieldActions.paste"), icon: "content_paste", onClick: () => { props.input.focus(); electronWindowAPI()?.clipboardPaste(); - }, + } }); return items; diff --git a/src/LogoMono.tsx b/src/LogoMono.tsx new file mode 100644 index 000000000..4931b1dc2 --- /dev/null +++ b/src/LogoMono.tsx @@ -0,0 +1,16 @@ +export const LogoMono = () => { + return ( + + + + ); +}; diff --git a/src/chat-api/Bitwise.ts b/src/chat-api/Bitwise.ts index 309bdcfe3..70687e9b3 100644 --- a/src/chat-api/Bitwise.ts +++ b/src/chat-api/Bitwise.ts @@ -1,71 +1,333 @@ +import { t } from "@nerimity/i18lite"; export interface Bitwise { - name: string; - description?: string; + name: () => string; + description?: () => string; bit: number; - icon?: string + icon?: string; + textColor?: string; showSettings?: boolean; // determine should this permission reveal the "settings" option context menu } +const USER_BADGE_BITS = { + FOUNDER: 1, + ADMIN: 2, + CONTRIBUTOR: 4, + SUPPORTER: 8, + PALESTINE: 16, + BOT: 32, + MOD: 64, + EMO_SUPPORTER: 128, + + CAT_EARS_WHITE: 256, + CAT_EARS_BLUE: 512, + + FOX_EARS_GOLD: 1024, + FOX_EARS_BROWN: 2048, + + BUNNY_EARS_BLACK: 4096, + BUNNY_EARS_MAID: 8192, + DOG_EARS_BROWN: 16384, + DOG_SHIBA: 32768, + + WOLF_EARS: 65536, + GOAT_EARS_WHITE: 131072, + DEER_EARS_HORNS: 262144, + GOAT_HORNS: 524288, + DEER_EARS_HORNS_DARK: 1048576, + CAT_EARS_MAID: 2097152, + CAT_EARS_PURPLE: 4194304, + DEER_EARS_WHITE: 8388608 +} as const; + +export interface UserBadge { + name: () => string; + bit: (typeof USER_BADGE_BITS)[keyof typeof USER_BADGE_BITS]; + color: string; + overlay?: boolean; + description: () => string; + textColor?: string; + credit?: () => string; + type?: "earned"; + icon?: string; + removable?: boolean; +} + export const USER_BADGES = { + // overlays + DEER_EARS_WHITE: { + name: () => t("badges.deer.name"), + bit: USER_BADGE_BITS.DEER_EARS_WHITE, + color: "linear-gradient(273deg, #fb83a7, #ffffff)", + textColor: "#2a1d1d", + overlay: true, + description: () => t("badges.deer.deerWhiteDescription"), + icon: "pets" + }, + DEER_EARS_HORNS_DARK: { + name: () => t("badges.deer.name"), + bit: USER_BADGE_BITS.DEER_EARS_HORNS_DARK, + color: "linear-gradient(267deg, #8f8f8f, #090a25)", + textColor: "#ffffff", + overlay: true, + description: () => t("badges.deer.deerDarkDescription"), + icon: "pets" + }, + DEER_EARS_HORNS: { + name: () => t("badges.deer.name"), + bit: USER_BADGE_BITS.DEER_EARS_HORNS, + color: "linear-gradient(270deg, #aa4908, #ffd894)", + textColor: "#321515", + overlay: true, + description: () => t("badges.deer.deerDescription"), + icon: "pets" + }, + GOAT_HORNS: { + name: () => t("badges.goat.name"), + bit: USER_BADGE_BITS.GOAT_HORNS, + color: "linear-gradient(268deg, #cb75d7, #390a8f)", + overlay: true, + description: () => t("badges.goat.goatHornDescription"), + icon: "pets" + }, + GOAT_EARS_WHITE: { + name: () => t("badges.goat.name"), + bit: USER_BADGE_BITS.GOAT_EARS_WHITE, + color: "linear-gradient(89deg, #ffecc2, #94e4ff)", + textColor: "#503030", + overlay: true, + description: () => t("badges.goat.goatDescription"), + icon: "pets" + }, + WOLF_EARS: { + name: () => t("badges.wolf.name"), + bit: USER_BADGE_BITS.WOLF_EARS, + color: "linear-gradient(90deg, #585858ff 0%, #252525ff 100%)", + textColor: "#ffffff", + overlay: true, + description: () => t("badges.wolf.wolfDescription"), + icon: "pets" + }, + DOG_SHIBA: { + name: () => t("badges.dog.name"), + bit: USER_BADGE_BITS.DOG_SHIBA, + color: "linear-gradient(261deg, #ffeeb3, #9e7aff)", + textColor: "#2e1919", + overlay: true, + description: () => t("badges.dog.shibaDescription"), + icon: "sound_detection_dog_barking" + }, + DOG_EARS_BROWN: { + name: () => t("badges.dog.name"), + bit: USER_BADGE_BITS.DOG_EARS_BROWN, + color: "linear-gradient(90deg, #bb7435 0%, #ffbd67ff 100%)", + overlay: true, + description: () => t("badges.dog.dogDescription"), + icon: "sound_detection_dog_barking" + }, + BUNNY_EARS_MAID: { + name: () => t("badges.bunny.name"), + bit: USER_BADGE_BITS.BUNNY_EARS_MAID, + color: "linear-gradient(100deg, #ff94e2, #ffffff)", + textColor: "#2a1d1d", + overlay: true, + description: () => t("badges.bunny.maidDescription"), + icon: "cruelty_free" + }, + BUNNY_EARS_BLACK: { + name: () => t("badges.bunny.name"), + bit: USER_BADGE_BITS.BUNNY_EARS_BLACK, + color: "linear-gradient(90deg, #585858ff 0%, #252525ff 100%)", + textColor: "#ffffff", + overlay: true, + description: () => t("badges.bunny.bunnyDescription"), + icon: "cruelty_free" + }, + CAT_EARS_MAID: { + name: () => t("badges.kitty.name"), + bit: USER_BADGE_BITS.CAT_EARS_MAID, + color: "linear-gradient(100deg, #ff94e2, #ffffff)", + textColor: "#2a1d1d", + overlay: true, + description: () => t("badges.kitty.maidDescription"), + icon: "pets" + }, + CAT_EARS_PURPLE: { + name: () => t("badges.kitty.name"), + bit: USER_BADGE_BITS.CAT_EARS_PURPLE, + color: "linear-gradient(268deg, #cb75d7, #390a8f)", + textColor: "#ffffff", + overlay: true, + description: () => t("badges.kitty.purpleDescription"), + icon: "pets" + }, + CAT_EARS_BLUE: { + name: () => t("badges.kitty.name"), + bit: USER_BADGE_BITS.CAT_EARS_BLUE, + color: "linear-gradient(90deg, #78a5ff 0%, #ffffff 100%)", + overlay: true, + description: () => t("badges.kitty.blueDescription"), + icon: "pets" + }, + + CAT_EARS_WHITE: { + name: () => t("badges.kitty.name"), + bit: USER_BADGE_BITS.CAT_EARS_WHITE, + color: "linear-gradient(90deg, #ffa761 0%, #ffffff 100%)", + overlay: true, + description: () => t("badges.kitty.whiteDescription"), + icon: "pets" + }, + + FOX_EARS_GOLD: { + name: () => t("badges.foxy.name"), + bit: USER_BADGE_BITS.FOX_EARS_GOLD, + color: "linear-gradient(90deg, #ffb100 0%, #ffffff 100%)", + overlay: true, + description: () => t("badges.foxy.foxyGoldDescription"), + icon: "pets" + }, + + FOX_EARS_BROWN: { + name: () => t("badges.foxy.name"), + bit: USER_BADGE_BITS.FOX_EARS_BROWN, + color: "linear-gradient(90deg, #bb7435 0%, #ffffff 100%)", + overlay: true, + description: () => t("badges.foxy.foxyBrownDescription"), + icon: "pets" + }, + FOUNDER: { - name: "Founder", - bit: 1, - description: "Creator of Nerimity", - color: "linear-gradient(90deg, #4fffbd 0%, #4a5efc 100%);", - credit: "Avatar Border by upklyak on Freepik" + name: () => t("badges.founder.name"), + bit: USER_BADGE_BITS.FOUNDER, + description: () => t("badges.founder.description"), + color: "linear-gradient(90deg, #4fffbd 0%, #4a5efc 100%)", + credit: () => + t("badges.credit.avatarBorder", { + author: "upklyak", + platform: "Freepik" + }), + type: "earned", + icon: "crown", + removable: false, }, + ADMIN: { - name: "Admin", - bit: 2, - description: "Admin of Nerimity", - color: "linear-gradient(90deg, rgba(224,26,185,1) 0%, rgba(64,122,255,1) 100%);", - credit: "Avatar Border by upklyak on Freepik" + name: () => t("badges.admin.name"), + bit: USER_BADGE_BITS.ADMIN, + description: () => t("badges.admin.description"), + color: + "linear-gradient(90deg, rgba(224,26,185,1) 0%, rgba(64,122,255,1) 100%)", + credit: () => + t("badges.credit.avatarBorderEdited", { + author: "upklyak", + platform: "Freepik", + editor: "Supertiger" + }), + type: "earned", + icon: "verified_user", + removable: false, + }, + + MOD: { + name: () => t("badges.mod.name"), + bit: USER_BADGE_BITS.MOD, + description: () => t("badges.mod.description"), + color: "linear-gradient(90deg, #57acfa 0%, #1485ed 100%)", + credit: () => + t("badges.credit.avatarBorder", { + author: "upklyak", + platform: "Freepik" + }), + type: "earned", + icon: "shield", + removable: false, }, + + EMO_SUPPORTER: { + name: () => t("badges.emoSupporter.name"), + description: () => t("badges.emoSupporter.description"), + bit: USER_BADGE_BITS.EMO_SUPPORTER, + textColor: "rgba(255,255,255,0.8)", + color: "linear-gradient(90deg, #424242 0%, #303030 100%)", + credit: () => + t("badges.credit.avatarBorderEdited", { + author: "upklyak", + platform: "Freepik", + editor: "Supertiger" + }), + type: "earned", + icon: "favorite" + }, + SUPPORTER: { - name: "Supporter", - description: "Supported this project by donating money", - bit: 8, - color: "linear-gradient(90deg, rgba(235,78,209,1) 0%, rgba(243,189,247,1) 100%)", - credit: "Avatar Border by upklyak on Freepik" + name: () => t("badges.supporter.name"), + description: () => t("badges.supporter.description"), + bit: USER_BADGE_BITS.SUPPORTER, + color: + "linear-gradient(90deg, rgba(235,78,209,1) 0%, rgba(243,189,247,1) 100%)", + credit: () => + t("badges.credit.avatarBorder", { + author: "upklyak", + platform: "Freepik" + }), + type: "earned", + icon: "favorite" }, + CONTRIBUTOR: { - name: "Contributor", - description: "Helped with this project in some way", - bit: 4, - color: "#ffffff" + name: () => t("badges.contributor.name"), + description: () => t("badges.contributor.description"), + bit: USER_BADGE_BITS.CONTRIBUTOR, + color: "#ffffff", + type: "earned", + icon: "crowdsource" }, + PALESTINE: { - name: "Palestine", - description: "[Click To Help](https://arab.org/click-to-help/palestine/)", - bit: 16, - credit: "Avatar Border by upklyak on Freepik, edited by Supertiger", - color: "linear-gradient(90deg, red, white, green);" + name: () => t("badges.palestine.name"), + description: () => + "[Click to help](https://arab.org/click-to-help/palestine/)", + bit: USER_BADGE_BITS.PALESTINE, + color: "linear-gradient(90deg, red, white, green)", + credit: () => + t("badges.credit.avatarBorderEdited", { + author: "upklyak", + platform: "Freepik", + editor: "Supertiger" + }), + icon: "volunteer_activism" }, + BOT: { - name: "Bot User", - description: "Bot User", - bit: 32, - color: "var(--primary-color)" - }, -}; + name: () => t("badges.bot.name"), + description: () => t("badges.bot.description"), + bit: USER_BADGE_BITS.BOT, + color: "var(--primary-color)", + type: "earned", + icon: "robot_2", + removable: false, + } +} satisfies Record; + +export const USER_BADGES_VALUES = Object.values(USER_BADGES) as UserBadge[]; export const CHANNEL_PERMISSIONS = { - PRIVATE_CHANNEL: { - name: "servers.channelPermissions.privateChannel", - description: "servers.channelPermissions.privateChannelDescription", + PUBLIC_CHANNEL: { + name: () => t("servers.channelPermissions.publicChannel"), + description: () => t("servers.channelPermissions.publicChannelDescription"), bit: 1, - icon: "lock" + icon: "public" }, SEND_MESSAGE: { - name: "servers.channelPermissions.sendMessage", - description: "servers.channelPermissions.sendMessageDescription", + name: () => t("servers.channelPermissions.sendMessage"), + description: () => t("servers.channelPermissions.sendMessageDescription"), bit: 2, icon: "mail" }, JOIN_VOICE: { - name: "servers.channelPermissions.joinVoice", - description: "servers.channelPermissions.joinVoiceDescription", + name: () => t("servers.channelPermissions.joinVoice"), + description: () => t("servers.channelPermissions.joinVoiceDescription"), bit: 4, icon: "call" } @@ -73,57 +335,84 @@ export const CHANNEL_PERMISSIONS = { export const ROLE_PERMISSIONS = { ADMIN: { - name: "servers.rolePermissions.admin", - description: "servers.rolePermissions.adminDescription", + name: () => t("servers.rolePermissions.admin"), + description: () => t("servers.rolePermissions.adminDescription"), bit: 1, // icon: 'mail', // looks good even without icon showSettings: true }, SEND_MESSAGE: { - name: "servers.rolePermissions.sendMessage", - description: "servers.rolePermissions.sendMessageDescription", + name: () => t("servers.rolePermissions.sendMessage"), + description: () => t("servers.rolePermissions.sendMessageDescription"), bit: 2, icon: "mail" }, MANAGE_ROLES: { - name: "servers.rolePermissions.manageRoles", - description: "servers.rolePermissions.manageRolesDescription", + name: () => t("servers.rolePermissions.manageRoles"), + description: () => t("servers.rolePermissions.manageRolesDescription"), icon: "leaderboard", bit: 4, showSettings: true }, MANAGE_CHANNELS: { - name: "servers.rolePermissions.manageChannels", - description: "servers.rolePermissions.manageChannelsDescription", + name: () => t("servers.rolePermissions.manageChannels"), + description: () => t("servers.rolePermissions.manageChannelsDescription"), icon: "storage", bit: 8, showSettings: true }, KICK: { - name: "servers.rolePermissions.kick", - description: "servers.rolePermissions.kickDescription", + name: () => t("servers.rolePermissions.kick"), + description: () => t("servers.rolePermissions.kickDescription"), bit: 16, icon: "logout", showSettings: true }, BAN: { - name: "servers.rolePermissions.ban", - description: "servers.rolePermissions.banDescription", + name: () => t("servers.rolePermissions.ban"), + description: () => t("servers.rolePermissions.banDescription"), bit: 32, showSettings: true, icon: "block" }, MENTION_EVERYONE: { - name: "servers.rolePermissions.mentionEveryone", - description: "servers.rolePermissions.mentionEveryoneDescription", + name: () => t("servers.rolePermissions.mentionEveryone"), + description: () => t("servers.rolePermissions.mentionEveryoneDescription"), bit: 64, icon: "alternate_email" }, NICKNAME_MEMBER: { - name: "servers.rolePermissions.nicknameMember", - description: "servers.rolePermissions.nicknameMemberDescription", + name: () => t("servers.rolePermissions.nicknameMember"), + description: () => t("servers.rolePermissions.nicknameMemberDescription"), bit: 128, icon: "edit" + }, + MENTION_ROLES: { + name: () => "Mention Roles", + bit: 256, + description: () => "Allow users to mention roles", + icon: "alternate_email" + } +}; + +export const APPLICATION_SCOPES = { + USER_INFO: { + name: () => "User Info", + description: () => "Access to your user information.", + bit: 1, + icon: "person" + }, + USER_EMAIL: { + name: () => "User Email", + description: () => "Access to your email address", + bit: 2, + icon: "mail" + }, + USER_SERVERS: { + name: () => "User Servers", + description: () => "Get the list of servers you are in", + bit: 4, + icon: "server" } }; @@ -138,8 +427,11 @@ export const removeBit = (permissions: number, bit: number) => { return permissions & ~bit; }; -export const getAllPermissions = (permissionList: Record, permissions: number) => { - return Object.values(permissionList).map(permission => { +export const getAllPermissions = ( + permissionList: Record, + permissions: number +) => { + return Object.values(permissionList).map((permission) => { const hasPerm = hasBit(permissions, permission.bit); return { ...permission, diff --git a/src/chat-api/EventNames.ts b/src/chat-api/EventNames.ts index dbb588fbe..490bb41a5 100644 --- a/src/chat-api/EventNames.ts +++ b/src/chat-api/EventNames.ts @@ -3,7 +3,7 @@ export const ClientEvents = { NOTIFICATION_DISMISS: "notification:dismiss", VOICE_SIGNAL_SEND: "voice:signal_send", - UPDATE_ACTIVITY: "user:update_activity", + UPDATE_ACTIVITY: "user:update_activity" }; export const ServerEvents = { @@ -18,6 +18,7 @@ export const ServerEvents = { USER_NOTIFICATION_SETTINGS_UPDATE: "user:notification_settings_update", + USER_AUTH_QUEUE_POSITION: "user:auth_queue_position", USER_AUTHENTICATED: "user:authenticated", USER_PRESENCE_UPDATE: "user:presence_update", @@ -25,6 +26,10 @@ export const ServerEvents = { USER_BLOCKED: "user:blocked", USER_UNBLOCKED: "user:unblocked", + USER_REMINDER_ADD: "user:reminder_add", + USER_REMINDER_UPDATE: "user:reminder_update", + USER_REMINDER_REMOVE: "user:reminder_remove", + FRIEND_REQUEST_SENT: "friend:request_sent", FRIEND_REQUEST_PENDING: "friend:request_pending", FRIEND_REQUEST_ACCEPTED: "friend:request_accepted", @@ -50,16 +55,26 @@ export const ServerEvents = { SERVER_CHANNEL_UPDATED: "server:channel_updated", SERVER_CHANNEL_DELETED: "server:channel_deleted", SERVER_ORDER_UPDATED: "server:order_updated", + SERVER_FOLDER_CREATED: "server:folder_created", + SERVER_FOLDER_UPDATED: "server:folder_updated", + + SERVER_CHANNEL_PERMISSIONS_UPDATED: "server:channel_permissions_updated", SERVER_EMOJI_ADD: "server:emoji_add", SERVER_EMOJI_REMOVE: "server:emoji_remove", SERVER_EMOJI_UPDATE: "server:emoji_update", + SERVER_SCHEDULE_DELETE: "server:schedule_delete", + SERVER_REMOVE_SCHEDULE_DELETE: "server:remove_schedule_delete", + + SERVER_CLAN_UPDATED: "server:clan_updated", + CHANNEL_TYPING: "channel:typing", MESSAGE_CREATED: "message:created", MESSAGE_UPDATED: "message:updated", MESSAGE_DELETED: "message:deleted", MESSAGE_DELETED_BATCH: "message:deleted_batch", + MESSAGE_MARK_UNREAD: "message:mark_unread", MESSAGE_REACTION_ADDED: "message:reaction_added", MESSAGE_REACTION_REMOVED: "message:reaction_removed", @@ -68,5 +83,5 @@ export const ServerEvents = { VOICE_USER_JOINED: "voice:user_joined", VOICE_USER_LEFT: "voice:user_left", - VOICE_SIGNAL_RECEIVED: "voice:signal_received", + VOICE_SIGNAL_RECEIVED: "voice:signal_received" } as const; diff --git a/src/chat-api/RawData.ts b/src/chat-api/RawData.ts index 1642c8868..1edc937ad 100644 --- a/src/chat-api/RawData.ts +++ b/src/chat-api/RawData.ts @@ -4,16 +4,49 @@ export interface RawServer { hexColor: string; defaultChannelId: string; systemChannelId?: string; + clan?: RawServerClan; avatar?: string; banner?: string; defaultRoleId: string; createdById: string; createdAt: number; verified: boolean; + // will be set to false after user dismisses notification notice on left drawer. + joinedThisSession?: boolean; customEmojis: RawCustomEmoji[]; _count?: { welcomeQuestions: number; }; + scheduledForDeletion?: { + scheduledAt: number; + }; +} +export interface RawServerClan { + serverId?: string; + tag: string; + icon: string; +} + +export const InventoryItemType = { + Badge: "badge" +} as const; +export interface RawInventoryItem { + id: string; + itemType: (typeof InventoryItemType)[keyof typeof InventoryItemType]; + userId: string; + itemId: string; + acquiredAt: number; +} + +export interface RawWebhook { + name: string; + id: string; + hexColor: string; + avatar: string | null; + channelId: string; + serverId: string | null; + createdById: string; + createdAt: number; } export interface RawVoice { @@ -29,6 +62,8 @@ export enum MessageType { KICK_USER = 3, BAN_USER = 4, CALL_STARTED = 5, + BUMP_SERVER = 6, + PINNED_MESSAGE = 7 } export interface HtmlEmbedItem { @@ -39,10 +74,19 @@ export interface HtmlEmbedItem { export interface RawMessage { id: string; channelId: string; + silent?: boolean; content?: string; - createdBy: RawUser; + + createdBy: RawUser & { + avatarUrl?: string; + profile?: { + font?: number; + clan?: RawServerClan; + }; + }; type: MessageType; createdAt: number; + pinned?: boolean; editedAt?: number; mentions?: Array; attachments?: Array; @@ -56,6 +100,8 @@ export interface RawMessage { }[]; buttons: RawMessageButton[]; + roleMentions: RawServerRole[]; + webhookId?: string; } export interface RawMessageButton { @@ -65,6 +111,13 @@ export interface RawMessageButton { alert?: boolean; } +export interface RawServerFolder { + id: string; + name: string; + color: string; + serverIds: string[]; +} + export interface RawEmbed { title?: string; type?: string; @@ -73,6 +126,7 @@ export interface RawEmbed { origUrl?: string; imageUrl?: string; imageWidth?: number; + animated?: boolean; imageHeight?: number; imageMime?: string; @@ -89,6 +143,7 @@ export interface RawMessageReaction { name: string; emojiId?: string | null; gif?: boolean; + webp?: boolean; reacted: boolean; count: number; @@ -101,17 +156,22 @@ export interface RawChannelNotice { userId: string; } +export type AttachmentProviders = "local" | "google_drive"; export interface RawAttachment { id: string; - provider?: "local" | "google_drive"; + provider?: AttachmentProviders; fileId?: string; mime?: string; messageId?: string; + duration?: number; path?: string; width?: number; height?: number; createdAt?: number; + + filesize?: number; + expireAt?: number; } export interface RawUser { @@ -126,23 +186,27 @@ export interface RawUser { bot: boolean; lastOnlineStatus?: number; lastOnlineAt?: number; + profile?: { + font?: number; + clan?: RawServerClan; + }; } export interface RawUserConnection { id: string; - provider: "GOOGLE"; + provider: "GOOGLE" | "GOOGLE_DRIVE"; connectedAt: number; } export enum ServerNotificationSoundMode { ALL = 0, MENTIONS_ONLY = 1, - MUTE = 2, + MUTE = 2 } export enum ServerNotificationPingMode { ALL = 0, MENTIONS_ONLY = 1, - MUTE = 2, + MUTE = 2 } export interface RawUserNotificationSettings { notificationSoundMode: ServerNotificationSoundMode; @@ -157,24 +221,25 @@ export interface RawServerMember { joinedAt: number; nickname?: string | null; roleIds: string[]; + muteExpireAt?: number; } export enum ChannelType { DM_TEXT = 0, SERVER_TEXT = 1, - CATEGORY = 2, + CATEGORY = 2 } export enum TicketStatus { WAITING_FOR_MODERATOR_RESPONSE = 0, WAITING_FOR_USER_RESPONSE = 1, CLOSED_AS_DONE = 2, - CLOSED_AS_INVALID = 3, + CLOSED_AS_INVALID = 3 } export const CloseTicketStatuses = [ TicketStatus.CLOSED_AS_DONE, - TicketStatus.CLOSED_AS_INVALID, + TicketStatus.CLOSED_AS_INVALID ]; export interface RawTicket { @@ -188,6 +253,7 @@ export interface RawTicket { openedBy?: RawUser; openedAt: Date; seen?: boolean; + ignoredByUsers?: { userId: string }[]; } export interface RawChannel { @@ -198,7 +264,7 @@ export interface RawChannel { createdById?: string; serverId?: string; type: ChannelType; - permissions?: number; + permissions?: ServerChannelPermissions[]; createdAt: number; lastMessagedAt?: number; order?: number; @@ -207,10 +273,16 @@ export interface RawChannel { _count?: { attachments: number }; } +interface ServerChannelPermissions { + permissions: number; + roleId: string; +} + export interface RawCustomEmoji { id: string; name: string; gif: boolean; + webp: boolean; serverId?: string; } @@ -219,19 +291,21 @@ export interface RawServerRole { name: string; icon?: string; order: number; - hexColor: string; + hexColor?: string; + font?: number; createdById: string; permissions: number; serverId: string; hideRole: boolean; botRole?: boolean; + applyOnJoin?: boolean; } export enum FriendStatus { SENT = 0, PENDING = 1, FRIENDS = 2, - BLOCKED = 3, + BLOCKED = 3 } export enum TicketCategory { @@ -239,7 +313,7 @@ export enum TicketCategory { ACCOUNT = 1, ABUSE = 2, OTHER = 3, - SERVER_VERIFICATION = 4, + SERVER_VERIFICATION = 4 } export interface RawFriend { @@ -270,22 +344,49 @@ export interface ActivityStatus { title?: string; subtitle?: string; link?: string; + emoji?: string; } export interface RawPresence { userId: string; custom?: string; status: number; - activity?: ActivityStatus; + activities?: ActivityStatus[]; } -export interface RawPublicServer { +export interface RawExploreItem { + id: string; serverId: string; createdAt: number; bumpedAt: number; description: string; bumpCount: number; + pinnedAt?: number; lifetimeBumpCount: number; - server?: RawServer & { _count: { serverMembers: number } }; + server?: RawServer & { + _count: { serverMembers: number }; + createdBy: RawUser; + }; + botPermissions?: number; + botApplication?: { + id: string; + botUser: RawUser & { + online: boolean; + _count: { + servers: number; + }; + }; + creatorAccount: { + user: RawUser; + }; + }; +} + +export interface RawBotCommand { + name: string; + description: string; + args: string; + botUserId: string; + permissions: number | null; } export interface RawPost { @@ -297,11 +398,16 @@ export interface RawPost { commentToId: string; commentTo?: RawPost; createdBy: RawUser; + embed?: RawEmbed; createdAt: number; + mentions: RawUser[]; editedAt: number; likedBy: { id: string }[]; // if you liked this post, array will not be empty - _count: { likedBy: number; comments: number }; + reposts: { id: string; createdBy: { id: string; username: string } }[]; + repost?: RawPost; + _count: { likedBy: number; comments: number; reposts: number }; views: number; + announcement: any; poll?: RawPostPoll; } @@ -328,6 +434,8 @@ export enum PostNotificationType { LIKED = 0, REPLIED = 1, FOLLOWED = 2, + REPOSTED = 3, + MENTIONED = 4 } export interface RawPostNotification { @@ -352,6 +460,8 @@ export interface RawApplication { }; creatorAccountId: string; createdAt: number; + redirectUris: string[]; + clientSecret?: string; } export interface RawServerWelcomeQuestion { @@ -382,3 +492,12 @@ export interface RawNotice { createdAt: number; createdBy: { username: string }; } + +export interface RawReminder { + id: string; + message?: RawMessage; + post?: RawPost; + channelId?: string; + createdAt: number; + remindAt: number; +} diff --git a/src/chat-api/emits/userEmits.ts b/src/chat-api/emits/userEmits.ts index 20991f8c5..880187ddc 100644 --- a/src/chat-api/emits/userEmits.ts +++ b/src/chat-api/emits/userEmits.ts @@ -6,6 +6,6 @@ export function dismissChannelNotification(channelId: string) { socketClient.socket.emit(ClientEvents.NOTIFICATION_DISMISS, { channelId }); } -export function emitActivityStatus(activity: ActivityStatus | null) { - socketClient.socket.emit(ClientEvents.UPDATE_ACTIVITY, activity); -} \ No newline at end of file +export function emitActivityStatus(activities: ActivityStatus[] | null) { + socketClient.socket.emit(ClientEvents.UPDATE_ACTIVITY, activities); +} diff --git a/src/chat-api/emits/voiceEmits.ts b/src/chat-api/emits/voiceEmits.ts index 0e42509bc..cfffc3b2d 100644 --- a/src/chat-api/emits/voiceEmits.ts +++ b/src/chat-api/emits/voiceEmits.ts @@ -2,6 +2,14 @@ import type SimplePeer from "@thaunknown/simple-peer"; import { ClientEvents } from "../EventNames"; import socketClient from "../socketClient"; -export function emitVoiceSignal(channelId: string, toUserId: string, signal: SimplePeer.SignalData) { - socketClient.socket.emit(ClientEvents.VOICE_SIGNAL_SEND, { channelId, toUserId, signal }); -} \ No newline at end of file +export function emitVoiceSignal( + channelId: string, + toUserId: string, + signal: SimplePeer.SignalData +) { + socketClient.socket.emit(ClientEvents.VOICE_SIGNAL_SEND, { + channelId, + toUserId, + signal + }); +} diff --git a/src/chat-api/events/connectionEventTypes.ts b/src/chat-api/events/connectionEventTypes.ts index 483886e09..a3ff9c255 100644 --- a/src/chat-api/events/connectionEventTypes.ts +++ b/src/chat-api/events/connectionEventTypes.ts @@ -1,10 +1,26 @@ -import { RawChannel, RawFriend, RawInboxWithoutChannel, RawPresence, RawServer, RawServerMember, RawServerRole, RawUserNotificationSettings, RawUser, RawUserConnection, RawVoice, RawNotice } from "../RawData"; +import { + RawChannel, + RawFriend, + RawInboxWithoutChannel, + RawPresence, + RawServer, + RawServerMember, + RawServerRole, + RawUserNotificationSettings, + RawUser, + RawUserConnection, + RawVoice, + RawNotice, + RawReminder, + RawServerFolder, + RawServerClan +} from "../RawData"; export interface AuthenticatedPayload { user: SelfUser; servers: RawServer[]; serverMembers: RawServerMember[]; - messageMentions: MessageMention[] + messageMentions: MessageMention[]; channels: RawChannel[]; serverRoles: RawServerRole[]; notificationSettings: RawUserNotificationSettings[]; @@ -13,8 +29,8 @@ export interface AuthenticatedPayload { inbox: RawInboxWithoutChannel[]; lastSeenServerChannelIds: Record; // { [channelId]: timestamp } voiceChannelUsers: RawVoice[]; - - + newToken?: string; + sessionId: string; } interface MessageMention { @@ -22,27 +38,26 @@ interface MessageMention { mentionedBy: RawUser; count: number; serverId?: string; - channelId: string + channelId: string; createdAt: number; } export enum LastOnlineStatus { HIDDEN = 0, FRIENDS = 1, - FRIENDS_AND_SERVERS = 2, + FRIENDS_AND_SERVERS = 2 } export enum DmStatus { OPEN = 0, FRIENDS_AND_SERVERS = 1, - FRIENDS = 2, + FRIENDS = 2 } export enum FriendRequestStatus { OPEN = 0, SERVERS = 1, - CLOSED = 2, + CLOSED = 2 } - export interface SelfUser { id: string; email: string; @@ -53,14 +68,19 @@ export interface SelfUser { badges: number; tag: string; customStatus?: string; - orderedServerIds: string[] - dmStatus: DmStatus - friendRequestStatus: FriendRequestStatus - lastOnlineStatus?: number - emailConfirmed: boolean - connections: RawUserConnection[] - notices: RawNotice[] - + orderedServerIds: string[]; + dmStatus: DmStatus; + friendRequestStatus: FriendRequestStatus; + lastOnlineStatus?: number; + emailConfirmed: boolean; + connections: RawUserConnection[]; + notices: RawNotice[]; + serverFolders: RawServerFolder[]; hideFollowers: boolean; hideFollowing: boolean; -} \ No newline at end of file + reminders: RawReminder[]; + profile?: { + font?: number; + clan?: RawServerClan; + }; +} diff --git a/src/chat-api/events/connectionEvents.ts b/src/chat-api/events/connectionEvents.ts index 43e6da899..d2279d1cd 100644 --- a/src/chat-api/events/connectionEvents.ts +++ b/src/chat-api/events/connectionEvents.ts @@ -6,33 +6,55 @@ import useAccount from "../store/useAccount"; import useStore from "../store/useStore"; import { AuthenticatedPayload } from "./connectionEventTypes"; import useVoiceUsers from "../store/useVoiceUsers"; -import { StorageKeys, getStorageObject } from "@/common/localStorage"; -import { ProgramWithAction, electronWindowAPI } from "@/common/Electron"; +import { + StorageKeys, + getStorageObject, + setStorageString, + useCollapsedServerCategories +} from "@/common/localStorage"; +import { ProgramWithExtras, electronWindowAPI } from "@/common/Electron"; import { emitActivityStatus } from "../emits/userEmits"; -import { isExperimentEnabled, useExperiment } from "@/common/experiments"; import { localRPC } from "@/common/LocalRPC"; import { reactNativeAPI } from "@/common/ReactNative"; -import { useWindowProperties } from "@/common/useWindowProperties"; import useChannelProperties from "../store/useChannelProperties"; +import { useDiscordActivityTracker } from "@/common/useDiscordActivityTracker"; +import { useLastFmActivityTracker } from "@/common/useLastFmActivityTracker"; +import { type DisconnectDescription } from "socket.io-client/build/esm/socket"; +import { isExperimentEnabled } from "@/common/experiments"; +import { decompressObject } from "@/common/zstd"; +import { log } from "@/common/logger"; +import socketClient from "../socketClient"; + +// const partial = isExperimentEnabled("WEBSOCKET_PARTIAL_AUTH")(); +const zstd = isExperimentEnabled("WEBSOCKET_ZSTD")(); export const onConnect = (socket: Socket, token?: string) => { const account = useAccount(); account.setSocketDetails({ socketId: socket.id, socketConnected: true, - socketAuthenticated: false, + socketAuthenticated: false + }); + socket.emit(ClientEvents.AUTHENTICATE, { + token, + // ...(partial ? { partial: true } : {}), + ...(zstd ? { compression: "zstd" } : {}) }); - socket.emit(ClientEvents.AUTHENTICATE, { token }); }; -export const onDisconnect = () => { +export const onDisconnect = ( + reason: string, + details: DisconnectDescription | undefined +) => { + log("WebSocket", "Disconnected.", reason, details); + const account = useAccount(); const voiceUsers = useVoiceUsers(); const channelProperties = useChannelProperties(); account.setSocketDetails({ socketId: null, socketConnected: false, - socketAuthenticated: false, + socketAuthenticated: false }); channelProperties.staleAll(); voiceUsers.resetAll(); @@ -44,7 +66,7 @@ export const onAuthenticateError = (error: { message: string; data: any }) => { socketId: null, socketConnected: false, socketAuthenticated: false, - authenticationError: error, + authenticationError: error }); }; @@ -53,15 +75,16 @@ export const onReconnectAttempt = () => { account.setSocketDetails({ socketId: null, socketConnected: false, - socketAuthenticated: false, + socketAuthenticated: false }); }; electronWindowAPI()?.activityStatusChanged((window) => { + const id = "electron-activity"; if (!window) { - return emitActivityStatus(null); + return localRPC.updateRPC(id); } - const programs = getStorageObject( + const programs = getStorageObject( StorageKeys.PROGRAM_ACTIVITY_STATUS, [] ); @@ -70,36 +93,37 @@ electronWindowAPI()?.activityStatusChanged((window) => { ); if (!program) { - return emitActivityStatus(null); + localRPC.updateRPC(id); } - emitActivityStatus({ - action: program.action || "Playing", - name: program.name, - startedAt: window.createdAt, + localRPC.updateRPC(id, { + action: program?.action || "Playing", + name: program?.name || "", + startedAt: window?.createdAt, + emoji: program?.emoji }); }); electronWindowAPI()?.rpcChanged((data) => { - if (!data) { - const programs = getStorageObject( - StorageKeys.PROGRAM_ACTIVITY_STATUS, - [] - ); - electronWindowAPI()?.restartActivityStatus(programs); - return; - } - emitActivityStatus({ startedAt: Date.now(), ...data }); + localRPC.updateElectronRPCs(data); }); localRPC.onUpdateRPC = (data) => { - if (!data) { - emitActivityStatus(null); - } - emitActivityStatus({ startedAt: Date.now(), ...data }); + emitActivityStatus(data.map((data) => ({ startedAt: Date.now(), ...data }))); }; export const onAuthenticated = (payload: AuthenticatedPayload) => { + if (payload instanceof ArrayBuffer) { + const t = performance.now(); + payload = decompressObject(new Uint8Array(payload)); + log("WebSocket", "Decompression took", performance.now() - t, "ms"); + } + socketClient.setSessionId(payload.sessionId); + if (payload.newToken) { + setStorageString(StorageKeys.USER_TOKEN, payload.newToken); + socketClient.updateToken(payload.newToken); + log("WebSocket", "Updated token."); + } const { account, servers, @@ -111,9 +135,9 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => { mentions, serverRoles, voiceUsers, - tickets, + tickets } = useStore(); - console.log("[WS] Authenticated."); + log("WebSocket", " Authenticated."); reactNativeAPI()?.authenticated(payload.user.id); @@ -132,7 +156,7 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => { account.setSocketDetails({ socketConnected: true, socketAuthenticated: true, - lastAuthenticatedAt: Date.now(), + lastAuthenticatedAt: Date.now() }); users.set(payload.user); tickets.fetchUpdated(); @@ -160,7 +184,7 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => { for (let i = 0; i < payload.inbox.length; i++) { const item = payload.inbox[i]; if (item.lastSeen) { - channels.get(item.channelId)!.updateLastSeen(item.lastSeen); + channels.get(item.channelId)?.updateLastSeen(item.lastSeen); } inbox.set(item); } @@ -185,7 +209,7 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => { for (const channelId in payload.lastSeenServerChannelIds) { const timestamp = payload.lastSeenServerChannelIds[channelId]; - channels.get(channelId)!.updateLastSeen(timestamp); + channels.get(channelId)?.updateLastSeen(timestamp); } for (let i = 0; i < payload.messageMentions.length; i++) { @@ -205,25 +229,45 @@ export const onAuthenticated = (payload: AuthenticatedPayload) => { channelId: mention.channelId, userId: mention.mentionedById, count: mention.count, - serverId: mention.serverId, + serverId: mention.serverId }); } + const previousVoiceUserChannelId = voiceUsers.currentUser()?.channelId; for (let i = 0; i < payload.voiceChannelUsers.length; i++) { - const voiceChannelUser = payload.voiceChannelUsers[i]; - voiceUsers.set(voiceChannelUser); + const voiceChannelUser = payload.voiceChannelUsers[i]!; + voiceUsers.createVoiceUser( + voiceChannelUser, + !!previousVoiceUserChannelId + ); } }); const t1 = performance.now(); - console.log(`${t1 - t0} milliseconds.`); + log("WebSocket", `${t1 - t0} milliseconds.`); - const programs = getStorageObject( + const programs = getStorageObject( StorageKeys.PROGRAM_ACTIVITY_STATUS, [] ); + localRPC.start(); electronWindowAPI()?.restartActivityStatus(programs); electronWindowAPI()?.restartRPCServer(); - localRPC.start(); + useDiscordActivityTracker().restart(); + useLastFmActivityTracker().restart(); + + const [collapsedCategories, setCollapsedServerCategories] = + useCollapsedServerCategories(); + const newCollapsedCategories = [...collapsedCategories()].filter((id) => { + const exists = channels.get(id); + return exists; + }); + setCollapsedServerCategories(newCollapsedCategories); + + const previousVoiceUserChannelId = voiceUsers.currentUser()?.channelId; + + if (previousVoiceUserChannelId) { + channels.get(previousVoiceUserChannelId)?.joinCall(true); + } }; diff --git a/src/chat-api/events/friendEvents.ts b/src/chat-api/events/friendEvents.ts index 251d05637..bf7bfe14b 100644 --- a/src/chat-api/events/friendEvents.ts +++ b/src/chat-api/events/friendEvents.ts @@ -1,7 +1,6 @@ import { FriendStatus, RawFriend } from "../RawData"; import useFriends from "../store/useFriends"; - const friends = useFriends(); export const onFriendRequestSent = (payload: RawFriend) => { @@ -12,10 +11,10 @@ export const onFriendRequestPending = (payload: RawFriend) => { friends.set(payload); }; -export const onFriendRequestAccepted = (payload: {friendId: string}) => { +export const onFriendRequestAccepted = (payload: { friendId: string }) => { friends.updateStatus(payload.friendId, FriendStatus.FRIENDS); }; -export const onFriendRemoved = (payload: {friendId: string}) => { +export const onFriendRemoved = (payload: { friendId: string }) => { friends.delete(payload.friendId); -}; \ No newline at end of file +}; diff --git a/src/chat-api/events/inboxEvents.ts b/src/chat-api/events/inboxEvents.ts index d00c576c7..e385d5d1c 100644 --- a/src/chat-api/events/inboxEvents.ts +++ b/src/chat-api/events/inboxEvents.ts @@ -3,16 +3,17 @@ import { RawChannel, RawInboxWithoutChannel } from "../RawData"; import useChannels from "../store/useChannels"; import useInbox from "../store/useInbox"; - -export const onInboxOpened = (payload: RawInboxWithoutChannel & {channel: RawChannel}) => { +export const onInboxOpened = ( + payload: RawInboxWithoutChannel & { channel: RawChannel } +) => { const channels = useChannels(); const inbox = useInbox(); batch(() => { - channels.set({...payload.channel, lastSeen: payload.lastSeen}); - inbox.set({...payload, channelId: payload.channel.id}); + channels.set({ ...payload.channel, lastSeen: payload.lastSeen }); + inbox.set({ ...payload, channelId: payload.channel.id }); }); }; -export const onInboxClosed = (payload: {channelId: string}) => { +export const onInboxClosed = (payload: { channelId: string }) => { const channels = useChannels(); const inbox = useInbox(); const channel = channels.get(payload.channelId); @@ -20,6 +21,5 @@ export const onInboxClosed = (payload: {channelId: string}) => { inbox.removeInbox(payload.channelId); channel?.recipient()?.setInboxChannelId(undefined); channels.deleteChannel(payload.channelId); - }); -}; \ No newline at end of file +}; diff --git a/src/chat-api/events/messageEvents.ts b/src/chat-api/events/messageEvents.ts index 4ce3c1365..c27ef430c 100644 --- a/src/chat-api/events/messageEvents.ts +++ b/src/chat-api/events/messageEvents.ts @@ -1,82 +1,150 @@ import { playMessageNotification } from "@/common/Sound"; import { useWindowProperties } from "@/common/useWindowProperties"; import { batch } from "solid-js"; -import { FriendStatus, RawMessage } from "../RawData"; +import { FriendStatus, MessageType, RawMessage } from "../RawData"; import useAccount from "../store/useAccount"; import useChannels from "../store/useChannels"; import useHeader from "../store/useHeader"; import useMention from "../store/useMention"; -import useMessages, { MessageSentStatus } from "../store/useMessages"; +import useMessages from "../store/useMessages"; import useUsers from "../store/useUsers"; import socketClient from "../socketClient"; import { createDesktopNotification } from "@/common/desktopNotification"; import useServerMembers from "../store/useServerMembers"; -import { ROLE_PERMISSIONS } from "../Bitwise"; +import { hasBit, ROLE_PERMISSIONS } from "../Bitwise"; import useFriends from "../store/useFriends"; +import { pushMessageNotification } from "@/components/in-app-notification-previews/useInAppNotificationPreviews"; +import useServers from "../store/useServers"; +export function onMessageCreated(payload: { + socketId: string; + serverId?: string; + member?: { + id: string; + nickname?: string; + permissions: number; + }; + message: RawMessage; +}) { + const channels = useChannels(); + const channel = channels.get(payload.message.channelId); - -export function onMessageCreated(payload: {socketId: string, message: RawMessage}) { + batch(() => { + if (payload.message.attachments?.length) { + const attachmentCreatedCount = payload.message.attachments.length; + const channelAttachments = channel?._count?.attachments; + const attachmentCount = channelAttachments || 0; + channel?.update({ + _count: { + attachments: attachmentCount + attachmentCreatedCount + } + }); + } + }); if (socketClient.id() === payload.socketId) return; const header = useHeader(); const messages = useMessages(); - const channels = useChannels(); const mentions = useMention(); const members = useServerMembers(); const users = useUsers(); const account = useAccount(); - const channel = channels.get(payload.message.channelId); const friends = useFriends(); - const {hasFocus} = useWindowProperties(); + const servers = useServers(); + const { hasFocus } = useWindowProperties(); const accountUser = account.user(); - const hasBlockedRecipient = friends.get(payload.message.createdBy.id)?.status === FriendStatus.BLOCKED; + const hasBlockedRecipient = + friends.get(payload.message.createdBy.id)?.status === FriendStatus.BLOCKED; - batch(() => { + const server = servers.get(payload.serverId!); + const member = payload.member + ? { + isServerCreator: () => { + return server?.createdById === payload.message.createdBy.id; + }, + hasPermission: (bitwise: { bit: number }) => { + const member = payload.member!; + const message = payload.message; + const isCreator = server?.createdById === message.createdBy.id; + if (isCreator) return true; + + const isAdmin = hasBit( + member.permissions || 0, + ROLE_PERMISSIONS.ADMIN.bit + ); + + if (isAdmin) return true; + return hasBit(member.permissions || 0, bitwise.bit); + } + } + : null; + + const selfMember = members.get(channel?.serverId!, accountUser?.id!); + const isSystemMessage = payload.message.type !== MessageType.CONTENT; + const isSelf = + !isSystemMessage && accountUser?.id === payload.message.createdBy.id; + + batch(() => { channel?.updateLastMessaged(payload.message.createdAt); - if (accountUser?.id === payload.message.createdBy.id) { + if (!isSystemMessage && isSelf) { channel?.updateLastSeen(payload.message.createdAt + 1); - } - else if (!channel || channel.recipient()) { + } else if (!channel || channel.recipient()) { const user = users.get(payload.message.createdBy.id); if (!user) { users.set(payload.message.createdBy); } - } - const mentionCount = () => mentions.get(payload.message.channelId)?.count || 0; + const mentionCount = () => + mentions.get(payload.message.channelId)?.count || 0; const isMentioned = () => { if (hasBlockedRecipient) return false; const everyoneMentioned = payload.message.content?.includes("[@:e]"); - if (everyoneMentioned && channel?.serverId) { - const member = members.get(channel.serverId, payload.message.createdBy.id); - const hasPerm = member?.isServerCreator() || member?.hasPermission(ROLE_PERMISSIONS.MENTION_EVERYONE); + if (everyoneMentioned && payload.serverId) { + const hasPerm = + member?.isServerCreator() || + member?.hasPermission(ROLE_PERMISSIONS.MENTION_EVERYONE); if (hasPerm) return true; } - const mention = payload.message.mentions?.find(u => u.id === accountUser?.id); + const mention = payload.message.mentions?.find( + (u) => u.id === accountUser?.id + ); if (mention) return true; - const quoteMention = payload.message.quotedMessages?.find(m => m.createdBy?.id === accountUser?.id); + const quoteMention = payload.message.quotedMessages?.find( + (m) => m.createdBy?.id === accountUser?.id + ); if (quoteMention) return true; - - const replyMention = payload.message.mentionReplies && payload.message.replyMessages.find(m => m.replyToMessage?.createdBy?.id === accountUser?.id); + + const isRoleMentioned = + member?.hasPermission(ROLE_PERMISSIONS.MENTION_ROLES) && + payload.message.roleMentions.find( + (r) => + server?.defaultRoleId !== r.id && members.hasRole(selfMember!, r.id) + ); + + if (isRoleMentioned) return true; + + const replyMention = + payload.message.mentionReplies && + payload.message.replyMessages.find( + (m) => m.replyToMessage?.createdBy?.id === accountUser?.id + ); return replyMention; }; - - if (payload.message.createdBy.id !== accountUser?.id) { - - if (!channel?.serverId || isMentioned()) { + + if (!isSelf) { + if (!payload.serverId || isMentioned()) { mentions.set({ channelId: payload.message.channelId, userId: payload.message.createdBy.id, count: mentionCount() + 1, - serverId: channel?.serverId + serverId: payload.serverId }); } } @@ -84,48 +152,99 @@ export function onMessageCreated(payload: {socketId: string, message: RawMessage messages.pushMessage(payload.message.channelId, payload.message); }); - // only play notifications if: + // only play notifications if: // it does not have focus (has focus) // channel is not selected (is selected) - if (payload.message.createdBy.id !== accountUser?.id) { + if (!isSelf) { if (hasBlockedRecipient) return; - const isChannelSelected = header.details().id === "MessagePane" && header.details().channelId === payload.message.channelId; + const isChannelSelected = + header.details().id === "MessagePane" && + header.details().channelId === payload.message.channelId; if (hasFocus() && isChannelSelected) return; - playMessageNotification({message: payload.message, serverId: channel?.serverId}); + playMessageNotification({ + message: payload.message, + serverId: channel?.serverId + }); createDesktopNotification(payload.message); + pushMessageNotification(payload.message); } - } -export function onMessageUpdated(payload: {channelId: string, messageId: string, updated: Partial}) { +export function onMessageUpdated(payload: { + channelId: string; + messageId: string; + updated: Partial; +}) { const messages = useMessages(); - messages.updateLocalMessage({...payload.updated, sentStatus: undefined}, payload.channelId, payload.messageId); + messages.updateLocalMessage( + { ...payload.updated, sentStatus: undefined }, + payload.channelId, + payload.messageId + ); } - -export function onMessageDeleted(payload: {channelId: string, messageId: string}) { +export function onMessageDeleted(payload: { + channelId: string; + messageId: string; + deletedAttachmentCount: number; +}) { const messages = useMessages(); messages.locallyRemoveMessage(payload.channelId, payload.messageId); + + if (payload.deletedAttachmentCount) { + const channels = useChannels(); + const channel = channels.get(payload.channelId); + const attachmentDeletedCount = payload.deletedAttachmentCount; + const channelAttachments = channel?._count?.attachments; + const attachmentCount = channelAttachments || attachmentDeletedCount; + channel?.update({ + _count: { + attachments: attachmentCount - attachmentDeletedCount + } + }); + } +} + +export function onMessageMarkUnread(payload: { + channelId: string; + at: number; +}) { + const channels = useChannels(); + const mentions = useMention(); + + const channel = channels.get(payload.channelId); + if (!channel) return; + channel?.updateLastSeen(payload.at); + + if (!channel.serverId) { + mentions.set({ + channelId: channel.id, + userId: channel.recipientId!, + count: 1, + serverId: channel.serverId + }); + } } export function onMessageDeletedBatch(payload: { - userId: string - serverId: string - fromTime: number, - toTime: number, + userId: string; + serverId: string; + fromTime: number; + toTime: number; }) { const messages = useMessages(); messages.locallyRemoveServerMessagesBatch(payload); } interface ReactionAddedPayload { - messageId: string, - channelId: string, - count: number - reactedByUserId: string, - emojiId?: string, - name: string, - gif?: boolean, + messageId: string; + channelId: string; + count: number; + reactedByUserId: string; + emojiId?: string; + name: string; + gif?: boolean; + webp?: boolean; } export function onMessageReactionAdded(payload: ReactionAddedPayload) { @@ -138,26 +257,28 @@ export function onMessageReactionAdded(payload: ReactionAddedPayload) { name: payload.name, emojiId: payload.emojiId || null, gif: payload.gif, - ...(reactedByMe? { reacted: true } : undefined) + webp: payload.webp, + ...(reactedByMe ? { reacted: true } : undefined) }); } interface ReactionRemovedPayload { - messageId: string, - channelId: string, - count: number - reactionRemovedByUserId: string, - emojiId?: string, - name: string, + messageId: string; + channelId: string; + count: number; + reactionRemovedByUserId: string; + emojiId?: string; + name: string; } export function onMessageReactionRemoved(payload: ReactionRemovedPayload) { const messages = useMessages(); const account = useAccount(); - const reactionRemovedByMe = account.user()?.id === payload.reactionRemovedByUserId; + const reactionRemovedByMe = + account.user()?.id === payload.reactionRemovedByUserId; messages.updateMessageReaction(payload.channelId, payload.messageId, { count: payload.count, name: payload.name, emojiId: payload.emojiId, ...(reactionRemovedByMe ? { reacted: false } : undefined) }); -} \ No newline at end of file +} diff --git a/src/chat-api/events/serverEvents.ts b/src/chat-api/events/serverEvents.ts index 50db99d7c..25ff6f6ef 100644 --- a/src/chat-api/events/serverEvents.ts +++ b/src/chat-api/events/serverEvents.ts @@ -1,23 +1,23 @@ import { runWithContext } from "@/common/runWithContext"; -import { batch, from } from "solid-js"; +import { batch } from "solid-js"; import { - ChannelType, RawChannel, RawCustomEmoji, RawPresence, RawServer, + RawServerClan, + RawServerFolder, RawServerMember, RawServerRole, - RawVoice, + RawVoice } from "../RawData"; import useAccount from "../store/useAccount"; -import useChannels, { Channel } from "../store/useChannels"; +import useChannels from "../store/useChannels"; import useServerMembers from "../store/useServerMembers"; import useServerRoles from "../store/useServerRoles"; import useServers from "../store/useServers"; import useUsers from "../store/useUsers"; -import { CHANNEL_PERMISSIONS, addBit, hasBit } from "../Bitwise"; -import { useParams } from "solid-navigator"; + import useVoiceUsers from "../store/useVoiceUsers"; import useChannelProperties from "../store/useChannelProperties"; @@ -60,8 +60,8 @@ export const onServerJoined = (payload: ServerJoinedPayload) => { users.setPresence(presence.userId, presence); } for (let i = 0; i < payload.voiceChannelUsers.length; i++) { - const rawVoice = payload.voiceChannelUsers[i]; - voiceUsers.set(rawVoice); + const rawVoice = payload.voiceChannelUsers[i]!; + voiceUsers.createVoiceUser(rawVoice); } }); }; @@ -75,7 +75,7 @@ export const onServerLeft = (payload: { serverId: string }) => const roles = useServerRoles(); const voiceUsers = useVoiceUsers(); - const currentVoiceChannelId = voiceUsers.currentVoiceChannelId(); + const currentVoiceChannelId = voiceUsers.currentUser()?.channelId; const serverChannels = channels.getChannelsByServerId(payload.serverId); @@ -89,9 +89,9 @@ export const onServerLeft = (payload: { serverId: string }) => for (let i = 0; i < serverChannels.length; i++) { const channel = serverChannels[i]!; account.removeNotificationSettings(channel.id); - } - if (currentVoiceChannelId) { - voiceUsers.setCurrentVoiceChannelId(null); + if (currentVoiceChannelId === channel.id) { + voiceUsers.setCurrentChannelId(null); + } } }); }); @@ -132,7 +132,9 @@ interface ServerMemberUpdated { export const onServerMemberUpdated = (payload: ServerMemberUpdated) => { const serverMembers = useServerMembers(); const member = serverMembers.get(payload.serverId, payload.userId); - member?.update(payload.updated); + if (!member) return; + + serverMembers.update(member, payload.updated); }; export const onServerEmojiAdd = (payload: { @@ -142,7 +144,7 @@ export const onServerEmojiAdd = (payload: { const servers = useServers(); const server = servers.get(payload.serverId); server?.update({ - customEmojis: [...server.customEmojis, payload.emoji], + customEmojis: [...server.customEmojis, payload.emoji] }); }; @@ -156,7 +158,7 @@ export const onServerEmojiUpdate = (payload: { server?.update({ customEmojis: server.customEmojis.map((e) => e.id !== payload.emojiId ? e : { ...e, name: payload.name } - ), + ) }); }; @@ -167,10 +169,24 @@ export const onServerEmojiRemove = (payload: { const servers = useServers(); const server = servers.get(payload.serverId); server?.update({ - customEmojis: server.customEmojis.filter((e) => e.id !== payload.emojiId), + customEmojis: server.customEmojis.filter((e) => e.id !== payload.emojiId) }); }; +export const onServerScheduleDelete = (payload: { + serverId: string; + scheduleAt: number; +}) => { + const servers = useServers(); + const server = servers.get(payload.serverId); + server?.update({ scheduledForDeletion: { scheduledAt: payload.scheduleAt } }); +}; +export const onServerRemoveScheduleDelete = (payload: { serverId: string }) => { + const servers = useServers(); + const server = servers.get(payload.serverId); + server?.update({ scheduledForDeletion: undefined }); +}; + export const onServerUpdated = (payload: ServerUpdated) => { const servers = useServers(); const server = servers.get(payload.serverId); @@ -179,7 +195,35 @@ export const onServerUpdated = (payload: ServerUpdated) => { export const onServerOrderUpdated = (payload: { serverIds: string[] }) => { const account = useAccount(); account.setUser({ - orderedServerIds: payload.serverIds, + orderedServerIds: payload.serverIds + }); +}; + +export const onServerFolderCreated = (payload: { folder: RawServerFolder }) => { + const account = useAccount(); + + const existingFolders = account.user()?.serverFolders || []; + + account.setUser({ + serverFolders: [...existingFolders, payload.folder] + }); +}; +export const onServerFolderUpdated = (payload: { folder: RawServerFolder }) => { + const account = useAccount(); + + const existingFolders = account.user()?.serverFolders || []; + + if (payload.folder.serverIds.length === 0) { + account.setUser({ + serverFolders: existingFolders.filter((f) => f.id !== payload.folder.id) + }); + return; + } + + account.setUser({ + serverFolders: existingFolders.map((f) => + f.id === payload.folder.id ? { ...f, ...payload.folder } : f + ) }); }; @@ -208,30 +252,6 @@ export const onServerChannelUpdated = (payload: ServerChannelUpdated) => { const channelProperties = useChannelProperties(); const channel = channels.get(payload.channelId); - const isCategoryChannel = channel?.type === ChannelType.CATEGORY; - const isPrivateCategory = hasBit( - payload.updated.permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit - ); - - if (isCategoryChannel && isPrivateCategory) { - const serverChannels = channels.getChannelsByServerId(payload.serverId); - - batch(() => { - for (let i = 0; i < serverChannels.length; i++) { - const channel = serverChannels[i]; - if (channel?.categoryId !== payload.channelId) continue; - channel?.update({ - permissions: addBit( - channel.permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit - ), - }); - } - }); - } - - console.log(payload.updated.slowModeSeconds); if ( payload.updated.slowModeSeconds || payload.updated.slowModeSeconds === null @@ -289,19 +309,65 @@ export const onServerRoleDeleted = (payload: { }) => { const serverRoles = useServerRoles(); const serverMembers = useServerMembers(); + const channels = useChannels(); const members = serverMembers.array(payload.serverId); + const serverChannels = channels.getChannelsByServerId( + payload.serverId, + false + ); + batch(() => { for (let i = 0; i < members.length; i++) { const member = members[i]; if (!member?.roleIds.includes(payload.roleId)) continue; - member.update({ - roleIds: member.roleIds.filter((ids) => ids !== payload.roleId), + serverMembers.update(member, { + roleIds: member.roleIds.filter((ids) => ids !== payload.roleId) }); } + + for (let i = 0; i < serverChannels.length; i++) { + const channel = serverChannels[i]!; + const channelWithoutRole = channel.permissions?.filter( + (p) => p.roleId !== payload.roleId + ); + if (channelWithoutRole?.length !== channel.permissions?.length) { + channel.update({ + permissions: channelWithoutRole + }); + } + } + serverRoles.deleteRole(payload.serverId, payload.roleId); }); }; +interface ServerChannelPermissionsUpdated { + permissions: number; + roleId: string; + serverId: string; + channelId: string; +} + +export const onServerChannelPermissionsUpdated = ( + payload: ServerChannelPermissionsUpdated +) => { + const channels = useChannels(); + const channel = channels.get(payload.channelId); + if (!channel) return; + const permissions = [...(channel.permissions || [])]; + const roleChannelIndex = permissions.findIndex( + (p) => p.roleId === payload.roleId + ); + if (roleChannelIndex === -1) { + permissions.push(payload); + channel.update({ permissions }); + return; + } + + permissions[roleChannelIndex]! = payload; + channel.update({ permissions }); +}; + interface ServerChannelOrderUpdatedPayload { serverId: string; categoryId?: string; @@ -316,13 +382,6 @@ export const onServerChannelOrderUpdated = ( payload.serverId ); - const categoryChannel = () => channels.get(payload.categoryId!)!; - const isPrivateCategory = () => - hasBit( - categoryChannel().permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit - ); - batch(() => { for (let i = 0; i < orderedChannels.length; i++) { const channel = orderedChannels[i]; @@ -337,7 +396,7 @@ export const onServerChannelOrderUpdated = ( payload.categoryId !== channel.categoryId && payload.orderedChannelIds.includes(channel.id) ? { - categoryId: payload.categoryId, + categoryId: payload.categoryId } : undefined; @@ -346,28 +405,24 @@ export const onServerChannelOrderUpdated = ( channel.categoryId && payload.orderedChannelIds.includes(channel.id) ? { - categoryId: undefined, - } - : undefined; - - const updatePermissions = - payload.orderedChannelIds.includes(channel.id) && - payload.categoryId && - isPrivateCategory() - ? { - permissions: addBit( - channel.permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit - ), + categoryId: undefined } : undefined; channel?.update({ - ...updatePermissions, ...updateOrder, ...updateOrAddCategoryId, - ...removeCategoryId, + ...removeCategoryId }); } }); }; + +export const onServerClanUpdated = (payload: { + clan: RawServerClan; + serverId: string; +}) => { + const servers = useServers(); + const server = servers.get(payload.serverId); + server?.update({ clan: payload.clan }); +}; diff --git a/src/chat-api/events/userEvents.ts b/src/chat-api/events/userEvents.ts index 270293089..d7e1d4352 100644 --- a/src/chat-api/events/userEvents.ts +++ b/src/chat-api/events/userEvents.ts @@ -11,37 +11,20 @@ import { RawUser, RawUserConnection, RawNotice, + RawReminder } from "../RawData"; import useFriends from "../store/useFriends"; import useAccount from "../store/useAccount"; -import { StorageKeys, getStorageObject } from "@/common/localStorage"; -import { ProgramWithAction, electronWindowAPI } from "@/common/Electron"; -import { isExperimentEnabled } from "@/common/experiments"; -import { userInfo } from "os"; export function onUserPresenceUpdate(payload: { userId: string; status?: UserStatus; custom?: string; - activity?: ActivityStatus; + activities?: ActivityStatus[]; }) { const users = useUsers(); const account = useAccount(); - if (payload.status !== undefined && account.user()?.id === payload.userId) { - const user = users.get(payload.userId); - const wasOffline = - !user?.presence()?.status && payload.status !== UserStatus.OFFLINE; - if (wasOffline) { - const programs = getStorageObject( - StorageKeys.PROGRAM_ACTIVITY_STATUS, - [] - ); - electronWindowAPI()?.restartActivityStatus(programs); - electronWindowAPI()?.restartRPCServer(); - } - } - if (payload.status === UserStatus.OFFLINE) { users.updateLastOnlineAt(payload.userId); } @@ -49,9 +32,9 @@ export function onUserPresenceUpdate(payload: { users.setPresence(payload.userId, { ...(payload.status !== undefined ? { status: payload.status } : undefined), ...(payload.custom !== undefined ? { custom: payload.custom } : undefined), - ...(payload.activity !== undefined - ? { activity: payload.activity } - : undefined), + ...(payload.activities !== undefined + ? { activities: payload.activities } + : undefined) }); } @@ -66,7 +49,14 @@ export function onNotificationDismissed(payload: { channelId: string }) { } export function onUserUpdatedSelf(payload: Partial) { - const { account, users } = useStore(); + const { account, users, servers } = useStore(); + + const clanServerId = (payload.profile as any)?.clanServerId; + if (clanServerId) { + if (!payload.profile) payload.profile = {}; + payload.profile.clan = servers.get(clanServerId)?.clan; + } + account.setUser(payload); const user = users.get(account.user()?.id!); @@ -108,7 +98,7 @@ export function onUserBlocked(payload: { user: RawUser }) { createdAt: Date.now(), recipient: payload.user, userId: account.user()?.id!, - status: FriendStatus.BLOCKED, + status: FriendStatus.BLOCKED }); } export function onUserUnblocked(payload: { userId: string }) { @@ -121,7 +111,7 @@ export function onUserConnectionAdded(payload: { }) { const account = useAccount(); account.setUser({ - connections: [...(account.user()?.connections || []), payload.connection], + connections: [...(account.user()?.connections || []), payload.connection] }); } @@ -130,7 +120,7 @@ export function onUserConnectionRemoved(payload: { connectionId: string }) { account.setUser({ connections: account .user() - ?.connections.filter((c) => c.id !== payload.connectionId), + ?.connections.filter((c) => c.id !== payload.connectionId) }); } @@ -140,3 +130,16 @@ export function onUserNoticeUpdated(payload: RawNotice) { notices.push(payload); account.setUser({ notices }); } + +export function onUserReminderAdd(payload: RawReminder) { + const account = useAccount(); + account.addReminder(payload); +} +export function onUserReminderUpdate(payload: RawReminder) { + const account = useAccount(); + account.updateReminder(payload); +} +export function onUserReminderRemove(payload: { id: string }) { + const account = useAccount(); + account.removeReminder(payload.id); +} diff --git a/src/chat-api/events/voiceEvents.ts b/src/chat-api/events/voiceEvents.ts index 91697f433..76036f76a 100644 --- a/src/chat-api/events/voiceEvents.ts +++ b/src/chat-api/events/voiceEvents.ts @@ -2,21 +2,33 @@ import type SimplePeer from "simple-peer"; import { RawVoice } from "../RawData"; import useAccount from "../store/useAccount"; import useVoiceUsers from "../store/useVoiceUsers"; +import { getCustomSound, playSound } from "@/common/Sound"; export function onVoiceUserJoined(payload: RawVoice) { - const {set} = useVoiceUsers(); + const { createVoiceUser, currentUser } = useVoiceUsers(); - set(payload); + if (currentUser()?.channelId === payload.channelId) { + playSound(getCustomSound("CALL_JOIN")); + } + + createVoiceUser(payload); } -export function onVoiceUserLeft(payload: {userId: string, channelId: string}) { - const {removeUserInVoice, setCurrentVoiceChannelId} = useVoiceUsers(); - const {user} = useAccount(); +export function onVoiceUserLeft(payload: { + userId: string; + channelId: string; +}) { + const { removeVoiceUser, setCurrentChannelId, currentUser } = useVoiceUsers(); + const { user } = useAccount(); + + if (currentUser()?.channelId === payload.channelId) { + playSound(getCustomSound("CALL_LEAVE")); + } if (user()?.id === payload.userId) { - setCurrentVoiceChannelId(null); + setCurrentChannelId(null); } - removeUserInVoice(payload.channelId, payload.userId); + removeVoiceUser(payload.channelId, payload.userId); } interface VoiceSignalReceivedPayload { @@ -27,12 +39,16 @@ interface VoiceSignalReceivedPayload { export function onVoiceSignalReceived(payload: VoiceSignalReceivedPayload) { const voiceUsers = useVoiceUsers(); - const voiceUser = voiceUsers.getVoiceUser(payload.channelId, payload.fromUserId); + + const voiceUser = voiceUsers.getVoiceUser( + payload.channelId, + payload.fromUserId + ); if (!voiceUser) return; if (!voiceUser.peer) { - return voiceUser.addPeer(payload.signal); + return voiceUsers.createPeer(voiceUser, payload.signal); } - voiceUser.addSignal(payload.signal); -} \ No newline at end of file + voiceUsers.signal(voiceUser, payload.signal); +} diff --git a/src/chat-api/services/ApplicationService.ts b/src/chat-api/services/ApplicationService.ts index c267506f9..cf675ac72 100644 --- a/src/chat-api/services/ApplicationService.ts +++ b/src/chat-api/services/ApplicationService.ts @@ -37,7 +37,10 @@ export const createAppBotUser = async (appId: string) => { }); return data; }; -export const updateAppBotUser = async (appId: string, update: {username?: string, tag?: string}) => { +export const updateAppBotUser = async ( + appId: string, + update: { username?: string; tag?: string } +) => { const data = await request({ method: "PATCH", url: env.SERVER_URL + `/api/applications/${appId}/bot`, @@ -46,7 +49,10 @@ export const updateAppBotUser = async (appId: string, update: {username?: string }); return data; }; -export const updateApp = async (appId: string, update: {name?: string}) => { +export const updateApp = async ( + appId: string, + update: { name?: string; redirectUris?: string[] } +) => { const data = await request>({ method: "PATCH", url: env.SERVER_URL + `/api/applications/${appId}`, @@ -57,7 +63,7 @@ export const updateApp = async (appId: string, update: {name?: string}) => { }; export const getAppBotToken = async (appId: string) => { - const data = await request<{token: string}>({ + const data = await request<{ token: string }>({ method: "GET", url: env.SERVER_URL + `/api/applications/${appId}/token`, useToken: true @@ -65,7 +71,7 @@ export const getAppBotToken = async (appId: string) => { return data; }; export const refreshAppBotToken = async (appId: string) => { - const data = await request<{status: true}>({ + const data = await request<{ status: true }>({ method: "POST", url: env.SERVER_URL + `/api/applications/${appId}/token`, useToken: true @@ -73,7 +79,7 @@ export const refreshAppBotToken = async (appId: string) => { return data; }; export const deleteApp = async (appId: string) => { - const data = await request<{success: string}>({ + const data = await request<{ success: string }>({ method: "DELETE", url: env.SERVER_URL + `/api/applications/${appId}`, useToken: true @@ -81,17 +87,31 @@ export const deleteApp = async (appId: string) => { return data; }; +export type RawBotUser = RawUser & { + application: { creatorAccount: { user: RawUser } }; +}; - -export type RawBotUser = RawUser & {application: {creatorAccount: {user: RawUser}}}; - - -export const getApplicationBot = async (appId: string, includeServers: boolean) => { - const data = await request<{bot: RawBotUser, servers: {id: string, name: string}[]}>({ +export const getApplicationBot = async ( + appId: string, + includeServers: boolean +) => { + const data = await request<{ + bot: RawBotUser; + servers: { id: string; name: string }[]; + }>({ method: "GET", url: env.SERVER_URL + `/api/applications/${appId}/bot`, params: { includeServers }, useToken: true }); return data; -}; \ No newline at end of file +}; + +export const refreshAppClientSecret = async (appId: string) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + `/api/applications/${appId}/client-secret-refresh`, + useToken: true + }); + return data; +}; diff --git a/src/chat-api/services/ChannelService.ts b/src/chat-api/services/ChannelService.ts index 1065e5058..29f6a87bd 100644 --- a/src/chat-api/services/ChannelService.ts +++ b/src/chat-api/services/ChannelService.ts @@ -4,28 +4,44 @@ import { RawChannelNotice } from "../RawData"; import env from "@/common/env"; export const getChannelNotice = async (channelId: string) => { - const data = await request<{notice: RawChannelNotice}>({ + const data = await request<{ notice: RawChannelNotice }>({ method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.channel(channelId) + "/notice", + url: + env.SERVER_URL + "/api" + ServiceEndpoints.channel(channelId) + "/notice", useToken: true }); return data; }; -export const updateChannelNotice = async (serverId: string, channelId: string, content: string) => { - const data = await request<{notice: RawChannelNotice}>({ +export const updateChannelNotice = async ( + serverId: string, + channelId: string, + content: string +) => { + const data = await request<{ notice: RawChannelNotice }>({ method: "PUT", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannel(serverId, channelId) + "/notice", - body: {content}, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + "/notice", + body: { content }, useToken: true }); return data; }; -export const deleteChannelNotice = async (serverId: string, channelId: string) => { +export const deleteChannelNotice = async ( + serverId: string, + channelId: string +) => { const data = await request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannel(serverId, channelId) + "/notice", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + "/notice", useToken: true }); return data; diff --git a/src/chat-api/services/ExploreService.ts b/src/chat-api/services/ExploreService.ts new file mode 100644 index 000000000..0d0aecc46 --- /dev/null +++ b/src/chat-api/services/ExploreService.ts @@ -0,0 +1,59 @@ +import env from "@/common/env"; +import { RawExploreItem } from "../RawData"; +import { request } from "./Request"; +import ServiceEndpoints from "./ServiceEndpoints"; + +export async function BumpExploreItem(id: string, token: string) { + return request({ + method: "POST", + body: { token }, + url: env.SERVER_URL + "/api" + ServiceEndpoints.explore(id) + "/bump", + useToken: true + }); +} + +export async function upsertExploreBotApp( + botAppId: string, + description: string, + permissions: number +) { + return request({ + method: "POST", + url: env.SERVER_URL + "/api" + ServiceEndpoints.explore("bots/" + botAppId), + body: { description, permissions }, + useToken: true + }); +} +export async function getExploreBotApp(botAppId: string) { + return request({ + method: "GET", + url: env.SERVER_URL + "/api" + ServiceEndpoints.explore("bots/" + botAppId), + useToken: true + }); +} + +export type PublicServerSort = + | "pinned_at" + | "most_bumps" + | "most_members" + | "recently_added" + | "recently_bumped" + | "most_active"; +export type PublicServerFilter = "pinned" | "all" | "verified"; + +interface getExploreItemsOpts { + sort: PublicServerSort; + filter: PublicServerFilter; + limit?: number; + afterId?: string; + search?: string; + type?: "bot" | "server"; +} +export async function getExploreItems(opts: getExploreItemsOpts) { + return request({ + params: opts, + method: "GET", + url: env.SERVER_URL + "/api" + ServiceEndpoints.explore(""), + useToken: true + }); +} diff --git a/src/chat-api/services/FriendService.ts b/src/chat-api/services/FriendService.ts index 4ec17462e..f68a51986 100644 --- a/src/chat-api/services/FriendService.ts +++ b/src/chat-api/services/FriendService.ts @@ -1,32 +1,29 @@ - import env from "../../common/env"; import { RawFriend } from "../RawData"; import { request } from "./Request"; import Endpoints from "./ServiceEndpoints"; - interface AddFriendOpts { username: string; tag: string; } - export const addFriend = async (opts: AddFriendOpts) => { const data = await request({ method: "POST", url: env.SERVER_URL + "/api" + Endpoints.addFriend(), - body: {username: opts.username, tag: opts.tag}, + body: { username: opts.username, tag: opts.tag }, useToken: true }); return data; }; interface AcceptFriendOpts { - friendId: string + friendId: string; } export const acceptFriendRequest = async (opts: AcceptFriendOpts) => { - const data = await request<{message: string}>({ + const data = await request<{ message: string }>({ method: "POST", url: env.SERVER_URL + "/api" + Endpoints.friends(opts.friendId), useToken: true @@ -35,14 +32,14 @@ export const acceptFriendRequest = async (opts: AcceptFriendOpts) => { }; interface RemoveFriendOpts { - friendId: string + friendId: string; } export const removeFriend = async (opts: RemoveFriendOpts) => { - const data = await request<{message: string}>({ + const data = await request<{ message: string }>({ method: "DELETE", url: env.SERVER_URL + "/api" + Endpoints.friends(opts.friendId), useToken: true }); return data; -}; \ No newline at end of file +}; diff --git a/src/chat-api/services/MessageService.ts b/src/chat-api/services/MessageService.ts index c60e2bbab..5e164eed7 100644 --- a/src/chat-api/services/MessageService.ts +++ b/src/chat-api/services/MessageService.ts @@ -1,6 +1,7 @@ import env from "../../common/env"; import { RawAttachment, RawMessage, RawUser } from "../RawData"; -import { request, xhrRequest } from "./Request"; + +import { request } from "./Request"; import Endpoints from "./ServiceEndpoints"; interface FetchMessageOpts { @@ -21,9 +22,75 @@ export const fetchMessages = async ( limit: opts?.limit || env.MESSAGE_LIMIT, ...(opts?.afterMessageId ? { after: opts.afterMessageId } : undefined), ...(opts?.beforeMessageId ? { before: opts.beforeMessageId } : undefined), - ...(opts?.aroundMessageId ? { around: opts.aroundMessageId } : undefined), + ...(opts?.aroundMessageId ? { around: opts.aroundMessageId } : undefined) }, - useToken: true, + useToken: true + }); + return data; +}; +interface SearchMessageOpts { + limit?: number; + order?: "asc" | "desc"; + beforeMessageId?: string; + afterMessageId?: string; + userIds?: string[]; +} + +export const searchMessages = async ( + query: string, + channelId: string, + opts?: SearchMessageOpts +) => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api" + Endpoints.messages(channelId) + "/search", + paramsArrayMode: "keys", + params: { + limit: opts?.limit || env.MESSAGE_LIMIT, + ...(opts?.afterMessageId ? { after: opts.afterMessageId } : undefined), + ...(opts?.beforeMessageId ? { before: opts.beforeMessageId } : undefined), + ...(opts?.order ? { order: opts.order } : undefined), + ...(query.trim() ? { query } : undefined), + ...(opts?.userIds?.length ? { user_id: opts.userIds } : undefined), + ...(opts?.order ? { order: opts.order } : undefined) + }, + useToken: true + }); + return data; +}; + +export const pinMessage = async (channelId: string, messageId: string) => { + const data = await request<{ status: boolean }>({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + Endpoints.messages(channelId) + + `/pins/${messageId}`, + + useToken: true + }); + return data; +}; +export const unpinMessage = async (channelId: string, messageId: string) => { + const data = await request<{ status: boolean }>({ + method: "DELETE", + url: + env.SERVER_URL + + "/api" + + Endpoints.messages(channelId) + + `/pins/${messageId}`, + + useToken: true + }); + return data; +}; +export const fetchPinnedMessages = async (channelId: string) => { + const data = await request<{ messages: RawMessage[] }>({ + method: "GET", + url: env.SERVER_URL + "/api" + Endpoints.messages(channelId) + "/pins", + + useToken: true }); return data; }; @@ -40,20 +107,22 @@ export const fetchChannelAttachments = async ( params: { limit, ...(afterAttachmentId ? { after: afterAttachmentId } : undefined), - ...(beforeAttachmentId ? { before: beforeAttachmentId } : undefined), + ...(beforeAttachmentId ? { before: beforeAttachmentId } : undefined) }, - useToken: true, + useToken: true }); return data; }; interface PostMessageOpts { + htmlEmbed?: string; content?: string; channelId: string; socketId?: string; - attachment?: File; replyToMessageIds?: string[]; mentionReplies?: boolean; + silent?: boolean; + nerimityCdnFileId?: string; googleDriveAttachment?: { id: string; mime: string; @@ -62,64 +131,59 @@ interface PostMessageOpts { } export const postMessage = async (opts: PostMessageOpts) => { - let body: any = { + const body: any = { content: opts.content?.trim() || undefined, + ...(opts.htmlEmbed ? { htmlEmbed: opts.htmlEmbed } : {}), + ...(opts.silent ? { silent: true } : {}), ...(opts.replyToMessageIds?.length ? { replyToMessageIds: opts.replyToMessageIds, - mentionReplies: opts.mentionReplies, + mentionReplies: opts.mentionReplies } : {}), + ...(opts.nerimityCdnFileId + ? { nerimityCdnFileId: opts.nerimityCdnFileId } + : {}), + ...(opts.googleDriveAttachment ? { googleDriveAttachment: opts.googleDriveAttachment } : {}), - ...(opts.socketId ? { socketId: opts.socketId } : {}), + ...(opts.socketId ? { socketId: opts.socketId } : {}) }; - if (opts.attachment) { - const fd = new FormData(); - opts.content && fd.append("content", opts.content); - if (opts.socketId) { - fd.append("socketId", opts.socketId); - } - - if (opts.replyToMessageIds?.length) { - fd.append("replyToMessageIds", JSON.stringify(opts.replyToMessageIds)); - fd.append("mentionReplies", String(opts.mentionReplies)); - } - fd.append("attachment", opts.attachment); - body = fd; - - const data = await xhrRequest( - { - method: "POST", - url: env.SERVER_URL + "/api" + Endpoints.messages(opts.channelId), - useToken: true, - body, - }, - opts.onUploadProgress - ); - - return data; - } - const data = await request({ method: "POST", url: env.SERVER_URL + "/api" + Endpoints.messages(opts.channelId), useToken: true, - body, + body }); return data; }; - interface UpdateMessageOpts { content: string; channelId: string; messageId: string; } +interface MarkMessageUnreadOpts { + channelId: string; + messageId: string; +} + +export const markMessageUnread = async (opts: MarkMessageUnreadOpts) => { + const data = await request>({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + Endpoints.message(opts.channelId, opts.messageId) + + "/mark-unread", + useToken: true + }); + return data; +}; export const updateMessage = async (opts: UpdateMessageOpts) => { const data = await request>({ method: "PATCH", @@ -129,8 +193,8 @@ export const updateMessage = async (opts: UpdateMessageOpts) => { Endpoints.message(opts.channelId, opts.messageId), useToken: true, body: { - content: opts.content, - }, + content: opts.content + } }); return data; }; @@ -140,7 +204,7 @@ export const postChannelTyping = async (channelId: string) => { method: "POST", url: env.SERVER_URL + "/api" + Endpoints.channelTyping(channelId), useToken: true, - notJSON: true, + notJSON: true }); return data; }; @@ -157,7 +221,7 @@ export const deleteMessage = async (opts: DeleteMessageOpts) => { env.SERVER_URL + "/api" + Endpoints.message(opts.channelId, opts.messageId), - useToken: true, + useToken: true }); return data; }; @@ -167,6 +231,7 @@ export const addMessageReaction = async (opts: { name: string; emojiId?: string | null; gif?: boolean; + webp?: boolean; }) => { const data = await request({ method: "POST", @@ -179,8 +244,9 @@ export const addMessageReaction = async (opts: { name: opts.name, emojiId: opts.emojiId, gif: opts.gif, + webp: opts.webp }, - useToken: true, + useToken: true }); return data; }; @@ -199,9 +265,9 @@ export const removeMessageReaction = async (opts: { "/reactions/remove", body: { name: opts.name, - emojiId: opts.emojiId, + emojiId: opts.emojiId }, - useToken: true, + useToken: true }); return data; }; @@ -228,9 +294,9 @@ export const fetchMessageReactedUsers = async (opts: { params: { name: opts.name, ...(opts.limit ? { limit: opts.limit } : undefined), - ...(opts.emojiId ? { emojiId: opts.emojiId } : undefined), + ...(opts.emojiId ? { emojiId: opts.emojiId } : undefined) }, - useToken: true, + useToken: true }); return data; }; @@ -238,7 +304,8 @@ export const fetchMessageReactedUsers = async (opts: { export const messageButtonClick = async ( channelId: string, messageId: string, - buttonId: string + buttonId: string, + inputs: Record ) => { const data = await request({ method: "POST", @@ -248,8 +315,12 @@ export const messageButtonClick = async ( Endpoints.message(channelId, messageId) + "/buttons/" + buttonId, + params: { + type: inputs ? "modal_click" : "button_click" + }, + body: inputs, useToken: true, - notJSON: true, + notJSON: true }); return data; }; diff --git a/src/chat-api/services/ModerationService.ts b/src/chat-api/services/ModerationService.ts index ca1b8189d..ff9a977cd 100644 --- a/src/chat-api/services/ModerationService.ts +++ b/src/chat-api/services/ModerationService.ts @@ -1,23 +1,30 @@ - import env from "../../common/env"; -import { RawApplication, RawFriend, RawServer, RawTicket, RawUser, TicketStatus } from "../RawData"; +import { + RawApplication, + RawChannel, + RawInventoryItem, + RawMessage, + RawServer, + RawTicket, + RawUser, + TicketStatus +} from "../RawData"; import { request } from "./Request"; -import Endpoints from "./ServiceEndpoints"; - - interface GetTicketsOpts { limit: number; afterId?: string; status?: TicketStatus; + includeIgnored?: boolean; } export const getModerationTickets = async (opts: GetTicketsOpts) => { const data = await request({ method: "GET", params: { - ...(opts.afterId ? {after: opts.afterId} : undefined), - ...(opts.status !== undefined ? {status: opts.status} : undefined), + ...(opts.afterId ? { after: opts.afterId } : undefined), + ...(opts.status !== undefined ? { status: opts.status } : undefined), + ...(opts.includeIgnored ? { includeIgnored: true } : undefined), limit: opts.limit }, url: env.SERVER_URL + "/api/moderation/tickets", @@ -34,23 +41,33 @@ export const getModerationTicket = async (id: string) => { return data; }; - -export const updateModerationTicket = async (id: string, status: TicketStatus) => { +export const updateModerationTicket = async ( + id: string, + status: TicketStatus +) => { const data = await request({ method: "POST", url: env.SERVER_URL + `/api/moderation/tickets/${id}`, - body: {status}, + body: { status }, useToken: true }); return data; }; +export const muteTicket = async (id: string) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + `/api/moderation/tickets/${id}/mute`, + useToken: true + }); + return data; +}; export const getServers = async (limit: number, afterId?: string) => { const data = await request({ method: "GET", params: { - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/servers", @@ -62,7 +79,7 @@ export const getPosts = async (limit: number, afterId?: string) => { const data = await request({ method: "GET", params: { - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/posts", @@ -70,13 +87,29 @@ export const getPosts = async (limit: number, afterId?: string) => { }); return data; }; +export const getMessages = async (channelId: string, messageId: string) => { + const data = await request<{ messages: RawMessage[]; channel: RawChannel }>({ + method: "GET", + url: + env.SERVER_URL + "/api/moderation/channels/" + channelId + "/messages/", + params: { + aroundId: messageId + }, + useToken: true + }); + return data; +}; -export const searchPosts = async (query: string, limit: number, afterId?: string) => { +export const searchPosts = async ( + query: string, + limit: number, + afterId?: string +) => { const data = await request({ method: "GET", params: { q: query, - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/posts/search", @@ -85,8 +118,10 @@ export const searchPosts = async (query: string, limit: number, afterId?: string return data; }; - -export const deletePosts = async (confirmPassword: string, postIds: string[]) => { +export const deletePosts = async ( + confirmPassword: string, + postIds: string[] +) => { const data = await request({ method: "POST", body: { @@ -98,13 +133,52 @@ export const deletePosts = async (confirmPassword: string, postIds: string[]) => }); return data; }; +export const addAnnouncePost = async ( + confirmPassword: string, + postId: string +) => { + const data = await request({ + method: "POST", + body: { + password: confirmPassword + }, + url: env.SERVER_URL + `/api/moderation/posts/${postId}/announcement`, + useToken: true + }); + return data; +}; +export const removeAnnouncePost = async ( + confirmPassword: string, + postId: string +) => { + const data = await request({ + method: "DELETE", + body: { + password: confirmPassword + }, + url: env.SERVER_URL + `/api/moderation/posts/${postId}/announcement`, + useToken: true + }); + return data; +}; - -export const getUsers = async (limit: number, afterId?: string) => { +export const getUsers = async ( + limit: number, + afterId?: string, + opts?: { + orderBy?: "joinedAt" | "username"; + order?: "asc" | "desc"; + filters?: string[] | string; + } +) => { const data = await request({ method: "GET", params: { - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), + + ...(opts?.orderBy ? { orderBy: opts.orderBy } : undefined), + ...(opts?.order ? { order: opts.order } : undefined), + ...(opts?.filters ? { filters: opts.filters } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/users", @@ -112,11 +186,15 @@ export const getUsers = async (limit: number, afterId?: string) => { }); return data; }; -export const getUsersWithSameIPAddress = async (userId: string, limit: number, afterId?: string) => { +export const getUsersWithSameIPAddress = async ( + userId: string, + limit: number, + afterId?: string +) => { const data = await request({ method: "GET", params: { - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + `/api/moderation/users/${userId}/users-with-same-ip`, @@ -125,12 +203,16 @@ export const getUsersWithSameIPAddress = async (userId: string, limit: number, a return data; }; -export const searchUsers = async (query: string, limit: number, afterId?: string) => { +export const searchUsers = async ( + query: string, + limit: number, + afterId?: string +) => { const data = await request({ method: "GET", params: { q: query, - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/users/search", @@ -139,7 +221,6 @@ export const searchUsers = async (query: string, limit: number, afterId?: string return data; }; - export const AuditLogType = { userSuspend: 0, userUnsuspend: 1, @@ -148,7 +229,11 @@ export const AuditLogType = { serverUpdate: 4, postDelete: 5, userSuspendUpdate: 6, - userWarned: 7 + userWarned: 7, + ipBan: 8, + serverUndoDelete: 9, + userShadowBanned: 10, + userShadowUnbanned: 11 } as const; export interface AuditLog { @@ -156,8 +241,8 @@ export interface AuditLog { createdAt: number; actionById: string; - actionBy: RawUser - actionType: typeof AuditLogType[keyof typeof AuditLogType]; + actionBy: RawUser; + actionType: (typeof AuditLogType)[keyof typeof AuditLogType]; serverName?: string; serverId?: string; @@ -167,31 +252,46 @@ export interface AuditLog { userId?: string; username?: string; - + ipAddress?: string; + count?: number; reason?: string; - expireAt?: number + expireAt?: number; } - -export const getAuditLog = async (limit: number, afterId?: string) => { +interface getAuditLogOpts { + search?: string; + limit: number; + afterId?: string; +} +export const getAuditLog = async ({ + limit, + afterId, + search +}: getAuditLogOpts) => { const data = await request({ method: "GET", params: { - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), + ...(search ? { q: search } : undefined), limit }, - url: env.SERVER_URL + "/api/moderation/audit-logs", + url: + env.SERVER_URL + "/api/moderation/audit-logs" + (search ? "/search" : ""), useToken: true }); return data; }; -export const searchServers = async (query: string, limit: number, afterId?: string) => { +export const searchServers = async ( + query: string, + limit: number, + afterId?: string +) => { const data = await request({ method: "GET", params: { q: query, - ...(afterId ? {after: afterId} : undefined), + ...(afterId ? { after: afterId } : undefined), limit }, url: env.SERVER_URL + "/api/moderation/servers/search", @@ -199,12 +299,26 @@ export const searchServers = async (query: string, limit: number, afterId?: stri }); return data; }; +export const activeServers = async () => { + const data = await request({ + method: "GET", -export const deleteServer = async (serverId: string, confirmPassword: string) => { + url: env.SERVER_URL + "/api/moderation/servers/active", + useToken: true + }); + return data; +}; + +export const deleteServer = async ( + serverId: string, + confirmPassword: string, + reason: string +) => { const data = await request({ method: "DELETE", body: { - password: confirmPassword + password: confirmPassword, + reason }, url: env.SERVER_URL + `/api/moderation/servers/${serverId}`, useToken: true @@ -212,6 +326,38 @@ export const deleteServer = async (serverId: string, confirmPassword: string) => return data; }; +export const pinServer = async (serverId: string) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + `/api/moderation/servers/${serverId}/pin`, + useToken: true + }); + return data; +}; +export const unpinServer = async (serverId: string) => { + const data = await request({ + method: "DELETE", + url: env.SERVER_URL + `/api/moderation/servers/${serverId}/pin`, + useToken: true + }); + return data; +}; + +export const undoDeleteServer = async ( + serverId: string, + confirmPassword: string +) => { + const data = await request({ + method: "DELETE", + body: { + password: confirmPassword + }, + url: env.SERVER_URL + `/api/moderation/servers/${serverId}/schedule-delete`, + useToken: true + }); + return data; +}; + interface SuspendUsersOpts { confirmPassword: string; userIds: string[]; @@ -238,7 +384,11 @@ export const suspendUsers = async (opts: SuspendUsersOpts) => { return data; }; -export const warnUsers = async (confirmPassword: string, userIds: string[], reason?: string) => { +export const warnUsers = async ( + confirmPassword: string, + userIds: string[], + reason?: string +) => { const data = await request({ method: "POST", body: { @@ -251,8 +401,44 @@ export const warnUsers = async (confirmPassword: string, userIds: string[], reas }); return data; }; +export const shadowBan = async ( + confirmPassword: string, + userIds: string[], + reason?: string +) => { + const data = await request({ + method: "POST", + body: { + userIds, + reason, + password: confirmPassword + }, + url: env.SERVER_URL + "/api/moderation/users/shadow-ban", + useToken: true + }); + return data; +}; +export const undoShadowBan = async ( + confirmPassword: string, + userIds: string[] +) => { + const data = await request({ + method: "DELETE", + body: { + userIds, + password: confirmPassword + }, + url: env.SERVER_URL + "/api/moderation/users/shadow-ban", + useToken: true + }); + return data; +}; -export const editSuspendUsers = async (confirmPassword: string, userIds: string[], update: {days?: number, reason?: string}) => { +export const editSuspendUsers = async ( + confirmPassword: string, + userIds: string[], + update: { days?: number; reason?: string } +) => { const data = await request({ method: "PATCH", body: { @@ -266,7 +452,10 @@ export const editSuspendUsers = async (confirmPassword: string, userIds: string[ return data; }; -export const unsuspendUsers = async (confirmPassword: string, userIds: string[]) => { +export const unsuspendUsers = async ( + confirmPassword: string, + userIds: string[] +) => { const data = await request({ method: "DELETE", body: { @@ -279,7 +468,10 @@ export const unsuspendUsers = async (confirmPassword: string, userIds: string[]) return data; }; -export const updateServer = async (serverId: string, update: {name?: string, verified?: boolean, password?: string}) => { +export const updateServer = async ( + serverId: string, + update: { name?: string; verified?: boolean; password?: string } +) => { const data = await request({ method: "POST", body: update, @@ -298,7 +490,6 @@ export const getServer = async (serverId: string) => { return data; }; - export const getOnlineUsers = async () => { const data = await request({ method: "GET", @@ -308,22 +499,31 @@ export const getOnlineUsers = async () => { return data; }; - -export type ModerationUser = RawUser & { - account?: {email: string; emailConfirmed?: boolean, warnCount?: number, warnExpiresAt?: number}, - application?: RawApplication - suspension?: ModerationSuspension - servers?: RawServer[] -} +export type ModerationUser = RawUser & { + account?: { + email: string; + emailConfirmed?: boolean; + warnCount?: number; + warnExpiresAt?: number; + }; + inventory: RawInventoryItem[]; + application?: RawApplication; + suspension?: ModerationSuspension; + shadowBan?: any; + servers?: RawServer[]; +}; export interface ModerationSuspension { - expireAt?: number | null - reason?: string - suspendedAt: number - suspendBy: RawUser + expireAt?: number | null; + reason?: string; + suspendedAt: number; + suspendBy: RawUser; } -export const updateUser = async (userId: string, update: {email?: string, username?: string, tag?: string}) => { +export const updateUser = async ( + userId: string, + update: { email?: string; username?: string; tag?: string } +) => { const data = await request({ method: "POST", body: update, @@ -343,11 +543,11 @@ export const getUser = async (userId: string) => { }; export interface ModerationStats { - totalRegisteredUsers: number, - weeklyRegisteredUsers: number, - totalCreatedServers: number, - totalCreatedMessages: number, - weeklyCreatedMessages: number, + totalRegisteredUsers: number; + weeklyRegisteredUsers: number; + totalCreatedServers: number; + totalCreatedMessages: number; + weeklyCreatedMessages: number; } export const getStats = async () => { @@ -358,3 +558,85 @@ export const getStats = async () => { }); return data; }; + +export interface UserAuditLog { + actionType: string; + actionById: string; + createdAt: number; + serverId?: string; + reason?: string; + data?: { + serverName?: string; + bannedUserId?: string; + unbannedUserId?: string; + kickedUserId?: string; + }; +} +interface UserAuditLogResponse { + users: RawUser[]; + servers: RawServer[]; + auditLogs: UserAuditLog[]; +} +export const getUsersAuditLogs = async (opts: { + query?: string; + afterId?: string; + limit?: number; +}) => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api/moderation/users/audit-logs", + params: { + ...(opts.query ? { q: opts.query } : {}), + ...(opts.afterId ? { after: opts.afterId } : {}), + limit: opts.limit + }, + useToken: true + }); + return data; +}; + +interface GetSuggestActionsOpts { + limit: number; + afterId?: string; +} + +export const getSuggestionActions = async (opts: GetSuggestActionsOpts) => { + const data = await request<{ data: any[] }>({ + method: "GET", + params: { + ...(opts.afterId ? { after: opts.afterId } : undefined), + limit: opts.limit + }, + url: env.SERVER_URL + "/api/moderation/suggest_action", + useToken: true + }); + return data; +}; + +interface UpsertSuggestActionsOpts { + actionType: number; + serverId?: string; + userId: string; + reason: string; + postId?: string; +} + +export const upsertSuggestActions = async (opts: UpsertSuggestActionsOpts) => { + const data = await request({ + method: "POST", + + url: env.SERVER_URL + "/api/moderation/suggest_action", + useToken: true, + body: opts + }); + return data; +}; +export const deleteSuggestActions = async (id: string) => { + const data = await request({ + method: "DELETE", + + url: env.SERVER_URL + "/api/moderation/suggest_action/" + id, + useToken: true + }); + return data; +}; diff --git a/src/chat-api/services/OAuthService.ts b/src/chat-api/services/OAuthService.ts new file mode 100644 index 000000000..8d88a3748 --- /dev/null +++ b/src/chat-api/services/OAuthService.ts @@ -0,0 +1,67 @@ +import env from "@/common/env"; +import { request } from "./Request"; +import { RawApplication, RawUser } from "../RawData"; + +export interface OAuth2Details { + user: RawUser; + application: RawApplication; +} +export const Oauth2GetDetails = async (opts: { + clientId: string; + redirectUri: string; +}) => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api/oauth2/authorize", + params: { + clientId: opts.clientId, + redirectUri: opts.redirectUri + }, + useToken: true + }); + return data; +}; + +interface Oauth2AuthorizeResponse { + redirectUri: string; + code: string; +} +export const Oauth2Authorize = async (opts: { + clientId: string; + redirectUri: string; + scopes: string[]; +}) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + "/api/oauth2/authorize", + params: { + clientId: opts.clientId, + redirectUri: opts.redirectUri, + scopes: opts.scopes.join(" ") + }, + useToken: true + }); + return data; +}; + +export interface OAuth2AuthorizedApplication { + application: RawApplication; + createdAt: number; + id: string; +} +export const OAuth2AuthorizedApplications = async () => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api/oauth2/applications", + useToken: true + }); + return data; +}; +export const OAuth2Unauthorize = async (appId: string) => { + const data = await request<{ status: true }>({ + method: "DELETE", + url: env.SERVER_URL + `/api/oauth2/applications/${appId}`, + useToken: true + }); + return data; +}; diff --git a/src/chat-api/services/PostService.ts b/src/chat-api/services/PostService.ts index c89a175ce..1005be0e5 100644 --- a/src/chat-api/services/PostService.ts +++ b/src/chat-api/services/PostService.ts @@ -3,41 +3,61 @@ import { RawPost, RawPostNotification, RawUser } from "../RawData"; import { Post } from "../store/usePosts"; import { request } from "./Request"; import ServiceEndpoints from "./ServiceEndpoints"; +import { uploadAttachment } from "./nerimityCDNService"; +import useAccount from "../store/useAccount"; interface GetFeedPostsOpts { - limit?: number - beforeId?: string - afterId?: string + limit?: number; + beforeId?: string; + afterId?: string; } +export const getAnnouncementPosts = async () => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api/posts/announcement", + useToken: true + }); + return data; +}; export const getFeedPosts = async (opts?: GetFeedPostsOpts) => { const data = await request({ method: "GET", url: env.SERVER_URL + "/api" + ServiceEndpoints.feedPosts(), params: { - ...(opts?.limit ? {limit: opts.limit} : undefined), - ...(opts?.beforeId ? {beforeId: opts.beforeId} : undefined), - ...(opts?.afterId ? {afterId: opts.afterId} : undefined) + ...(opts?.limit ? { limit: opts.limit } : undefined), + ...(opts?.beforeId ? { beforeId: opts.beforeId } : undefined), + ...(opts?.afterId ? { afterId: opts.afterId } : undefined) }, useToken: true }); return data; }; +export type DiscoverSort = + | "mostLiked7Days" + | "mostLiked30days" + | "mostLikedAllTime"; + interface GetDiscoverPostsOpts { - limit?: number - beforeId?: string - afterId?: string + limit?: number; + beforeId?: string; + afterId?: string; + + sort?: DiscoverSort; + abortSignal?: AbortSignal; } export const getDiscoverPosts = async (opts?: GetDiscoverPostsOpts) => { const data = await request({ method: "GET", url: env.SERVER_URL + "/api" + ServiceEndpoints.post("discover"), + abortSignal: opts?.abortSignal, params: { - ...(opts?.limit ? {limit: opts.limit} : undefined), - ...(opts?.beforeId ? {beforeId: opts.beforeId} : undefined), - ...(opts?.afterId ? {afterId: opts.afterId} : undefined) + ...(opts?.sort ? { sort: opts.sort } : undefined), + ...(opts?.limit ? { limit: opts.limit } : undefined), + ...(opts?.beforeId ? { beforeId: opts.beforeId } : undefined), + ...(opts?.afterId ? { afterId: opts.afterId } : undefined) }, useToken: true }); @@ -45,11 +65,11 @@ export const getDiscoverPosts = async (opts?: GetDiscoverPostsOpts) => { }; interface GetPostsOpts { - userId?: string - withReplies?: boolean - limit?: number - beforeId?: string - afterId?: string + userId?: string; + withReplies?: boolean; + limit?: number; + beforeId?: string; + afterId?: string; } export const getPosts = async (opts: GetPostsOpts) => { @@ -60,10 +80,14 @@ export const getPosts = async (opts: GetPostsOpts) => { const data = await request({ method: "GET", params: { - ...(defaultOpts.withReplies ? {withReplies: defaultOpts.withReplies} : undefined), - ...(defaultOpts.limit ? {limit: defaultOpts.limit} : undefined), - ...(defaultOpts.beforeId ? {beforeId: defaultOpts.beforeId} : undefined), - ...(defaultOpts.afterId ? {afterId: defaultOpts.afterId} : undefined) + ...(defaultOpts.withReplies + ? { withReplies: defaultOpts.withReplies } + : undefined), + ...(defaultOpts.limit ? { limit: defaultOpts.limit } : undefined), + ...(defaultOpts.beforeId + ? { beforeId: defaultOpts.beforeId } + : undefined), + ...(defaultOpts.afterId ? { afterId: defaultOpts.afterId } : undefined) }, url: env.SERVER_URL + "/api" + ServiceEndpoints.posts(defaultOpts.userId), useToken: true @@ -80,6 +104,32 @@ export const getPostsLiked = async (userId: string) => { return data; }; +export const pinPost = async (postId: string) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + "/api" + ServiceEndpoints.post(postId) + "/pin", + useToken: true + }); + return data; +}; + +export const repostPost = async (postId: string) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + "/api" + ServiceEndpoints.post(postId) + "/repost", + useToken: true + }); + return data; +}; + +export const unpinPost = async (postId: string) => { + const data = await request({ + method: "DELETE", + url: env.SERVER_URL + "/api" + ServiceEndpoints.post(postId) + "/pin", + useToken: true + }); + return data; +}; export const getPost = async (postId: string) => { const data = await request({ method: "GET", @@ -100,22 +150,28 @@ export const editPost = async (postId: string, content: string) => { const data = await request({ method: "PATCH", url: env.SERVER_URL + "/api" + ServiceEndpoints.post(postId), - body: { content}, + body: { content }, useToken: true }); return data; }; -export const postVotePoll = async (postId: string, pollId: string, choiceId: string) => { +export const postVotePoll = async ( + postId: string, + pollId: string, + choiceId: string +) => { const data = await request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.postVotePoll(postId, pollId, choiceId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.postVotePoll(postId, pollId, choiceId), useToken: true }); return data; }; - interface GetCommentPostsOpts { postId: string; limit?: number; @@ -128,16 +184,19 @@ export const getCommentPosts = async (opts: GetCommentPostsOpts) => { method: "GET", url: env.SERVER_URL + "/api" + ServiceEndpoints.postComments(opts.postId), params: { - ...(opts.limit ? {limit: opts.limit} : undefined), - ...(opts.beforeId ? {beforeId: opts.beforeId} : undefined), - ...(opts.afterId ? {afterId: opts.afterId} : undefined) + ...(opts.limit ? { limit: opts.limit } : undefined), + ...(opts.beforeId ? { beforeId: opts.beforeId } : undefined), + ...(opts.afterId ? { afterId: opts.afterId } : undefined) }, useToken: true }); return data; }; -export interface LikedPost {likedBy: RawUser, createdAt: number} +export interface LikedPost { + likedBy: RawUser; + createdAt: number; +} export const getLikesPosts = async (postId: string) => { const data = await request({ @@ -148,6 +207,15 @@ export const getLikesPosts = async (postId: string) => { return data; }; +export const getPostReposts = async (postId: string) => { + const data = await request({ + method: "GET", + url: env.SERVER_URL + "/api" + ServiceEndpoints.postReposts(postId), + useToken: true + }); + return data; +}; + export const getPostNotifications = async () => { const data = await request({ method: "GET", @@ -174,30 +242,30 @@ export const getPostNotificationDismiss = async () => { return data; }; +export const createPost = async (opts: { + content?: string; + attachment?: File; + replyToPostId?: string; + poll?: { choices: string[] }; +}) => { + const account = useAccount(); + const userId = account.user()?.id; -export const createPost = async (opts: {content?: string, attachment?: File, replyToPostId?: string, poll?: {choices: string[]}}) => { + let fileId; + if (opts.attachment) { + const res = await uploadAttachment(userId!, { + file: opts.attachment + }); + fileId = res.fileId; + } - let body: any = { + const body: any = { content: opts.content, poll: opts.poll, - ...(opts.replyToPostId ? {postId: opts.replyToPostId} : undefined) + ...(fileId ? { nerimityCdnFileId: fileId } : undefined), + ...(opts.replyToPostId ? { postId: opts.replyToPostId } : undefined) }; - if (opts.attachment) { - const fd = new FormData(); - - if (opts.content?.trim()) { - fd.append("content", opts.content); - } - if (opts.poll) { - fd.append("poll", JSON.stringify(opts.poll)); - } - if (opts.replyToPostId) fd.append("postId", opts.replyToPostId); - fd.append("attachment", opts.attachment); - body = fd; - } - - const data = await request({ method: "POST", body, @@ -224,4 +292,3 @@ export const unlikePost = async (postId: string) => { }); return data; }; - diff --git a/src/chat-api/services/ReminderService.ts b/src/chat-api/services/ReminderService.ts new file mode 100644 index 000000000..53a35f018 --- /dev/null +++ b/src/chat-api/services/ReminderService.ts @@ -0,0 +1,35 @@ +import env from "../../common/env"; +import { RawReminder } from "../RawData"; +import { request } from "./Request"; + +interface AddReminderOpts { + postId?: string; + messageId?: string; + timestamp: number; +} + +export const addReminder = async (opts: AddReminderOpts) => { + const data = await request({ + method: "POST", + url: env.SERVER_URL + "/api/reminders", + body: opts, + useToken: true + }); + return data; +}; + +export const deleteReminder = async (reminderId: string) => { + return await request({ + method: "DELETE", + url: env.SERVER_URL + "/api/reminders/" + reminderId, + useToken: true + }); +}; +export const updateReminder = async (reminderId: string, timestamp: number) => { + return await request({ + method: "POST", + url: env.SERVER_URL + "/api/reminders/" + reminderId, + useToken: true, + body: { timestamp } + }); +}; diff --git a/src/chat-api/services/Request.ts b/src/chat-api/services/Request.ts index 1a0c3fec8..6c6358f72 100644 --- a/src/chat-api/services/Request.ts +++ b/src/chat-api/services/Request.ts @@ -1,3 +1,4 @@ +import { AsyncFunctionQueue } from "@/common/AsyncFunctionQueue"; import { getStorageString, StorageKeys } from "../../common/localStorage"; // most, if not all of these messages come from cloudflare @@ -12,7 +13,7 @@ const ErrorCodeToMessage: Record = { 507: "Insufficient Storage", 508: "Loop Detected", 510: "Not Extended", - 511: "Network Authentication Required", + 511: "Network Authentication Required" }; interface RequestOpts { @@ -22,100 +23,176 @@ interface RequestOpts { useToken?: boolean; notJSON?: boolean; params?: Record; + paramsArrayMode?: "keys" | "spaces"; token?: string | null; + abortSignal?: AbortSignal; } +const queue = new AsyncFunctionQueue(); + export async function request(opts: RequestOpts): Promise { - const token = getStorageString(StorageKeys.USER_TOKEN, ""); - const url = new URL(opts.url); - url.search = new URLSearchParams(opts.params || {}).toString(); - - const response = await fetch(url, { - method: opts.method, - body: opts.body instanceof FormData ? opts.body : JSON.stringify(opts.body), - headers: { - ...(!(opts.body instanceof FormData) - ? { "Content-Type": "application/json" } - : undefined), - Authorization: opts.useToken ? opts.token || token : "", - }, - }).catch((err) => { - throw { message: "Could not connect to server. " + err.message }; - }); + return queue.add(async () => { + const token = getStorageString(StorageKeys.USER_TOKEN, ""); + const url = new URL(opts.url); + + let params: string[][] | undefined = undefined; + if (opts.paramsArrayMode === "keys") { + params = []; + for (const [key, value] of Object.entries(opts.params || {})) { + if (Array.isArray(value)) { + for (const v of value) { + params.push([key, v]); + } + continue; + } + params.push([key, value]); + } + } + url.search = new URLSearchParams(params || opts.params || {}).toString(); - const text = await response.text(); + const response = await fetch(url, { + signal: opts.abortSignal, + method: opts.method, + body: + opts.body instanceof FormData ? opts.body : JSON.stringify(opts.body), + headers: { + ...(!(opts.body instanceof FormData) + ? { "Content-Type": "application/json" } + : undefined), + ...(opts.useToken || opts.token + ? { Authorization: opts.token || token } + : {}) + } + }).catch((err) => { + throw { message: "Could not connect to server. " + err.message, code: 0 }; + }); - try { - if (!response.ok) { + const text = await response.text(); + + try { + if (!response.ok) { + const json = JSON.parse(text); + return Promise.reject(json); + } + if (opts.notJSON) return text as T; + return JSON.parse(text); + } catch { const code = response.status; const message = ErrorCodeToMessage[code]; if (message) { - throw { message, code }; + return Promise.reject({ message, code }); } - const json = JSON.parse(text); - return Promise.reject(json); + throw { message: text }; } - if (opts.notJSON) return text as T; - return JSON.parse(text); - } catch (e) { - throw { message: text }; - } + }); } interface XHROpts { url: string; method: "POST" | "GET" | "PUT" | "PATCH" | "DELETE"; body: FormData; - useToken?: boolean; + useToken?: boolean | string; notJSON?: boolean; params?: Record; } export function xhrRequest( opts: XHROpts, - onProgress?: (percent: number) => void + onProgress?: (percent: number, speed?: string) => void ): Promise { - const token = getStorageString(StorageKeys.USER_TOKEN, ""); - const url = new URL(opts.url); - url.search = new URLSearchParams(opts.params || {}).toString(); - - const xhr = new XMLHttpRequest(); - xhr.open(opts.method, opts.url, true); + return queue.add(async () => { + const token = getStorageString(StorageKeys.USER_TOKEN, ""); + const url = new URL(opts.url); + url.search = new URLSearchParams(opts.params || {}).toString(); - xhr.setRequestHeader("Authorization", token); + const xhr = new XMLHttpRequest(); + xhr.open(opts.method, url, true); - xhr.upload.onprogress = (e) => { - if (e.lengthComputable) { - const percentComplete = (e.loaded / e.total) * 100; - onProgress?.(Math.round(percentComplete)); + if (opts.useToken) { + xhr.setRequestHeader( + "Authorization", + typeof opts.useToken == "string" ? opts.useToken : token + ); } - }; - return new Promise((res, rej) => { - xhr.onreadystatechange = function () { - if (xhr.readyState == XMLHttpRequest.DONE) { - const text = xhr.responseText; - try { - if (xhr.status === 0) { - return rej({ message: "Could not connect to server." }); - } - const message = ErrorCodeToMessage[xhr.status]; - if (message) { - throw { message, code: xhr.status }; - } - if (xhr.status !== 200) { + const progressHandler = createProgressHandler(onProgress); + + xhr.upload.onprogress = (e) => { + progressHandler(e); + }; + + return new Promise((res, rej) => { + xhr.onreadystatechange = function () { + if (xhr.readyState == XMLHttpRequest.DONE) { + const text = xhr.responseText; + try { + if (xhr.status === 0) { + return rej({ message: "Could not connect to server." }); + } + if (xhr.status != 200) { + try { + const json = JSON.parse(text); + return rej(json); + } catch { + return rej({ message: text }); + } + } + + if (opts.notJSON) return res(text as T); const json = JSON.parse(text); - return rej(json); + return res(json); + } catch { + const message = ErrorCodeToMessage[xhr.status]; + if (message) { + throw { message, code: xhr.status }; + } + throw { message: text }; } - if (opts.notJSON) return res(text as T); - const json = JSON.parse(text); - return res(json); - } catch (e) { - throw { message: text }; } - } - }; + }; - xhr.send(opts.body); + const file = [...opts.body.values()][0] as File; + xhr.setRequestHeader("Content-Type", file.type); + xhr.setRequestHeader("File-Name", encodeURIComponent(file.name)); + + xhr.send(file); + }); }); } + +export const createProgressHandler = ( + onProgress?: (percent: number, speed?: string) => void +) => { + let startTime = 0; + let uploadedSize = 0; + return (e: ProgressEvent) => { + if (!startTime) { + startTime = Date.now(); + } + uploadedSize = e.loaded; + + const elapsedTime = Date.now() - startTime; + const uploadSpeed = uploadedSize / (elapsedTime / 1000); // Bytes per second + const uploadSpeedKBps = uploadSpeed / 1024; // Kilobytes per second + const uploadSpeedMBps = uploadSpeedKBps / 1024; // Megabytes per second + + // Choose the appropriate unit based on the speed + let unit = " KB/s"; + if (uploadSpeedMBps >= 1) { + unit = " MB/s"; + } + let speed: string | undefined = + uploadSpeedMBps >= 1 + ? uploadSpeedMBps.toFixed(2) + unit + : uploadSpeedKBps.toFixed(0) + unit; + + if (uploadSpeedMBps == Infinity) { + speed = "0 KB/s"; + } + + if (e.lengthComputable) { + const percentComplete = (e.loaded / e.total) * 100; + onProgress?.(Math.round(percentComplete), speed); + } + }; +}; diff --git a/src/chat-api/services/ServerService.ts b/src/chat-api/services/ServerService.ts index 85ff13a6e..e93000fa5 100644 --- a/src/chat-api/services/ServerService.ts +++ b/src/chat-api/services/ServerService.ts @@ -1,8 +1,40 @@ import { request } from "./Request"; import ServiceEndpoints from "./ServiceEndpoints"; -import {ChannelType, RawChannel, RawCustomEmoji, RawPublicServer, RawServer, RawServerRole, RawServerWelcomeAnswer, RawServerWelcomeQuestion, RawUser} from "../RawData"; +import { + RawBotCommand, + ChannelType, + RawChannel, + RawCustomEmoji, + RawExploreItem, + RawServer, + RawServerRole, + RawServerWelcomeQuestion, + RawUser, + RawServerFolder +} from "../RawData"; import env from "../../common/env"; +import { uploadEmoji } from "./nerimityCDNService"; +export async function deleteClan(serverId: string): Promise { + return request({ + method: "DELETE", + url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/clans", + useToken: true + }); +} + +export async function updateClan( + serverId: string, + tag: string, + icon: string +): Promise { + return request({ + method: "POST", + url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/clans", + body: { tag, icon }, + useToken: true + }); +} export async function getInvites(serverId: string): Promise { return request({ @@ -11,8 +43,40 @@ export async function getInvites(serverId: string): Promise { useToken: true }); } +export async function transferOwnership( + serverId: string, + password: string, + newOwnerUserId: string +): Promise { + return request({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/transfer-ownership", + body: { password, newOwnerUserId }, + useToken: true + }); +} +export async function getServerBotCommands( + serverId: string +): Promise<{ commands: RawBotCommand[] }> { + return request({ + method: "GET", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/bot-commands", + useToken: true + }); +} -export async function updateServer(serverId: string, update: any): Promise { +export async function updateServer( + serverId: string, + update: any +): Promise { return request({ method: "POST", body: update, @@ -20,27 +84,86 @@ export async function updateServer(serverId: string, update: any): Promise useToken: true }); } -export async function kickServerMember(serverId: string, userId: string): Promise { +export async function kickServerMember( + serverId: string, + userId: string +): Promise { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMemberKick(serverId, userId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMemberKick(serverId, userId), useToken: true }); } -export async function BanServerMember(serverId: string, userId: string, shouldDeleteRecentMessages?: boolean): Promise { +export async function BanServerMember( + serverId: string, + userId: string, + shouldDeleteRecentMessages?: boolean, + reason?: string +): Promise { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMemberBan(serverId, userId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMemberBan(serverId, userId), params: { shouldDeleteRecentMessages // delete messages sent in the last 7 hours. }, + body: { + reason + }, useToken: true }); } -export async function removeBanServerMember(serverId: string, userId: string): Promise { + +export async function removeBanServerMember( + serverId: string, + userId: string +): Promise { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMemberBan(serverId, userId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMemberBan(serverId, userId), + useToken: true + }); +} + +export async function muteServerMember( + serverId: string, + userId: string, + expireAt: number, + reason?: string +): Promise { + return request({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMemberMute(serverId, userId), + + body: { + reason, + expireAt + }, + useToken: true + }); +} + +export async function removeMuteServerMember( + serverId: string, + userId: string +): Promise { + return request({ + method: "DELETE", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMemberMute(serverId, userId), useToken: true }); } @@ -53,28 +176,34 @@ export interface Ban { export async function bannedMembersList(serverId: string): Promise { return request({ method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMemberBan(serverId, ""), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverMemberBan(serverId, ""), useToken: true }); } interface CreateServerChannelOpts { - serverId: string + serverId: string; name?: string; type?: ChannelType; } -export async function createServerChannel(opts: CreateServerChannelOpts): Promise { +export async function createServerChannel( + opts: CreateServerChannelOpts +): Promise { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannels(opts.serverId), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverChannels(opts.serverId), body: { - ...(opts.name ? {name: opts.name} : undefined), - ...(opts.type ? {type: opts.type} : undefined) + ...(opts.name ? { name: opts.name } : undefined), + ...(opts.type ? { type: opts.type } : undefined) }, useToken: true }); } -export async function createServerRole(serverId: string): Promise { +export async function createServerRole( + serverId: string +): Promise { return request({ method: "POST", url: env.SERVER_URL + "/api" + ServiceEndpoints.serverRoles(serverId), @@ -82,91 +211,184 @@ export async function createServerRole(serverId: string): Promise }); } -export async function updateServerOrder(serverIds: string[]): Promise { +export async function updateServerOrder( + serverIds: string[] +): Promise { return request({ method: "POST", - body: {serverIds}, + body: { serverIds }, url: env.SERVER_URL + "/api" + ServiceEndpoints.serverOrder(), useToken: true }); } +export async function createServerFolder( + serverIds: string[] +): Promise { + return request({ + method: "POST", + body: { serverIds }, + url: env.SERVER_URL + "/api" + ServiceEndpoints.server("folders"), + useToken: true + }); +} +export async function updateServerFolder( + folderId: string, + serverIds: string[] +): Promise { + return request({ + method: "POST", + body: { serverIds }, + url: + env.SERVER_URL + "/api" + ServiceEndpoints.server("folders/" + folderId), + useToken: true + }); +} +export async function updateServerFolderExtra( + folderId: string, + update: { name?: string; color: string } +): Promise { + return request({ + method: "POST", + body: { name: update.name, color: update.color }, + url: + env.SERVER_URL + "/api" + ServiceEndpoints.server("folders/" + folderId), + useToken: true + }); +} -export async function updateServerChannelOrder(serverId: string, updated: {channelIds: string[], categoryId?: string}): Promise { +export async function updateServerChannelOrder( + serverId: string, + updated: { channelIds: string[]; categoryId?: string } +): Promise { return request({ method: "POST", - body: {channelIds: updated.channelIds, categoryId: updated.categoryId}, - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannelOrder(serverId), + body: { channelIds: updated.channelIds, categoryId: updated.categoryId }, + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverChannelOrder(serverId), useToken: true }); } -export async function updateServerRole(serverId: string, roleId: string, update: any): Promise { +export async function updateServerRole( + serverId: string, + roleId: string, + update: any +): Promise { return request({ method: "POST", body: update, - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverRole(serverId, roleId), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverRole(serverId, roleId), useToken: true }); } -export async function updateServerRoleOrder(serverId: string, roleIds: string[]): Promise { +export async function updateServerRoleOrder( + serverId: string, + roleIds: string[] +): Promise { return request({ method: "POST", - body: {roleIds}, + body: { roleIds }, url: env.SERVER_URL + "/api" + ServiceEndpoints.serverRolesOrder(serverId), useToken: true }); } -export async function deleteServerRole(serverId: string, roleId: string): Promise { +export async function deleteServerRole( + serverId: string, + roleId: string +): Promise { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverRole(serverId, roleId), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverRole(serverId, roleId), useToken: true }); } -export async function updateServerMemberProfile(serverId: string, userId: string, update: {nickname?: null | string}): Promise { +export async function updateServerMemberProfile( + serverId: string, + userId: string, + update: { nickname?: null | string } +): Promise { return request({ method: "POST", body: update, - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMember(serverId, userId) + "/profile", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverMember(serverId, userId) + + "/profile", useToken: true }); } - - -export async function updateServerMember(serverId: string, userId: string, update: any): Promise { +export async function updateServerMember( + serverId: string, + userId: string, + update: any +): Promise { return request({ method: "POST", body: update, - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverMember(serverId, userId), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverMember(serverId, userId), + useToken: true + }); +} + +export async function updateServerChannelPermissions(opts: { + serverId: string; + channelId: string; + roleId: string; + permissions: number; +}): Promise { + return request({ + method: "POST", + body: { permissions: opts.permissions }, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(opts.serverId, opts.channelId) + + `/permissions/${opts.roleId}`, useToken: true }); } -export async function updateServerChannel(serverId: string, channelId: string, update: any): Promise { +export async function updateServerChannel( + serverId: string, + channelId: string, + update: any +): Promise { return request({ method: "POST", body: update, - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannel(serverId, channelId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId), useToken: true }); } -export async function deleteServerChannel(serverId: string, channelId: string): Promise { +export async function deleteServerChannel( + serverId: string, + channelId: string +): Promise { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverChannel(serverId, channelId), + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId), useToken: true }); } - export async function createServer(serverName: string): Promise { return request({ method: "POST", url: env.SERVER_URL + "/api" + ServiceEndpoints.servers(), useToken: true, - body: {name: serverName} + body: { name: serverName } }); } @@ -177,19 +399,33 @@ export async function createInvite(serverId: string): Promise { useToken: true }); } -export async function deleteInvite(serverId: string, code: string): Promise { +export async function deleteInvite( + serverId: string, + code: string +): Promise { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverInvites(serverId) + `/${code}`, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverInvites(serverId) + + `/${code}`, useToken: true }); } -export async function createCustomInvite(code: string, serverId: string): Promise { +export async function createCustomInvite( + code: string, + serverId: string +): Promise { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverInvites(serverId) + "/custom", - body: {code}, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverInvites(serverId) + + "/custom", + body: { code }, useToken: true }); } @@ -213,21 +449,27 @@ export async function leaveServer(serverId: string): Promise { export async function joinServerByInviteCode(inviteCode: string) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.serverInviteCode(inviteCode), + url: + env.SERVER_URL + "/api" + ServiceEndpoints.serverInviteCode(inviteCode), useToken: true }); } -export async function inviteBot(serverId: string, appId: string, permissions: number) { - return request<{success: boolean}>({ +export async function inviteBot( + serverId: string, + appId: string, + permissions: number +) { + return request<{ success: boolean }>({ method: "POST", - url: env.SERVER_URL + `/api/servers/${serverId}/invites/applications/${appId}/bot`, - params: {permissions}, + url: + env.SERVER_URL + + `/api/servers/${serverId}/invites/applications/${appId}/bot`, + params: { permissions }, useToken: true }); } -export type ServerWithMemberCount = RawServer & { memberCount: number }; - +export type ServerWithMemberCount = RawServer & { memberCount: number }; export async function serverDetailsByInviteCode(inviteCode: string) { return request({ @@ -237,7 +479,7 @@ export async function serverDetailsByInviteCode(inviteCode: string) { } export async function publicServerByEmojiId(id: string) { - return request({ + return request({ method: "GET", url: env.SERVER_URL + `/api/emojis/${id}/server`, useToken: true @@ -248,79 +490,89 @@ export async function publicServerByEmojiId(id: string) { export async function joinPublicServer(serverId: string) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(serverId) + "/join" , - useToken: true - }); -} -export async function BumpPublicServer(serverId: string, token: string) { - return request({ - method: "POST", - body: {token}, - url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(serverId) + "/bump" , + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.exploreServer(serverId) + + "/join", useToken: true }); } export async function getPublicServer(serverId: string) { - return request({ + return request({ method: "GET", url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(serverId), useToken: true }); } -export async function getPublicServers(sort: "most_bumps" | "most_members" | "recently_added" | "recently_bumped", filter: "all" | "verified", limit?: number) { - return request({ - params: {sort, filter, limit}, - method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(""), - useToken: true - }); -} - -export async function updatePublicServer(serverId: string, description: string) { - return request({ +export async function updatePublicServer( + serverId: string, + description: string +) { + return request({ method: "POST", url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(serverId), - body: {description}, + body: { description }, useToken: true }); } -export async function deletePublicServer(serverId: string) { - return request({ +export async function deleteExploreItem(id: string) { + return request<{ status: boolean }>({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.exploreServer(serverId), + url: env.SERVER_URL + "/api" + ServiceEndpoints.explore(id), useToken: true }); } +export async function addServerEmoji( + serverId: string, + emojiName: string, + file: File +) { + const { fileId } = await uploadEmoji({ + file + }); -export async function addServerEmoji(serverId: string, emojiName: string, base64: string) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis", + url: + env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis", body: { name: emojiName, - emoji: base64 + fileId }, useToken: true }); } -export type RawCustomEmojiWithCreator = RawCustomEmoji & {uploadedBy: RawUser}; +export type RawCustomEmojiWithCreator = RawCustomEmoji & { + uploadedBy: RawUser; +}; export async function getServerEmojis(serverId: string) { return request({ method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis", + url: + env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis", useToken: true }); } -export async function updateServerEmoji(serverId: string, emojiId: string, newName: string) { +export async function updateServerEmoji( + serverId: string, + emojiId: string, + newName: string +) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis/" + emojiId, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/emojis/" + + emojiId, body: { name: newName }, @@ -330,7 +582,12 @@ export async function updateServerEmoji(serverId: string, emojiId: string, newNa export async function deleteServerEmoji(serverId: string, emojiId: string) { return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/emojis/" + emojiId, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/emojis/" + + emojiId, notJSON: true, useToken: true }); @@ -339,39 +596,54 @@ export async function deleteServerEmoji(serverId: string, emojiId: string) { export interface CreateQuestion { title: string; multiselect: boolean; - answers: CreateAnswer[] + answers: CreateAnswer[]; } export interface CreateAnswer { title: string; - roleIds: string[] + roleIds: string[]; order?: number; } -export async function createWelcomeQuestion(serverId: string, question: CreateQuestion) { +export async function createWelcomeQuestion( + serverId: string, + question: CreateQuestion +) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/questions", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/questions", body: question, useToken: true }); } - export interface UpdateQuestion { id?: string; title: string; multiselect: boolean; - answers: UpdateAnswer[] + answers: UpdateAnswer[]; } export interface UpdateAnswer { id: string; title: string; - roleIds: string[] + roleIds: string[]; order?: number; } -export async function updateWelcomeQuestion(serverId: string, questionId: string, question: UpdateQuestion) { +export async function updateWelcomeQuestion( + serverId: string, + questionId: string, + question: UpdateQuestion +) { return request({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/questions/" + questionId, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/questions/" + + questionId, body: question, useToken: true }); @@ -380,37 +652,145 @@ export async function updateWelcomeQuestion(serverId: string, questionId: string export async function getWelcomeQuestions(serverId: string) { return request({ method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/questions", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/questions", useToken: true }); } export async function getWelcomeQuestion(serverId: string, questionId: string) { return request({ method: "GET", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/questions/" + questionId, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/questions/" + + questionId, useToken: true }); } -export async function deleteWelcomeQuestion(serverId: string, questionId: string) { - return request<{status: boolean}>({ +export async function deleteWelcomeQuestion( + serverId: string, + questionId: string +) { + return request<{ status: boolean }>({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/questions/" + questionId, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/questions/" + + questionId, useToken: true }); } export async function addAnswerToMember(serverId: string, answerId: string) { - return request<{status: boolean}>({ + return request<{ status: boolean }>({ method: "POST", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/answers/" + answerId + "/answer", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/answers/" + + answerId + + "/answer", useToken: true }); } -export async function removeAnswerFromMember(serverId: string, answerId: string) { - return request<{status: boolean}>({ +export async function removeAnswerFromMember( + serverId: string, + answerId: string +) { + return request<{ status: boolean }>({ + method: "DELETE", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/welcome/answers/" + + answerId + + "/answer", + useToken: true + }); +} + +export interface UserAuditLog { + actionType: string; + actionById: string; + createdAt: number; + serverId?: string; + data?: { + serverName?: string; + }; +} +interface UserAuditLogResponse { + users: RawUser[]; + servers: RawServer[]; + auditLogs: UserAuditLog[]; +} +export const getServerAuditLogs = async (opts: { + serverId: string; + afterId?: string; + limit?: number; +}) => { + const data = await request({ + method: "GET", + url: + env.SERVER_URL + + `/api/${ServiceEndpoints.server(opts.serverId)}/audit-logs`, + params: { + ...(opts.afterId ? { after: opts.afterId } : {}), + limit: opts.limit + }, + useToken: true + }); + return data; +}; + +export async function createServerExternalEmbed( + serverId: string, + inviteId: string +): Promise<{ id: string }> { + return request({ + method: "POST", + params: { inviteId }, + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/external-embed", + useToken: true + }); +} +export async function getServerExternalEmbed( + serverId: string +): Promise<{ id: string }> { + return request({ + method: "GET", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/external-embed", + useToken: true + }); +} + +export async function deleteServerExternalEmbed( + serverId: string +): Promise<{ id: string }> { + return request({ method: "DELETE", - url: env.SERVER_URL + "/api" + ServiceEndpoints.server(serverId) + "/welcome/answers/" + answerId + "/answer", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.server(serverId) + + "/external-embed", useToken: true }); } diff --git a/src/chat-api/services/ServiceEndpoints.ts b/src/chat-api/services/ServiceEndpoints.ts index 844b36f66..fc96211b7 100644 --- a/src/chat-api/services/ServiceEndpoints.ts +++ b/src/chat-api/services/ServiceEndpoints.ts @@ -2,7 +2,6 @@ export default { login: () => "/users/login", register: () => "/users/register", - tickets: (id?: string) => `/tickets/${id || ""}`, servers: () => "/servers", @@ -10,18 +9,26 @@ export default { serverInvites: (serverId: string) => `/servers/${serverId}/invites`, serverInviteCode: (inviteCode: string) => `/servers/invites/${inviteCode}`, serverChannels: (serverId: string) => `/servers/${serverId}/channels`, - serverChannel: (serverId: string, channelId: string) => `/servers/${serverId}/channels/${channelId}`, + serverChannel: (serverId: string, channelId: string) => + `/servers/${serverId}/channels/${channelId}`, serverRoles: (serverId: string) => `/servers/${serverId}/roles`, serverRolesOrder: (serverId: string) => `/servers/${serverId}/roles/order`, - serverChannelOrder: (serverId: string) => `/servers/${serverId}/channels/order`, + serverChannelOrder: (serverId: string) => + `/servers/${serverId}/channels/order`, serverOrder: () => "/servers/order", - serverRole: (serverId: string, roleId: string) => `/servers/${serverId}/roles/${roleId}`, - serverMember: (serverId: string, userId: string) => `/servers/${serverId}/members/${userId}`, - serverMemberKick: (serverId: string, userId: string) => `/servers/${serverId}/members/${userId}/kick`, - serverMemberBan: (serverId: string, userId: string) => `/servers/${serverId}/bans/${userId}`, + serverRole: (serverId: string, roleId: string) => + `/servers/${serverId}/roles/${roleId}`, + serverMember: (serverId: string, userId: string) => + `/servers/${serverId}/members/${userId}`, + serverMemberKick: (serverId: string, userId: string) => + `/servers/${serverId}/members/${userId}/kick`, + serverMemberBan: (serverId: string, userId: string) => + `/servers/${serverId}/bans/${userId}`, + serverMemberMute: (serverId: string, userId: string) => + `/servers/${serverId}/mutes/${userId}`, exploreServer: (serverId: string) => `/explore/servers/${serverId}`, - + explore: (id: string) => `/explore/${id}`, user: (userId: string) => `/users/${userId}`, @@ -31,17 +38,19 @@ export default { channel: (channelId: string) => `/channels/${channelId}`, messages: (channelId: string) => `/channels/${channelId}/messages`, - channelAttachments: (channelId: string) => `/channels/${channelId}/attachments`, + channelAttachments: (channelId: string) => + `/channels/${channelId}/attachments`, channelTyping: (channelId: string) => `/channels/${channelId}/typing`, - message: (channelId: string, messageId: string) => `/channels/${channelId}/messages/${messageId}`, + message: (channelId: string, messageId: string) => + `/channels/${channelId}/messages/${messageId}`, addFriend: () => "/friends/add", friends: (friendId: string) => `/friends/${friendId}`, posts: (userId?: string) => { - return userId ? `/users/${userId}/posts` : "/posts"; + return userId ? `/users/${userId}/posts` : "/posts"; }, likedPosts: (userId: string) => { - return `/users/${userId}/posts/liked`; + return `/users/${userId}/posts/liked`; }, postComments: (postId: string) => { @@ -50,17 +59,19 @@ export default { postLikes: (postId: string) => { return `/posts/${postId}/likes`; }, + postReposts: (postId: string) => { + return `/posts/${postId}/reposts`; + }, postNotifications: () => "/posts/notifications", postNotificationDismiss: () => "/posts/notifications/dismiss", postNotificationCount: () => "/posts/notifications/count", post: (postId: string) => `/posts/${postId}`, - postVotePoll: (postId: string, pollId: string, choiceId: string) => `/posts/${postId}/polls/${pollId}/choices/${choiceId}`, + postVotePoll: (postId: string, pollId: string, choiceId: string) => + `/posts/${postId}/polls/${pollId}/choices/${choiceId}`, likePost: (postId: string) => `/posts/${postId}/like`, unlikePost: (postId: string) => `/posts/${postId}/unlike`, feedPosts: () => "/posts/feed", - userFollow: (userId: string) => `/users/${userId}/follow` - -}; \ No newline at end of file +}; diff --git a/src/chat-api/services/TenorService.ts b/src/chat-api/services/TenorService.ts index e8790bb20..114dd6d53 100644 --- a/src/chat-api/services/TenorService.ts +++ b/src/chat-api/services/TenorService.ts @@ -17,18 +17,24 @@ export const getTenorCategories = async () => { return data; }; - export interface TenorImage { - url: string; + gifUrl: string; previewUrl: string; + previewHeight: number; + previewWidth: number; } -export const getTenorImages = async (query: string) => { - const data = await request({ +export interface GetTenorImageResponse { + results: TenorImage[]; + next: string; +} +export const getTenorImages = async (query: string, pos?: string) => { + const data = await request({ method: "GET", - url: env.SERVER_URL + "/api/tenor/search", + url: env.SERVER_URL + "/api/v2/tenor/search", params: { - query + query, + ...(pos ? { pos } : {}) }, useToken: true }); diff --git a/src/chat-api/services/TicketService.ts.ts b/src/chat-api/services/TicketService.ts.ts index 6aa009cd6..abc62c752 100644 --- a/src/chat-api/services/TicketService.ts.ts +++ b/src/chat-api/services/TicketService.ts.ts @@ -1,20 +1,20 @@ import { request } from "./Request"; import ServiceEndpoints from "./ServiceEndpoints"; -import { RawChannel, RawChannelNotice, RawTicket, TicketCategory, TicketStatus } from "../RawData"; +import { RawTicket, TicketCategory, TicketStatus } from "../RawData"; import env from "@/common/env"; interface GetTicketsOpts { limit: number; status?: TicketStatus; - seen?: boolean + seen?: boolean; } export const getTickets = async (opts: GetTicketsOpts) => { const data = await request({ method: "GET", params: { - ...(opts.status !== undefined ? {status: opts.status} : undefined), - ...(opts.seen !== undefined ? {seen: opts.seen} : undefined), + ...(opts.status !== undefined ? { status: opts.status } : undefined), + ...(opts.seen !== undefined ? { seen: opts.seen } : undefined), limit: opts.limit }, url: env.SERVER_URL + "/api" + ServiceEndpoints.tickets(), @@ -52,8 +52,8 @@ export const updateTicket = async (ticketId: string, status: TicketStatus) => { const data = await request({ method: "POST", url: env.SERVER_URL + "/api" + ServiceEndpoints.tickets(ticketId), - body: {status}, + body: { status }, useToken: true }); return data; -}; \ No newline at end of file +}; diff --git a/src/chat-api/services/UserService.ts b/src/chat-api/services/UserService.ts index 6ff369bd2..9dc587892 100644 --- a/src/chat-api/services/UserService.ts +++ b/src/chat-api/services/UserService.ts @@ -1,24 +1,64 @@ import env from "../../common/env"; import { + RawBotCommand, RawChannel, RawChannelNotice, RawInboxWithoutChannel, + RawInventoryItem, RawMessage, RawPost, RawServer, + RawServerClan, RawUser, - RawUserConnection, + RawUserConnection } from "../RawData"; -import { Presence, UserStatus } from "../store/useUsers"; +import { Presence } from "../store/useUsers"; import { request } from "./Request"; import ServiceEndpoints from "./ServiceEndpoints"; -export async function createGoogleAccountLink(): Promise { +export async function createGoogleDriveAccountLink() { return request({ - url: env.SERVER_URL + "/api/google/create-link", + url: env.SERVER_URL + "/api/connections/google-drive/create-link", method: "GET", notJSON: true, - useToken: true, + useToken: true + }); +} + +export async function createGoogleAccountLink( + login?: boolean +): Promise { + return request({ + url: env.SERVER_URL + "/api/connections/google/create-link", + params: login ? { login } : undefined, + method: "GET", + notJSON: true, + useToken: true + }); +} + +export async function unlinkAccountWithGoogle(): Promise<{ + status: boolean; +}> { + return request({ + url: env.SERVER_URL + "/api/connections/google/unlink-account", + method: "POST", + useToken: true + }); +} + +export async function linkAccountWithGoogle( + code: string, + nerimityUserToken: string +): Promise<{ connection: RawUserConnection }> { + return request({ + url: env.SERVER_URL + "/api/connections/google/link-account", + method: "POST", + body: { + code, + nerimityToken: nerimityUserToken + }, + useToken: false }); } @@ -28,37 +68,49 @@ export async function registerFCM(token: string) { body: { token }, method: "POST", useToken: true, - notJSON: true, + notJSON: true }); } -export async function linkAccountWithGoogle( +export async function fetchInventory() { + return request({ + url: env.SERVER_URL + "/api" + ServiceEndpoints.user("inventory"), + method: "GET", + useToken: true + }); +} + +export async function linkAccountWithGoogleDrive( code: string, nerimityUserToken: string ): Promise<{ connection: RawUserConnection }> { return request({ - url: env.SERVER_URL + "/api/google/link-account", + url: env.SERVER_URL + "/api/connections/google-drive/link-account", method: "POST", body: { code, - nerimityToken: nerimityUserToken, + nerimityToken: nerimityUserToken }, - useToken: false, + useToken: false }); } -export async function unlinkAccountWithGoogle(): Promise<{ status: boolean }> { +export async function unlinkAccountWithGoogleDrive(): Promise<{ + status: boolean; +}> { return request({ - url: env.SERVER_URL + "/api/google/unlink-account", + url: env.SERVER_URL + "/api/connections/google-drive/unlink-account", method: "POST", - useToken: true, + useToken: true }); } -export async function getGoogleAccessToken(): Promise<{ accessToken: string }> { +export async function getGoogleDriveAccessToken(): Promise<{ + accessToken: string; +}> { return request({ - url: env.SERVER_URL + "/api/google/access-token", + url: env.SERVER_URL + "/api/connections/google-drive/access-token", method: "GET", - useToken: true, + useToken: true }); } @@ -71,7 +123,7 @@ export async function sendResetPassword( "/api" + ServiceEndpoints.user("reset-password/send-code"), body: { email }, - method: "POST", + method: "POST" }); } @@ -85,7 +137,7 @@ export async function resetPassword( params: { code, userId }, body: { newPassword }, method: "POST", - useToken: true, + useToken: true }); } @@ -96,7 +148,7 @@ export async function sendEmailConfirmCode(): Promise<{ message: string }> { "/api" + ServiceEndpoints.user("emails/verify/send-code"), method: "POST", - useToken: true, + useToken: true }); } export async function verifyEmailConfirmCode( @@ -106,7 +158,7 @@ export async function verifyEmailConfirmCode( url: env.SERVER_URL + "/api" + ServiceEndpoints.user("emails/verify"), params: { code }, method: "POST", - useToken: true, + useToken: true }); } @@ -114,7 +166,8 @@ export async function verifyEmailConfirmCode( // error returns {path?, message} export async function loginRequest( email: string, - password: string + password: string, + googleCode?: string ): Promise<{ token: string }> { const isUsernameAndTag = email.includes(":"); return request({ @@ -123,7 +176,8 @@ export async function loginRequest( body: { ...(isUsernameAndTag ? { usernameAndTag: email } : { email }), password, - }, + ...(googleCode ? { googleCode } : {}) + } }); } @@ -142,16 +196,18 @@ export async function registerRequest( email, username, password, - token, - }, + token + } }); } export interface UserDetails { block: boolean; suspensionExpiresAt?: number; + followsYou: boolean; user: RawUser & { application?: { + botCommands?: RawBotCommand[]; creatorAccount: { user: { username: string; @@ -175,6 +231,7 @@ export interface UserDetails { mutualFriendIds: string[]; mutualServerIds: string[]; latestPost: RawPost; + pinnedPosts: RawPost[]; profile?: UserProfile; hideFollowers?: boolean; hideFollowing?: boolean; @@ -184,13 +241,32 @@ export interface UserProfile { bgColorOne?: string; bgColorTwo?: string; primaryColor?: string; + font?: number; + clan?: RawServerClan; } -export async function getUserDetailsRequest(userId?: string) { +export async function getUserDetailsRequest( + userId?: string, + includePinnedPosts?: boolean, + includeBotCommands?: boolean +) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.user(userId || ""), method: "GET", - useToken: true, + params: { + ...(includePinnedPosts ? { includePinnedPosts } : {}), + ...(includeBotCommands ? { includeBotCommands } : {}) + }, + useToken: true + }); +} + +export async function getSearchUsers(search: string) { + return request({ + url: env.SERVER_URL + "/api" + ServiceEndpoints.user("search"), + method: "GET", + params: { q: search }, + useToken: true }); } @@ -203,7 +279,7 @@ export async function getUserNotificationsRequest() { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.user("notifications"), method: "GET", - useToken: true, + useToken: true }); } @@ -215,7 +291,7 @@ export async function getFollowers(userId?: string) { ServiceEndpoints.user(userId || "") + "/followers", method: "GET", - useToken: true, + useToken: true }); } export async function getFollowing(userId?: string) { @@ -226,7 +302,7 @@ export async function getFollowing(userId?: string) { ServiceEndpoints.user(userId || "") + "/following", method: "GET", - useToken: true, + useToken: true }); } @@ -235,21 +311,21 @@ export async function toggleBadge(bit: number) { url: env.SERVER_URL + "/api/users/badges/toggle", body: { bit }, method: "POST", - useToken: true, + useToken: true }); } export async function openDMChannelRequest(userId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.openUserDM(userId), method: "POST", - useToken: true, + useToken: true }); } export async function closeDMChannelRequest(channelId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.channel(channelId), method: "DELETE", - useToken: true, + useToken: true }); } @@ -257,14 +333,14 @@ export async function blockUser(userId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.user(userId) + "/block", method: "POST", - useToken: true, + useToken: true }); } export async function unblockUser(userId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.user(userId) + "/block", method: "DELETE", - useToken: true, + useToken: true }); } @@ -273,15 +349,15 @@ export async function updatePresence(presence: Partial) { url: env.SERVER_URL + "/api" + ServiceEndpoints.updatePresence(), method: "POST", body: presence, - useToken: true, + useToken: true }); } interface UpdateUserOptions { email?: string; username?: string; - avatar?: string; - banner?: string; + avatarId?: string; + bannerId?: string; tag?: string; password?: string; newPassword?: string; @@ -302,21 +378,21 @@ export async function updateUser( method: "POST", body, useToken: true, - token, + token }); } export async function followUser(userId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.userFollow(userId), method: "POST", - useToken: true, + useToken: true }); } export async function unfollowUser(userId: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.userFollow(userId), method: "DELETE", - useToken: true, + useToken: true }); } @@ -334,7 +410,7 @@ export async function updateNotificationSettings( url: env.SERVER_URL + "/api" + ServiceEndpoints.user("notifications"), method: "POST", useToken: true, - body: update, + body: update }); } @@ -343,7 +419,7 @@ export async function deleteAccount(password: string, deleteContent: boolean) { url: env.SERVER_URL + "/api" + ServiceEndpoints.user("delete-account"), method: "DELETE", useToken: true, - body: { password, deleteContent }, + body: { password, deleteContent } }); } @@ -356,7 +432,7 @@ export const updateDMChannelNotice = async ( url: env.SERVER_URL + "/api" + ServiceEndpoints.user("channel-notice"), body: { content }, useToken: true, - token, + token }); return data; }; @@ -366,7 +442,7 @@ export const deleteDMChannelNotice = async (token?: string | null) => { method: "DELETE", url: env.SERVER_URL + "/api" + ServiceEndpoints.user("channel-notice"), useToken: true, - token, + token }); return data; }; @@ -375,7 +451,7 @@ export const getDMChannelNotice = async (token?: string | null) => { method: "GET", url: env.SERVER_URL + "/api" + ServiceEndpoints.user("channel-notice"), useToken: true, - token, + token }); return data; }; @@ -384,6 +460,48 @@ export async function userNoticeDismiss(id: string) { return request({ url: env.SERVER_URL + "/api" + ServiceEndpoints.user("notices/" + id), method: "DELETE", - useToken: true, + useToken: true + }); +} + +export async function userLogout() { + return request({ + url: env.SERVER_URL + "/api" + ServiceEndpoints.user("logout"), + method: "DELETE", + useToken: true + }); +} + +export const DeviceType = { + Browser: 0, + Desktop: 1, + Mobile: 2 +} as const; + +export type DeviceTypeId = (typeof DeviceType)[keyof typeof DeviceType]; + +export interface UserSession { + lastSeenAt: number; + location: string; + sessionId: string; + deviceType: DeviceTypeId; +} +export async function fetchUserSessions() { + return request({ + url: env.SERVER_URL + "/api" + ServiceEndpoints.user("sessions"), + method: "GET", + useToken: true + }); +} + +export async function destroySession(password: string, sessionId?: string) { + return request({ + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.user("sessions/" + (sessionId || "")), + body: { password }, + method: "DELETE", + useToken: true }); } diff --git a/src/chat-api/services/VoiceService.ts b/src/chat-api/services/VoiceService.ts index 36d1b4244..0dd878d2b 100644 --- a/src/chat-api/services/VoiceService.ts +++ b/src/chat-api/services/VoiceService.ts @@ -2,7 +2,6 @@ import env from "../../common/env"; import { request } from "./Request"; import Endpoints from "./ServiceEndpoints"; - export const postJoinVoice = async (channelId: string, socketId: string) => { const data = await request({ method: "POST", @@ -17,8 +16,35 @@ export const postJoinVoice = async (channelId: string, socketId: string) => { export const postLeaveVoice = async (channelId: string) => { const data = await request({ method: "POST", - url: env.SERVER_URL + "/api" + Endpoints.channel(channelId) + "/voice/leave", + url: + env.SERVER_URL + "/api" + Endpoints.channel(channelId) + "/voice/leave", useToken: true }); return data; }; + +const lastCredentials = { + generatedAt: null as null | number, + result: null as null | any +}; + +export const getCachedCredentials = () => lastCredentials.result; +export const postGenerateCredential = async () => { + if (lastCredentials.generatedAt) { + const diff = Date.now() - lastCredentials.generatedAt; + // 1 hour after last generated + if (diff < 60 * 60 * 1000) { + return lastCredentials as { result: any }; + } + } + const data = await request<{ result: any }>({ + method: "POST", + url: env.SERVER_URL + "/api/voice/generate", + useToken: true + }); + + lastCredentials.generatedAt = Date.now(); + lastCredentials.result = data.result; + + return data; +}; diff --git a/src/chat-api/services/WebhookService.ts b/src/chat-api/services/WebhookService.ts new file mode 100644 index 000000000..70d3c131d --- /dev/null +++ b/src/chat-api/services/WebhookService.ts @@ -0,0 +1,101 @@ +import { request } from "./Request"; +import ServiceEndpoints from "./ServiceEndpoints"; +import { RawWebhook } from "../RawData"; +import env from "@/common/env"; + +export const createWebhook = async (serverId: string, channelId: string) => { + const data = await request({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + "/webhooks", + useToken: true + }); + return data; +}; + +export const getWebhookToken = async ( + serverId: string, + channelId: string, + webhookId: string +) => { + const data = await request<{ token: string }>({ + method: "GET", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + `/webhooks/${webhookId}/token`, + useToken: true + }); + return data; +}; + +export const getWebhooks = async (serverId: string, channelId: string) => { + const data = await request({ + method: "GET", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + "/webhooks", + useToken: true + }); + return data; +}; + +export const deleteWebhook = async ( + serverId: string, + channelId: string, + webhookId: string +) => { + const data = await request({ + method: "DELETE", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + `/webhooks/${webhookId}`, + useToken: true + }); + return data; +}; + +export const getWebhook = async ( + serverId: string, + channelId: string, + webhookId: string +) => { + const data = await request({ + method: "GET", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + `/webhooks/${webhookId}`, + useToken: true + }); + return data; +}; +export const updateWebhook = async ( + serverId: string, + channelId: string, + webhookId: string, + update: { + name?: string; + } +) => { + const data = await request({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + ServiceEndpoints.serverChannel(serverId, channelId) + + `/webhooks/${webhookId}`, + useToken: true, + body: update + }); + return data; +}; diff --git a/src/chat-api/services/nerimityCDNService.ts b/src/chat-api/services/nerimityCDNService.ts new file mode 100644 index 000000000..99d280f08 --- /dev/null +++ b/src/chat-api/services/nerimityCDNService.ts @@ -0,0 +1,113 @@ +import env from "@/common/env"; +import { request, xhrRequest } from "./Request"; +import { StorageKeys, useLocalStorage } from "@/common/localStorage"; +import ServiceEndpoints from "./ServiceEndpoints"; + +const [tokens, setTokens] = useLocalStorage< + { + token: string; + channelId?: string; + createdAt: number; + }[] +>(StorageKeys.CDN_TOKEN, []); + +const generateToken = async (channelId?: string, userToken?: string | null) => { + if (!Array.isArray(tokens())) { + setTokens([]); + } + const existingToken = tokens().find((t) => t.channelId === channelId); + + if (existingToken) { + const expired = Date.now() - existingToken.createdAt > 2 * 60 * 1000; + if (!expired) { + return existingToken.token; + } + } + + const res = await request<{ token: string }>({ + method: "POST", + url: + env.SERVER_URL + + "/api" + + (channelId ? ServiceEndpoints.channel(channelId) : "") + + "/cdn/token", + useToken: true, + token: userToken + }); + + const newToken = { + token: res.token, + channelId, + createdAt: Date.now() + }; + + setTokens([ + newToken, + ...tokens() + .filter((t) => Date.now() - t.createdAt <= 2 * 60 * 1000) + .slice(0, 9) + ]); + + return res.token; +}; +interface NerimityCDNRequestOpts { + file: File; + onUploadProgress?: (progress: number) => void; + channelId?: string; + userToken?: string | null; +} + +export async function uploadBanner( + groupId: string, + opts: NerimityCDNRequestOpts & { points?: number[] } +) { + return nerimityCDNUploadRequest({ + ...opts, + type: "profile_banners", + groupId + }); +} + +export async function uploadAvatar( + groupId: string, + opts: NerimityCDNRequestOpts & { points?: number[] } +) { + return nerimityCDNUploadRequest({ ...opts, type: "avatars", groupId }); +} + +export async function uploadEmoji(opts: NerimityCDNRequestOpts) { + return nerimityCDNUploadRequest({ ...opts, type: "emojis" }); +} + +export async function uploadAttachment( + groupId: string, + opts: NerimityCDNRequestOpts +) { + return nerimityCDNUploadRequest({ ...opts, type: "attachments", groupId }); +} + +async function nerimityCDNUploadRequest(opts: { + type: "avatars" | "profile_banners" | "emojis" | "attachments"; + channelId?: string; + points?: number[]; + file: File; + groupId?: string; + userToken?: string | null; + onUploadProgress?: (percent: number, speed?: string) => void; +}) { + const url = new URL(`${env.NERIMITY_CDN}${opts.type}/${opts.groupId || ""}`); + + const formData = new FormData(); + formData.append("f", opts.file); + + return xhrRequest<{ fileId: string }>( + { + method: "POST", + url: url.href, + body: formData, + params: opts.points ? { points: JSON.stringify(opts.points) } : undefined, + useToken: await generateToken(opts.channelId, opts.userToken) + }, + opts.onUploadProgress + ); +} diff --git a/src/chat-api/socketClient.ts b/src/chat-api/socketClient.ts index 12d94e635..6f2788a40 100644 --- a/src/chat-api/socketClient.ts +++ b/src/chat-api/socketClient.ts @@ -1,19 +1,89 @@ import io from "socket.io-client"; import env from "../common/env"; import { ServerEvents } from "./EventNames"; -import { onAuthenticated, onAuthenticateError, onConnect, onDisconnect, onReconnectAttempt } from "./events/connectionEvents"; -import { onFriendRemoved, onFriendRequestAccepted, onFriendRequestPending, onFriendRequestSent } from "./events/friendEvents"; +import { + onAuthenticated, + onAuthenticateError, + onConnect, + onDisconnect, + onReconnectAttempt +} from "./events/connectionEvents"; +import { + onFriendRemoved, + onFriendRequestAccepted, + onFriendRequestPending, + onFriendRequestSent +} from "./events/friendEvents"; import { onInboxClosed, onInboxOpened } from "./events/inboxEvents"; -import { onMessageCreated, onMessageDeleted, onMessageDeletedBatch, onMessageReactionAdded, onMessageReactionRemoved, onMessageUpdated } from "./events/messageEvents"; -import { onServerChannelCreated, onServerChannelDeleted, onServerChannelOrderUpdated, onServerChannelUpdated, onServerEmojiAdd, onServerEmojiRemove, onServerEmojiUpdate, onServerJoined, onServerLeft, onServerMemberJoined, onServerMemberLeft, onServerMemberUpdated, onServerOrderUpdated, onServerRoleCreated, onServerRoleDeleted, onServerRoleOrderUpdated, onServerRoleUpdated, onServerUpdated } from "./events/serverEvents"; -import { onNotificationDismissed, onUserBlocked, onUserConnectionAdded, onUserConnectionRemoved, onUserNoticeUpdated, onUserNotificationSettingsUpdate, onUserPresenceUpdate, onUserUnblocked, onUserUpdated, onUserUpdatedSelf } from "./events/userEvents"; -import { onCleanup, onMount } from "solid-js"; -import { onVoiceSignalReceived, onVoiceUserJoined, onVoiceUserLeft } from "./events/voiceEvents"; - - -const socket = io(env.SERVER_URL, { transports: ["websocket"], autoConnect: false}); +import { + onMessageCreated, + onMessageDeleted, + onMessageDeletedBatch, + onMessageMarkUnread, + onMessageReactionAdded, + onMessageReactionRemoved, + onMessageUpdated +} from "./events/messageEvents"; +import { + onServerChannelCreated, + onServerChannelDeleted, + onServerChannelOrderUpdated, + onServerChannelPermissionsUpdated, + onServerChannelUpdated, + onServerClanUpdated, + onServerEmojiAdd, + onServerEmojiRemove, + onServerEmojiUpdate, + onServerFolderCreated, + onServerFolderUpdated, + onServerJoined, + onServerLeft, + onServerMemberJoined, + onServerMemberLeft, + onServerMemberUpdated, + onServerOrderUpdated, + onServerRemoveScheduleDelete, + onServerRoleCreated, + onServerRoleDeleted, + onServerRoleOrderUpdated, + onServerRoleUpdated, + onServerScheduleDelete, + onServerUpdated +} from "./events/serverEvents"; +import { + onNotificationDismissed, + onUserBlocked, + onUserConnectionAdded, + onUserConnectionRemoved, + onUserNoticeUpdated, + onUserNotificationSettingsUpdate, + onUserPresenceUpdate, + onUserReminderAdd, + onUserReminderRemove, + onUserReminderUpdate, + onUserUnblocked, + onUserUpdated, + onUserUpdatedSelf +} from "./events/userEvents"; +import { createSignal, onCleanup, onMount } from "solid-js"; +import { + onVoiceSignalReceived, + onVoiceUserJoined, + onVoiceUserLeft +} from "./events/voiceEvents"; +import { reactNativeAPI, ReactSocketIO } from "@/common/ReactNative"; +import { isExperimentEnabled } from "@/common/experiments"; + +const socket = + reactNativeAPI()?.isReactNative && isExperimentEnabled("RN_NATIVE_WS")() + ? new ReactSocketIO(env.WS_URL || env.SERVER_URL) + : io(env.WS_URL || env.SERVER_URL, { + transports: ["websocket"], + autoConnect: false + }); let token: undefined | string; +const [sessionId, setSessionId] = createSignal(null); type ValueOf = T[keyof T]; @@ -22,13 +92,18 @@ export default { token = newToken; socket.connect(); }, + setSessionId, + sessionId, updateToken(newToken: string) { token = newToken; + setTimeout(() => { + location.reload(); + }, 1000); }, id: () => socket.id, socket, - useSocketOn: (name: ValueOf , event: any) => { + useSocketOn: (name: ValueOf, event: any) => { onMount(() => { socket.on(name, event); onCleanup(() => { @@ -38,17 +113,18 @@ export default { } }; - socket.io.on("reconnect_attempt", onReconnectAttempt); - socket.on(ServerEvents.CONNECT, () => onConnect(socket, token)); socket.on(ServerEvents.AUTHENTICATE_ERROR, onAuthenticateError); socket.on("disconnect", onDisconnect); socket.on(ServerEvents.USER_AUTHENTICATED, onAuthenticated); socket.on(ServerEvents.USER_UPDATED_SELF, onUserUpdatedSelf); socket.on(ServerEvents.USER_UPDATED, onUserUpdated); -socket.on(ServerEvents.USER_NOTIFICATION_SETTINGS_UPDATE, onUserNotificationSettingsUpdate); +socket.on( + ServerEvents.USER_NOTIFICATION_SETTINGS_UPDATE, + onUserNotificationSettingsUpdate +); socket.on(ServerEvents.USER_NOTICE_CREATED, onUserNoticeUpdated); socket.on(ServerEvents.USER_CONNECTION_ADDED, onUserConnectionAdded); @@ -57,9 +133,12 @@ socket.on(ServerEvents.USER_CONNECTION_REMOVED, onUserConnectionRemoved); socket.on(ServerEvents.USER_BLOCKED, onUserBlocked); socket.on(ServerEvents.USER_UNBLOCKED, onUserUnblocked); - socket.on(ServerEvents.USER_PRESENCE_UPDATE, onUserPresenceUpdate); +socket.on(ServerEvents.USER_REMINDER_ADD, onUserReminderAdd); +socket.on(ServerEvents.USER_REMINDER_UPDATE, onUserReminderUpdate); +socket.on(ServerEvents.USER_REMINDER_REMOVE, onUserReminderRemove); + socket.on(ServerEvents.FRIEND_REQUEST_SENT, onFriendRequestSent); socket.on(ServerEvents.FRIEND_REQUEST_PENDING, onFriendRequestPending); socket.on(ServerEvents.FRIEND_REQUEST_ACCEPTED, onFriendRequestAccepted); @@ -71,16 +150,27 @@ socket.on(ServerEvents.NOTIFICATION_DISMISSED, onNotificationDismissed); socket.on(ServerEvents.MESSAGE_CREATED, onMessageCreated); socket.on(ServerEvents.MESSAGE_UPDATED, onMessageUpdated); +socket.on(ServerEvents.MESSAGE_MARK_UNREAD, onMessageMarkUnread); socket.on(ServerEvents.MESSAGE_DELETED, onMessageDeleted); socket.on(ServerEvents.MESSAGE_DELETED_BATCH, onMessageDeletedBatch); - socket.on(ServerEvents.SERVER_JOINED, onServerJoined); socket.on(ServerEvents.SERVER_LEFT, onServerLeft); socket.on(ServerEvents.SERVER_UPDATED, onServerUpdated); socket.on(ServerEvents.SERVER_ORDER_UPDATED, onServerOrderUpdated); + +socket.on(ServerEvents.SERVER_FOLDER_CREATED, onServerFolderCreated); +socket.on(ServerEvents.SERVER_FOLDER_UPDATED, onServerFolderUpdated); + socket.on(ServerEvents.SERVER_ROLE_ORDER_UPDATED, onServerRoleOrderUpdated); -socket.on(ServerEvents.SERVER_CHANNEL_ORDER_UPDATED, onServerChannelOrderUpdated); +socket.on( + ServerEvents.SERVER_CHANNEL_ORDER_UPDATED, + onServerChannelOrderUpdated +); +socket.on( + ServerEvents.SERVER_CHANNEL_PERMISSIONS_UPDATED, + onServerChannelPermissionsUpdated +); socket.on(ServerEvents.SERVER_ROLE_CREATED, onServerRoleCreated); socket.on(ServerEvents.SERVER_ROLE_UPDATED, onServerRoleUpdated); @@ -94,12 +184,19 @@ socket.on(ServerEvents.SERVER_EMOJI_ADD, onServerEmojiAdd); socket.on(ServerEvents.SERVER_EMOJI_UPDATE, onServerEmojiUpdate); socket.on(ServerEvents.SERVER_EMOJI_REMOVE, onServerEmojiRemove); +socket.on(ServerEvents.SERVER_SCHEDULE_DELETE, onServerScheduleDelete); +socket.on( + ServerEvents.SERVER_REMOVE_SCHEDULE_DELETE, + onServerRemoveScheduleDelete +); socket.on(ServerEvents.SERVER_CHANNEL_CREATED, onServerChannelCreated); socket.on(ServerEvents.SERVER_CHANNEL_UPDATED, onServerChannelUpdated); socket.on(ServerEvents.SERVER_CHANNEL_DELETED, onServerChannelDeleted); +socket.on(ServerEvents.SERVER_CLAN_UPDATED, onServerClanUpdated); + socket.on(ServerEvents.MESSAGE_REACTION_ADDED, onMessageReactionAdded); socket.on(ServerEvents.MESSAGE_REACTION_REMOVED, onMessageReactionRemoved); socket.on(ServerEvents.VOICE_USER_JOINED, onVoiceUserJoined); socket.on(ServerEvents.VOICE_USER_LEFT, onVoiceUserLeft); -socket.on(ServerEvents.VOICE_SIGNAL_RECEIVED, onVoiceSignalReceived); \ No newline at end of file +socket.on(ServerEvents.VOICE_SIGNAL_RECEIVED, onVoiceSignalReceived); diff --git a/src/chat-api/store/UseTicket.ts b/src/chat-api/store/UseTicket.ts index 4de8270e5..d9bafd34f 100644 --- a/src/chat-api/store/UseTicket.ts +++ b/src/chat-api/store/UseTicket.ts @@ -6,7 +6,7 @@ import { TicketStatus } from "../RawData"; import { getTickets } from "../services/TicketService.ts"; const [hasModerationTicketNotification, setHasModerationTicketNotification] = - createSignal(false); + createSignal(0); const [hasTicketNotification, setHasTicketNotification] = createSignal(false); const updateModerationTicketNotification = async () => { @@ -17,16 +17,17 @@ const updateModerationTicketNotification = async () => { if (!hasModeratorPerm()) return; const tickets = await getModerationTickets({ - limit: 1, + limit: 10, status: TicketStatus.WAITING_FOR_MODERATOR_RESPONSE, + includeIgnored: false }); - setHasModerationTicketNotification(tickets.length > 0); + setHasModerationTicketNotification(tickets.length); }; const updateTicketNotification = async () => { const tickets = await getTickets({ limit: 1, - seen: false, + seen: false }); setHasTicketNotification(tickets.length > 0); }; @@ -35,9 +36,12 @@ const fetchUpdated = () => { updateTicketNotification(); }; -window.setInterval(() => { - fetchUpdated(); -}, 10 * 60 * 1000); // 10 minutes +window.setInterval( + () => { + fetchUpdated(); + }, + 10 * 60 * 1000 +); // 10 minutes export default function useTicket() { return { @@ -45,6 +49,6 @@ export default function useTicket() { hasModerationTicketNotification, updateTicketNotification, hasTicketNotification, - fetchUpdated, + fetchUpdated }; } diff --git a/src/chat-api/store/useAccount.ts b/src/chat-api/store/useAccount.ts index 8b7ca8442..3b67e795d 100644 --- a/src/chat-api/store/useAccount.ts +++ b/src/chat-api/store/useAccount.ts @@ -1,23 +1,25 @@ -import env from "@/common/env"; -import {createStore} from "solid-js/store"; +import { createStore } from "solid-js/store"; import { SelfUser } from "../events/connectionEventTypes"; -import { RawUserNotificationSettings, ServerNotificationPingMode, ServerNotificationSoundMode } from "../RawData"; +import { + RawReminder, + RawUserNotificationSettings, + ServerNotificationPingMode, + ServerNotificationSoundMode +} from "../RawData"; import { USER_BADGES, hasBit } from "../Bitwise"; import { updateNotificationSettings } from "../services/UserService"; - interface Account { - user: SelfUser | null, + user: SelfUser | null; - socketId: string | null, - socketConnected: boolean, - socketAuthenticated: boolean, - authenticationError: {message: string, data: any} | null; - notificationSettings: Record + socketId: string | null; + socketConnected: boolean; + socketAuthenticated: boolean; + authenticationError: { message: string; data: any } | null; + notificationSettings: Record; lastAuthenticatedAt: null | number; } - const [account, setAccount] = createStore({ user: null, socketId: null, @@ -32,38 +34,54 @@ const removeNotificationSettings = (channelOrServerId: string) => { setAccount("notificationSettings", channelOrServerId, undefined!); }; -const setNotificationSettings = (channelOrServerId: string, setting: Partial) => { +const setNotificationSettings = ( + channelOrServerId: string, + setting: Partial +) => { setAccount("notificationSettings", channelOrServerId, setting); }; -const getRawNotificationSettings = (serverOrChannelId: string) => account.notificationSettings[serverOrChannelId] as RawUserNotificationSettings | undefined; +const getRawNotificationSettings = (serverOrChannelId: string) => + account.notificationSettings[serverOrChannelId] as + | RawUserNotificationSettings + | undefined; + +const getCombinedNotificationSettings = ( + serverId?: string, + channelId?: string +) => { + const channelNotification = account.notificationSettings[channelId!] as + | RawUserNotificationSettings + | undefined; + const serverNotification = account.notificationSettings[serverId!] as + | RawUserNotificationSettings + | undefined; -const getCombinedNotificationSettings = (serverId?: string, channelId?: string) => { - const channelNotification = account.notificationSettings[channelId!] as RawUserNotificationSettings | undefined; - const serverNotification = account.notificationSettings[serverId!] as RawUserNotificationSettings | undefined; - if (!channelNotification) return serverNotification; return { ...channelNotification, ...serverNotification, - notificationPingMode: channelNotification.notificationPingMode ?? serverNotification?.notificationPingMode, - notificationSoundMode: channelNotification.notificationSoundMode ?? serverNotification?.notificationSoundMode + notificationPingMode: + channelNotification.notificationPingMode ?? + serverNotification?.notificationPingMode, + notificationSoundMode: + channelNotification.notificationSoundMode ?? + serverNotification?.notificationSoundMode } as RawUserNotificationSettings; }; interface SetSocketDetailsArgs { - socketId?: string | null, - socketAuthenticated?: boolean, - socketConnected?: boolean, - authenticationError?: {message: string, data: any} | null, + socketId?: string | null; + socketAuthenticated?: boolean; + socketConnected?: boolean; + authenticationError?: { message: string; data: any } | null; lastAuthenticatedAt?: number; } const setSocketDetails = (details: SetSocketDetailsArgs) => { setAccount(details); }; - const setUser = (user: Partial | null) => setAccount("user", user); const user = () => account.user; @@ -74,16 +92,27 @@ const authenticationError = () => account.authenticationError; const lastAuthenticatedAt = () => account.lastAuthenticatedAt; -const avatarUrl = () => user()?.avatar ? env.NERIMITY_CDN + user()?.avatar : null; - -const hasModeratorPerm = () => hasBit(user()?.badges || 0, USER_BADGES.FOUNDER.bit) || hasBit(user()?.badges || 0, USER_BADGES.ADMIN.bit); - - +const hasModeratorPerm = (includeModBadges = false) => + hasBit(user()?.badges || 0, USER_BADGES.FOUNDER.bit) || + hasBit(user()?.badges || 0, USER_BADGES.ADMIN.bit) || + (includeModBadges && hasBit(user()?.badges || 0, USER_BADGES.MOD.bit)); +const hasOnlyModBadge = () => { + const isAdmin = hasModeratorPerm(); + const isMod = hasBit(user()?.badges || 0, USER_BADGES.MOD.bit); + return isMod && !isAdmin; +}; -const updateUserNotificationSettings = (opts: {serverId?: string, channelId?: string, notificationPingMode?: number | null, notificationSoundMode?: number | null}) => { - const currentNotificationSoundMode = () => getRawNotificationSettings(opts.channelId || opts.serverId!)?.notificationSoundMode ?? (opts.channelId ? null : 0); +const updateUserNotificationSettings = (opts: { + serverId: string; + channelId?: string; + notificationPingMode?: number | null; + notificationSoundMode?: number | null; +}) => { + const currentNotificationSoundMode = () => + getRawNotificationSettings(opts.channelId || opts.serverId!) + ?.notificationSoundMode ?? (opts.channelId ? null : 0); if (opts.notificationSoundMode !== undefined) { return updateNotificationSettings({ @@ -95,10 +124,16 @@ const updateUserNotificationSettings = (opts: {serverId?: string, channelId?: st let notificationSoundMode: number | null | undefined = null; - if (opts.notificationPingMode !== null && currentNotificationSoundMode() === null) { + if ( + opts.notificationPingMode !== null && + currentNotificationSoundMode() === null + ) { notificationSoundMode = opts.notificationPingMode; } - if (opts.notificationPingMode === ServerNotificationPingMode.MENTIONS_ONLY && currentNotificationSoundMode() === ServerNotificationSoundMode.ALL) { + if ( + opts.notificationPingMode === ServerNotificationPingMode.MENTIONS_ONLY && + currentNotificationSoundMode() === ServerNotificationSoundMode.ALL + ) { notificationSoundMode = ServerNotificationSoundMode.MENTIONS_ONLY; } if (opts.notificationPingMode === ServerNotificationPingMode.MUTE) { @@ -107,17 +142,48 @@ const updateUserNotificationSettings = (opts: {serverId?: string, channelId?: st return updateNotificationSettings({ notificationPingMode: opts.notificationPingMode, - ...(notificationSoundMode !== null ? {notificationSoundMode} : undefined), + ...(notificationSoundMode !== null ? { notificationSoundMode } : undefined), serverId: opts.serverId, channelId: opts.channelId }); }; +const isMe = (userId: string) => account.user && account.user.id === userId; + +const reminders = (channelId?: string) => { + if (!account.user?.reminders) return []; + if (!channelId) + return [...account.user.reminders].sort((a, b) => a.remindAt - b.remindAt); + return [...account.user.reminders] + .filter((r) => r.channelId === channelId) + .sort((a, b) => a.remindAt - b.remindAt); +}; + +const addReminder = (reminder: RawReminder) => { + if (!account.user?.reminders) return; + setUser({ reminders: [...account.user.reminders, reminder] }); +}; +const updateReminder = (reminder: RawReminder) => { + if (!account.user?.reminders) return; + setUser({ + reminders: account.user.reminders.map((r) => + r.id === reminder.id ? reminder : r + ) + }); +}; +const removeReminder = (reminderId: string) => { + if (!account.user?.reminders) return; + const reminders = account.user.reminders.filter((r) => r.id !== reminderId); + setUser({ reminders }); +}; export default function useAccount() { return { user, - avatarUrl, + reminders, + updateReminder, + addReminder, + removeReminder, setUser, setSocketDetails, isConnected, @@ -129,6 +195,8 @@ export default function useAccount() { updateUserNotificationSettings, removeNotificationSettings, hasModeratorPerm, - lastAuthenticatedAt + hasOnlyModBadge, + lastAuthenticatedAt, + isMe }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useChannelProperties.ts b/src/chat-api/store/useChannelProperties.ts index 356443547..a30aa92d6 100644 --- a/src/chat-api/store/useChannelProperties.ts +++ b/src/chat-api/store/useChannelProperties.ts @@ -1,6 +1,6 @@ -import { createStore } from "solid-js/store"; +import { createStore, SetStoreFunction } from "solid-js/store"; import { Message } from "./useMessages"; -import { RawMessage } from "../RawData"; +import { RawBotCommand, RawMessage } from "../RawData"; import { batch } from "solid-js"; export type ChannelProperties = { @@ -10,7 +10,10 @@ export type ChannelProperties = { replyToMessages: RawMessage[]; mentionReplies?: boolean; - attachment?: File; + attachment?: { + file: File; + uploadTo: "google_drive" | "nerimity_cdn"; + }; scrollTop?: number; @@ -25,6 +28,9 @@ export type ChannelProperties = { }; stale?: boolean; + selectedBotCommand?: RawBotCommand; + // advanced markup html + htmlEnabled?: boolean; }; const [properties, setChannelProperties] = createStore< @@ -48,7 +54,7 @@ const initIfMissing = (channelId: string) => { setChannelProperties(channelId, { content: "", isScrolledBottom: false, - replyToMessages: [], + replyToMessages: [] }); }; @@ -56,24 +62,28 @@ const addReply = (channelId: string, message: RawMessage) => { initIfMissing(channelId); const property = get(channelId)!; if (property.replyToMessages.length >= 5) return; - if (property.replyToMessages.find((m) => m.id === message.id)) return; + if (property.replyToMessages.find((m) => m.id === message.id)) { + // toggle it + removeReply(channelId, message.id); + return; + } setChannelProperties(channelId, { - replyToMessages: [message, ...property.replyToMessages], - ...(!property.replyToMessages.length ? { mentionReplies: true } : {}), + replyToMessages: [...property.replyToMessages, message], + ...(!property.replyToMessages.length ? { mentionReplies: true } : {}) }); }; const removeReply = (channelId: string, messageId: string) => { const property = get(channelId)!; setChannelProperties(channelId, { - replyToMessages: property.replyToMessages.filter((m) => m.id !== messageId), + replyToMessages: property.replyToMessages.filter((m) => m.id !== messageId) }); }; const removeReplies = (channelId: string) => { setChannelProperties(channelId, { replyToMessages: [], - mentionReplies: true, + mentionReplies: true }); }; @@ -88,6 +98,14 @@ const updateContent = (channelId: string, content: string) => { setChannelProperties(channelId, "content", content); }; +const update: SetStoreFunction> = ( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + ...args: any +) => { + initIfMissing(args[0]); + setChannelProperties(...(args as ["1", "content", "test"])); +}; + const get = (channelId: string) => properties[channelId] as ChannelProperties | undefined; @@ -96,14 +114,29 @@ const setEditMessage = (channelId: string, message?: Message) => { if (!message && !get(channelId)?.editMessageId) return; setChannelProperties(channelId, { editMessageId: message?.id, - content: message?.content || "", + content: message?.content || "" }); }; -const setAttachment = (channelId: string, file?: File) => { +const setAttachment = ( + channelId: string, + file?: File, + uploadTo?: "google_drive" | "nerimity_cdn" +) => { initIfMissing(channelId); - setChannelProperties(channelId, { - attachment: file, + if (!file && !uploadTo) { + setChannelProperties(channelId, "attachment", undefined); + return; + } + + const isMoreThan50MB = file && file.size > 50 * 1024 * 1024; + + const _uploadTo = + uploadTo || (isMoreThan50MB ? "google_drive" : "nerimity_cdn"); + + setChannelProperties(channelId, "attachment", { + ...(file ? { file } : undefined), + uploadTo: _uploadTo }); }; @@ -111,7 +144,7 @@ const setScrollTop = (channelId: string, scrollTop: number) => { initIfMissing(channelId); const isScrolledBottom = get(channelId)?.isScrolledBottom; setChannelProperties(channelId, { - scrollTop: !isScrolledBottom ? scrollTop : undefined, + scrollTop: !isScrolledBottom ? scrollTop : undefined }); }; const setScrolledBottom = (channelId: string, isScrolledBottom: boolean) => { @@ -120,10 +153,12 @@ const setScrolledBottom = (channelId: string, isScrolledBottom: boolean) => { }; const setMoreTopToLoad = (channelId: string, value: boolean) => { + initIfMissing(channelId); setChannelProperties(channelId, { moreTopToLoad: value }); }; const setMoreBottomToLoad = (channelId: string, value: boolean) => { + initIfMissing(channelId); setChannelProperties(channelId, { moreBottomToLoad: value }); }; @@ -135,8 +170,17 @@ const updateSlowDownMode = ( setChannelProperties(channelId, "slowDownMode", slowDownMode); }; +const updateSelectedBotCommand = ( + channelId: string, + botCommand?: RawBotCommand +) => { + if (!get(channelId)) return; + setChannelProperties(channelId, "selectedBotCommand", botCommand); +}; + export default function useChannelProperties() { return { + update, updateContent, get, setEditMessage, @@ -152,5 +196,6 @@ export default function useChannelProperties() { toggleMentionReplies, staleAll, updateSlowDownMode, + updateSelectedBotCommand }; } diff --git a/src/chat-api/store/useChannels.ts b/src/chat-api/store/useChannels.ts index ffb3c5cae..41b79535b 100644 --- a/src/chat-api/store/useChannels.ts +++ b/src/chat-api/store/useChannels.ts @@ -1,24 +1,40 @@ import { runWithContext } from "@/common/runWithContext"; -import { batch } from "solid-js"; +import { batch, createMemo } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { useWindowProperties } from "../../common/useWindowProperties"; import { dismissChannelNotification } from "../emits/userEmits"; import { CHANNEL_PERMISSIONS, - getAllPermissions, Bitwise, hasBit, ROLE_PERMISSIONS, + addBit } from "../Bitwise"; -import { RawChannel } from "../RawData"; +import { + ChannelType, + RawChannel, + ServerNotificationPingMode +} from "../RawData"; import useMessages from "./useMessages"; import useUsers from "./useUsers"; -import useServerMembers from "./useServerMembers"; +import useServerMembers, { ServerMember } from "./useServerMembers"; import useAccount from "./useAccount"; import useMention from "./useMention"; import socketClient from "../socketClient"; -import { postJoinVoice, postLeaveVoice } from "../services/VoiceService"; +import { + postGenerateCredential, + postJoinVoice, + postLeaveVoice +} from "../services/VoiceService"; import useVoiceUsers from "./useVoiceUsers"; +import { useMatch, useNavigate } from "solid-navigator"; +import RouterEndpoints from "@/common/RouterEndpoints"; +import useServers from "./useServers"; +import { loadSimplePeer } from "@/components/LazySimplePeer"; +import { getCustomSound, playSound } from "@/common/Sound"; +import { getStorageBoolean, StorageKeys } from "@/common/localStorage"; +import { isExperimentEnabled } from "@/common/experiments"; +import { reactNativeAPI } from "@/common/ReactNative"; export type Channel = Omit & { updateLastSeen(this: Channel, timestamp?: number): void; @@ -27,16 +43,28 @@ export type Channel = Omit & { setRecipientId(this: Channel, userId: string): void; update: (this: Channel, update: Partial) => void; - permissionList: typeof permissionList; + membersWithChannelAccess: typeof membersWithChannelAccess; recipient: typeof recipient; recipientId?: string; lastSeen?: number; hasNotifications: typeof hasNotifications; mentionCount: typeof mentionCount; - joinCall: () => void; + joinCall: (reconnect?: boolean) => void; leaveCall: () => void; callJoinedAt?: number; setCallJoinedAt: (this: Channel, joinedAt: number | undefined) => void; + permissionBits: ( + this: Channel, + defaultRoleOnly?: boolean, + userId?: string + ) => number; + hasPermission: ( + this: Channel, + bitwise: Bitwise, + defaultRoleOnly?: boolean, + userId?: string + ) => boolean; + canSendMessage: (this: Channel, userId: string) => boolean; }; const [channels, setChannels] = createStore< @@ -48,7 +76,7 @@ const set = (channel: RawChannel & { lastSeen?: number }) => { ...channel, recipient, hasNotifications, - permissionList, + membersWithChannelAccess, mentionCount, updateLastSeen, updateLastMessaged, @@ -58,14 +86,87 @@ const set = (channel: RawChannel & { lastSeen?: number }) => { update, joinCall, leaveCall, + permissionBits, + hasPermission, + canSendMessage }; setChannels(channel.id, newChannel); }; -function permissionList(this: Channel) { - const permissions = this.permissions || 0; - return getAllPermissions(CHANNEL_PERMISSIONS, permissions); +function permissionBits( + this: Channel, + defaultRoleOnly = false, + userId?: string +): number { + if (!this.serverId) return 0; + + const account = useAccount(); + const serverMembers = useServerMembers(); + const servers = useServers(); + const member = serverMembers.get( + this.serverId, + userId || (account.user()?.id as string) + ); + + const defaultRoleId = servers.get(member?.serverId!)?.defaultRoleId; + + if (defaultRoleOnly) { + const permissions = this.permissions?.find( + (p) => p.roleId === defaultRoleId! + )?.permissions; + return permissions || 0; + } + + const roleIds = [...(member?.roleIds || []), defaultRoleId]; + let permissions = 0; + for (let i = 0; i < this.permissions!.length; i++) { + const perm = this.permissions![i]!; + if (roleIds.includes(perm.roleId)) { + permissions = addBit(permissions, perm?.permissions); + } + } + + return permissions; +} + +function hasPermission( + this: Channel, + bitwise: Bitwise, + defaultRoleOnly = false, + userId?: string +) { + if (!this.serverId) return false; + const permissions = this.permissionBits(defaultRoleOnly, userId); + return hasBit(permissions, bitwise.bit); +} + +function canSendMessage(this: Channel, userId: string) { + const serverMembers = useServerMembers(); + const account = useAccount(); + + const member = this.serverId + ? serverMembers.get(this.serverId, userId) + : undefined; + const muted = + member?.muteExpireAt && new Date(member?.muteExpireAt) > new Date(); + const emailConfirmed = + account.user()?.id != userId || account.user()?.emailConfirmed; + + if (!emailConfirmed) { + return false; + } + if (!this.serverId) return true; + if (!member) return false; + if (serverMembers.hasPermission(member, ROLE_PERMISSIONS.ADMIN)) return true; + + if (muted) return false; + + if (!this.hasPermission(CHANNEL_PERMISSIONS.SEND_MESSAGE, false, userId)) { + return false; + } + + return serverMembers.hasPermission(member, ROLE_PERMISSIONS.SEND_MESSAGE); } function mentionCount(this: Channel) { @@ -80,14 +181,33 @@ function hasNotifications(this: Channel) { const account = useAccount(); const mentions = useMention(); const isAdminChannel = () => - hasBit(this.permissions || 0, CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit); + !this.hasPermission(CHANNEL_PERMISSIONS.PUBLIC_CHANNEL); + + const notifySettings = account.getRawNotificationSettings(this.id); + + if ( + notifySettings?.notificationPingMode === ServerNotificationPingMode.MUTE + ) { + return; + } + + if ( + notifySettings?.notificationPingMode == + ServerNotificationPingMode.MENTIONS_ONLY + ) { + const hasMentions = mentions.get(this.id)?.count; + return hasMentions ? "mention" : false; + } if (this.serverId && isAdminChannel()) { const member = serverMembers.get( this.serverId, account.user()?.id as string ); - const hasAdminPermission = member?.hasPermission(ROLE_PERMISSIONS.ADMIN); + const hasAdminPermission = serverMembers.hasPermission( + member!, + ROLE_PERMISSIONS.ADMIN + ); if (!hasAdminPermission) return false; } @@ -106,16 +226,33 @@ function recipient(this: Channel) { return users.get(this.recipientId!); } -function joinCall(this: Channel) { - const { setCurrentVoiceChannelId } = useVoiceUsers(); +async function joinCall(this: Channel, reconnect = false) { + if (isExperimentEnabled("RN_NATIVE_WEBRTC")() && reactNativeAPI()?.joinCall) { + reactNativeAPI()?.joinCall(this.id); + return; + } + const { setCurrentChannelId } = useVoiceUsers(); + await loadSimplePeer(); + if (getStorageBoolean(StorageKeys.voiceUseTurnServers, true)) { + await postGenerateCredential(); + } postJoinVoice(this.id, socketClient.id()!).then(() => { - setCurrentVoiceChannelId(this.id); + if (reconnect) return; + setCurrentChannelId(this.id, reconnect); + this.setCallJoinedAt(Date.now()); }); } function leaveCall(this: Channel) { - const { setCurrentVoiceChannelId } = useVoiceUsers(); + const { setCurrentChannelId, removeVoiceUser } = useVoiceUsers(); + const account = useAccount(); + if (!account.isAuthenticated()) { + setCurrentChannelId(null); + removeVoiceUser(this.id, account.user()?.id as string); + return; + } postLeaveVoice(this.id).then(() => { - setCurrentVoiceChannelId(null); + playSound(getCustomSound("CALL_LEAVE")); + setCurrentChannelId(null); }); } function update(this: Channel, update: Partial) { @@ -149,8 +286,40 @@ function updateLastSeen(this: Channel, timestamp?: number) { const deleteChannel = (channelId: string, serverId?: string) => runWithContext(() => { const messages = useMessages(); + const voice = useVoiceUsers(); + const voiceChannelId = voice.currentUser()?.channelId; + if (serverId) { + const servers = useServers(); + const defaultChannelId = servers.get(serverId)?.defaultChannelId; + const channel = get(channelId); + if (channel?.type === ChannelType.CATEGORY) { + const serverChannels = getChannelsByServerId(serverId, false, true); + batch(() => { + for (let i = 0; i < serverChannels.length; i++) { + const serverChannel = serverChannels[i]!; + if (serverChannel.categoryId === channelId) { + setChannels(serverChannel.id, "categoryId", undefined); + } + } + }); + } + if (defaultChannelId) { + const match = useMatch(() => "/app/servers/:serverId/:channelId")(); + const matchedChannelId = match?.params.channelId; + if (matchedChannelId === channelId) { + useNavigate()( + RouterEndpoints.SERVER_MESSAGES(serverId, defaultChannelId), + { replace: true } + ); + } + } + } batch(() => { + if (voiceChannelId && voiceChannelId === channelId) { + voice.setCurrentChannelId(null); + } + messages.deleteChannelMessages(channelId); setChannels(channelId, undefined); }); @@ -161,7 +330,9 @@ const get = (channelId?: string) => { return channels[channelId]; }; -const array = () => Object.values(channels) as Channel[]; +const array = createMemo(() => { + return Object.values(channels) as Channel[]; +}); const serverChannelsWithPerm = () => { const serverMembers = useServerMembers(); @@ -170,12 +341,14 @@ const serverChannelsWithPerm = () => { return array().filter((channel) => { if (!channel.serverId) return; const member = serverMembers.get(channel.serverId, account.user()?.id!); - const hasAdminPerm = member?.hasPermission(ROLE_PERMISSIONS.ADMIN); + const hasAdminPerm = serverMembers.hasPermission( + member!, + ROLE_PERMISSIONS.ADMIN + ); if (hasAdminPerm) return true; - const isPrivateChannel = hasBit( - channel?.permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit + const isPrivateChannel = !channel.hasPermission( + CHANNEL_PERMISSIONS.PUBLIC_CHANNEL ); return !isPrivateChannel; }); @@ -183,22 +356,27 @@ const serverChannelsWithPerm = () => { const getChannelsByServerId = ( serverId: string, - hidePrivateIfNoPerm = false + hidePrivateIfNoPerm = false, + showPrivateCategories = false ) => { if (!hidePrivateIfNoPerm) return array().filter((channel) => channel?.serverId === serverId); const serverMembers = useServerMembers(); const account = useAccount(); const member = serverMembers.get(serverId, account.user()?.id!); - const hasAdminPerm = member?.hasPermission(ROLE_PERMISSIONS.ADMIN); + const hasAdminPerm = serverMembers.hasPermission( + member!, + ROLE_PERMISSIONS.ADMIN + ); if (hasAdminPerm) return array().filter((channel) => channel?.serverId === serverId); return array().filter((channel) => { const isServerChannel = channel?.serverId === serverId; - const isPrivateChannel = hasBit( - channel?.permissions || 0, - CHANNEL_PERMISSIONS.PRIVATE_CHANNEL.bit + if (channel.type === ChannelType.CATEGORY && showPrivateCategories) + return isServerChannel; + const isPrivateChannel = !channel.hasPermission( + CHANNEL_PERMISSIONS.PUBLIC_CHANNEL ); return isServerChannel && !isPrivateChannel; }); @@ -207,9 +385,14 @@ const getChannelsByServerId = ( // if order field exists, sort by order, else, sort by created date const getSortedChannelsByServerId = ( serverId: string, - hidePrivateIfNoPerm = false + hidePrivateIfNoPerm = false, + showPrivateCategories = false ) => { - return getChannelsByServerId(serverId, hidePrivateIfNoPerm).sort((a, b) => { + return getChannelsByServerId( + serverId, + hidePrivateIfNoPerm, + showPrivateCategories + ).sort((a, b) => { if (a!.order && b!.order) { return a!.order - b!.order; } else { @@ -218,6 +401,17 @@ const getSortedChannelsByServerId = ( }); }; +// members that can view this channel +function membersWithChannelAccess(this: Channel) { + if (!this.serverId) return []; + const members = useServerMembers(); + + const serverMembers = members.array(this.serverId); + return serverMembers.filter((member) => + members.canViewChannel(member!, this.id) + ) as ServerMember[]; +} + const removeAllServerChannels = (serverId: string) => { const channelsArr = array(); batch(() => { @@ -242,6 +436,6 @@ export default function useChannels() { get, set, removeAllServerChannels, - serverChannelsWithPerm, + serverChannelsWithPerm }; } diff --git a/src/chat-api/store/useFriends.ts b/src/chat-api/store/useFriends.ts index de2713e18..299864e99 100644 --- a/src/chat-api/store/useFriends.ts +++ b/src/chat-api/store/useFriends.ts @@ -1,29 +1,28 @@ -import {createStore} from "solid-js/store"; +import { createStore } from "solid-js/store"; import { FriendStatus, RawFriend } from "../RawData"; -import { acceptFriendRequest, removeFriend, addFriend } from "../services/FriendService"; -import useUsers, { User } from "./useUsers"; - - +import { + acceptFriendRequest, + removeFriend, + addFriend +} from "../services/FriendService"; +import useUsers from "./useUsers"; export type Friend = Omit & { recipientId: string; recipient: typeof recipient; accept: (this: Friend) => Promise; remove: (this: Friend) => Promise; -} - +}; const [friends, setFriends] = createStore>({}); - const set = (friend: RawFriend) => { const users = useUsers(); users.set(friend.recipient); - const newFriend: Friend = { - ...friend, + ...friend, recipientId: friend.recipient.id, recipient, accept, @@ -33,23 +32,21 @@ const set = (friend: RawFriend) => { setFriends(friend.recipient.id, newFriend); }; -function recipient (this: Friend) { +function recipient(this: Friend) { const users = useUsers(); return users.get(this.recipientId); } async function remove(this: Friend) { - await removeFriend({friendId: this.recipientId}); + await removeFriend({ friendId: this.recipientId }); setFriends(this.recipientId, undefined!); } async function accept(this: Friend) { - await acceptFriendRequest({friendId: this.recipientId}); + await acceptFriendRequest({ friendId: this.recipientId }); setFriends(this.recipientId, "status", FriendStatus.FRIENDS); } - - const get = (userId: string) => friends[userId]; -const deleteFriend = (userId: string) => setFriends({[userId]: undefined}); +const deleteFriend = (userId: string) => setFriends({ [userId]: undefined }); const updateStatus = (userId: string, status: FriendStatus) => { if (!friends[userId]) return; @@ -57,7 +54,7 @@ const updateStatus = (userId: string, status: FriendStatus) => { }; const sendRequest = async (username: string, tag: string) => { - const friend = await addFriend({username, tag}); + const friend = await addFriend({ username, tag }); }; const hasBeenBlockedByMe = (userId: string) => { @@ -65,7 +62,6 @@ const hasBeenBlockedByMe = (userId: string) => { return friend?.status === FriendStatus.BLOCKED; }; - const array = () => Object.values(friends); export default function useFriends() { @@ -80,10 +76,8 @@ export default function useFriends() { }; } - - interface Test { id: string; recipientId: string; - test: string + test: string; } diff --git a/src/chat-api/store/useHeader.ts b/src/chat-api/store/useHeader.ts index d0b03513b..04b212f53 100644 --- a/src/chat-api/store/useHeader.ts +++ b/src/chat-api/store/useHeader.ts @@ -1,9 +1,5 @@ import { createSignal } from "solid-js"; - - - - export interface HeaderDetail { title: string; iconName?: string; @@ -18,16 +14,13 @@ const [details, setDetails] = createSignal({ title: "Nothing Selected" }); - const updateHeader = (header: HeaderDetail) => { setDetails(header); }; - - export default function useHeader() { return { updateHeader, details }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useInbox.ts b/src/chat-api/store/useInbox.ts index bc9783c5f..6f2ae8608 100644 --- a/src/chat-api/store/useInbox.ts +++ b/src/chat-api/store/useInbox.ts @@ -1,18 +1,15 @@ -import {createStore} from "solid-js/store"; +import { createStore } from "solid-js/store"; import { RawInboxWithoutChannel } from "../RawData"; import useChannels, { Channel } from "./useChannels"; import useMention from "./useMention"; import useUsers from "./useUsers"; - export type Inbox = RawInboxWithoutChannel & { - channel: () => Channel, -} - + channel: () => Channel; +}; const [inbox, setInbox] = createStore>({}); - const set = (item: RawInboxWithoutChannel) => { { const channels = useChannels(); @@ -20,23 +17,22 @@ const set = (item: RawInboxWithoutChannel) => { const channel = channels.get(item.channelId)!; const user = useUsers(); user.set(item.recipient); - + channel.setRecipientId(item.recipient.id); channel.recipient()?.setInboxChannelId(item.channelId); } - + setInbox(item.channelId, { ...item, channel }); }; -function channel (this: Inbox) { +function channel(this: Inbox) { const channels = useChannels(); return channels.get(this.channelId)!; } - const removeInbox = (channelId: string) => { setInbox(channelId, undefined!); }; @@ -45,7 +41,6 @@ const get = (userId: string) => { return inbox[userId]; }; - const array = () => Object.values(inbox); const mentions = useMention(); @@ -74,4 +69,4 @@ export default function useInbox() { notificationCount, removeInbox }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useMention.ts b/src/chat-api/store/useMention.ts index 9f67023a7..6116df412 100644 --- a/src/chat-api/store/useMention.ts +++ b/src/chat-api/store/useMention.ts @@ -1,4 +1,4 @@ -import {createStore} from "solid-js/store"; +import { createStore } from "solid-js/store"; import useChannels from "./useChannels"; import useAccount from "./useAccount"; import { ServerNotificationPingMode } from "../RawData"; @@ -8,20 +8,23 @@ export type Mention = { userId: string; count: number; serverId?: string; -} +}; // [channelId]: Mention -const [mentions, setMentions] = createStore>({}); - +const [mentions, setMentions] = createStore< + Record +>({}); const set = (mention: Mention) => { - const channels = useChannels(); const channel = channels.get(mention.channelId); const account = useAccount(); - + if (channel?.serverId) { - const notificationPingMode = account.getCombinedNotificationSettings(channel.serverId, mention.channelId)?.notificationPingMode; + const notificationPingMode = account.getCombinedNotificationSettings( + channel.serverId, + mention.channelId + )?.notificationPingMode; if (notificationPingMode === ServerNotificationPingMode.MUTE) return; } @@ -33,24 +36,29 @@ const get = (channelId: string) => mentions[channelId]; const getDmCount = (userId: string) => { const channels = useChannels(); - return array().find(m => { - const channel = channels.get(m?.channelId!); - return m?.userId === userId && (!channel || channel.recipientId); - })?.count || 0; + return ( + array().find((m) => { + if (m?.serverId) return; + const channel = channels.get(m?.channelId!); + return m?.userId === userId && (!channel || channel.recipientId); + })?.count || 0 + ); }; +const count = () => + array().reduce((acc, mention) => acc + (mention?.count || 0) || 0, 0); + const remove = (channelId: string) => { setMentions(channelId, undefined); }; - - export default function useMention() { return { array, set, + count, get, getDmCount, remove }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useMessages.ts b/src/chat-api/store/useMessages.ts index cff313fe4..39ff0b3c6 100644 --- a/src/chat-api/store/useMessages.ts +++ b/src/chat-api/store/useMessages.ts @@ -1,35 +1,32 @@ import env from "@/common/env"; import { createStore, produce, reconcile } from "solid-js/store"; -import { - MessageType, - RawMessage, - RawMessageReaction, - RawUser, -} from "../RawData"; +import { MessageType, RawMessage, RawMessageReaction } from "../RawData"; import { fetchMessages, postMessage, - updateMessage, + updateMessage } from "../services/MessageService"; import socketClient from "../socketClient"; import useAccount from "./useAccount"; import useChannelProperties from "./useChannelProperties"; import useChannels from "./useChannels"; -import { getGoogleAccessToken } from "../services/UserService"; +import { getGoogleDriveAccessToken } from "../services/UserService"; import { uploadFileGoogleDrive } from "@/common/driveAPI"; import { batch } from "solid-js"; +import { uploadAttachment } from "../services/nerimityCDNService"; const account = useAccount(); export enum MessageSentStatus { SENDING = 0, - FAILED = 1, + FAILED = 1 } export type Message = RawMessage & { tempId?: string; sentStatus?: MessageSentStatus; - uploadingAttachment?: { file: File; progress: number }; + uploadingAttachment?: { file: File; progress: number; speed?: string }; + local?: boolean; }; const [messages, setMessages] = createStore< @@ -44,27 +41,29 @@ const fetchAndStoreMessages = async (channelId: string, force = false) => { const newMessages = await fetchMessages(channelId); setMessages({ - [channelId]: newMessages, + [channelId]: newMessages }); }; const loadMoreTopAndStoreMessages = async ( channelId: string, beforeSet: () => void, - afterSet: (data: { hasMore: boolean }) => void + afterSet: (data: { hasMore: boolean; data: RawMessage[] }) => void ) => { const channelMessages = messages[channelId]!; const newMessages = await fetchMessages(channelId, { - beforeMessageId: channelMessages[0].id, + beforeMessageId: channelMessages[0].id }); const clamp = sliceEnd([...newMessages, ...channelMessages]); const hasMore = newMessages.length === env.MESSAGE_LIMIT; beforeSet(); - setMessages({ - [channelId]: clamp, - }); - afterSet({ hasMore }); + if (newMessages.length) { + setMessages({ + [channelId]: clamp + }); + } + afterSet({ hasMore, data: newMessages }); }; const loadMoreBottomAndStoreMessages = async ( @@ -74,14 +73,14 @@ const loadMoreBottomAndStoreMessages = async ( ) => { const channelMessages = messages[channelId]!; const newMessages = await fetchMessages(channelId, { - afterMessageId: channelMessages[channelMessages.length - 1].id, + afterMessageId: channelMessages[channelMessages.length - 1].id }); const clamp = sliceBeginning([...channelMessages, ...newMessages]); const hasMore = newMessages.length === env.MESSAGE_LIMIT; beforeSet(); setMessages({ - [channelId]: clamp, + [channelId]: clamp }); afterSet({ hasMore }); }; @@ -93,7 +92,7 @@ const loadAroundAndStoreMessages = async ( const newMessages = await fetchMessages(channelId, { aroundMessageId }); setMessages({ - [channelId]: newMessages, + [channelId]: newMessages }); }; @@ -116,13 +115,13 @@ const editAndStoreMessage = async ( if (messages[index].content === content) return; setMessages(channelId, index, { sentStatus: MessageSentStatus.SENDING, - content, + content }); await updateMessage({ channelId, messageId, - content, + content }).catch(() => { updateLocalMessage( { sentStatus: MessageSentStatus.FAILED }, @@ -143,19 +142,48 @@ const updateLocalMessage = async ( setMessages(channelId, index, message); }; +const silentRegex = /^@silent([\s]|$)/; + +const generateLocalId = () => `local-${Date.now()}-${Math.random()}`; + const sendAndStoreMessage = async (channelId: string, content?: string) => { const channels = useChannels(); const channelProperties = useChannelProperties(); const properties = channelProperties.get(channelId); - const tempMessageId = `${Date.now()}-${Math.random()}`; + const file = properties?.attachment?.file; + const tempMessageId = generateLocalId(); const channel = channels.get(channelId); + const htmlMode = properties?.htmlEnabled; + channelProperties.update(channelId, "htmlEnabled", false); + + if (properties?.selectedBotCommand && content) { + const args = content?.split(" "); + args[0] = `${args[0]}:${properties.selectedBotCommand.botUserId}`; + content = args.join(" "); + channelProperties.updateSelectedBotCommand(channelId, undefined); + } + + const isSilent = !!content && silentRegex.test(content); + + if (content && isSilent) { + if (content === "@silent") { + content = undefined; + if (!file) return; + } else { + content = content.replace(silentRegex, "").trim(); + if (!content && !file) return; + } + } + const user = account.user(); if (!user) return; const localMessage: Message = { - id: "", + buttons: [], + id: tempMessageId, tempId: tempMessageId, + silent: isSilent, channelId, content, createdAt: Date.now(), @@ -163,52 +191,60 @@ const sendAndStoreMessage = async (channelId: string, content?: string) => { type: MessageType.CONTENT, ...(!properties?.attachment ? undefined - : { uploadingAttachment: { file: properties.attachment, progress: 0 } }), + : { + uploadingAttachment: { + file: properties.attachment.file, + progress: 0 + } + }), reactions: [], + roleMentions: [], quotedMessages: [], replyMessages: properties?.replyToMessages.map((m) => ({ - replyToMessage: { ...m }, + replyToMessage: { ...m } })) || [], createdBy: { + bot: false, + profile: { + font: user.profile?.font, + clan: user.profile?.clan + }, id: user.id, username: user.username, tag: user.tag, badges: user.badges, hexColor: user.hexColor, - avatar: user.avatar, - }, + avatar: user.avatar + } }; - !properties?.moreBottomToLoad && + if (!properties?.moreBottomToLoad) { setMessages({ - [channelId]: sliceBeginning([...messages[channelId]!, localMessage]), + [channelId]: sliceBeginning([...messages[channelId]!, localMessage]) }); + } - const onUploadProgress = (percent: number) => { + const onUploadProgress = (percent: number, speed?: string) => { const messageIndex = messages[channelId]!.findIndex( (m) => m.tempId === tempMessageId ); if (messageIndex === -1) return; - setMessages( - channelId, - messageIndex, - "uploadingAttachment", - "progress", - percent - ); + setMessages(channelId, messageIndex, "uploadingAttachment", { + progress: percent, + speed + }); }; - const isImage = properties?.attachment?.type?.startsWith("image/"); - const isMoreThan12MB = - properties?.attachment && properties.attachment.size > 12 * 1024 * 1024; - const shouldUploadToGoogleDrive = !isImage || isMoreThan12MB; + const shouldUploadToGoogleDrive = + properties?.attachment?.uploadTo === "google_drive"; + const shouldUploadToNerimityCdn = + properties?.attachment?.uploadTo === "nerimity_cdn"; - const file = properties?.attachment; let googleDriveFileId: string | undefined; if (file && shouldUploadToGoogleDrive) { try { - const accessToken = await getGoogleAccessToken(); + const accessToken = await getGoogleDriveAccessToken(); const res = await uploadFileGoogleDrive( file, accessToken.accessToken, @@ -216,29 +252,10 @@ const sendAndStoreMessage = async (channelId: string, content?: string) => { ); googleDriveFileId = res.id; } catch (err: any) { - pushMessage(channelId, { - channelId: channelId, - createdAt: Date.now(), - createdBy: { - username: "Nerimity", - tag: "owo", - badges: 0, - hexColor: "0", - id: "0", - }, - reactions: [], - quotedMessages: [], - id: Math.random().toString(), - type: MessageType.CONTENT, - content: - "Failed to upload file to Google Drive. ```Error\n" + - err.message + - "\nbody: " + - content + - "\nFilename: " + - file.name + - "```", - }); + channelProperties.updateContent(channelId, content || ""); + channelProperties.update(channelId, "htmlEnabled", htmlMode); + channelProperties.setAttachment(channelId, file, "google_drive"); + pushFailedMessage(channelId, err.message || "Failed to upload File."); const index = messages[channelId]?.findIndex( (m) => m.tempId === tempMessageId ); @@ -252,17 +269,41 @@ const sendAndStoreMessage = async (channelId: string, content?: string) => { channelProperties.removeReplies(channelId); + let nerimityCdnFileId: string | undefined; + if (shouldUploadToNerimityCdn && file) { + const data = await uploadAttachment(channelId, { + file, + channelId, + onUploadProgress + }).catch((err) => { + channelProperties.updateContent(channelId, content || ""); + channelProperties.update(channelId, "htmlEnabled", htmlMode); + channelProperties.setAttachment(channelId, file, "nerimity_cdn"); + pushFailedMessage(channelId, err.message || "Failed to upload File. "); + const index = messages[channelId]?.findIndex( + (m) => m.tempId === tempMessageId + ); + setMessages(channelId, index!, "sentStatus", MessageSentStatus.FAILED); + return; + }); + if (!data) { + return; + } + nerimityCdnFileId = data.fileId; + } + const message: void | Message = await postMessage({ - content, + ...(htmlMode ? { htmlEmbed: content } : { content }), + silent: isSilent, channelId, socketId: socketClient.id(), replyToMessageIds, mentionReplies, - attachment: !shouldUploadToGoogleDrive ? properties?.attachment : undefined, + nerimityCdnFileId, googleDriveAttachment: googleDriveFileId ? { id: googleDriveFileId, mime: file?.type! } : undefined, - onUploadProgress, + onUploadProgress }).catch((err) => { console.log(err); @@ -270,37 +311,26 @@ const sendAndStoreMessage = async (channelId: string, content?: string) => { if (channel?.slowModeSeconds) { channelProperties.updateSlowDownMode(channelId, { startedAt: Date.now(), - ttl: err.ttl, + ttl: err.ttl }); } } - pushMessage(channelId, { - channelId: channelId, - createdAt: Date.now(), - createdBy: { - username: "Nerimity", - tag: "owo", - badges: 0, - hexColor: "0", - id: "0", - }, - reactions: [], - quotedMessages: [], - id: Math.random().toString(), - type: MessageType.CONTENT, - content: - "This message couldn't be sent. Try again later. ```Error\n" + - err.message + - "\nbody: " + - content + - "```", - }); + channelProperties.updateContent(channelId, content || ""); + channelProperties.update(channelId, "htmlEnabled", htmlMode); + if (properties?.attachment) { + channelProperties.setAttachment( + channelId, + file, + properties?.attachment?.uploadTo + ); + } + pushFailedMessage(channelId, err.message || "Failed to send message. "); }); if (message && channel?.slowModeSeconds) { channelProperties.updateSlowDownMode(message.channelId, { startedAt: message.createdAt, - ttl: channel.slowModeSeconds * 1000, + ttl: channel.slowModeSeconds * 1000 }); } channel?.updateLastSeen(message?.createdAt! + 1); @@ -311,24 +341,51 @@ const sendAndStoreMessage = async (channelId: string, content?: string) => { ); if (!message) { - !properties?.moreBottomToLoad && + if (!properties?.moreBottomToLoad) { setMessages(channelId, index!, "sentStatus", MessageSentStatus.FAILED); + } return; } message.tempId = tempMessageId; - !properties?.moreBottomToLoad && + if (!properties?.moreBottomToLoad) { setMessages(channelId, index!, reconcile(message, { key: "tempId" })); + } }; const pushMessage = (channelId: string, message: Message) => { if (!messages[channelId]) return; const channelProperties = useChannelProperties(); const properties = channelProperties.get(channelId); - !properties?.moreBottomToLoad && + if (!properties?.moreBottomToLoad) { setMessages({ - [channelId]: sliceBeginning([...messages[channelId]!, message]), + [channelId]: sliceBeginning([...messages[channelId]!, message]) }); + } +}; + +const pushFailedMessage = (channelId: string, content: string) => { + pushMessage(channelId, { + channelId: channelId, + createdAt: Date.now(), + buttons: [], + replyMessages: [], + createdBy: { + username: "Nerimity", + tag: "owo", + bot: true, + badges: 0, + hexColor: "0", + id: "0" + }, + reactions: [], + roleMentions: [], + quotedMessages: [], + id: generateLocalId(), + type: MessageType.CONTENT, + local: true, + content + }); }; const locallyRemoveMessage = (channelId: string, messageId: string) => { @@ -441,6 +498,6 @@ export default function useMessages() { get, updateLocalMessage, updateMessageReaction, - locallyRemoveServerMessagesBatch, + locallyRemoveServerMessagesBatch }; } diff --git a/src/chat-api/store/usePosts.ts b/src/chat-api/store/usePosts.ts index 1b0b56bca..bd8b1d604 100644 --- a/src/chat-api/store/usePosts.ts +++ b/src/chat-api/store/usePosts.ts @@ -1,12 +1,29 @@ import { batch } from "solid-js"; import { createStore, reconcile, unwrap } from "solid-js/store"; import { RawPost } from "../RawData"; -import { createPost, deletePost, editPost, getCommentPosts, getDiscoverPosts, getFeedPosts, getPost, getPosts, getPostsLiked, likePost, postVotePoll, unlikePost } from "../services/PostService"; +import { + createPost, + deletePost, + DiscoverSort, + editPost, + getCommentPosts, + getDiscoverPosts, + getFeedPosts, + getPost, + getPosts, + getPostsLiked, + likePost, + postVotePoll, + repostPost, + unlikePost +} from "../services/PostService"; import useAccount from "./useAccount"; - +import { toast } from "@/components/ui/custom-portal/CustomPortal"; export type Post = RawPost & { like(this: Post): Promise; + repostPost(this: Post): Promise; + unRepostPost(this: Post): Promise; delete(this: Post): Promise; unlike(this: Post): Promise; loadComments(this: Post): Promise; @@ -14,16 +31,19 @@ export type Post = RawPost & { editPost(this: Post, content: string): Promise; votePoll(this: Post, choiceId: string): Promise; - commentIds: string[] | undefined - cachedComments(this: Post): Post[] | undefined - submitReply(this: Post, opts: {content: string, attachment?: File}): Promise; -} + commentIds: string[] | undefined; + cachedComments(this: Post): Post[] | undefined; + submitReply( + this: Post, + opts: { content: string; attachment?: File; poll?: { choices: string[] } } + ): Promise; +}; interface State { userPostIds: Record; // userPostIds[userId] -> postIds - posts: Record - feedPostIds: string[] - discoverPostIds: string[] + posts: Record; + feedPostIds: string[]; + discoverPostIds: string[]; } const [state, setState] = createStore({ @@ -34,51 +54,97 @@ const [state, setState] = createStore({ }); export function usePosts() { - - const pushPost = (post: RawPost, userId?: string, prependUserPost = false) => { + const pushPost = ( + post: RawPost, + userId?: string, + prependUserPost = false + ) => { batch(() => { if (post.commentTo) { pushPost(post.commentTo); } setState("posts", post.id, { ...post, - commentTo: undefined, + commentTo: undefined, async delete() { await deletePost(this.id); - setState("posts", this.id, { ...this, content: undefined, deleted: true }); + setState("posts", this.id, { + ...this, + content: undefined, + deleted: true + }); + }, + + async repostPost() { + await repostPost(this.id).then((res) => { + setState("posts", this.id, res.repost); + }); + return this.id; + }, + async unRepostPost() { + const account = useAccount(); + const userId = account.user()?.id; + + const repostId = this.reposts.find( + (r) => r.createdBy.id === userId + )?.id; + + if (!repostId) return ""; + + await deletePost(repostId).then(() => { + setState( + "posts", + this.id, + reconcile({ + ...this, + reposts: this.reposts.filter((r) => r.createdBy.id !== userId), + _count: { + ...this._count, + reposts: this._count.reposts - 1 + } + }) + ); + }); + + return this.id; }, + async like() { const newPost = await likePost(this.id); - setState("posts", newPost.id, {...this, ...newPost}); + setState("posts", newPost.id, { ...this, ...newPost }); return this.id; }, async unlike() { const newPost = await unlikePost(this.id); - setState("posts", newPost.id, {...this, ...newPost}); + setState("posts", newPost.id, { ...this, ...newPost }); return this.id; }, async editPost(content: string) { const newPost = await editPost(this.id, content); - setState("posts", newPost.id, {...this, ...newPost}); + setState("posts", newPost.id, { ...this, ...newPost }); }, async votePoll(choiceId) { + await postVotePoll(this.id, this.poll?.id!, choiceId) + .then(() => { + const poll = structuredClone(unwrap(this.poll!)); + poll._count.votedUsers++; + const choiceIndex = poll.choices.findIndex( + (choice) => choice.id === choiceId + ); + if (choiceIndex === -1) return; + poll.choices[choiceIndex]!._count.votedUsers++; - await postVotePoll(this.id, this.poll?.id!, choiceId).then(() => { - - const poll = structuredClone(unwrap(this.poll!)); - poll._count.votedUsers++; - const choiceIndex = poll.choices.findIndex(choice => choice.id === choiceId); - if (choiceIndex === -1) return; - poll.choices[choiceIndex]!._count.votedUsers++; - - poll.votedUsers = [{pollChoiceId: choiceId}]; - - setState("posts", this.id, {...this, poll}); - }).catch(e => alert(e.message)); + poll.votedUsers = [{ pollChoiceId: choiceId }]; + setState("posts", this.id, { ...this, poll }); + }) + .catch((e) => toast(e.message)); }, async loadComments() { - const comments = await getCommentPosts({postId: this.id, limit: 30}); + const comments = await getCommentPosts({ + postId: this.id, + limit: 30 + }); setState("posts", this.id, "commentIds", reconcile([])); batch(() => { for (let index = 0; index < comments.length; index++) { @@ -88,7 +154,10 @@ export function usePosts() { setState("posts", this.id, "commentIds", []); } if (this.commentIds?.includes(comment.id)) continue; - setState("posts", this.id, "commentIds", [comment.id, ...this.commentIds!]); + setState("posts", this.id, "commentIds", [ + comment.id, + ...this.commentIds! + ]); } }); return comments; @@ -96,7 +165,11 @@ export function usePosts() { async loadMoreComments() { const afterId = this.commentIds?.at(-1); if (!afterId) return []; - const comments = await getCommentPosts({postId: this.id, limit: 30, afterId}); + const comments = await getCommentPosts({ + postId: this.id, + limit: 30, + afterId + }); comments.reverse(); batch(() => { for (let index = 0; index < comments.length; index++) { @@ -106,16 +179,24 @@ export function usePosts() { setState("posts", this.id, "commentIds", []); } if (this.commentIds?.includes(comment.id)) continue; - setState("posts", this.id, "commentIds", [...this.commentIds!, comment.id]); + setState("posts", this.id, "commentIds", [ + ...this.commentIds!, + comment.id + ]); } }); return comments; }, - async submitReply(opts: {content: string, attachment?: File}) { + async submitReply(opts: { content: string; attachment?: File }) { const account = useAccount(); const formattedContent = opts.content.trim(); - const post = await createPost({content: formattedContent, attachment: opts.attachment, replyToPostId: this.id}).catch((err) => { - alert(err.message); + const post = await createPost({ + content: formattedContent, + attachment: opts.attachment, + replyToPostId: this.id, + poll: opts.poll + }).catch((err) => { + toast(err.message); }); if (!post) return; batch(() => { @@ -123,44 +204,63 @@ export function usePosts() { setState("posts", this.id, "commentIds", []); } pushPost(post, account.user()?.id!); - setState("posts", this.id, "commentIds", [post.id, ...this.commentIds!]); + setState("posts", this.id, "commentIds", [ + post.id, + ...this.commentIds! + ]); }); + return true; }, cachedComments() { - return this.commentIds?.map(postId => state.posts[postId] as Post); + return this.commentIds?.map((postId) => state.posts[postId] as Post); } }); if (!userId) return; - if (!state.userPostIds[userId]) { setState("userPostIds", userId, []); } if (state.userPostIds[userId]?.includes(post.id)) return; if (prependUserPost) { - setState("userPostIds", userId, [...state.userPostIds[userId]!, post.id]); - } - else { - setState("userPostIds", userId, [post.id, ...state.userPostIds[userId]!]); + setState("userPostIds", userId, [ + ...state.userPostIds[userId]!, + post.id + ]); + } else { + setState("userPostIds", userId, [ + post.id, + ...state.userPostIds[userId]! + ]); } }); }; - const submitPost = async (opts: {content: string, file?: File, poll?: {choices: string[]}}) => { + const submitPost = async (opts: { + content: string; + file?: File; + poll?: { choices: string[] }; + }) => { const account = useAccount(); const formattedContent = opts.content.trim(); - const post = await createPost({content: formattedContent, attachment: opts.file, poll: opts.poll}).catch((err) => { - alert(err.message); + const post = await createPost({ + content: formattedContent, + attachment: opts.file, + poll: opts.poll + }).catch((err) => { + toast(err.message); }); if (!post) return; - pushPost(post, account.user()?.id!); - setState("feedPostIds", [post.id, ...state.feedPostIds]); - setState("discoverPostIds", [post.id, ...state.discoverPostIds]); + batch(() => { + pushPost(post, account.user()?.id!); + setState("feedPostIds", [post.id, ...state.feedPostIds]); + setState("discoverPostIds", [post.id, ...state.discoverPostIds]); + }); + return true; }; const fetchUserPosts = async (userId: string, withReplies?: boolean) => { setState("userPostIds", userId, []); - const posts = await getPosts({userId, withReplies}); + const posts = await getPosts({ userId, withReplies }); batch(() => { for (let i = 0; i < posts.length; i++) { const post = posts[i]; @@ -172,7 +272,7 @@ export function usePosts() { const fetchMoreUserPosts = async (userId: string, withReplies?: boolean) => { const afterId = state.userPostIds?.[userId]?.at(-1); if (!afterId) return []; - const posts = await getPosts({userId, withReplies, afterId}); + const posts = await getPosts({ userId, withReplies, afterId }); posts.reverse(); batch(() => { for (let i = 0; i < posts.length; i++) { @@ -195,7 +295,7 @@ export function usePosts() { const cachedUserPosts = (userId: string) => { const postIds = state.userPostIds?.[userId]; - return postIds?.map(postId => state.posts[postId] as Post); + return postIds?.map((postId) => state.posts[postId] as Post); }; const fetchAndPushPost = async (postId: string) => { if (state.posts[postId]) return state.posts[postId]; @@ -205,7 +305,6 @@ export function usePosts() { return state.posts[postId]; }; - const fetchFeed = async () => { setState("feedPostIds", []); const posts = await getFeedPosts(); @@ -222,7 +321,7 @@ export function usePosts() { const fetchMoreFeed = async () => { const afterId = state.feedPostIds?.at(-1); if (!afterId) return []; - const posts = await getFeedPosts({afterId}); + const posts = await getFeedPosts({ afterId }); batch(() => { for (let index = 0; index < posts.length; index++) { const post = posts[index]; @@ -233,12 +332,14 @@ export function usePosts() { return posts; }; - - const fetchDiscover = async () => { + const fetchDiscover = async ( + sort?: DiscoverSort, + abortSignal?: AbortSignal + ) => { setState("discoverPostIds", []); - const posts = await getDiscoverPosts(); + const posts = await getDiscoverPosts({ sort, abortSignal }); posts.reverse(); - + batch(() => { for (let index = 0; index < posts.length; index++) { const post = posts[index]; @@ -249,10 +350,10 @@ export function usePosts() { return posts; }; - const fetchMoreDiscover = async () => { + const fetchMoreDiscover = async (sort?: DiscoverSort) => { const afterId = state.discoverPostIds?.at(-1); if (!afterId) return []; - const posts = await getDiscoverPosts({afterId}); + const posts = await getDiscoverPosts({ afterId, sort }); posts.reverse(); batch(() => { for (let index = 0; index < posts.length; index++) { @@ -264,12 +365,26 @@ export function usePosts() { return posts; }; - - - - const cachedFeed = () => state.feedPostIds.map(id => state.posts[id] as Post); - const cachedDiscover = () => state.discoverPostIds.map(id => state.posts[id] as Post); + const cachedFeed = () => + state.feedPostIds.map((id) => state.posts[id] as Post); + const cachedDiscover = () => + state.discoverPostIds.map((id) => state.posts[id] as Post); const cachedPost = (postId: string) => state.posts[postId]; - return {cachedDiscover, fetchDiscover, fetchMoreDiscover, pushPost, fetchFeed, cachedFeed, fetchMoreFeed, cachedPost, fetchUserPosts, fetchMoreUserPosts, cachedUserPosts, submitPost, fetchAndPushPost, fetchUserLikedPosts}; + return { + cachedDiscover, + fetchDiscover, + fetchMoreDiscover, + pushPost, + fetchFeed, + cachedFeed, + fetchMoreFeed, + cachedPost, + fetchUserPosts, + fetchMoreUserPosts, + cachedUserPosts, + submitPost, + fetchAndPushPost, + fetchUserLikedPosts + }; } diff --git a/src/chat-api/store/useServerMembers.ts b/src/chat-api/store/useServerMembers.ts index b9fda9da9..5e8a1b3ed 100644 --- a/src/chat-api/store/useServerMembers.ts +++ b/src/chat-api/store/useServerMembers.ts @@ -1,30 +1,24 @@ -import {createStore, reconcile} from "solid-js/store"; -import { addBit, Bitwise, hasBit, ROLE_PERMISSIONS } from "../Bitwise"; +import { createStore, reconcile } from "solid-js/store"; +import { + addBit, + CHANNEL_PERMISSIONS, + hasBit, + ROLE_PERMISSIONS +} from "../Bitwise"; import { RawServerMember } from "../RawData"; import useServerRoles, { ServerRole } from "./useServerRoles"; -import useServers, { Server } from "./useServers"; -import useUsers, { User } from "./useUsers"; +import useServers from "./useServers"; +import useUsers from "./useUsers"; import useVoiceUsers from "./useVoiceUsers"; - +import useChannels from "./useChannels"; export type ServerMember = Omit & { - userId: string - user: () => User - server: () => Server - roles: (sorted?: boolean) => ServerRole[] - update: (this: ServerMember, update: Partial) => void; - hasRole: (this: ServerMember, roleId: string) => boolean | undefined; - permissions: () => number; - hasPermission: (this: ServerMember, bitwise: Bitwise, ignoreAdmin?: boolean, ignoreCreator?: boolean) => boolean | void; - topRole: () => ServerRole; - topRoleWithIcon: () => ServerRole | undefined; - roleColor: () => string; - unhiddenRole: () => ServerRole | undefined; - isServerCreator: () => boolean | undefined; -} - -const [serverMembers, setMember] = createStore | undefined>>({}); + userId: string; +}; +const [serverMembers, setMember] = createStore< + Record | undefined> +>({}); const set = (member: RawServerMember) => { const users = useUsers(); @@ -36,71 +30,53 @@ const set = (member: RawServerMember) => { const newMember: ServerMember = { ...member, - userId: member.user.id, - server, - user, - update, - roles, - hasRole, - isServerCreator, - topRole, - topRoleWithIcon, - roleColor, - unhiddenRole, - permissions, - hasPermission + userId: member.user.id }; - - + (newMember as unknown as { user: unknown }).user = undefined; setMember(member.serverId, member.user.id, reconcile(newMember)); }; - -function user(this: ServerMember) { - const users = useUsers(); - return users.get(this.userId); -} -function server(this: ServerMember) { - const servers = useServers(); - return servers.get(this.serverId)!; -} -function update(this: ServerMember, update: Partial) { - setMember(this.serverId, this.userId, update); +function update(member: ServerMember, update: Partial) { + setMember(member.serverId, member.userId, update); } -function isServerCreator(this: ServerMember) { +function isServerCreator(member: ServerMember | undefined) { const servers = useServers(); - const server = servers.get(this.serverId); + const server = servers.get(member?.serverId!); if (!server) return; - return server.createdById === this.userId; + return server.createdById === member?.userId; } -function hasRole(this: ServerMember, roleId: string) { +function hasRole(member: ServerMember | undefined, roleId: string) { const servers = useServers(); - const server = servers.get(this.serverId); + const server = servers.get(member?.serverId!); if (!server) return; - if (server.defaultRoleId === roleId) return true; - return this.roleIds.includes(roleId); + if (server?.defaultRoleId === roleId) return true; + return member?.roleIds.includes(roleId); } -function topRole(this: ServerMember) { +function topRole(member: ServerMember | undefined) { const servers = useServers(); - const roles = useServerRoles(); + const serverRoles = useServerRoles(); - const sortedRoles = this.roles().sort((a, b) => b?.order! - a?.order!); - const defaultRoleId = () => servers.get(this.serverId)?.defaultRoleId; - const defaultRole = () => roles.get(this.serverId, defaultRoleId()!); + const sortedRoles = roles(member).sort((a, b) => b?.order! - a?.order!); + const defaultRoleId = () => servers.get(member?.serverId!)?.defaultRoleId; + const defaultRole = () => + serverRoles.get(member?.serverId!, defaultRoleId()!); return sortedRoles[0] || defaultRole()!; } -function topRoleWithIcon (this: ServerMember) { +function topRoleWithIcon(member: ServerMember | undefined) { const servers = useServers(); - const roles = useServerRoles(); + const serverRoles = useServerRoles(); - const sortedRoles = this.roles().filter(r => r.icon).sort((a, b) => b?.order! - a?.order!); - const defaultRoleId = () => servers.get(this.serverId)?.defaultRoleId; - const defaultRole = () => roles.get(this.serverId, defaultRoleId()!); + const sortedRoles = roles(member) + .filter((r) => r?.icon) + .sort((a, b) => b?.order! - a?.order!); + const defaultRoleId = () => servers.get(member?.serverId!)?.defaultRoleId; + const defaultRole = () => + serverRoles.get(member?.serverId!, defaultRoleId()!); if (sortedRoles[0]?.icon) { return sortedRoles[0]; @@ -109,23 +85,61 @@ function topRoleWithIcon (this: ServerMember) { return dRole?.icon ? dRole : undefined; } -function roleColor (this: ServerMember) { - return this.topRole().hexColor || "white"; +interface Color { + hexColor: string; + gradient?: string; +} +function topRoleWithColor(member: ServerMember | undefined): { + hexColor: string; + gradient?: string; +} { + const highestRole = roles(member).reduce( + (best, current) => { + if (!current?.hexColor) return best; + + if (!best || (current.order ?? 0) > (best.order ?? 0)) { + return current; + } + + return best; + }, + null as ServerRole | null + ); + + if (highestRole?.hexColor) { + return highestRole as Color; + } + + const servers = useServers(); + const serverRoles = useServerRoles(); + + const defaultRoleId = servers.get(member?.serverId!)?.defaultRoleId; + + if (defaultRoleId) { + const defaultRole = serverRoles.get(member?.serverId!, defaultRoleId); + if (defaultRole?.hexColor) { + return defaultRole as Color; + } + } + + return { hexColor: "#fff" }; } -function unhiddenRole (this: ServerMember) { - const sortedRoles = this.roles().sort((a, b) => b?.order! - a?.order!); - return sortedRoles.find(role => !role?.hideRole); + +function unhiddenRole(member: ServerMember | undefined) { + const memberRoles = roles(member); + const sortedRoles = memberRoles.sort((a, b) => b?.order! - a?.order!); + return sortedRoles.find((role) => !role?.hideRole); } -function permissions (this: ServerMember) { +function permissions(member: ServerMember | undefined) { const servers = useServers(); - const roles = useServerRoles(); + const serverRoles = useServerRoles(); - const defaultRoleId = servers.get(this.serverId)?.defaultRoleId; - const defaultRole = roles.get(this.serverId, defaultRoleId!); + const defaultRoleId = servers.get(member?.serverId!)?.defaultRoleId; + const defaultRole = serverRoles.get(member?.serverId!, defaultRoleId!); let currentPermissions = defaultRole?.permissions || 0; - const memberRoles = this.roles(); + const memberRoles = roles(member); for (let i = 0; i < memberRoles.length; i++) { const role = memberRoles[i]; currentPermissions = addBit(currentPermissions, role?.permissions || 0); @@ -133,33 +147,59 @@ function permissions (this: ServerMember) { return currentPermissions; } -function hasPermission (this: ServerMember, bitwise: Bitwise, ignoreAdmin = false, ignoreCreator = false) { +function hasPermission( + member: ServerMember | undefined, + bitwise: { bit: number }, + ignoreAdmin = false, + ignoreCreator = false +) { + const memberPermissions = permissions(member); + const servers = useServers(); if (!ignoreCreator) { - if (this.server().createdById === this.userId) return true; + const server = servers.get(member?.serverId!); + if (server?.createdById === member?.userId) return true; } if (!ignoreAdmin) { - if (hasBit(this.permissions(), ROLE_PERMISSIONS.ADMIN.bit)) return true; + if (hasBit(memberPermissions, ROLE_PERMISSIONS.ADMIN.bit)) return true; } - return hasBit(this.permissions(), bitwise.bit); + return hasBit(memberPermissions, bitwise.bit); +} + +function canViewChannel(member: ServerMember | undefined, channelId: string) { + const channel = useChannels().get(channelId); + if (!channel) return false; + if (hasPermission(member, ROLE_PERMISSIONS.ADMIN)) return true; + + return channel.hasPermission( + CHANNEL_PERMISSIONS.PUBLIC_CHANNEL, + false, + member?.userId + ); } -function roles (this: ServerMember, sorted = false) { +function roles(member: ServerMember | undefined, sorted = false) { const serverRoles = useServerRoles(); - const roles = this.roleIds.map(id => serverRoles.get(this.serverId, id)!) || []; + const roles = + member?.roleIds + .map((id) => serverRoles.get(member.serverId, id)!) + .filter((r) => r) || []; if (!sorted) return roles; return roles.sort((a, b) => b?.order! - a?.order!); } - - - const remove = (serverId: string, userId: string) => { const users = useUsers(); + const channels = useChannels(); const voiceUsers = useVoiceUsers(); const user = users.get(userId); - + const voiceChannelId = user?.voiceChannelId; - voiceChannelId && voiceUsers.removeUserInVoice(voiceChannelId, userId); + if (voiceChannelId) { + const channel = channels.get(voiceChannelId); + if (serverId === channel?.serverId) { + voiceUsers.removeVoiceUser(voiceChannelId, userId); + } + } setMember(serverId, userId, undefined); }; @@ -168,8 +208,10 @@ const removeAllServerMembers = (serverId: string) => { setMember(serverId, undefined); }; -const array = (serverId: string) => Object.values(serverMembers?.[serverId] || []); -const get = (serverId: string, userId: string) => serverMembers[serverId]?.[userId]; +const array = (serverId: string) => + Object.values(serverMembers?.[serverId] || []); +const get = (serverId: string, userId: string) => + serverMembers[serverId]?.[userId]; const reset = () => { setMember(reconcile({})); @@ -182,6 +224,16 @@ export default function useServerMembers() { set, remove, removeAllServerMembers, - get + get, + update, + topRole, + topRoleWithIcon, + topRoleWithColor, + canViewChannel, + unhiddenRole, + hasRole, + isServerCreator, + roles, + hasPermission }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useServerRoles.ts b/src/chat-api/store/useServerRoles.ts index 7806d065b..ce1c42fee 100644 --- a/src/chat-api/store/useServerRoles.ts +++ b/src/chat-api/store/useServerRoles.ts @@ -2,28 +2,69 @@ import { batch } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { RawServerRole } from "../RawData"; import useServers from "./useServers"; +import { convertShorthandToLinearGradient } from "@/common/color"; -export type ServerRole = RawServerRole; +export type ServerRole = RawServerRole & { + gradient?: string; +}; // serverRoles[serverId][roleId] = Role -const [serverRoles, setServerRoles] = createStore | undefined>>({}); - +const [serverRoles, setServerRoles] = createStore< + Record | undefined> +>({}); + +const set = (serverId: string, _role: RawServerRole) => { + const role: ServerRole = { ..._role }; + + if (role.hexColor?.startsWith("lg")) { + const [converted] = convertShorthandToLinearGradient(role.hexColor); + if (converted) { + role.hexColor = converted.colors[0]!; + role.gradient = converted.gradient; + } + } -const set = (serverId: string, role: RawServerRole) => { if (!serverRoles[serverId]) { setServerRoles(serverId, {}); } setServerRoles(serverId, role.id, reconcile(role)); }; -const update = (serverId: string, roleId: string, update: Partial) => { +const update = ( + serverId: string, + roleId: string, + update: Partial +) => { if (!serverRoles[serverId]?.[roleId]) { return; } - setServerRoles(serverId, roleId, update); -}; -const addNewRole = (serverId: string, role: RawServerRole) => { + batch(() => { + setServerRoles(serverId, roleId, update); + + if (update.hexColor === null) { + setServerRoles(serverId, roleId, { + gradient: undefined, + hexColor: undefined + }); + } + + if (update?.hexColor) { + setServerRoles(serverId, roleId, "gradient", undefined); + if (update?.hexColor?.startsWith("lg")) { + const [converted] = convertShorthandToLinearGradient(update.hexColor); + if (converted) { + setServerRoles(serverId, roleId, { + hexColor: converted.colors[0]!, + gradient: converted.gradient + }); + } + } + } + }); +}; + +const addNewRole = (serverId: string, role: RawServerRole) => { const servers = useServers(); const server = servers.get(serverId); @@ -35,21 +76,19 @@ const addNewRole = (serverId: string, role: RawServerRole) => { const newOrder = roles.length - i; if (server?.defaultRoleId === role?.id) continue; setServerRoles(serverId, role?.id!, "order", newOrder + 1); - } set(serverId, role); }); - - }; - - const getAllByServerId = (serverId: string) => { - return Object.values(serverRoles[serverId] || {}).sort((a, b) => b!.order - a!.order); + return Object.values(serverRoles[serverId] || {}).sort( + (a, b) => b!.order - a!.order + ); }; -const get = (serverId: string, roleId: string) => serverRoles[serverId]?.[roleId]; +const get = (serverId: string, roleId: string) => + serverRoles[serverId]?.[roleId]; const deleteAllByServerId = (serverId: string) => { setServerRoles(serverId, undefined); @@ -65,11 +104,8 @@ const deleteRole = (serverId: string, roleId: string) => { setServerRoles(serverId, role?.id!, "order", newOrder); } }); - - }; - export default function useServerRoles() { return { set, @@ -80,4 +116,4 @@ export default function useServerRoles() { deleteRole, deleteAllByServerId }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useServers.ts b/src/chat-api/store/useServers.ts index 70da54251..e9d95e89c 100644 --- a/src/chat-api/store/useServers.ts +++ b/src/chat-api/store/useServers.ts @@ -1,28 +1,29 @@ -import env from "@/common/env"; -import {createStore} from "solid-js/store"; -import { ChannelType, RawCustomEmoji, RawServer, ServerNotificationPingMode } from "../RawData"; -import { deleteServer, leaveServer } from "../services/ServerService"; +import { createStore } from "solid-js/store"; +import { + RawBotCommand, + ChannelType, + RawCustomEmoji, + RawServer, + ServerNotificationPingMode +} from "../RawData"; +import { getServerBotCommands, leaveServer } from "../services/ServerService"; import useAccount from "./useAccount"; import useChannels from "./useChannels"; import useMention from "./useMention"; -import { createEffect, createMemo, createRoot } from "solid-js"; +import { createMemo } from "solid-js"; import { emojiShortcodeToUnicode } from "@/emoji"; export type Server = RawServer & { hasNotifications: () => boolean; - isCurrentUserCreator: () => boolean | undefined + isCurrentUserCreator: () => boolean | undefined; update: (this: Server, update: Partial) => void; leave: () => Promise; mentionCount: () => number; - avatarUrl(this: Server): string | null -} -const [servers, setServers] = createStore>({}); - - -export const avatarUrl = (item: {avatar?: string}): string | null => item?.avatar ? env.NERIMITY_CDN + item?.avatar : null; -export const bannerUrl = (item: {banner?: string}): string | null => item?.banner ? env.NERIMITY_CDN + item?.banner : null; - - + botCommands?: RawBotCommand[]; +}; +const [servers, setServers] = createStore>( + {} +); const set = (server: RawServer) => { const newServer: Server = { @@ -31,33 +32,54 @@ const set = (server: RawServer) => { update, leave, hasNotifications, - mentionCount, - avatarUrl: function () { - return avatarUrl(this); - } + mentionCount }; - + setServers(server.id, newServer); }; -function hasNotifications (this: Server) { +function hasNotifications(this: Server) { const channels = useChannels(); - const account = useAccount(); - - return channels.getChannelsByServerId(this.id).some(channel => { - const notificationPingMode = account.getCombinedNotificationSettings(this.id, channel.id)?.notificationPingMode; + const mentions = useMention(); + + // needed when using partial auth. + if ( + mentions.array().find((mention) => { + const notificationPingMode = account.getCombinedNotificationSettings( + this.id, + mention?.channelId + )?.notificationPingMode; + if (notificationPingMode === ServerNotificationPingMode.MUTE) + return false; + + return mention?.serverId === this.id; + }) + ) + return true; + + return channels.getChannelsByServerId(this.id).some((channel) => { + const notificationPingMode = account.getCombinedNotificationSettings( + this.id, + channel.id + )?.notificationPingMode; if (notificationPingMode === ServerNotificationPingMode.MUTE) return false; const hasNotification = channel!.hasNotifications(); - if (hasNotification !== "mention" && notificationPingMode === ServerNotificationPingMode.MENTIONS_ONLY ) return false; + if ( + hasNotification !== "mention" && + notificationPingMode === ServerNotificationPingMode.MENTIONS_ONLY + ) + return false; return hasNotification && channel?.type === ChannelType.SERVER_TEXT; }); } -function mentionCount (this: Server) { +function mentionCount(this: Server) { const mention = useMention(); let count = 0; - const mentions = mention.array().filter(mention => mention!.serverId === this.id); + const mentions = mention + .array() + .filter((mention) => mention!.serverId === this.id); for (let i = 0; i < mentions.length; i++) { const mention = mentions[i]; count += mention?.count || 0; @@ -65,17 +87,14 @@ function mentionCount (this: Server) { return count; } - -function leave (this: Server) { +function leave(this: Server) { return leaveServer(this.id); } -function update (this: Server, update: Partial) { +function update(this: Server, update: Partial) { setServers(this.id, update); } - - -const remove = (serverId: string) => { +const remove = (serverId: string) => { setServers(serverId, undefined); }; @@ -85,46 +104,89 @@ function isCurrentUserCreator(this: Server) { return this.createdById === account.user()?.id; } - const get = (serverId: string) => servers[serverId]; const array = () => Object.values(servers) as Server[]; -const orderedArray = () => { +const validServerFolders = createMemo(() => { + const account = useAccount(); + return account.user()?.serverFolders?.map((f) => { + return { + ...f, + serverIds: f.serverIds.filter((s) => get(s)) + }; + }); +}); + +const orderedArray = (includeFolders = false) => { const account = useAccount(); const serverIdsArray = account.user()?.orderedServerIds; const order: Record = {}; serverIdsArray?.forEach((a, i) => { order[a] = i; }); - - return array() + + const servers = array() .sort((a, b) => a.createdAt - b.createdAt) - .sort((a, b) => { - const orderA = order[a.id]; - const orderB = order[b.id]; - if (orderA === undefined) { - return -1; - } - if (orderB === undefined) { - return 1; - } - return orderA - orderB; - }); + .map((s) => ({ ...s, type: "server" as const })); + const folders = validServerFolders()?.map((f) => ({ + ...f, + type: "folder" as const + }))!; + + const serversAndFolders = includeFolders + ? [...servers, ...(folders || [])] + : servers; + + return serversAndFolders.sort((a, b) => { + const orderA = order[a.id]; + const orderB = order[b.id]; + if (orderA === undefined) { + return -1; + } + if (orderB === undefined) { + return 1; + } + return orderA - orderB; + }); }; - -const hasAllNotifications = () => { - return array().find(s => s?.hasNotifications()); +const hasAllNotifications = () => { + return array().find((s) => s?.hasNotifications()); }; -const emojis = createRoot(() => createMemo(() => orderedArray().map(s => (s.customEmojis.map(emoji => ({...emoji, serverId: s.id})))).flat())); +const emojis = createMemo(() => { + const arr = orderedArray(true); + const serverIdsInFolder = arr + .filter((item) => item.type === "folder") + .reduce>((set, item) => { + item.serverIds.forEach((id) => set.add(id)); + return set; + }, new Set()); + + const servers: Server[] = []; + + for (const item of arr) { + if (item.type === "folder") { + for (const id of item.serverIds) { + const server = get(id); + if (server) servers.push(server); + } + } else if (!serverIdsInFolder.has(item.id)) { + servers.push(item); + } + } + + return servers.flatMap((server) => + server.customEmojis.map((emoji) => ({ ...emoji, serverId: server.id })) + ); +}); -const emojisUpdatedDupName = createRoot(() => createMemo(() => { +const emojisUpdatedDupName = createMemo(() => { const uniqueNamedEmojis: RawCustomEmoji[] = []; - const counts: {[key: string]: number} = {}; - + const counts: { [key: string]: number } = {}; + for (let i = 0; i < emojis().length; i++) { - const emoji = emojis()[i]; + const emoji = emojis()[i]!; let count = counts[emoji.name] || 0; const hasEmojiShortcode = emojiShortcodeToUnicode(emoji.name); if (hasEmojiShortcode) count++; @@ -134,19 +196,27 @@ const emojisUpdatedDupName = createRoot(() => createMemo(() => { uniqueNamedEmojis.push({ ...emoji, name: newName }); } return uniqueNamedEmojis; -})); - +}); -const customEmojiNamesToEmoji = createRoot(() => createMemo(() => { - const obj: {[key: string]: RawCustomEmoji} = {}; +const customEmojiNamesToEmoji = createMemo(() => { + const obj: { [key: string]: RawCustomEmoji } = {}; for (let index = 0; index < emojisUpdatedDupName().length; index++) { - const emoji = emojisUpdatedDupName()[index]; - obj[emoji.name] = emoji; + const emoji = emojisUpdatedDupName()[index]!; + obj[emoji.name] = emoji; } return obj; -})); +}); +const fetchAndStoreServerBotCommands = async (serverId: string) => { + const server = servers[serverId]; + if (server?.botCommands) return; + const result = await getServerBotCommands(serverId).catch(() => ({ + commands: [] + })); + + setServers(serverId, "botCommands", result.commands); +}; export default function useServers() { return { @@ -158,6 +228,8 @@ export default function useServers() { set, hasNotifications: hasAllNotifications, orderedArray, - remove + validServerFolders, + remove, + fetchAndStoreServerBotCommands }; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useStore.ts b/src/chat-api/store/useStore.ts index 24c4cf47f..e0652df12 100644 --- a/src/chat-api/store/useStore.ts +++ b/src/chat-api/store/useStore.ts @@ -30,7 +30,6 @@ interface Store { posts: ReturnType; voiceUsers: ReturnType; tickets: ReturnType; - } let store: Store | null = null; @@ -75,4 +74,4 @@ export default function useStore() { store = obj; return obj; -} \ No newline at end of file +} diff --git a/src/chat-api/store/useUsers.ts b/src/chat-api/store/useUsers.ts index 2c7d94f80..dcadd10ad 100644 --- a/src/chat-api/store/useUsers.ts +++ b/src/chat-api/store/useUsers.ts @@ -3,13 +3,12 @@ import { ActivityStatus, FriendStatus, RawUser } from "../RawData"; import useInbox from "./useInbox"; import { closeDMChannelRequest, - openDMChannelRequest, + openDMChannelRequest } from "../services/UserService"; import useChannels from "./useChannels"; import RouterEndpoints from "../../common/RouterEndpoints"; import { useNavigate } from "solid-navigator"; import { runWithContext } from "@/common/runWithContext"; -import env from "@/common/env"; import useAccount from "./useAccount"; import { LastOnlineStatus } from "../events/connectionEventTypes"; import useFriends from "./useFriends"; @@ -19,22 +18,16 @@ export enum UserStatus { ONLINE = 1, LTP = 2, // Looking To Play AFK = 3, // Away from keyboard - DND = 4, // Do not disturb + DND = 4 // Do not disturb } export interface Presence { userId: string; custom?: string | null; status: UserStatus; - activity?: ActivityStatus; + activities?: ActivityStatus[]; } -export const avatarUrl = (item: { avatar?: string }): string | null => - item?.avatar ? env.NERIMITY_CDN + item?.avatar : null; - -export const bannerUrl = (item: { banner?: string }): string | null => - item?.banner ? env.NERIMITY_CDN + item?.banner : null; - export type User = { presence: () => Presence | undefined; inboxChannelId?: string; @@ -43,7 +36,6 @@ export type User = { setVoiceChannelId: (this: User, channelId: string | undefined) => void; openDM: (this: User) => Promise; closeDM: (this: User) => Promise; - avatarUrl(this: User): string | null; update(this: User, update: Partial): void; } & RawUser; @@ -61,10 +53,8 @@ const set = (user: RawUser) => setVoiceChannelId, openDM: openDMScoped, closeDM, - avatarUrl: function () { - return avatarUrl(this); - }, - update, + + update }; setUsers(user.id, newUser); @@ -88,7 +78,7 @@ function openDMScoped(this: User) { return openDM(this.id); } -const openDM = async (userId: string) => +const openDM = async (userId: string, messageId?: string) => runWithContext(async () => { const navigate = useNavigate(); const inbox = useInbox(); @@ -102,7 +92,10 @@ const openDM = async (userId: string) => inbox.set({ ...rawInbox, channelId: rawInbox.channel.id }); user()?.setInboxChannelId(rawInbox.channel.id); } - navigate(RouterEndpoints.INBOX_MESSAGES(inboxItem()?.channelId!)); + navigate( + RouterEndpoints.INBOX_MESSAGES(inboxItem()?.channelId!) + + (messageId ? "?messageId=" + messageId : "") + ); }); async function closeDM(this: User) { @@ -116,23 +109,25 @@ const array = () => Object.values(users); const setPresence = (userId: string, presence: Partial) => { const account = useAccount(); - if (account.user()?.id === userId) { + const isSelf = account.user()?.id === userId; + + if (isSelf) { account.setUser({ ...(presence.custom !== undefined ? { - customStatus: presence.custom || undefined, + customStatus: presence.custom || undefined } - : undefined), + : undefined) }); } const isOffline = presence.status !== undefined && presence.status === UserStatus.OFFLINE; - if (isOffline) { + if (isOffline && !isSelf) { setUserPresences(userId, undefined!); return; } if (presence.custom === null) presence.custom = undefined; - if (presence.activity === null) presence.activity = undefined; + setUserPresences(userId, { ...presence, userId }); }; @@ -183,6 +178,6 @@ export default function useUsers() { openDM, reset, presencesArray, - updateLastOnlineAt, + updateLastOnlineAt }; } diff --git a/src/chat-api/store/useVoiceUsers.ts b/src/chat-api/store/useVoiceUsers.ts index cc287a2e6..8ea4d2bee 100644 --- a/src/chat-api/store/useVoiceUsers.ts +++ b/src/chat-api/store/useVoiceUsers.ts @@ -1,478 +1,774 @@ -import { batch, createSignal } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; import { RawVoice } from "../RawData"; -import useUsers, { User } from "./useUsers"; +import { batch, createEffect, createMemo, createSignal, on } from "solid-js"; +import { getCachedCredentials } from "../services/VoiceService"; +import { emitVoiceSignal } from "../emits/voiceEmits"; + import type SimplePeer from "@thaunknown/simple-peer"; +import useUsers, { User } from "./useUsers"; +import { + getStorageBoolean, + getStorageObject, + getStorageString, + StorageKeys, + useVoiceInputMode +} from "@/common/localStorage"; import useAccount from "./useAccount"; -import { emitVoiceSignal } from "../emits/voiceEmits"; -import useChannels from "./useChannels"; -import env from "@/common/env"; import vad from "voice-activity-detection"; -import { getStorageString, StorageKeys } from "@/common/localStorage"; +import { downKeys, useGlobalKey } from "@/common/GlobalKey"; +import { arrayEquals } from "@/common/arrayEquals"; +import { LazySimplePeer } from "@/components/LazySimplePeer"; +import { log } from "@/common/logger"; + +const createIceServers = () => [ + ...(getStorageBoolean(StorageKeys.voiceUseTurnServers, true) + ? [getCachedCredentials()] + : []), + { + urls: ["stun:stun.l.google.com:19302"] + }, + { + urls: "stun:stun.relay.metered.ca:80" + }, + { + urls: "turn:a.relay.metered.ca:80", + username: "b9fafdffb3c428131bd9ae10", + credential: "DTk2mXfXv4kJYPvD" + }, + { + urls: "turn:a.relay.metered.ca:80?transport=tcp", + username: "b9fafdffb3c428131bd9ae10", + credential: "DTk2mXfXv4kJYPvD" + }, + { + urls: "turn:a.relay.metered.ca:443", + username: "b9fafdffb3c428131bd9ae10", + credential: "DTk2mXfXv4kJYPvD" + }, + { + urls: "turn:a.relay.metered.ca:443?transport=tcp", + username: "b9fafdffb3c428131bd9ae10", + credential: "DTk2mXfXv4kJYPvD" + } +]; + +type StreamWithTracks = { + stream: MediaStream; + tracks: MediaStreamTrack[]; +}; + +// cachedVolumes[userId] = volume +export const [cachedVolumes, setCachedVolumes] = createStore< + Record +>({}); +export type ConnectionStatus = "CONNECTED" | "DISCONNECTED" | "CONNECTING"; -interface VADInstance { - connect: () => void; - disconnect: () => void; - destroy: () => void; -} export type VoiceUser = RawVoice & { user: () => User; peer?: SimplePeer.Instance; - addSignal(this: VoiceUser, signal: SimplePeer.SignalData): void; - addPeer(this: VoiceUser, signal: SimplePeer.SignalData): void; - audioStream?: MediaStream; - videoStream?: MediaStream; - vad?: VADInstance; - voiceActivity: boolean; + streamWithTracks?: StreamWithTracks[]; + audio?: HTMLAudioElement; + voiceActivity?: boolean; + vadInstance?: ReturnType; + connectionStatus: ConnectionStatus; }; +type ChannelUsersMap = Record; +type VoiceUsersMap = Record; + // voiceUsers[channelId][userId] = VoiceUser -const [voiceUsers, setVoiceUsers] = createStore< - Record> ->({}); -const [currentVoiceChannelId, _setCurrentVoiceChannelId] = createSignal< - null | string ->(null); +const [voiceUsers, setVoiceUsers] = createStore({}); +const [deafened, setDeafened] = createStore({ + enabled: false, + wasMicEnabled: false +}); -interface LocalStreams { +interface CurrentVoiceUser { + channelId: string; audioStream: MediaStream | null; videoStream: MediaStream | null; - vadStream: MediaStream | null; - vad: VADInstance | null; + vadInstance?: ReturnType; + vadAudioStream?: MediaStream | null; } -const [localStreams, setLocalStreams] = createStore({ - audioStream: null, - videoStream: null, - vad: null, - vadStream: null, -}); +const [currentVoiceUser, setCurrentVoiceUser] = createSignal< + CurrentVoiceUser | undefined +>(undefined); + +const { start, stop } = useGlobalKey(); +const [voiceMode] = useVoiceInputMode(); + +createEffect( + on(currentVoiceUser, (current) => { + stop(); + if (!current?.channelId) return; + if (voiceMode() !== "PTT") return; + start(); + }) +); + +function toggleDeafen() { + const newDeafenEnabled = !deafened.enabled; + const currentUser = currentVoiceUser(); + if (!currentUser) return; + + const isMicEnabled = !!currentUser.audioStream; + + const voiceUsers = getVoiceUsersByChannelId(currentUser.channelId); + voiceUsers.forEach((voiceUser) => { + if (voiceUser.audio) { + voiceUser.audio.muted = newDeafenEnabled; + } + }); -const set = async (voiceUser: RawVoice) => { - const users = useUsers(); - const account = useAccount(); + if (!newDeafenEnabled && deafened.wasMicEnabled) { + enableMic(); + } - if (!voiceUsers[voiceUser.channelId]) { - setVoiceUsers(voiceUser.channelId, {}); + if (newDeafenEnabled && isMicEnabled) { + disableMic(); } - let peer: SimplePeer.Instance | undefined; + batch(() => { + setDeafened("enabled", newDeafenEnabled); + setDeafened("wasMicEnabled", isMicEnabled); + }); +} - { - const isSelf = voiceUser.userId === account.user()?.id; - const isInVoice = currentVoiceChannelId() === voiceUser.channelId; +const micTrack = createMemo(() => { + const current = currentVoiceUser(); + return current?.audioStream?.getAudioTracks()[0]; +}); - if (!isSelf && isInVoice) { - peer = await createPeer(voiceUser); +createEffect( + on( + () => downKeys.length, + () => { + const bound = getStorageObject(StorageKeys.PTTBoundKeys, []); + if (!bound.length) return; + const mic = micTrack(); + if (!mic) return; + const current = currentVoiceUser(); + if (!current) return; + + if (!arrayEquals(downKeys, bound)) { + mic.enabled = false; + setVoiceUsers(current.channelId, useAccount().user()?.id!, { + voiceActivity: false + }); + return; + } + mic.enabled = true; + setVoiceUsers(current.channelId, useAccount().user()?.id!, { + voiceActivity: true + }); } - - const user = users.get(voiceUser.userId); - user.setVoiceChannelId(voiceUser.channelId); + ) +); + +const setCurrentChannelId = (channelId: string | null, reconnect = false) => { + const current = currentVoiceUser(); + if (current?.channelId) { + removeAllPeers(current?.channelId); + current.vadInstance?.destroy(); + current.vadAudioStream?.getAudioTracks()[0]?.stop(); + batch(() => { + getVoiceUsersByChannelId(current.channelId).forEach((voiceUser) => { + voiceUser.vadInstance?.destroy(); + setVoiceUsers(current.channelId, voiceUser.userId, { + voiceActivity: false, + vadInstance: undefined + }); + }); + }); } + if (!channelId) { + setCurrentVoiceUser(undefined); + setDeafened("wasMicEnabled", false); - const newVoice: VoiceUser = { - ...voiceUser, - peer, - voiceActivity: false, - user, - addSignal, - addPeer, - }; + current?.audioStream?.getTracks().forEach((track) => { + track.stop(); + }); + current?.videoStream?.getTracks().forEach((track) => { + track.stop(); + }); - setVoiceUsers(voiceUser.channelId, voiceUser.userId, reconcile(newVoice)); + return; + } + if (!reconnect) { + setCurrentVoiceUser({ + channelId, + audioStream: null, + videoStream: null, + vadAudioStream: null, + vadInstance: undefined, + micMuted: true + }); + } }; -function addSignal(this: VoiceUser, signal: SimplePeer.SignalData) { - this.peer?.signal(signal); -} +const activeRemoteStream = (userId: string, kind: "audio" | "video") => { + const current = currentVoiceUser(); + if (!current) return; + const voiceUser = getVoiceUser(current.channelId, userId); + if (!voiceUser) return; + + if (kind === "audio") { + return voiceUser.streamWithTracks?.find((stream) => + stream.tracks.every((track) => track.kind === kind) + )?.stream; + } else { + return voiceUser.streamWithTracks?.find((stream) => + stream.tracks.find((track) => track.kind === kind) + )?.stream; + } +}; -async function addPeer(this: VoiceUser, signal: SimplePeer.SignalData) { - const user = this.user(); - console.log(user.username, "peer added"); - - const { default: LazySimplePeer } = await import("@thaunknown/simple-peer"); - - const peer = new LazySimplePeer({ - initiator: false, - trickle: true, - config: { - iceServers: [ - { - urls: ["stun:stun.l.google.com:19302"], - }, - { - urls: "stun:stun.relay.metered.ca:80", - }, - { - urls: "turn:a.relay.metered.ca:80", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:80?transport=tcp", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:443", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:443?transport=tcp", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - ], - }, - streams: [localStreams.audioStream, localStreams.videoStream].filter( - (stream) => stream - ) as MediaStream[], +const removeAllPeers = (channelIdToRemove?: string) => { + batch(() => { + for (const channelId in voiceUsers) { + for (const userId in voiceUsers[channelId]) { + const voiceUser = getVoiceUser(channelId, userId); + if (!voiceUser) continue; + if (channelIdToRemove && voiceUser?.channelId !== channelIdToRemove) + continue; + voiceUser.peer?.destroy(); + voiceUser.vadInstance?.destroy(); + voiceUser.audio?.remove(); + setVoiceUsers(channelId, userId, "peer", undefined); + setVoiceUsers(channelId, userId, "streamWithTracks", []); + } + } }); +}; - peer.on("signal", (signal) => { - emitVoiceSignal(this.channelId, this.userId, signal); - }); +const getVoiceUsersByChannelId = (id: string) => { + return Object.values(voiceUsers[id] || {}) as VoiceUser[]; +}; - peer.on("stream", (stream) => { - console.log("stream"); - onStream(this, stream); +const getVoiceUser = (channelId?: string, userId?: string) => { + return voiceUsers[channelId!]?.[userId!]; +}; +const removeVoiceUser = (channelId: string, userId: string) => { + const voiceUser = getVoiceUser(channelId, userId); + if (!voiceUser) return; + batch(() => { + voiceUser?.vadInstance?.destroy(); + voiceUser.peer?.destroy(); + voiceUser.audio?.remove(); + setVoiceUsers(channelId, userId, undefined); }); +}; - peer.on("connect", () => { - console.log("connect"); - }); - peer.on("end", () => { - user.username, console.log("end"); - }); - peer.on("error", (err) => { - console.log(err); - }); - peer.signal(signal); +const createVoiceUser = (rawVoice: RawVoice, reconnecting = false) => { + const account = useAccount(); + const users = useUsers(); - setVoiceUsers(this.channelId, this.userId, "peer", peer); -} + if (!voiceUsers[rawVoice.channelId]) { + setVoiceUsers(rawVoice.channelId, {}); + } + + { + const user = users.get(rawVoice.userId); + user?.setVoiceChannelId(rawVoice.channelId); + } + + const newVoiceUser: VoiceUser = { + connectionStatus: "CONNECTING", + ...rawVoice, + user, + streamWithTracks: [] + }; + + if (!reconnecting || rawVoice.userId !== account.user()?.id) { + setVoiceUsers(rawVoice.channelId, rawVoice.userId, newVoiceUser); + } + + const isCurrentUserInVoice = + rawVoice.channelId === currentVoiceUser()?.channelId; + + if (isCurrentUserInVoice) { + if (!reconnecting) { + createPeer(newVoiceUser); + } + } +}; function user(this: VoiceUser) { const users = useUsers(); - return users.get(this.userId); + return users.get(this.userId)!; } -const removeUserInVoice = (channelId: string, userId: string) => { - const voiceUser = voiceUsers[channelId][userId]; - batch(() => { - voiceUser?.vad?.destroy(); - voiceUser?.user().setVoiceChannelId(undefined); - voiceUser?.peer?.destroy(); - setVoiceUsers(channelId, userId, undefined); - }); -}; - -const getVoiceUsers = (channelId: string): VoiceUser[] => { - const account = useAccount(); - const selfUserId = account.user()?.id!; - return Object.values(voiceUsers[channelId] || {}).map((v) => { - if (v?.userId !== selfUserId) return v; - return { - ...v, - audioStream: localStreams.audioStream || undefined, - videoStream: localStreams.videoStream || undefined, - }; - }) as VoiceUser[]; +const updateConnectionStatus = ( + voiceUser: VoiceUser, + status: ConnectionStatus +) => { + try { + setVoiceUsers( + voiceUser.channelId, + voiceUser.userId, + "connectionStatus", + status + ); + } catch { + /* empty */ + } }; -const getVoiceUser = (channelId: string, userId: string) => { - return voiceUsers[channelId][userId]; -}; +const createPeer = (voiceUser: VoiceUser, signal?: SimplePeer.SignalData) => { + if (!LazySimplePeer) { + console.log("No LazySimplePeer"); + return; + } + const current = currentVoiceUser(); + if (voiceUser.userId === useAccount().user()?.id) return; + const initiator = !signal; -export async function createPeer(voiceUser: VoiceUser | RawVoice) { - const users = useUsers(); - const user = users.get(voiceUser.userId); - console.log(user.username, "peer created"); - - const { default: LazySimplePeer } = await import("@thaunknown/simple-peer"); - - const peer = new LazySimplePeer({ - initiator: true, - trickle: true, - config: { - iceServers: [ - { - urls: ["stun:stun.l.google.com:19302"], - }, - { - urls: "stun:stun.relay.metered.ca:80", - }, - { - urls: "turn:a.relay.metered.ca:80", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:80?transport=tcp", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:443", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - { - urls: "turn:a.relay.metered.ca:443?transport=tcp", - username: "b9fafdffb3c428131bd9ae10", - credential: "DTk2mXfXv4kJYPvD", - }, - ], - }, - streams: [localStreams.audioStream, localStreams.videoStream].filter( - (stream) => stream - ) as MediaStream[], - }); + const streams: MediaStream[] = []; + if (current?.audioStream) { + streams.push(current.audioStream); + } + if (current?.videoStream) { + streams.push(current.videoStream); + } - peer.on("signal", (signal) => { - emitVoiceSignal(voiceUser.channelId, voiceUser.userId, signal); - }); + const peer = + voiceUser.peer || + new LazySimplePeer({ + initiator, + trickle: true, + config: { + iceServers: createIceServers() + }, + streams + }); - peer.on("stream", (stream) => { - console.log("stream"); - onStream(voiceUser, stream); - }); + setVoiceUsers(voiceUser.channelId, voiceUser.userId, "peer", peer); peer.on("connect", () => { - console.log("connect"); + log("RTC", "Connected to", voiceUser.user().username + "!"); + updateConnectionStatus(voiceUser, "CONNECTED"); }); peer.on("end", () => { - console.log(user.username, "end"); + log("RTC", "Disconnected from", voiceUser.user().username + "."); + updateConnectionStatus(voiceUser, "DISCONNECTED"); + }); + peer.on("close", () => { + log("RTC", voiceUser.user().username, "disconnected."); + updateConnectionStatus(voiceUser, "DISCONNECTED"); }); peer.on("error", (err) => { - console.log(err); + console.error(err); }); - return peer; -} + peer.on("signal", (data) => { + emitVoiceSignal(voiceUser.channelId, voiceUser.userId, data); + }); + + peer.on("track", (track, stream) => { + const channelId = voiceUser.channelId; + const userId = voiceUser.userId; + + stream.onremovetrack = (event) => { + const newVoiceUser = getVoiceUser(channelId, userId); + const activeAudioStream = activeRemoteStream(userId, "audio"); + if (activeAudioStream?.id === stream.id) { + newVoiceUser?.vadInstance?.destroy(); + setVoiceUsers(channelId, userId, { + voiceActivity: false, + vadInstance: undefined + }); + } + + const streams = newVoiceUser?.streamWithTracks; + if (!streams) return; + const streamWithTracksIndex = streams.findIndex( + (s) => s.stream?.id === stream?.id + ); + const tracks = streams[streamWithTracksIndex]?.tracks; + + const newTracks = tracks?.filter((t) => t.id !== event.track.id); + if (!newTracks?.length) { + const newStreamWithTracks = streams.filter( + (s) => s.stream?.id !== stream?.id + ); + setVoiceUsers( + channelId, + userId, + "streamWithTracks", + newStreamWithTracks + ); + return; + } + + setVoiceUsers( + channelId, + userId, + "streamWithTracks", + streamWithTracksIndex, + "tracks", + newTracks + ); + }; + + pushVoiceUserTrack(voiceUser, track, stream); + + const newVoiceUser = getVoiceUser(channelId, userId); + + const streams = newVoiceUser?.streamWithTracks; + if (!streams) return; -function setLocalVAD(stream: MediaStream) { + const audio = newVoiceUser.audio || new Audio(); + const volume = cachedVolumes[userId] || 1; + audio.volume = volume; + audio.muted = deafened.enabled; + const deviceId = getStorageString(StorageKeys.outputDeviceId, undefined); + if (deviceId) { + audio.setSinkId(JSON.parse(deviceId)); + } + const activeAudio = activeRemoteStream(userId, "audio"); + + newVoiceUser.vadInstance?.destroy(); + + const vadInstance = createVadInstance(activeAudio, undefined, userId); + batch(() => { + setVoiceUsers(channelId, userId, "vadInstance", vadInstance); + + audio.srcObject = activeAudio || null; + audio.play(); + if (!audio.srcObject) { + setVoiceUsers(channelId, userId, "audio", undefined); + } + setVoiceUsers(channelId, userId, "audio", audio); + }); + }); + + if (signal) { + peer.signal(signal); + } +}; + +function createVadInstance( + vadStream?: MediaStream, + originalStream?: MediaStream, + userId?: string +) { + if (!vadStream) return; const account = useAccount(); + const originalStreamTrack = originalStream?.getAudioTracks()[0]; + + const current = currentVoiceUser(); + if (!current) return; const audioContext = new AudioContext(); - const track = localStreams.audioStream?.getAudioTracks()[0]!; - const vadInstance = vad(audioContext, stream, { - minNoiseLevel: 0.15, - noiseCaptureDuration: 0, + const vadInstance = vad(audioContext, vadStream, { + ...(!userId + ? { + minNoiseLevel: 0.15, + noiseCaptureDuration: 0 + } + : { + minNoiseLevel: 0, + noiseCaptureDuration: 100, + avgNoiseMultiplier: 0.1, + maxNoiseLevel: 0.01 + }), onVoiceStart: function () { - setVoiceUsers(currentVoiceChannelId()!, account.user()?.id!, { - voiceActivity: true, + setVoiceUsers(current.channelId, userId || account.user()?.id!, { + voiceActivity: true }); - track.enabled = true; + if (originalStreamTrack) { + originalStreamTrack.enabled = true; + } }, onVoiceStop: function () { - setVoiceUsers(currentVoiceChannelId()!, account.user()?.id!, { - voiceActivity: false, + setVoiceUsers(current.channelId, userId || account.user()?.id!, { + voiceActivity: false }); - track.enabled = false; - }, + if (originalStreamTrack) { + originalStreamTrack.enabled = false; + } + } }); - setLocalStreams({ vad: vadInstance }); + + return vadInstance; } -function setVAD(stream: MediaStream, voiceUser: RawVoice) { - const audioContext = new AudioContext(); - const vadInstance = vad(audioContext, stream, { - minNoiseLevel: 0, +const pushVoiceUserTrack = ( + voiceUser: VoiceUser, + track: MediaStreamTrack, + stream: MediaStream +) => { + const channelId = voiceUser.channelId; + const userId = voiceUser.userId; + + const newVoiceUser = getVoiceUser(channelId, userId); + + const streams = newVoiceUser?.streamWithTracks; + if (!streams) return; + + const streamWithTracksIndex = streams.findIndex( + (s) => s.stream.id === stream.id + ); + const streamWithTracks = streams[streamWithTracksIndex]; + + if (streamWithTracks && streamWithTracksIndex >= 0) { + setVoiceUsers( + channelId, + userId, + "streamWithTracks", + streamWithTracksIndex, + { + tracks: [...streamWithTracks.tracks, track] + } + ); + return; + } - noiseCaptureDuration: 0, - avgNoiseMultiplier: 0.1, - onVoiceStart: function () { - setVoiceUsers(voiceUser.channelId, voiceUser.userId, { - voiceActivity: true, - }); - }, - onVoiceStop: function () { - setVoiceUsers(voiceUser.channelId, voiceUser.userId, { - voiceActivity: false, - }); - }, + setVoiceUsers(channelId, userId, "streamWithTracks", streams.length, { + stream, + tracks: [track] }); - setVoiceUsers(voiceUser.channelId, voiceUser.userId, { vad: vadInstance }); -} +}; -const onStream = (voiceUser: VoiceUser | RawVoice, stream: MediaStream) => { - const videoTracks = stream.getVideoTracks(); - const streamType = videoTracks.length ? "videoStream" : "audioStream"; +const disableMic = () => { + const userId = useAccount().user()?.id!; + const current = currentVoiceUser(); + if (!current) return; - stream.onremovetrack = () => { - setVoiceUsers(voiceUser.channelId, voiceUser.userId, { - [streamType]: null, - voiceActivity: false, + if (current.audioStream) { + current.vadInstance?.destroy(); + + current.vadAudioStream?.getTracks().forEach((track) => { + track.stop(); + }); + removeStream(current.audioStream); + setCurrentVoiceUser({ ...current, audioStream: null }); + setVoiceUsers(current.channelId, userId, { + voiceActivity: false }); - stream.onremovetrack = null; - }; - if (streamType === "audioStream") { - setVAD(stream, voiceUser); - const mic = new Audio(); - const deviceId = getStorageString(StorageKeys.outputDeviceId, undefined); - if (deviceId) { - mic.setSinkId(JSON.parse(deviceId)); - } - mic.srcObject = stream; - mic.play(); + return; } - setVoiceUsers(voiceUser.channelId, voiceUser.userId, { - [streamType]: stream, - }); }; -const isLocalMicMuted = () => localStreams.audioStream === null; - -const toggleMic = async () => { +const getUserMic = (shouldLog = true) => { const deviceId = getStorageString(StorageKeys.inputDeviceId, undefined); - if (isLocalMicMuted()) { - const stream = await navigator.mediaDevices.getUserMedia({ - audio: !deviceId ? true : { deviceId: JSON.parse(deviceId) }, - video: false, + const constraints = getStorageObject(StorageKeys.voiceMicConstraints, { + echo: true, + noise: true, + gain: true + }); + + const audioConstraints: MediaTrackConstraints = { + echoCancellation: constraints.echo, + noiseSuppression: constraints.noise, + autoGainControl: constraints.gain + }; + + const rtcLog = (...args: unknown[]) => { + if (shouldLog) { + log("RTC", ...args); + } + }; + + if (!deviceId) { + rtcLog("Using Default Microphone"); + return navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: false }); - const vadStream = await navigator.mediaDevices.getUserMedia({ - audio: !deviceId ? true : { deviceId: JSON.parse(deviceId) }, - video: false, + } + return navigator.mediaDevices + .getUserMedia({ + audio: { deviceId: { exact: JSON.parse(deviceId) } }, + video: false + }) + .then((stream) => { + rtcLog("Using Microphone with deviceId", JSON.parse(deviceId)); + return stream; + }) + .catch(() => { + rtcLog( + "RTC", + "Failed to get microphone with deviceId", + JSON.parse(deviceId), + "Falling back to default microphone" + ); + return navigator.mediaDevices.getUserMedia({ + audio: audioConstraints, + video: false + }); }); +}; - setLocalStreams({ audioStream: stream, vadStream }); - setLocalVAD(vadStream); - sendStreamToPeer(stream, "audio"); +const enableMic = async () => { + const current = currentVoiceUser(); + if (!current) return; + + if (current.audioStream) { return; } - const account = useAccount(); - localStreams.vadStream?.getAudioTracks()[0].stop(); - localStreams.vad?.destroy(); - stopStream(localStreams.audioStream!); - setLocalStreams({ audioStream: null }); - setVoiceUsers(currentVoiceChannelId()!, account.user()?.id!, { - voiceActivity: false, + const stream = await getUserMic(); + + let vadStream: MediaStream | undefined; + let vadInstance: ReturnType | undefined; + + if (voiceMode() === "OPEN") { + setVoiceUsers(current.channelId, useAccount().user()?.id!, { + voiceActivity: true + }); + } + + if (voiceMode() !== "OPEN") { + stream.getAudioTracks()[0]!.enabled = false; + } + + if (voiceMode() === "VOICE_ACTIVITY") { + vadStream = await getUserMic(false); + vadInstance = createVadInstance(vadStream, stream); + } + + addStreamToPeers(stream); + + setCurrentVoiceUser({ + ...current, + audioStream: stream, + vadInstance, + vadAudioStream: vadStream }); }; +const toggleMic = async () => { + const current = currentVoiceUser(); + if (!current) return; + + if (current.audioStream) { + disableMic(); + return; + } + enableMic(); +}; + const setVideoStream = (stream: MediaStream | null) => { - if (localStreams.videoStream) { - localStreams.videoStream.getTracks().forEach((t) => t.stop()); - removeStreamFromPeer(localStreams.videoStream); + const current = currentVoiceUser(); + if (!current) return; + if (current.videoStream) { + removeStream(current.videoStream); } - setLocalStreams({ videoStream: stream }); + setCurrentVoiceUser({ ...current, videoStream: stream }); if (!stream) return; - sendStreamToPeer(stream, "video"); + addStreamToPeers(stream); - const videoTrack = stream.getVideoTracks()[0]; + const videoTrack = stream.getVideoTracks()[0]!; videoTrack.onended = () => { - stopStream(stream); - setLocalStreams({ videoStream: null }); + removeStream(stream); + setCurrentVoiceUser({ ...current, videoStream: null }); videoTrack.onended = null; }; }; -const micEnabled = (channelId: string, userId: string) => { - const account = useAccount(); - if (account.user()?.id === userId) { - return !!localStreams.audioStream; - } - return !!voiceUsers[channelId][userId]?.audioStream; -}; -const videoEnabled = (channelId: string, userId: string) => { - const account = useAccount(); - if (account.user()?.id === userId) { - return !!localStreams.videoStream; - } - return !!voiceUsers[channelId][userId]?.videoStream; +const removeStream = (stream: MediaStream) => { + removeStreamFromPeers(stream); + stream.getTracks().forEach((track) => { + track.stop(); + }); }; -const sendStreamToPeer = (stream: MediaStream, type: "audio" | "video") => { - console.log("sending stream..."); +const addStreamToPeers = (stream: MediaStream) => { + const current = currentVoiceUser(); + if (!current) return; + const voiceUsers = getVoiceUsersByChannelId(current.channelId); - const voiceUsers = getVoiceUsers(currentVoiceChannelId()!); - for (let i = 0; i < voiceUsers.length; i++) { - const voiceUser = voiceUsers[i]; - voiceUser?.peer?.addStream(stream); - } -}; -const removeStreamFromPeer = (stream: MediaStream) => { - console.log("removing stream..."); - const voiceUsers = getVoiceUsers(currentVoiceChannelId()!); - for (let i = 0; i < voiceUsers.length; i++) { - const voiceUser = voiceUsers[i]; - voiceUser?.peer?.removeStream(stream); - } -}; - -const stopStream = (mediaStream: MediaStream) => { - removeStreamFromPeer(mediaStream); - mediaStream.getTracks().forEach((track) => track.stop()); + voiceUsers.forEach((voiceUser) => { + voiceUser.peer?.addStream(stream); + }); }; -const setCurrentVoiceChannelId = (channelId: string | null) => { - const channels = useChannels(); +const removeStreamFromPeers = (stream: MediaStream) => { + const current = currentVoiceUser(); + if (!current) return; + const voiceUsers = getVoiceUsersByChannelId(current.channelId); - const oldChannelId = currentVoiceChannelId(); - if (oldChannelId) { - const channel = channels.get(oldChannelId); - channel?.setCallJoinedAt(undefined); - } - - const channel = channels.get(channelId!); - channel?.setCallJoinedAt(Date.now()); - - const voiceUsers = getVoiceUsers(currentVoiceChannelId()!); - _setCurrentVoiceChannelId(channelId); + voiceUsers.forEach((voiceUser) => { + voiceUser.peer?.removeStream(stream); + }); +}; - localStreams.videoStream && stopStream(localStreams.videoStream); - if (localStreams.audioStream) { - localStreams.vadStream?.getAudioTracks()[0].stop(); - localStreams.vad?.destroy(); - stopStream(localStreams.audioStream); +const signal = (voiceUser: VoiceUser, signal: SimplePeer.SignalData) => { + if (!voiceUser.peer) { + console.error("No peer for voice user", voiceUser); + return; } - setLocalStreams({ videoStream: null, audioStream: null }); - if (!voiceUsers) return; - - batch(() => { - voiceUsers.forEach((voiceUser) => { - voiceUser?.peer?.destroy(); - voiceUser?.vad?.destroy(); - setVoiceUsers(voiceUser?.channelId!, voiceUser?.userId!, { - peer: undefined, - audioStream: undefined, - videoStream: undefined, - vad: undefined, - voiceActivity: false, - }); - }); - }); + voiceUser.peer.signal(signal); }; function resetAll() { + const account = useAccount(); + const current = currentVoiceUser(); batch(() => { - if (currentVoiceChannelId()) { - setCurrentVoiceChannelId(null); + removeAllPeers(); + // setCurrentVoiceUser(undefined); + + if (current) { + const currentVoiceUser = getVoiceUser( + current.channelId, + account.user()?.id! + ); + if (currentVoiceUser) { + setVoiceUsers( + reconcile({ + [current.channelId]: { [account.user()?.id!]: currentVoiceUser } + }) + ); + } + } else { + setVoiceUsers(reconcile({})); } - setVoiceUsers(reconcile({})); }); } +const micEnabled = (userId: string) => { + const account = useAccount(); + if (account.user()?.id === userId) { + const currentUser = currentVoiceUser(); + return !!currentUser?.audioStream; + } + return activeRemoteStream(userId, "audio"); +}; + +const videoEnabled = (userId: string) => { + const account = useAccount(); + if (account.user()?.id === userId) { + const currentUser = currentVoiceUser(); + return currentUser?.videoStream; + } + return activeRemoteStream(userId, "video"); +}; + export default function useVoiceUsers() { return { - set, + createPeer, + createVoiceUser, getVoiceUser, - getVoiceUsers, - removeUserInVoice, - currentVoiceChannelId, - setCurrentVoiceChannelId, - isLocalMicMuted, - micEnabled, + getVoiceUsersByChannelId, + signal, + removeVoiceUser, + setCurrentChannelId, + currentUser: currentVoiceUser, + activeRemoteStream, videoEnabled, toggleMic, setVideoStream, resetAll, - localStreams, + + isLocalMicMuted: () => !currentVoiceUser()?.audioStream, + + micEnabled, + toggleDeafen, + deafened }; } diff --git a/src/chat-api/useJoinServer.ts b/src/chat-api/useJoinServer.ts new file mode 100644 index 000000000..bb00daf70 --- /dev/null +++ b/src/chat-api/useJoinServer.ts @@ -0,0 +1,63 @@ +import RouterEndpoints from "@/common/RouterEndpoints"; +import { createEffect, createSignal } from "solid-js"; +import { useNavigate } from "solid-navigator"; +import useStore from "./store/useStore"; +import { toast } from "@/components/ui/custom-portal/CustomPortal"; +import { + joinPublicServer, + joinServerByInviteCode +} from "./services/ServerService"; + +export const useJoinServer = () => { + const [serverId, setServerId] = createSignal(null); + const [joining, setJoining] = createSignal(false); + + const navigate = useNavigate(); + const store = useStore(); + + const cachedServer = () => { + return store.servers.get(serverId()!); + }; + + createEffect(() => { + const hasJoined = joining() && cachedServer(); + if (!hasJoined) return; + + cachedServer()?.update({ + joinedThisSession: true + }); + + const hasWelcomeQuestions = cachedServer()!._count?.welcomeQuestions; + const defaultChannelId = cachedServer()!.defaultChannelId; + + const route = RouterEndpoints.SERVER_MESSAGES( + cachedServer()!.id, + hasWelcomeQuestions ? "welcome" : defaultChannelId! + ); + + navigate(route); + }); + + const joinByInviteCode = async (code: string, serverId: string) => { + if (joining()) return; + setServerId(serverId); + setJoining(true); + + await joinServerByInviteCode(code).catch((err) => { + toast(err.message); + setJoining(false); + }); + }; + const joinPublicById = async (serverId: string) => { + if (joining()) return; + setServerId(serverId); + setJoining(true); + + await joinPublicServer(serverId).catch((err) => { + toast(err.message); + setJoining(false); + }); + }; + + return { joining, joinPublicById, joinByInviteCode }; +}; diff --git a/src/common/AsyncFunctionQueue.ts b/src/common/AsyncFunctionQueue.ts new file mode 100644 index 000000000..0b3484457 --- /dev/null +++ b/src/common/AsyncFunctionQueue.ts @@ -0,0 +1,116 @@ +/** + * Interface for an item in the queue. + * @template T The type of the result that the async function resolves to. + */ +interface QueueItem { + asyncFunc: () => Promise; + resolve: (value: T | PromiseLike) => void; + reject: (reason?: unknown) => void; +} + +/** + * A simple and fast async function queue for web environments, with TypeScript types. + * It ensures that asynchronous functions are executed one after another in the order they were added. + */ +export class AsyncFunctionQueue { + /** + * The array that holds the queue items. + * Using `unknown` is a type-safe alternative to `any`. + * Type safety for individual items is handled by the generic `add` method. + * @private + */ + private queue: QueueItem[] = []; + + /** + * A flag to indicate whether the queue is currently processing an item. + * @private + */ + private isProcessing: boolean = false; + + /** + * Adds an async function to the queue and returns a promise that resolves with the function's return value + * or rejects with its error. + * @template T The type of the result that the async function resolves to. + * @param {() => Promise} asyncFunc - An async function to be added to the queue. It must be a function that returns a Promise. + * @returns {Promise} A promise that resolves or rejects when the added function is executed. + */ + public add(asyncFunc: () => Promise): Promise { + return new Promise((resolve, reject) => { + // Push the function along with its resolve/reject handlers to the queue. + // We cast here to satisfy TypeScript's strict checks on function parameter contravariance. + // This is safe because we ensure that the `resolve` function for a given `asyncFunc` + // is always called with the result of that specific function. + this.queue.push({ + asyncFunc, + resolve, + reject + } as QueueItem); + + // If not already processing, start processing the queue. + if (!this.isProcessing) { + this._processNext(); + } + }); + } + + /** + * Processes the next item in the queue. + * This is a private method and should not be called directly. + * @private + */ + private async _processNext(): Promise { + // If there's nothing in the queue, stop processing. + if (this.queue.length === 0) { + this.isProcessing = false; + return; + } + + // Set the flag to indicate that processing has started. + this.isProcessing = true; + + // Get the next item from the front of the queue. + const item = this.queue.shift(); + + if (!item) { + // This case should not be hit due to the length check, but it's good practice for type safety. + this.isProcessing = false; + return; + } + + try { + // Execute the async function. + const result = await item.asyncFunc(); + // Resolve the promise associated with this function. + item.resolve(result); + } catch (error: unknown) { + // Reject the promise if the function throws an error. + item.reject(error); + console.error("An error occurred in the async queue:", error); + } finally { + // After the function is done (either resolved or rejected), + // recursively call _processNext to handle the next item in the queue. + // Using requestAnimationFrame or setTimeout can prevent potential stack overflow with a very long queue of synchronous tasks. + if (typeof requestAnimationFrame !== "undefined") { + requestAnimationFrame(() => this._processNext()); + } else { + setTimeout(() => this._processNext(), 0); + } + } + } + + /** + * Gets the current size of the queue. + * @returns {number} The number of items currently in the queue. + */ + public get size(): number { + return this.queue.length; + } + + /** + * Checks if the queue is currently processing an item. + * @returns {boolean} True if processing, false otherwise. + */ + public get isBusy(): boolean { + return this.isProcessing; + } +} diff --git a/src/common/BrowserTitle.ts b/src/common/BrowserTitle.ts index 4bf05cadb..be23f5a12 100644 --- a/src/common/BrowserTitle.ts +++ b/src/common/BrowserTitle.ts @@ -1,34 +1,24 @@ import { electronWindowAPI } from "./Electron"; -import env from "./env"; -let title = ""; -let alert = false; +let alert: boolean | null = null; +let count = 0; -export const updateTitle = (newTitle: string) => { - if (title === newTitle) return; - title = newTitle; - update(); -}; -export const updateTitleAlert = (newAlert: boolean) => { - if (newAlert === alert) return; +export const updateTitleAlert = (newAlert: boolean, newCount?: number) => { alert = newAlert; + if (newCount !== undefined) count = newCount; update(); }; const update = () => { - if (title) { - document.title = `${title} - Nerimity - ${env.DEV_MODE ? " - DEV" : ""}`; - } - else { - document.title = "Nerimity" + (env.DEV_MODE ? " - DEV" : ""); - } const link = document.querySelector("link[rel~='icon']") as HTMLLinkElement; if (alert) { link.href = "/favicon-alert.ico"; - } - else { + } else { link.href = "/favicon.ico"; } - electronWindowAPI()?.setNotification(alert); + electronWindowAPI()?.setNotification(alert || false, count); }; +window.addEventListener("focus", () => { + update(); +}); diff --git a/src/common/Delay.tsx b/src/common/Delay.tsx index a476423e9..6208953da 100644 --- a/src/common/Delay.tsx +++ b/src/common/Delay.tsx @@ -1,6 +1,6 @@ import { JSXElement, Show, createSignal, onCleanup } from "solid-js"; -export function Delay(props: {ms?: number, children: JSXElement}) { +export function Delay(props: { ms?: number; children: JSXElement }) { const [show, setShow] = createSignal(false); const interval = setTimeout(() => { setShow(true); @@ -10,7 +10,5 @@ export function Delay(props: {ms?: number, children: JSXElement}) { clearInterval(interval); }); - return ( - {props.children} - ); -} \ No newline at end of file + return {props.children}; +} diff --git a/src/common/Electron.ts b/src/common/Electron.ts index 7916cdbe6..82ebbb6c1 100644 --- a/src/common/Electron.ts +++ b/src/common/Electron.ts @@ -10,8 +10,9 @@ export interface Program { name: string; filename: string; } -export type ProgramWithAction = Program & { +export type ProgramWithExtras = Program & { action: string; + emoji?: string; }; export interface RPC { @@ -19,13 +20,30 @@ export interface RPC { action: string; imgSrc?: string; title?: string; + subtitle?: string; + link?: string; startedAt?: number; + endsAt?: number; + speed?: number; + updatedAt?: number; + emoji?: string; } export const [spellcheckSuggestions, setSpellcheckSuggestions] = createSignal< string[] >([]); +type KeyState = "DOWN" | "UP"; + +interface GlobalKeyEvent { + event: { + name: string; + vKey: number; + state: KeyState; + }; + down: Record; +} + interface WindowAPI { isElectron: boolean; minimize(): void; @@ -35,11 +53,18 @@ interface WindowAPI { getAutostart(): Promise; setAutostart(value: boolean): void; + getHardwareAccelerationDisabled(): Promise; + setHardwareAccelerationDisabled(value: boolean): void; + + getCustomTitlebarDisabled(): Promise; + setCustomTitlebarDisabled(value: boolean): void; + getAutostartMinimized(): Promise; setAutostartMinimized(value: boolean): void; - setNotification(value: boolean): void; + setNotification(value: boolean, count?: number): void; getDesktopCaptureSources(): Promise; + setDesktopCaptureSourceId(sourceId: string): Promise; getRunningPrograms(ignoredPrograms?: Program[]): Promise; restartActivityStatus(listenToPrograms: Program[]): void; @@ -48,7 +73,7 @@ interface WindowAPI { ): void; restartRPCServer(): void; - rpcChanged(callback: (data: RPC | false) => void): void; + rpcChanged(callback: (data: { id: string; data: RPC }[]) => void): void; relaunchApp(): void; onSpellcheck(callback: (suggestions: string[]) => void): void; @@ -57,6 +82,15 @@ interface WindowAPI { clipboardPaste(): void; clipboardCopy(text: string): void; clipboardCut(): void; + + startGlobalKeyListener: () => void; + stopGlobalKeyListener: () => void; + onGlobalKey: (callback: (event: GlobalKeyEvent) => void) => void; + + appLoopbackStart: (captureSourceId: string) => void; + appLoopbackReset: () => void; + appLoopbackData: (callback: (data: Uint8Array) => void) => void; + getAppVersion: () => Promise; } export function electronWindowAPI(): WindowAPI | undefined { @@ -66,3 +100,5 @@ export function electronWindowAPI(): WindowAPI | undefined { electronWindowAPI()?.onSpellcheck?.((suggestions) => { setSpellcheckSuggestions(suggestions); }); + +electronWindowAPI()?.appLoopbackReset?.(); diff --git a/src/common/Fragment.tsx b/src/common/Fragment.tsx new file mode 100644 index 000000000..60c3c9db7 --- /dev/null +++ b/src/common/Fragment.tsx @@ -0,0 +1,5 @@ +import { JSXElement } from "solid-js"; + +export const Fragment = (props: { children: JSXElement }) => ( + <>{props.children} +); diff --git a/src/common/GlobalEvents.ts b/src/common/GlobalEvents.ts index 8f7cf7fd5..74607b342 100644 --- a/src/common/GlobalEvents.ts +++ b/src/common/GlobalEvents.ts @@ -5,22 +5,40 @@ import { onCleanup } from "solid-js"; export const GlobalEventName = { SCROLL_TO_MESSAGE: "scrollToMessage", MODERATION_USER_SUSPENDED: "moderationUserSuspended", + MODERATION_SERVER_DELETED: "moderationServerDeleted", + MODERATION_UNDO_SERVER_DELETE: "moderationUndoServerDelete", + MODERATION_SHOW_MESSAGES: "moderationShowMessages", DRAWER_GO_TO_MAIN: "drawerGoToMain" } as const; - const EE = new EventEmitter(); -export function emitScrollToMessage(payload: {messageId: string}) { +export function emitScrollToMessage(payload: { messageId: string }) { EE.emit("scrollToMessage", payload); } +export function useScrollToMessageListener() { + return useEventListen<{ messageId: string }>("scrollToMessage"); +} +export function emitModerationShowMessages(payload: { + messageId: string; + channelId: string; +}) { + EE.emit("moderationShowMessages", payload); +} - -export function useScrollToMessageListener() { - return useEventListen<{messageId: string}>("scrollToMessage"); +export function useModerationShowMessages() { + return useEventListen<{ messageId: string; channelId: string }>( + "moderationShowMessages" + ); } +export function emitModerationServerDeleted(servers: any[]) { + EE.emit("moderationServerDeleted", servers); +} +export function emitModerationUndoServerDelete(serverId: string) { + EE.emit(GlobalEventName.MODERATION_UNDO_SERVER_DELETE, serverId); +} export function emitModerationUserSuspended(payload: ModerationSuspension) { EE.emit("moderationUserSuspended", payload); } @@ -29,16 +47,24 @@ export function emitDrawerGoToMain() { EE.emit("drawerGoToMain"); } - -export function useModerationUserSuspendedListener() { +export function useModerationUserSuspendedListener() { return useEventListen("moderationUserSuspended"); } -export function useEventListen(name: typeof GlobalEventName[keyof typeof GlobalEventName]) { +export function useModerationServerDeletedListener() { + return useEventListen("moderationServerDeleted"); +} +export function useModerationUndoServerDeleteListener() { + return useEventListen(GlobalEventName.MODERATION_UNDO_SERVER_DELETE); +} + +export function useEventListen( + name: (typeof GlobalEventName)[keyof typeof GlobalEventName] +) { return (callback: (event: TReturn) => void) => { EE.addListener(name, callback); onCleanup(() => { EE.removeListener(name, callback); }); }; -} \ No newline at end of file +} diff --git a/src/common/GlobalKey.ts b/src/common/GlobalKey.ts new file mode 100644 index 000000000..082e68a61 --- /dev/null +++ b/src/common/GlobalKey.ts @@ -0,0 +1,84 @@ +import { onCleanup } from "solid-js"; +import { electronWindowAPI } from "./Electron"; +import { createStore, reconcile } from "solid-js/store"; + +export const [downKeys, setDownKeys] = createStore<(string | number)[]>([]); + +const onMouseDown = (e: MouseEvent) => { + if (e.button === 0) return; + const code = `MOUSE ${e.button}`; + if (!downKeys.includes(code)) { + setDownKeys([...downKeys, code]); + } +}; +const onMouseUp = (e: MouseEvent) => { + if (e.button === 0) return; + const code = `MOUSE ${e.button}`; + setDownKeys(downKeys.filter((k) => k !== code)); +}; + +const onKeyDown = (e: KeyboardEvent) => { + let code = e.code || e.key; + if (code.startsWith("Key")) { + code = code.slice(3); + } + if (!downKeys.includes(code)) { + setDownKeys([...downKeys, code]); + } +}; + +const onKeyUp = (e: KeyboardEvent) => { + let code = e.code || e.key; + if (code.startsWith("Key")) { + code = code.slice(3); + } + setDownKeys(downKeys.filter((k) => k !== code)); +}; + +if (electronWindowAPI()?.isElectron) { + electronWindowAPI()?.onGlobalKey(({ event }) => { + const key = event.name || event.vKey; + if (event.name === "MOUSE LEFT") return; + if (event.state === "DOWN") { + if (!downKeys.includes(key)) { + setDownKeys([...downKeys, key]); + } + } else { + setDownKeys(downKeys.filter((k) => k !== key)); + } + }); +} + +export const useGlobalKey = () => { + let started = false; + const start = () => { + started = true; + if (!electronWindowAPI()?.isElectron) { + document.addEventListener("mousedown", onMouseDown); + document.addEventListener("mouseup", onMouseUp); + document.addEventListener("keydown", onKeyDown); + document.addEventListener("keyup", onKeyUp); + return; + } + electronWindowAPI()?.startGlobalKeyListener(); + }; + + const stop = () => { + if (!started) return; + setDownKeys(reconcile([])); + if (!electronWindowAPI()?.isElectron) { + document.removeEventListener("mousedown", onMouseDown); + document.removeEventListener("mouseup", onMouseUp); + document.removeEventListener("keydown", onKeyDown); + document.removeEventListener("keyup", onKeyUp); + return; + } + electronWindowAPI()?.stopGlobalKeyListener(); + }; + + onCleanup(() => { + stop(); + }); + + return { start, stop }; +}; diff --git a/src/common/GoogleTranslate.ts b/src/common/GoogleTranslate.ts new file mode 100644 index 000000000..51c664ff4 --- /dev/null +++ b/src/common/GoogleTranslate.ts @@ -0,0 +1,103 @@ +const BASE_URL = "https://translate.googleapis.com/translate_a/single"; + +export interface TranslateRes { + src: string; + sentences: { + trans: string; + }[]; + translationString: string; +} + +const GOOGLE_LANGUAGE_MAP: Record = { + "en-gb": "en", + "en-us": "en", + "af-za": "af", + "ar-ps": "ar", + "be-tarask": "be", + "pt-br": "pt", + "zh-hans": "zh-CN", + "zh-hant": "zh-TW", + "nl-nl": "nl", + "fr-fr": "fr", + "de-de": "de", + "hu-hu": "hu", + "ja-jp": "ja", + "fil-ph": "tl", + "pl-pl": "pl", + "ro-ro": "ro", + "ru-ru": "ru", + "es-es": "es", + "th-th": "th", + "tr-tr": "tr", + "sv-sv": "sv", + uk: "uk", + "uw-uw": "en" +}; + +function getActiveLanguageCode() { + try { + const stored = localStorage.getItem("appLanguage"); + return stored?.replace("_", "-").toLowerCase() || "en-gb"; + } catch { + return "en-gb"; + } +} + +const UrlBuilder = (query: string) => { + const activeLang = getActiveLanguageCode(); + const targetLang = GOOGLE_LANGUAGE_MAP[activeLang] ?? "en"; + + const url = new URL(BASE_URL); + + url.searchParams.set("client", "gtx"); + url.searchParams.set("sl", "auto"); + url.searchParams.set("tl", targetLang); + url.searchParams.set("dt", "t"); + url.searchParams.set("dj", "1"); + url.searchParams.set("source", "input"); + url.searchParams.set("q", query); + + return url.href; +}; + +const MAX_CACHE_SIZE = 20; +const translateCache = new Map(); + +const addToCache = (key: string, value: TranslateRes) => { + if (translateCache.size >= MAX_CACHE_SIZE) { + translateCache.delete(translateCache.keys().next().value!); + } + translateCache.set(key, value); +}; + +export const fetchTranslation = async (query: string) => { + const activeLang = getActiveLanguageCode(); + const cacheKey = `${activeLang}:${query}`; + + if (translateCache.has(cacheKey)) { + return translateCache.get(cacheKey)!; + } + + const url = UrlBuilder(query); + const res = await fetch(url); + + if (!res.ok) { + throw new Error("Translation request failed"); + } + + const json = (await res.json()) as TranslateRes; + + const sentences = Array.isArray(json.sentences) ? json.sentences : []; + + const translationString = sentences + .map((s) => s?.trans || "") + .filter(Boolean) + .join(""); + + json.translationString = translationString; + json.src = json.src?.toLowerCase?.() ?? json.src ?? "auto"; + + addToCache(cacheKey, json); + + return json; +}; diff --git a/src/common/LocalRPC.ts b/src/common/LocalRPC.ts index 26bbf797c..acea123ce 100644 --- a/src/common/LocalRPC.ts +++ b/src/common/LocalRPC.ts @@ -1,44 +1,53 @@ import { RPC } from "./Electron"; export class LocalRPC { - onUpdateRPC: (data: RPC | false) => void = () => {}; + onUpdateRPC: (data: RPC[]) => void = () => {}; - RPCs: {data: RPC, id: string}[] = []; + RPCs: { data: RPC; id: string }[] = []; + discordRPCs: { data: RPC; id: string }[] = []; + electronRPCs: { data: RPC; id: string }[] = []; constructor() { - - - window.addEventListener("message", (ev) => { - const payload = ev.data; - const id = payload.id; - if (payload.name === "UPDATE_RPC") { - this.updateRPC(id, payload.data); - } - }, true); - - - + window.addEventListener( + "message", + (ev) => { + const payload = ev.data; + const id = payload.id; + if (payload.name === "UPDATE_RPC") { + this.updateRPC(id, payload.data); + } + }, + true + ); } start() { - window.postMessage({name: "NERIMITY_READY"}, "*"); + window.parent.postMessage({ name: "NERIMITY_READY" }, "*"); } emitEvent() { - const firstRPC = this.RPCs[0]; - if (!firstRPC) { - return this.onUpdateRPC(false); - } - this.onUpdateRPC(firstRPC.data); + const RPCs = this.RPCs.map((rpc) => rpc.data); + const electronRPCs = this.electronRPCs.map((rpc) => rpc.data); + const discordRPCs = this.discordRPCs.map((rpc) => rpc.data); + this.onUpdateRPC([...electronRPCs, ...discordRPCs, ...RPCs]); } - updateRPC(id: string, data: RPC) { + updateElectronRPCs(data: { id: string; data: RPC }[]) { + this.electronRPCs = data; + this.emitEvent(); + } + updateDiscordRPCs(data: { id: string; data: RPC }[]) { + this.discordRPCs = data; + this.emitEvent(); + } + updateRPC(id: string, data?: RPC) { if (!data) return this.removeRPC(id); + const index = this.RPCs.findIndex((rpc) => rpc.id === id); if (index === -1) { this.RPCs.push({ id, data: sanitizedData(data) }); - if (this.RPCs.length === 1) this.emitEvent(); + this.emitEvent(); return; } @@ -46,7 +55,7 @@ export class LocalRPC { return; } this.RPCs[index]!.data = sanitizedData(data); - if (index === 0) this.emitEvent(); + this.emitEvent(); } removeRPC(id: string) { const index = this.RPCs.findIndex((rpc) => rpc.id === id); @@ -54,19 +63,14 @@ export class LocalRPC { return; } this.RPCs.splice(index, 1); - if (index === 0) { - this.emitEvent(); - } + this.emitEvent(); } - - } function JSONCompare(a?: Record, b?: Record) { return JSON.stringify(a) === JSON.stringify(b); } - const sanitizedData = (data: any) => { // name: "Spotify", // action: "Listening to", @@ -74,18 +78,21 @@ const sanitizedData = (data: any) => { // title: data.title, // subtitle: data.subtitle // startedAt: data.startedAt - return JSON.parse(JSON.stringify({ - name: data.name?.substring(0, 30), - action: data.action?.substring(0, 20), - imgSrc: data.imgSrc?.substring(0, 250), - title: data.title?.substring(0, 30), - subtitle: data.subtitle?.substring(0, 30), - link: data.link?.substring(0, 250), - startedAt: data.startedAt, - endsAt: data.endsAt, - speed: data.speed, - updatedAt: data.updatedAt - })); + return JSON.parse( + JSON.stringify({ + name: data.name, + action: data.action, + imgSrc: data.imgSrc, + title: data.title, + subtitle: data.subtitle, + link: data.link, + startedAt: data.startedAt, + endsAt: data.endsAt, + speed: data.speed, + updatedAt: data.updatedAt, + emoji: data.emoji + }) + ); }; -export const localRPC = new LocalRPC(); \ No newline at end of file +export const localRPC = new LocalRPC(); diff --git a/src/common/MetaTitle.tsx b/src/common/MetaTitle.tsx index 4027bcebd..d529e049c 100644 --- a/src/common/MetaTitle.tsx +++ b/src/common/MetaTitle.tsx @@ -1,11 +1,13 @@ -import { Title } from "@solidjs/meta"; import { children, createEffect, JSXElement } from "solid-js"; import env from "./env"; export const MetaTitle = (props: { children: JSXElement }) => { const el = children(() => props.children); - const text = el.toArray().join(" "); - const full = `${text || ""} - Nerimity ${env.DEV_MODE ? "DEV" : ""}`; + const text = () => el.toArray().join(" "); + const full = () => `${text() || ""} - Nerimity ${env.DEV_MODE ? "DEV" : ""}`; - return {full}; + createEffect(() => { + document.title = full(); + }); + return <>; }; diff --git a/src/common/ReactNative.ts b/src/common/ReactNative.ts index c056e3036..b7f4006b7 100644 --- a/src/common/ReactNative.ts +++ b/src/common/ReactNative.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ import { onCleanup, onMount } from "solid-js"; interface CustomEventMap { @@ -6,6 +7,7 @@ interface CustomEventMap { registerFCM: { token: string }; openChannel: { userId: string; channelId: string; serverId?: string }; + sio_event: { event: string; payload: any }; } type EventByType = { @@ -14,6 +16,7 @@ type EventByType = { interface WindowAPI { isReactNative: boolean; + version?: string; playVideo(url: string): void; playAudio(url?: string): void; pauseAudio(): void; @@ -21,6 +24,8 @@ interface WindowAPI { authenticated(userId: string): string; logout(): void; + post(event: string, payload: any): void; + joinCall(channelId: string): void; on( event: K, @@ -62,3 +67,69 @@ export function useReactNativeEvent( }); }); } + +export class ReactSocketIO { + url: string; + id?: string; + handlers: Record void>> = {}; + + io = { + on: (event: string, callback: (data: any) => void) => { + this.on(event, callback); + }, + off: (event: string, callback: (data: any) => void) => { + this.off(event, callback); + } + }; + + constructor(url: string) { + this.url = url; + + reactNativeAPI()?.on( + "sio_event", + (data: { event: string; payload: any; type?: "binary" }) => { + if (data.event === "connect") { + this.id = data.payload.id; + } + if (data.event === "disconnect") { + const handlers = this.handlers[data.event]; + if (handlers) { + handlers.forEach((handler) => + handler(data.payload.reason, data.payload.description) + ); + } + return; + } + if (data.event === "reconnect_attempt") { + const handlers = this.handlers[data.event]; + if (handlers) { + handlers.forEach((handler) => handler(data.payload.attempt)); + } + return; + } + + if (data.type === "binary") { + data.payload = new Uint8Array(data.payload).buffer; + } + + const handlers = this.handlers[data.event]; + if (handlers) { + handlers.forEach((handler) => handler(data.payload)); + } + } + ); + } + connect() { + reactNativeAPI()?.post("sio_connect", { url: this.url }); + } + on(event: string, callback: (data: any) => void) { + if (!this.handlers[event]) this.handlers[event] = new Set(); + this.handlers[event]?.add(callback); + } + off(event: string, callback: (data: any) => void) { + this.handlers[event]?.delete(callback); + } + emit(event: string, data: any) { + reactNativeAPI()?.post("sio_emit", { event, payload: data }); + } +} diff --git a/src/common/RouterEndpoints.ts b/src/common/RouterEndpoints.ts index 5c14a5557..0ba7f7fc8 100644 --- a/src/common/RouterEndpoints.ts +++ b/src/common/RouterEndpoints.ts @@ -1,26 +1,37 @@ export default { - - SERVER_MESSAGES: (serverId: string, channelId: string) => `/app/servers/${serverId}/${channelId}`, + SERVER_MESSAGES: (serverId: string, channelId: string) => + `/app/servers/${serverId}/${channelId}`, SERVER: (serverId: string) => `/app/servers/${serverId}`, - LOGIN: (redirect?: string) => `/login${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`, - REGISTER: (redirect?: string) => `/register${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`, - - SERVER_SETTINGS_GENERAL: (serverId: string) => `/app/servers/${serverId}/settings/general`, - SERVER_SETTINGS_INVITES: (serverId: string) => `/app/servers/${serverId}/settings/invites`, - SERVER_SETTINGS_NOTIFICATIONS: (serverId: string) => `/app/servers/${serverId}/settings/notifications`, - SERVER_SETTINGS_CHANNELS: (serverId: string) => `/app/servers/${serverId}/settings/channels`, - SERVER_SETTINGS_CHANNEL: (serverId: string, channelId: string) => `/app/servers/${serverId}/settings/channels/${channelId}`, - SERVER_SETTINGS_ROLES: (serverId: string) => `/app/servers/${serverId}/settings/roles`, - SERVER_SETTINGS_ROLE: (serverId: string, roleId: string) => `/app/servers/${serverId}/settings/roles/${roleId}`, - + LOGIN: (redirect?: string) => + `/login${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`, + REGISTER: (redirect?: string) => + `/register${redirect ? `?redirect=${encodeURIComponent(redirect)}` : ""}`, + + SERVER_SETTINGS_GENERAL: (serverId: string) => + `/app/servers/${serverId}/settings/general`, + SERVER_SETTINGS_INVITES: (serverId: string) => + `/app/servers/${serverId}/settings/invites`, + SERVER_SETTINGS_NOTIFICATIONS: (serverId: string) => + `/app/servers/${serverId}/settings/notifications`, + SERVER_SETTINGS_CHANNELS: (serverId: string) => + `/app/servers/${serverId}/settings/channels`, + SERVER_SETTINGS_CHANNEL: (serverId: string, channelId: string) => + `/app/servers/${serverId}/settings/channels/${channelId}`, + SERVER_SETTINGS_ROLES: (serverId: string) => + `/app/servers/${serverId}/settings/roles`, + SERVER_SETTINGS_ROLE: (serverId: string, roleId: string) => + `/app/servers/${serverId}/settings/roles/${roleId}`, + EXPLORE_SERVER: (serverId: string) => `/app/explore/servers/${serverId}`, - + EXPLORE_SERVERS: () => "/app/explore/servers", + PROFILE: (userId: string) => `/app/profile/${userId}`, - EXPLORE_SERVER_INVITE: (inviteId: string) => `/app/explore/servers/invites/${inviteId}`, + EXPLORE_SERVER_INVITE: (inviteId: string) => + `/app/explore/servers/invites/${inviteId}`, EXPLORE_SERVER_INVITE_SHORT: (inviteId: string) => `/i/${inviteId}`, - + INBOX: () => "/app/inbox", INBOX_MESSAGES: (channelId: string) => `/app/inbox/${channelId}` }; diff --git a/src/common/ServerSettings.ts b/src/common/ServerSettings.ts index 7659ec45f..675c97335 100644 --- a/src/common/ServerSettings.ts +++ b/src/common/ServerSettings.ts @@ -1,130 +1,202 @@ import { Bitwise, ROLE_PERMISSIONS } from "@/chat-api/Bitwise"; +import { t } from "@nerimity/i18lite"; import { lazy } from "solid-js"; export interface ServerSetting { - path?: string; - routePath: string; - name: string; - icon: string; - requiredRolePermission?: Bitwise, - hideDrawer?: boolean - element: any, + path?: string; + routePath: string; + name: () => string; + icon: string; + requiredRolePermission?: Bitwise; + hideDrawer?: boolean; + element: any; } -const serverSettings: ServerSetting[] = [ +const serverSettings: ServerSetting[] = [ { path: "general", routePath: "/general", - name: "servers.settings.drawer.general", + name: () => t("servers.settings.drawer.general"), icon: "info", requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/ServerGeneralSettings")) + element: lazy( + () => import("@/components/servers/settings/ServerGeneralSettings") + ) + }, + { + path: "audit-logs", + routePath: "/audit-logs", + name: () => t("servers.settings.drawer.audit-logs"), + icon: "history", + requiredRolePermission: ROLE_PERMISSIONS.ADMIN, + element: lazy(() => import("@/components/servers/settings/ServerAuditLogs")) }, { path: "notifications", routePath: "/notifications", - name: "settings.drawer.notifications", + name: () => t("settings.drawer.notifications"), icon: "notifications", - element: lazy(() => import("@/components/servers/settings/ServerNotificationSettings")) + element: lazy( + () => import("@/components/servers/settings/ServerNotificationSettings") + ) }, { path: "notifications/:channelId", routePath: "/notifications/:channelId", - name: "settings.drawer.notifications", + name: () => t("settings.drawer.notifications"), icon: "notifications", hideDrawer: true, - element: lazy(() => import("@/components/servers/settings/ServerNotificationSettings")) + element: lazy( + () => import("@/components/servers/settings/ServerNotificationSettings") + ) }, { path: "roles/:roleId", routePath: "/roles/:roleId", - name: "servers.settings.drawer.role", + name: () => t("servers.settings.drawer.role"), icon: "leaderboard", hideDrawer: true, requiredRolePermission: ROLE_PERMISSIONS.MANAGE_ROLES, - element: lazy(() => import("@/components/servers/settings/role/ServerSettingsRole")) + element: lazy( + () => import("@/components/servers/settings/role/ServerSettingsRole") + ) }, { - name: "servers.settings.drawer.roles", + name: () => t("servers.settings.drawer.roles"), path: "roles", routePath: "/roles", icon: "leaderboard", requiredRolePermission: ROLE_PERMISSIONS.MANAGE_ROLES, - element: lazy(() => import("@/components/servers/settings/roles/ServerSettingsRoles")) + element: lazy( + () => import("@/components/servers/settings/roles/ServerSettingsRoles") + ) }, { - name: "servers.settings.drawer.welcome-screen", + name: () => t("servers.settings.drawer.welcome-screen"), path: "welcome-screen", routePath: "/welcome-screen", icon: "task_alt", requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/welcome-screen/WelcomeScreen")) + element: lazy( + () => import("@/components/servers/settings/welcome-screen/WelcomeScreen") + ) }, { - name: "servers.settings.drawer.welcome-screen", + name: () => t("servers.settings.drawer.welcome-screen"), path: "welcome-screen", routePath: "/welcome-screen/:questionId", icon: "task_alt", hideDrawer: true, requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/welcome-question/WelcomeQuestion")) + element: lazy( + () => + import("@/components/servers/settings/welcome-question/WelcomeQuestion") + ) + }, + { + path: "channels/:channelId/webhooks/:webhookId", + routePath: "/channels/:channelId/webhooks/:webhookId", + name: () => t("servers.settings.drawer.channel"), + icon: "storage", + requiredRolePermission: ROLE_PERMISSIONS.ADMIN, + hideDrawer: true, + element: lazy( + () => import("@/components/servers/settings/ServerSettingsWebhook") + ) }, { path: "channels/:channelId", - routePath: "/channels/:channelId", - name: "servers.settings.drawer.channel", + routePath: "/channels/:channelId/:tab?", + name: () => t("servers.settings.drawer.channel"), icon: "storage", requiredRolePermission: ROLE_PERMISSIONS.MANAGE_CHANNELS, hideDrawer: true, - element: lazy(() => import("@/components/servers/settings/channel/ServerSettingsChannel")) + element: lazy( + () => + import("@/components/servers/settings/channel/ServerSettingsChannel") + ) }, + { - name: "servers.settings.drawer.channels", + name: () => t("servers.settings.drawer.channels"), path: "channels", routePath: "/channels", icon: "storage", requiredRolePermission: ROLE_PERMISSIONS.MANAGE_CHANNELS, - element: lazy(() => import("@/components/servers/settings/channels/ServerSettingsChannels")) + element: lazy( + () => + import("@/components/servers/settings/channels/ServerSettingsChannels") + ) }, { path: "emojis", routePath: "/emojis", - name: "servers.settings.drawer.emojis", + name: () => t("servers.settings.drawer.emoji"), icon: "face", requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/ServerSettingsEmojis")) + element: lazy( + () => import("@/components/servers/settings/ServerSettingsEmojis") + ) }, { - name: "servers.settings.drawer.bans", + name: () => t("servers.settings.drawer.bans"), path: "bans", routePath: "/bans", icon: "block", requiredRolePermission: ROLE_PERMISSIONS.BAN, - element: lazy(() => import("@/components/servers/settings/ServerSettingsBans")) + element: lazy( + () => import("@/components/servers/settings/ServerSettingsBans") + ) }, { path: "invites", routePath: "/invites", - name: "servers.settings.drawer.invites", + name: () => t("servers.settings.drawer.invites"), icon: "mail", - element: lazy(() => import("@/components/servers/settings/invites/ServerSettingsInvite")) + element: lazy( + () => import("@/components/servers/settings/invites/ServerSettingsInvite") + ) }, { - name: "servers.settings.drawer.publishServer", + name: () => t("servers.settings.drawer.publishServer"), path: "publish-server", routePath: "/publish-server", icon: "public", requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/PublishServerSettings")) + element: lazy( + () => import("@/components/servers/settings/PublishServerSettings") + ) + }, + { + path: "clans", + routePath: "/clans", + name: () => t("servers.settings.drawer.clanTag"), + icon: "label", + requiredRolePermission: ROLE_PERMISSIONS.ADMIN, + element: lazy( + () => import("@/components/servers/settings/ServerClanTagSettings") + ) }, { path: "verify", routePath: "/verify", - name: "servers.settings.drawer.verify", + name: () => t("servers.settings.drawer.verify"), icon: "verified", requiredRolePermission: ROLE_PERMISSIONS.ADMIN, - element: lazy(() => import("@/components/servers/settings/ServerVerifySettings")) + element: lazy( + () => import("@/components/servers/settings/ServerVerifySettings") + ) + }, + { + path: "external-embed", + routePath: "/external-embed", + name: () => t("servers.settings.drawer.external-embed"), + icon: "link", + requiredRolePermission: ROLE_PERMISSIONS.ADMIN, + element: lazy( + () => import("@/components/servers/settings/ExternalEmbedSettings") + ) } ]; -export default serverSettings; \ No newline at end of file +export default serverSettings; diff --git a/src/common/Settings.ts b/src/common/Settings.ts index bfb1144c7..0200e5900 100644 --- a/src/common/Settings.ts +++ b/src/common/Settings.ts @@ -1,10 +1,11 @@ import { lazy } from "solid-js"; import { ExperimentIds } from "./experiments"; +import { t } from "@nerimity/i18lite"; export interface Setting { path: string; routePath: string; - name: string; + name: () => string; icon: string; element: any; hide?: boolean; @@ -17,165 +18,196 @@ const DeveloperApplicationBotSettings = lazy( import("@/components/settings/developer/DeveloperApplicationBotSettings") ); +const DeveloperApplicationSettings = lazy( + () => import("@/components/settings/developer/DeveloperApplicationSettings") +); + const settings: Setting[] = [ { path: "account", routePath: "/account", - name: "settings.drawer.account", + name: () => t("settings.drawer.account"), icon: "account_circle", - element: lazy(() => import("@/components/settings/AccountSettings")), + element: lazy(() => import("@/components/settings/AccountSettings")) }, + { - path: "/account/profile", - routePath: "/account/profile", - name: "settings.drawer.account", - icon: "account_circle", - element: lazy(() => import("@/components/settings/ProfileSettings")), - hide: true, + path: "profile", + routePath: "/profile", + name: () => t("settings.account.profile"), + icon: "person", + element: lazy(() => import("@/components/settings/ProfileSettings")) + }, + { + path: "sessions", + routePath: "/sessions", + name: () => t("settings.drawer.sessions"), + icon: "data_loss_prevention", + element: lazy(() => import("@/components/settings/SessionSettings")) + }, + { + path: "badges", + routePath: "/badges", + name: () => t("settings.drawer.badges"), + icon: "local_police", + element: lazy(() => import("@/components/settings/BadgeSettings")) }, { path: "interface", routePath: "/interface", - name: "settings.drawer.interface", + name: () => t("settings.drawer.interface"), icon: "brush", - element: lazy(() => import("@/components/settings/InterfaceSettings")), + element: lazy(() => import("@/components/settings/InterfaceSettings")) }, { path: "/interface/custom-css", routePath: "/interface/custom-css", - name: "settings.drawer.interface", + name: () => t("settings.drawer.interface"), icon: "code", element: lazy(() => import("@/components/settings/CustomCssSettings")), - hide: true, + hide: true }, { path: "notifications", routePath: "/notifications", - name: "settings.drawer.notifications", + name: () => t("settings.drawer.notifications"), icon: "notifications", - element: lazy(() => import("@/components/settings/NotificationsSettings")), + element: lazy(() => import("@/components/settings/NotificationsSettings")) }, { path: "call-settings", routePath: "/call-settings", - name: "settings.drawer.call-settings", + name: () => t("settings.drawer.call-settings"), icon: "call", - element: lazy(() => import("@/components/settings/CallSettings")), + element: lazy(() => import("@/components/settings/CallSettings")) }, { path: "connections", routePath: "/connections", - name: "settings.drawer.connections", + name: () => t("settings.drawer.connections"), icon: "hub", - element: lazy(() => import("@/components/settings/ConnectionsSettings")), + element: lazy(() => import("@/components/settings/ConnectionsSettings")) }, { path: "privacy", routePath: "/privacy", - name: "settings.drawer.privacy", + name: () => t("settings.drawer.privacy"), icon: "shield", - element: lazy(() => import("@/components/settings/PrivacySettings")), + element: lazy(() => import("@/components/settings/PrivacySettings")) }, { path: "window-settings", routePath: "/window-settings", - name: "settings.drawer.window-settings", - icon: "launch", - element: lazy(() => import("@/components/settings/WindowSettings")), + name: () => t("settings.drawer.window-settings"), + icon: "open_in_new", + element: lazy(() => import("@/components/settings/WindowSettings")) }, { path: "activity-status", routePath: "/activity-status", - name: "settings.drawer.activity-status", - icon: "games", - element: lazy(() => import("@/components/settings/ActivityStatus")), + name: () => t("settings.drawer.activity-status"), + icon: "gamepad", + element: lazy(() => import("@/components/settings/ActivityStatus")) }, { path: "language", routePath: "/language", - name: "settings.drawer.language", + name: () => t("settings.drawer.language"), icon: "flag", - element: lazy(() => import("@/components/settings/LanguageSettings")), + element: lazy(() => import("@/components/settings/LanguageSettings")) }, { path: "developer", routePath: "/developer", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), icon: "code", element: lazy( () => import("@/components/settings/developer/DeveloperSettings") - ), + ) }, { path: "developer/applications", routePath: "/developer/applications", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), icon: "code", hide: true, element: lazy( () => import("@/components/settings/developer/DeveloperApplicationsSettings") - ), + ) }, { path: "developer/applications", routePath: "/developer/applications/:id", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), hideHeader: true, icon: "code", hide: true, - element: lazy( - () => - import("@/components/settings/developer/DeveloperApplicationSettings") - ), + element: DeveloperApplicationSettings + }, + { + path: "developer/applications", + routePath: "/developer/applications/:id/oauth2", + name: () => t("settings.drawer.developer"), + hideHeader: true, + icon: "code", + hide: true, + element: DeveloperApplicationSettings }, { path: "developer/applications", routePath: "/developer/applications/:id/bot/create-link", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), hideHeader: true, icon: "code", hide: true, element: lazy( () => - import( - "@/components/settings/developer/DeveloperApplicationBotCreateLinkSettings" - ) - ), + import("@/components/settings/developer/DeveloperApplicationBotCreateLinkSettings") + ) }, { path: "developer/applications", routePath: "/developer/applications/:id/bot/profile", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), hideHeader: true, icon: "code", hide: true, - element: DeveloperApplicationBotSettings, + element: DeveloperApplicationBotSettings }, { path: "developer/applications", routePath: "/developer/applications/:id/bot", - name: "settings.drawer.developer", + name: () => t("settings.drawer.developer"), + hideHeader: true, + icon: "code", + hide: true, + element: DeveloperApplicationBotSettings + }, + { + path: "developer/applications", + routePath: "/developer/applications/:id/bot/publish", + name: () => t("settings.drawer.developer"), hideHeader: true, icon: "code", hide: true, - element: DeveloperApplicationBotSettings, + element: DeveloperApplicationBotSettings }, { path: "experiments", routePath: "/experiments", - name: "settings.drawer.experiments", + name: () => t("settings.drawer.experiments"), icon: "science", - element: lazy(() => import("@/components/settings/ExperimentSettings")), + element: lazy(() => import("@/components/settings/ExperimentSettings")) }, { path: "tickets", routePath: "/tickets/:id?", - name: "settings.drawer.tickets", + name: () => t("settings.drawer.tickets"), icon: "sell", - element: lazy(() => import("@/components/settings/TicketSettings")), - }, + element: lazy(() => import("@/components/settings/TicketSettings")) + } ]; export default settings; diff --git a/src/common/Sound.ts b/src/common/Sound.ts index d90366123..a39945016 100644 --- a/src/common/Sound.ts +++ b/src/common/Sound.ts @@ -1,4 +1,9 @@ -import { getStorageBoolean, getStorageNumber, getStorageObject, StorageKeys } from "./localStorage"; +import { + getStorageBoolean, + getStorageNumber, + getStorageObject, + StorageKeys +} from "./localStorage"; import useStore from "@/chat-api/store/useStore"; import { UserStatus } from "@/chat-api/store/useUsers"; import { RawMessage, ServerNotificationSoundMode } from "@/chat-api/RawData"; @@ -7,6 +12,8 @@ import { ROLE_PERMISSIONS } from "@/chat-api/Bitwise"; export const Sounds = [ "nerimity-mute", "default", + "default-call-join", + "default-call-leave", "a-sudden-appearance", "button", "ding", @@ -25,10 +32,8 @@ export const Sounds = [ "the-notification-email" ] as const; - - const audio = new Audio(); -export function playSound(name: typeof Sounds[number] = "default") { +export function playSound(name: (typeof Sounds)[number] = "default") { if (name === "nerimity-mute") return; audio.src = `/assets/sounds/${name}.mp3`; audio.volume = getStorageNumber(StorageKeys.NOTIFICATION_VOLUME, 10) / 100; @@ -36,56 +41,107 @@ export function playSound(name: typeof Sounds[number] = "default") { audio.play(); } - interface MessageNotificationOpts { - force?: boolean - message?: RawMessage + force?: boolean; + message?: RawMessage; serverId?: string; } export function playMessageNotification(opts?: MessageNotificationOpts) { + if (opts?.message?.silent) return; if (opts?.force) return playSound(getCustomSound("MESSAGE")); if (getStorageBoolean(StorageKeys.ARE_NOTIFICATIONS_MUTED, false)) return; - const {account, users, serverMembers} = useStore(); + const { account, users, serverMembers } = useStore(); const userId = account.user()?.id; const user = users.get(userId!); if (user?.presence()?.status === UserStatus.DND) return; - const notificationSoundMode = !opts?.serverId ? undefined : account.getCombinedNotificationSettings(opts.serverId, opts.message?.channelId)?.notificationSoundMode; + const notificationSoundMode = !opts?.serverId + ? undefined + : account.getCombinedNotificationSettings( + opts.serverId, + opts.message?.channelId + )?.notificationSoundMode; if (notificationSoundMode === ServerNotificationSoundMode.MUTE) return; - + if (opts?.message) { - const mentionedMe = opts.message.mentions?.find(m => m.id === account.user()?.id); - if (mentionedMe) { + const mentioned = isMentioned(opts.message, opts.serverId); + if (mentioned) { return playSound(getCustomSound("MESSAGE_MENTION")); } + } + + if (notificationSoundMode === ServerNotificationSoundMode.MENTIONS_ONLY) + return; - const quoteMention = opts.message.quotedMessages?.find(m => m.createdBy?.id === userId); + playSound(getCustomSound("MESSAGE")); +} - const replyMention = opts.message.mentionReplies && opts.message.replyMessages.find(m => m.replyToMessage?.createdBy?.id === userId); +const defaults: Record< + "MESSAGE" | "MESSAGE_MENTION" | "REMINDER" | "CALL_JOIN" | "CALL_LEAVE", + (typeof Sounds)[number] +> = { + MESSAGE: "default", + MESSAGE_MENTION: "default", + REMINDER: "level-up", + CALL_JOIN: "default-call-join", + CALL_LEAVE: "default-call-leave" +}; - if (quoteMention || replyMention) { - return playSound(getCustomSound("MESSAGE_MENTION")); - } +export function getCustomSound( + type: "MESSAGE" | "MESSAGE_MENTION" | "REMINDER" | "CALL_JOIN" | "CALL_LEAVE" +) { + const storage = getStorageObject<{ + [key: string]: (typeof Sounds)[number] | undefined; + }>(StorageKeys.NOTIFICATION_SOUNDS, {}); + return storage[type] || defaults[type]; +} - - const everyoneMentioned = opts.message.content?.includes("[@:e]"); - if (everyoneMentioned && opts.serverId) { - const member = serverMembers.get(opts.serverId, opts.message.createdBy.id); - const hasPerm = member?.isServerCreator() || member?.hasPermission(ROLE_PERMISSIONS.MENTION_EVERYONE); - if (hasPerm) { - return playSound(getCustomSound("MESSAGE_MENTION")); - } - } +export function isMentioned(message: RawMessage, serverId?: string) { + const { account, serverMembers, servers } = useStore(); + const userId = account.user()?.id; + + const member = serverMembers.get(serverId!, message.createdBy.id); + const selfMember = serverMembers.get(serverId!, userId!); + const server = servers.get(serverId!); + + const mentionedMe = message.mentions?.find( + (m) => m.id === account.user()?.id + ); + if (mentionedMe) { + return true; } - - if (notificationSoundMode === ServerNotificationSoundMode.MENTIONS_ONLY) return; - playSound(getCustomSound("MESSAGE")); -} + const quoteMention = message.quotedMessages?.find( + (m) => m.createdBy?.id === userId + ); + + const replyMention = + message.mentionReplies && + message.replyMessages.find( + (m) => m.replyToMessage?.createdBy?.id === userId + ); -function getCustomSound (type: "MESSAGE" | "MESSAGE_MENTION") { - const storage = getStorageObject<{[key: string]: typeof Sounds[number] | undefined}>(StorageKeys.NOTIFICATION_SOUNDS, {}); - return storage[type]; -} \ No newline at end of file + const isRoleMentioned = + serverMembers.hasPermission(selfMember!, ROLE_PERMISSIONS.MENTION_ROLES) && + message.roleMentions.find( + (r) => + r.id !== server?.defaultRoleId && + serverMembers.hasRole(selfMember!, r.id) + ); + + if (quoteMention || replyMention || isRoleMentioned) { + return true; + } + + const everyoneMentioned = message.content?.includes("[@:e]"); + if (everyoneMentioned && serverId) { + const hasPerm = + serverMembers.isServerCreator(member!) || + serverMembers.hasPermission(member!, ROLE_PERMISSIONS.MENTION_EVERYONE); + if (hasPerm) { + return true; + } + } +} diff --git a/src/common/SystemMessage.ts b/src/common/SystemMessage.ts new file mode 100644 index 000000000..c999c093d --- /dev/null +++ b/src/common/SystemMessage.ts @@ -0,0 +1,68 @@ +import { MessageType } from "@/chat-api/RawData"; + +const tn = (v: string) => v; + +export const getSystemMessage = (messageType: MessageType, isBot = false) => { + switch (messageType) { + case MessageType.CONTENT: + return null; + + case MessageType.JOIN_SERVER: + return { + icon: "login", + color: "var(--success-color)", + message: isBot + ? tn("systemMessages.joinServer.bot") + : tn("systemMessages.joinServer.user") + }; + + case MessageType.LEAVE_SERVER: + return { + icon: "logout", + color: "var(--alert-color)", + message: tn("systemMessages.leaveServer") + }; + + case MessageType.KICK_USER: + return { + icon: "logout", + color: "var(--alert-color)", + message: tn("systemMessages.kickUser") + }; + + case MessageType.BAN_USER: + return { + icon: "block", + color: "var(--alert-color)", + message: tn("systemMessages.banUser") + }; + + case MessageType.CALL_STARTED: + return { + icon: "call", + color: "var(--success-color)", + message: tn("systemMessages.callStarted") + }; + + case MessageType.BUMP_SERVER: + return { + icon: "trending_up", + color: "var(--primary-color)", + message: tn("systemMessages.bumpServer") + }; + + case MessageType.PINNED_MESSAGE: + return { + icon: "keep", + color: "var(--primary-color)", + message: tn("systemMessages.pinnedMessage") + }; + + default: + return { + icon: "info", + color: "var(--alert-color)", + message: tn("systemMessages.unsupported") + }; + } +}; diff --git a/src/common/WorldTimezones.ts b/src/common/WorldTimezones.ts new file mode 100644 index 000000000..e59757276 --- /dev/null +++ b/src/common/WorldTimezones.ts @@ -0,0 +1,596 @@ +export const WorldTimezones = [ + "Africa/Abidjan", + "Africa/Accra", + "Africa/Addis_Ababa", + "Africa/Algiers", + "Africa/Asmara", + "Africa/Asmera", + "Africa/Bamako", + "Africa/Bangui", + "Africa/Banjul", + "Africa/Bissau", + "Africa/Blantyre", + "Africa/Brazzaville", + "Africa/Bujumbura", + "Africa/Cairo", + "Africa/Casablanca", + "Africa/Ceuta", + "Africa/Conakry", + "Africa/Dakar", + "Africa/Dar_es_Salaam", + "Africa/Djibouti", + "Africa/Douala", + "Africa/El_Aaiun", + "Africa/Freetown", + "Africa/Gaborone", + "Africa/Harare", + "Africa/Johannesburg", + "Africa/Juba", + "Africa/Kampala", + "Africa/Khartoum", + "Africa/Kigali", + "Africa/Kinshasa", + "Africa/Lagos", + "Africa/Libreville", + "Africa/Lome", + "Africa/Luanda", + "Africa/Lubumbashi", + "Africa/Lusaka", + "Africa/Malabo", + "Africa/Maputo", + "Africa/Maseru", + "Africa/Mbabane", + "Africa/Mogadishu", + "Africa/Monrovia", + "Africa/Nairobi", + "Africa/Ndjamena", + "Africa/Niamey", + "Africa/Nouakchott", + "Africa/Ouagadougou", + "Africa/Porto-Novo", + "Africa/Sao_Tome", + "Africa/Timbuktu", + "Africa/Tripoli", + "Africa/Tunis", + "Africa/Windhoek", + "America/Adak", + "America/Anchorage", + "America/Anguilla", + "America/Antigua", + "America/Araguaina", + "America/Argentina/Buenos_Aires", + "America/Argentina/Catamarca", + "America/Argentina/ComodRivadavia", + "America/Argentina/Cordoba", + "America/Argentina/Jujuy", + "America/Argentina/La_Rioja", + "America/Argentina/Mendoza", + "America/Argentina/Rio_Gallegos", + "America/Argentina/Salta", + "America/Argentina/San_Juan", + "America/Argentina/San_Luis", + "America/Argentina/Tucuman", + "America/Argentina/Ushuaia", + "America/Aruba", + "America/Asuncion", + "America/Atikokan", + "America/Atka", + "America/Bahia", + "America/Bahia_Banderas", + "America/Barbados", + "America/Belem", + "America/Belize", + "America/Blanc-Sablon", + "America/Boa_Vista", + "America/Bogota", + "America/Boise", + "America/Buenos_Aires", + "America/Cambridge_Bay", + "America/Campo_Grande", + "America/Cancun", + "America/Caracas", + "America/Catamarca", + "America/Cayenne", + "America/Cayman", + "America/Chicago", + "America/Chihuahua", + "America/Coral_Harbour", + "America/Cordoba", + "America/Costa_Rica", + "America/Creston", + "America/Cuiaba", + "America/Curacao", + "America/Danmarkshavn", + "America/Dawson", + "America/Dawson_Creek", + "America/Denver", + "America/Detroit", + "America/Dominica", + "America/Edmonton", + "America/Eirunepe", + "America/El_Salvador", + "America/Ensenada", + "America/Fort_Nelson", + "America/Fort_Wayne", + "America/Fortaleza", + "America/Glace_Bay", + "America/Godthab", + "America/Goose_Bay", + "America/Grand_Turk", + "America/Grenada", + "America/Guadeloupe", + "America/Guatemala", + "America/Guayaquil", + "America/Guyana", + "America/Halifax", + "America/Havana", + "America/Hermosillo", + "America/Indiana/Indianapolis", + "America/Indiana/Knox", + "America/Indiana/Marengo", + "America/Indiana/Petersburg", + "America/Indiana/Tell_City", + "America/Indiana/Vevay", + "America/Indiana/Vincennes", + "America/Indiana/Winamac", + "America/Indianapolis", + "America/Inuvik", + "America/Iqaluit", + "America/Jamaica", + "America/Jujuy", + "America/Juneau", + "America/Kentucky/Louisville", + "America/Kentucky/Monticello", + "America/Knox_IN", + "America/Kralendijk", + "America/La_Paz", + "America/Lima", + "America/Los_Angeles", + "America/Louisville", + "America/Lower_Princes", + "America/Maceio", + "America/Managua", + "America/Manaus", + "America/Marigot", + "America/Martinique", + "America/Matamoros", + "America/Mazatlan", + "America/Mendoza", + "America/Menominee", + "America/Merida", + "America/Metlakatla", + "America/Mexico_City", + "America/Miquelon", + "America/Moncton", + "America/Monterrey", + "America/Montevideo", + "America/Montreal", + "America/Montserrat", + "America/Nassau", + "America/New_York", + "America/Nipigon", + "America/Nome", + "America/Noronha", + "America/North_Dakota/Beulah", + "America/North_Dakota/Center", + "America/North_Dakota/New_Salem", + "America/Nuuk", + "America/Ojinaga", + "America/Panama", + "America/Pangnirtung", + "America/Paramaribo", + "America/Phoenix", + "America/Port-au-Prince", + "America/Port_of_Spain", + "America/Porto_Acre", + "America/Porto_Velho", + "America/Puerto_Rico", + "America/Punta_Arenas", + "America/Rainy_River", + "America/Rankin_Inlet", + "America/Recife", + "America/Regina", + "America/Resolute", + "America/Rio_Branco", + "America/Rosario", + "America/Santa_Isabel", + "America/Santarem", + "America/Santiago", + "America/Santo_Domingo", + "America/Sao_Paulo", + "America/Scoresbysund", + "America/Shiprock", + "America/Sitka", + "America/St_Barthelemy", + "America/St_Johns", + "America/St_Kitts", + "America/St_Lucia", + "America/St_Thomas", + "America/St_Vincent", + "America/Swift_Current", + "America/Tegucigalpa", + "America/Thule", + "America/Thunder_Bay", + "America/Tijuana", + "America/Toronto", + "America/Tortola", + "America/Vancouver", + "America/Virgin", + "America/Whitehorse", + "America/Winnipeg", + "America/Yakutat", + "America/Yellowknife", + "Antarctica/Casey", + "Antarctica/Davis", + "Antarctica/DumontDUrville", + "Antarctica/Macquarie", + "Antarctica/Mawson", + "Antarctica/McMurdo", + "Antarctica/Palmer", + "Antarctica/Rothera", + "Antarctica/South_Pole", + "Antarctica/Syowa", + "Antarctica/Troll", + "Antarctica/Vostok", + "Arctic/Longyearbyen", + "Asia/Aden", + "Asia/Almaty", + "Asia/Amman", + "Asia/Anadyr", + "Asia/Aqtau", + "Asia/Aqtobe", + "Asia/Ashgabat", + "Asia/Ashkhabad", + "Asia/Atyrau", + "Asia/Baghdad", + "Asia/Bahrain", + "Asia/Baku", + "Asia/Bangkok", + "Asia/Barnaul", + "Asia/Beirut", + "Asia/Bishkek", + "Asia/Brunei", + "Asia/Calcutta", + "Asia/Chita", + "Asia/Choibalsan", + "Asia/Chongqing", + "Asia/Chungking", + "Asia/Colombo", + "Asia/Dacca", + "Asia/Damascus", + "Asia/Dhaka", + "Asia/Dili", + "Asia/Dubai", + "Asia/Dushanbe", + "Asia/Famagusta", + "Asia/Gaza", + "Asia/Harbin", + "Asia/Hebron", + "Asia/Ho_Chi_Minh", + "Asia/Hong_Kong", + "Asia/Hovd", + "Asia/Irkutsk", + "Asia/Istanbul", + "Asia/Jakarta", + "Asia/Jayapura", + "Asia/Jerusalem", + "Asia/Kabul", + "Asia/Kamchatka", + "Asia/Karachi", + "Asia/Kashgar", + "Asia/Kathmandu", + "Asia/Katmandu", + "Asia/Khandyga", + "Asia/Kolkata", + "Asia/Krasnoyarsk", + "Asia/Kuala_Lumpur", + "Asia/Kuching", + "Asia/Kuwait", + "Asia/Macao", + "Asia/Macau", + "Asia/Magadan", + "Asia/Makassar", + "Asia/Manila", + "Asia/Muscat", + "Asia/Nicosia", + "Asia/Novokuznetsk", + "Asia/Novosibirsk", + "Asia/Omsk", + "Asia/Oral", + "Asia/Phnom_Penh", + "Asia/Pontianak", + "Asia/Pyongyang", + "Asia/Qatar", + "Asia/Qostanay", + "Asia/Qyzylorda", + "Asia/Rangoon", + "Asia/Riyadh", + "Asia/Saigon", + "Asia/Sakhalin", + "Asia/Samarkand", + "Asia/Seoul", + "Asia/Shanghai", + "Asia/Singapore", + "Asia/Srednekolymsk", + "Asia/Taipei", + "Asia/Tashkent", + "Asia/Tbilisi", + "Asia/Tehran", + "Asia/Tel_Aviv", + "Asia/Thimbu", + "Asia/Thimphu", + "Asia/Tokyo", + "Asia/Tomsk", + "Asia/Ujung_Pandang", + "Asia/Ulaanbaatar", + "Asia/Ulan_Bator", + "Asia/Urumqi", + "Asia/Ust-Nera", + "Asia/Vientiane", + "Asia/Vladivostok", + "Asia/Yakutsk", + "Asia/Yangon", + "Asia/Yekaterinburg", + "Asia/Yerevan", + "Atlantic/Azores", + "Atlantic/Bermuda", + "Atlantic/Canary", + "Atlantic/Cape_Verde", + "Atlantic/Faeroe", + "Atlantic/Faroe", + "Atlantic/Jan_Mayen", + "Atlantic/Madeira", + "Atlantic/Reykjavik", + "Atlantic/South_Georgia", + "Atlantic/St_Helena", + "Atlantic/Stanley", + "Australia/ACT", + "Australia/Adelaide", + "Australia/Brisbane", + "Australia/Broken_Hill", + "Australia/Canberra", + "Australia/Currie", + "Australia/Darwin", + "Australia/Eucla", + "Australia/Hobart", + "Australia/LHI", + "Australia/Lindeman", + "Australia/Lord_Howe", + "Australia/Melbourne", + "Australia/NSW", + "Australia/North", + "Australia/Perth", + "Australia/Queensland", + "Australia/South", + "Australia/Sydney", + "Australia/Tasmania", + "Australia/Victoria", + "Australia/West", + "Australia/Yancowinna", + "Brazil/Acre", + "Brazil/DeNoronha", + "Brazil/East", + "Brazil/West", + "CET", + "CST6CDT", + "Canada/Atlantic", + "Canada/Central", + "Canada/Eastern", + "Canada/Mountain", + "Canada/Newfoundland", + "Canada/Pacific", + "Canada/Saskatchewan", + "Canada/Yukon", + "Chile/Continental", + "Chile/EasterIsland", + "Cuba", + "EET", + "EST", + "EST5EDT", + "Egypt", + "Eire", + "Etc/GMT", + "Etc/GMT+0", + "Etc/GMT+1", + "Etc/GMT+10", + "Etc/GMT+11", + "Etc/GMT+12", + "Etc/GMT+2", + "Etc/GMT+3", + "Etc/GMT+4", + "Etc/GMT+5", + "Etc/GMT+6", + "Etc/GMT+7", + "Etc/GMT+8", + "Etc/GMT+9", + "Etc/GMT-0", + "Etc/GMT-1", + "Etc/GMT-10", + "Etc/GMT-11", + "Etc/GMT-12", + "Etc/GMT-13", + "Etc/GMT-14", + "Etc/GMT-2", + "Etc/GMT-3", + "Etc/GMT-4", + "Etc/GMT-5", + "Etc/GMT-6", + "Etc/GMT-7", + "Etc/GMT-8", + "Etc/GMT-9", + "Etc/GMT0", + "Etc/Greenwich", + "Etc/UCT", + "Etc/UTC", + "Etc/Universal", + "Etc/Zulu", + "Europe/Amsterdam", + "Europe/Andorra", + "Europe/Astrakhan", + "Europe/Athens", + "Europe/Belfast", + "Europe/Belgrade", + "Europe/Berlin", + "Europe/Bratislava", + "Europe/Brussels", + "Europe/Bucharest", + "Europe/Budapest", + "Europe/Busingen", + "Europe/Chisinau", + "Europe/Copenhagen", + "Europe/Dublin", + "Europe/Gibraltar", + "Europe/Guernsey", + "Europe/Helsinki", + "Europe/Isle_of_Man", + "Europe/Istanbul", + "Europe/Jersey", + "Europe/Kaliningrad", + "Europe/Kiev", + "Europe/Kirov", + "Europe/Lisbon", + "Europe/Ljubljana", + "Europe/London", + "Europe/Luxembourg", + "Europe/Madrid", + "Europe/Malta", + "Europe/Mariehamn", + "Europe/Minsk", + "Europe/Monaco", + "Europe/Moscow", + "Europe/Nicosia", + "Europe/Oslo", + "Europe/Paris", + "Europe/Podgorica", + "Europe/Prague", + "Europe/Riga", + "Europe/Rome", + "Europe/Samara", + "Europe/San_Marino", + "Europe/Sarajevo", + "Europe/Saratov", + "Europe/Simferopol", + "Europe/Skopje", + "Europe/Sofia", + "Europe/Stockholm", + "Europe/Tallinn", + "Europe/Tirane", + "Europe/Tiraspol", + "Europe/Ulyanovsk", + "Europe/Uzhgorod", + "Europe/Vaduz", + "Europe/Vatican", + "Europe/Vienna", + "Europe/Vilnius", + "Europe/Volgograd", + "Europe/Warsaw", + "Europe/Zagreb", + "Europe/Zaporozhye", + "Europe/Zurich", + "GB", + "GB-Eire", + "GMT", + "GMT+0", + "GMT-0", + "GMT0", + "Greenwich", + "HST", + "Hongkong", + "Iceland", + "Indian/Antananarivo", + "Indian/Chagos", + "Indian/Christmas", + "Indian/Cocos", + "Indian/Comoro", + "Indian/Kerguelen", + "Indian/Mahe", + "Indian/Maldives", + "Indian/Mauritius", + "Indian/Mayotte", + "Indian/Reunion", + "Iran", + "Israel", + "Jamaica", + "Japan", + "Kwajalein", + "Libya", + "MET", + "MST", + "MST7MDT", + "Mexico/BajaNorte", + "Mexico/BajaSur", + "Mexico/General", + "NZ", + "NZ-CHAT", + "Navajo", + "PRC", + "PST8PDT", + "Pacific/Apia", + "Pacific/Auckland", + "Pacific/Bougainville", + "Pacific/Chatham", + "Pacific/Chuuk", + "Pacific/Easter", + "Pacific/Efate", + "Pacific/Enderbury", + "Pacific/Fakaofo", + "Pacific/Fiji", + "Pacific/Funafuti", + "Pacific/Galapagos", + "Pacific/Gambier", + "Pacific/Guadalcanal", + "Pacific/Guam", + "Pacific/Honolulu", + "Pacific/Johnston", + "Pacific/Kanton", + "Pacific/Kiritimati", + "Pacific/Kosrae", + "Pacific/Kwajalein", + "Pacific/Majuro", + "Pacific/Marquesas", + "Pacific/Midway", + "Pacific/Nauru", + "Pacific/Niue", + "Pacific/Norfolk", + "Pacific/Noumea", + "Pacific/Pago_Pago", + "Pacific/Palau", + "Pacific/Pitcairn", + "Pacific/Pohnpei", + "Pacific/Ponape", + "Pacific/Port_Moresby", + "Pacific/Rarotonga", + "Pacific/Saipan", + "Pacific/Samoa", + "Pacific/Tahiti", + "Pacific/Tarawa", + "Pacific/Tongatapu", + "Pacific/Truk", + "Pacific/Wake", + "Pacific/Wallis", + "Pacific/Yap", + "Poland", + "Portugal", + "ROC", + "ROK", + "Singapore", + "Turkey", + "UCT", + "US/Alaska", + "US/Aleutian", + "US/Arizona", + "US/Central", + "US/East-Indiana", + "US/Eastern", + "US/Hawaii", + "US/Indiana-Starke", + "US/Michigan", + "US/Mountain", + "US/Pacific", + "US/Samoa", + "UTC", + "Universal", + "W-SU", + "WET", + "Zulu" +]; diff --git a/src/common/activityType.ts b/src/common/activityType.ts new file mode 100644 index 000000000..c16da33ec --- /dev/null +++ b/src/common/activityType.ts @@ -0,0 +1,24 @@ +import { ActivityStatus } from "@/chat-api/RawData"; +import { t } from "@nerimity/i18lite"; + +export interface ActivityType { + icon: string; + isMusic?: boolean; + isGame?: boolean; + isVideo?: boolean; +} + +export function getActivityType(activity?: ActivityStatus): ActivityType { + const isMusic = + activity?.action.startsWith(t("activityNames.listening")) || + activity?.action.startsWith("Listening to"); + + const isVideo = + activity?.action.startsWith(t("activityNames.watching")) || + activity?.action.startsWith("Watching"); + + if (isMusic) return { icon: "music_note", isMusic: true }; + + if (isVideo) return { icon: "movie", isVideo: true }; + return { icon: "gamepad", isGame: true }; +} diff --git a/src/common/arrayEquals.ts b/src/common/arrayEquals.ts new file mode 100644 index 000000000..d57774a35 --- /dev/null +++ b/src/common/arrayEquals.ts @@ -0,0 +1,6 @@ +export function arrayEquals( + a: A, + b: B +) { + return a.length === b.length && a.every((v, i) => v === b[i]); +} diff --git a/src/common/arrayMove.ts b/src/common/arrayMove.ts new file mode 100644 index 000000000..6c9091dcd --- /dev/null +++ b/src/common/arrayMove.ts @@ -0,0 +1,15 @@ +export function arrayMove( + arr: T[], + oldIndex: number, + newIndex: number +) { + const length = arr.length; + if (oldIndex === newIndex || oldIndex >= length || newIndex >= length) { + return arr; + } + + const newArr = arr.slice(); + const [itemToMove] = newArr.splice(oldIndex, 1); + newArr.splice(newIndex, 0, itemToMove!); + return newArr; +} diff --git a/src/common/classNames.ts b/src/common/classNames.ts index 291f29fac..0ae893a93 100644 --- a/src/common/classNames.ts +++ b/src/common/classNames.ts @@ -1,4 +1,6 @@ -export function classNames(...args: Array): string { +export function classNames( + ...args: Array +): string { return args.filter(Boolean).join(" "); } diff --git a/src/common/clipboard.ts b/src/common/clipboard.ts index c15f7e884..718c55a5a 100644 --- a/src/common/clipboard.ts +++ b/src/common/clipboard.ts @@ -1,3 +1,3 @@ export const copyToClipboard = (text: string) => { return navigator.clipboard.writeText(text); -}; \ No newline at end of file +}; diff --git a/src/common/color.ts b/src/common/color.ts new file mode 100644 index 000000000..c75789185 --- /dev/null +++ b/src/common/color.ts @@ -0,0 +1,128 @@ +export const convertShorthandToLinearGradient = (shorthand: string) => { + const parts = shorthand.trim().split(/\s+/); + + // Validation: Needs at least 3 parts (Start, Middle, End) for 2 colors + // 2 colors = 3 parts ("lg0#..." "0#..." "100") + // 3 colors = 4 parts + // 4 colors = 5 parts + if (parts.length < 3 || parts.length > 5) { + return [null, "Error: Invalid format (must represent 2-4 colors)"] as const; + } + + // 1. Parse First Chunk (lg + Degree + Hex1) + // Example: "lg0#2a7b9b" -> Deg: 0, Hex: #2a7b9b + const startMatch = parts[0]?.match(/^lg(\d+)(#[a-f0-9]{3,6})$/i); + if (!startMatch) + return [null, "Invalid start format (e.g., lg0#ffffff)"] as const; + + const degree = startMatch[1]; + const colors = [startMatch[2]]; // Start collecting hexes + const stops: string[] = []; // Start collecting stops + + // 2. Parse Middle Chunks (Stop + Hex) + // Example: "0#c5f3d8" -> Stop: 0, Hex: #c5f3d8 + for (let i = 1; i < parts.length - 1; i++) { + const middleMatch = parts[i]?.match(/^(\d+)(#[a-f0-9]{3,6})$/i); + if (!middleMatch) + return [null, `Invalid middle format at part ${i + 1}`] as const; + + stops.push(middleMatch[1]!); // Stop for previous color + colors.push(middleMatch[2]); // Hex for current color + } + + // 3. Parse Last Chunk (Final Stop) + // Example: "100" + const endMatch = parts[parts.length - 1]?.match(/^(\d+)$/); + if (!endMatch) + return [null, "Invalid end format (must be a number)"] as const; + + stops.push(endMatch[1]!); + + // 4. Combine into CSS String + // We zip the colors and stops arrays together + const cssStops = colors.map((hex, i) => `${hex} ${stops[i]}%`).join(", "); + + return [ + { gradient: `linear-gradient(${degree}deg, ${cssStops})`, colors }, + null + ] as const; +}; + +export interface ColorStop { + color: string; + percent: number; +} + +interface GradientData { + angle: number; + stops: ColorStop[]; +} + +const splitGradientParts = (value: string) => { + const parts: string[] = []; + let current = ""; + let depth = 0; + + for (let i = 0; i < value.length; i++) { + const char = value[i]!; + if (char === "(") depth += 1; + if (char === ")") depth = Math.max(0, depth - 1); + + if (char === "," && depth === 0) { + parts.push(current.trim()); + current = ""; + continue; + } + + current += char; + } + + if (current.trim()) { + parts.push(current.trim()); + } + + return parts; +}; + +const isSupportedColorStop = (value: string) => + /^#([a-f0-9]{3,4}|[a-f0-9]{6}|[a-f0-9]{8})$/i.test(value) || + /^rgba?\(/i.test(value) || + /^hsla?\(/i.test(value); + +export const parseGradient = (str: string): GradientData => { + const match = str.match(/linear-gradient\((.*)\)/i)?.[1] ?? ""; + + if (!match) { + return { angle: 180, stops: [] }; + } + + const parts = splitGradientParts(match); + + let angle = 180; + const firstPart = parts[0] ?? ""; + + if (firstPart.toLowerCase().includes("deg")) { + const degMatch = firstPart.match(/^(\d+)deg$/i); + if (degMatch?.[1]) { + angle = parseInt(degMatch[1], 10); + } + parts.shift(); + } + + const stops = parts.reduce((acc: ColorStop[], part) => { + const stopMatch = part.match(/^(.+?)\s+(\d+(?:\.\d+)?)%$/); + const color = stopMatch?.[1]?.trim(); + const percent = stopMatch?.[2]; + + if (color && percent && isSupportedColorStop(color)) { + acc.push({ + color, + percent: parseFloat(percent) + }); + } + + return acc; + }, []); + + return { angle, stops }; +}; diff --git a/src/common/createPreloader.ts b/src/common/createPreloader.ts new file mode 100644 index 000000000..4f758d865 --- /dev/null +++ b/src/common/createPreloader.ts @@ -0,0 +1,84 @@ +import { getUserDetailsRequest } from "@/chat-api/services/UserService"; +import useStore from "@/chat-api/store/useStore"; + +export function createPreloader( + fun: (...args: U) => Promise +) { + let timeout: NodeJS.Timeout | null = null; + + let currentPromise: Promise | null = null; + + let cache: { data: T; savedAt: number; key: string } | null = null; + const CACHE_TTL = 10000; + + const run = (...args: U): Promise => { + const newArgsStr = JSON.stringify(args); + + if (cache && cache.key === newArgsStr) { + if (Date.now() - cache.savedAt < CACHE_TTL) { + return Promise.resolve(cache.data); + } + cache = null; + } + + if (currentPromise && lastArgsStr === newArgsStr) { + return currentPromise; + } + + lastArgsStr = newArgsStr; + + const thisPromise = fun(...args) + .then((newData) => { + if (lastArgsStr === newArgsStr) { + cache = { + data: newData, + savedAt: Date.now(), + key: newArgsStr // Store the key with the data + }; + } + return newData; + }) + .catch((error) => { + if (currentPromise === thisPromise) { + currentPromise = null; + } + throw error; + }); + + currentPromise = thisPromise; + return currentPromise; + }; + + let lastArgsStr: string | null = null; + + const preload = (...args: U) => { + if (timeout) { + clearTimeout(timeout); + } + timeout = setTimeout(() => { + run(...args).catch(() => {}); + }, 200); + }; + + return { run, preload }; +} + +export const userDetailsPreloader = createPreloader(getUserDetailsRequest); + +export const messagesPreloader = createPreloader(async (channelId: string) => { + const store = useStore(); + + const messages = store.messages.getMessagesByChannelId(channelId); + + if (!messages) { + store.channelProperties.update(channelId, { + scrollTop: 9999, + moreTopToLoad: true, + moreBottomToLoad: false, + isScrolledBottom: true + }); + } + + await store.messages.fetchAndStoreMessages(channelId); + return true; +}); diff --git a/src/common/createUpdatedSignal.ts b/src/common/createUpdatedSignal.ts index 33ac3c44b..589379977 100644 --- a/src/common/createUpdatedSignal.ts +++ b/src/common/createUpdatedSignal.ts @@ -1,18 +1,24 @@ import { createEffect } from "solid-js"; import { createStore, reconcile } from "solid-js/store"; -export type CreatedUpdateSignal = [() => T, () => Partial, (key: keyof T, value: T[K]) => void]; +export type CreatedUpdateSignal = [ + () => T, + () => Partial, + (key: keyof T, value: T[K]) => void, + () => void +]; -export function createUpdatedSignal(defaultValues: () => T): CreatedUpdateSignal { // defaultValues will be an object - - const [values, setValues] = createStore({...defaultValues()} as any); +export function createUpdatedSignal( + defaultValues: () => T +): CreatedUpdateSignal { + // defaultValues will be an object + const [values, setValues] = createStore({ ...defaultValues() } as any); createEffect(() => { - setValues(reconcile({...defaultValues()})); + setValues(reconcile({ ...defaultValues() })); }); - // params of key and value const updateValue = (key: keyof T, value: T[K]) => { setValues(key as any, value as any); @@ -23,11 +29,12 @@ export function createUpdatedSignal(defaultValues: () => T): CreatedUpdateSig for (const key in values) { const defaultValue = defaultValues()[key]; const updatedValue = values[key]; - if (defaultValue !== updatedValue) { + if (JSON.stringify(defaultValue) !== JSON.stringify(updatedValue)) { updatedValues[key] = updatedValue; } } return updatedValues; }; - return [() => values, updatedValues, updateValue]; -} \ No newline at end of file + const undo = () => setValues(reconcile({ ...defaultValues() })); + return [() => values, updatedValues, updateValue, undo]; +} diff --git a/src/common/date.ts b/src/common/date.ts index 60fb5394f..272844058 100644 --- a/src/common/date.ts +++ b/src/common/date.ts @@ -1,134 +1,292 @@ -// make a function where if the number is less than 10, it will add a 0 in front of it -function pad(num: number) { - return num < 10 ? `0${num}` : num; -} +import { useLocalStorage, StorageKeys } from "@/common/localStorage"; +import "@formatjs/intl-durationformat/polyfill.js"; +import { getCurrentLanguageISO } from "@/locales/languages"; +import { Temporal, Intl } from "temporal-polyfill"; +import { t } from "@nerimity/i18lite"; +import { createMemo } from "solid-js"; -// convert timestamp to today at 13:00 or yesterday at 13:00 or date. add zero if single digit -export function formatTimestamp(timestamp: number) { - const date = new Date(timestamp); - const today = new Date(); +export const [timeFormat, setTimeFormat] = useLocalStorage<"12hr" | "24hr">( + StorageKeys.TIME_FORMAT, + "24hr", + true +); - const yesterday = new Date(); - yesterday.setDate(yesterday.getDate() - 1); +export const formatters = createMemo(() => { + const lang = getCurrentLanguageISO(); + const options = [lang, "en-GB"]; + return { + duration: { + long: new Intl.DurationFormat(options, { + style: "long" + }), + narrow: new Intl.DurationFormat(options, { + style: "narrow" + }), + narrowForceSeconds: new Intl.DurationFormat(options, { + style: "narrow", + secondsDisplay: "always" + }), + // H:MM:SS or MM:SS + digital: new Intl.DurationFormat(options, { + style: "narrow", + hoursDisplay: "auto", + hours: "numeric", + minutes: "2-digit", + seconds: "2-digit" + }), + // H:MM:SS or M:SS + digitalShort: new Intl.DurationFormat(options, { + style: "narrow", + hoursDisplay: "auto", + hours: "numeric", + minutes: "numeric", + seconds: "2-digit" + }) + }, + datetime: { + longDate: new Intl.DateTimeFormat(options, { + dateStyle: "full", + timeStyle: "short", + hour12: timeFormat() === "12hr" + }), + mediumDate: new Intl.DateTimeFormat(options, { + dateStyle: "medium", + timeStyle: "short", + hour12: timeFormat() === "12hr" + }), + seconds: new Intl.DateTimeFormat(options, { + timeStyle: "medium", + hour12: timeFormat() === "12hr" + }) + }, + relative: new Intl.RelativeTimeFormat(options, { + numeric: "auto" + }) + }; +}); - const sameYear = today.getFullYear() === date.getFullYear(); +/** + * Round a duration to two significant units. + */ +function roundDuration( + duration: Temporal.Duration, + start?: Temporal.ZonedDateTime, + options?: { + useWeeks?: boolean; + largestUnit?: Temporal.LargestUnit; + roundingMode?: Temporal.RoundingMode; + roundingIncrement?: number; + } +) { + if (options?.largestUnit) { + // Ensure the duration is balanced (turn 150s to 2m 30s) + duration = duration.round({ + relativeTo: start, + largestUnit: options.largestUnit + }); + } - if ( - sameYear && - date.getDate() === today.getDate() && - date.getMonth() === today.getMonth() - ) { - return `${pad(date.getHours())}:${pad(date.getMinutes())}`; - } else if (sameYear && yesterday.toDateString() === date.toDateString()) { - return `Yesterday at ${pad(date.getHours())}:${pad(date.getMinutes())}`; + if (options?.useWeeks) { + duration = duration.with({ + weeks: duration.weeks + Math.floor(duration.days / 7), + days: duration.days % 7 + }); + } + + const baseDuration = duration; + if (duration.sign === -1) { + duration = duration.negated(); + } + + let smallestUnit: Temporal.SmallestUnit; + let secondsOnly = false; + if (duration.years > 0) { + smallestUnit = "months"; + } else if (duration.months > 0) { + smallestUnit = options?.useWeeks ? "weeks" : "days"; + } else if (duration.weeks > 0) { + smallestUnit = "days"; + } else if (duration.days > 0) { + smallestUnit = "hours"; + } else if (duration.hours > 0) { + smallestUnit = "minutes"; + } else if (duration.minutes > 0) { + smallestUnit = "seconds"; } else { - return `${Intl.DateTimeFormat("en-GB", { - day: "2-digit", - month: "short", - year: "numeric", - }).format(date)} at ${pad(date.getHours())}:${pad(date.getMinutes())}`; + secondsOnly = true; + smallestUnit = "seconds"; } -} -// get days ago from timestamp -export function getDaysAgo(timestamp: number) { - const rtf = new Intl.RelativeTimeFormat("en", { - numeric: "auto", + const rounded = baseDuration.round({ + relativeTo: start, + smallestUnit, + ...options }); - const oneDayInMs = 1000 * 60 * 60 * 24; - const daysDifference = Math.round((timestamp - Date.now()) / oneDayInMs); - - return rtf.format(daysDifference, "day"); + return { + duration: rounded, + secondsOnly + }; } -export function timeSince(timestamp: number, showSeconds = false) { - const now = new Date(); - const secondsPast = Math.abs((now.getTime() - timestamp) / 1000); - if (secondsPast < 60) { - if (showSeconds) { - return Math.trunc(secondsPast) + " seconds ago"; - } - return "few seconds ago"; - } - if (secondsPast < 3600) { - return Math.trunc(secondsPast / 60) + " minutes ago"; - } - if (secondsPast <= 86400) { - return Math.trunc(secondsPast / 3600) + " hours ago"; - } - return formatTimestamp(timestamp); -} +// Format a message timestamp +export function formatTimestamp(timestampMs: number, seconds = false) { + try { + const today = Temporal.Now.zonedDateTimeISO(); + const timestamp = Temporal.Instant.fromEpochMilliseconds( + Math.round(timestampMs) + ) + .toZonedDateTimeISO(today.timeZoneId) + .round({ + roundingMode: "trunc", + smallestUnit: "second" + }); -export function timeElapsed( - timestamp: number, - onlyPadSeconds = false, - speed = 1, - updatedAt?: number -) { - const ms = Date.now() - timestamp; + const yesterday = today.subtract(Temporal.Duration.from({ days: 1 })); + const date = timestamp.toPlainDate(); - let seconds = ms / 1000; + const dateFormat = formatters().datetime.mediumDate; + const timeFormatSeconds = formatters().datetime.seconds; - if (updatedAt) { - const seekedSeconds = (updatedAt - timestamp) / 1000; - const seekedSecondsWithSpeed = seekedSeconds * speed; - const seekedSpeed = -(seekedSeconds - seekedSecondsWithSpeed); - seconds = seconds * speed - seekedSpeed; + if (date.equals(today.toPlainDate())) { + const formatter = seconds ? timeFormatSeconds : dateFormat; + return formatter.format(timestamp.toPlainTime()); + } else if (date.equals(yesterday.toPlainDate())) { + return t("datetime.yesterdayTime", { + time: dateFormat.format(timestamp.toPlainTime()) + }); + } else { + return t("datetime.dateTime", { + date: dateFormat.format(timestamp.toPlainDate()), + time: dateFormat.format(timestamp.toPlainTime()) + }); + } + } catch (e) { + console.warn(e); + return t("datetime.error"); } +} - seconds = Math.floor(seconds); +export const fullDate = (timestamp: number) => { + try { + const datetime = Temporal.Instant.fromEpochMilliseconds( + Math.round(timestamp) + ).toZonedDateTimeISO(Temporal.Now.timeZoneId()); + return formatters().datetime.longDate.format(datetime.toPlainDate()); + } catch (e) { + console.warn(e); + return t("datetime.error"); + } +}; - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds - hours * 3600) / 60); - seconds -= hours * 3600 + minutes * 60; - const formattedTime = - (hours - ? hours.toString().padStart(onlyPadSeconds ? 1 : 2, "0") + ":" - : "") + - minutes.toString().padStart(onlyPadSeconds ? 1 : 2, "0") + - ":" + - seconds.toString().padStart(2, "0"); - return formattedTime; -} -export function millisecondsToHhMmSs( - timestamp: number, - onlyPadSeconds = false -) { - let seconds = Math.floor(timestamp / 1000); - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds - hours * 3600) / 60); - seconds -= hours * 3600 + minutes * 60; - const formattedTime = - (hours - ? hours.toString().padStart(onlyPadSeconds ? 1 : 2, "0") + ":" - : "") + - minutes.toString().padStart(onlyPadSeconds ? 1 : 2, "0") + - ":" + - seconds.toString().padStart(2, "0"); - return formattedTime; +export function getDaysAgo(timestamp: number) { + try { + const now = Temporal.Now.zonedDateTimeISO(); + const start = Temporal.Instant.fromEpochMilliseconds( + Math.round(timestamp) + ).toZonedDateTimeISO(now.timeZoneId); + const elapsed = start.until(now, { + smallestUnit: "day" + }); + return formatters().relative.format(-elapsed.days, "day"); + } catch (e) { + console.warn(e); + return t("datetime.error"); + } } -export function millisecondsToReadable(timestamp: number) { - let seconds = Math.floor(timestamp / 1000); - const hours = Math.floor(seconds / 3600); - const minutes = Math.floor((seconds - hours * 3600) / 60); - seconds -= hours * 3600 + minutes * 60; +/** + * Format the duration since a timestamp with a single significant unit; + * falls back to using `formatTimestamp` if the duration is greater than + * a day unless `timestampFallback` is `false`. + */ +export function timeSince(timestamp: number, timestampFallback = true) { + try { + const now = Temporal.Now.zonedDateTimeISO(); + const start = Temporal.Instant.fromEpochMilliseconds( + Math.round(timestamp) + ).toZonedDateTimeISO(now.timeZoneId); + const elapsed = start.until(now, { + largestUnit: "day", + roundingMode: "trunc" + }); - let text = []; - - if (hours) { - text.push(`${hours}h`); + if (elapsed.days < 1 || !timestampFallback) { + const formatter = formatters().relative; + if (elapsed.days) { + return formatter.format(-elapsed.days, "day"); + } else if (elapsed.hours) { + return formatter.format(-elapsed.hours, "hour"); + } else if (elapsed.minutes) { + return formatter.format(-elapsed.minutes, "minute"); + } else { + return t("datetime.lessThanAMinuteAgo"); + } + } else { + return formatTimestamp(timestamp); + } + } catch (e) { + console.warn(e); + return t("datetime.error"); } +} - if (minutes) { - text.push(`${minutes}m`); +/** + * Formats the duration since a timestamp as a digital clock, rounding down. + */ +export function timeSinceDigital(timestamp: number) { + try { + const now = Temporal.Now.instant(); + const start = Temporal.Instant.fromEpochMilliseconds(Math.round(timestamp)); + const elapsed = start.until(now, { + largestUnit: "hour", + smallestUnit: "second", + roundingMode: "floor" + }); + return formatters().duration.digital.format(elapsed); + } catch (e) { + console.warn(e); + return t("datetime.error"); } +} - if (seconds) { - text.push(`${seconds}s`); +export function formatMillisElapsedDigital(milliseconds: number) { + try { + const duration = Temporal.Duration.from({ + milliseconds: Math.round(milliseconds) + }); + const rounded = duration.round({ + largestUnit: "hour", + smallestUnit: "second", + roundingMode: "floor" + }); + return formatters().duration.digitalShort.format(rounded); + } catch (e) { + console.warn(e); + return t("datetime.error"); } +} - return text.join(" "); +/** + * Formats a remaining duration with narrow units, rounding up. + * This will return "0s" when the duration is empty. + */ +export function formatMillisRemainingNarrow(millis: number) { + try { + const duration = Temporal.Duration.from({ + milliseconds: Math.round(millis) + }); + const rounded = roundDuration(duration, undefined, { + roundingMode: "ceil", + largestUnit: "hour" + }); + const formatter = rounded.secondsOnly + ? formatters().duration.narrowForceSeconds + : formatters().duration.narrow; + return formatter.format(rounded.duration); + } catch (e) { + console.warn(e); + return t("datetime.error"); + } } export function calculateTimeElapsedForActivityStatus( @@ -137,40 +295,96 @@ export function calculateTimeElapsedForActivityStatus( speed = 1, updatedAt?: number ) { - // Get the current time in milliseconds. - const now = Date.now(); - // Calculate the time elapsed in milliseconds. - const timeElapsedMS = now - startTime; - // Convert the time elapsed from milliseconds to seconds. - const timeElapsedInSeconds = timeElapsedMS / 1000; + try { + if (music) { + return activityMusicTimeElapsed(startTime, speed, updatedAt); + } + return activityStatusDuration(startTime); + } catch (e) { + console.warn(e); + return t("datetime.error"); + } +} + +function activityMusicTimeElapsed( + timestamp: number, + speed = 1, + updatedAt?: number +) { + const ms = Date.now() - timestamp; + let seconds = ms / 1000; + + if (updatedAt) { + const seekedSeconds = (updatedAt - timestamp) / 1000; + const seekedSecondsWithSpeed = seekedSeconds * speed; + const seekedSpeed = -(seekedSeconds - seekedSecondsWithSpeed); + seconds = seconds * speed - seekedSpeed; + } + return formatMillisElapsedDigital(seconds * 1000); +} + +function activityStatusDuration(startTime: number) { + const now = Temporal.Now.zonedDateTimeISO(); + const start = Temporal.Instant.fromEpochMilliseconds( + Math.round(startTime) + ).toZonedDateTimeISO(now.timeZoneId); + let elapsed = start.until(now, { + largestUnit: "years" + }); - if (music) { - return timeElapsed(startTime, true, speed, updatedAt); + if (elapsed.sign == -1) { + elapsed = new Temporal.Duration(); } + const rounded = roundDuration(elapsed, start, { useWeeks: true }); - // Return the time elapsed in seconds. - return convertSecondsForActivityStatus(timeElapsedInSeconds); + const formatter = rounded.secondsOnly + ? formatters().duration.narrowForceSeconds + : formatters().duration.narrow; + return formatter.format(rounded.duration); } -function convertSecondsForActivityStatus(totalSeconds: number) { - const days = Math.floor(totalSeconds / (24 * 60 * 60)); - totalSeconds %= 24 * 60 * 60; - const hours = Math.floor(totalSeconds / (60 * 60)); - totalSeconds %= 60 * 60; - const minutes = Math.floor(totalSeconds / 60); - const seconds = totalSeconds % 60; +type RelativeMode = "instant" | "duration" | "none"; - const roundedSeconds = Math.round(seconds); +/** + * Formats a timestamp as a relative offset to the current time. + */ +export function formatTimestampRelative( + timestamp: number, + mode?: RelativeMode +) { + try { + const now = Temporal.Now.zonedDateTimeISO(); + const start = Temporal.Instant.fromEpochMilliseconds( + Math.round(timestamp) + ).toZonedDateTimeISO(now.timeZoneId); + let elapsed = start.until(now, { + largestUnit: "years" + }); - if (days) { - return `${days}d ${hours}h`; - } + const inFuture = elapsed.sign == -1; + if (inFuture) { + elapsed = elapsed.negated(); + } + const rounded = roundDuration(elapsed, inFuture ? now : start, { + useWeeks: true + }); - if (hours) { - return `${hours}h ${minutes}m`; - } - if (minutes) { - return `${minutes} minute${minutes <= 1 ? "" : "s"}`; + if (rounded.secondsOnly && rounded.duration.seconds < 1) { + return t("datetime.relativeNow"); + } + + const duration = formatters().duration.long.format(rounded.duration); + if (mode === "none") { + return duration; + } else if (mode === "duration") { + return t("datetime.duration", { duration }); + } else if (inFuture) { + return t("datetime.relativeFuture", { duration }); + } else { + return t("datetime.relativePast", { duration }); + } + } catch (e) { + console.warn(e); + return t("datetime.error"); } - return `${roundedSeconds} second${roundedSeconds <= 1 ? "" : "s"}`; } diff --git a/src/common/debounce.ts b/src/common/debounce.ts new file mode 100644 index 000000000..e278e72cf --- /dev/null +++ b/src/common/debounce.ts @@ -0,0 +1,15 @@ +export function debounce void>( + func: T, + wait: number = 300 +): (...args: Parameters) => void { + let timeoutId: ReturnType | undefined; + + return (...args: Parameters) => { + if (timeoutId !== undefined) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + func(...args); + }, wait); + }; +} diff --git a/src/common/deepMerge.ts b/src/common/deepMerge.ts new file mode 100644 index 000000000..d36d5e063 --- /dev/null +++ b/src/common/deepMerge.ts @@ -0,0 +1,56 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +/** + * Recursively merges the properties of one or more source objects into a target object. + * + * @param target - The object to merge properties into. + * @param sources - The source objects to merge from. + * @returns A new object with the merged properties. + */ +export function deepMerge(target: T, ...sources: any[]): T { + if (!sources.length) { + return target; + } + + const source = sources.shift(); + + if (isObject(target) && isObject(source)) { + const output = { ...target } as T; // Create a shallow copy to avoid mutating the original target + + for (const key in source) { + if (Object.prototype.hasOwnProperty.call(source, key)) { + if (isObject(source[key])) { + if (!(key in target)) { + Object.assign(output, { [key]: source[key] }); + } else { + output[key] = deepMerge(target[key], source[key]); + } + } else if ( + Array.isArray(source[key]) && + Array.isArray((target as any)[key]) + ) { + // Concatenate arrays. You might want a different strategy here (e.g., merging objects within arrays by ID) + (output as any)[key] = [...(target as any)[key], ...source[key]]; + } else { + Object.assign(output, { [key]: source[key] }); + } + } + } + return deepMerge(output, ...sources); // Merge the rest of the sources + } else { + // If target or source is not an object (or different types), + // the source value overwrites the target value + return source; + } +} + +/** + * Checks if an item is a plain object (excluding arrays, null, and other object types like Date). + * + * @param item - The item to check. + * @returns True if the item is a plain object, false otherwise. + */ +function isObject(item: any): boolean { + return ( + item && typeof item === "object" && !Array.isArray(item) && item !== null + ); +} diff --git a/src/common/desktopNotification.ts b/src/common/desktopNotification.ts index 042ec5f97..43cffcb16 100644 --- a/src/common/desktopNotification.ts +++ b/src/common/desktopNotification.ts @@ -2,38 +2,59 @@ import { Channel } from "@/chat-api/store/useChannels"; import { Message } from "@/chat-api/store/useMessages"; import useStore from "@/chat-api/store/useStore"; import { StorageKeys, getStorageBoolean } from "./localStorage"; -import { MessageType, ServerNotificationPingMode } from "@/chat-api/RawData"; -import env from "./env"; -import { avatarUrl } from "@/chat-api/store/useUsers"; +import { + MessageType, + RawMessage, + ServerNotificationPingMode +} from "@/chat-api/RawData"; + +import { UserStatus } from "@/chat-api/store/useUsers"; import { ROLE_PERMISSIONS } from "@/chat-api/Bitwise"; +import { getSystemMessage } from "./SystemMessage"; + +import { t } from "@nerimity/i18lite"; +import { generateUrl } from "./image"; export function createDesktopNotification(message: Message) { - const enabled = getStorageBoolean(StorageKeys.ENABLE_DESKTOP_NOTIFICATION, false); + const enabled = getStorageBoolean( + StorageKeys.ENABLE_DESKTOP_NOTIFICATION, + false + ); if (!enabled) return; - const {channels, account, serverMembers} = useStore(); + const { channels, account, users, serverMembers } = useStore(); const channel = channels.get(message.channelId); + const user = () => users.get(account.user()?.id!); + + if (user()?.presence()?.status === UserStatus.DND) return; + const serverId = channel?.serverId; const channelId = channel?.id; - const notificationPing = !serverId? undefined : account.getCombinedNotificationSettings(serverId, channelId)?.notificationPingMode; + const notificationPing = !serverId + ? undefined + : account.getCombinedNotificationSettings(serverId, channelId) + ?.notificationPingMode; if (notificationPing === ServerNotificationPingMode.MUTE) return; - - let showNotification = false; - + let showNotification = true; if (notificationPing === ServerNotificationPingMode.MENTIONS_ONLY) { - const mentionedMe = message.mentions?.find(m => m.id === account.user()?.id); + showNotification = false; + const mentionedMe = message.mentions?.find( + (m) => m.id === account.user()?.id + ); if (mentionedMe) { showNotification = true; } - + const everyoneMentioned = message.content?.includes("[@:e]"); - if (!showNotification && (everyoneMentioned && serverId)) { + if (!showNotification && everyoneMentioned && serverId) { const member = serverMembers.get(serverId, message.createdBy.id); - const hasPerm = member?.isServerCreator() || member?.hasPermission(ROLE_PERMISSIONS.MENTION_EVERYONE); + const hasPerm = + serverMembers.isServerCreator(member!) || + serverMembers.hasPermission(member!, ROLE_PERMISSIONS.MENTION_EVERYONE); if (hasPerm) { showNotification = true; } @@ -43,63 +64,60 @@ export function createDesktopNotification(message: Message) { showNotification = true; } - if (!showNotification) return; - if (channel?.serverId) return createServerDesktopNotification(message, channel); + if (channel?.serverId) + return createServerDesktopNotification(message, channel); else return createDMDesktopNotification(message); - } function createServerDesktopNotification(message: Message, channel: Channel) { - const {servers, serverMembers} = useStore(); + const { servers, serverMembers } = useStore(); const server = servers.get(channel.serverId!); const member = serverMembers.get(server?.id || "", message.createdBy.id); let title = `${message.createdBy.username} (${server?.name} #${channel.name})`; let body = message.content; + if (body) { + body = formatMessage(message); + } + const username = member?.nickname || message.createdBy.username; if (!body && message.attachments?.length) { - body = "Image Message"; - } - if (message.type === MessageType.BAN_USER) { - body = `${username} has been banned.`, - title = `${server?.name} #${channel.name}`; - } - if (message.type === MessageType.KICK_USER) { - body = `${username} has been kicked.`, - title = `${server?.name} #${channel.name}`; - } - if (message.type === MessageType.JOIN_SERVER) { - body = `${username} joined the server.`, - title = `${server?.name} #${channel.name}`; - } - if (message.type === MessageType.LEAVE_SERVER) { - body = `${username} left the server.`, - title = `${server?.name} #${channel.name}`; + body = t("message.imageMessage"); } - if (message.type === MessageType.CALL_STARTED) { - body = `${username} started a call.`, + const systemMessage = getSystemMessage(message.type); + + if (systemMessage) { + const message = t(systemMessage.message) + .replace("", username) + .replace("<2>", "") + .replace("", ""); + + body = message; title = `${server?.name} #${channel.name}`; } - new Notification(title, { body, silent: true, tag: channel.id, renotify: true, - icon: server?.avatarUrl() || undefined + icon: generateUrl(server, "avatar") || undefined }); } function createDMDesktopNotification(message: Message) { const title = message.createdBy.username; let body = message.content; + if (body) { + body = formatMessage(message); + } + if (!body && message.attachments?.length) { - body = "Image Message"; + body = t("message.imageMessage"); } if (message.type === MessageType.CALL_STARTED) { body = `${message.createdBy.username} started a call.`; @@ -110,6 +128,34 @@ function createDMDesktopNotification(message: Message) { silent: true, tag: message.channelId, renotify: true, - icon: avatarUrl(message.createdBy) || undefined + icon: generateUrl(message.createdBy, "avatar") || undefined + }); +} + +const UserMentionRegex = /\[@:(.*?)\]/g; +const RoleMentionRegex = /\[r:(.*?)\]/g; +const CustomEmojiRegex = /\[[a]?ce:(.*?):(.*?)\]/g; +const commandRegex = /^(\/[^:\s]*):\d+( .*)?$/m; + +function formatMessage(message: RawMessage) { + const content = message.content; + if (!content) return; + + const mentionReplace = content.replace(UserMentionRegex, (_, id) => { + const user = message.mentions?.find((m) => m.id === id); + return user ? `@${user.username}` : _; }); -} \ No newline at end of file + + const roleReplace = mentionReplace.replace(RoleMentionRegex, (_, id) => { + const role = message.roleMentions?.find((m) => m.id === id); + return role ? `@${role.name}` : _; + }); + + const cEmojiReplace = roleReplace.replace(CustomEmojiRegex, (_, __, p2) => { + return `:${p2}:`; + }); + + const commandReplace = cEmojiReplace.replace(commandRegex, "$1$2"); + + return commandReplace; +} diff --git a/src/common/driveAPI.ts b/src/common/driveAPI.ts index 1f561a6da..9f89ab305 100644 --- a/src/common/driveAPI.ts +++ b/src/common/driveAPI.ts @@ -1,27 +1,31 @@ import { createSignal } from "solid-js"; import env from "./env"; +import { createProgressHandler } from "@/chat-api/services/Request"; -export const [googleApiInitialized, setGoogleApiInitialized] = createSignal(false); - -let initializing = false; -export const initializeGoogleDrive = (accessToken?: string) => new Promise(res => { - if (googleApiInitialized()) return; - if (initializing) return; - initializing = true; - const start = async () => { - await gapi.client.init({ - apiKey: env.GOOGLE_API_KEY, - discoveryDocs: ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"], - clientId: env.GOOGLE_CLIENT_ID - }); - accessToken && gapi.client.setToken({access_token: accessToken}); - initializing = false; - setGoogleApiInitialized(true); - res(); - }; - gapi.load("client", start); -}); +export const [googleApiInitialized, setGoogleApiInitialized] = + createSignal(false); +let initializing = false; +export const initializeGoogleDrive = (accessToken?: string) => + new Promise((res) => { + if (googleApiInitialized()) return; + if (initializing) return; + initializing = true; + const start = async () => { + await gapi.client.init({ + apiKey: env.GOOGLE_API_KEY, + discoveryDocs: [ + "https://www.googleapis.com/discovery/v1/apis/drive/v3/rest" + ], + clientId: env.GOOGLE_CLIENT_ID + }); + accessToken && gapi.client.setToken({ access_token: accessToken }); + initializing = false; + setGoogleApiInitialized(true); + res(); + }; + gapi.load("client", start); + }); let nerimityUploadsFolder: gapi.client.drive.File | undefined; @@ -37,7 +41,7 @@ export const getOrCreateUploadsFolder = async (accessToken: string) => { nerimityUploadsFolder = folder; return nerimityUploadsFolder; } - + const newFolder = await gapi.client.drive.files.create({ resource: { name: "nerimity_uploads", @@ -49,72 +53,72 @@ export const getOrCreateUploadsFolder = async (accessToken: string) => { return nerimityUploadsFolder; }; - // https://stackoverflow.com/questions/53839499/google-drive-api-and-file-uploads-from-the-browser -export const uploadFileGoogleDrive = async (file: File, accessToken: string, onProgress?: (percent: number) => void) => { +export const uploadFileGoogleDrive = async ( + file: File, + accessToken: string, + onProgress?: (percent: number, speed?: string) => void +) => { if (!googleApiInitialized()) await initializeGoogleDrive(accessToken); - gapi.client.setToken({access_token: accessToken}); + gapi.client.setToken({ access_token: accessToken }); const folder = await getOrCreateUploadsFolder(accessToken); const metadata = { - "name": file.name, - "mimeType": file.type, + name: file.name, + mimeType: file.type, parents: [folder.id!] }; - const form = new FormData(); - form.append("metadata", new Blob([JSON.stringify(metadata)], {type: "application/json"})); + form.append( + "metadata", + new Blob([JSON.stringify(metadata)], { type: "application/json" }) + ); form.append("file", file); - + const xhr = new XMLHttpRequest(); - xhr.open("post", "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,kind"); + xhr.open( + "post", + "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart&fields=id,name,kind" + ); xhr.setRequestHeader("Authorization", "Bearer " + accessToken); xhr.responseType = "json"; - - xhr.upload.onprogress = e => { - if (e.lengthComputable) { - const percentComplete = (e.loaded / e.total) * 100; - onProgress?.(Math.round(percentComplete)); - } + const progressHandler = createProgressHandler(onProgress); + xhr.upload.onprogress = (e) => { + progressHandler(e); }; - return new Promise<{id: string}>((resolve, reject) => { - xhr.onload = async () => { - + return new Promise<{ id: string }>((resolve, reject) => { + xhr.onload = async () => { if (xhr.status === 0) { - return reject({message: "Could not connect to server."}); + return reject({ message: "Could not connect to server." }); } if (xhr.status !== 200) { nerimityUploadsFolder = undefined; return reject(xhr.response); } const id = xhr.response.id; - + const body = { value: "default", type: "anyone", role: "reader" }; - - await gapi.client.drive.permissions - .create({ - fileId: id, - resource: body - }); + + await gapi.client.drive.permissions.create({ + fileId: id, + resource: body + }); resolve(xhr.response); }; xhr.send(form); }); }; - - export const getFile = async (fileId: string, fields?: string) => { const res = await gapi.client.drive.files.get({ fileId: fileId, fields: fields || "*" }); return res.result; - -}; \ No newline at end of file +}; diff --git a/src/common/emojiToUrl.ts b/src/common/emojiToUrl.ts new file mode 100644 index 000000000..b3384f8b3 --- /dev/null +++ b/src/common/emojiToUrl.ts @@ -0,0 +1,17 @@ +import { unicodeToTwemojiUrl } from "@/emoji"; +import env from "./env"; + +export const emojiToUrl = (emoji: string, hovered: boolean, size?: number) => { + if (emoji.includes(".")) { + const url = new URL( + `${env.NERIMITY_CDN}emojis/${emoji}${ + !hovered && emoji?.endsWith(".gif") ? "?type=webp" : "" + }` + ); + if (size) { + url.searchParams.set("size", size.toString()); + } + return url.href; + } + return unicodeToTwemojiUrl(emoji); +}; diff --git a/src/common/env.ts b/src/common/env.ts index 5cf9eb1fd..07171bb36 100644 --- a/src/common/env.ts +++ b/src/common/env.ts @@ -1,14 +1,16 @@ export default { SERVER_URL: import.meta.env.VITE_SERVER_URL, + WS_URL: import.meta.env.VITE_WS_URL, APP_URL: import.meta.env.VITE_APP_URL, MOBILE_WIDTH: parseInt(import.meta.env.VITE_MOBILE_WIDTH), APP_VERSION: import.meta.env.VITE_APP_VERSION as string | undefined, DEV_MODE: import.meta.env.VITE_DEV_MODE === "true", - MESSAGE_LIMIT: parseInt(import.meta.env.VITE_MESSAGE_LIMIT || "50"), + MESSAGE_LIMIT: parseInt(import.meta.env.VITE_MESSAGE_LIMIT || "50"), TURNSTILE_SITEKEY: import.meta.env.VITE_TURNSTILE_SITEKEY, EMOJI_URL: import.meta.env.VITE_EMOJI_URL, NERIMITY_CDN: import.meta.env.VITE_NERIMITY_CDN, OFFICIAL_SERVER: import.meta.env.VITE_OFFICIAL_SERVER || "nerimity", GOOGLE_CLIENT_ID: import.meta.env.VITE_GOOGLE_CLIENT_ID as string | undefined, - GOOGLE_API_KEY: import.meta.env.VITE_GOOGLE_API_KEY as string | undefined -}; \ No newline at end of file + GOOGLE_API_KEY: import.meta.env.VITE_GOOGLE_API_KEY as string | undefined, + RELEASE_TIMESTAMP: parseInt(import.meta.env.VITE_RELEASE_TIMESTAMP || 0) +}; diff --git a/src/common/experiments.tsx b/src/common/experiments.tsx index ef6e7fb7f..da5fcd05b 100644 --- a/src/common/experiments.tsx +++ b/src/common/experiments.tsx @@ -1,4 +1,4 @@ -import { StorageKeys, useReactiveLocalStorage } from "./localStorage"; +import { StorageKeys, useLocalStorage } from "./localStorage"; import { JSXElement, Show } from "solid-js"; export interface Experiment { @@ -6,28 +6,46 @@ export interface Experiment { name: string; description?: string; electron?: boolean; + reactNative?: boolean; reloadRequired?: boolean; + onToggle?: () => void; } export const Experiments = [ + // { + // id: "WEBSOCKET_PARTIAL_AUTH", + // name: "WebSocket Partial Authentication", + // description: + // "VERY BROKEN. Don't send all auth data when authenticating. This will be used in the future to speed up authentication, hopefully." + // }, { - id: "HOME_DRAWER", - name: "New Home Drawer", + id: "WEBSOCKET_ZSTD", + name: "WebSocket Zstandard Compression", description: - "Enable the new home drawer, which replaces the current inbox drawer.", + "Compress some events with Zstandard compression. This can reduce bandwidth usage." }, { - id: "QUICK_TRAVEL", - name: "Quick Travel", - description: "Press CTRL + SPACE to open Quick Travel.", + id: "RN_NATIVE_WS", + name: "React Native Native WebSocket", + reactNative: true, + description: + "Use the socket.io in react native instead of webview. Will be needed for native WebRTC for stable video calls on the mobile app." }, + { + id: "RN_NATIVE_WEBRTC", + name: "React Native Native WebRTC", + reactNative: true, + description: + "Use native WebRTC instead of webview. IMPORTANT: Need to enable react native native websocket first." + } ] as const; export type ExperimentIds = (typeof Experiments)[number]["id"]; -const [enabledExperiments, setEnabledExperiments] = useReactiveLocalStorage< - string[] ->(StorageKeys.ENABLED_EXPERIMENTS, []); +const [enabledExperiments, setEnabledExperiments] = useLocalStorage( + StorageKeys.ENABLED_EXPERIMENTS, + [] +); export const isExperimentEnabled = (experimentId: ExperimentIds) => { return () => enabledExperiments().includes(experimentId); @@ -66,6 +84,6 @@ export const useExperiment = (experimentId: () => ExperimentIds) => { return { experiment, - toggleExperiment: toggle, + toggleExperiment: toggle }; }; diff --git a/src/common/exploreRoutes.ts b/src/common/exploreRoutes.ts index 054425492..8bad17147 100644 --- a/src/common/exploreRoutes.ts +++ b/src/common/exploreRoutes.ts @@ -1,22 +1,38 @@ +import { t } from "@nerimity/i18lite"; import { lazy } from "solid-js"; export interface ExploreRoute { - path?: string; - routePath: string; - name: string; - icon: string; - element: any + path?: string; + match?: string; + routePath: string; + name: () => string; + icon: string; + element: any; } -const exploreRoutes: ExploreRoute[] = [ +const exploreRoutes: ExploreRoute[] = [ { path: "servers", routePath: "/servers", - name: "explore.drawer.servers", + match: "/servers/*", + name: () => t("explore.drawer.servers"), icon: "dns", element: lazy(() => import("@/components/explore/ExploreServers")) + }, + { + path: "bots", + routePath: "/bots", + name: () => t("explore.drawer.bots"), + icon: "smart_toy", + element: lazy(() => import("@/components/explore/ExploreBots")) + }, + { + path: "themes", + routePath: "/themes", + name: () => t("explore.drawer.themes"), + icon: "brush", + element: lazy(() => import("@/components/explore/ExploreThemes")) } - ]; -export default exploreRoutes; \ No newline at end of file +export default exploreRoutes; diff --git a/src/common/favoritesStore.ts b/src/common/favoritesStore.ts new file mode 100644 index 000000000..2d1804873 --- /dev/null +++ b/src/common/favoritesStore.ts @@ -0,0 +1,41 @@ +import { StorageKeys, useLocalStorage } from "./localStorage"; + +export interface FavoriteGif { + url: string; + gifUrl: string; + previewUrl: string; + previewHeight: number; + previewWidth: number; + tags?: string[]; +} + +// Map of URL -> FavoriteGif +type FavoritesMap = Record; + +export const [favorites, setFavorites] = useLocalStorage( + StorageKeys.FAVORITE_GIFS, + {} +); + +export const favoritesStore = { + add: (gif: FavoriteGif) => { + const newFavorites = { + ...favorites(), + [gif.url]: gif + }; + setFavorites(newFavorites); + }, + remove: (url: string) => { + const newFavorites = { + ...favorites() + }; + delete newFavorites[url]; + setFavorites(newFavorites); + }, + isFavorite: (url: string) => { + return !!favorites()[url]; + }, + getFavorites: () => { + return Object.values(favorites()); + } +}; diff --git a/src/common/fileToDataUrl.ts b/src/common/fileToDataUrl.ts index 68d33b1ae..3bc88c7d9 100644 --- a/src/common/fileToDataUrl.ts +++ b/src/common/fileToDataUrl.ts @@ -1,12 +1,15 @@ - export function fileToDataUrl(file: File): Promise { - return new Promise(resolve => { + return new Promise((resolve) => { const reader = new FileReader(); - reader.addEventListener("load", () => { - resolve(reader.result as string); - },false); - + reader.addEventListener( + "load", + () => { + resolve(reader.result as string); + }, + false + ); + reader.readAsDataURL(file); }); -} \ No newline at end of file +} diff --git a/src/common/fonts.ts b/src/common/fonts.ts new file mode 100644 index 000000000..1d403a3fa --- /dev/null +++ b/src/common/fonts.ts @@ -0,0 +1,109 @@ +export interface Font { + id: number; + name: string; + class: string; + import: () => Promise<{ default: typeof import("*.css") }>; + scale: number; + lineHeight?: number; + letterSpacing?: string; +} +export const Fonts: Font[] = [ + { + id: 0, + name: "Inter", + class: "font-inter", + import: () => import("@fontsource/inter/latin-400.css"), + scale: 1, + lineHeight: 1.2 + }, + { + id: 1, + name: "Pixelify Sans", + class: "font-pixelify-sans", + import: () => import("@fontsource/pixelify-sans/latin-400.css"), + scale: 1 + }, + { + id: 2, + name: "Indie Flower", + class: "font-indie-flower", + import: () => import("@fontsource/indie-flower/latin-400.css"), + scale: 1.33, + lineHeight: 0.9 + }, + { + id: 3, + name: "IBM Plex Mono", + class: "font-ibm-plex-mono", + import: () => import("@fontsource/ibm-plex-mono/latin-400.css"), + scale: 0.9 + }, + { + id: 4, + name: "Dancing Script", + class: "font-dancing-script", + import: () => import("@fontsource/dancing-script/latin-400.css"), + scale: 1 + }, + { + id: 5, + name: "Mochiy Pop One", + class: "font-mochiy-pop-one", + import: () => import("@fontsource/mochiy-pop-one/latin-400.css"), + scale: 0.8 + }, + { + id: 6, + name: "Grandstander", + class: "font-grandstander", + import: () => import("@fontsource/grandstander/latin-400.css"), + scale: 1.2 + }, + { + id: 7, + name: "Sora", + class: "font-sora", + import: () => import("@fontsource/sora/latin-400.css"), + scale: 0.9 + }, + { + id: 8, + name: "Roboto Slab", + class: "font-roboto-slab", + import: () => import("@fontsource/roboto-slab/latin-400.css"), + scale: 0.9 + }, + { + id: 9, + name: "Finger Paint", + class: "font-finger-paint", + import: () => import("@fontsource/finger-paint/latin-400.css"), + scale: 1 + } +]; + +const generateStylesheet = () => { + const el = document.createElement("style"); + el.textContent = Fonts.map((f) => { + return `.${f.class} { + --font: '${f.name}'; + --lh: ${f.lineHeight ?? "initial"}; + --scale: ${f.scale ?? "initial"}; + --ls: ${f.letterSpacing ?? "initial"}; + }`; + }).join("\n"); + return el; +}; + +document.head.appendChild(generateStylesheet()); + +export const getFont = (id: number) => + Fonts.find((f) => f.id === id) || Fonts[0]; + +export const loadAllFonts = async () => { + for (const font of Fonts) { + await font.import(); + } +}; + +loadAllFonts(); diff --git a/src/common/image.ts b/src/common/image.ts new file mode 100644 index 000000000..84a0bff8b --- /dev/null +++ b/src/common/image.ts @@ -0,0 +1,6 @@ +import env from "./env"; + +export const generateUrl = ( + item: undefined | { avatar?: string; banner?: string }, + type: "avatar" | "banner" +): string | null => (item?.[type] ? env.NERIMITY_CDN + item?.[type] : null); diff --git a/src/common/kaomoji.ts b/src/common/kaomoji.ts index f216666a3..5a1d3ca9c 100644 --- a/src/common/kaomoji.ts +++ b/src/common/kaomoji.ts @@ -65,4 +65,5 @@ const kaomojis = [ "||Φ|(|゚|∀|゚|)|Φ||" ]; -export const randomKaomoji = () => kaomojis[Math.floor(Math.random() * kaomojis.length)]; \ No newline at end of file +export const randomKaomoji = () => + kaomojis[Math.floor(Math.random() * kaomojis.length)]; diff --git a/src/common/localCache.ts b/src/common/localCache.ts index c4a476b18..f86f11579 100644 --- a/src/common/localCache.ts +++ b/src/common/localCache.ts @@ -1,8 +1,7 @@ import { set, get, clear } from "idb-keyval"; - export enum LocalCacheKey { - Account = "account", + Account = "account" } export function saveCache(name: LocalCacheKey, data: any) { @@ -12,4 +11,4 @@ export function saveCache(name: LocalCacheKey, data: any) { export function getCache(name: LocalCacheKey) { return get(name); } -export const clearCache = clear; \ No newline at end of file +export const clearCache = clear; diff --git a/src/common/localStorage.ts b/src/common/localStorage.ts index 5aaf6101e..0c49fec9b 100644 --- a/src/common/localStorage.ts +++ b/src/common/localStorage.ts @@ -1,26 +1,53 @@ -import { createSignal, onMount } from "solid-js"; - -export enum StorageKeys { - USER_TOKEN = "userToken", - SEEN_APP_VERSION = "seenAppVersion", - INBOX_DRAWER_SELECTED_INDEX = "inboxDrawerSelectedIndex", - APP_LANGUAGE = "appLanguage", - FIRST_TIME = "firstTime", // After registering, this is set to true. - ARE_NOTIFICATIONS_MUTED = "areNotificationsMuted", - NOTIFICATION_VOLUME = "notificationVolume", - ENABLE_DESKTOP_NOTIFICATION = "enableDesktopNotification", - LAST_SELECTED_SERVER_CHANNELS = "lastSelectedServerChannels", - LAST_SEEN_CHANNEL_NOTICES = "lastSeenChannelNotices", - PROGRAM_ACTIVITY_STATUS = "programActivityStatus", - BLUR_EFFECT_ENABLED = "blurEffectEnabled", - ENABLED_EXPERIMENTS = "enabledExperiments", - DISABLED_ADVANCED_MARKUP = "disabledAdvancedMarkup", - NOTIFICATION_SOUNDS = "notificationSounds", - CUSTOM_CSS = "customCss", - CUSTOM_COLORS = "customColors", - inputDeviceId = "inputDeviceId", - outputDeviceId = "outputDeviceId", -} +import { Signal } from "solid-js"; +import { createStore } from "solid-js/store"; + +export const StorageKeys = { + USER_TOKEN: "userToken", + CDN_TOKEN: "cdnToken", + SEEN_APP_VERSION: "seenAppVersion", + INBOX_DRAWER_SELECTED_INDEX: "inboxDrawerSelectedIndex", + APP_LANGUAGE: "appLanguage", + FIRST_TIME: "firstTime", // After registering, this is set to true. + ARE_NOTIFICATIONS_MUTED: "areNotificationsMuted", + IN_APP_NOTIFICATIONS_PREVIEW: "inAppNotificationsPreview", + NOTIFICATION_VOLUME: "notificationVolume", + ENABLE_DESKTOP_NOTIFICATION: "enableDesktopNotification", + LAST_SELECTED_SERVER_CHANNELS: "lastSelectedServerChannels", + LAST_SEEN_CHANNEL_NOTICES: "lastSeenChannelNotices", + PROGRAM_ACTIVITY_STATUS: "programActivityStatus", + BLUR_EFFECT_ENABLED: "blurEffectEnabled", + REDUCE_MOTION_MODE: "reduceMotionMode", + ENABLED_EXPERIMENTS: "enabledExperiments", + DISABLED_ADVANCED_MARKUP: "disabledAdvancedMarkup", + NOTIFICATION_SOUNDS: "notificationSounds", + CUSTOM_CSS: "customCss", + CUSTOM_COLORS: "customColors", + inputDeviceId: "inputDeviceId", + outputDeviceId: "outputDeviceId", + voiceInputMode: "voiceInputMode", + voiceMicConstraints: "voiceMicConstraints", + voiceUseTurnServers: "useTurnServers", + PTTBoundKeys: "pttBoundKeys", + USE_TWITTER_EMBED: "useTwitterEmbed", + DISCORD_USER_ID: "discordUserId", + LASTFM: "lastfm", + SIDEBAR_WIDTH: "sidebarWidth", + LEFT_DRAWER_WIDTH: "leftDrawerWidth", + RIGHT_DRAWER_WIDTH: "rightDrawerWidth", + COLLAPSED_SERVER_CATEGORIES: "collapsedServerCategories", + ANNOUNCEMENTS_CACHE: "announcementsCache", + HIDDEN_ANNOUNCEMENT_IDS: "hiddenAnnouncementIds", + CHAT_BAR_OPTIONS: "chatBarOptions", + MENTION_REPLIES: "mentionReplies", + TIME_FORMAT: "timeFormat", + DASHBOARD_POST_SORT: "dashboardPostSort", + rightDrawerMode: "rightDrawerMode", + FAVORITE_GIFS: "favoriteGifs", + USE_LATEST_URL: "useLatestURL", // check for mobile and desktop app on wether to use the latest.nerimity.com url or not + HOME_ICON: "homeIcon" +} as const; + +export type StorageKeys = (typeof StorageKeys)[keyof typeof StorageKeys]; export function getStorageBoolean( key: StorageKeys, @@ -56,11 +83,15 @@ export function setStorageNumber(key: StorageKeys, value: number) { } export function getStorageObject(key: StorageKeys, defaultValue: T): T { - const value = getStorageString(key, null); - if (value === null) { + try { + const value = getStorageString(key, null); + if (value === null) { + return defaultValue; + } + return JSON.parse(value); + } catch { return defaultValue; } - return JSON.parse(value); } export function setStorageObject(key: StorageKeys, value: T) { @@ -71,16 +102,55 @@ export function removeStorage(key: StorageKeys) { localStorage.removeItem(key); } -export function useReactiveLocalStorage(key: StorageKeys, defaultValue: T) { - const [value, setValue] = createSignal(defaultValue); +export function useLocalStorage( + key: StorageKeys, + defaultValue: T, + stringMode = false +) { + const [value, setValue] = createStore<{ v: T }>({ v: defaultValue }); - const storedValue = getStorageObject(key, defaultValue); - setValue(() => storedValue); + const storedValue = stringMode + ? getStorageString(key, defaultValue) + : getStorageObject(key, defaultValue); + setValue({ v: storedValue as T }); - const setCustomValue = (value: T) => { - setValue(() => value); - setStorageString(key, JSON.stringify(value)); + const setCustomValue = (newValue: T) => { + setValue("v", newValue); + if (stringMode) return setStorageString(key, value.v as string); + setStorageString(key, JSON.stringify(value.v)); }; - return [value, setCustomValue] as const; + const getVal = () => value.v; + + return [getVal, setCustomValue] as Signal; } + +type VoiceInputMode = "OPEN" | "VOICE_ACTIVITY" | "PTT"; +const voiceInputMode = useLocalStorage( + StorageKeys.voiceInputMode, + "VOICE_ACTIVITY" +); + +const collapsedServerCategories = useLocalStorage( + StorageKeys.COLLAPSED_SERVER_CATEGORIES, + [] +); + +export const useCollapsedServerCategories = () => collapsedServerCategories; + +export const useVoiceInputMode = () => voiceInputMode; + +export const useChatBarOptions = () => { + return useLocalStorage(StorageKeys.CHAT_BAR_OPTIONS, [ + "vm", + "gif", + "emoji", + "send" + ] as const); +}; + +type RightDrawerMode = "SWIPE" | "HEADER_CLICK"; +export const rightDrawerMode = useLocalStorage( + StorageKeys.rightDrawerMode, + "SWIPE" +); diff --git a/src/common/logger.ts b/src/common/logger.ts new file mode 100644 index 000000000..d79ae4731 --- /dev/null +++ b/src/common/logger.ts @@ -0,0 +1,20 @@ +const LogType = { + WebSocket: { + prefix: "WebSocket", + color: "color: #4F46E5;" + }, + RTC: { + prefix: "WebRTC", + color: "color: #059669;" + }, + UPDATER: { + prefix: "Updater", + color: "color: #D97706;" + } +} as const; + +export const log = (type: keyof typeof LogType, ...args: unknown[]) => { + const logType = LogType[type]; + if (!logType) return; + console.log(`%c[${logType.prefix}]`, logType.color, ...args); +}; diff --git a/src/common/logout.ts b/src/common/logout.ts index 15cb3de21..5f53fac76 100644 --- a/src/common/logout.ts +++ b/src/common/logout.ts @@ -1,9 +1,19 @@ +import { userLogout } from "@/chat-api/services/UserService"; import { clearCache } from "./localCache"; import { reactNativeAPI } from "./ReactNative"; -export const logout = async () => { - reactNativeAPI()?.logout(); - await clearCache(); - localStorage.clear(); - location.href = "/"; +export const logout = (redirect = true, keepCache = false) => { + userLogout(); + + setTimeout(async () => { + reactNativeAPI()?.logout(); + if (!keepCache) { + await clearCache(); + localStorage.clear(); + } + localStorage.removeItem("userToken"); + if (redirect) { + location.href = "/"; + } + }, 500); }; diff --git a/src/common/prettyBytes.ts b/src/common/prettyBytes.ts index 2458d78c1..823608bb9 100644 --- a/src/common/prettyBytes.ts +++ b/src/common/prettyBytes.ts @@ -1,7 +1,10 @@ export const prettyBytes = (num: number, precision = 3, addSpace = true) => { const UNITS = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"]; if (Math.abs(num) < 1) return num + (addSpace ? " " : ""); - const exponent = Math.min(Math.floor(Math.log10(num) / Math.log10(1024)), UNITS.length - 1); + const exponent = Math.min( + Math.floor(Math.log10(num) / Math.log10(1024)), + UNITS.length - 1 + ); const n = Number(((num < 0 ? -1 : 1) * num) / Math.pow(1024, exponent)); return (num < 0 ? "-" : "") + n.toFixed(precision) + " " + UNITS[exponent]; -}; \ No newline at end of file +}; diff --git a/src/common/promiseTimers.ts b/src/common/promiseTimers.ts new file mode 100644 index 000000000..a080dc120 --- /dev/null +++ b/src/common/promiseTimers.ts @@ -0,0 +1,3 @@ +export const promiseTimers = { + setTimeout: (ms: number) => new Promise((res) => setTimeout(res, ms)) +}; diff --git a/src/common/regex.ts b/src/common/regex.ts new file mode 100644 index 000000000..874047622 --- /dev/null +++ b/src/common/regex.ts @@ -0,0 +1,11 @@ +import env from "./env"; + +export const inviteLinkRegex = new RegExp( + `(?)` +); + +export const youtubeLinkRegex = + /(youtu.*be.*)\/(watch\?v=|embed\/|v|shorts|)(.*?((?=[&#?])|$))/; + +export const twitterStatusLinkRegex = + /https:\/\/(www.)?(twitter|x)\.com(\/[a-zA-Z0-9_]+\/status\/[0-9]+)/; diff --git a/src/common/runWithContext.ts b/src/common/runWithContext.ts index 296673b8e..690b90475 100644 --- a/src/common/runWithContext.ts +++ b/src/common/runWithContext.ts @@ -8,4 +8,4 @@ export const setContext = () => { export function runWithContext(callback: () => T) { return runWithOwner(ctx!, callback); -} \ No newline at end of file +} diff --git a/src/common/themes.ts b/src/common/themes.ts index 55996e6f0..5df79c146 100644 --- a/src/common/themes.ts +++ b/src/common/themes.ts @@ -1,49 +1,349 @@ -import { StorageKeys, useReactiveLocalStorage } from "./localStorage"; - -export const theme = { - "background-color": "hsl(216deg 9% 8%)", - "pane-color": "hsl(216deg 8% 15%)", - "header-background-color": "rgba(48, 48, 48, 0.86)", - "header-background-color-blur-disabled": "rgb(48, 48, 48)", - "tooltip-background-color": "rgb(40, 40, 40)", - "primary-color": "#4c93ff", - "alert-color": "#eb6e6e", - "warn-color": "#e8a859", - "success-color": "#78e380", - "success-color-dark": "#1c221d", - "primary-color-dark": "#2d3746", - "alert-color-dark": "#3e2626", - "warn-color-dark": "#3a3229", -}; - -type ThemeKey = keyof typeof theme; - -const [customColors, setCustomColors] = useReactiveLocalStorage< +import { reconcile } from "solid-js/store"; +import { StorageKeys, useLocalStorage, setStorageString } from "./localStorage"; + +export const ThemeCategory = { + Surface: "Surface", + Overlays: "Overlays", + Input: "Input", + MarkupBar: "Markup Bar", + Message: "Message", + Accent: "Accent", + Alert: "Alert", + Warn: "Warn", + Success: "Success", + Status: "Status", + Text: "Text", + Markup: "Markup", + Drawer: "Drawer" +} as const; + +const ThemeTokensBase = [ + // Surface + { + key: "background-color", + category: ThemeCategory.Surface, + value: "#000000", + allowGradient: true + }, + { + key: "pane-color", + category: ThemeCategory.Surface, + value: "#000000", + allowGradient: true + }, + { + key: "side-pane-color", + category: ThemeCategory.Surface, + value: "#0f0f0f", + allowGradient: true + }, + + // Overlays + { + key: "header-background-color", + category: ThemeCategory.Overlays, + value: "#111111cc" + }, + { + key: "header-background-color-blur-disabled", + category: ThemeCategory.Overlays, + value: "#000000" + }, + { + key: "tooltip-background-color", + category: ThemeCategory.Overlays, + value: "#0a0a0a" + }, + + // Input + { + key: "chat-input-background-color", + category: ThemeCategory.Input, + value: "rgba(0, 0, 0, 0.86)" + }, + { + key: "chat-input-background-color-blur-disabled", + category: ThemeCategory.Input, + value: "black" + }, + + // Markup bar + { + key: "chat-markup-bar-background-color", + category: ThemeCategory.MarkupBar, + value: "rgba(0, 0, 0, 0.86)" + }, + { + key: "chat-markup-bar-background-color-blur-disabled", + category: ThemeCategory.MarkupBar, + value: "black" + }, + + // Message + { + key: "message-hover-background-color", + category: ThemeCategory.Message, + value: "rgba(255, 255, 255, 0.1)" + }, + { + key: "message-floating-options-background-color", + category: ThemeCategory.Message, + value: "rgba(15, 15, 15, 1)" + }, + + // Accent (Primary) + { key: "primary-color", category: ThemeCategory.Accent, value: "#4c93ff" }, + { + key: "primary-color-dark", + category: ThemeCategory.Accent, + value: "#2d3746" + }, + + // Alert + { key: "alert-color", category: ThemeCategory.Alert, value: "#eb6e6e" }, + { key: "alert-color-dark", category: ThemeCategory.Alert, value: "#3e2626" }, + + // Warn + { key: "warn-color", category: ThemeCategory.Warn, value: "#ff8f2c" }, + { key: "warn-color-dark", category: ThemeCategory.Warn, value: "#3a3229" }, + + // Success + { key: "success-color", category: ThemeCategory.Success, value: "#78e380" }, + { + key: "success-color-dark", + category: ThemeCategory.Success, + value: "#1c221d" + }, + + // Status + { key: "status-offline", category: ThemeCategory.Status, value: "#adadad" }, + { key: "status-online", category: ThemeCategory.Status, value: "#78e380" }, + { + key: "status-looking-to-play", + category: ThemeCategory.Status, + value: "#78a5e3" + }, + { + key: "status-away-from-keyboard", + category: ThemeCategory.Status, + value: "#e3a878" + }, + { + key: "status-do-not-disturb", + category: ThemeCategory.Status, + value: "#e37878" + }, + + // Text + { key: "text-color", category: ThemeCategory.Text, value: "white" }, + { + key: "content-color", + category: ThemeCategory.Text, + value: "rgba(255, 255, 255, 0.8)" + }, + { key: "side-pane-text-color", category: ThemeCategory.Text, value: "white" }, + { + key: "typing-indicator-color", + category: ThemeCategory.Text, + value: "white" + }, + { + key: "typing-indicator-secondary-color", + category: ThemeCategory.Text, + value: "rgba(255, 255, 255, 0.7)" + }, + + // Markup + { + key: "markup-code-background-color", + category: ThemeCategory.Markup, + value: "rgba(255, 255, 255, 0.12)" + }, + { + key: "markup-mention-background-color", + category: ThemeCategory.Markup, + value: "rgba(255, 255, 255, 0.1)" + }, + { + key: "markup-mention-background-color-hover", + category: ThemeCategory.Markup, + value: "rgba(255, 255, 255, 0.12)" + }, + { + key: "markup-codeblock-background-color", + category: ThemeCategory.Markup, + value: "rgba(255, 255, 255, 0.1)" + }, + { + key: "markup-spoiler-background-color", + category: ThemeCategory.Markup, + value: "#1d1f20ff" + }, + { + key: "markup-spoiler-background-color-hover", + category: ThemeCategory.Markup, + value: "#2b2e30ff" + }, + + // Drawer + { + key: "drawer-item-background-color", + category: ThemeCategory.Drawer, + value: "rgba(66, 70, 76, 0.6)" + }, + { + key: "drawer-item-hover-background-color", + category: ThemeCategory.Drawer, + value: "rgba(66, 70, 76, 0.4)" + } +] as const; + +// Get the order of categories as defined in ThemeCategory +const categoryOrder = Object.values(ThemeCategory); + +export const ThemeTokens = [...ThemeTokensBase].sort((a, b) => { + const categoryIndexA = categoryOrder.indexOf(a.category); + const categoryIndexB = categoryOrder.indexOf(b.category); + return categoryIndexA - categoryIndexB; +}); + +type ThemeKey = (typeof ThemeTokensBase)[number]["key"]; + +export const DefaultTheme = ThemeTokens.reduce( + (acc, token) => { + acc[token.key] = token.value; + return acc; + }, + {} as Record +); + +const [customColors, setCustomColors] = useLocalStorage< Partial> >(StorageKeys.CUSTOM_COLORS, {}); -const currentTheme = () => ({ ...theme, ...customColors() }); +const currentTheme = () => ({ ...DefaultTheme, ...customColors() }); -export const updateTheme = () => { - const newTheme = currentTheme(); +export const themeVars = ( + theme: Record +): Record => { + const vars: Record = {}; + for (const key of Object.keys(theme)) { + vars[`--${key}`] = theme[key as ThemeKey]; + } + vars["--text-color-secondary"] ??= dimmedColor(theme["text-color"], 0.6); + vars["--alert-color-faded"] ??= dimmedColor(theme["alert-color"], 0.6); + vars["--content-color-dim60"] ??= dimmedColor(theme["content-color"], 0.6); + vars["--content-color-dim80"] ??= dimmedColor(theme["content-color"], 0.8); + return vars; +}; - for (const key in newTheme) { - document.documentElement.style.setProperty( - `--${key}`, - newTheme[key as ThemeKey] - ); +export const updateTheme = () => { + const vars = themeVars(currentTheme()); + for (const key in vars) { + document.documentElement.style.setProperty(key, vars[key] ?? null); } }; -export const setThemeColor = (key: string, value?: string) => { +export const setThemeColor = (key: ThemeKey, value?: string) => { if (value === undefined) { const temp = { ...customColors() }; delete temp[key]; - setCustomColors(temp); + setCustomColors(reconcile(temp)); } else { setCustomColors({ ...customColors(), [key]: value }); } updateTheme(); }; -export { currentTheme, customColors }; +// Theme presets +export type ThemePreset = { + colors: Partial>; + maintainers: string[]; +}; + +export const themePresets: Record = { + Default: { + colors: DefaultTheme, + maintainers: ["Superkitten", "Asraye"] + }, + Classic: { + colors: { + "background-color": "hsl(216deg 9% 8%)", + "pane-color": "hsl(216deg 8% 15%)", + "side-pane-color": "hsl(216deg 7.82% 12.55%)", + "header-background-color": "hsla(216deg 8% 15% / 80%)", + "header-background-color-blur-disabled": "hsl(216deg 8% 15%)", + "tooltip-background-color": "rgb(40, 40, 40)", + "markup-code-background-color": "rgba(0, 0, 0, 0.6)", + "markup-mention-background-color": "rgba(0, 0, 0, 0.2)", + "markup-mention-background-color-hover": "rgba(0, 0, 0, 0.6)", + "markup-codeblock-background-color": "rgba(0, 0, 0, 0.6)", + "message-hover-background-color": "rgba(255, 255, 255, 0.03)", + "message-floating-options-background-color": "rgb(40, 40, 40)", + "markup-spoiler-background-color": "#0e0f10", + "markup-spoiler-background-color-hover": "#1c1e20" + }, + maintainers: ["Superkitten", "Asraye"] + } +}; + +// Apply a preset +export const applyTheme = (name: string, themeObj?: ThemePreset) => { + const preset = themeObj || themePresets[name]; + if (!preset || !preset.colors) return; + + // Clear previous + Object.keys(customColors()).forEach((key) => + setThemeColor(key as ThemeKey, undefined) + ); + + // Apply + Object.entries(preset.colors).forEach(([key, value]) => + setThemeColor(key as ThemeKey, value) + ); + + // Persist + setStorageString(StorageKeys.CUSTOM_COLORS, JSON.stringify(preset.colors)); +}; + +const placeholder = document.createElement("span"); +placeholder.style.display = "none"; +document.body.appendChild(placeholder); + +const computedColor = ( + color: string +): [number, number, number, number] | null => { + placeholder.style.color = ""; + placeholder.style.color = color; + if (placeholder.style.color == "") return null; + + const computed = window.getComputedStyle(placeholder).color; + const match = computed.match(/^rgba?\((.*)\)$/)?.[1]; + const colors = match?.split(",")?.map(Number); + if (colors === undefined || colors.length < 3 || colors.length > 4) + return null; + colors[3] = colors[3] ?? 1.0; + return colors as [number, number, number, number]; +}; + +const supportsColorMix = CSS.supports( + "color", + "color-mix(in srgb, #FFF 50%, transparent)" +); + +const dimmedColor = (color: string, opacity: number): string => { + if (supportsColorMix) + return `color-mix(in srgb, ${color} ${opacity * 100}%, transparent)`; + + const computed = computedColor(color); + if (computed === null) return color; + + const [r, g, b, a] = computed; + return `rgba(${r},${g},${b},${a * opacity})`; +}; + +updateTheme(); + +export const defaultThemeCSSVars = themeVars(DefaultTheme); + +export { DefaultTheme as theme, currentTheme, customColors, setCustomColors }; diff --git a/src/common/transitionViewIfSupported.ts b/src/common/transitionViewIfSupported.ts new file mode 100644 index 000000000..e9c168614 --- /dev/null +++ b/src/common/transitionViewIfSupported.ts @@ -0,0 +1,7 @@ +export const transitionViewIfSupported = (updateCb: () => void) => { + if (document.startViewTransition) { + document.startViewTransition(updateCb); + } else { + updateCb(); + } +}; diff --git a/src/common/useAppVersion.ts b/src/common/useAppVersion.ts index 7e1f8a7a4..7d31362de 100644 --- a/src/common/useAppVersion.ts +++ b/src/common/useAppVersion.ts @@ -4,8 +4,9 @@ import env from "./env"; import { getStorageString, setStorageString, - StorageKeys, + StorageKeys } from "./localStorage"; +import { log } from "./logger"; const [updateAvailable, setUpdateAvailable] = createSignal(false); const [latestRelease, setLatestRelease] = createSignal(null); @@ -30,19 +31,19 @@ const showChangelog = () => { }; const checkForUpdate = async () => { - console.log("[UPDATE] Checking..."); + log("UPDATER", "Checking..."); if (!env.APP_VERSION) { - console.log("[UPDATE] Skipping (reason: No App Version)"); + log("UPDATER", "Skipping (reason: No App Version)"); } if (env.DEV_MODE) { - console.log("[UPDATE] Skipping (reason: Dev Mode)"); + log("UPDATER", "Skipping (reason: Dev Mode)"); return; } let hasUpdate = false; // NOT latest.nerimity.com - let isRelease = env.APP_VERSION?.startsWith("v"); + const isRelease = env.APP_VERSION?.startsWith("v"); const appVersion = env.APP_VERSION; let latestVersion = ""; @@ -58,12 +59,12 @@ const checkForUpdate = async () => { hasUpdate = sha !== appVersion; } - console.log(`[UPDATE] Current: ${appVersion} Latest: ${latestVersion}`); + log("UPDATER", `Current: ${appVersion} Latest: ${latestVersion}`); setUpdateAvailable(hasUpdate); - if (hasUpdate) console.log("[UPDATE] Update available!"); - if (!hasUpdate) console.log("[UPDATE] No update available."); + if (hasUpdate) log("UPDATER", "Update available!"); + if (!hasUpdate) log("UPDATER", "No update available."); }; export function useAppVersion() { diff --git a/src/common/useChannelNotice.ts b/src/common/useChannelNotice.ts index 3b33c9863..27af827fc 100644 --- a/src/common/useChannelNotice.ts +++ b/src/common/useChannelNotice.ts @@ -1,42 +1,57 @@ -import { createSignal, onMount } from "solid-js"; +import { createSignal, createEffect } from "solid-js"; import { RawChannelNotice } from "../chat-api/RawData"; import { getChannelNotice } from "../chat-api/services/ChannelService"; -import { StorageKeys, getStorageObject, setStorageObject } from "@/common/localStorage"; +import { + StorageKeys, + getStorageObject, + setStorageObject +} from "@/common/localStorage"; import { createStore } from "solid-js/store"; -const [cachedNotices, setCachedNotices] = createStore>({}); +const [cachedNotices, setCachedNotices] = createStore< + Record +>({}); -export const getCachedNotice = (channelId: () => string) => cachedNotices[channelId()]; +export const getCachedNotice = (channelId: () => string) => + cachedNotices[channelId()]; export const useNotice = (channelId: () => string) => { - const [notice, setNotice] = createSignal(null); - onMount(async () => { - const cachedNotice = cachedNotices[channelId()]; + createEffect(async () => { + const id = channelId(); + if (!id) return; + const cachedNotice = cachedNotices[id]; if (cachedNotice !== undefined) { setNotice(cachedNotice); return; } - const noticeRes = await getChannelNotice(channelId()).catch(() => { }); + const noticeRes = await getChannelNotice(id).catch(() => {}); if (!noticeRes) { - setCachedNotices(channelId(), null); + setCachedNotices(id, null); + setNotice(null); return; } - setCachedNotices(channelId(), noticeRes.notice); + setCachedNotices(id, noticeRes.notice); setNotice(noticeRes.notice); }); const hasAlreadySeenNotice = () => { if (!notice()) return; - const lastSeenObj = getStorageObject>(StorageKeys.LAST_SEEN_CHANNEL_NOTICES, {}); + const lastSeenObj = getStorageObject>( + StorageKeys.LAST_SEEN_CHANNEL_NOTICES, + {} + ); const lastSeen = lastSeenObj[channelId()]; if (!lastSeen) return false; return lastSeen > notice()!.updatedAt; }; const updateLastSeen = () => { if (!notice()) return; - let lastSeenObj = getStorageObject>(StorageKeys.LAST_SEEN_CHANNEL_NOTICES, {}); + let lastSeenObj = getStorageObject>( + StorageKeys.LAST_SEEN_CHANNEL_NOTICES, + {} + ); lastSeenObj[channelId()] = Date.now(); // keep the top 50 last seen notices const sorted = Object.entries(lastSeenObj).sort((a, b) => b[1] - a[1]); diff --git a/src/common/useDiscordActivityTracker.ts b/src/common/useDiscordActivityTracker.ts new file mode 100644 index 000000000..b7f04cc17 --- /dev/null +++ b/src/common/useDiscordActivityTracker.ts @@ -0,0 +1,173 @@ +import { + getStorageString, + setStorageString, + StorageKeys +} from "./localStorage"; +import { localRPC } from "./LocalRPC"; +import { debounce } from "./debounce"; + +const URL = "https://supertiger.nerimity.com/trackdispresence"; +const NERIMITY_APP_ID = "1630300334100500480"; + +let ws: WebSocket | null = null; +let pingIntervalId: NodeJS.Timeout; +let restartDelayTimeoutId: NodeJS.Timeout; + +interface FormattedPresence { + status: string; + activities: FormattedActivity[]; +} +export interface FormattedActivity { + name: string; + applicationId: string | null; + createdTimestamp: number | null; + details: string | null; + state: string | null; + syncId: string | null; + url?: string | null; + type: number; + assets?: { + largeText?: string | null; + smallText?: string | null; + largeImageUrl: string | null; + smallImageUrl: string | null; + largeImage?: string | null; + smallImage?: string | null; + }; + timestamps?: { + start: number | null; + end: number | null; + } | null; +} + +const ActivityType = { + PLAYING: 0, + STREAMING: 1, + LISTENING: 2, + WATCHING: 3, + CUSTOM: 4, + COMPETING: 5 +}; +const ActivityTypeToNameAndAction = (activity: FormattedActivity) => { + switch (activity.type) { + case ActivityType.PLAYING: + return { name: activity.name || "Unknown", action: "Playing" }; + case ActivityType.STREAMING: + return { name: activity.name || "Unknown", action: "Streaming" }; + case ActivityType.LISTENING: + return { name: activity.name || "Unknown", action: "Listening to" }; + case ActivityType.WATCHING: + return { name: activity.name || "Unknown", action: "Watching" }; + case ActivityType.CUSTOM: + return { name: activity.state || "Unknown", action: "Custom" }; + case ActivityType.COMPETING: + return { name: activity.name || "Unknown", action: "Competing in" }; + default: + return { name: activity.name || "Unknown", action: "Playing" }; + } +}; +export const useDiscordActivityTracker = () => { + const start = () => { + clearTimeout(restartDelayTimeoutId); + const userId = getStorageString(StorageKeys.DISCORD_USER_ID, ""); + if (!userId) return; + if (ws) return; + clearInterval(pingIntervalId); + ws = new WebSocket(URL + "/" + userId); + ws.onopen = () => { + console.log("discord activity tracker connected"); + startPingInterval(); + }; + ws.onmessage = (event) => { + const rawData = event.data; + const data = JSON.parse(rawData as string); + if (data.error) { + console.error(data.error); + setStorageString(StorageKeys.DISCORD_USER_ID, ""); + return; + } + + handleActivity(data); + }; + const handleActivity = debounce((data: FormattedPresence) => { + const activities = data.activities + .filter((a) => a.type !== ActivityType.CUSTOM) + .sort((a, b) => { + const isASpotify = !!a.assets?.largeImage?.startsWith("spotify:"); + const isBSpotify = !!b.assets?.largeImage?.startsWith("spotify:"); + + // Ensure Spotify activities are placed at the end of the list + if (isASpotify && !isBSpotify) { + return 1; // a comes after b + } + if (!isASpotify && isBSpotify) { + return -1; // a comes before b + } + + // If neither or both are Spotify, maintain the original order (or add another sorting criteria) + return 0; + }); + + let url: string | undefined = undefined; + + const mapped = activities.map((activity) => { + const isSpotify = !!activity.assets?.largeImage?.startsWith("spotify:"); + + if (isSpotify && activity.syncId) { + url = `https://open.spotify.com/track/${activity.syncId}`; + } + + console.log(`Activity Update: ${activity?.name || null}`); + return { + id: activity.syncId || activity.applicationId || NERIMITY_APP_ID, + data: { + startedAt: + activity.timestamps?.start || + activity.createdTimestamp || + undefined, + endsAt: activity.timestamps?.end || undefined, + imgSrc: + activity.assets?.largeImageUrl || + activity.assets?.smallImageUrl || + undefined, + title: activity.details || undefined, + subtitle: activity.state || undefined, + link: url || activity.url || undefined, + ...ActivityTypeToNameAndAction(activity) + } + }; + }); + + localRPC.updateDiscordRPCs(mapped); + }, 500); + + ws.onclose = () => { + localRPC.updateDiscordRPCs([]); + clearInterval(pingIntervalId); + clearTimeout(restartDelayTimeoutId); + restartDelayTimeoutId = setTimeout(() => { + restart(); + }, 5000); + }; + }; + + const startPingInterval = () => { + pingIntervalId = setInterval(() => { + ws?.send("ping"); + }, 30000); + }; + + const restart = () => { + clearTimeout(restartDelayTimeoutId); + + localRPC.updateRPC(NERIMITY_APP_ID); + clearInterval(pingIntervalId); + if (ws) { + ws.onclose = () => {}; + ws.close(); + } + ws = null; + start(); + }; + return { start, restart }; +}; diff --git a/src/common/useDocumentListener.ts b/src/common/useDocumentListener.ts new file mode 100644 index 000000000..7ef64f67d --- /dev/null +++ b/src/common/useDocumentListener.ts @@ -0,0 +1,13 @@ +import { onCleanup, onMount } from "solid-js"; + +export const useDocumentListener = ( + type: K, + listener: (ev: DocumentEventMap[K]) => any +) => { + onMount(() => { + document.addEventListener(type, listener); + onCleanup(() => { + document.removeEventListener(type, listener); + }); + }); +}; diff --git a/src/common/useLastFmActivityTracker.ts b/src/common/useLastFmActivityTracker.ts new file mode 100644 index 000000000..2d8e4ed9e --- /dev/null +++ b/src/common/useLastFmActivityTracker.ts @@ -0,0 +1,119 @@ +import { getStorageObject, StorageKeys } from "./localStorage"; +import { localRPC } from "./LocalRPC"; +import { t } from "@nerimity/i18lite"; + +const LASTFM_APP_ID = "lastfm-activity-tracker"; +const POLL_INTERVAL_MS = 15000; + +let pollIntervalId: ReturnType | null = null; +let lastTrackKey: string | null = null; + +interface LastFmImage { + "#text": string; + size: string; +} + +interface LastFmTrack { + name: string; + artist: { "#text": string }; + album: { "#text": string }; + image: LastFmImage[]; + url: string; + "@attr"?: { nowplaying: string }; +} + +interface LastFmResponse { + recenttracks?: { + track: LastFmTrack[]; + }; + error?: number; + message?: string; +} + +async function fetchNowPlaying( + username: string, + apiKey: string +): Promise { + try { + const url = `https://ws.audioscrobbler.com/2.0/?method=user.getrecenttracks&user=${encodeURIComponent(username)}&api_key=${encodeURIComponent(apiKey)}&format=json&limit=1`; + const response = await fetch(url); + if (!response.ok) return; + + const data: LastFmResponse = await response.json(); + + const PERMANENT_ERRORS = new Set([6, 10, 13]); // invalid user, invalid key, invalid method + if (data.error) { + if (PERMANENT_ERRORS.has(data.error)) { + localRPC.updateRPC(LASTFM_APP_ID); + lastTrackKey = null; + } + // transient errors (e.g., rate limit) preserve existing activity + return; + } + if (!data.recenttracks?.track?.length) { + localRPC.updateRPC(LASTFM_APP_ID); + lastTrackKey = null; + return; + } + + const track = data.recenttracks.track[0]; + if (!track) return; + + const isNowPlaying = track["@attr"]?.nowplaying === "true"; + + if (!isNowPlaying) { + localRPC.updateRPC(LASTFM_APP_ID); + lastTrackKey = null; + return; + } + + const trackKey = `${track.name}::${track.artist["#text"]}`; + if (trackKey === lastTrackKey) return; + lastTrackKey = trackKey; + + const imgSrc = + track.image.find((i) => i.size === "extralarge")?.["#text"] || + track.image.find((i) => i.size === "large")?.["#text"] || + undefined; + + localRPC.updateRPC(LASTFM_APP_ID, { + action: t("activityNames.listening"), + name: "Last.fm", + title: track.name, + subtitle: track.artist["#text"], + imgSrc: imgSrc || undefined, + link: track.url || undefined, + startedAt: Date.now() + }); + } catch { + // Network error => we will keep the exisitng activity + } +} + +export const useLastFmActivityTracker = () => { + const start = () => { + const { username, apiKey } = getStorageObject(StorageKeys.LASTFM, { + username: "", + apiKey: "" + }); + if (!username || !apiKey) return; + if (pollIntervalId !== null) return; + + fetchNowPlaying(username, apiKey); + pollIntervalId = setInterval(() => { + fetchNowPlaying(username, apiKey); + }, POLL_INTERVAL_MS); + }; + + const restart = () => { + if (pollIntervalId !== null) { + clearInterval(pollIntervalId); + pollIntervalId = null; + } + lastTrackKey = null; + localRPC.updateRPC(LASTFM_APP_ID); + start(); + }; + + return { start, restart }; +}; diff --git a/src/common/useLastSelectedServerChannel.ts b/src/common/useLastSelectedServerChannel.ts index cc1e94716..45102e8a4 100644 --- a/src/common/useLastSelectedServerChannel.ts +++ b/src/common/useLastSelectedServerChannel.ts @@ -1,33 +1,51 @@ -import { createSignal } from "solid-js"; -import { StorageKeys, getStorageString, setStorageString } from "./localStorage"; +import { + StorageKeys, + getStorageString, + setStorageString +} from "./localStorage"; import { createStore, reconcile } from "solid-js/store"; +const [lastSelected, set] = createStore( + getLocalStorageLastSelectedServerChannelIds() +); - -const [lastSelected, set] = createStore(getLocalStorageLastSelectedServerChannelIds()); - - -export function getLastSelectedChannelId (serverId: string, defaultChannelId?: string) { +export function getLastSelectedChannelId( + serverId: string, + defaultChannelId?: string +) { return lastSelected[serverId] || defaultChannelId; } function getLocalStorageLastSelectedServerChannelIds() { - const stringEntries = getStorageString(StorageKeys.LAST_SELECTED_SERVER_CHANNELS, null); + const stringEntries = getStorageString( + StorageKeys.LAST_SELECTED_SERVER_CHANNELS, + null + ); if (!stringEntries) return {}; const obj = Object.fromEntries(JSON.parse(stringEntries)); return obj || {}; } -export function setLastSelectedServerChannelId(serverId: string, channelId: string) { - const stringObjEntries = getStorageString(StorageKeys.LAST_SELECTED_SERVER_CHANNELS, "[]"); +export function setLastSelectedServerChannelId( + serverId: string, + channelId: string +) { + const stringObjEntries = getStorageString( + StorageKeys.LAST_SELECTED_SERVER_CHANNELS, + "[]" + ); const entries: [string, string][] = JSON.parse(stringObjEntries); - const index = entries.findIndex(([entryServerId]) => serverId === entryServerId); + const index = entries.findIndex( + ([entryServerId]) => serverId === entryServerId + ); if (index >= 0) { entries[index] = [serverId, channelId]; - } - else { + } else { entries.unshift([serverId, channelId]); } set(reconcile(Object.fromEntries(entries))); - setStorageString(StorageKeys.LAST_SELECTED_SERVER_CHANNELS, JSON.stringify(entries.slice(0, 20))); -} \ No newline at end of file + setStorageString( + StorageKeys.LAST_SELECTED_SERVER_CHANNELS, + JSON.stringify(entries.slice(0, 20)) + ); +} diff --git a/src/common/useOnContextMenu.ts b/src/common/useOnContextMenu.ts index 767f701b3..7ef01edfa 100644 --- a/src/common/useOnContextMenu.ts +++ b/src/common/useOnContextMenu.ts @@ -1,20 +1,15 @@ // Because of trashy safari on ios not haivng a proper contextmenu event, we have to do some bs touchstart and stuff. - -import { createUniqueId, onCleanup } from "solid-js"; +import { onCleanup } from "solid-js"; import { useWindowProperties } from "./useWindowProperties"; -import { isFileServingAllowed } from "vite"; -import { sourceMapsEnabled } from "process"; let timer: number; type Handler = (event: any) => void; -let handlers = new Map(); - -const {isSafari, isMobileAgent} = useWindowProperties(); - +const handlers = new Map(); +const { isSafari, isMobileAgent } = useWindowProperties(); if (isSafari && isMobileAgent()) { let isTouchDown = false; @@ -22,57 +17,62 @@ if (isSafari && isMobileAgent()) { let startY = 0; let diffX = 0; let diffY = 0; - document.addEventListener('touchstart', (event) => { - startX = event.touches[0]?.clientX || 0; - startY = event.touches[0]?.clientY || 0; - timer = window.setTimeout(function() { - if (diffX >= 10 || diffY >= 10) return; - if (event.target instanceof HTMLElement) { - const el = event.target.closest("[ctx]"); - if (!el) return; - const handler = handlers.get(el as HTMLDivElement); - if (handler) { - isTouchDown = true; - handler(event); + document.addEventListener( + "touchstart", + (event) => { + startX = event.touches[0]?.clientX || 0; + startY = event.touches[0]?.clientY || 0; + timer = window.setTimeout(function () { + if (diffX >= 10 || diffY >= 10) return; + if (event.target instanceof Element) { + const el = event.target.closest("[ctx]"); + if (!el) return; + const handler = handlers.get(el as HTMLDivElement); + if (handler) { + isTouchDown = true; + handler(event); + } } - } - }, 1000) - }, false) + }, 1000); + }, + false + ); document.addEventListener("touchmove", (event) => { diffX = Math.abs(startX - (event.touches[0]?.clientX || 0)); diffY = Math.abs(startY - (event.touches[0]?.clientY || 0)); - }) - - document.addEventListener('touchend', (event) => { + }); + + document.addEventListener("touchend", (event) => { if (isTouchDown) { isTouchDown = false; event.preventDefault(); } - - window.clearTimeout(timer) - }) + + window.clearTimeout(timer); + }); } -document.addEventListener('contextmenu', (event) => { +document.addEventListener("contextmenu", (event) => { if (!event.target) return; - if (event.target instanceof HTMLElement) { + if (event.target instanceof Element) { const el = event.target.closest("[ctx]"); if (!el) return; const handler = handlers.get(el as HTMLDivElement); handler?.(event); - } -}) - - + } +}); -export function onContextMenu(el: HTMLDivElement, value: () => Handler | undefined) { - const handler = value() +export function onContextMenu( + el: HTMLDivElement, + value: () => Handler | undefined +) { + const handler = value(); if (!handler) return; - el.setAttribute("ctx", "") + el.setAttribute("ctx", ""); handlers.set(el, handler); onCleanup(() => { handlers.delete(el); - }) + }); } diff --git a/src/common/usePromise.ts b/src/common/usePromise.ts new file mode 100644 index 000000000..057833d4b --- /dev/null +++ b/src/common/usePromise.ts @@ -0,0 +1,17 @@ +import { createEffect, createSignal } from "solid-js"; + +export function usePromise(func: () => Promise) { + const [data, setData] = createSignal(null); + const [error, setError] = createSignal(null); + const [loading, setLoading] = createSignal(false); + + createEffect(() => { + setLoading(true); + func() + .then(setData) + .catch(setError) + .finally(() => setLoading(false)); + }); + + return { data, error, loading }; +} diff --git a/src/common/useResizeObserver.ts b/src/common/useResizeObserver.ts index 941280d30..aa2b7093e 100644 --- a/src/common/useResizeObserver.ts +++ b/src/common/useResizeObserver.ts @@ -1,36 +1,45 @@ -import { createEffect, on, onCleanup, onMount } from "solid-js"; +import { createEffect, on, onCleanup } from "solid-js"; import { createStore } from "solid-js/store"; - - -export function useResizeObserver(element: () => HTMLElement | undefined | null) { - const [dimensions, setDimensions] = createStore({width: 0, height: 0}); - createEffect(on(element, (el) => { - if (!el) return; - const resizeObserver = new ResizeObserver((entries) => { - setDimensions({ - width: entries[0].contentRect.width, - height: entries[0].contentRect.height +export function useResizeObserver( + element: () => HTMLElement | undefined | null +) { + const [dimensions, setDimensions] = createStore({ width: 0, height: 0 }); + createEffect( + on(element, (el) => { + if (!el) return; + const resizeObserver = new ResizeObserver((entries) => { + setDimensions({ + width: entries[0].contentRect.width, + height: entries[0].contentRect.height + }); }); - }); - resizeObserver.observe(el); + resizeObserver.observe(el); - onCleanup(() => { - resizeObserver.disconnect(); - }); - })); - return {width: () => dimensions.width, height: () => dimensions.height} as const; + onCleanup(() => { + resizeObserver.disconnect(); + }); + }) + ); + return { + width: () => dimensions.width, + height: () => dimensions.height + } as const; } +export function useMutationObserver( + element: () => HTMLElement | undefined | null, + callback: MutationCallback +) { + createEffect( + on(element, (el) => { + if (!el) return; + const resizeObserver = new MutationObserver(callback); + resizeObserver.observe(el, { childList: true, subtree: true }); -export function useMutationObserver(element: () => HTMLElement | undefined | null, callback: () => void) { - createEffect(on(element, (el) => { - if (!el) return; - const resizeObserver = new MutationObserver(callback); - resizeObserver.observe(el, {childList: true, subtree: true}); - - onCleanup(() => { - resizeObserver.disconnect(); - }); - })); -} \ No newline at end of file + onCleanup(() => { + resizeObserver.disconnect(); + }); + }) + ); +} diff --git a/src/common/useSelectedSuggestion.ts b/src/common/useSelectedSuggestion.ts new file mode 100644 index 000000000..c3fd4ff89 --- /dev/null +++ b/src/common/useSelectedSuggestion.ts @@ -0,0 +1,62 @@ +import { createEffect, createSignal, onCleanup } from "solid-js"; + +export function useSelectedSuggestion( + length: () => number, + textArea: () => HTMLTextAreaElement, + onEnterClick: (i: number) => void, + sendButtonRef?: () => HTMLButtonElement | undefined +) { + const [current, setCurrent] = createSignal(0); + + createEffect(() => { + sendButtonRef?.()?.addEventListener("click", onSendClick); + textArea()?.addEventListener("keydown", onKey); + onCleanup(() => { + sendButtonRef?.()?.removeEventListener("click", onSendClick); + textArea()?.removeEventListener("keydown", onKey); + }); + }); + + const next = () => { + if (current() + 1 >= length()) { + setCurrent(0); + } else { + setCurrent(current() + 1); + } + }; + + const previous = () => { + if (current() - 1 < 0) { + setCurrent(length() - 1); + } else { + setCurrent(current() - 1); + } + }; + + const onSendClick = (event: MouseEvent) => { + if (!length()) return; + event.stopPropagation(); + event.preventDefault(); + onEnterClick(current()); + }; + + const onKey = (event: KeyboardEvent) => { + if (event.shiftKey) return; + if (!length()) return; + if (event.key === "ArrowDown") { + event.preventDefault(); + next(); + } else if (event.key === "ArrowUp") { + event.preventDefault(); + previous(); + } + + if (event.key === "Enter" || event.key === "Tab") { + event.stopPropagation(); + event.preventDefault(); + onEnterClick(current()); + } + }; + + return [current, next, previous, setCurrent] as const; +} diff --git a/src/common/useWindowProperties.ts b/src/common/useWindowProperties.ts index 29df2779c..b9c72a42b 100644 --- a/src/common/useWindowProperties.ts +++ b/src/common/useWindowProperties.ts @@ -1,13 +1,16 @@ import { createStore } from "solid-js/store"; import env from "./env"; -import { createSignal } from "solid-js"; -import { StorageKeys, useReactiveLocalStorage } from "./localStorage"; +import { createMemo, createSignal } from "solid-js"; +import { StorageKeys, useLocalStorage } from "./localStorage"; + +const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); const [windowProperties, setWindowProperties] = createStore({ width: window.innerWidth, height: window.innerHeight, paneWidth: null as null | number, hasFocus: document.hasFocus(), + reduceMotion: motionQuery.matches }); window.addEventListener("resize", () => { @@ -22,25 +25,40 @@ window.addEventListener("blur", () => { setWindowProperties("hasFocus", false); }); +motionQuery.addEventListener("change", () => { + setWindowProperties("reduceMotion", motionQuery.matches); +}); + function setPaneWidth(val: number) { setWindowProperties({ paneWidth: val }); } -const isMobileAgent = () => - /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); +const isMobileAgent = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const isFirefox = navigator.userAgent.includes("Firefox"); const isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent); const isChrome = /^((?!chrome|android).)*chrome/i.test(navigator.userAgent); -const [blurEffectEnabled, setBlurEffectEnabled] = useReactiveLocalStorage( +const [blurEffectEnabled, setBlurEffectEnabled] = useLocalStorage( StorageKeys.BLUR_EFFECT_ENABLED, isChrome ); +export type ReduceMotionMode = "enabled" | "disabled" | "auto"; + +const [reduceMotionMode, setReduceMotionMode] = + useLocalStorage(StorageKeys.REDUCE_MOTION_MODE, "auto"); + const [paneBackgroundColor, setPaneBackgroundColor] = createSignal< undefined | string >(undefined); +const userReduceMotion = createMemo(() => { + const mode = reduceMotionMode(); + const reduceMotion = + mode == "auto" ? windowProperties.reduceMotion : mode == "enabled"; + return reduceMotion; +}); + export function useWindowProperties() { const isWindowFocusedAndBlurEffectEnabled = () => { if (!windowProperties.hasFocus) return false; @@ -50,16 +68,25 @@ export function useWindowProperties() { return { blurEffectEnabled, setBlurEffectEnabled, + reduceMotionMode, + setReduceMotionMode, isWindowFocusedAndBlurEffectEnabled, setPaneWidth, width: () => windowProperties.width, height: () => windowProperties.height, isMobileWidth: () => windowProperties.width <= env.MOBILE_WIDTH, + isMainPaneMobileWidth: () => + (windowProperties.paneWidth || windowProperties.width) <= 600, paneWidth: () => windowProperties.paneWidth, hasFocus: () => windowProperties.hasFocus, - isMobileAgent, + reduceMotion: userReduceMotion, + shouldAnimate: (hover: boolean = false) => { + return windowProperties.hasFocus && (hover || !userReduceMotion()); + }, + isMobileAgent: () => isMobileAgent, isSafari, + isFirefox, paneBackgroundColor, - setPaneBackgroundColor, + setPaneBackgroundColor }; } diff --git a/src/common/userStatus.ts b/src/common/userStatus.ts index 326d22341..fa895acb9 100644 --- a/src/common/userStatus.ts +++ b/src/common/userStatus.ts @@ -1,11 +1,11 @@ +import { t } from "@nerimity/i18lite"; interface Status { - name: string; + name: () => string; id: string; color: string; } - // OFFLINE = 0, // ONLINE = 1, // LTP = 2, // Looking To Play @@ -13,13 +13,33 @@ interface Status { // DND = 4, // Do not disturb export const UserStatuses: Status[] = [ - { name: "Offline", id: "offline", color: "#adadad" }, - { name: "Online", id: "online", color: "#78e380" }, - { name: "Looking To Play", id: "ltp", color: "#78a5e3" }, - { name: "Away From Keyboard", id: "afk", color: "#e3a878" }, - { name: "Do Not Disturb", id: "dnd", color: "#e37878" } + { + name: () => t("status.offline"), + id: "offline", + color: "var(--status-offline)" + }, + { + name: () => t("status.online"), + id: "online", + color: "var(--status-online)" + }, + { + name: () => t("status.ltp"), + id: "ltp", + color: "var(--status-looking-to-play)" + }, + { + name: () => t("status.afk"), + id: "afk", + color: "var(--status-away-from-keyboard)" + }, + { + name: () => t("status.dnd"), + id: "dnd", + color: "var(--status-do-not-disturb)" + } ]; -export function userStatusDetail(status: number): Status { - return UserStatuses[status]; -} \ No newline at end of file +export function userStatusDetail(status: number) { + return UserStatuses[status] || (UserStatuses[0] as Status); +} diff --git a/src/common/worldEvents.ts b/src/common/worldEvents.ts index 8d97dc20a..11f18abaa 100644 --- a/src/common/worldEvents.ts +++ b/src/common/worldEvents.ts @@ -1,6 +1,6 @@ interface Event { - start: { day: number; month: number; }, - dayDuration: number + start: { day: number; month: number }; + dayDuration: number; } const halloween = { @@ -17,7 +17,7 @@ const now = Date.now(); export const isHalloween = isEventActive(halloween); export const isChristmas = false; -function isEventActive({start, dayDuration}: Event) { +function isEventActive({ start, dayDuration }: Event) { const startDate = new Date(); startDate.setDate(start.day); startDate.setMonth(start.month - 1); @@ -29,7 +29,6 @@ function isEventActive({start, dayDuration}: Event) { endDate.setHours(23); endDate.setMinutes(59); - if (startDate.getFullYear() !== endDate.getFullYear()) { endDate.setFullYear(startDate.getFullYear()); startDate.setFullYear(startDate.getFullYear() - 1); @@ -39,7 +38,7 @@ function isEventActive({start, dayDuration}: Event) { } export const appLogoUrl = () => { - if (isHalloween) return "/assets/halloween-logo.png"; - if (isChristmas) return "/assets/christmas-logo.png"; + // if (isHalloween) return "/assets/halloween-logo.png"; + // if (isChristmas) return "/assets/christmas-logo.png"; return "/assets/logo.png"; -}; \ No newline at end of file +}; diff --git a/src/common/zip.ts b/src/common/zip.ts index 92c8dee88..517d5ed75 100644 --- a/src/common/zip.ts +++ b/src/common/zip.ts @@ -17,9 +17,9 @@ export function unzipJson(base64: string) { } function base64ToArrayBuffer(base64: string) { - var binaryString = atob(base64); - var bytes = new Uint8Array(binaryString.length); - for (var i = 0; i < binaryString.length; i++) { + const binaryString = atob(base64); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { bytes[i] = binaryString.charCodeAt(i); } return bytes; diff --git a/src/common/zstd.ts b/src/common/zstd.ts new file mode 100644 index 000000000..e6012a054 --- /dev/null +++ b/src/common/zstd.ts @@ -0,0 +1,12 @@ +import * as fzstd from "fzstd"; + +const decompressString = (input: Uint8Array) => { + return fzstd.decompress(input); +}; + +export const decompressObject = (input: Uint8Array) => { + const decompressed = decompressString(input); + const decoder = new TextDecoder(); + const jsonString = decoder.decode(decompressed); + return JSON.parse(jsonString) as T; +}; diff --git a/src/components/ChangelogModal.tsx b/src/components/ChangelogModal.tsx index e601463a2..bca895727 100644 --- a/src/components/ChangelogModal.tsx +++ b/src/components/ChangelogModal.tsx @@ -1,12 +1,13 @@ import { formatTimestamp } from "@/common/date"; import Marked from "@/components/marked/Marked"; import { useAppVersion } from "@/common/useAppVersion"; -import { A } from "solid-navigator"; + import Button from "./ui/Button"; import { FlexColumn } from "./ui/Flexbox"; import LegacyModal from "./ui/legacy-modal/LegacyModal"; import Text from "./ui/Text"; import env from "@/common/env"; +import { t } from "@nerimity/i18lite"; import { Show } from "solid-js"; export function ChangelogModal(props: { close: () => void }) { @@ -21,8 +22,7 @@ export function ChangelogModal(props: { close: () => void }) { }; const ActionButtons = ( - void }) { } target="_blank" rel="noopener noreferrer" - > -