1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 02:12:38 +01:00

Merge pull request #340 from spacebarchat/dev

merge dev
This commit is contained in:
Puyodead1 2024-07-11 20:10:36 -04:00 committed by GitHub
commit 09072f0a9b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
52 changed files with 5301 additions and 2551 deletions

View File

@ -1,4 +1,4 @@
<!doctype html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<!-- Primary Meta Tags --> <!-- Primary Meta Tags -->

View File

@ -6,7 +6,7 @@
"dependencies": { "dependencies": {
"@emotion/react": "^11.11.4", "@emotion/react": "^11.11.4",
"@emotion/styled": "^11.11.0", "@emotion/styled": "^11.11.0",
"@floating-ui/react": "^0.26.9", "@floating-ui/react": "^0.26.17",
"@fontsource/roboto": "^5.0.8", "@fontsource/roboto": "^5.0.8",
"@fontsource/roboto-mono": "^5.0.16", "@fontsource/roboto-mono": "^5.0.16",
"@hcaptcha/react-hcaptcha": "^1.10.1", "@hcaptcha/react-hcaptcha": "^1.10.1",
@ -14,84 +14,87 @@
"@mattjennings/react-modal-stack": "^1.0.4", "@mattjennings/react-modal-stack": "^1.0.4",
"@mdi/js": "^7.4.47", "@mdi/js": "^7.4.47",
"@mdi/react": "^1.6.1", "@mdi/react": "^1.6.1",
"@mui/material": "^5.15.11", "@mui/material": "^5.15.14",
"@originjs/vite-plugin-commonjs": "^1.0.3", "@originjs/vite-plugin-commonjs": "^1.0.3",
"@rollup/plugin-replace": "^5.0.5", "@rollup/plugin-replace": "^5.0.5",
"@spacebarchat/spacebar-api-types": "0.37.51", "@spacebarchat/spacebar-api-types": "0.37.51",
"@tauri-apps/api": "2.0.0-beta.3", "@tauri-apps/api": "2.0.0-beta.7",
"@tauri-apps/plugin-authenticator": "2.0.0-beta.1", "@tauri-apps/plugin-authenticator": "2.0.0-beta.2",
"@tauri-apps/plugin-autostart": "2.0.0-beta.1", "@tauri-apps/plugin-autostart": "2.0.0-beta.3",
"@tauri-apps/plugin-clipboard-manager": "2.0.0-beta.1", "@tauri-apps/plugin-clipboard-manager": "2.0.0-beta.2",
"@tauri-apps/plugin-dialog": "2.0.0-beta.1", "@tauri-apps/plugin-dialog": "2.0.0-beta.5",
"@tauri-apps/plugin-log": "2.0.0-beta.1", "@tauri-apps/plugin-log": "2.0.0-beta.3",
"@tauri-apps/plugin-notification": "2.0.0-beta.1", "@tauri-apps/plugin-notification": "2.0.0-beta.1",
"@tauri-apps/plugin-os": "2.0.0-beta.1", "@tauri-apps/plugin-os": "2.0.0-beta.1",
"@tauri-apps/plugin-process": "2.0.0-beta.1", "@tauri-apps/plugin-process": "2.0.0-beta.2",
"@tauri-apps/plugin-stronghold": "2.0.0-beta.1", "@tauri-apps/plugin-stronghold": "2.0.0-beta.3",
"@tauri-apps/plugin-updater": "2.0.0-beta.1", "@tauri-apps/plugin-updater": "2.0.0-beta.1",
"@testing-library/jest-dom": "^6.4.2", "@testing-library/jest-dom": "^6.4.2",
"@testing-library/react": "^14.2.1", "@testing-library/react": "^15.0.2",
"@testing-library/user-event": "^14.5.2", "@testing-library/user-event": "^14.5.2",
"@types/react-measure": "^2.0.12", "@types/react-measure": "^2.0.12",
"@types/react-portal": "^4.0.7", "@types/react-portal": "^4.0.7",
"@uidotdev/usehooks": "^2.4.1",
"classnames": "^2.5.1", "classnames": "^2.5.1",
"csstype": "^3.1.3", "csstype": "^3.1.3",
"dayjs": "^1.11.10", "dayjs": "^1.11.10",
"framer-motion": "^11.0.6", "framer-motion": "^11.1.3",
"marked-react": "^2.0.0", "marked-react": "^2.0.0",
"missing-native-js-functions": "^1.4.3", "missing-native-js-functions": "^1.4.3",
"mobx": "^6.12.0", "mobx": "^6.12.4",
"mobx-react-lite": "^4.0.5", "mobx-react-lite": "^4.0.7",
"murmurhash-js": "^1.0.0", "murmurhash-js": "^1.0.0",
"prismjs": "^1.29.0", "prismjs": "^1.29.0",
"react": "^18.2.0", "react": "^18.2.0",
"react-advanced-cropper": "^0.19.5", "react-advanced-cropper": "^0.19.6",
"react-colorful": "^5.6.1", "react-colorful": "^5.6.1",
"react-device-detect": "^2.2.3", "react-device-detect": "^2.2.3",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"react-fps-stats": "^0.3.1", "react-fps-stats": "^0.3.1",
"react-hook-form": "^7.50.1", "react-hook-form": "^7.51.3",
"react-infinite-scroll-component": "^6.1.0", "react-infinite-scroll-component": "^6.1.0",
"react-loading-skeleton": "^3.4.0", "react-loading-skeleton": "^3.4.0",
"react-markdown": "^9.0.1", "react-markdown": "^9.0.1",
"react-measure": "^2.5.2", "react-measure": "^2.5.2",
"react-portal": "^4.2.2", "react-portal": "^4.2.2",
"react-router-dom": "^6.22.1", "react-router-dom": "^6.22.3",
"react-secure-storage": "^1.3.2", "react-secure-storage": "^1.3.2",
"react-select-search": "^4.1.7", "react-select-search": "^4.1.7",
"react-spinners": "^0.13.8", "react-spinners": "^0.13.8",
"react-spring": "^9.7.3",
"react-string-replace": "^1.1.1", "react-string-replace": "^1.1.1",
"react-syntax-highlighter": "^15.5.0", "react-syntax-highlighter": "^15.5.0",
"react-use-error-boundary": "^3.0.0", "react-use-error-boundary": "^3.0.0",
"react-use-gesture": "^9.1.3",
"react-virtualized": "^9.22.5", "react-virtualized": "^9.22.5",
"remark-gfm": "^4.0.0", "remark-gfm": "^4.0.0",
"reoverlay": "^1.0.3", "reoverlay": "^1.0.3",
"styled-components": "5.3.11", "styled-components": "5.3.11",
"use-resize-observer": "^9.1.0", "use-resize-observer": "^9.1.0",
"yup": "^1.3.3" "yup": "^1.4.0"
}, },
"devDependencies": { "devDependencies": {
"@craco/craco": "^7.1.0", "@craco/craco": "^7.1.0",
"@tauri-apps/cli": "2.0.0-beta.5", "@tauri-apps/cli": "2.0.0-beta.5",
"@types/jest": "^29.5.12", "@types/jest": "^29.5.12",
"@types/loadable__component": "^5.13.8", "@types/loadable__component": "^5.13.9",
"@types/murmurhash-js": "^1.0.6", "@types/murmurhash-js": "^1.0.6",
"@types/node": "^20.11.20", "@types/node": "^20.14.2",
"@types/react": "^18.2.60", "@types/react": "^18.2.79",
"@types/react-dom": "^18.2.19", "@types/react-dom": "^18.2.25",
"@types/react-syntax-highlighter": "^15.5.11", "@types/react-syntax-highlighter": "^15.5.11",
"@types/react-virtualized": "^9.21.29", "@types/react-virtualized": "^9.21.29",
"@types/styled-components": "^5.1.34", "@types/styled-components": "^5.1.34",
"@typescript-eslint/eslint-plugin": "^7.1.0", "@typescript-eslint/eslint-plugin": "^7.1.0",
"@typescript-eslint/parser": "^7.1.0", "@typescript-eslint/parser": "^7.8.0",
"@vitejs/plugin-react": "^4.2.1", "@vitejs/plugin-react": "^4.2.1",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^8.57.0", "eslint": "^8.57.0",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react-refresh": "^0.4.5", "eslint-plugin-react-refresh": "^0.4.6",
"internal-ip": "^8.0.0", "internal-ip": "^8.0.0",
"typescript": "^5.3.3", "typescript": "^5.4.5",
"vite": "^5.1.7", "vite": "^5.2.7",
"vite-plugin-chunk-split": "^0.5.0", "vite-plugin-chunk-split": "^0.5.0",
"vite-plugin-clean": "^1.0.0", "vite-plugin-clean": "^1.0.0",
"vite-plugin-html": "^3.2.2", "vite-plugin-html": "^3.2.2",

File diff suppressed because it is too large Load Diff

View File

@ -1,97 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="AutoImportSettings"> <component name="AutoImportSettings">
<option name="autoReloadType" value="ALL" /> <option name="autoReloadType" value="NONE" />
</component> </component>
<component name="CargoProjects"> <component name="CargoProjects">
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" /> <cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="0756a13d-f814-41e0-81ae-eb872c2c747f" name="Changes" comment=""> <list default="true" id="0756a13d-f814-41e0-81ae-eb872c2c747f" name="Changes" comment="">
<change beforePath="$PROJECT_DIR$/../.editorconfig" beforeDir="false" afterPath="$PROJECT_DIR$/../.editorconfig" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../package.json" beforeDir="false" afterPath="$PROJECT_DIR$/../package.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../.eslintignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.eslintignore" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../pnpm-lock.yaml" beforeDir="false" afterPath="$PROJECT_DIR$/../pnpm-lock.yaml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../.eslintrc" beforeDir="false" afterPath="$PROJECT_DIR$/../.eslintrc" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/.gitignore" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/bug_report.md" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/bug_report.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/compiler.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/config.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/config.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/deploymentTargetDropDown.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/feature_request.md" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/feature_request.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/discord.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.github/workflows/pages-deploy.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/workflows/pages-deploy.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/gradle.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.github/workflows/tauri.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/workflows/tauri.yml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/kotlinc.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.gitignore" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/migrations.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.prettierignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.prettierignore" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/misc.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.vscode/extensions.json" beforeDir="false" afterPath="$PROJECT_DIR$/../.vscode/extensions.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/.idea/vcs.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../.vscode/settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/../.vscode/settings.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/buildSrc/build.gradle.kts" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../LICENSE" beforeDir="false" afterPath="$PROJECT_DIR$/../LICENSE" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/BuildTask.kt" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../flake.nix" beforeDir="false" afterPath="$PROJECT_DIR$/../flake.nix" afterDir="false" /> <change beforePath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/RustPlugin.kt" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/../flake.template.nix" beforeDir="false" afterPath="$PROJECT_DIR$/../flake.template.nix" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/App.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/App.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../nix-build-test.sh" beforeDir="false" afterPath="$PROJECT_DIR$/../nix-build-test.sh" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/components/ChannelSidebar.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/ChannelSidebar.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../nix-rebuild-flake.sh" beforeDir="false" afterPath="$PROJECT_DIR$/../nix-rebuild-flake.sh" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/components/GuildSidebar.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/GuildSidebar.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../public/manifest.json" beforeDir="false" afterPath="$PROJECT_DIR$/../public/manifest.json" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/components/messaging/Chat.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/messaging/Chat.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../public/robots.txt" beforeDir="false" afterPath="$PROJECT_DIR$/../public/robots.txt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/components/modals/JoinServerModal.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/modals/JoinServerModal.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../public/splashscreen.css" beforeDir="false" afterPath="$PROJECT_DIR$/../public/splashscreen.css" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../public/splashscreen.html" beforeDir="false" afterPath="$PROJECT_DIR$/../public/splashscreen.html" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.rs" beforeDir="false" afterPath="$PROJECT_DIR$/build.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.editorconfig" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.editorconfig" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/compiler.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/compiler.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/deploymentTargetDropDown.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/deploymentTargetDropDown.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/discord.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/discord.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/gradle.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/gradle.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/kotlinc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/kotlinc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/migrations.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/migrations.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/.idea/vcs.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/proguard-rules.pro" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/proguard-rules.pro" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/AndroidManifest.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/AndroidManifest.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/java/chat/spacebar/app/MainActivity.kt" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/java/chat/spacebar/app/MainActivity.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/drawable-v24/ic_launcher_foreground.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/drawable/ic_launcher_background.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/drawable/ic_launcher_background.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/layout/activity_main.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/layout/activity_main.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/values-night/themes.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/values-night/themes.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/values/colors.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/values/colors.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/values/strings.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/values/strings.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/values/themes.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/values/themes.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/app/src/main/res/xml/file_paths.xml" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/app/src/main/res/xml/file_paths.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/buildSrc/build.gradle.kts" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/buildSrc/build.gradle.kts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/BuildTask.kt" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/BuildTask.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/RustPlugin.kt" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/buildSrc/src/main/java/chat/spacebar/app/kotlin/RustPlugin.kt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/gradle.properties" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/gradle.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/gradle/wrapper/gradle-wrapper.properties" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/gradle/wrapper/gradle-wrapper.properties" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/gradlew" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/gradlew" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/gradlew.bat" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/gradlew.bat" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/android/settings.gradle" beforeDir="false" afterPath="$PROJECT_DIR$/gen/android/settings.gradle" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/ExportOptions.plist" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/ExportOptions.plist" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/Podfile" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/Podfile" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/Sources/app/bindings/bindings.h" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/Sources/app/bindings/bindings.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/Sources/app/main.mm" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/Sources/app/main.mm" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.pbxproj" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.pbxproj" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.xcworkspace/contents.xcworkspacedata" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.xcworkspace/contents.xcworkspacedata" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app.xcodeproj/xcshareddata/xcschemes/app_iOS.xcscheme" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app.xcodeproj/xcshareddata/xcschemes/app_iOS.xcscheme" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app_iOS/Info.plist" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app_iOS/Info.plist" afterDir="false" />
<change beforePath="$PROJECT_DIR$/gen/apple/app_iOS/app_iOS.entitlements" beforeDir="false" afterPath="$PROJECT_DIR$/gen/apple/app_iOS/app_iOS.entitlements" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/lib.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/lib.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/main.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/main.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/src/tray.rs" beforeDir="false" afterPath="$PROJECT_DIR$/src/tray.rs" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/assets/images/logo/Logo-Blue.svg" beforeDir="false" afterPath="$PROJECT_DIR$/../src/assets/images/logo/Logo-Blue.svg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/assets/images/logo/Logo-White.svg" beforeDir="false" afterPath="$PROJECT_DIR$/../src/assets/images/logo/Logo-White.svg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/assets/images/logo/Spacebar_Icon.svg" beforeDir="false" afterPath="$PROJECT_DIR$/../src/assets/images/logo/Spacebar_Icon.svg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/assets/images/logo/Spacebar_Logo_Blue.svg" beforeDir="false" afterPath="$PROJECT_DIR$/../src/assets/images/logo/Spacebar_Logo_Blue.svg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/assets/images/logo/icon-rounded.svg" beforeDir="false" afterPath="$PROJECT_DIR$/../src/assets/images/logo/icon-rounded.svg" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/components/ChannelList/ChannelListItem.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/ChannelList/ChannelListItem.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/components/modals/ModalComponents.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/modals/ModalComponents.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/components/modals/index.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/components/modals/index.ts" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/controllers/modals/ModalController.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/controllers/modals/ModalController.tsx" afterDir="false" /> <change beforePath="$PROJECT_DIR$/../src/controllers/modals/ModalController.tsx" beforeDir="false" afterPath="$PROJECT_DIR$/../src/controllers/modals/ModalController.tsx" afterDir="false" />
<change beforePath="$PROJECT_DIR$/../src/controllers/modals/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/controllers/modals/types.ts" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" /> <option name="LAST_RESOLUTION" value="IGNORE" />
</component> </component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
</component> </component>
@ -110,10 +55,13 @@
"keyToString": { "keyToString": {
"RunOnceActivity.OpenProjectViewOnStart": "true", "RunOnceActivity.OpenProjectViewOnStart": "true",
"RunOnceActivity.ShowReadmeOnStart": "true", "RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.rust.reset.selective.auto.import": "true", "RunOnceActivity.rust.reset.selective.auto.import": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"git-widget-placeholder": "dev", "git-widget-placeholder": "dev",
"ignore.virus.scanning.warn.message": "true", "ignore.virus.scanning.warn.message": "true",
"last_opened_file_path": "C:/Users/23562/Documents/Code/workspaces/spacebar/client-react/src-tauri/Cargo.toml", "last_opened_file_path": "E:/client-react/src-tauri",
"node.js.detected.package.eslint": "true", "node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true", "node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)", "node.js.selected.package.eslint": "(autodetect)",

2226
src-tauri/Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -14,15 +14,15 @@ crate-type = ["staticlib", "cdylib", "rlib"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[build-dependencies] [build-dependencies]
tauri-build = { version = "2.0.0-alpha", features = [] } tauri-build = { version = "2.0.0-beta.15", features = [] }
[dependencies] [dependencies]
tauri = { version = "2.0.0-alpha", features = ["devtools", "tray-icon"] } tauri = { version = "2.0.0-beta.19", features = ["devtools", "tray-icon"] }
tauri-plugin-updater = "2.0.0-alpha" tauri-plugin-updater = "2.0.0-beta.4"
tauri-plugin-process = "2.0.0-alpha" tauri-plugin-process = "2.0.0-alpha"
tauri-plugin-log = "2.0.0-alpha" tauri-plugin-log = "2.0.0-alpha"
tauri-plugin-os = "2.0.0-alpha" tauri-plugin-os = "2.0.0-alpha"
reqwest = { version = "0.11.22", default-features = false, features = [ reqwest = { version = "0.12.4", default-features = false, features = [
"json", "json",
"rustls-tls", "rustls-tls",
] } ] }
@ -31,9 +31,9 @@ chrono = "0.4"
log = "0.4.20" log = "0.4.20"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tauri-plugin-notification = "2.0.0-beta" tauri-plugin-notification = "2.0.0-beta.5"
tauri-plugin-single-instance = "2.0.0-beta" tauri-plugin-single-instance = "2.0.0-beta.6"
tauri-plugin-autostart = "2.0.0-beta" tauri-plugin-autostart = "2.0.0-beta.4"
[features] [features]
# this feature is used for production builds or when `devPath` points to the filesystem # this feature is used for production builds or when `devPath` points to the filesystem

File diff suppressed because one or more lines are too long

View File

@ -1287,6 +1287,13 @@
"updater:allow-check" "updater:allow-check"
] ]
}, },
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{ {
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1294,6 +1301,13 @@
"updater:allow-download-and-install" "updater:allow-download-and-install"
] ]
}, },
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{ {
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1301,6 +1315,13 @@
"updater:deny-check" "updater:deny-check"
] ]
}, },
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{ {
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1308,6 +1329,13 @@
"updater:deny-download-and-install" "updater:deny-download-and-install"
] ]
}, },
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"description": "webview:default -> Default permissions for the plugin.", "description": "webview:default -> Default permissions for the plugin.",
"type": "string", "type": "string",
@ -1371,6 +1399,13 @@
"webview:allow-set-webview-size" "webview:allow-set-webview-size"
] ]
}, },
{
"description": "webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.",
"type": "string",
"enum": [
"webview:allow-set-webview-zoom"
]
},
{ {
"description": "webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.", "description": "webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1448,6 +1483,13 @@
"webview:deny-set-webview-size" "webview:deny-set-webview-size"
] ]
}, },
{
"description": "webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.",
"type": "string",
"enum": [
"webview:deny-set-webview-zoom"
]
},
{ {
"description": "webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.", "description": "webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1511,6 +1553,13 @@
"window:allow-current-monitor" "window:allow-current-monitor"
] ]
}, },
{
"description": "window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-cursor-position"
]
},
{ {
"description": "window:allow-destroy -> Enables the destroy command without any pre-configured scope.", "description": "window:allow-destroy -> Enables the destroy command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1630,6 +1679,13 @@
"window:allow-minimize" "window:allow-minimize"
] ]
}, },
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{ {
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", "description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1861,6 +1917,13 @@
"window:allow-start-dragging" "window:allow-start-dragging"
] ]
}, },
{
"description": "window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-start-resize-dragging"
]
},
{ {
"description": "window:allow-theme -> Enables the theme command without any pre-configured scope.", "description": "window:allow-theme -> Enables the theme command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1931,6 +1994,13 @@
"window:deny-current-monitor" "window:deny-current-monitor"
] ]
}, },
{
"description": "window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-cursor-position"
]
},
{ {
"description": "window:deny-destroy -> Denies the destroy command without any pre-configured scope.", "description": "window:deny-destroy -> Denies the destroy command without any pre-configured scope.",
"type": "string", "type": "string",
@ -2050,6 +2120,13 @@
"window:deny-minimize" "window:deny-minimize"
] ]
}, },
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{ {
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", "description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@ -2281,6 +2358,13 @@
"window:deny-start-dragging" "window:deny-start-dragging"
] ]
}, },
{
"description": "window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-start-resize-dragging"
]
},
{ {
"description": "window:deny-theme -> Denies the theme command without any pre-configured scope.", "description": "window:deny-theme -> Denies the theme command without any pre-configured scope.",
"type": "string", "type": "string",

View File

@ -1287,6 +1287,13 @@
"updater:allow-check" "updater:allow-check"
] ]
}, },
{
"description": "updater:allow-download -> Enables the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-download"
]
},
{ {
"description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.", "description": "updater:allow-download-and-install -> Enables the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1294,6 +1301,13 @@
"updater:allow-download-and-install" "updater:allow-download-and-install"
] ]
}, },
{
"description": "updater:allow-install -> Enables the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:allow-install"
]
},
{ {
"description": "updater:deny-check -> Denies the check command without any pre-configured scope.", "description": "updater:deny-check -> Denies the check command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1301,6 +1315,13 @@
"updater:deny-check" "updater:deny-check"
] ]
}, },
{
"description": "updater:deny-download -> Denies the download command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-download"
]
},
{ {
"description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.", "description": "updater:deny-download-and-install -> Denies the download_and_install command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1308,6 +1329,13 @@
"updater:deny-download-and-install" "updater:deny-download-and-install"
] ]
}, },
{
"description": "updater:deny-install -> Denies the install command without any pre-configured scope.",
"type": "string",
"enum": [
"updater:deny-install"
]
},
{ {
"description": "webview:default -> Default permissions for the plugin.", "description": "webview:default -> Default permissions for the plugin.",
"type": "string", "type": "string",
@ -1371,6 +1399,13 @@
"webview:allow-set-webview-size" "webview:allow-set-webview-size"
] ]
}, },
{
"description": "webview:allow-set-webview-zoom -> Enables the set_webview_zoom command without any pre-configured scope.",
"type": "string",
"enum": [
"webview:allow-set-webview-zoom"
]
},
{ {
"description": "webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.", "description": "webview:allow-webview-close -> Enables the webview_close command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1448,6 +1483,13 @@
"webview:deny-set-webview-size" "webview:deny-set-webview-size"
] ]
}, },
{
"description": "webview:deny-set-webview-zoom -> Denies the set_webview_zoom command without any pre-configured scope.",
"type": "string",
"enum": [
"webview:deny-set-webview-zoom"
]
},
{ {
"description": "webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.", "description": "webview:deny-webview-close -> Denies the webview_close command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1511,6 +1553,13 @@
"window:allow-current-monitor" "window:allow-current-monitor"
] ]
}, },
{
"description": "window:allow-cursor-position -> Enables the cursor_position command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-cursor-position"
]
},
{ {
"description": "window:allow-destroy -> Enables the destroy command without any pre-configured scope.", "description": "window:allow-destroy -> Enables the destroy command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1630,6 +1679,13 @@
"window:allow-minimize" "window:allow-minimize"
] ]
}, },
{
"description": "window:allow-monitor-from-point -> Enables the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-monitor-from-point"
]
},
{ {
"description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.", "description": "window:allow-outer-position -> Enables the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1861,6 +1917,13 @@
"window:allow-start-dragging" "window:allow-start-dragging"
] ]
}, },
{
"description": "window:allow-start-resize-dragging -> Enables the start_resize_dragging command without any pre-configured scope.",
"type": "string",
"enum": [
"window:allow-start-resize-dragging"
]
},
{ {
"description": "window:allow-theme -> Enables the theme command without any pre-configured scope.", "description": "window:allow-theme -> Enables the theme command without any pre-configured scope.",
"type": "string", "type": "string",
@ -1931,6 +1994,13 @@
"window:deny-current-monitor" "window:deny-current-monitor"
] ]
}, },
{
"description": "window:deny-cursor-position -> Denies the cursor_position command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-cursor-position"
]
},
{ {
"description": "window:deny-destroy -> Denies the destroy command without any pre-configured scope.", "description": "window:deny-destroy -> Denies the destroy command without any pre-configured scope.",
"type": "string", "type": "string",
@ -2050,6 +2120,13 @@
"window:deny-minimize" "window:deny-minimize"
] ]
}, },
{
"description": "window:deny-monitor-from-point -> Denies the monitor_from_point command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-monitor-from-point"
]
},
{ {
"description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.", "description": "window:deny-outer-position -> Denies the outer_position command without any pre-configured scope.",
"type": "string", "type": "string",
@ -2281,6 +2358,13 @@
"window:deny-start-dragging" "window:deny-start-dragging"
] ]
}, },
{
"description": "window:deny-start-resize-dragging -> Denies the start_resize_dragging command without any pre-configured scope.",
"type": "string",
"enum": [
"window:deny-start-resize-dragging"
]
},
{ {
"description": "window:deny-theme -> Denies the theme command without any pre-configured scope.", "description": "window:deny-theme -> Denies the theme command without any pre-configured scope.",
"type": "string", "type": "string",

View File

@ -1,6 +1,6 @@
use tauri::{ use tauri::{
menu::{Menu, MenuItem}, menu::{Menu, MenuItem},
tray::{ClickType, TrayIconBuilder}, tray::{MouseButton, MouseButtonState, TrayIconBuilder, TrayIconEvent},
Manager, Runtime, Manager, Runtime,
}; };
@ -22,7 +22,12 @@ pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
_ => {} _ => {}
}) })
.on_tray_icon_event(|tray, event| { .on_tray_icon_event(|tray, event| {
if event.click_type == ClickType::Left { if let TrayIconEvent::Click {
button: MouseButton::Left,
button_state: MouseButtonState::Up,
..
} = event
{
let app = tray.app_handle(); let app = tray.app_handle();
if let Some(window) = app.get_webview_window("main") { if let Some(window) = app.get_webview_window("main") {
let _ = window.show(); let _ = window.show();

View File

@ -8,11 +8,11 @@ import RegistrationPage from "./pages/RegistrationPage";
import { getTauriVersion, getVersion } from "@tauri-apps/api/app"; import { getTauriVersion, getVersion } from "@tauri-apps/api/app";
import { arch, locale, platform, version } from "@tauri-apps/plugin-os"; import { arch, locale, platform, version } from "@tauri-apps/plugin-os";
import { useNetworkState } from "@uidotdev/usehooks";
import { reaction } from "mobx"; import { reaction } from "mobx";
import ErrorBoundary from "./components/ErrorBoundary"; import ErrorBoundary from "./components/ErrorBoundary";
import Loader from "./components/Loader"; import Loader from "./components/Loader";
import { UnauthenticatedGuard } from "./components/guards/UnauthenticatedGuard"; import { UnauthenticatedGuard } from "./components/guards/UnauthenticatedGuard";
import { BannerContext } from "./contexts/BannerContext";
import useLogger from "./hooks/useLogger"; import useLogger from "./hooks/useLogger";
import AppPage from "./pages/AppPage"; import AppPage from "./pages/AppPage";
import LogoutPage from "./pages/LogoutPage"; import LogoutPage from "./pages/LogoutPage";
@ -21,14 +21,14 @@ import { useAppStore } from "./stores/AppStore";
import { Globals } from "./utils/Globals"; import { Globals } from "./utils/Globals";
// @ts-expect-error no types // @ts-expect-error no types
import FPSStats from "react-fps-stats"; import FPSStats from "react-fps-stats";
import VeryImportantBanner from "./components/banners/VeryImportantBanner"; import { bannerController } from "./controllers/banners";
import { isTauri } from "./utils/Utils"; import { isTauri } from "./utils/Utils";
function App() { function App() {
const app = useAppStore(); const app = useAppStore();
const bannerContext = React.useContext(BannerContext);
const logger = useLogger("App"); const logger = useLogger("App");
const navigate = useNavigate(); const navigate = useNavigate();
const networkState = useNetworkState();
React.useEffect(() => { React.useEffect(() => {
// Handles gateway connection/disconnection on token change // Handles gateway connection/disconnection on token change
@ -69,13 +69,6 @@ function App() {
}; };
}; };
const hasVeryImportantBannerShown = localStorage.getItem("very_important_banner_dismissed");
if (!hasVeryImportantBannerShown) {
bannerContext.setContent({
element: <VeryImportantBanner />,
});
}
isTauri && loadAsyncGlobals(); isTauri && loadAsyncGlobals();
Globals.load(); Globals.load();
app.loadSettings(); app.loadSettings();
@ -86,14 +79,19 @@ function App() {
return dispose; return dispose;
}, []); }, []);
// React.useEffect(() => { React.useEffect(() => {
// if (!app.isNetworkConnected) if (!networkState.online) {
// bannerContext.setContent({ bannerController.push(
// forced: true, {
// element: <OfflineBanner />, type: "offline",
// }); },
// else bannerContext.close(); "offline",
// }, [app.isNetworkConnected, bannerContext]); );
} else {
// only close if the current banner is the offline banner
bannerController.remove("offline");
}
}, [networkState]);
return ( return (
<ErrorBoundary section="app"> <ErrorBoundary section="app">

View File

@ -6,8 +6,8 @@ export const Wrapper = styled(Container)`
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
height: 100vh;
background-color: var(--background-tertiary); background-color: var(--background-tertiary);
flex: 1;
`; `;
export const AuthContainer = styled(Container)` export const AuthContainer = styled(Container)`

View File

@ -7,50 +7,26 @@ import { useAppStore } from "../stores/AppStore";
import Presence from "../stores/objects/Presence"; import Presence from "../stores/objects/Presence";
import User from "../stores/objects/User"; import User from "../stores/objects/User";
import Container from "./Container"; import Container from "./Container";
import Floating from "./floating/Floating";
import FloatingTrigger from "./floating/FloatingTrigger";
const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>` const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>`
width: ${(props) => props.size}px;
height: ${(props) => props.size}px;
position: relative;
background-color: transparent; background-color: transparent;
display: flex;
&:hover { flex-direction: column;
text-decoration: underline;
cursor: ${(props) => (props.hasClick ? "pointer" : "default")};
}
`; `;
const StatusDot = styled.span<{ color: string; width?: number; height?: number }>`
position: absolute;
bottom: 0;
right: 0;
background-color: ${(props) => props.color};
border-radius: 50%;
border: 2px solid var(--background-primary);
width: ${(props) => props.width ?? 10}px;
height: ${(props) => props.height ?? 10}px;
`;
function Yes(onClick: React.MouseEventHandler<HTMLDivElement>) {
return ({ children }: { children: React.ReactNode }) => {
return <div onClick={onClick}>{children}</div>;
};
}
interface Props { interface Props {
user?: User | AccountStore; user?: User | AccountStore;
size?: number; size: 32 | 80;
style?: React.CSSProperties; style?: React.CSSProperties;
onClick?: React.MouseEventHandler<HTMLDivElement> | null; onClick?: React.MouseEventHandler<HTMLDivElement> | null;
popoutPlacement?: "left" | "right" | "top" | "bottom"; popoutPlacement?: "left" | "right" | "top" | "bottom";
presence?: Presence; presence?: Presence;
statusDotStyle?: { statusDotStyle?: {
width?: number; size?: number;
height?: number; borderThickness?: number;
}; };
showPresence?: boolean; showPresence?: boolean;
isFloating?: boolean;
} }
function Avatar(props: Props) { function Avatar(props: Props) {
@ -61,38 +37,79 @@ function Avatar(props: Props) {
const user = props.user ?? app.account; const user = props.user ?? app.account;
if (!user) return null; if (!user) return null;
// if onClick is null, use a div. if we pass a function, use yes. otherwise use FloatingTrigger const presenceRingsTreatment = app.experiments.getTreatment("presence_rings");
const Base = props.onClick === null ? "div" : props.onClick ? Yes(props.onClick) : FloatingTrigger; const ringsEnabled = presenceRingsTreatment && presenceRingsTreatment.id === 2;
return ( const children = (
<Floating <Wrapper size={props.size} style={props.style} ref={ref} hasClick={props.onClick !== null}>
placement="right-start" {props.showPresence && props.presence ? (
type="userPopout" !ringsEnabled ? (
props={{ <div
user: user as unknown as User, style={{
}} position: "relative",
> display: "inline-block",
<Base> width: props.size,
<Wrapper size={props.size ?? 32} style={props.style} ref={ref} hasClick={props.onClick !== null}> height: props.size,
}}
>
<img
style={{
borderRadius: "50%",
width: props.size,
height: props.size,
objectFit: "cover",
}}
src={user.avatarUrl}
loading="eager"
/>
<div
style={{
position: "absolute",
width: props.statusDotStyle?.size ?? 14,
height: props.statusDotStyle?.size ?? 14,
backgroundColor: app.theme.getStatusColor(
props.presence?.status ?? PresenceUpdateStatus.Offline,
),
borderRadius: "50%",
bottom: 0,
right: 0,
border: `${
props.statusDotStyle?.borderThickness ?? 0.2
}rem solid var(--background-secondary)`,
}}
></div>
</div>
) : (
<img <img
width={props.size}
height={props.size}
style={{ style={{
borderRadius: "50%", borderRadius: "50%",
pointerEvents: "none",
border: `0.2rem solid ${app.theme.getStatusColor(
props.presence?.status ?? PresenceUpdateStatus.Offline,
)}`,
}} }}
src={user.avatarUrl} src={user.avatarUrl}
width={props.size ?? 32}
height={props.size ?? 32}
loading="eager" loading="eager"
/> />
{props.showPresence && ( )
<StatusDot ) : (
color={app.theme.getStatusColor(props.presence?.status ?? PresenceUpdateStatus.Offline)} <img
{...props.statusDotStyle} width={props.size}
/> height={props.size}
)} style={{
</Wrapper> borderRadius: "50%",
</Base> pointerEvents: "none",
</Floating> }}
src={user.avatarUrl}
loading="eager"
/>
)}
</Wrapper>
); );
return children;
} }
export default observer(Avatar); export default observer(Avatar);

View File

@ -1,70 +0,0 @@
import { AnimatePresence, motion } from "framer-motion";
import React from "react";
import styled from "styled-components";
import { BannerContext } from "../contexts/BannerContext";
import useLogger from "../hooks/useLogger";
import Icon from "./Icon";
import IconButton from "./IconButton";
const Container = styled(motion.div)`
display: flex;
justify-content: center;
align-items: center;
`;
const CloseWrapper = styled(IconButton)`
position: absolute;
right: 1%;
`;
function Banner() {
const logger = useLogger("Banner");
const bannerContext = React.useContext(BannerContext);
if (!bannerContext.content) return null;
return (
<AnimatePresence>
<Container
variants={{
show: {
// slide down
y: 0,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
hide: {
// slide up
y: "-100%",
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
}}
initial="hide"
animate="show"
exit="hide"
onAnimationComplete={() => {
logger.debug("animation complete");
}}
style={bannerContext.content.style}
>
{bannerContext.content.element}
{!bannerContext.content.forced && (
<CloseWrapper
onClick={() => {
bannerContext.close();
}}
>
<Icon icon="mdiClose" color="var(--text)" size="24px" />
</CloseWrapper>
)}
</Container>
</AnimatePresence>
);
}
export default Banner;

View File

@ -1,5 +1,9 @@
import { useWindowSize } from "@uidotdev/usehooks";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { isDesktop } from "react-device-detect";
import styled from "styled-components"; import styled from "styled-components";
import { isTouchscreenDevice } from "../utils/isTouchscreenDevice";
import ChannelHeader from "./ChannelHeader"; import ChannelHeader from "./ChannelHeader";
import ChannelList from "./ChannelList/ChannelList"; import ChannelList from "./ChannelList/ChannelList";
import Container from "./Container"; import Container from "./Container";
@ -8,21 +12,36 @@ import UserPanel from "./UserPanel";
const Wrapper = styled(Container)` const Wrapper = styled(Container)`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 0 0 240px;
background-color: var(--background-secondary); background-color: var(--background-secondary);
@media (max-width: 810px) {
display: none;
}
`; `;
function ChannelSidebar() { function ChannelSidebar() {
const windowSize = useWindowSize();
//const isSmallScreen = useMediaQuery("only screen and (max-width: 810px)");
const [size, setSize] = useState<number | undefined>();
useEffect(() => {
if (!windowSize.width) return;
const screenPercent = (windowSize.width * 80) / 100;
setSize(screenPercent - 72);
}, [windowSize]);
return ( return (
<Wrapper> <Wrapper
style={
!isDesktop
? {
width: size,
}
: {
flex: "0 0 240px",
}
}
>
{/* TODO: replace with dm search if no guild */} {/* TODO: replace with dm search if no guild */}
<ChannelHeader /> <ChannelHeader />
<ChannelList /> <ChannelList />
<UserPanel /> {!isTouchscreenDevice && <UserPanel />}
</Wrapper> </Wrapper>
); );
} }

View File

@ -4,4 +4,5 @@ export default styled.div`
background-color: var(--background-tertiary); background-color: var(--background-tertiary);
color: var(--text); color: var(--text);
overflow: hidden; overflow: hidden;
display: flex;
`; `;

View File

@ -13,9 +13,9 @@ const Container = styled.div`
flex: 0 0 72px; flex: 0 0 72px;
margin: 4px 0 0 0; margin: 4px 0 0 0;
@media (max-width: 560px) { // @media (max-width: 560px) {
display: none; // display: none;
} // }
.ReactVirtualized__List { .ReactVirtualized__List {
scrollbar-width: none; /* Firefox */ scrollbar-width: none; /* Firefox */

View File

@ -1,36 +1,7 @@
import HCaptchaLib from "@hcaptcha/react-hcaptcha"; import HCaptchaLib from "@hcaptcha/react-hcaptcha";
import React from "react"; import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import Container from "./Container"; import { AuthContainer, Wrapper } from "./AuthComponents";
export const Wrapper = styled(Container)`
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
background-color: var(--background-secondary);
`;
export const AuthBox = styled(Container)`
background-color: var(--background-primary-alt);
padding: 32px;
font-size: 18px;
color: var(--text-muted);
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
@media (max-width: 480px) {
width: 100%;
height: 100%;
}
@media (min-width: 480px) {
width: 480px;
border-radius: 18px;
}
`;
export const HeaderContainer = styled.div` export const HeaderContainer = styled.div`
width: 100%; width: 100%;
@ -63,7 +34,7 @@ interface Props {
function HCaptcha(props: Props) { function HCaptcha(props: Props) {
return ( return (
<Wrapper> <Wrapper>
<AuthBox> <AuthContainer>
<HeaderContainer> <HeaderContainer>
<Header>Welcome Back!</Header> <Header>Welcome Back!</Header>
<SubHeader>Beep boop. Boop beep?</SubHeader> <SubHeader>Beep boop. Boop beep?</SubHeader>
@ -79,7 +50,7 @@ function HCaptcha(props: Props) {
onExpire={props.onExpire} onExpire={props.onExpire}
/> />
</HeaderContainer> </HeaderContainer>
</AuthBox> </AuthContainer>
</Wrapper> </Wrapper>
); );
} }

View File

@ -12,7 +12,7 @@ const Container = styled.div`
flex: 0 0 240px; flex: 0 0 240px;
flex-direction: column; flex-direction: column;
background-color: var(--background-secondary); background-color: var(--background-secondary);
height: 100%; overflow-x: hidden;
@media (max-width: 1050px) { @media (max-width: 1050px) {
display: none; display: none;
@ -23,9 +23,9 @@ const List = styled.ul`
padding: 0; padding: 0;
margin: 0; margin: 0;
list-style: none; list-style: none;
overflow-y: auto; // overflow-y: auto;
height: 100%; // height: 100%;
width: 100%; // width: 100%;
`; `;
function MemberList() { function MemberList() {
@ -59,7 +59,7 @@ function MemberList() {
<MemberListItem item={x} /> <MemberListItem item={x} />
))} ))}
/> />
)) ))
: null} : null}
</List> </List>
</Container> </Container>

View File

@ -8,5 +8,5 @@ export const SectionHeader = styled.div`
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
white-space: nowrap; white-space: nowrap;
height: 24px; height: 50px;
`; `;

View File

@ -0,0 +1,26 @@
import React from "react";
import styled from "styled-components";
const Container = styled.div`
display: flex;
`;
const Text = styled.h2`
color: var(--text);
margin-bottom: 20px;
font-size: 20px;
font-weight: var(--font-weight-medium);
flex: 1;
`;
interface Props {}
function SectionTitle({ children }: React.PropsWithChildren<Props>) {
return (
<Container>
<Text>{children}</Text>
</Container>
);
}
export default SectionTitle;

View File

@ -0,0 +1,80 @@
import { useWindowSize } from "@uidotdev/usehooks";
import React from "react";
import { animated, config, useSpring } from "react-spring";
import { useDrag } from "react-use-gesture";
import styles from "./styles.module.css";
interface Props {
leftChildren: React.ReactNode;
rightChildren: React.ReactNode;
children: React.ReactNode;
}
function SwipeableLayout({ leftChildren, children, rightChildren }: Props) {
const size = useWindowSize();
const [{ x }, api] = useSpring(() => ({
x: 0,
}));
const open = (canceled: boolean) => {
// when cancel is true, it means that the user passed the upwards threshold
// so we change the spring config to create a nice wobbly effect
api.start({ x: (size.width! * 80) / 100, immediate: false, config: canceled ? config.wobbly : config.stiff });
};
const close = (velocity = 0) => {
api.start({ x: 0, immediate: false, config: { ...config.stiff, velocity } });
};
const bind = useDrag(
({ last, velocity: v, direction: [dx], offset: [ox], cancel, canceled }) => {
const maxWidth = size.width! * 0.5;
console.debug("=-=-=-=-=-=-=-=-=-=-");
console.debug(`X is: `, x.get());
console.debug(`Max width is: `, maxWidth);
console.debug(`Last`, last);
console.debug(`Velocity is: `, v);
console.debug(`Direction is: `, dx);
console.debug(`Offset is: `, ox);
// // on release, check if passed threshold to close, or reset to open pos
// if (last) {
// // if direction is < 0 (left), and offset is less than 50% of the screen width then close
// ox < maxWidth && dx === -1 ? close(v) : open(canceled);
// } else api.start({ x: ox });
api.start({ x: ox });
},
{
from: () => [x.get(), 0],
filterTaps: true,
bounds: { left: 0, right: (size.width! * 80) / 100 },
rubberband: true,
// initial: () => [x.get(), 0],
axis: "x",
},
);
// handle resize
React.useEffect(() => {
console.log("width change");
if (x.get() > 0) {
open(false);
} else {
close();
}
}, [size.width]);
return (
<animated.div {...bind()} className={styles.item}>
{leftChildren}
<animated.div className={styles.fg} style={{ x }}>
{children}
</animated.div>
</animated.div>
);
}
export default SwipeableLayout;

View File

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite";
import styled from "styled-components"; import styled from "styled-components";
import { modalController } from "../controllers/modals"; import { modalController } from "../controllers/modals";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../stores/AppStore";
import User from "../stores/objects/User";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import Icon from "./Icon"; import Icon from "./Icon";
import IconButton from "./IconButton"; import IconButton from "./IconButton";
@ -22,7 +22,7 @@ const Container = styled.div`
background-color: var(--background-secondary-alt); background-color: var(--background-secondary-alt);
`; `;
const AvatarWrapper = styled(FloatingTrigger)` const AvatarWrapper = styled.div`
display: flex; display: flex;
align-items: center; align-items: center;
min-width: 120px; min-width: 120px;
@ -30,10 +30,6 @@ const AvatarWrapper = styled(FloatingTrigger)`
margin-right: 8px; margin-right: 8px;
border-radius: 4px; border-radius: 4px;
cursor: default; cursor: default;
&:hover {
background-color: var(--background-primary-alt);
}
`; `;
const Name = styled.div` const Name = styled.div`
@ -68,8 +64,15 @@ const ActionsWrapper = styled.div`
display: flex; display: flex;
`; `;
const SettingsButton = styled(IconButton)`
&:hover {
opacity: 0.8;
}
`;
function UserPanel() { function UserPanel() {
const app = useAppStore(); const app = useAppStore();
const presence = app.presences.get(app.account!.id);
const openSettingsModal = () => { const openSettingsModal = () => {
modalController.push({ modalController.push({
@ -78,43 +81,35 @@ function UserPanel() {
}; };
return ( return (
<Floating <Section>
placement="bottom" <Container>
type="userPopout" <AvatarWrapper>
props={{ <Avatar popoutPlacement="top" onClick={null} showPresence presence={presence} size={32} />
user: app.account! as unknown as User, <Name>
}} <Username>{app.account?.username}</Username>
> <Subtext>#{app.account?.discriminator}</Subtext>
<Section> </Name>
<Container> </AvatarWrapper>
<AvatarWrapper>
<Avatar popoutPlacement="top" onClick={null} />
<Name>
<Username>{app.account?.username}</Username>
<Subtext>#{app.account?.discriminator}</Subtext>
</Name>
</AvatarWrapper>
<ActionsWrapper> <ActionsWrapper>
<Floating <Floating
placement="top" placement="top"
type="tooltip" type="tooltip"
offset={10} offset={10}
props={{ props={{
content: <span>Settings</span>, content: <span>Settings</span>,
}} }}
> >
<FloatingTrigger> <FloatingTrigger>
<IconButton aria-label="settings" color="#fff" onClick={openSettingsModal}> <SettingsButton aria-label="settings" color="#fff" onClick={openSettingsModal}>
<Icon icon="mdiCog" size="20px" /> <Icon icon="mdiCog" size="20px" />
</IconButton> </SettingsButton>
</FloatingTrigger> </FloatingTrigger>
</Floating> </Floating>
</ActionsWrapper> </ActionsWrapper>
</Container> </Container>
</Section> </Section>
</Floating>
); );
} }
export default UserPanel; export default observer(UserPanel);

View File

@ -4,6 +4,8 @@ import Icon from "../Icon";
const Wrapper = styled.div` const Wrapper = styled.div`
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex: 1;
justify-content: center;
align-items: center; align-items: center;
`; `;

View File

@ -1,39 +0,0 @@
import styled from "styled-components";
import { modalController } from "../../controllers/modals";
import Button from "../Button";
const Wrapper = styled.div`
display: flex;
flex-direction: row;
background-color: var(--primary);
flex: 1;
justify-content: center;
align-items: center;
`;
const Text = styled.span`
padding: 10px;
`;
function VeryImportantBanner() {
return (
<Wrapper>
<Text>We have a suprise for you!</Text>
<Button
onClick={() => {
modalController.push({
type: "very_important",
});
}}
style={{
backgroundColor: "var(--primary-light)",
border: "1px solid var(--primary-dark)",
}}
>
Show me
</Button>
</Wrapper>
);
}
export default VeryImportantBanner;

View File

@ -22,9 +22,7 @@ const Container = styled.div`
width: 340px; width: 340px;
max-height: 600px; max-height: 600px;
overflow: hidden; overflow: hidden;
box-shadow: box-shadow: 0 0 0 1px rgb(0 0 0 / 15%), 0 4px 8px rgb(0 0 0 / 15%);
0 0 0 1px rgb(0 0 0 / 15%),
0 4px 8px rgb(0 0 0 / 15%);
color: var(--text); color: var(--text);
`; `;
@ -185,10 +183,11 @@ function UserProfilePopout({ user, member }: Props) {
user={user} user={user}
presence={presence} presence={presence}
statusDotStyle={{ statusDotStyle={{
width: 16, size: 25,
height: 16, borderThickness: 0.3,
}} }}
showPresence showPresence
isFloating
/> />
</Top> </Top>
<Bottom> <Bottom>

View File

@ -18,6 +18,7 @@ iframe {
.embedImage { .embedImage {
cursor: pointer; cursor: pointer;
border-radius: 8px; border-radius: 8px;
max-width: 100%;
} }
.website { .website {
@ -29,7 +30,6 @@ iframe {
border-inline-start-style: solid; border-inline-start-style: solid;
padding: 12px; padding: 12px;
width: fit-content;
background: var(--background-secondary); background: var(--background-secondary);
border-radius: 4px; border-radius: 4px;
} }
@ -44,7 +44,6 @@ iframe {
font-size: 12px; font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
font-weight: var(--font-weight-regular); font-weight: var(--font-weight-regular);
width: fit-content;
} }
.embedAuthor { .embedAuthor {
@ -100,6 +99,7 @@ iframe {
.embedDescription { .embedDescription {
font-size: 14px; font-size: 14px;
line-height: 16px;
overflow: hidden; overflow: hidden;
display: -webkit-box; display: -webkit-box;
white-space: pre-wrap; white-space: pre-wrap;

View File

@ -6,6 +6,27 @@ import { modalController } from "../../controllers/modals";
import Icon from "../Icon"; import Icon from "../Icon";
import styles from "./Embed.module.css"; import styles from "./Embed.module.css";
function getScaledDimensions(originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number) {
const aspectRatio = originalWidth / originalHeight;
let newWidth = originalWidth;
let newHeight = originalHeight;
if (newWidth > maxWidth) {
newWidth = maxWidth;
newHeight = newWidth / aspectRatio;
}
if (newHeight > maxHeight) {
newHeight = maxHeight;
newWidth = newHeight * aspectRatio;
}
return { width: Math.round(newWidth), height: Math.round(newHeight) };
}
function shouldScaleImage(originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number) {
return originalWidth > maxWidth || originalHeight > maxHeight;
}
interface Props { interface Props {
embed: APIEmbed; embed: APIEmbed;
width?: number; width?: number;
@ -14,12 +35,49 @@ interface Props {
} }
function EmbedMedia({ embed, width, height, thumbnail }: Props) { function EmbedMedia({ embed, width, height, thumbnail }: Props) {
let maxWidth = 400;
let maxHeight = 300;
if (!width || !height) {
if (embed.video) {
width = embed.video.width;
height = embed.video.height;
} else if (embed.image) {
width = embed.image.width;
height = embed.image.height;
} else if (embed.thumbnail) {
if (embed.type !== EmbedType.Image && embed.provider?.name !== "GitHub") {
maxWidth = 80;
maxHeight = 80;
}
width = embed.thumbnail.width;
height = embed.thumbnail.height;
} else {
console.log("No media size provided");
width = 400;
height = 300;
}
}
const originalWidth = width;
const originalHeight = height;
// Scale image if it's too large
if (shouldScaleImage(width!, height!, maxWidth, maxHeight)) {
const { width: newWidth, height: newHeight } = getScaledDimensions(width!, height!, maxWidth, maxHeight);
width = newWidth;
height = newHeight;
}
console.log(`Original size: ${originalWidth}x${originalHeight} - Scaled size: ${width}x${height}`);
switch (embed.provider?.name) { switch (embed.provider?.name) {
case "YouTube": { case "YouTube": {
if (!embed.video?.url) return null; if (!embed.video?.url) return null;
const url = embed.video.url; const url = embed.video.url;
return <iframe loading="lazy" src={url} allowFullScreen style={{ height }} />; return <iframe loading="lazy" src={url} allowFullScreen style={{ height, width }} />;
} }
case "Spotify": { case "Spotify": {
const url = embed.url; const url = embed.url;
@ -128,7 +186,6 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
className={styles.embedImage} className={styles.embedImage}
src={url} src={url}
loading="lazy" loading="lazy"
style={{ width: "100%", height: "100%" }}
onClick={() => { onClick={() => {
modalController.push({ modalController.push({
type: "image_viewer", type: "image_viewer",
@ -145,7 +202,7 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
className={thumbnail ? styles.embedThumbnail : styles.embedImage} className={thumbnail ? styles.embedThumbnail : styles.embedImage}
src={url} src={url}
loading="lazy" loading="lazy"
style={{ width, height }} style={{ height, width }}
onClick={() => { onClick={() => {
modalController.push({ modalController.push({
type: "image_viewer", type: "image_viewer",

View File

@ -42,7 +42,7 @@ function Message({ message, header }: Props) {
> >
<MessageInfo> <MessageInfo>
{header ? ( {header ? (
<Avatar key={message.author.id} user={message.author} size={40} /> <Avatar key={message.author.id} user={message.author} size={32} />
) : ( ) : (
<MessageDetails message={message} position="left" /> <MessageDetails message={message} position="left" />
)} )}

View File

@ -9,13 +9,11 @@ import Markdown from "../markdown/Markdown";
import MarkdownRenderer from "../markdown/MarkdownRenderer"; import MarkdownRenderer from "../markdown/MarkdownRenderer";
import styles from "./Embed.module.css"; import styles from "./Embed.module.css";
import EmbedMedia from "./EmbedMedia"; import EmbedMedia from "./EmbedMedia";
import { MESSAGE_AREA_PADDING, MessageAreaWidthContext } from "./MessageList"; import { MessageAreaWidthContext } from "./MessageList";
const MAX_EMBED_WIDTH = 300; const LINK_EMBED_MAX_WIDTH = 516;
const MAX_EMBED_HEIGHT = 640; const RICH_EMBED_MAX_WIDTH = 428;
const THUMBNAIL_MAX_WIDTH = 80;
const CONTAINER_PADDING = 24; const CONTAINER_PADDING = 24;
const MAX_PREVIEW_SIZE = 150;
const EMBEDDABLE_PROVIDERS = ["Spotify" /*, "Bandcamp"*/]; const EMBEDDABLE_PROVIDERS = ["Spotify" /*, "Bandcamp"*/];
interface Props { interface Props {
@ -24,19 +22,19 @@ interface Props {
function MessageEmbed({ embed }: Props) { function MessageEmbed({ embed }: Props) {
const c = React.useContext(MessageAreaWidthContext); const c = React.useContext(MessageAreaWidthContext);
const maxWidth = Math.min(c - MESSAGE_AREA_PADDING, MAX_EMBED_WIDTH); // const maxWidth = Math.min(c - MESSAGE_AREA_PADDING, MAX_EMBED_WIDTH);
function calculateSize(w: number, h: number): { width: number; height: number } { // function calculateSize(w: number, h: number): { width: number; height: number } {
const limitingWidth = Math.min(w, maxWidth); // const limitingWidth = Math.min(w, maxWidth);
const limitingHeight = Math.min(MAX_EMBED_HEIGHT, h); // const limitingHeight = Math.min(MAX_EMBED_HEIGHT, h);
// Calculate smallest possible WxH. // // Calculate smallest possible WxH.
const width = Math.min(limitingWidth, limitingHeight * (w / h)); // const width = Math.min(limitingWidth, limitingHeight * (w / h));
const height = Math.min(limitingHeight, limitingWidth * (h / w)); // const height = Math.min(limitingHeight, limitingWidth * (h / w));
return { width, height }; // return { width, height };
} // }
// Determine special embed size. // Determine special embed size.
let mw, mh; let mw, mh;
@ -47,42 +45,14 @@ function MessageEmbed({ embed }: Props) {
embed.type === EmbedType.GIFV || embed.type === EmbedType.GIFV ||
embed.type === EmbedType.Image; embed.type === EmbedType.Image;
if (embed.image) { // const { width, height } = calculateSize(mw, mh);
mw = embed.image?.width ?? MAX_EMBED_WIDTH;
mh = embed.image?.height ?? 0;
} else if (embed.thumbnail) {
mw = embed.thumbnail.width ?? MAX_EMBED_WIDTH;
mh = embed.thumbnail.height ?? 0;
} else {
switch (embed.provider?.name) {
case "YouTube":
case "Bandcamp": {
mw = embed.video?.width ?? 1280;
mh = embed.video?.height ?? 720;
break;
}
case "Twitch":
case "Lightspeed":
case "Streamable": {
mw = 1280;
mh = 720;
break;
}
default: {
mw = MAX_EMBED_WIDTH;
mh = 1;
}
}
}
const { width, height } = calculateSize(mw, mh);
if ( if (
embed.type === EmbedType.GIFV || embed.type === EmbedType.GIFV ||
embed.type === EmbedType.Image || embed.type === EmbedType.Image ||
embed.type === EmbedType.Video || (embed.type === EmbedType.Video && embed.provider?.name !== "YouTube") ||
EMBEDDABLE_PROVIDERS.includes(embed.provider?.name ?? "") EMBEDDABLE_PROVIDERS.includes(embed.provider?.name ?? "")
) { ) {
return <EmbedMedia embed={embed} width={height} height={height} />; return <EmbedMedia embed={embed} />;
} }
return ( return (
@ -90,10 +60,15 @@ function MessageEmbed({ embed }: Props) {
className={classNames(styles.embed, styles.website)} className={classNames(styles.embed, styles.website)}
style={{ style={{
borderInlineStartColor: embed.color ? decimalColorToHex(embed.color) : "var(--background-tertiary)", borderInlineStartColor: embed.color ? decimalColorToHex(embed.color) : "var(--background-tertiary)",
maxWidth: width + CONTAINER_PADDING, maxWidth: 432,
}} }}
> >
<div className={styles.embedGap}> <div
className={styles.embedGap}
style={{
maxWidth: embed.type === EmbedType.Rich ? RICH_EMBED_MAX_WIDTH : LINK_EMBED_MAX_WIDTH,
}}
>
<div style={{ display: "flex" }}> <div style={{ display: "flex" }}>
<div className={styles.embedGap}> <div className={styles.embedGap}>
{embed.type !== EmbedType.Rich && embed.provider && ( {embed.type !== EmbedType.Rich && embed.provider && (
@ -108,7 +83,7 @@ function MessageEmbed({ embed }: Props) {
className={styles.embedAuthorIcon} className={styles.embedAuthorIcon}
src={embed.author.icon_url} src={embed.author.icon_url}
draggable={false} draggable={false}
onError={(e) => (e.currentTarget.style.display = "none")} // onError={(e) => (e.currentTarget.style.display = "none")}
/> />
)} )}
{embed.author.url ? ( {embed.author.url ? (
@ -178,7 +153,7 @@ function MessageEmbed({ embed }: Props) {
{(largeMedia || embed.type === EmbedType.Rich) && ( {(largeMedia || embed.type === EmbedType.Rich) && (
<div> <div>
<EmbedMedia embed={embed} width={width} /> <EmbedMedia embed={embed} width={embed.provider?.name === "YouTube" ? 400 : undefined} />
</div> </div>
)} )}
@ -190,7 +165,7 @@ function MessageEmbed({ embed }: Props) {
className={styles.embedFooterIcon} className={styles.embedFooterIcon}
src={embed.footer.icon_url} src={embed.footer.icon_url}
draggable={false} draggable={false}
onError={(e) => (e.currentTarget.style.display = "none")} // onError={(e) => (e.currentTarget.style.display = "none")}
/> />
)} )}
<span className={styles.embedFooterText}> <span className={styles.embedFooterText}>

View File

@ -7,7 +7,8 @@ import Icon from "../../Icon";
import IconButton from "../../IconButton"; import IconButton from "../../IconButton";
const Container = styled.div` const Container = styled.div`
width: 100%; width: 520px;
min-width: auto;
border: 1px solid transparent; border: 1px solid transparent;
padding: 10px; padding: 10px;
border-radius: 4px; border-radius: 4px;

View File

@ -26,7 +26,15 @@ export function ImageViewerModal(props: Props) {
const height = props.height ?? props.attachment.height ?? 0; const height = props.height ?? props.attachment.height ?? 0;
return ( return (
<Modal {...props} transparent maxWidth="100vw" maxHeight="100vh" withoutCloseButton withEmptyActionBar> <Modal
{...props}
transparent
maxWidth="100vw"
maxHeight="100vh"
withoutCloseButton
withEmptyActionBar
padding="0"
>
<Container> <Container>
{props.isVideo ? ( {props.isVideo ? (
<video <video

View File

@ -27,6 +27,7 @@ interface ModalProps {
disabled?: boolean; disabled?: boolean;
withEmptyActionBar?: boolean; withEmptyActionBar?: boolean;
withoutCloseButton?: boolean; withoutCloseButton?: boolean;
fullScreen?: boolean;
} }
/** /**
@ -48,7 +49,7 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
animation-fill-mode: forwards; animation-fill-mode: forwards;
display: grid; display: grid;
overflow-y: auto; overflow: hidden;
place-items: center; place-items: center;
color: var(--text); color: var(--text);
@ -72,12 +73,8 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
* Wrapper for modal content, handles the sizing and positioning * Wrapper for modal content, handles the sizing and positioning
*/ */
export const ModalWrapper = styled.div< export const ModalWrapper = styled.div<
Pick<ModalProps, "transparent" | "maxWidth" | "maxHeight"> & { actions: boolean } Pick<ModalProps, "transparent" | "maxWidth" | "maxHeight"> & { actions: boolean; fullScreen?: boolean }
>` >`
min-height: 200px;
max-width: min(calc(100vw - 20px), ${(props) => props.maxWidth ?? "450px"});
max-height: min(calc(100vh - 20px), ${(props) => props.maxHeight ?? "650px"});
margin: 20px; margin: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -86,17 +83,27 @@ export const ModalWrapper = styled.div<
animation-duration: 0.25s; animation-duration: 0.25s;
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1); animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
overflow: hidden;
background: var(--background-tertiary);
${(props) => ${(props) =>
!props.maxWidth && !props.fullScreen &&
css`
max-width: min(calc(100vw - 20px), ${props.maxWidth ?? "450px"});
max-height: min(calc(100vh - 20px), ${props.maxHeight ?? "650px"});
`}
${(props) =>
props.fullScreen &&
css` css`
width: 100%; width: 100%;
height: 100%;
`} `}
${(props) => ${(props) =>
!props.transparent && !props.transparent &&
!props.fullScreen &&
css` css`
overflow: hidden;
background: var(--background-primary);
border-radius: 8px; border-radius: 8px;
`} `}
`; `;
@ -122,12 +129,6 @@ export const ModalContentContainer = styled.div<Pick<ModalProps, "transparent" |
overflow-y: auto; overflow-y: auto;
font-size: 0.9375rem; font-size: 0.9375rem;
${(props) =>
!props.transparent &&
css`
background: var(--background-primary);
`}
`; `;
const Actions = styled.div` const Actions = styled.div`

View File

@ -1,105 +1,264 @@
import { FormControlLabel, FormGroup, Switch } from "@mui/material";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import styled from "styled-components"; import { useState } from "react";
import styled, { css } from "styled-components";
import { ModalProps } from "../../controllers/modals"; import { ModalProps } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../stores/AppStore";
import { isTauri } from "../../utils/Utils"; import { isTauri } from "../../utils/Utils";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
import Button from "../Button"; import Icon from "../Icon";
import Link from "../Link"; import Link from "../Link";
import { Modal } from "./ModalComponents"; import { Modal } from "./ModalComponents";
import AccountSettingsPage from "./SettingsPages/AccountSettingsPage";
import DeveloperSettingsPage from "./SettingsPages/DeveloperSettingsPage";
import ExperimentsPage from "./SettingsPages/ExperimentsPage";
const Wrapper = styled.div` const SidebarView = styled.div`
padding: 16px 0; display: flex;
gap: 8px; flex: 1;
overflow: hidden;
`;
const Sidebar = styled.div`
display: flex;
flex: 1 0 220px;
justify-content: flex-end;
`;
const SidebarInner = styled.div`
overflow: hidden scroll;
display: flex;
flex: 1 0 auto;
flex-direction: row;
justify-content: flex-end;
align-items: flex-start;
background: var(--background-secondary);
`;
const SidebarNav = styled.nav`
width: 220px;
padding: 60px 6px 20px;
box-sizing: border-box;
`;
const SidebarNavWrapper = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
`; `;
const ActionWrapper = styled.div` const Content = styled.div`
margin-top: 20px;
gap: 8px;
display: flex; display: flex;
flex: 1 1 800px;
align-items: flex-start;
background: var(--background-primary);
`; `;
const VersionWrapper = styled.div` const ContentInner = styled.div`
overflow: hidden scroll;
justify-content: flex-start;
position: static;
display: flex; display: flex;
flex-direction: column; flex-direction: row;
user-select: text; align-items: flex-start;
background: var(--background-primary);
box-sizing: border-box;
`;
& > span { const ContentColumn = styled.div`
color: var(--text-secondary); padding: 60px 40px 80px;
flex: 1 1 auto;
max-width: 740px;
min-width: 460px;
min-height: 100%;
box-sizing: border-box;
`;
const Header = styled.div`
padding: 6px 10px;
color: var(--text-secondary);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex-shrink: 0;
font-size: 14px;
font-weight: var(--font-weight-bold);
letter-spacing: 0.5px;
`;
const Item = styled.div<{ selected?: boolean; textColor?: string }>`
padding: 5px 10px;
margin-bottom: 5px;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
font-weight: var(--font-weight-regular);
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
flex-shrink: 0;
color: ${(props) => props.textColor ?? "var(--text-secondary);"};
&:hover {
background-color: hsl(var(--background-primary-hsl) / 0.6);
cursor: pointer;
} }
${(props) =>
props.selected &&
css`
background-color: var(--background-primary);
color: var(--text);
`}
`;
const Divider = styled.div`
margin: 8px 10px;
height: 1px;
background-color: var(--text-disabled);
`;
const VersionInfo = styled.div`
padding: 8px 10px;
color: var(--text-secondary);
font-size: 12px;
font-weight: var(--font-weight-regular);
`;
const CloseContainer = styled.div`
margin-right: 20px;
flex: 0 0 36px;
width: 60px;
padding-top: 60px;
position: relative;
`;
const CloseContainerInner = styled.div`
position: fixed;
`;
const CloseContainerWrapper = styled.div`
display: flex;
flex-direction: column;
align-items: center;
`;
const CloseButtonWrapper = styled.div`
display: flex;
justify-content: center;
align-items: center;
flex: 0 0 40px;
border: solid 1px;
border-radius: 50%;
width: 40px;
height: 40px;
cursor: pointer;
color: var(--text-secondary);
`; `;
export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => { export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => {
const app = useAppStore(); const app = useAppStore();
const [index, setIndex] = useState(0);
const onClick = (e: React.MouseEvent<HTMLDivElement>) => {
const value = e.currentTarget.getAttribute("data-value");
if (value) {
setIndex(parseInt(value));
}
};
return ( return (
<Modal {...props}> <Modal {...props} fullScreen withoutCloseButton withEmptyActionBar padding="0">
<Wrapper> <SidebarView>
<FormGroup> <Sidebar>
<FormControlLabel <SidebarInner>
control={<Switch checked={app.fpsShown} onChange={(e) => app.setFpsShown(e.target.checked)} />} <SidebarNav>
label="Show FPS Graph" <SidebarNavWrapper>
/> <Header>User Settings</Header>
</FormGroup> <Item data-value="0" onClick={onClick}>
Account
{isTauri && ( </Item>
<FormGroup> <Divider />
<FormControlLabel <Item data-value="1" onClick={onClick}>
control={ Developer Options
<Switch </Item>
checked={app.updaterStore?.enabled} <Item data-value="2" onClick={onClick}>
onChange={(e) => app.updaterStore?.setEnabled(e.target.checked)} Experiments
/> </Item>
} <Divider />
label="Enabled auto updater" <Item onClick={app.logout}>
/> <div
</FormGroup> style={{
)} display: "flex",
justifyContent: "space-between",
<VersionWrapper> alignItems: "center",
<span> color: "var(--error)",
Client Version:{" "} }}
<Link href={`${REPO_URL}/commit/${GIT_REVISION}`} target="_blank" rel="noreferrer"> >
{GIT_REVISION.substring(0, 7)} Log Out
</Link> <Icon icon="mdiLogout" size="16px" color="var(--error)" />
{` `} </div>
<Link </Item>
href={GIT_BRANCH !== "DETACHED" ? `${REPO_URL}/tree/${GIT_BRANCH}` : undefined} <Divider />
target="_blank" <VersionInfo>
rel="noreferrer" <span>
> {GIT_BRANCH} {APP_VERSION} (
({GIT_BRANCH}) <Link
</Link> href={`${REPO_URL}/commit/${GIT_REVISION}`}
</span> target="_blank"
rel="noreferrer"
{isTauri && ( >
<> {GIT_REVISION.substring(0, 7)}
<span>App Version: {window.globals.appVersion ?? "Fetching version information..."}</span> </Link>
<span> )
Tauri Version: {window.globals.tauriVersion ?? "Fetching version information..."} </span>
</span> {isTauri && (
<span>Platform: {window.globals.platform.name}</span> <>
<span>Arch: {window.globals.platform.arch}</span> {/* <span>
<span>OS Version: {window.globals.platform.version}</span> {window.globals.appVersion
<span>Locale: {window.globals.platform.locale ?? "Unknown"}</span> ? `${window.globals.appVersion} (${(
</> <Link
)} href={`${REPO_URL}/commit/${GIT_REVISION}`}
</VersionWrapper> target="_blank"
rel="noreferrer"
<ActionWrapper> >
<Button {GIT_REVISION.substring(0, 7)}
palette="danger" </Link>
onClick={() => { )})`
app.logout(); : "Fetching version information..."}
}} </span> */}
> <span>
Logout Tauri {window.globals.tauriVersion ?? "Fetching version information..."}
</Button> </span>
</ActionWrapper> <span>{`${window.globals.platform.name} ${window.globals.platform.arch} (${window.globals.platform.version})`}</span>
</Wrapper> <span>{window.globals.platform.locale ?? "Unknown"}</span>
</>
)}
</VersionInfo>
</SidebarNavWrapper>
</SidebarNav>
</SidebarInner>
</Sidebar>
<Content>
<ContentInner>
<ContentColumn>
{index === 0 && <AccountSettingsPage />}
{index === 1 && <DeveloperSettingsPage />}
{index === 2 && <ExperimentsPage />}
</ContentColumn>
<CloseContainer>
<CloseContainerInner></CloseContainerInner>
<CloseContainerWrapper>
<CloseButtonWrapper
onClick={() => {
console.log("Close modal");
}}
>
<Icon icon="mdiClose" size="18px" />
</CloseButtonWrapper>
</CloseContainerWrapper>
</CloseContainer>
</ContentInner>
</Content>
</SidebarView>
</Modal> </Modal>
); );
}); });

View File

@ -0,0 +1,146 @@
import { observer } from "mobx-react-lite";
import { useState } from "react";
import styled, { css } from "styled-components";
import { useAppStore } from "../../../stores/AppStore";
import SectionTitle from "../../SectionTitle";
const Content = styled.div`
display: flex;
flex-direction: column;
`;
const UserInfoContainer = styled.div`
border-radius: 8px;
background-color: var(--background-secondary);
padding: 16px;
`;
const Field = styled.div<{ spacerTop?: boolean; spacerBottom?: boolean }>`
display: flex;
flex-direction: row;
justify-content: space-between;
${(props) =>
props.spacerTop &&
css`
margin-top: 24px;
`}
${(props) =>
props.spacerBottom &&
css`
margin-bottom: 24px;
`}
`;
const Row = styled.div`
display: flex;
flex: 1;
flex-direction: column;
overflow: hidden;
margin-right: 16px;
`;
const FieldTitle = styled.span`
margin-bottom: 4px;
color: var(--text-secondary);
font-size: 12px;
font-weight: var(--font-weight-medium);
letter-spacing: 0.5px;
`;
const FieldValue = styled.div`
overflow: hidden;
text-overflow: ellipsis;
`;
const FieldValueText = styled.span`
color: var(--text);
font-size: 16px;
font-weight: var(--font-weight-regular);
`;
const FieldValueToggle = styled.button`
color: var(--text-link);
cursor: pointer;
width: auto;
display: inline;
height: auto;
padding: 2px 4px;
position: relative;
background: none;
border: none;
border-radius: 4px;
font-size: 14px;
font-weight: var(--font-weight-medium);
user-select: none;
text-rendering: optimizeLegibility;
`;
function AccountSettingsPage() {
const app = useAppStore();
const [shouldRedactEmail, setShouldRedactEmail] = useState(true);
const redactEmail = (email: string) => {
const [username, domain] = email.split("@");
return `${"*".repeat(username.length)}@${domain}`;
};
const refactPhoneNumber = (phoneNumber: string) => {
const lastFour = phoneNumber.slice(-4);
return "*".repeat(phoneNumber.length - 4) + lastFour;
};
return (
<div>
<SectionTitle>Account</SectionTitle>
<Content>
<UserInfoContainer>
<Field spacerBottom>
<Row>
<FieldTitle>Username</FieldTitle>
<FieldValue>
<FieldValueText>
{app.account?.username}#{app.account?.discriminator}
</FieldValueText>
</FieldValue>
</Row>
</Field>
<Field>
<Row>
<FieldTitle>Email</FieldTitle>
<FieldValue>
<FieldValueText>
{app.account?.email
? shouldRedactEmail
? redactEmail(app.account.email)
: app.account.email
: "No email added."}
<FieldValueToggle onClick={() => setShouldRedactEmail(!shouldRedactEmail)}>
{shouldRedactEmail ? "Reveal" : "Hide"}
</FieldValueToggle>
</FieldValueText>
</FieldValue>
</Row>
</Field>
<Field spacerTop>
<Row>
<FieldTitle>Phone Number</FieldTitle>
<FieldValue>
<FieldValueText>No phone number added.</FieldValueText>
</FieldValue>
</Row>
</Field>
</UserInfoContainer>
</Content>
</div>
);
}
export default observer(AccountSettingsPage);

View File

@ -0,0 +1,19 @@
import { observer } from "mobx-react-lite";
import styled from "styled-components";
import SectionTitle from "../../SectionTitle";
const Content = styled.div`
display: flex;
flex-direction: column;
`;
function DeveloperSettingsPage() {
return (
<div>
<SectionTitle>Developer Options</SectionTitle>
<Content></Content>
</div>
);
}
export default observer(DeveloperSettingsPage);

View File

@ -0,0 +1,118 @@
import { observer } from "mobx-react-lite";
import { useState } from "react";
import styled from "styled-components";
import { useAppStore } from "../../../stores/AppStore";
import { EXPERIMENT_LIST, Experiment as ExperimentType } from "../../../stores/ExperimentsStore";
import SectionTitle from "../../SectionTitle";
const Content = styled.div`
display: flex;
flex-direction: column;
`;
const ExperimentList = styled.ul`
display: grid;
list-style: none;
padding: 0;
margin: 0;
gap: 10px;
`;
const Experiment = styled.li`
display: flex;
flex-direction: column;
`;
const Title = styled.span`
font-size: 16px;
font-weight: var(--font-weight-medium);
color: var(--text);
`;
const Subtitle = styled.div`
color: var(--text-disabled);
font-size: 14px;
font-weight: var(--font-weight-regular);
`;
const OverrideText = styled.div`
color: var(--text.muted);
margin-bottom: 10px;
font-size: 12px;
font-weight: var(--font-weight-bold);
`;
const Select = styled.select`
appearance: none;
/* safari */
-webkit-appearance: none;
background-color: var(--background-tertiary);
border-color: var(--background-tertiary);
color: var(--text);
font-weight: var(--font-weight-medium);
border: 1px solid transparent;
padding: 8px 8px 8px 12px;
cursor: pointer;
display: grid;
grid-template-columns: 1fr auto;
align-items: center;
border-radius: 4px;
`;
function ExperimentItem({ experiment }: { experiment: ExperimentType }) {
const app = useAppStore();
const isActive = app.experiments.isExperimentEnabled(experiment.id);
const activeTreatment = app.experiments.getTreatment(experiment.id);
const [isExpanded, setExpanded] = useState(isActive);
const toggle = () => setExpanded(!isExpanded);
const onChange = (e: React.ChangeEvent<HTMLSelectElement>) => {
const value = Number.parseInt(e.target.value);
app.experiments.setTreatment(experiment.id, value);
};
return (
<Experiment key={experiment.id}>
<div style={{ marginBottom: "10px", cursor: "pointer" }} onClick={toggle}>
<Title>{experiment.name}</Title>
<Subtitle>{experiment.description}</Subtitle>
</div>
{isExpanded && (
<div style={{ display: "flex", flexDirection: "column" }}>
<OverrideText>Treatment Override</OverrideText>
<Select onChange={onChange}>
{experiment.treatments.map((treatment) => (
<option
key={treatment.id}
value={treatment.id}
selected={(!isActive && treatment.id === 0) || activeTreatment?.id === treatment.id}
>
{`${treatment.name}${treatment.description ? ": " + treatment.description : ""}`}
</option>
))}
</Select>
</div>
)}
</Experiment>
);
}
function ExperimentsPage() {
const app = useAppStore();
return (
<div>
<SectionTitle>Experiments</SectionTitle>
<Content>
<ExperimentList>
{EXPERIMENT_LIST.map((experiment) => (
<ExperimentItem experiment={experiment} />
))}
</ExperimentList>
</Content>
</div>
);
}
export default observer(ExperimentsPage);

View File

@ -1,36 +0,0 @@
import { useContext } from "react";
import styled from "styled-components";
import { BannerContext } from "../../contexts/BannerContext";
import { ModalProps } from "../../controllers/modals/types";
import { Modal } from "./ModalComponents";
const Text = styled.span`
padding: 10px;
justify-content: center;
align-items: center;
display: flex;
flex: 1;
`;
export function VeryImportantModal({ ...props }: ModalProps<"very_important">) {
const bannerContext = useContext(BannerContext);
return (
<Modal
{...props}
actions={[
{
onClick: () => {
localStorage.setItem("very_important_banner_dismissed", "true");
bannerContext.close();
return true;
},
confirmation: true,
children: <span>I've been clowned</span>,
palette: "primary",
},
]}
>
<Text>April fools! 🤡</Text>
</Modal>
);
}

View File

@ -0,0 +1,37 @@
.item {
position: relative;
/* width: 100%; */
/* height: 100%; */
pointer-events: auto;
/* transform-origin: 50% 50% 0px; */
/* padding-left: 32px; */
/* padding-right: 32px; */
box-sizing: border-box;
display: flex;
flex: 1;
/* align-items: center; */
/* text-align: center; */
/* border-radius: 5px; */
/* box-shadow: 0px 10px 10px -5px rgba(0, 0, 0, 0.2); */
-webkit-user-select: none;
user-select: none;
}
.fg {
cursor: -webkit-grab;
position: absolute;
height: 100%;
width: 100%;
display: grid;
}
.fg > * {
pointer-events: none;
}
.container {
display: flex;
align-items: center;
height: 100%;
justify-content: center;
}

View File

@ -1,31 +0,0 @@
// context to handle banner open/close state
import { MotionStyle } from "framer-motion";
import React from "react";
export interface BannerContent {
element: React.ReactNode;
style?: MotionStyle;
forced?: boolean;
}
export type BannerContextType = {
content?: BannerContent;
setContent: React.Dispatch<React.SetStateAction<BannerContent | undefined>>;
close: () => void;
};
// @ts-expect-error not specifying a default value here
export const BannerContext = React.createContext<BannerContextType>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const BannerContextProvider: React.FC<any> = ({ children }) => {
const [content, setContent] = React.useState<BannerContent>();
const close = () => {
// clear content
setContent(undefined);
};
return <BannerContext.Provider value={{ content, setContent, close }}>{children}</BannerContext.Provider>;
};

View File

@ -0,0 +1,168 @@
import { AnimatePresence, motion } from "framer-motion";
import { action, computed, makeObservable, observable } from "mobx";
import styled from "styled-components";
import IconButton from "../../components/IconButton";
import OfflineBanner from "../../components/banners/OfflineBanner";
import { Banner } from "./types";
const Container = styled(motion.div)`
display: flex;
justify-content: center;
align-items: center;
`;
const CloseWrapper = styled(IconButton)`
position: absolute;
right: 1%;
`;
function randomUUID() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
// eslint-disable-next-line no-bitwise
const r = (Math.random() * 16) | 0;
// eslint-disable-next-line no-bitwise, no-mixed-operators
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Components = Record<string, React.FC<any>>;
/**
* Handles layering and displaying banners to the user.
*/
class BannerController<T extends Banner> {
stack: T[] = [];
components: Components;
constructor(components: Components) {
this.components = components;
makeObservable(this, {
stack: observable,
push: action,
pop: action,
remove: action,
rendered: computed,
isVisible: computed,
});
this.close = this.close.bind(this);
this.closeAll = this.closeAll.bind(this);
}
/**
* Display a new banner on the stack
* @param banner banner data
*/
push(banner: T, key?: string) {
if (key && this.stack.find((x) => x.key === key)) {
console.warn(`Banner with key '${key}' already exists on the stack!`);
return;
}
this.stack = [
...this.stack,
{
...banner,
key: key ?? randomUUID(),
},
];
}
/**
* Remove the top banner from the screen
*/
pop() {
this.stack = this.stack.map((entry, index) => (index === this.stack.length - 1 ? entry : entry));
}
/**
* Close the top banner
*/
close() {
this.pop();
}
/**
* Close all banners on the stack
*/
closeAll() {
this.stack = [];
}
/**
* Remove the keyed banner from the stack
*/
remove(key: string) {
this.stack = this.stack.filter((x) => x.key !== key);
}
/**
* Render banners
*/
get rendered() {
return (
<>
{this.stack.map((banner) => {
const Component = this.components[banner.type];
if (!Component) return null;
return (
<AnimatePresence>
<Container
variants={{
show: {
// slide down
y: 0,
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
hide: {
// slide up
y: "-100%",
transition: {
delayChildren: 0.3,
staggerChildren: 0.2,
},
},
}}
initial="hide"
animate="show"
exit="hide"
onAnimationComplete={() => {
console.debug("animation complete");
}}
//style={bannerContext.content.style}
>
<Component {...banner} onClose={() => this.remove(banner.key!)} />
{/* {!bannerContext.content.forced && (
<CloseWrapper
onClick={() => {
bannerContext.close();
}}
>
<Icon icon="mdiClose" color="var(--text)" size="24px" />
</CloseWrapper>
)} */}
</Container>
</AnimatePresence>
);
})}
</>
);
}
/**
* Whether a banner is currently visible
*/
get isVisible() {
return this.stack.length > 0;
}
}
export const bannerController = new BannerController({
offline: OfflineBanner,
});

View File

@ -0,0 +1,6 @@
import { observer } from "mobx-react-lite";
import { bannerController } from ".";
export default observer(() => {
return <>{bannerController.rendered}</>;
});

View File

@ -0,0 +1,3 @@
export * from "./BannerController";
export * from "./BannerRenderer";
export * from "./types";

View File

@ -0,0 +1,9 @@
export type Banner = {
key?: string;
} & {
type: "offline";
};
export type BannerProps<T extends Banner["type"]> = Banner & { type: T } & {
onClose: () => void;
};

View File

@ -15,7 +15,6 @@ import {
LeaveServerModal, LeaveServerModal,
SettingsModal, SettingsModal,
} from "../../components/modals"; } from "../../components/modals";
import { VeryImportantModal } from "../../components/modals/VeryImportantModal";
import { Modal } from "./types"; import { Modal } from "./types";
function randomUUID() { function randomUUID() {
@ -188,5 +187,4 @@ export const modalController = new ModalControllerExtended({
// modify_displayname: ModifyDisplayname, // modify_displayname: ModifyDisplayname,
// changelog_usernames: ChangelogUsernames, // changelog_usernames: ChangelogUsernames,
settings: SettingsModal, settings: SettingsModal,
very_important: VeryImportantModal,
}); });

View File

@ -10,7 +10,7 @@ export type Modal = {
key?: string; key?: string;
} & ( } & (
| { | {
type: "add_server" | "create_server" | "join_server" | "settings" | "very_important"; type: "add_server" | "create_server" | "join_server" | "settings";
} }
| { | {
type: "error"; type: "error";

View File

@ -1,32 +1,27 @@
html, html,
body, body,
#root, #root {
#root > div {
height: 100%; height: 100%;
} width: 100%;
overflow: hidden;
h1, display: flex;
h2,
h3,
h4,
h5,
h6,
p,
span,
textarea {
padding: 0;
margin: 0;
}
html *:not(code) {
font-family: var(--font-family);
} }
body { body {
margin: 0; margin: 0;
-webkit-font-smoothing: antialiased; -webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
overflow: hidden; /* overflow: hidden; */
}
*,
*:after,
*:before {
box-sizing: border-box;
}
html *:not(code) {
font-family: var(--font-family);
} }
code { code {

View File

@ -19,7 +19,6 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { ErrorBoundaryContext } from "react-use-error-boundary"; import { ErrorBoundaryContext } from "react-use-error-boundary";
import App from "./App"; import App from "./App";
import { BannerContextProvider } from "./contexts/BannerContext";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext"; import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
import Theme from "./contexts/Theme"; import Theme from "./contexts/Theme";
import ModalRenderer from "./controllers/modals/ModalRenderer"; import ModalRenderer from "./controllers/modals/ModalRenderer";
@ -32,12 +31,10 @@ dayjs.extend(calendar, calendarStrings);
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render( ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<ErrorBoundaryContext> <ErrorBoundaryContext>
<BrowserRouter> <BrowserRouter>
<BannerContextProvider> <ContextMenuContextProvider>
<ContextMenuContextProvider> <App />
<App /> <ModalRenderer />
<ModalRenderer /> </ContextMenuContextProvider>
</ContextMenuContextProvider>
</BannerContextProvider>
<Theme /> <Theme />
</BrowserRouter> </BrowserRouter>
</ErrorBoundaryContext>, </ErrorBoundaryContext>,

View File

@ -11,8 +11,8 @@ const Wrapper = styled.div`
justify-content: center; justify-content: center;
align-items: center; align-items: center;
display: flex; display: flex;
height: 100vh;
flex-direction: column; flex-direction: column;
flex: 1;
`; `;
const SpacebarLogo = styled(SpacebarLogoBlue)` const SpacebarLogo = styled(SpacebarLogoBlue)`
@ -29,7 +29,11 @@ function LoadingPage() {
const app = useAppStore(); const app = useAppStore();
return ( return (
<Container> <Container
style={{
flex: 1,
}}
>
<Wrapper> <Wrapper>
<SpacebarLogo /> <SpacebarLogo />
<PulseLoader color="var(--text)" /> <PulseLoader color="var(--text)" />

View File

@ -1,17 +1,20 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import React from "react"; import React from "react";
import { isMobile } from "react-device-detect";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import Banner from "../../components/Banner";
import ChannelSidebar from "../../components/ChannelSidebar"; import ChannelSidebar from "../../components/ChannelSidebar";
import ContainerComponent from "../../components/Container"; import ContainerComponent from "../../components/Container";
import ErrorBoundary from "../../components/ErrorBoundary"; import ErrorBoundary from "../../components/ErrorBoundary";
import GuildSidebar from "../../components/GuildSidebar"; import GuildSidebar from "../../components/GuildSidebar";
import SwipeableLayout from "../../components/SwipeableLayout";
import Chat from "../../components/messaging/Chat"; import Chat from "../../components/messaging/Chat";
import BannerRenderer from "../../controllers/banners/BannerRenderer";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../stores/AppStore";
const Container = styled(ContainerComponent)` const Container = styled(ContainerComponent)`
display: flex; display: flex;
flex: 1;
flex-direction: column; flex-direction: column;
`; `;
@ -22,6 +25,24 @@ const Wrapper = styled.div`
overflow: hidden; overflow: hidden;
`; `;
function LeftPanel() {
return (
<div
style={{
display: "flex",
flex: 1,
}}
>
<GuildSidebar />
<ChannelSidebar />
</div>
);
}
function RightPanel() {
return <div style={{ height: "100%", backgroundColor: "green", color: "white" }}>Right Panel</div>;
}
function ChannelPage() { function ChannelPage() {
const app = useAppStore(); const app = useAppStore();
@ -32,9 +53,22 @@ function ChannelPage() {
app.setActiveChannelId(channelId); app.setActiveChannelId(channelId);
}, [guildId, channelId]); }, [guildId, channelId]);
if (isMobile) {
return (
<Container>
<BannerRenderer />
<SwipeableLayout leftChildren={<LeftPanel />} rightChildren={<RightPanel />}>
<ErrorBoundary section="component">
<Chat />
</ErrorBoundary>
</SwipeableLayout>
</Container>
);
}
return ( return (
<Container> <Container>
<Banner /> <BannerRenderer />
<Wrapper> <Wrapper>
<GuildSidebar /> <GuildSidebar />
<ChannelSidebar /> <ChannelSidebar />

View File

@ -31,7 +31,6 @@ export default class AppStore {
// whether the app is still loading // whether the app is still loading
@observable isAppLoading = true; @observable isAppLoading = true;
@observable isNetworkConnected = true;
@observable tokenLoaded = false; @observable tokenLoaded = false;
@observable token: string | null = null; @observable token: string | null = null;
@observable fpsShown: boolean = process.env.NODE_ENV === "development"; @observable fpsShown: boolean = process.env.NODE_ENV === "development";
@ -69,9 +68,6 @@ export default class AppStore {
// bind this in windowToggleFps // bind this in windowToggleFps
this.windowToggleFps = this.windowToggleFps.bind(this); this.windowToggleFps = this.windowToggleFps.bind(this);
window.windowToggleFps = this.windowToggleFps; window.windowToggleFps = this.windowToggleFps;
window.addEventListener("online", () => this.setNetworkConnected(true));
window.addEventListener("offline", () => this.setNetworkConnected(false));
} }
@action @action
@ -89,17 +85,12 @@ export default class AppStore {
this.account = new AccountStore(user); this.account = new AccountStore(user);
} }
@action
setNetworkConnected(value: boolean) {
this.isNetworkConnected = value;
}
@computed @computed
/** /**
* Whether the app is done loading and ready to be displayed * Whether the app is done loading and ready to be displayed
*/ */
get isReady() { get isReady() {
return !this.isAppLoading && this.isGatewayReady && this.isNetworkConnected; return !this.isAppLoading && this.isGatewayReady;
} }
@action @action

View File

@ -1,6 +1,6 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
export type ExperimentType = "test" | "message_queue"; export type ExperimentType = "test" | "message_queue" | "presence_rings";
export interface ExperimentTreatment { export interface ExperimentTreatment {
id: number; id: number;
@ -56,6 +56,27 @@ export const EXPERIMENT_LIST: Experiment[] = [
}, },
], ],
}, },
{
id: "presence_rings",
name: "Presence Rings",
description: "Use rings for presence status instead of dots",
treatments: [
{
id: 0,
name: "Control",
},
{
id: 1,
name: "Treatment 1",
description: "Use presence dots",
},
{
id: 2,
name: "Treatment 2",
description: "Use presence rings",
},
],
},
]; ];
export interface Data { export interface Data {
@ -76,7 +97,7 @@ export default class ExperimentsStore {
} }
@computed @computed
getTreatment(id: ExperimentType) { getTreatment(id: ExperimentType): ExperimentTreatment | undefined {
const treatment = this.experiments.get(id); const treatment = this.experiments.get(id);
const experiment = EXPERIMENT_LIST.find((x) => x.id === id); const experiment = EXPERIMENT_LIST.find((x) => x.id === id);
return experiment?.treatments.find((x) => x.id === treatment); return experiment?.treatments.find((x) => x.id === treatment);