mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-22 02:12:38 +01:00
commit
23c4458f08
16
.github/workflows/tauri.yml
vendored
16
.github/workflows/tauri.yml
vendored
@ -31,12 +31,20 @@ jobs:
|
|||||||
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.1-dev libappindicator3-dev librsvg2-dev patchelf libsoup-3.0-dev libjavascriptcoregtk-4.1-dev
|
||||||
- name: Install Frontend Dependencies
|
- name: Install Frontend Dependencies
|
||||||
run: pnpm i
|
run: pnpm i
|
||||||
- uses: tauri-apps/tauri-action@v0
|
- name: Run version generator
|
||||||
|
run: pnpm run ci:prebuild
|
||||||
|
- uses: spacebarchat/tauri-action@dev
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY }}
|
||||||
|
TAURI_SIGNING_PRIVATE_KEY_PASSWORD: ${{ secrets.TAURI_SIGNING_PRIVATE_KEY_PASSWORD }}
|
||||||
with:
|
with:
|
||||||
tagName: spacebar-v__VERSION__
|
tagName: client-__BRANCH__-v__VERSION__
|
||||||
releaseName: "Spacebar Client v__VERSION__"
|
releaseName: "Spacebar Client v__VERSION__ (__BRANCH__)"
|
||||||
releaseBody: "See the assets to download this version and install."
|
releaseBody: "See the assets to download this version and install. The current commit is __SHA__."
|
||||||
releaseDraft: false
|
releaseDraft: false
|
||||||
prerelease: true
|
prerelease: true
|
||||||
|
includeDebug: true
|
||||||
|
includeRelease: true
|
||||||
|
includeUpdaterJson: true
|
||||||
|
buildIdAsVersion: true
|
||||||
|
2
.gitignore
vendored
2
.gitignore
vendored
@ -9,7 +9,7 @@
|
|||||||
/coverage
|
/coverage
|
||||||
|
|
||||||
# production
|
# production
|
||||||
/build
|
/dist
|
||||||
|
|
||||||
# misc
|
# misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
@ -3,3 +3,5 @@ dist
|
|||||||
node_modules
|
node_modules
|
||||||
.github
|
.github
|
||||||
.vscode
|
.vscode
|
||||||
|
src-tauri/target
|
||||||
|
src-tauri/gen
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
makeCacheWritable = true;
|
makeCacheWritable = true;
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
cp -r build $out/
|
cp -r dist $out/
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -26,7 +26,7 @@
|
|||||||
makeCacheWritable = true;
|
makeCacheWritable = true;
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
runHook preInstall
|
runHook preInstall
|
||||||
cp -r build $out/
|
cp -r dist $out/
|
||||||
runHook postInstall
|
runHook postInstall
|
||||||
'';
|
'';
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<!DOCTYPE html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<!-- Primary Meta Tags -->
|
<!-- Primary Meta Tags -->
|
||||||
|
131
package.json
131
package.json
@ -4,111 +4,120 @@
|
|||||||
"url": "https://github.com/spacebarchat/client/issues"
|
"url": "https://github.com/spacebarchat/client/issues"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.11.1",
|
"@emotion/react": "^11.11.4",
|
||||||
"@emotion/styled": "^11.11.0",
|
"@emotion/styled": "^11.11.0",
|
||||||
"@fontsource/roboto": "^4.5.8",
|
"@floating-ui/react": "^0.26.9",
|
||||||
"@fontsource/roboto-mono": "^5.0.8",
|
"@fontsource/roboto": "^5.0.8",
|
||||||
"@hcaptcha/react-hcaptcha": "^1.8.1",
|
"@fontsource/roboto-mono": "^5.0.16",
|
||||||
|
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||||
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@mattjennings/react-modal-stack": "^1.0.4",
|
"@mattjennings/react-modal-stack": "^1.0.4",
|
||||||
"@mdi/js": "^7.2.96",
|
"@mdi/js": "^7.4.47",
|
||||||
"@mdi/react": "^1.6.1",
|
"@mdi/react": "^1.6.1",
|
||||||
"@mui/material": "^5.14.9",
|
"@mui/material": "^5.15.11",
|
||||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||||
"@rollup/plugin-replace": "^5.0.2",
|
"@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-alpha.8",
|
"@tauri-apps/api": "2.0.0-beta.3",
|
||||||
"@tauri-apps/plugin-authenticator": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-authenticator": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-autostart": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-autostart": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-clipboard-manager": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-dialog": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-dialog": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-log": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-log": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-notification": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-notification": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-process": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-os": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-stronghold": "2.0.0-alpha.2",
|
"@tauri-apps/plugin-process": "2.0.0-beta.1",
|
||||||
"@tauri-apps/plugin-updater": "2.0.0-alpha.1",
|
"@tauri-apps/plugin-stronghold": "2.0.0-beta.1",
|
||||||
"@testing-library/jest-dom": "^5.17.0",
|
"@tauri-apps/plugin-updater": "2.0.0-beta.1",
|
||||||
"@testing-library/react": "^13.4.0",
|
"@testing-library/jest-dom": "^6.4.2",
|
||||||
"@testing-library/user-event": "^13.5.0",
|
"@testing-library/react": "^14.2.1",
|
||||||
|
"@testing-library/user-event": "^14.5.2",
|
||||||
"@types/react-measure": "^2.0.12",
|
"@types/react-measure": "^2.0.12",
|
||||||
"classnames": "^2.3.2",
|
"@types/react-portal": "^4.0.7",
|
||||||
"dayjs": "^1.11.9",
|
"classnames": "^2.5.1",
|
||||||
"framer-motion": "^10.16.4",
|
"csstype": "^3.1.3",
|
||||||
|
"dayjs": "^1.11.10",
|
||||||
|
"framer-motion": "^11.0.6",
|
||||||
"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.10.2",
|
"mobx": "^6.12.0",
|
||||||
"mobx-react-lite": "^3.4.3",
|
"mobx-react-lite": "^4.0.5",
|
||||||
"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.18.0",
|
"react-advanced-cropper": "^0.19.5",
|
||||||
"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.46.1",
|
"react-hook-form": "^7.50.1",
|
||||||
"react-infinite-scroll-component": "^6.1.0",
|
"react-infinite-scroll-component": "^6.1.0",
|
||||||
"react-loading-skeleton": "^3.3.1",
|
"react-loading-skeleton": "^3.4.0",
|
||||||
"react-markdown": "^8.0.7",
|
"react-markdown": "^9.0.1",
|
||||||
"react-measure": "^2.5.2",
|
"react-measure": "^2.5.2",
|
||||||
"react-router-dom": "^6.16.0",
|
"react-portal": "^4.2.2",
|
||||||
"react-secure-storage": "^1.3.0",
|
"react-router-dom": "^6.22.1",
|
||||||
"react-select-search": "^4.1.6",
|
"react-secure-storage": "^1.3.2",
|
||||||
|
"react-select-search": "^4.1.7",
|
||||||
"react-spinners": "^0.13.8",
|
"react-spinners": "^0.13.8",
|
||||||
"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-virtualized": "^9.22.5",
|
"react-virtualized": "^9.22.5",
|
||||||
"remark-gfm": "^3.0.1",
|
"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"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@craco/craco": "^7.1.0",
|
"@craco/craco": "^7.1.0",
|
||||||
"@tauri-apps/cli": "2.0.0-alpha.16",
|
"@tauri-apps/cli": "2.0.0-beta.5",
|
||||||
"@types/jest": "^27.5.2",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/loadable__component": "^5.13.5",
|
"@types/loadable__component": "^5.13.8",
|
||||||
"@types/murmurhash-js": "^1.0.6",
|
"@types/murmurhash-js": "^1.0.6",
|
||||||
"@types/node": "^16.18.50",
|
"@types/node": "^20.11.20",
|
||||||
"@types/react": "^18.2.22",
|
"@types/react": "^18.2.60",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/react-syntax-highlighter": "^15.5.7",
|
"@types/react-syntax-highlighter": "^15.5.11",
|
||||||
"@types/react-virtualized": "^9.21.22",
|
"@types/react-virtualized": "^9.21.29",
|
||||||
"@types/styled-components": "^5.1.27",
|
"@types/styled-components": "^5.1.34",
|
||||||
"@typescript-eslint/eslint-plugin": "^6.7.0",
|
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||||
"@typescript-eslint/parser": "^6.7.0",
|
"@typescript-eslint/parser": "^7.1.0",
|
||||||
"@vitejs/plugin-react": "^4.0.4",
|
"@vitejs/plugin-react": "^4.2.1",
|
||||||
"eslint": "^8.49.0",
|
"cross-env": "^7.0.3",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.3",
|
"eslint-plugin-react-refresh": "^0.4.5",
|
||||||
"internal-ip": "^7.0.0",
|
"internal-ip": "^8.0.0",
|
||||||
"typescript": "^5.2.2",
|
"typescript": "^5.3.3",
|
||||||
"vite": "^4.5.2",
|
"vite": "^5.1.4",
|
||||||
"vite-plugin-chunk-split": "^0.4.7",
|
"vite-plugin-chunk-split": "^0.5.0",
|
||||||
"vite-plugin-clean": "^1.0.0",
|
"vite-plugin-clean": "^1.0.0",
|
||||||
"vite-plugin-html": "^3.2.0",
|
"vite-plugin-html": "^3.2.2",
|
||||||
"vite-plugin-progress": "^0.0.7",
|
"vite-plugin-progress": "^0.0.7",
|
||||||
"vite-plugin-svgr": "^3.2.0"
|
"vite-plugin-svgr": "^4.2.0"
|
||||||
},
|
},
|
||||||
"homepage": "https://spacebar.chat",
|
"homepage": "https://spacebar.chat",
|
||||||
"license": "AGPL-3.0-only",
|
"license": "AGPL-3.0-only",
|
||||||
"name": "spacebar-client",
|
"name": "spacebar-client",
|
||||||
"packageManager": "pnpm@8.10.2",
|
"packageManager": "pnpm@8.14.0",
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
"url": "git+https://github.com/spacebarchat/client.git"
|
"url": "git+https://github.com/spacebarchat/client.git"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "tsc && vite build",
|
"build": "tsc && vite build",
|
||||||
|
"ci:prebuild": "node scripts/tauri-version.js",
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||||
"lint:fix": "pnpx prettier . --write",
|
"lint:fix": "pnpx prettier . --write",
|
||||||
"preview": "vite preview",
|
"preview": "vite preview",
|
||||||
"tauri:dev": "tauri dev",
|
"tauri:dev": "pnpm run ci:prebuild && tauri dev",
|
||||||
"tauri:build": "tauri build",
|
"tauri:build": "pnpm run ci:prebuild && tauri build",
|
||||||
"tauri:android:dev": "tauri android dev",
|
"tauri:android:dev": "pnpm run ci:prebuild && tauri android dev",
|
||||||
"tauri:android:build": "tauri android build"
|
"tauri:android:build": "pnpm run ci:prebuild && tauri android build"
|
||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"version": "0.1.1"
|
"version": "0.1.2"
|
||||||
}
|
}
|
||||||
|
6805
pnpm-lock.yaml
6805
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Before Width: | Height: | Size: 28 KiB |
@ -1,3 +1,15 @@
|
|||||||
|
/* roboto-latin-400-normal */
|
||||||
|
@font-face {
|
||||||
|
font-family: "Roboto";
|
||||||
|
font-style: normal;
|
||||||
|
font-display: swap;
|
||||||
|
font-weight: 400;
|
||||||
|
src: url(https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff2) format("woff2"),
|
||||||
|
url(https://cdn.jsdelivr.net/fontsource/fonts/roboto@latest/latin-400-normal.woff) format("woff");
|
||||||
|
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+0304, U+0308, U+0329,
|
||||||
|
U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -5,76 +17,19 @@
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: url(Spacebar.png);
|
background-color: #121212;
|
||||||
background-size: cover;
|
color: #fff;
|
||||||
|
font-family: "Roboto", sans-serif;
|
||||||
}
|
}
|
||||||
|
|
||||||
img {
|
svg {
|
||||||
max-width: 100%;
|
max-width: 80vw;
|
||||||
max-height: 100vh;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.loader-wrapper {
|
.container {
|
||||||
height: 100vh;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: flex-end;
|
align-items: center;
|
||||||
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.lds-ellipsis {
|
|
||||||
display: inline-block;
|
|
||||||
position: relative;
|
|
||||||
width: 80px;
|
|
||||||
height: 80px;
|
|
||||||
}
|
|
||||||
.lds-ellipsis div {
|
|
||||||
position: absolute;
|
|
||||||
top: 33px;
|
|
||||||
width: 13px;
|
|
||||||
height: 13px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
animation-timing-function: cubic-bezier(0, 1, 1, 0);
|
|
||||||
}
|
|
||||||
.lds-ellipsis div:nth-child(1) {
|
|
||||||
left: 8px;
|
|
||||||
animation: lds-ellipsis1 0.6s infinite;
|
|
||||||
}
|
|
||||||
.lds-ellipsis div:nth-child(2) {
|
|
||||||
left: 8px;
|
|
||||||
animation: lds-ellipsis2 0.6s infinite;
|
|
||||||
}
|
|
||||||
.lds-ellipsis div:nth-child(3) {
|
|
||||||
left: 32px;
|
|
||||||
animation: lds-ellipsis2 0.6s infinite;
|
|
||||||
}
|
|
||||||
.lds-ellipsis div:nth-child(4) {
|
|
||||||
left: 56px;
|
|
||||||
animation: lds-ellipsis3 0.6s infinite;
|
|
||||||
}
|
|
||||||
@keyframes lds-ellipsis1 {
|
|
||||||
0% {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes lds-ellipsis3 {
|
|
||||||
0% {
|
|
||||||
transform: scale(1);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: scale(0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@keyframes lds-ellipsis2 {
|
|
||||||
0% {
|
|
||||||
transform: translate(0, 0);
|
|
||||||
}
|
|
||||||
100% {
|
|
||||||
transform: translate(24px, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -7,12 +7,73 @@
|
|||||||
<title>Splashscreen</title>
|
<title>Splashscreen</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="loader-wrapper">
|
<div class="container">
|
||||||
<div class="lds-ellipsis">
|
<svg width="1442" viewBox="0 0 1442 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<div></div>
|
<g id="Iconmark">
|
||||||
<div></div>
|
<path
|
||||||
<div></div>
|
id="Vector"
|
||||||
<div></div>
|
fill-rule="evenodd"
|
||||||
|
clip-rule="evenodd"
|
||||||
|
d="M235.497 138C238.806 153.394 234.982 169.46 225.097 181.709C215.211 193.958 200.321 201.077 184.589 201.077H52.0732C36.3408 201.077 21.451 193.958 11.5658 181.709C1.67958 169.46 -2.14373 153.394 1.1649 138L24.4985 29.4245C25.5849 24.3682 29.0541 20.1534 33.8052 18.1191C38.5563 16.0838 43.9981 16.4817 48.4028 19.1861L118.331 62.1159L188.259 19.1861C192.664 16.4817 198.106 16.0838 202.857 18.1191C207.608 20.1534 211.077 24.3682 212.163 29.4245L235.497 138ZM181.883 112.957C181.883 105.091 175.511 98.7142 167.652 98.7142H140.235C132.375 98.7142 126.005 105.091 126.005 112.957V140.398C126.005 148.264 132.375 154.64 140.235 154.64H167.652C175.511 154.64 181.883 148.264 181.883 140.398V112.957ZM110.657 112.957C110.657 105.091 104.287 98.7142 96.4272 98.7142H69.0101C61.1507 98.7142 54.7792 105.091 54.7792 112.957V140.398C54.7792 148.264 61.1507 154.64 69.0101 154.64H96.4272C104.287 154.64 110.657 148.264 110.657 140.398V112.957Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="RightEye"
|
||||||
|
d="M125.8 112.7C125.8 104.968 132.068 98.7 139.8 98.7H167.8C175.532 98.7 181.8 104.968 181.8 112.7V140.7C181.8 148.432 175.532 154.7 167.8 154.7H139.8C132.068 154.7 125.8 148.432 125.8 140.7V112.7Z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="LeftEye"
|
||||||
|
d="M54.8 112.7C54.8 104.968 61.068 98.7 68.8 98.7H96.8C104.532 98.7 110.8 104.968 110.8 112.7V140.7C110.8 148.432 104.532 154.7 96.8 154.7H68.8C61.068 154.7 54.8 148.432 54.8 140.7V112.7Z"
|
||||||
|
fill="none"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
<g id="Wordmark">
|
||||||
|
<path
|
||||||
|
id="Vector_2"
|
||||||
|
d="M379.118 10.8465C335.763 10.8465 309.821 35.8002 309.821 68.8373C309.821 103.909 339.541 116.525 361.461 124.852C379.37 131.916 392.488 136.963 392.488 148.065C392.488 158.158 382.145 165.474 364.487 165.474C344.578 165.474 324.177 156.643 310.828 145.289V190.428C324.177 199.754 345.838 207.064 369.532 207.064C413.897 207.064 441.098 182.111 441.098 146.551C441.098 108.703 409.615 96.5916 383.659 87.0039C366.253 80.1914 356.919 76.1541 356.919 67.3229C356.919 58.2396 366.506 52.4358 382.397 52.4358C398.281 52.4358 417.675 57.9874 429.765 66.5664V23.7016C416.667 16.14 398.785 10.8465 379.118 10.8465Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_3"
|
||||||
|
d="M448.725 256H495.572V183.874C503.893 197.233 520.538 206.812 538.443 206.812C571.203 206.812 599.916 177.825 599.916 132.682C599.916 87.5075 569.943 58.269 537.183 58.269C516.752 58.269 501.623 69.1075 492.801 81.9626L488.519 62.0529H448.725V256ZM495.067 132.667C495.067 113.486 506.919 100.11 523.56 100.11C540.46 100.11 552.561 113.991 552.561 132.414C552.561 150.585 540.46 164.97 523.312 164.97C506.919 164.97 495.067 151.342 495.067 132.667Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_4"
|
||||||
|
d="M667.938 206.812C689.12 206.812 703.748 195.973 712.318 183.118H712.566L717.102 203.028H756.899V62.0529H717.102L712.566 81.9626H712.318C703.748 69.1075 689.12 58.269 667.938 58.269C636.438 58.269 605.708 88.2641 605.708 132.903C605.708 176.817 636.438 206.812 667.938 206.812ZM653.056 132.682C653.056 114.496 665.164 100.11 682.312 100.11C698.702 100.11 710.804 113.739 710.804 132.667C710.804 151.342 698.702 164.97 682.312 164.97C665.164 164.97 653.056 151.096 653.056 132.682Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_5"
|
||||||
|
d="M811.607 132.667C811.607 112.729 827.233 100.11 846.893 100.11C858.995 100.11 870.085 103.139 881.677 110.458V68.0988C869.328 61.2937 853.701 58.269 839.583 58.269C797.508 58.269 764.259 88.7677 764.259 132.414C764.259 176.061 797.254 206.812 839.583 206.812C854.204 206.812 869.576 203.283 881.677 196.982V154.623C870.085 162.951 858.238 164.97 847.148 164.97C827.233 164.97 811.607 152.604 811.607 132.667Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_6"
|
||||||
|
d="M967.641 206.812C985.532 206.812 1001.9 203.031 1016.26 195.219V158.144C1003.41 165.216 989.311 170.52 973.939 170.52C954.534 170.52 937.896 162.185 931.852 145.263H1024.32C1025.07 140.728 1025.58 135.689 1025.58 130.146C1025.58 83.22 995.603 58.269 958.816 58.269C918.242 58.269 886.99 88.5129 886.99 132.918C886.99 175.56 915.971 206.812 967.641 206.812ZM930.84 118.304C934.619 103.654 945.709 94.5615 958.568 94.5615C971.923 94.5615 980.748 104.159 981.25 118.304H930.84Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_7"
|
||||||
|
d="M1080.04 0H1033.19V203.028H1072.99L1077.27 183.118C1086.09 195.973 1101.22 206.812 1121.39 206.812C1154.15 206.812 1184.38 177.825 1184.38 132.667C1184.38 87.2554 1156.17 58.2691 1123.67 58.2691C1105.76 58.2691 1088.36 67.8474 1080.04 81.2061V0ZM1079.53 132.667C1079.53 113.739 1091.39 100.111 1107.78 100.111C1124.93 100.111 1137.03 114.496 1137.03 132.919C1137.03 151.09 1124.93 164.97 1108.03 164.97C1091.39 164.97 1079.53 151.595 1079.53 132.667Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_8"
|
||||||
|
d="M1252.4 206.812C1273.59 206.812 1288.21 195.973 1296.78 183.118H1297.03L1301.57 203.028H1341.36V62.0529H1301.57L1297.03 81.9626H1296.78C1288.21 69.1075 1273.59 58.269 1252.4 58.269C1220.9 58.269 1190.17 88.2641 1190.17 132.903C1190.17 176.817 1220.9 206.812 1252.4 206.812ZM1237.52 132.682C1237.52 114.496 1249.63 100.11 1266.78 100.11C1283.17 100.11 1295.27 113.739 1295.27 132.667C1295.27 151.342 1283.17 164.97 1266.78 164.97C1249.63 164.97 1237.52 151.096 1237.52 132.682Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
id="Vector_9"
|
||||||
|
d="M1351.05 203.028H1397.9V138.214C1397.9 112.471 1413.27 103.894 1429.65 103.894C1433.68 103.894 1437.97 104.399 1442 105.408V61.8008C1438.72 60.7907 1435.45 60.5392 1431.16 60.5392C1418.31 60.5392 1402.69 66.0862 1393.36 82.7312H1392.86L1388.33 62.0529H1351.05V203.028Z"
|
||||||
|
fill="#0185FF"
|
||||||
|
/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<span>Starting...</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</body>
|
</body>
|
||||||
|
23
scripts/tauri-version.js
Normal file
23
scripts/tauri-version.js
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import process from "process";
|
||||||
|
|
||||||
|
// if (!process.env.CI) {
|
||||||
|
// console.log("Not running in CI, skipping. Please do not run this script manually!");
|
||||||
|
// process.exit(0);
|
||||||
|
// }
|
||||||
|
|
||||||
|
const GITHUB_RUN_ID = process.env.GITHUB_RUN_ID || "0";
|
||||||
|
const GITHUB_RUN_ATTEMPT = process.env.GITHUB_RUN_ATTEMPT || "0";
|
||||||
|
const GITHUB_REF_NAME = process.env.GITHUB_REF_NAME;
|
||||||
|
|
||||||
|
const pkgJsonPath = path.resolve("./package.json");
|
||||||
|
const pkgJson = JSON.parse(fs.readFileSync(pkgJsonPath, "utf8"));
|
||||||
|
const pkgVersion = pkgJson.version;
|
||||||
|
|
||||||
|
// const tauriJsonPath = path.resolve("./src-tauri/tauri.conf.json");
|
||||||
|
const tauriJsonPath = path.resolve("./src-tauri/version.json");
|
||||||
|
const tauriJson = {
|
||||||
|
version: `${pkgVersion}+${GITHUB_RUN_ID}${GITHUB_RUN_ATTEMPT}`,
|
||||||
|
};
|
||||||
|
fs.writeFileSync(tauriJsonPath, JSON.stringify(tauriJson, null, 4));
|
1
src-tauri/.gitignore
vendored
1
src-tauri/.gitignore
vendored
@ -1,4 +1,3 @@
|
|||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
|
8
src-tauri/.idea/.gitignore
vendored
Normal file
8
src-tauri/.idea/.gitignore
vendored
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
||||||
|
# Editor-based HTTP Client requests
|
||||||
|
/httpRequests/
|
||||||
|
# Datasource local storage ignored files
|
||||||
|
/dataSources/
|
||||||
|
/dataSources.local.xml
|
12
src-tauri/.idea/discord.xml
Normal file
12
src-tauri/.idea/discord.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="PROJECT_FILES" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
<option name="theme" value="material" />
|
||||||
|
<option name="button1Title" value="" />
|
||||||
|
<option name="button1Url" value="" />
|
||||||
|
<option name="button2Title" value="" />
|
||||||
|
<option name="button2Url" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
8
src-tauri/.idea/modules.xml
Normal file
8
src-tauri/.idea/modules.xml
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectModuleManager">
|
||||||
|
<modules>
|
||||||
|
<module fileurl="file://$PROJECT_DIR$/.idea/src-tauri.iml" filepath="$PROJECT_DIR$/.idea/src-tauri.iml" />
|
||||||
|
</modules>
|
||||||
|
</component>
|
||||||
|
</project>
|
11
src-tauri/.idea/src-tauri.iml
Normal file
11
src-tauri/.idea/src-tauri.iml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module type="EMPTY_MODULE" version="4">
|
||||||
|
<component name="NewModuleRootManager">
|
||||||
|
<content url="file://$MODULE_DIR$">
|
||||||
|
<sourceFolder url="file://$MODULE_DIR$/src" isTestSource="false" />
|
||||||
|
<excludeFolder url="file://$MODULE_DIR$/target" />
|
||||||
|
</content>
|
||||||
|
<orderEntry type="inheritedJdk" />
|
||||||
|
<orderEntry type="sourceFolder" forTests="false" />
|
||||||
|
</component>
|
||||||
|
</module>
|
6
src-tauri/.idea/vcs.xml
Normal file
6
src-tauri/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
148
src-tauri/.idea/workspace.xml
Normal file
148
src-tauri/.idea/workspace.xml
Normal file
@ -0,0 +1,148 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="AutoImportSettings">
|
||||||
|
<option name="autoReloadType" value="ALL" />
|
||||||
|
</component>
|
||||||
|
<component name="CargoProjects">
|
||||||
|
<cargoProject FILE="$PROJECT_DIR$/Cargo.toml" />
|
||||||
|
</component>
|
||||||
|
<component name="ChangeListManager">
|
||||||
|
<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$/../.eslintignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.eslintignore" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.eslintrc" beforeDir="false" afterPath="$PROJECT_DIR$/../.eslintrc" afterDir="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$/../.github/ISSUE_TEMPLATE/config.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/ISSUE_TEMPLATE/config.yml" afterDir="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$/../.github/workflows/pages-deploy.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/workflows/pages-deploy.yml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.github/workflows/tauri.yml" beforeDir="false" afterPath="$PROJECT_DIR$/../.github/workflows/tauri.yml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.gitignore" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.prettierignore" beforeDir="false" afterPath="$PROJECT_DIR$/../.prettierignore" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.vscode/extensions.json" beforeDir="false" afterPath="$PROJECT_DIR$/../.vscode/extensions.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../.vscode/settings.json" beforeDir="false" afterPath="$PROJECT_DIR$/../.vscode/settings.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../LICENSE" beforeDir="false" afterPath="$PROJECT_DIR$/../LICENSE" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../flake.nix" beforeDir="false" afterPath="$PROJECT_DIR$/../flake.nix" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../flake.template.nix" beforeDir="false" afterPath="$PROJECT_DIR$/../flake.template.nix" 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$/../nix-rebuild-flake.sh" beforeDir="false" afterPath="$PROJECT_DIR$/../nix-rebuild-flake.sh" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../public/manifest.json" beforeDir="false" afterPath="$PROJECT_DIR$/../public/manifest.json" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/../public/robots.txt" beforeDir="false" afterPath="$PROJECT_DIR$/../public/robots.txt" 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/types.ts" beforeDir="false" afterPath="$PROJECT_DIR$/../src/controllers/modals/types.ts" afterDir="false" />
|
||||||
|
</list>
|
||||||
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
|
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||||
|
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||||
|
</component>
|
||||||
|
<component name="Git.Settings">
|
||||||
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/.." />
|
||||||
|
</component>
|
||||||
|
<component name="MacroExpansionManager">
|
||||||
|
<option name="directoryName" value="dm7tjlct" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectColorInfo">{
|
||||||
|
"associatedIndex": 4
|
||||||
|
}</component>
|
||||||
|
<component name="ProjectId" id="2d1XCyoDrz9y2JsvssmYsjETVjR" />
|
||||||
|
<component name="ProjectViewState">
|
||||||
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
|
<option name="showLibraryContents" value="true" />
|
||||||
|
</component>
|
||||||
|
<component name="PropertiesComponent"><![CDATA[{
|
||||||
|
"keyToString": {
|
||||||
|
"RunOnceActivity.OpenProjectViewOnStart": "true",
|
||||||
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
|
"RunOnceActivity.rust.reset.selective.auto.import": "true",
|
||||||
|
"git-widget-placeholder": "dev",
|
||||||
|
"ignore.virus.scanning.warn.message": "true",
|
||||||
|
"last_opened_file_path": "C:/Users/23562/Documents/Code/workspaces/spacebar/client-react/src-tauri/Cargo.toml",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"org.rust.cargo.project.model.PROJECT_DISCOVERY": "true",
|
||||||
|
"org.rust.cargo.project.model.impl.CargoExternalSystemProjectAware.subscribe.first.balloon": "",
|
||||||
|
"settings.editor.selected.configurable": "discord-project",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
|
}
|
||||||
|
}]]></component>
|
||||||
|
<component name="RustProjectSettings">
|
||||||
|
<option name="toolchainHomeDirectory" value="D:/cache/.cargo/bin" />
|
||||||
|
</component>
|
||||||
|
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||||
|
<component name="TaskManager">
|
||||||
|
<task active="true" id="Default" summary="Default task">
|
||||||
|
<changelist id="0756a13d-f814-41e0-81ae-eb872c2c747f" name="Changes" comment="" />
|
||||||
|
<created>1709176435458</created>
|
||||||
|
<option name="number" value="Default" />
|
||||||
|
<option name="presentableId" value="Default" />
|
||||||
|
<updated>1709176435458</updated>
|
||||||
|
<workItem from="1709176436794" duration="84000" />
|
||||||
|
<workItem from="1709176527868" duration="30000" />
|
||||||
|
<workItem from="1709176564971" duration="695000" />
|
||||||
|
</task>
|
||||||
|
<servers />
|
||||||
|
</component>
|
||||||
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
|
<option name="version" value="3" />
|
||||||
|
</component>
|
||||||
|
</project>
|
3139
src-tauri/Cargo.lock
generated
3139
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "app"
|
name = "app"
|
||||||
version = "0.1.0"
|
version = "0.0.0"
|
||||||
description = "Spacebar Client"
|
description = "Spacebar Client"
|
||||||
authors = ["Puyodead1"]
|
authors = ["Puyodead1"]
|
||||||
license = "AGPL-3.0-only"
|
license = "AGPL-3.0-only"
|
||||||
@ -17,12 +17,21 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
|||||||
tauri-build = { version = "2.0.0-alpha", features = [] }
|
tauri-build = { version = "2.0.0-alpha", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
# tauri = { version = "2.0.0-alpha", features = [] }
|
tauri = { version = "2.0.0-alpha", features = ["devtools", "tray-icon"] }
|
||||||
tauri = { git = "https://github.com/tauri-apps/tauri.git", branch = "dev", features = [
|
tauri-plugin-updater = "2.0.0-alpha"
|
||||||
"devtools",
|
tauri-plugin-process = "2.0.0-alpha"
|
||||||
] }
|
tauri-plugin-log = "2.0.0-alpha"
|
||||||
|
tauri-plugin-os = "2.0.0-alpha"
|
||||||
|
reqwest = { version = "0.11.22", features = ["json"] }
|
||||||
|
url = "2.4.1"
|
||||||
|
chrono = "0.4"
|
||||||
|
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.1"
|
||||||
|
tauri-plugin-single-instance = "2.0.0-beta.2"
|
||||||
|
tauri-plugin-autostart = "2.0.0-beta.1"
|
||||||
|
#openssl = { version = "0.10", features = ["vendored"] }
|
||||||
|
|
||||||
[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
|
||||||
|
24
src-tauri/capabilities/base.json
Normal file
24
src-tauri/capabilities/base.json
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"identifier": "base",
|
||||||
|
"description": "base",
|
||||||
|
"windows": ["main", "splashscreen"],
|
||||||
|
"permissions": [
|
||||||
|
"path:default",
|
||||||
|
"event:default",
|
||||||
|
"window:default",
|
||||||
|
"app:default",
|
||||||
|
"resources:default",
|
||||||
|
"menu:default",
|
||||||
|
"tray:default",
|
||||||
|
"updater:default",
|
||||||
|
"notification:default",
|
||||||
|
"os:allow-platform",
|
||||||
|
"os:allow-arch",
|
||||||
|
"os:allow-family",
|
||||||
|
"os:allow-locale",
|
||||||
|
"os:allow-os-type",
|
||||||
|
"os:allow-version",
|
||||||
|
"webview:allow-internal-toggle-devtools"
|
||||||
|
],
|
||||||
|
"platforms": ["linux", "macOS", "windows", "android", "iOS"]
|
||||||
|
}
|
3
src-tauri/gen/android/.idea/.gitignore
vendored
Normal file
3
src-tauri/gen/android/.idea/.gitignore
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# Default ignored files
|
||||||
|
/shelf/
|
||||||
|
/workspace.xml
|
6
src-tauri/gen/android/.idea/compiler.xml
Normal file
6
src-tauri/gen/android/.idea/compiler.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="CompilerConfiguration">
|
||||||
|
<bytecodeTargetLevel target="17" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
src-tauri/gen/android/.idea/deploymentTargetDropDown.xml
Normal file
10
src-tauri/gen/android/.idea/deploymentTargetDropDown.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="deploymentTargetDropDown">
|
||||||
|
<value>
|
||||||
|
<entry key="app">
|
||||||
|
<State />
|
||||||
|
</entry>
|
||||||
|
</value>
|
||||||
|
</component>
|
||||||
|
</project>
|
7
src-tauri/gen/android/.idea/discord.xml
Normal file
7
src-tauri/gen/android/.idea/discord.xml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="DiscordProjectSettings">
|
||||||
|
<option name="show" value="PROJECT_FILES" />
|
||||||
|
<option name="description" value="" />
|
||||||
|
</component>
|
||||||
|
</project>
|
32
src-tauri/gen/android/.idea/gradle.xml
Normal file
32
src-tauri/gen/android/.idea/gradle.xml
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="GradleMigrationSettings" migrationVersion="1" />
|
||||||
|
<component name="GradleSettings">
|
||||||
|
<option name="linkedExternalProjectsSettings">
|
||||||
|
<GradleProjectSettings>
|
||||||
|
<compositeConfiguration>
|
||||||
|
<compositeBuild compositeDefinitionSource="SCRIPT">
|
||||||
|
<builds>
|
||||||
|
<build path="$PROJECT_DIR$/buildSrc" name="buildSrc">
|
||||||
|
<projects>
|
||||||
|
<project path="$PROJECT_DIR$/buildSrc" />
|
||||||
|
</projects>
|
||||||
|
</build>
|
||||||
|
</builds>
|
||||||
|
</compositeBuild>
|
||||||
|
</compositeConfiguration>
|
||||||
|
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||||
|
<option name="gradleJvm" value="#GRADLE_LOCAL_JAVA_HOME" />
|
||||||
|
<option name="modules">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
<option value="$PROJECT_DIR$/app" />
|
||||||
|
<option value="$PROJECT_DIR$/buildSrc" />
|
||||||
|
<option value="D:/cache/.cargo/registry/src/index.crates.io-6f17d22bba15001f/tauri-2.0.0-alpha.20/mobile/android" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
<option name="resolveExternalAnnotations" value="false" />
|
||||||
|
</GradleProjectSettings>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
6
src-tauri/gen/android/.idea/kotlinc.xml
Normal file
6
src-tauri/gen/android/.idea/kotlinc.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="KotlinJpsPluginSettings">
|
||||||
|
<option name="version" value="1.8.10" />
|
||||||
|
</component>
|
||||||
|
</project>
|
10
src-tauri/gen/android/.idea/migrations.xml
Normal file
10
src-tauri/gen/android/.idea/migrations.xml
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="ProjectMigrations">
|
||||||
|
<option name="MigrateToGradleLocalJavaHome">
|
||||||
|
<set>
|
||||||
|
<option value="$PROJECT_DIR$" />
|
||||||
|
</set>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
|
</project>
|
9
src-tauri/gen/android/.idea/misc.xml
Normal file
9
src-tauri/gen/android/.idea/misc.xml
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<project version="4">
|
||||||
|
<component name="ExternalStorageConfigurationManager" enabled="true" />
|
||||||
|
<component name="ProjectRootManager" version="2" languageLevel="JDK_17" default="true" project-jdk-name="jbr-17" project-jdk-type="JavaSDK">
|
||||||
|
<output url="file://$PROJECT_DIR$/build/classes" />
|
||||||
|
</component>
|
||||||
|
<component name="ProjectType">
|
||||||
|
<option name="id" value="Android" />
|
||||||
|
</component>
|
||||||
|
</project>
|
6
src-tauri/gen/android/.idea/vcs.xml
Normal file
6
src-tauri/gen/android/.idea/vcs.xml
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<project version="4">
|
||||||
|
<component name="VcsDirectoryMappings">
|
||||||
|
<mapping directory="$PROJECT_DIR$/../../.." vcs="Git" />
|
||||||
|
</component>
|
||||||
|
</project>
|
@ -32,7 +32,7 @@ open class BuildTask : DefaultTask() {
|
|||||||
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
val rootDirRel = rootDirRel ?: throw GradleException("rootDirRel cannot be null")
|
||||||
val target = target ?: throw GradleException("target cannot be null")
|
val target = target ?: throw GradleException("target cannot be null")
|
||||||
val release = release ?: throw GradleException("release cannot be null")
|
val release = release ?: throw GradleException("release cannot be null")
|
||||||
val args = listOf("C:\\Users\\23562\\Documents\\Code\\workspaces\\spacebar\\client-react\\node_modules\\.bin\\\\..\\@tauri-apps\\cli\\tauri.js", "android", "android-studio-script");
|
val args = listOf("..\\node_modules\\.bin\\\\..\\@tauri-apps\\cli\\tauri.js", "android", "android-studio-script");
|
||||||
|
|
||||||
project.exec {
|
project.exec {
|
||||||
workingDir(File(project.projectDir, rootDirRel))
|
workingDir(File(project.projectDir, rootDirRel))
|
||||||
|
@ -1,116 +1,116 @@
|
|||||||
{
|
{
|
||||||
"images" : [
|
"images": [
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-20x20@2x.png",
|
"filename": "AppIcon-20x20@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-20x20@3x.png",
|
"filename": "AppIcon-20x20@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-29x29@2x-1.png",
|
"filename": "AppIcon-29x29@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-29x29@3x.png",
|
"filename": "AppIcon-29x29@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-40x40@2x.png",
|
"filename": "AppIcon-40x40@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-40x40@3x.png",
|
"filename": "AppIcon-40x40@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"size": "60x60",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-60x60@2x.png",
|
"filename": "AppIcon-60x60@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "60x60",
|
"size": "60x60",
|
||||||
"idiom" : "iphone",
|
"idiom": "iphone",
|
||||||
"filename" : "AppIcon-60x60@3x.png",
|
"filename": "AppIcon-60x60@3x.png",
|
||||||
"scale" : "3x"
|
"scale": "3x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-20x20@1x.png",
|
"filename": "AppIcon-20x20@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "20x20",
|
"size": "20x20",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-20x20@2x-1.png",
|
"filename": "AppIcon-20x20@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-29x29@1x.png",
|
"filename": "AppIcon-29x29@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "29x29",
|
"size": "29x29",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-29x29@2x.png",
|
"filename": "AppIcon-29x29@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-40x40@1x.png",
|
"filename": "AppIcon-40x40@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "40x40",
|
"size": "40x40",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-40x40@2x-1.png",
|
"filename": "AppIcon-40x40@2x-1.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
"size": "76x76",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-76x76@1x.png",
|
"filename": "AppIcon-76x76@1x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "76x76",
|
"size": "76x76",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-76x76@2x.png",
|
"filename": "AppIcon-76x76@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "83.5x83.5",
|
"size": "83.5x83.5",
|
||||||
"idiom" : "ipad",
|
"idiom": "ipad",
|
||||||
"filename" : "AppIcon-83.5x83.5@2x.png",
|
"filename": "AppIcon-83.5x83.5@2x.png",
|
||||||
"scale" : "2x"
|
"scale": "2x"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"size" : "1024x1024",
|
"size": "1024x1024",
|
||||||
"idiom" : "ios-marketing",
|
"idiom": "ios-marketing",
|
||||||
"filename" : "AppIcon-512@2x.png",
|
"filename": "AppIcon-512@2x.png",
|
||||||
"scale" : "1x"
|
"scale": "1x"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"version": 1,
|
||||||
"author" : "xcode"
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"info" : {
|
"info": {
|
||||||
"version" : 1,
|
"version": 1,
|
||||||
"author" : "xcode"
|
"author": "xcode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,88 +1,88 @@
|
|||||||
name: app
|
name: app
|
||||||
options:
|
options:
|
||||||
bundleIdPrefix: chat.spacebar
|
bundleIdPrefix: chat.spacebar
|
||||||
deploymentTarget:
|
deploymentTarget:
|
||||||
iOS: 13.0
|
iOS: 13.0
|
||||||
fileGroups: [../../src]
|
fileGroups: [../../src]
|
||||||
configs:
|
configs:
|
||||||
debug: debug
|
debug: debug
|
||||||
release: release
|
release: release
|
||||||
settingGroups:
|
settingGroups:
|
||||||
app:
|
app:
|
||||||
base:
|
base:
|
||||||
PRODUCT_NAME: Spacebar
|
PRODUCT_NAME: Spacebar
|
||||||
PRODUCT_BUNDLE_IDENTIFIER: chat.spacebar.app
|
PRODUCT_BUNDLE_IDENTIFIER: chat.spacebar.app
|
||||||
DEVELOPMENT_TEAM: 47RXBB8X9K
|
DEVELOPMENT_TEAM: 47RXBB8X9K
|
||||||
targetTemplates:
|
targetTemplates:
|
||||||
app:
|
app:
|
||||||
type: application
|
type: application
|
||||||
sources:
|
sources:
|
||||||
- path: Sources
|
- path: Sources
|
||||||
scheme:
|
scheme:
|
||||||
environmentVariables:
|
environmentVariables:
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
RUST_LOG: info
|
RUST_LOG: info
|
||||||
settings:
|
settings:
|
||||||
groups: [app]
|
groups: [app]
|
||||||
targets:
|
targets:
|
||||||
app_iOS:
|
app_iOS:
|
||||||
type: application
|
type: application
|
||||||
platform: iOS
|
platform: iOS
|
||||||
sources:
|
sources:
|
||||||
- path: Sources
|
- path: Sources
|
||||||
- path: Assets.xcassets
|
- path: Assets.xcassets
|
||||||
- path: Externals
|
- path: Externals
|
||||||
- path: app_iOS
|
- path: app_iOS
|
||||||
- path: assets
|
- path: assets
|
||||||
buildPhase: resources
|
buildPhase: resources
|
||||||
type: folder
|
type: folder
|
||||||
info:
|
info:
|
||||||
path: app_iOS/Info.plist
|
path: app_iOS/Info.plist
|
||||||
properties:
|
properties:
|
||||||
LSRequiresIPhoneOS: true
|
LSRequiresIPhoneOS: true
|
||||||
UILaunchStoryboardName: LaunchScreen
|
UILaunchStoryboardName: LaunchScreen
|
||||||
UIRequiredDeviceCapabilities: [arm64, metal]
|
UIRequiredDeviceCapabilities: [arm64, metal]
|
||||||
UISupportedInterfaceOrientations:
|
UISupportedInterfaceOrientations:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
- UIInterfaceOrientationLandscapeLeft
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
- UIInterfaceOrientationLandscapeRight
|
- UIInterfaceOrientationLandscapeRight
|
||||||
UISupportedInterfaceOrientations~ipad:
|
UISupportedInterfaceOrientations~ipad:
|
||||||
- UIInterfaceOrientationPortrait
|
- UIInterfaceOrientationPortrait
|
||||||
- UIInterfaceOrientationPortraitUpsideDown
|
- UIInterfaceOrientationPortraitUpsideDown
|
||||||
- UIInterfaceOrientationLandscapeLeft
|
- UIInterfaceOrientationLandscapeLeft
|
||||||
- UIInterfaceOrientationLandscapeRight
|
- UIInterfaceOrientationLandscapeRight
|
||||||
CFBundleShortVersionString: 0.1.1
|
CFBundleShortVersionString: 0.1.1
|
||||||
CFBundleVersion: 0.1.1
|
CFBundleVersion: 0.1.1
|
||||||
entitlements:
|
entitlements:
|
||||||
path: app_iOS/app_iOS.entitlements
|
path: app_iOS/app_iOS.entitlements
|
||||||
scheme:
|
scheme:
|
||||||
environmentVariables:
|
environmentVariables:
|
||||||
RUST_BACKTRACE: full
|
RUST_BACKTRACE: full
|
||||||
RUST_LOG: info
|
RUST_LOG: info
|
||||||
settings:
|
settings:
|
||||||
base:
|
base:
|
||||||
ENABLE_BITCODE: false
|
ENABLE_BITCODE: false
|
||||||
ARCHS: [arm64, x86_64]
|
ARCHS: [arm64, x86_64]
|
||||||
VALID_ARCHS: arm64 x86_64
|
VALID_ARCHS: arm64 x86_64
|
||||||
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
LIBRARY_SEARCH_PATHS[arch=x86_64]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||||
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
LIBRARY_SEARCH_PATHS[arch=arm64]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||||
LIBRARY_SEARCH_PATHS[arch=arm64-sim]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
LIBRARY_SEARCH_PATHS[arch=arm64-sim]: $(inherited) $(PROJECT_DIR)/Externals/$(CONFIGURATION) $(SDKROOT)/usr/lib/swift $(TOOLCHAIN_DIR)/usr/lib/swift/$(PLATFORM_NAME) $(TOOLCHAIN_DIR)/usr/lib/swift-5.0/$(PLATFORM_NAME)
|
||||||
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES: true
|
||||||
groups: [app]
|
groups: [app]
|
||||||
dependencies:
|
dependencies:
|
||||||
- framework: libspacebar.a
|
- framework: libspacebar.a
|
||||||
embed: false
|
embed: false
|
||||||
- sdk: CoreGraphics.framework
|
- sdk: CoreGraphics.framework
|
||||||
- sdk: Metal.framework
|
- sdk: Metal.framework
|
||||||
- sdk: MetalKit.framework
|
- sdk: MetalKit.framework
|
||||||
- sdk: QuartzCore.framework
|
- sdk: QuartzCore.framework
|
||||||
- sdk: Security.framework
|
- sdk: Security.framework
|
||||||
- sdk: UIKit.framework
|
- sdk: UIKit.framework
|
||||||
- sdk: WebKit.framework
|
- sdk: WebKit.framework
|
||||||
preBuildScripts:
|
preBuildScripts:
|
||||||
- script: node /Users/rileyzicafoose/Documents/client/./node_modules/.bin/../@tauri-apps/cli/tauri.js ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
- script: node /Users/rileyzicafoose/Documents/client/./node_modules/.bin/../@tauri-apps/cli/tauri.js ios xcode-script -v --platform ${PLATFORM_DISPLAY_NAME:?} --sdk-root ${SDKROOT:?} --framework-search-paths "${FRAMEWORK_SEARCH_PATHS:?}" --header-search-paths "${HEADER_SEARCH_PATHS:?}" --gcc-preprocessor-definitions "${GCC_PREPROCESSOR_DEFINITIONS:-}" --configuration ${CONFIGURATION:?} ${FORCE_COLOR} ${ARCHS:?}
|
||||||
name: Build Rust Code
|
name: Build Rust Code
|
||||||
basedOnDependencyAnalysis: false
|
basedOnDependencyAnalysis: false
|
||||||
outputFiles:
|
outputFiles:
|
||||||
- $(SRCROOT)/target/aarch64-apple-ios/${CONFIGURATION}/deps/libspacebar.a
|
- $(SRCROOT)/target/aarch64-apple-ios/${CONFIGURATION}/deps/libspacebar.a
|
||||||
- $(SRCROOT)/target/x86_64-apple-ios/${CONFIGURATION}/deps/libspacebar.a
|
- $(SRCROOT)/target/x86_64-apple-ios/${CONFIGURATION}/deps/libspacebar.a
|
||||||
|
1
src-tauri/gen/schemas/capabilities.json
Normal file
1
src-tauri/gen/schemas/capabilities.json
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"base":{"identifier":"base","description":"base","local":true,"windows":["main","splashscreen"],"permissions":["path:default","event:default","window:default","app:default","resources:default","menu:default","tray:default","updater:default","notification:default","os:allow-platform","os:allow-arch","os:allow-family","os:allow-locale","os:allow-os-type","os:allow-version","webview:allow-internal-toggle-devtools"],"platforms":["linux","macOS","windows","android","iOS"]}}
|
2254
src-tauri/gen/schemas/desktop-schema.json
Normal file
2254
src-tauri/gen/schemas/desktop-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
1
src-tauri/gen/schemas/plugin-manifests.json
Normal file
1
src-tauri/gen/schemas/plugin-manifests.json
Normal file
File diff suppressed because one or more lines are too long
2254
src-tauri/gen/schemas/windows-schema.json
Normal file
2254
src-tauri/gen/schemas/windows-schema.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,28 @@
|
|||||||
|
use std::{sync::Arc, sync::Mutex};
|
||||||
|
use tauri::{Manager, RunEvent, State, WebviewWindow};
|
||||||
|
use tauri_plugin_autostart::MacosLauncher;
|
||||||
|
use tauri_plugin_log::{Target, TargetKind, WEBVIEW_TARGET};
|
||||||
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
||||||
#[cfg(desktop)]
|
#[cfg(desktop)]
|
||||||
use tauri::Manager;
|
mod tray;
|
||||||
|
mod updater;
|
||||||
|
|
||||||
|
// wrappers around each Window
|
||||||
|
// we use a dedicated type because Tauri can only manage a single instance of a given type
|
||||||
|
struct SplashscreenWindow(Arc<Mutex<WebviewWindow>>);
|
||||||
|
struct MainWindow(Arc<Mutex<WebviewWindow>>);
|
||||||
|
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn close_splashscreen(window: tauri::Window) {
|
fn close_splashscreen(
|
||||||
#[cfg(desktop)]
|
_: WebviewWindow,
|
||||||
{
|
splashscreen: State<SplashscreenWindow>,
|
||||||
// Close splashscreen
|
main: State<MainWindow>,
|
||||||
if let Some(splashscreen) = window.get_window("splashscreen") {
|
) {
|
||||||
splashscreen.close().unwrap();
|
// Close splashscreen
|
||||||
}
|
splashscreen.0.lock().unwrap().close().unwrap();
|
||||||
|
// Show main window
|
||||||
// Show main window
|
main.0.lock().unwrap().show().unwrap();
|
||||||
window.get_window("main").unwrap().show().unwrap();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||||
@ -20,8 +30,124 @@ pub fn run() {
|
|||||||
std::env::set_var("RUST_BACKTRACE", "1");
|
std::env::set_var("RUST_BACKTRACE", "1");
|
||||||
std::env::set_var("RUST_LOG", "debug");
|
std::env::set_var("RUST_LOG", "debug");
|
||||||
|
|
||||||
tauri::Builder::default()
|
let mut context = tauri::generate_context!();
|
||||||
.invoke_handler(tauri::generate_handler![close_splashscreen,])
|
|
||||||
.run(tauri::generate_context!())
|
let config = context.config_mut();
|
||||||
|
|
||||||
|
let app = tauri::Builder::default()
|
||||||
|
.plugin(tauri_plugin_os::init())
|
||||||
|
.plugin(
|
||||||
|
tauri_plugin_log::Builder::default()
|
||||||
|
.clear_targets()
|
||||||
|
.targets([
|
||||||
|
Target::new(TargetKind::Webview),
|
||||||
|
Target::new(TargetKind::LogDir {
|
||||||
|
file_name: Some("webview".into()),
|
||||||
|
})
|
||||||
|
.filter(|metadata| metadata.target() == WEBVIEW_TARGET),
|
||||||
|
Target::new(TargetKind::LogDir {
|
||||||
|
file_name: Some("rust".into()),
|
||||||
|
})
|
||||||
|
.filter(|metadata| metadata.target() != WEBVIEW_TARGET),
|
||||||
|
])
|
||||||
|
.format(move |out, message, record| {
|
||||||
|
out.finish(format_args!(
|
||||||
|
"{} [{}] {}",
|
||||||
|
chrono::Local::now().format("%Y-%m-%d %H:%M:%S"),
|
||||||
|
record.level(),
|
||||||
|
message
|
||||||
|
));
|
||||||
|
})
|
||||||
|
.level(log::LevelFilter::Info)
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.plugin(tauri_plugin_notification::init())
|
||||||
|
.plugin(tauri_plugin_single_instance::init(|app, argv, cwd| {
|
||||||
|
println!("{}, {argv:?}, {cwd}", app.package_info().name);
|
||||||
|
app.notification()
|
||||||
|
.builder()
|
||||||
|
.title("This app is already running!")
|
||||||
|
.body("You can find it in the tray menu.")
|
||||||
|
.show()
|
||||||
|
.unwrap();
|
||||||
|
}))
|
||||||
|
.plugin(tauri_plugin_autostart::init(
|
||||||
|
MacosLauncher::LaunchAgent,
|
||||||
|
Some(vec![]),
|
||||||
|
))
|
||||||
|
.plugin(tauri_plugin_process::init())
|
||||||
|
.setup(move |app| {
|
||||||
|
let app_handle = app.handle();
|
||||||
|
// set the splashscreen and main windows to be globally available with the tauri state API
|
||||||
|
app.manage(SplashscreenWindow(Arc::new(Mutex::new(
|
||||||
|
app.get_webview_window("splashscreen").unwrap(),
|
||||||
|
))));
|
||||||
|
app.manage(MainWindow(Arc::new(Mutex::new(
|
||||||
|
app.get_webview_window("main").unwrap(),
|
||||||
|
))));
|
||||||
|
|
||||||
|
app_handle.plugin(tauri_plugin_updater::Builder::new().build())?;
|
||||||
|
|
||||||
|
#[cfg(desktop)]
|
||||||
|
{
|
||||||
|
// Tray
|
||||||
|
let handle = app.handle();
|
||||||
|
tray::create_tray(handle)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open the dev tools automatically when debugging the application
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
if let Some(main_window) = app.get_webview_window("main") {
|
||||||
|
main_window.open_devtools();
|
||||||
|
};
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.invoke_handler(tauri::generate_handler![
|
||||||
|
close_splashscreen,
|
||||||
|
updater::check_for_updates,
|
||||||
|
updater::download_update,
|
||||||
|
updater::install_update,
|
||||||
|
updater::clear_update_cache
|
||||||
|
])
|
||||||
|
.build(context)
|
||||||
.expect("error while running tauri application");
|
.expect("error while running tauri application");
|
||||||
|
|
||||||
|
#[cfg(desktop)]
|
||||||
|
app.run(|app, e| match e {
|
||||||
|
RunEvent::Ready => {
|
||||||
|
#[cfg(any(target_os = "macos", debug_assertions))]
|
||||||
|
let window = app.get_webview_window("main").unwrap();
|
||||||
|
|
||||||
|
#[cfg(debug_assertions)]
|
||||||
|
window.open_devtools();
|
||||||
|
|
||||||
|
println!("App is ready");
|
||||||
|
}
|
||||||
|
RunEvent::ExitRequested { api, code, .. } => {
|
||||||
|
// Keep the event loop running even if all windows are closed
|
||||||
|
// This allow us to catch tray icon events when there is no window
|
||||||
|
// if we manually requested an exit (code is Some(_)) we will let it go through
|
||||||
|
if code.is_none() {
|
||||||
|
api.prevent_exit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tauri::RunEvent::WindowEvent {
|
||||||
|
label,
|
||||||
|
event: tauri::WindowEvent::CloseRequested { api, .. },
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
tauri::AppHandle::hide(&app.app_handle()).unwrap();
|
||||||
|
}
|
||||||
|
#[cfg(not(target_os = "macos"))]
|
||||||
|
{
|
||||||
|
let window = app.get_webview_window(label.as_str()).unwrap();
|
||||||
|
window.hide().unwrap();
|
||||||
|
}
|
||||||
|
api.prevent_close();
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
36
src-tauri/src/tray.rs
Normal file
36
src-tauri/src/tray.rs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
use tauri::{
|
||||||
|
menu::{Menu, MenuItem},
|
||||||
|
tray::{ClickType, TrayIconBuilder},
|
||||||
|
Manager, Runtime,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub fn create_tray<R: Runtime>(app: &tauri::AppHandle<R>) -> tauri::Result<()> {
|
||||||
|
let branding = MenuItem::with_id(app, "name", "Spacebar", false, None::<String>)?;
|
||||||
|
let quit_i = MenuItem::with_id(app, "quit", "Quit", true, None::<String>)?;
|
||||||
|
let menu1 = Menu::with_items(app, &[&branding, &quit_i])?;
|
||||||
|
|
||||||
|
let _ = TrayIconBuilder::with_id("main")
|
||||||
|
.tooltip("Spacebar")
|
||||||
|
.icon(app.default_window_icon().unwrap().clone())
|
||||||
|
.menu(&menu1)
|
||||||
|
.menu_on_left_click(false)
|
||||||
|
.on_menu_event(move |app, event| match event.id.as_ref() {
|
||||||
|
"quit" => {
|
||||||
|
app.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_ => {}
|
||||||
|
})
|
||||||
|
.on_tray_icon_event(|tray, event| {
|
||||||
|
if event.click_type == ClickType::Left {
|
||||||
|
let app = tray.app_handle();
|
||||||
|
if let Some(window) = app.get_webview_window("main") {
|
||||||
|
let _ = window.show();
|
||||||
|
let _ = window.set_focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.build(app);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
451
src-tauri/src/updater.rs
Normal file
451
src-tauri/src/updater.rs
Normal file
@ -0,0 +1,451 @@
|
|||||||
|
use reqwest;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::sync::Mutex;
|
||||||
|
use tauri::{Manager, Runtime};
|
||||||
|
use tauri_plugin_updater::{Update, UpdaterExt};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
|
static UPDATE_INFO: Mutex<Option<Update>> = Mutex::new(None);
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Release {
|
||||||
|
assets: Vec<Asset>,
|
||||||
|
prerelease: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
struct Asset {
|
||||||
|
name: String,
|
||||||
|
browser_download_url: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, Debug, Clone)]
|
||||||
|
struct UpdateAvailable {
|
||||||
|
version: String,
|
||||||
|
body: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
// ignore_version: String
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Window<R>) {
|
||||||
|
let handle = window.app_handle().clone();
|
||||||
|
|
||||||
|
if std::env::var("DEVELOPMENT").is_ok() {
|
||||||
|
println!("[Updater] This is a development environment, not updating.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: readd this
|
||||||
|
// if !handle.config().tauri.bundle.updater.active {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
match window.emit("CHECKING_FOR_UPDATE", Some(serde_json::json!({}))) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update checking event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let package_path = handle
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("update.sbcup");
|
||||||
|
|
||||||
|
// if we already have an update package, remove it
|
||||||
|
if package_path.exists() {
|
||||||
|
clear_update_cache(window.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
tauri::async_runtime::spawn(async move {
|
||||||
|
println!("[Updater] Searching for update file on github.");
|
||||||
|
// Custom configure the updater.
|
||||||
|
let github_releases_endpoint = "https://api.github.com/repos/spacebarchat/client/releases";
|
||||||
|
let github_releases_endpoint = match Url::parse(github_releases_endpoint) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to parse url: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to parse url: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let client = reqwest::Client::new();
|
||||||
|
let req = client
|
||||||
|
.get(github_releases_endpoint.clone())
|
||||||
|
.header("Content-Type", "application/json")
|
||||||
|
// If this is not set you will get a 403 forbidden error.
|
||||||
|
.header("User-Agent", "spacebar-client");
|
||||||
|
let response = match req.send().await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to send request: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to send request: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if response.status() != reqwest::StatusCode::OK {
|
||||||
|
println!(
|
||||||
|
"[Updater] Non OK status code: {:?}. Failed to check for updates",
|
||||||
|
response.status()
|
||||||
|
);
|
||||||
|
emit_update_error(
|
||||||
|
window,
|
||||||
|
format!("Non OK status code: {:?}", response.status()),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let releases = match response.json::<Vec<Release>>().await {
|
||||||
|
Ok(releases) => releases,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to parse response: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to parse response: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// check if there are any releases
|
||||||
|
if releases.len() == 0 {
|
||||||
|
println!("[Updater] No releases found. Failed to check for updates");
|
||||||
|
match window.emit("UPDATE_NOT_AVAILABLE", Some({})) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to emit update not available event: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// if ignore_prereleases is true, find first release that is not a prerelease, otherwise get the first release
|
||||||
|
let latest_release = if ignore_prereleases {
|
||||||
|
releases.iter().find(|release| !release.prerelease).unwrap()
|
||||||
|
} else {
|
||||||
|
releases.get(0).unwrap()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Find an asset named "latest.json".
|
||||||
|
let tauri_release_asset = latest_release
|
||||||
|
.assets
|
||||||
|
.iter()
|
||||||
|
.find(|asset| asset.name == "latest.json");
|
||||||
|
|
||||||
|
// If we found the asset, set it as the updater endpoint.
|
||||||
|
let tauri_release_asset = match tauri_release_asset {
|
||||||
|
Some(tauri_release_asset) => tauri_release_asset,
|
||||||
|
None => {
|
||||||
|
println!("[Updater] Failed to find latest.json asset. Failed to check for updates\n\nFound Assets are:");
|
||||||
|
// Print a list of the assets found
|
||||||
|
for asset in latest_release.assets.iter() {
|
||||||
|
println!(" {:?}", asset.name);
|
||||||
|
}
|
||||||
|
emit_update_error(
|
||||||
|
window,
|
||||||
|
format!("Failed to find latest.json asset. Failed to check for updates"),
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let tauri_release_endpoint = match Url::parse(&tauri_release_asset.browser_download_url) {
|
||||||
|
Ok(url) => url,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to parse url: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to parse url: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let updater_builder = match handle
|
||||||
|
.updater_builder()
|
||||||
|
.version_comparator(|current_version, latest_version| {
|
||||||
|
println!("[Updater] Current version: {}", current_version);
|
||||||
|
println!(
|
||||||
|
"[Updater] Latest version: {}",
|
||||||
|
latest_version.version.clone()
|
||||||
|
);
|
||||||
|
|
||||||
|
// if the current build is 00 then its a dev environment and we should not update
|
||||||
|
if current_version.build.to_string() == "00" {
|
||||||
|
println!("[Updater] Build ID is 00, looks like a development environment, not updating.");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// upgrade
|
||||||
|
if latest_version.version > current_version {
|
||||||
|
println!("[Updater] An update is available.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// downgrade
|
||||||
|
if latest_version.version < current_version {
|
||||||
|
println!("[Updater] The installed version is newer than the latest version. A little odd, but ok.");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
})
|
||||||
|
.endpoints(vec![tauri_release_endpoint])
|
||||||
|
.header("User-Agent", "spacebar-client")
|
||||||
|
{
|
||||||
|
Ok(updater_builder) => updater_builder,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to build updater builder: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to build updater builder: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let updater = match updater_builder.build() {
|
||||||
|
Ok(updater) => updater,
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to build updater: {:?}. Failed to check for updates",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
emit_update_error(window, format!("Failed to build updater: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
println!("[Updater] Checking for updates");
|
||||||
|
|
||||||
|
let response = updater.check().await;
|
||||||
|
|
||||||
|
println!("[Updater] Update check response: {:?}", response);
|
||||||
|
|
||||||
|
match response {
|
||||||
|
Ok(Some(update)) => {
|
||||||
|
// if ignore_version == update.version {
|
||||||
|
// println!("Ignoring update as user has asked to ignore this version.");
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
UPDATE_INFO.lock().unwrap().replace(update.clone());
|
||||||
|
|
||||||
|
// otherwise emit the update available event
|
||||||
|
match window.emit("UPDATE_AVAILABLE", Some({})) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update available event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return download_update(window).await;
|
||||||
|
}
|
||||||
|
Ok(None) => {
|
||||||
|
println!("[Updater] No update available");
|
||||||
|
match window.emit("UPDATE_NOT_AVAILABLE", Some({})) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!(
|
||||||
|
"[Updater] Failed to emit update not available event: {:?}",
|
||||||
|
e
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to check for updates: {:?}.", e);
|
||||||
|
match window.emit("UPDATE_ERROR", Some(format!("{:?}", e))) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update error event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn download_update<R: Runtime>(window: tauri::Window<R>) {
|
||||||
|
if std::env::var("DEVELOPMENT").is_ok() {
|
||||||
|
println!("[Updater] This is a development environment, not updating.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[Updater] Downloading update package");
|
||||||
|
|
||||||
|
let update = match UPDATE_INFO.lock().unwrap().clone() {
|
||||||
|
Some(update) => update,
|
||||||
|
None => {
|
||||||
|
println!("[Updater] No update found to download");
|
||||||
|
emit_update_error(window, format!("No update found to download"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// emit UPDATE_DOWNLOADING
|
||||||
|
match window.emit("UPDATE_DOWNLOADING", Some({})) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update downloading event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_chunk = |size: usize, progress: Option<u64>| {
|
||||||
|
println!(
|
||||||
|
"[Updater] Received chunk: size={}, progress={:?}",
|
||||||
|
size, progress
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_download_finish = || {
|
||||||
|
println!("[Updater] Download finished!");
|
||||||
|
};
|
||||||
|
|
||||||
|
let download_response = update.download(on_chunk, on_download_finish).await;
|
||||||
|
|
||||||
|
if let Err(e) = download_response {
|
||||||
|
println!("[Updater] Failed to download update: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to download update: {:?}", e));
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
println!("[Updater] Update downloaded");
|
||||||
|
|
||||||
|
let handle = window.app_handle().clone();
|
||||||
|
let package_path = handle
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("update.sbcup");
|
||||||
|
println!("[Updater] Saving update package to {:?}", package_path);
|
||||||
|
|
||||||
|
// store download_response bytes to a file
|
||||||
|
match std::fs::write(package_path.clone(), download_response.unwrap()) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to save update package: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to save update package: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match window.emit(
|
||||||
|
"UPDATE_DOWNLOADED",
|
||||||
|
Some(UpdateAvailable {
|
||||||
|
version: update.version.clone(),
|
||||||
|
body: update.body.clone(),
|
||||||
|
}),
|
||||||
|
) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update downloaded event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub async fn install_update<R: Runtime>(window: tauri::Window<R>) {
|
||||||
|
if std::env::var("DEVELOPMENT").is_ok() {
|
||||||
|
println!("[Updater] This is a development environment, not updating.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("[Updater] Installing update package");
|
||||||
|
|
||||||
|
let update = match UPDATE_INFO.lock().unwrap().clone() {
|
||||||
|
Some(update) => update,
|
||||||
|
None => {
|
||||||
|
println!("[Updater] No update found to install");
|
||||||
|
emit_update_error(window, format!("No update found to install"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let handle = window.app_handle().clone();
|
||||||
|
let package_path = handle
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("update.sbcup");
|
||||||
|
|
||||||
|
// check if the update package exists
|
||||||
|
if !package_path.exists() {
|
||||||
|
println!("[Updater] No pending update found to install");
|
||||||
|
emit_update_error(window, format!("No pending update found to install"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// read in the update package bytes
|
||||||
|
let bytes = match std::fs::read(package_path.clone()) {
|
||||||
|
Ok(bytes) => bytes,
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to read update package: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to read update package: {:?}", e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let install_response = update.install(bytes);
|
||||||
|
|
||||||
|
if let Err(e) = install_response {
|
||||||
|
println!("[Updater] Failed to install update: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to install update: {:?}", e));
|
||||||
|
} else {
|
||||||
|
println!("[Updater] Update installed");
|
||||||
|
|
||||||
|
// remove the update package
|
||||||
|
match std::fs::remove_file(package_path) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to remove update package: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to remove update package: {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn clear_update_cache<R: Runtime>(window: tauri::Window<R>) {
|
||||||
|
let handle = window.app_handle().clone();
|
||||||
|
let package_path = handle
|
||||||
|
.path()
|
||||||
|
.app_local_data_dir()
|
||||||
|
.unwrap()
|
||||||
|
.join("update.sbcup");
|
||||||
|
|
||||||
|
// check if the update package exists
|
||||||
|
if !package_path.exists() {
|
||||||
|
println!("[Updater] No pending update found to clear");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove the update package
|
||||||
|
match std::fs::remove_file(package_path) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to remove update package: {:?}", e);
|
||||||
|
emit_update_error(window, format!("Failed to remove update package: {:?}", e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// utility function to emit UPDATE_ERROR
|
||||||
|
fn emit_update_error<R: Runtime>(window: tauri::Window<R>, error: String) {
|
||||||
|
match window.emit("UPDATE_ERROR", Some(error)) {
|
||||||
|
Ok(_) => {}
|
||||||
|
Err(e) => {
|
||||||
|
println!("[Updater] Failed to emit update error event: {:?}", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,56 +1,59 @@
|
|||||||
{
|
{
|
||||||
|
"productName": "Spacebar",
|
||||||
|
"version": "./version.json",
|
||||||
|
"identifier": "chat.spacebar.app",
|
||||||
"build": {
|
"build": {
|
||||||
|
"frontendDist": "../dist",
|
||||||
|
"devUrl": "http://localhost:1420",
|
||||||
"beforeDevCommand": "pnpm run dev",
|
"beforeDevCommand": "pnpm run dev",
|
||||||
"beforeBuildCommand": "pnpm run build",
|
"beforeBuildCommand": "pnpm run build"
|
||||||
"devPath": "http://localhost:1420",
|
|
||||||
"distDir": "../build",
|
|
||||||
"withGlobalTauri": false
|
|
||||||
},
|
},
|
||||||
"package": {
|
"app": {
|
||||||
"productName": "Spacebar",
|
"withGlobalTauri": true,
|
||||||
"version": "../package.json"
|
"windows": [
|
||||||
},
|
{
|
||||||
"tauri": {
|
"label": "main",
|
||||||
"bundle": {
|
"title": "Tauri",
|
||||||
"active": true,
|
"width": 800,
|
||||||
"targets": "all",
|
"height": 600,
|
||||||
"identifier": "chat.spacebar.app",
|
"visible": false
|
||||||
"icon": [
|
|
||||||
"icons/32x32.png",
|
|
||||||
"icons/128x128.png",
|
|
||||||
"icons/128x128@2x.png",
|
|
||||||
"icons/icon.icns",
|
|
||||||
"icons/icon.ico"
|
|
||||||
],
|
|
||||||
"publisher": "Spacebar",
|
|
||||||
"category": "SocialNetworking",
|
|
||||||
"shortDescription": "A free, opensource self-hostable discord-compatible chat, voice and video platform.",
|
|
||||||
"windows": {
|
|
||||||
"nsis": {
|
|
||||||
"license": "../LICENSE",
|
|
||||||
"sidebarImage": "./icons/sidebar.bmp",
|
|
||||||
"installerIcon": "./icons/icon.ico"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"updater": {
|
{
|
||||||
"active": false,
|
"label": "splashscreen",
|
||||||
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQxRkQwNTY1NzBEOTMyMTUKUldRVk10bHdaUVg5UWVoVm9JeDg4UEs1TkpMT3FKdzc3Y29CN2NZNk9vRE9sanJCUERqT09HVVYK",
|
"width": 400,
|
||||||
"windows": {
|
"height": 200,
|
||||||
"installMode": "passive"
|
"decorations": false,
|
||||||
}
|
"resizable": false,
|
||||||
|
"url": "splashscreen.html"
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"security": {
|
},
|
||||||
"csp": null
|
"bundle": {
|
||||||
},
|
"active": true,
|
||||||
"windows": []
|
"targets": ["deb", "rpm", "appimage", "nsis", "app", "dmg", "updater"],
|
||||||
|
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||||
|
"publisher": "Spacebar",
|
||||||
|
"category": "SocialNetworking",
|
||||||
|
"shortDescription": "A free, opensource self-hostable discord-compatible chat, voice and video platform.",
|
||||||
|
"licenseFile": "../LICENSE",
|
||||||
|
"windows": {
|
||||||
|
"nsis": {
|
||||||
|
"sidebarImage": "./icons/sidebar.bmp",
|
||||||
|
"installerIcon": "./icons/icon.ico"
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"plugins": {
|
"plugins": {
|
||||||
"shell": {
|
"shell": {
|
||||||
"open": true
|
"open": true
|
||||||
},
|
},
|
||||||
"updater": {
|
"updater": {
|
||||||
"endpoints": ["https://update.spacebar.chat/updates/{{target}}/{{arch}}"]
|
"active": true,
|
||||||
|
"endpoints": ["https://github.com/spacebarchat/client/releases/download/latest/latest.json"],
|
||||||
|
"pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDQxRkQwNTY1NzBEOTMyMTUKUldRVk10bHdaUVg5UWVoVm9JeDg4UEs1TkpMT3FKdzc3Y29CN2NZNk9vRE9sanJCUERqT09HVVYK",
|
||||||
|
"windows": {
|
||||||
|
"installMode": "passive"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
{
|
{
|
||||||
"tauri": {
|
"bundle": {
|
||||||
"bundle": {
|
"iOS": {
|
||||||
"iOS": {
|
"developmentTeam": "47RXBB8X9K"
|
||||||
"developmentTeam": "47RXBB8X9K"
|
}
|
||||||
}
|
},
|
||||||
},
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"fullscreen": false,
|
"fullscreen": false,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tauri": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tauri": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"tauri": {
|
"app": {
|
||||||
"windows": [
|
"windows": [
|
||||||
{
|
{
|
||||||
"width": 400,
|
"width": 400,
|
||||||
|
3
src-tauri/version.json
Normal file
3
src-tauri/version.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"version": "0.1.1+00"
|
||||||
|
}
|
21
src/App.tsx
21
src/App.tsx
@ -6,6 +6,8 @@ import LoginPage from "./pages/LoginPage";
|
|||||||
import NotFoundPage from "./pages/NotFound";
|
import NotFoundPage from "./pages/NotFound";
|
||||||
import RegistrationPage from "./pages/RegistrationPage";
|
import RegistrationPage from "./pages/RegistrationPage";
|
||||||
|
|
||||||
|
import { getTauriVersion, getVersion } from "@tauri-apps/api/app";
|
||||||
|
import { arch, locale, platform, version } from "@tauri-apps/plugin-os";
|
||||||
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";
|
||||||
@ -20,6 +22,7 @@ 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 { isTauri } from "./utils/Utils";
|
||||||
|
|
||||||
function App() {
|
function App() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
@ -51,8 +54,24 @@ function App() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const loadAsyncGlobals = async () => {
|
||||||
|
const [tauriVersion, appVersion, platformName, platformArch, platformVersion, platformLocale] =
|
||||||
|
await Promise.all([getTauriVersion(), getVersion(), platform(), arch(), version(), locale()]);
|
||||||
|
window.globals = {
|
||||||
|
tauriVersion: tauriVersion,
|
||||||
|
appVersion: appVersion,
|
||||||
|
platform: {
|
||||||
|
name: platformName,
|
||||||
|
arch: platformArch,
|
||||||
|
version: platformVersion,
|
||||||
|
locale: platformLocale,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
isTauri && loadAsyncGlobals();
|
||||||
Globals.load();
|
Globals.load();
|
||||||
app.loadToken();
|
app.loadSettings();
|
||||||
|
|
||||||
logger.debug("Loading complete");
|
logger.debug("Loading complete");
|
||||||
app.setAppLoading(false);
|
app.setAppLoading(false);
|
||||||
|
@ -1,11 +1,22 @@
|
|||||||
<svg width="1442" height="256" viewBox="0 0 1442 256" fill="none" xmlns="http://www.w3.org/2000/svg">
|
<svg width="1200" height="214" viewBox="0 0 1200 214" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M235.497 138C238.806 153.394 234.982 169.46 225.097 181.709C215.211 193.958 200.321 201.077 184.589 201.077H52.0732C36.3408 201.077 21.451 193.958 11.5658 181.709C1.67958 169.46 -2.14373 153.394 1.1649 138L24.4985 29.4245C25.5849 24.3682 29.0541 20.1534 33.8052 18.1191C38.5563 16.0838 43.9981 16.4817 48.4028 19.1861L118.331 62.1159L188.259 19.1861C192.664 16.4817 198.106 16.0838 202.857 18.1191C207.608 20.1534 211.077 24.3682 212.163 29.4245L235.497 138ZM181.883 112.957C181.883 105.091 175.511 98.7142 167.652 98.7142H140.235C132.375 98.7142 126.005 105.091 126.005 112.957V140.398C126.005 148.264 132.375 154.64 140.235 154.64H167.652C175.511 154.64 181.883 148.264 181.883 140.398V112.957ZM110.657 112.957C110.657 105.091 104.287 98.7142 96.4272 98.7142H69.0101C61.1507 98.7142 54.7792 105.091 54.7792 112.957V140.398C54.7792 148.264 61.1507 154.64 69.0101 154.64H96.4272C104.287 154.64 110.657 148.264 110.657 140.398V112.957Z" fill="#0185FF"/>
|
<g id="Logo-Blue 2" clip-path="url(#clip0_648_668)">
|
||||||
<path d="M379.118 10.8465C335.763 10.8465 309.821 35.8002 309.821 68.8373C309.821 103.909 339.541 116.525 361.461 124.852C379.37 131.916 392.488 136.963 392.488 148.065C392.488 158.158 382.145 165.474 364.487 165.474C344.578 165.474 324.177 156.643 310.828 145.289V190.428C324.177 199.754 345.838 207.064 369.532 207.064C413.897 207.064 441.098 182.111 441.098 146.551C441.098 108.703 409.615 96.5916 383.659 87.0039C366.253 80.1914 356.919 76.1541 356.919 67.3229C356.919 58.2396 366.506 52.4358 382.397 52.4358C398.281 52.4358 417.675 57.9874 429.765 66.5664V23.7016C416.667 16.14 398.785 10.8465 379.118 10.8465Z" fill="#0185FF"/>
|
<g id="Icon">
|
||||||
<path d="M448.725 256H495.572V183.874C503.893 197.233 520.538 206.812 538.443 206.812C571.203 206.812 599.916 177.825 599.916 132.682C599.916 87.5075 569.943 58.269 537.183 58.269C516.752 58.269 501.623 69.1075 492.801 81.9626L488.519 62.0529H448.725V256ZM495.067 132.667C495.067 113.486 506.919 100.11 523.56 100.11C540.46 100.11 552.561 113.991 552.561 132.414C552.561 150.585 540.46 164.97 523.312 164.97C506.919 164.97 495.067 151.342 495.067 132.667Z" fill="#0185FF"/>
|
<path id="Vector" fill-rule="evenodd" clip-rule="evenodd" d="M195.975 114.84C198.729 127.651 195.546 141.021 187.32 151.214C179.094 161.407 166.702 167.331 153.611 167.331H43.3341C30.242 167.331 17.851 161.407 9.62479 151.214C1.39771 141.021 -1.78396 127.651 0.969403 114.84L20.3871 24.4863C21.2912 20.2786 24.1781 16.7711 28.1319 15.0782C32.0856 13.3845 36.6142 13.7156 40.2797 15.9662L98.4723 51.6913L156.665 15.9662C160.33 13.7156 164.859 13.3845 168.813 15.0782C172.766 16.7711 175.653 20.2786 176.557 24.4863L195.975 114.84ZM151.359 94.0001C151.359 87.4542 146.056 82.1476 139.516 82.1476H116.7C110.159 82.1476 104.858 87.4542 104.858 94.0001V116.836C104.858 123.382 110.159 128.688 116.7 128.688H139.516C146.056 128.688 151.359 123.382 151.359 116.836V94.0001ZM92.0861 94.0001C92.0861 87.4542 86.7852 82.1476 80.2444 82.1476H57.4286C50.8882 82.1476 45.586 87.4542 45.586 94.0001V116.836C45.586 123.382 50.8882 128.688 57.4286 128.688H80.2444C86.7852 128.688 92.0861 123.382 92.0861 116.836V94.0001Z" fill="#0185FF"/>
|
||||||
<path d="M667.938 206.812C689.12 206.812 703.748 195.973 712.318 183.118H712.566L717.102 203.028H756.899V62.0529H717.102L712.566 81.9626H712.318C703.748 69.1075 689.12 58.269 667.938 58.269C636.438 58.269 605.708 88.2641 605.708 132.903C605.708 176.817 636.438 206.812 667.938 206.812ZM653.056 132.682C653.056 114.496 665.164 100.11 682.312 100.11C698.702 100.11 710.804 113.739 710.804 132.667C710.804 151.342 698.702 164.97 682.312 164.97C665.164 164.97 653.056 151.096 653.056 132.682Z" fill="#0185FF"/>
|
</g>
|
||||||
<path d="M811.607 132.667C811.607 112.729 827.233 100.11 846.893 100.11C858.995 100.11 870.085 103.139 881.677 110.458V68.0988C869.328 61.2937 853.701 58.269 839.583 58.269C797.508 58.269 764.259 88.7677 764.259 132.414C764.259 176.061 797.254 206.812 839.583 206.812C854.204 206.812 869.576 203.283 881.677 196.982V154.623C870.085 162.951 858.238 164.97 847.148 164.97C827.233 164.97 811.607 152.604 811.607 132.667Z" fill="#0185FF"/>
|
<g id="Wordmark">
|
||||||
<path d="M967.641 206.812C985.532 206.812 1001.9 203.031 1016.26 195.219V158.144C1003.41 165.216 989.311 170.52 973.939 170.52C954.534 170.52 937.896 162.185 931.852 145.263H1024.32C1025.07 140.728 1025.58 135.689 1025.58 130.146C1025.58 83.22 995.603 58.269 958.816 58.269C918.242 58.269 886.99 88.5129 886.99 132.918C886.99 175.56 915.971 206.812 967.641 206.812ZM930.84 118.304C934.619 103.654 945.709 94.5615 958.568 94.5615C971.923 94.5615 980.748 104.159 981.25 118.304H930.84Z" fill="#0185FF"/>
|
<path id="Vector_2" d="M315.493 9.02637C279.414 9.02637 257.826 29.7923 257.826 57.2851C257.826 86.471 282.558 96.9698 300.8 103.899C315.703 109.778 326.619 113.978 326.619 123.217C326.619 131.616 318.012 137.704 303.318 137.704C286.75 137.704 269.773 130.355 258.664 120.907V158.47C269.773 166.231 287.798 172.314 307.516 172.314C344.436 172.314 367.072 151.549 367.072 121.957C367.072 90.4605 340.872 80.3816 319.272 72.403C304.787 66.7337 297.02 63.374 297.02 56.0248C297.02 48.4659 304.998 43.6361 318.222 43.6361C331.44 43.6361 347.579 48.256 357.64 55.3953V19.7241C346.741 13.4315 331.86 9.02637 315.493 9.02637Z" fill="#0185FF"/>
|
||||||
<path d="M1080.04 0H1033.19V203.028H1072.99L1077.27 183.118C1086.09 195.973 1101.22 206.812 1121.39 206.812C1154.15 206.812 1184.38 177.825 1184.38 132.667C1184.38 87.2554 1156.17 58.2691 1123.67 58.2691C1105.76 58.2691 1088.36 67.8474 1080.04 81.2061V0ZM1079.53 132.667C1079.53 113.739 1091.39 100.111 1107.78 100.111C1124.93 100.111 1137.03 114.496 1137.03 132.919C1137.03 151.09 1124.93 164.97 1108.03 164.97C1091.39 164.97 1079.53 151.595 1079.53 132.667Z" fill="#0185FF"/>
|
<path id="Vector_3" d="M373.418 213.037H412.403V153.016C419.328 164.133 433.179 172.104 448.079 172.104C475.342 172.104 499.236 147.982 499.236 110.415C499.236 72.8219 474.293 48.4902 447.031 48.4902C430.029 48.4902 417.439 57.5098 410.097 68.2075L406.534 51.6391H373.418V213.037ZM411.983 110.403C411.983 94.4406 421.846 83.3094 435.694 83.3094C449.758 83.3094 459.828 94.8608 459.828 110.192C459.828 125.314 449.758 137.284 435.488 137.284C421.846 137.284 411.983 125.943 411.983 110.403Z" fill="#0185FF"/>
|
||||||
<path d="M1252.4 206.812C1273.59 206.812 1288.21 195.973 1296.78 183.118H1297.03L1301.57 203.028H1341.36V62.0529H1301.57L1297.03 81.9626H1296.78C1288.21 69.1075 1273.59 58.269 1252.4 58.269C1220.9 58.269 1190.17 88.2641 1190.17 132.903C1190.17 176.817 1220.9 206.812 1252.4 206.812ZM1237.52 132.682C1237.52 114.496 1249.63 100.11 1266.78 100.11C1283.17 100.11 1295.27 113.739 1295.27 132.667C1295.27 151.342 1283.17 164.97 1266.78 164.97C1249.63 164.97 1237.52 151.096 1237.52 132.682Z" fill="#0185FF"/>
|
<path id="Vector_4" d="M555.844 172.104C573.471 172.104 585.644 163.084 592.776 152.387H592.982L596.757 168.955H629.875V51.6391H596.757L592.982 68.2075H592.776C585.644 57.5098 573.471 48.4902 555.844 48.4902C529.631 48.4902 504.058 73.4515 504.058 110.599C504.058 147.143 529.631 172.104 555.844 172.104ZM543.46 110.415C543.46 95.2811 553.536 83.3094 567.806 83.3094C581.445 83.3094 591.516 94.6512 591.516 110.403C591.516 125.944 581.445 137.284 567.806 137.284C553.536 137.284 543.46 125.739 543.46 110.415Z" fill="#0185FF"/>
|
||||||
<path d="M1351.05 203.028H1397.9V138.214C1397.9 112.471 1413.27 103.894 1429.65 103.894C1433.68 103.894 1437.97 104.399 1442 105.408V61.8008C1438.72 60.7907 1435.45 60.5392 1431.16 60.5392C1418.31 60.5392 1402.69 66.0862 1393.36 82.7312H1392.86L1388.33 62.0529H1351.05V203.028Z" fill="#0185FF"/>
|
<path id="Vector_5" d="M675.401 110.403C675.401 93.8107 688.405 83.3094 704.765 83.3094C714.836 83.3094 724.065 85.8301 733.711 91.9208V56.6704C723.435 51.0073 710.431 48.4902 698.682 48.4902C663.668 48.4902 635.999 73.8706 635.999 110.192C635.999 146.514 663.457 172.104 698.682 172.104C710.849 172.104 723.641 169.168 733.711 163.924V128.674C724.065 135.604 714.206 137.284 704.977 137.284C688.405 137.284 675.401 126.994 675.401 110.403Z" fill="#0185FF"/>
|
||||||
|
<path id="Vector_6" d="M805.247 172.104C820.135 172.104 833.756 168.958 845.706 162.457V131.604C835.013 137.489 823.28 141.903 810.488 141.903C794.339 141.903 780.494 134.967 775.464 120.885H852.414C853.038 117.111 853.462 112.917 853.462 108.305C853.462 69.2539 828.516 48.4902 797.903 48.4902C764.138 48.4902 738.131 73.6585 738.131 110.611C738.131 146.097 762.248 172.104 805.247 172.104ZM774.622 98.45C777.767 86.2586 786.995 78.692 797.696 78.692C808.81 78.692 816.154 86.6789 816.572 98.45H774.622Z" fill="#0185FF"/>
|
||||||
|
<path id="Vector_7" d="M898.785 0H859.797V168.955H892.918L896.479 152.387C903.819 163.084 916.41 172.104 933.195 172.104C960.457 172.104 985.614 147.982 985.614 110.402C985.614 72.612 962.138 48.4902 935.092 48.4902C920.188 48.4902 905.708 56.4611 898.785 67.5779V0ZM898.36 110.402C898.36 94.651 908.23 83.3101 921.869 83.3101C936.141 83.3101 946.21 95.281 946.21 110.612C946.21 125.734 936.141 137.284 922.077 137.284C908.23 137.284 898.36 126.154 898.36 110.402Z" fill="#0185FF"/>
|
||||||
|
<path id="Vector_8" d="M1042.22 172.104C1059.85 172.104 1072.02 163.084 1079.15 152.387H1079.36L1083.14 168.955H1116.25V51.6391H1083.14L1079.36 68.2075H1079.15C1072.02 57.5098 1059.85 48.4902 1042.22 48.4902C1016.01 48.4902 990.433 73.4515 990.433 110.599C990.433 147.143 1016.01 172.104 1042.22 172.104ZM1029.84 110.415C1029.84 95.2811 1039.91 83.3094 1054.19 83.3094C1067.83 83.3094 1077.89 94.6512 1077.89 110.403C1077.89 125.944 1067.83 137.284 1054.19 137.284C1039.91 137.284 1029.84 125.739 1029.84 110.415Z" fill="#0185FF"/>
|
||||||
|
<path id="Vector_9" d="M1124.31 168.955H1163.3V115.019C1163.3 93.5959 1176.09 86.4583 1189.72 86.4583C1193.08 86.4583 1196.65 86.8785 1200 87.7182V51.4293C1197.27 50.5887 1194.55 50.3794 1190.98 50.3794C1180.29 50.3794 1167.29 54.9955 1159.52 68.8471H1159.11L1155.34 51.6391H1124.31V168.955Z" fill="#0185FF"/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<defs>
|
||||||
|
<clipPath id="clip0_648_668">
|
||||||
|
<rect width="1200" height="213.037" fill="white"/>
|
||||||
|
</clipPath>
|
||||||
|
</defs>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 4.8 KiB After Width: | Height: | Size: 5.2 KiB |
@ -66,7 +66,7 @@ export const LabelWrapper = styled.div<{ error?: boolean }>`
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
color: ${(props) => (props.error ? "var(--error)" : "var(--text)")};
|
color: ${(props) => (props.error ? "var(--error)" : "var(--text-header-secondary)")};
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export const InputErrorText = styled.label`
|
export const InputErrorText = styled.label`
|
||||||
@ -88,10 +88,9 @@ export const InputWrapper = styled.div`
|
|||||||
display: flex;
|
display: flex;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
// TODO: Fix border hover causing small layout shift
|
export const Input = styled.input<{ error?: boolean; disableFocusRing?: boolean }>`
|
||||||
export const Input = styled.input<{ error?: boolean }>`
|
|
||||||
outline: none;
|
outline: none;
|
||||||
background: var(--background-secondary-alt);
|
background: var(--background-secondary);
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
flex: 1;
|
flex: 1;
|
||||||
@ -100,17 +99,23 @@ export const Input = styled.input<{ error?: boolean }>`
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
border: none;
|
border: none;
|
||||||
aria-invalid: ${(props) => (props.error ? "true" : "false")};
|
aria-invalid: ${(props) => (props.error ? "true" : "false")};
|
||||||
border: ${(props) => (props.error ? "1px solid var(--error)" : "none")};
|
border: ${(props) => (props.error ? "1px solid var(--error)" : "1px solid var(--background-secondary)")};
|
||||||
|
|
||||||
&:focus {
|
${(props) =>
|
||||||
border: 1px solid var(--primary);
|
!props.disableFocusRing &&
|
||||||
}
|
`
|
||||||
|
&:focus {
|
||||||
|
border: 1px solid var(--primary);
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
|
||||||
// disabled styling
|
// disabled styling
|
||||||
&:disabled {
|
&:disabled {
|
||||||
background: var(--background-secondary-alt);
|
// TODO: this might need to be adjusted
|
||||||
|
background: var(--background-primary-alt);
|
||||||
color: var(--text-disabled);
|
color: var(--text-disabled);
|
||||||
border: 1px solid var(--background-secondary-alt);
|
border: 1px solid var(--background-secondary-alt);
|
||||||
|
cursor: not-allowed;
|
||||||
}
|
}
|
||||||
|
|
||||||
-moz-appearance: textfield;
|
-moz-appearance: textfield;
|
||||||
|
@ -1,68 +1,97 @@
|
|||||||
|
import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { PopoutContext } from "../contexts/PopoutContext";
|
|
||||||
import AccountStore from "../stores/AccountStore";
|
import AccountStore from "../stores/AccountStore";
|
||||||
import { useAppStore } from "../stores/AppStore";
|
import { useAppStore } from "../stores/AppStore";
|
||||||
|
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 UserProfilePopout from "./UserProfilePopout";
|
import Floating from "./floating/Floating";
|
||||||
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
const Wrapper = styled(Container)<{ size: number }>`
|
const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>`
|
||||||
width: ${(props) => props.size}px;
|
width: ${(props) => props.size}px;
|
||||||
height: ${(props) => props.size}px;
|
height: ${(props) => props.size}px;
|
||||||
border-radius: 50%;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
|
background-color: transparent;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
cursor: pointer;
|
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?: number;
|
||||||
style?: React.CSSProperties;
|
style?: React.CSSProperties;
|
||||||
onClick?: () => void;
|
onClick?: React.MouseEventHandler<HTMLDivElement> | null;
|
||||||
popoutPlacement?: "left" | "right" | "top" | "bottom";
|
popoutPlacement?: "left" | "right" | "top" | "bottom";
|
||||||
|
presence?: Presence;
|
||||||
|
statusDotStyle?: {
|
||||||
|
width?: number;
|
||||||
|
height?: number;
|
||||||
|
};
|
||||||
|
showPresence?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Avatar(props: Props) {
|
function Avatar(props: Props) {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
const popoutContext = React.useContext(PopoutContext);
|
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
const user = props.user ?? app.account;
|
const user = props.user ?? app.account;
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
|
||||||
const openPopout = () => {
|
// if onClick is null, use a div. if we pass a function, use yes. otherwise use FloatingTrigger
|
||||||
if (!ref.current) return;
|
const Base = props.onClick === null ? "div" : props.onClick ? Yes(props.onClick) : FloatingTrigger;
|
||||||
|
|
||||||
const rect = ref.current.getBoundingClientRect();
|
|
||||||
if (!rect) return;
|
|
||||||
|
|
||||||
popoutContext.open({
|
|
||||||
element: <UserProfilePopout user={user} />,
|
|
||||||
position: rect,
|
|
||||||
placement: props.popoutPlacement,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper
|
<Floating
|
||||||
size={props.size ?? 32}
|
placement="right-start"
|
||||||
style={props.style}
|
type="userPopout"
|
||||||
onClick={(e) => {
|
props={{
|
||||||
e.preventDefault();
|
user: user as unknown as User,
|
||||||
e.stopPropagation();
|
|
||||||
props.onClick ? props.onClick() : openPopout();
|
|
||||||
}}
|
}}
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
<img src={user.avatarUrl} width={props.size ?? 32} height={props.size ?? 32} loading="eager" />
|
<Base>
|
||||||
</Wrapper>
|
<Wrapper size={props.size ?? 32} style={props.style} ref={ref} hasClick={props.onClick !== null}>
|
||||||
|
<img
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
}}
|
||||||
|
src={user.avatarUrl}
|
||||||
|
width={props.size ?? 32}
|
||||||
|
height={props.size ?? 32}
|
||||||
|
loading="eager"
|
||||||
|
/>
|
||||||
|
{props.showPresence && (
|
||||||
|
<StatusDot
|
||||||
|
color={app.theme.getStatusColor(props.presence?.status ?? PresenceUpdateStatus.Offline)}
|
||||||
|
{...props.statusDotStyle}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
</Base>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,94 +1,102 @@
|
|||||||
import styled from "styled-components";
|
// Adapted from https://github.com/revoltchat/components/blob/master/src/components/design/atoms/inputs/Button.tsx
|
||||||
|
|
||||||
interface Props {
|
import styled, { css } from "styled-components";
|
||||||
variant?: "primary" | "secondary" | "danger" | "success" | "warning";
|
|
||||||
outlined?: boolean;
|
export interface Props {
|
||||||
|
readonly compact?: boolean | "icon";
|
||||||
|
palette?: "primary" | "secondary" | "success" | "warning" | "danger" | "accent" | "link";
|
||||||
|
size?: "small" | "medium" | "large";
|
||||||
|
grow?: boolean;
|
||||||
|
readonly disabled?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default styled.button<Props>`
|
export default styled.button<Props>`
|
||||||
background: ${(props) => {
|
|
||||||
if (props.outlined) return "transparent";
|
|
||||||
switch (props.variant) {
|
|
||||||
case "primary":
|
|
||||||
return "var(--primary)";
|
|
||||||
case "secondary":
|
|
||||||
return "var(--secondary)";
|
|
||||||
case "danger":
|
|
||||||
return "var(--danger)";
|
|
||||||
case "success":
|
|
||||||
return "var(--success)";
|
|
||||||
case "warning":
|
|
||||||
return "var(--warning)";
|
|
||||||
default:
|
|
||||||
return "var(--primary)";
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
|
|
||||||
border: ${(props) => {
|
|
||||||
if (!props.outlined) return "none";
|
|
||||||
switch (props.variant) {
|
|
||||||
case "primary":
|
|
||||||
return "1px solid var(--primary)";
|
|
||||||
case "secondary":
|
|
||||||
return "1px solid var(--secondary)";
|
|
||||||
case "danger":
|
|
||||||
return "1px solid var(--danger)";
|
|
||||||
case "success":
|
|
||||||
return "1px solid var(--success)";
|
|
||||||
case "warning":
|
|
||||||
return "1px solid var(--warning)";
|
|
||||||
default:
|
|
||||||
return "1px solid var(--primary)";
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
|
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
padding: 8px 16px;
|
padding: 2px 16px;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
font-size: 13px;
|
font-size: 14px;
|
||||||
font-weight: var(--font-weight-medium);
|
font-weight: var(--font-weight-medium);
|
||||||
cursor: pointer;
|
|
||||||
outline: none;
|
outline: none;
|
||||||
|
border: none;
|
||||||
transition: background 0.2s ease-in-out;
|
transition: background 0.2s ease-in-out;
|
||||||
pointer-events: ${(props) => (props.disabled ? "none" : null)};
|
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||||
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
|
opacity: ${(props) => (props.disabled ? 0.5 : 1)}
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
height: ${(props) => {
|
||||||
|
switch (props.size) {
|
||||||
|
default:
|
||||||
|
case "small":
|
||||||
|
return "32px;";
|
||||||
|
case "medium":
|
||||||
|
return "40px";
|
||||||
|
case "large":
|
||||||
|
return "45px";
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
min-height: ${(props) => {
|
||||||
|
switch (props.size) {
|
||||||
|
default:
|
||||||
|
case "small":
|
||||||
|
return "32px;";
|
||||||
|
case "medium":
|
||||||
|
return "40px";
|
||||||
|
case "large":
|
||||||
|
return "45px";
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
min-width: ${(props) => {
|
||||||
|
if (props.grow) return "auto";
|
||||||
|
switch (props.size) {
|
||||||
|
default:
|
||||||
|
case "small":
|
||||||
|
return "96px";
|
||||||
|
case "medium":
|
||||||
|
return "96px";
|
||||||
|
case "large":
|
||||||
|
return "130px";
|
||||||
|
}
|
||||||
|
}};
|
||||||
|
|
||||||
&:hover {
|
${(props) => {
|
||||||
background: ${(props) => {
|
if (!props.palette) props.palette = "primary";
|
||||||
switch (props.variant) {
|
switch (props.palette) {
|
||||||
case "primary":
|
case "primary":
|
||||||
return "var(--primary-light)";
|
case "secondary":
|
||||||
case "secondary":
|
case "success":
|
||||||
return "var(--secondary-light)";
|
case "warning":
|
||||||
case "danger":
|
case "danger":
|
||||||
return "var(--danger-light)";
|
case "accent":
|
||||||
case "success":
|
return css`
|
||||||
return "var(--success-light)";
|
background: var(--${props.palette});
|
||||||
case "warning":
|
|
||||||
return "var(--warning-light)";
|
|
||||||
default:
|
|
||||||
return "var(--primary-light)";
|
|
||||||
}
|
|
||||||
}};
|
|
||||||
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
&:hover {
|
||||||
background: ${(props) => {
|
filter: brightness(1.2);
|
||||||
switch (props.variant) {
|
}
|
||||||
case "primary":
|
|
||||||
return "var(--primary-dark)";
|
&:active {
|
||||||
case "secondary":
|
filter: brightness(0.8);
|
||||||
return "var(--secondary-dark)";
|
}
|
||||||
case "danger":
|
|
||||||
return "var(--danger-dark)";
|
&:disabled {
|
||||||
case "success":
|
filter: brightness(0.7);
|
||||||
return "var(--success-dark)";
|
}
|
||||||
case "warning":
|
`;
|
||||||
return "var(--warning-dark)";
|
case "link":
|
||||||
default:
|
return css`
|
||||||
return "var(--primary-dark)";
|
background: transparent;
|
||||||
}
|
|
||||||
}};
|
&:hover {
|
||||||
}
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
filter: brightness(0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
filter: brightness(0.7);
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}}
|
||||||
`;
|
`;
|
||||||
|
@ -1,13 +1,11 @@
|
|||||||
import { StackedModalProps, useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { ComponentType } from "react";
|
import React, { useEffect } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
|
||||||
import { useAppStore } from "../stores/AppStore";
|
import { useAppStore } from "../stores/AppStore";
|
||||||
import { IContextMenuItem } from "./ContextMenuItem";
|
import Icon, { IconProps } from "./Icon";
|
||||||
import Icon from "./Icon";
|
|
||||||
import { SectionHeader } from "./SectionHeader";
|
import { SectionHeader } from "./SectionHeader";
|
||||||
import LeaveServerModal from "./modals/LeaveServerModal";
|
import Floating from "./floating/Floating";
|
||||||
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
const Wrapper = styled(SectionHeader)`
|
const Wrapper = styled(SectionHeader)`
|
||||||
background-color: var(--background-secondary);
|
background-color: var(--background-secondary);
|
||||||
@ -24,64 +22,23 @@ const HeaderText = styled.header`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
function ChannelHeader() {
|
function ChannelHeader() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
|
||||||
const { openModal } = useModals();
|
|
||||||
|
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([]);
|
const [isOpen, setOpen] = React.useState(false);
|
||||||
|
const [icon, setIcon] = React.useState<IconProps["icon"]>("mdiChevronDown");
|
||||||
|
|
||||||
React.useEffect(() => {
|
const onOpenChange = (open: boolean) => {
|
||||||
if (app.activeGuild && app.activeGuild.ownerId !== app.account?.id) {
|
setOpen(open);
|
||||||
setContextMenuItems([
|
};
|
||||||
{
|
|
||||||
label: "Leave Server",
|
|
||||||
color: "var(--danger)",
|
|
||||||
onClick: async () => {
|
|
||||||
openModal(LeaveServerModal as ComponentType<StackedModalProps>, {
|
|
||||||
guild: app.activeGuild,
|
|
||||||
});
|
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiLocationExit",
|
|
||||||
color: "var(--danger)",
|
|
||||||
},
|
|
||||||
hover: {
|
|
||||||
color: "var(--text)",
|
|
||||||
backgroundColor: "var(--danger)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
} else {
|
|
||||||
setContextMenuItems([]);
|
|
||||||
}
|
|
||||||
}, [app.activeGuild]);
|
|
||||||
|
|
||||||
function openMenu(e: React.MouseEvent<HTMLDivElement>) {
|
useEffect(() => {
|
||||||
e.stopPropagation();
|
if (isOpen) setIcon("mdiClose");
|
||||||
|
else setIcon("mdiChevronDown");
|
||||||
if (contextMenu.visible) {
|
}, [isOpen]);
|
||||||
// "toggles" the menu
|
|
||||||
contextMenu.close();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const horizontalPadding = 5;
|
|
||||||
const verticalPadding = 10;
|
|
||||||
contextMenu.open({
|
|
||||||
position: {
|
|
||||||
x: e.currentTarget.offsetLeft + horizontalPadding, // centers the menu under the header
|
|
||||||
y: e.currentTarget.offsetHeight + horizontalPadding, // add a slight gap between the header and the menu
|
|
||||||
},
|
|
||||||
items: contextMenuItems,
|
|
||||||
style: {
|
|
||||||
width: e.currentTarget.clientWidth - verticalPadding, // adds "margin" to the left and right of the menu
|
|
||||||
boxSizing: "border-box",
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (app.activeGuildId === "@me") {
|
if (app.activeGuildId === "@me") {
|
||||||
return (
|
return (
|
||||||
@ -101,10 +58,14 @@ function ChannelHeader() {
|
|||||||
if (!app.activeGuild) return null;
|
if (!app.activeGuild) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper onClick={openMenu}>
|
<Floating type="guild" open={isOpen} onOpenChange={onOpenChange} props={{ guild: app.activeGuild! }}>
|
||||||
<HeaderText>{app.activeGuild.name}</HeaderText>
|
<FloatingTrigger>
|
||||||
<Icon icon="mdiChevronDown" size="20px" color="var(--text)" />
|
<Wrapper>
|
||||||
</Wrapper>
|
<HeaderText>{app.activeGuild.name}</HeaderText>
|
||||||
|
<Icon icon={icon} size="20px" color="var(--text)" />
|
||||||
|
</Wrapper>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,14 +10,10 @@ const Container = styled.div`
|
|||||||
flex: 1;
|
flex: 1;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
export function EmptyChannelList() {
|
|
||||||
return <Container></Container>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ChannelList() {
|
function ChannelList() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
if (!app.activeGuild || !app.activeChannel) return null;
|
if (!app.activeGuild || !app.activeChannel) return <Container />;
|
||||||
const { channels } = app.activeGuild;
|
const { channels } = app.activeGuild;
|
||||||
|
|
||||||
const rowRenderer = ({ index, key, style }: ListRowProps) => {
|
const rowRenderer = ({ index, key, style }: ListRowProps) => {
|
||||||
@ -25,6 +21,7 @@ function ChannelList() {
|
|||||||
|
|
||||||
const active = app.activeChannelId === item.id;
|
const active = app.activeChannelId === item.id;
|
||||||
const isCategory = item.type === ChannelType.GuildCategory;
|
const isCategory = item.type === ChannelType.GuildCategory;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div style={style}>
|
<div style={style}>
|
||||||
<ChannelListItem key={key} isCategory={isCategory} active={active} channel={item} />
|
<ChannelListItem key={key} isCategory={isCategory} active={active} channel={item} />
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
import React, { useContext, useEffect } from "react";
|
||||||
import React from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import Channel from "../../stores/objects/Channel";
|
import Channel from "../../stores/objects/Channel";
|
||||||
import { IContextMenuItem } from "../ContextMenuItem";
|
import { Permissions } from "../../utils/Permissions";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import CreateInviteModal from "../modals/CreateInviteModal";
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const ListItem = styled.div<{ isCategory?: boolean }>`
|
const ListItem = styled.div<{ isCategory?: boolean }>`
|
||||||
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
||||||
@ -19,19 +21,21 @@ const Wrapper = styled.div<{ isCategory?: boolean; active?: boolean }>`
|
|||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
padding: 0 8px;
|
padding: ${(props) => (props.isCategory ? "0 8px 0 8px" : "0 16px")};
|
||||||
background-color: ${(props) => (props.active ? "var(--background-primary-alt)" : "transparent")};
|
background-color: ${(props) => (props.active ? "var(--background-primary-alt)" : "transparent")};
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-primary-alt);
|
background-color: ${(props) => (props.isCategory ? "transparent" : "var(--background-primary-alt)")};
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Text = styled.span<{ isCategory?: boolean }>`
|
const Text = styled.span<{ isCategory?: boolean; hovered?: boolean }>`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--text-secondary);
|
color: ${(props) => (props.isCategory && props.hovered ? "var(--text)" : "var(--text-secondary)")};
|
||||||
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -41,33 +45,22 @@ interface Props {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function ChannelListItem({ channel, isCategory, active }: Props) {
|
function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||||
|
const app = useAppStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const contextMenu = useContext(ContextMenuContext);
|
||||||
|
|
||||||
const { openModal } = useModals();
|
const [wrapperHovered, setWrapperHovered] = React.useState(false);
|
||||||
|
const [createChannelHovered, setCreateChannelHovered] = React.useState(false);
|
||||||
|
const [createChannelDown, setChannelCreateDown] = React.useState(false);
|
||||||
|
const [hasCreateChannelPermission, setHasCreateChannelPermission] = React.useState(false);
|
||||||
|
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
useEffect(() => {
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
if (!isCategory) return;
|
||||||
{
|
|
||||||
index: 1,
|
const permission = Permissions.getPermission(app.account!.id, channel.guild, channel);
|
||||||
label: "Copy Channel ID",
|
const hasPermission = permission.has("MANAGE_CHANNELS");
|
||||||
onClick: () => {
|
setHasCreateChannelPermission(hasPermission);
|
||||||
navigator.clipboard.writeText(channel.id);
|
}, [channel]);
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiIdentifier",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
label: "Create Channel Invite",
|
|
||||||
onClick: () => {
|
|
||||||
openModal(CreateInviteModal, { guild_id: channel.guildId!, channel_id: channel.id });
|
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiAccountPlus",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -79,20 +72,92 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
|||||||
|
|
||||||
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
||||||
}}
|
}}
|
||||||
onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}
|
ref={contextMenu.setReferenceElement}
|
||||||
|
onContextMenu={(e) => contextMenu.onContextMenu(e, { type: "channel", channel })}
|
||||||
>
|
>
|
||||||
<Wrapper isCategory={isCategory} active={active}>
|
<Wrapper
|
||||||
{channel.channelIcon && (
|
isCategory={isCategory}
|
||||||
<Icon
|
active={active}
|
||||||
icon={channel.channelIcon}
|
onMouseOver={() => setWrapperHovered(true)}
|
||||||
size="16px"
|
onMouseOut={() => setWrapperHovered(false)}
|
||||||
style={{
|
>
|
||||||
marginRight: "8px",
|
<div
|
||||||
|
style={{
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "row",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{channel.channelIcon && !isCategory && (
|
||||||
|
<Icon
|
||||||
|
icon={channel.channelIcon}
|
||||||
|
size="16px"
|
||||||
|
style={{
|
||||||
|
marginRight: "8px",
|
||||||
|
}}
|
||||||
|
color="var(--text-secondary)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isCategory && (
|
||||||
|
<Icon
|
||||||
|
icon="mdiChevronDown"
|
||||||
|
size="12px"
|
||||||
|
color={wrapperHovered ? "var(--text)" : "var(--text-secondary)"}
|
||||||
|
style={{
|
||||||
|
marginRight: "8px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Text isCategory={isCategory} hovered={wrapperHovered}>
|
||||||
|
{channel.name}
|
||||||
|
</Text>
|
||||||
|
</div>
|
||||||
|
{isCategory && hasCreateChannelPermission && (
|
||||||
|
<Floating
|
||||||
|
placement="top"
|
||||||
|
type="tooltip"
|
||||||
|
offset={10}
|
||||||
|
props={{
|
||||||
|
content: <span>Create Channel</span>,
|
||||||
}}
|
}}
|
||||||
color="var(--text-secondary)"
|
>
|
||||||
/>
|
<FloatingTrigger>
|
||||||
|
<span
|
||||||
|
onMouseOver={() => setCreateChannelHovered(true)}
|
||||||
|
onMouseOut={() => setCreateChannelHovered(false)}
|
||||||
|
onMouseDown={() => setChannelCreateDown(true)}
|
||||||
|
onMouseUp={() => setChannelCreateDown(false)}
|
||||||
|
onClick={() => {
|
||||||
|
if (!channel.guild) {
|
||||||
|
console.warn("No guild found for channel", channel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
modalController.push({
|
||||||
|
type: "create_channel",
|
||||||
|
guild: channel.guild,
|
||||||
|
category: channel,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
icon="mdiPlus"
|
||||||
|
size="18px"
|
||||||
|
style={{
|
||||||
|
marginLeft: "auto",
|
||||||
|
}}
|
||||||
|
color={
|
||||||
|
createChannelDown
|
||||||
|
? "var(--text-header)"
|
||||||
|
: createChannelHovered
|
||||||
|
? "var(--text)"
|
||||||
|
: "var(--text-secondary)"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
)}
|
)}
|
||||||
<Text isCategory={isCategory}>{channel.name}</Text>
|
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
);
|
);
|
||||||
|
@ -1,8 +1,7 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { useAppStore } from "../stores/AppStore";
|
|
||||||
import ChannelHeader from "./ChannelHeader";
|
import ChannelHeader from "./ChannelHeader";
|
||||||
import ChannelList, { EmptyChannelList } from "./ChannelList/ChannelList";
|
import ChannelList from "./ChannelList/ChannelList";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import UserPanel from "./UserPanel";
|
import UserPanel from "./UserPanel";
|
||||||
|
|
||||||
@ -18,13 +17,11 @@ const Wrapper = styled(Container)`
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
function ChannelSidebar() {
|
function ChannelSidebar() {
|
||||||
const app = useAppStore();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Wrapper>
|
<Wrapper>
|
||||||
{/* TODO: replace with dm search if no guild */}
|
{/* TODO: replace with dm search if no guild */}
|
||||||
<ChannelHeader />
|
<ChannelHeader />
|
||||||
{app.activeGuild ? <ChannelList /> : <EmptyChannelList />}
|
<ChannelList />
|
||||||
<UserPanel />
|
<UserPanel />
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
@ -3,7 +3,8 @@
|
|||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Tooltip from "./Tooltip";
|
import Floating from "./floating/Floating";
|
||||||
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
const Actions = styled.div`
|
const Actions = styled.div`
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -57,9 +58,17 @@ function CodeBlock(props: Props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Actions>
|
<Actions>
|
||||||
<Tooltip title="Copy to Clipboard" placement="top">
|
<Floating
|
||||||
<a onClick={onCopy}>{text}</a>
|
placement="top"
|
||||||
</Tooltip>
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: <span>"Copy to Clipboard</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<a onClick={onCopy}>{text}</a>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
</Actions>
|
</Actions>
|
||||||
{props.children}
|
{props.children}
|
||||||
</pre>
|
</pre>
|
||||||
|
@ -1,56 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import { ContextMenuOpenProps } from "../contexts/ContextMenuContext";
|
|
||||||
import Container from "./Container";
|
|
||||||
import ContextMenuItem, { IContextMenuItem } from "./ContextMenuItem";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: (props: ContextMenuOpenProps) => void;
|
|
||||||
close: () => void;
|
|
||||||
visible: boolean;
|
|
||||||
position: {
|
|
||||||
x: number;
|
|
||||||
y: number;
|
|
||||||
};
|
|
||||||
items: IContextMenuItem[];
|
|
||||||
style?: React.CSSProperties;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenu({ position, close, items, style }: Props) {
|
|
||||||
// Close the context menu when the user clicks outside of it
|
|
||||||
React.useEffect(() => {
|
|
||||||
const listener = () => {
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", listener);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("click", listener);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container
|
|
||||||
onBlur={close}
|
|
||||||
style={{
|
|
||||||
...style,
|
|
||||||
position: "absolute",
|
|
||||||
minWidth: "10vw",
|
|
||||||
// maxWidth: "20vw",
|
|
||||||
borderRadius: 4,
|
|
||||||
zIndex: 4,
|
|
||||||
padding: "6px 8px",
|
|
||||||
top: position.y,
|
|
||||||
left: position.x,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{items
|
|
||||||
.filter((a) => a.visible !== false)
|
|
||||||
.sort((a, b) => (a.index ?? 0) - (b.index ?? 0))
|
|
||||||
.map((item, index) => {
|
|
||||||
return <ContextMenuItem key={index} item={item} close={close} index={index} />;
|
|
||||||
})}
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContextMenu;
|
|
@ -1,84 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import Container from "./Container";
|
|
||||||
import Icon, { IconProps } from "./Icon";
|
|
||||||
|
|
||||||
export interface IContextMenuItem {
|
|
||||||
index?: number;
|
|
||||||
label: string;
|
|
||||||
color?: string;
|
|
||||||
onClick: React.MouseEventHandler<HTMLDivElement>;
|
|
||||||
iconProps?: IconProps;
|
|
||||||
hover?: {
|
|
||||||
color?: string;
|
|
||||||
backgroundColor?: string;
|
|
||||||
};
|
|
||||||
visible?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ContextMenuContainer = styled(Container)`
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 32px;
|
|
||||||
cursor: pointer;
|
|
||||||
`;
|
|
||||||
|
|
||||||
// we handle the hover state ourselves to prevent "lag" with the icon color
|
|
||||||
const Wrapper = styled(Container)<{ hover?: IContextMenuItem["hover"]; hovered?: boolean }>`
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 6px 8px;
|
|
||||||
flex: 1 1 auto;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
color: ${(props) => (props.hovered ? props.hover?.color ?? "var(--text)" : props.color ?? "var(--text)")};
|
|
||||||
background-color: ${(props) => (props.hovered ? props.hover?.backgroundColor ?? "var(--primary)" : "transparent")};
|
|
||||||
`;
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
item: IContextMenuItem;
|
|
||||||
index: number;
|
|
||||||
close: () => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
function ContextMenuItem({ item, index, close }: Props) {
|
|
||||||
const [isHovered, setIsHovered] = React.useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ContextMenuContainer
|
|
||||||
key={index}
|
|
||||||
onClick={async (e) => {
|
|
||||||
await item.onClick(e);
|
|
||||||
close();
|
|
||||||
}}
|
|
||||||
onMouseEnter={() => setIsHovered(true)}
|
|
||||||
onMouseLeave={() => setIsHovered(false)}
|
|
||||||
>
|
|
||||||
<Wrapper hover={item.hover} hovered={isHovered} color={item.color}>
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
// color: item.color ?? "var(--text)",
|
|
||||||
fontWeight: 500,
|
|
||||||
fontSize: "14px",
|
|
||||||
whiteSpace: "nowrap",
|
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{item.label}
|
|
||||||
</div>
|
|
||||||
{item.iconProps && (
|
|
||||||
<Icon
|
|
||||||
{...item.iconProps}
|
|
||||||
size={item.iconProps.size ?? "20px"}
|
|
||||||
color={isHovered ? item.hover?.color ?? "var(--text)" : item.iconProps.color ?? "var(--text)"}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Wrapper>
|
|
||||||
</ContextMenuContainer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ContextMenuItem;
|
|
@ -7,7 +7,7 @@
|
|||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
background: var(--background-secondary-alt);
|
background: var(--background-secondary);
|
||||||
border: none;
|
border: none;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
outline: none;
|
outline: none;
|
||||||
|
@ -3,7 +3,7 @@ import styled from "styled-components";
|
|||||||
// TODO: migrate some things from AuthComponents
|
// TODO: migrate some things from AuthComponents
|
||||||
|
|
||||||
export const InputSelect = styled.select`
|
export const InputSelect = styled.select`
|
||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary);
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
outline: none;
|
outline: none;
|
||||||
border: 1px solid transparent;
|
border: 1px solid transparent;
|
||||||
|
@ -1,19 +1,18 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import { CDNRoutes, ChannelType, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
|
import { CDNRoutes, ChannelType, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||||
|
import useLogger from "../hooks/useLogger";
|
||||||
import { useAppStore } from "../stores/AppStore";
|
import { useAppStore } from "../stores/AppStore";
|
||||||
import Guild from "../stores/objects/Guild";
|
import Guild from "../stores/objects/Guild";
|
||||||
import { Permissions } from "../utils/Permissions";
|
import { Permissions } from "../utils/Permissions";
|
||||||
import REST from "../utils/REST";
|
import REST from "../utils/REST";
|
||||||
import Container from "./Container";
|
import Container from "./Container";
|
||||||
import { IContextMenuItem } from "./ContextMenuItem";
|
|
||||||
import SidebarPill, { PillType } from "./SidebarPill";
|
import SidebarPill, { PillType } from "./SidebarPill";
|
||||||
import Tooltip from "./Tooltip";
|
import Floating from "./floating/Floating";
|
||||||
import CreateInviteModal from "./modals/CreateInviteModal";
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
export const GuildSidebarListItem = styled.div`
|
export const GuildSidebarListItem = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
@ -31,7 +30,9 @@ const Wrapper = styled(Container)<{ active?: boolean; hasImage?: boolean }>`
|
|||||||
border-radius: ${(props) => (props.active ? "30%" : "50%")};
|
border-radius: ${(props) => (props.active ? "30%" : "50%")};
|
||||||
background-color: ${(props) =>
|
background-color: ${(props) =>
|
||||||
props.hasImage ? "transparent" : props.active ? "var(--primary)" : "var(--background-secondary)"};
|
props.hasImage ? "transparent" : props.active ? "var(--primary)" : "var(--background-secondary)"};
|
||||||
transition: border-radius 0.2s ease, background-color 0.2s ease;
|
transition:
|
||||||
|
border-radius 0.2s ease,
|
||||||
|
background-color 0.2s ease;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-radius: 30%;
|
border-radius: 30%;
|
||||||
@ -48,37 +49,14 @@ interface Props {
|
|||||||
* List item for use in the guild sidebar
|
* List item for use in the guild sidebar
|
||||||
*/
|
*/
|
||||||
function GuildItem({ guild, active }: Props) {
|
function GuildItem({ guild, active }: Props) {
|
||||||
|
const logger = useLogger("GuildItem");
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { openModal } = useModals();
|
const contextMenu = useContext(ContextMenuContext);
|
||||||
|
|
||||||
const [pillType, setPillType] = React.useState<PillType>("none");
|
const [pillType, setPillType] = React.useState<PillType>("none");
|
||||||
const [isHovered, setHovered] = React.useState(false);
|
const [isHovered, setHovered] = React.useState(false);
|
||||||
|
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
|
||||||
{
|
|
||||||
index: 1,
|
|
||||||
label: "Copy Guild ID",
|
|
||||||
onClick: () => {
|
|
||||||
navigator.clipboard.writeText(guild.id);
|
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiIdentifier",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
index: 0,
|
|
||||||
label: "Create Invite",
|
|
||||||
onClick: () => {
|
|
||||||
openModal(CreateInviteModal, { guild_id: guild.id });
|
|
||||||
},
|
|
||||||
iconProps: {
|
|
||||||
icon: "mdiAccountPlus",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
]);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (app.activeChannelId && app.activeGuildId === guild.id) return setPillType("active");
|
if (app.activeChannelId && app.activeGuildId === guild.id) return setPillType("active");
|
||||||
else if (isHovered) return setPillType("hover");
|
else if (isHovered) return setPillType("hover");
|
||||||
@ -95,36 +73,48 @@ function GuildItem({ guild, active }: Props) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GuildSidebarListItem onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}>
|
<GuildSidebarListItem
|
||||||
|
ref={contextMenu.setReferenceElement}
|
||||||
|
onContextMenu={(e) => contextMenu.onContextMenu(e, { type: "guild", guild })}
|
||||||
|
>
|
||||||
<SidebarPill type={pillType} />
|
<SidebarPill type={pillType} />
|
||||||
<Tooltip title={guild.name} placement="right">
|
<Floating
|
||||||
<Wrapper
|
placement="right"
|
||||||
onClick={doNavigate}
|
type="tooltip"
|
||||||
active={active}
|
offset={20}
|
||||||
hasImage={!!guild?.icon}
|
props={{
|
||||||
onMouseEnter={() => setHovered(true)}
|
content: <span>{guild.name}</span>,
|
||||||
onMouseLeave={() => setHovered(false)}
|
}}
|
||||||
>
|
>
|
||||||
{guild.icon ? (
|
<FloatingTrigger>
|
||||||
<img
|
<Wrapper
|
||||||
src={REST.makeCDNUrl(CDNRoutes.guildIcon(guild.id, guild?.icon, ImageFormat.PNG))}
|
onClick={doNavigate}
|
||||||
width={48}
|
active={active}
|
||||||
height={48}
|
hasImage={!!guild?.icon}
|
||||||
loading="lazy"
|
onMouseEnter={() => setHovered(true)}
|
||||||
/>
|
onMouseLeave={() => setHovered(false)}
|
||||||
) : (
|
>
|
||||||
<span
|
{guild.icon ? (
|
||||||
style={{
|
<img
|
||||||
fontSize: "18px",
|
src={REST.makeCDNUrl(CDNRoutes.guildIcon(guild.id, guild?.icon, ImageFormat.PNG))}
|
||||||
fontWeight: "bold",
|
width={48}
|
||||||
cursor: "pointer",
|
height={48}
|
||||||
}}
|
loading="lazy"
|
||||||
>
|
/>
|
||||||
{guild?.acronym}
|
) : (
|
||||||
</span>
|
<span
|
||||||
)}
|
style={{
|
||||||
</Wrapper>
|
fontSize: "18px",
|
||||||
</Tooltip>
|
fontWeight: "bold",
|
||||||
|
cursor: "pointer",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{guild?.acronym}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</Wrapper>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
</GuildSidebarListItem>
|
</GuildSidebarListItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import { AutoSizer, List, ListRowProps } from "react-virtualized";
|
import { AutoSizer, List, ListRowProps } from "react-virtualized";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { modalController } from "../controllers/modals";
|
||||||
import { useAppStore } from "../stores/AppStore";
|
import { useAppStore } from "../stores/AppStore";
|
||||||
import GuildItem, { GuildSidebarListItem } from "./GuildItem";
|
import GuildItem, { GuildSidebarListItem } from "./GuildItem";
|
||||||
import SidebarAction from "./SidebarAction";
|
import SidebarAction from "./SidebarAction";
|
||||||
import AddServerModal from "./modals/AddServerModal";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -39,7 +38,6 @@ const Divider = styled.div`
|
|||||||
function GuildSidebar() {
|
function GuildSidebar() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
|
|
||||||
const { openModal } = useModals();
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const { all } = app.guilds;
|
const { all } = app.guilds;
|
||||||
const itemCount = all.length + 3; // add the home button, divider, and add server button
|
const itemCount = all.length + 3; // add the home button, divider, and add server button
|
||||||
@ -80,7 +78,9 @@ function GuildSidebar() {
|
|||||||
color: "var(--success)",
|
color: "var(--success)",
|
||||||
}}
|
}}
|
||||||
action={() => {
|
action={() => {
|
||||||
openModal(AddServerModal);
|
modalController.push({
|
||||||
|
type: "add_server",
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
margin={false}
|
margin={false}
|
||||||
disablePill
|
disablePill
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import React from "react";
|
import React, { ReactNode } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Icon from "./Icon";
|
import Icon from "./Icon";
|
||||||
|
|
||||||
@ -35,7 +35,7 @@ const Item = styled.span`
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
name: string;
|
name: string;
|
||||||
items: string[];
|
items: ReactNode[];
|
||||||
}
|
}
|
||||||
|
|
||||||
function ListSection(props: Props) {
|
function ListSection(props: Props) {
|
||||||
@ -49,9 +49,10 @@ function ListSection(props: Props) {
|
|||||||
{props.name}
|
{props.name}
|
||||||
</Title>
|
</Title>
|
||||||
<Wrapper open={open}>
|
<Wrapper open={open}>
|
||||||
{props.items.map((item, i) => (
|
{/* {props.items.map((item, i) => (
|
||||||
<Item key={i}>{item}</Item>
|
<Item key={i}>{item}</Item>
|
||||||
))}
|
))} */}
|
||||||
|
{...props.items}
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { invoke } from "@tauri-apps/api";
|
import { invoke } from "@tauri-apps/api/core";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import LoadingPage from "../pages/LoadingPage";
|
import LoadingPage from "../pages/LoadingPage";
|
||||||
|
@ -5,6 +5,7 @@ import styled from "styled-components";
|
|||||||
import { useAppStore } from "../../stores/AppStore";
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import GuildMemberListStore from "../../stores/GuildMemberListStore";
|
import GuildMemberListStore from "../../stores/GuildMemberListStore";
|
||||||
import ListSection from "../ListSection";
|
import ListSection from "../ListSection";
|
||||||
|
import MemberListItem from "./MemberListItem";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -18,13 +19,6 @@ const Container = styled.div`
|
|||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.aside`
|
|
||||||
justify-content: center;
|
|
||||||
min-width: 240px;
|
|
||||||
max-height: 100%;
|
|
||||||
display: flex;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const List = styled.ul`
|
const List = styled.ul`
|
||||||
padding: 0;
|
padding: 0;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -61,11 +55,11 @@ function MemberList() {
|
|||||||
<ListSection
|
<ListSection
|
||||||
key={i}
|
key={i}
|
||||||
name={category.name}
|
name={category.name}
|
||||||
items={
|
items={category.items.map((x) => (
|
||||||
category.items.map((x) => x.nick ?? x.user?.username).filter((x) => x) as string[]
|
<MemberListItem item={x} />
|
||||||
}
|
))}
|
||||||
/>
|
/>
|
||||||
))
|
))
|
||||||
: null}
|
: null}
|
||||||
</List>
|
</List>
|
||||||
</Container>
|
</Container>
|
||||||
|
@ -1,68 +1,100 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import React from "react";
|
import { observer } from "mobx-react-lite";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useContext } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import GuildMember from "../../stores/objects/GuildMember";
|
import GuildMember from "../../stores/objects/GuildMember";
|
||||||
import { IContextMenuItem } from "../ContextMenuItem";
|
import Avatar from "../Avatar";
|
||||||
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const ListItem = styled.div<{ isCategory?: boolean }>`
|
const ListItem = styled(FloatingTrigger)<{ isCategory?: boolean }>`
|
||||||
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
padding: ${(props) => (props.isCategory ? "16px 8px 0 0" : "1px 8px 0 0")};
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Wrapper = styled.div<{ isCategory?: boolean }>`
|
const Container = styled.div`
|
||||||
margin-left: ${(props) => (props.isCategory ? "0" : "8px")};
|
max-width: 224px;
|
||||||
height: ${(props) => (props.isCategory ? "28px" : "33px")};
|
|
||||||
border-radius: 4px;
|
|
||||||
align-items: center;
|
|
||||||
display: flex;
|
|
||||||
padding: 0 8px;
|
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: 1px 0;
|
||||||
|
border-radius: 4px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-primary-alt);
|
background-color: var(--background-primary-alt);
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Text = styled.span<{ isCategory?: boolean }>`
|
const Wrapper = styled.div<{ offline?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 4px;
|
||||||
|
height: 42px;
|
||||||
|
padding: 0 8px;
|
||||||
|
opacity: ${(props) => (props.offline ? 0.5 : 1)};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Text = styled.span<{ color?: string }>`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
font-weight: var(--font-weight-regular);
|
font-weight: var(--font-weight-regular);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
color: var(--text-secondary);
|
color: ${(props) => props.color ?? "var(--text-secondary)"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const TextWrapper = styled.div`
|
||||||
|
min-width: 0;
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AvatarWrapper = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
margin-right: 12px;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
item: string | GuildMember;
|
item: GuildMember;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MemberListItem({ item }: Props) {
|
function MemberListItem({ item }: Props) {
|
||||||
const navigate = useNavigate();
|
const app = useAppStore();
|
||||||
|
const presence = app.presences.get(item.user!.id);
|
||||||
const { openModal } = useModals();
|
const contextMenu = useContext(ContextMenuContext);
|
||||||
|
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<Floating
|
||||||
key={typeof item === "string" ? item : item.user?.id}
|
placement="right-start"
|
||||||
isCategory={typeof item === "string"}
|
type="userPopout"
|
||||||
// onClick={() => {
|
offset={20}
|
||||||
// // prevent navigating to non-text channels
|
props={{
|
||||||
// if (!channel.isTextChannel) return;
|
user: item.user!,
|
||||||
|
member: item,
|
||||||
// navigate(`/channels/${channel.guildId}/${channel.id}`);
|
}}
|
||||||
// }}
|
|
||||||
onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}
|
|
||||||
>
|
>
|
||||||
<Wrapper isCategory={typeof item === "string"}>
|
<ListItem
|
||||||
<Text isCategory={typeof item === "string"}>
|
key={item.user?.id}
|
||||||
{typeof item === "string" ? item : item.nick ?? item.user?.username}
|
ref={contextMenu.setReferenceElement}
|
||||||
</Text>
|
onContextMenu={(e) => contextMenu.onContextMenu(e, { type: "user", user: item.user!, member: item })}
|
||||||
</Wrapper>
|
>
|
||||||
</ListItem>
|
<Container>
|
||||||
|
<Wrapper offline={presence?.status === PresenceUpdateStatus.Offline}>
|
||||||
|
<AvatarWrapper>
|
||||||
|
<Avatar user={item.user!} size={32} presence={presence} showPresence onClick={null} />
|
||||||
|
</AvatarWrapper>
|
||||||
|
<TextWrapper>
|
||||||
|
<Text color={item.roleColor}>{item.nick ?? item.user?.username}</Text>
|
||||||
|
</TextWrapper>
|
||||||
|
</Wrapper>
|
||||||
|
</Container>
|
||||||
|
</ListItem>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default MemberListItem;
|
export default observer(MemberListItem);
|
||||||
|
@ -1,144 +0,0 @@
|
|||||||
import React from "react";
|
|
||||||
import Measure, { BoundingRect, ContentRect } from "react-measure";
|
|
||||||
import { PopoutOpenProps } from "../contexts/PopoutContext";
|
|
||||||
|
|
||||||
const OFFSET = 10;
|
|
||||||
|
|
||||||
function isRectZero(rect: BoundingRect) {
|
|
||||||
return (
|
|
||||||
rect.bottom === 0 &&
|
|
||||||
rect.left === 0 &&
|
|
||||||
rect.right === 0 &&
|
|
||||||
rect.top === 0 &&
|
|
||||||
rect.width === 0 &&
|
|
||||||
rect.height === 0
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
open: (props: PopoutOpenProps) => void;
|
|
||||||
close: () => void;
|
|
||||||
position: DOMRect;
|
|
||||||
element: React.ReactNode;
|
|
||||||
isOpen: boolean;
|
|
||||||
placement?: "left" | "right" | "top" | "bottom";
|
|
||||||
}
|
|
||||||
|
|
||||||
function PopoutRenderer({ position, element, placement, close }: Props) {
|
|
||||||
const [rect, setRect] = React.useState<ContentRect>({});
|
|
||||||
const [positionStyle, setPositionStyle] = React.useState<React.CSSProperties>({
|
|
||||||
visibility: "hidden",
|
|
||||||
});
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
const listener = () => {
|
|
||||||
close();
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("click", listener);
|
|
||||||
return () => {
|
|
||||||
document.removeEventListener("click", listener);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (rect.bounds && !isRectZero(rect.bounds)) {
|
|
||||||
switch (placement) {
|
|
||||||
default:
|
|
||||||
case "right": {
|
|
||||||
let x = position.left + position.width + OFFSET;
|
|
||||||
let y = position.top;
|
|
||||||
if (x + rect.bounds.width > window.innerWidth) {
|
|
||||||
x = position.left - rect.bounds.width - OFFSET;
|
|
||||||
}
|
|
||||||
if (y + rect.bounds.height > window.innerHeight) {
|
|
||||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
|
||||||
}
|
|
||||||
setPositionStyle({
|
|
||||||
visibility: "visible",
|
|
||||||
top: y,
|
|
||||||
left: x,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "left": {
|
|
||||||
let x = position.left - rect.bounds.width - OFFSET;
|
|
||||||
let y = position.top;
|
|
||||||
if (x < 0) {
|
|
||||||
x = position.left + position.width + OFFSET;
|
|
||||||
}
|
|
||||||
if (y + rect.bounds.height > window.innerHeight) {
|
|
||||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
|
||||||
}
|
|
||||||
setPositionStyle({
|
|
||||||
visibility: "visible",
|
|
||||||
top: y,
|
|
||||||
left: x,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "top": {
|
|
||||||
let x = position.left - position.width / 1;
|
|
||||||
let y = position.top - rect.bounds.height - OFFSET;
|
|
||||||
if (x + rect.bounds.width > window.innerWidth) {
|
|
||||||
x = window.innerWidth - rect.bounds.width - OFFSET;
|
|
||||||
}
|
|
||||||
if (y < 0) {
|
|
||||||
y = position.top + position.height + OFFSET;
|
|
||||||
}
|
|
||||||
setPositionStyle({
|
|
||||||
visibility: "visible",
|
|
||||||
top: y,
|
|
||||||
left: x,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case "bottom": {
|
|
||||||
let x = position.left - position.width / 1;
|
|
||||||
let y = position.top + position.height + OFFSET;
|
|
||||||
if (x + rect.bounds.width > window.innerWidth) {
|
|
||||||
x = window.innerWidth - rect.bounds.width - OFFSET;
|
|
||||||
}
|
|
||||||
if (y + rect.bounds.height > window.innerHeight) {
|
|
||||||
y = window.innerHeight - rect.bounds.height - OFFSET;
|
|
||||||
}
|
|
||||||
setPositionStyle({
|
|
||||||
visibility: "visible",
|
|
||||||
top: y,
|
|
||||||
left: x,
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [rect, element]);
|
|
||||||
|
|
||||||
const handleResize = (contentRect: ContentRect) => setRect(contentRect);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
onBlur={close}
|
|
||||||
style={{
|
|
||||||
position: "absolute",
|
|
||||||
zIndex: 9999,
|
|
||||||
...positionStyle,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Measure bounds onResize={handleResize}>
|
|
||||||
{({ measureRef }) => (
|
|
||||||
<div
|
|
||||||
style={{
|
|
||||||
width: "fit-content",
|
|
||||||
height: "fit-content",
|
|
||||||
}}
|
|
||||||
ref={measureRef}
|
|
||||||
>
|
|
||||||
{element}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</Measure>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PopoutRenderer;
|
|
@ -4,7 +4,8 @@ import Container from "./Container";
|
|||||||
import { GuildSidebarListItem } from "./GuildItem";
|
import { GuildSidebarListItem } from "./GuildItem";
|
||||||
import Icon, { IconProps } from "./Icon";
|
import Icon, { IconProps } from "./Icon";
|
||||||
import SidebarPill, { PillType } from "./SidebarPill";
|
import SidebarPill, { PillType } from "./SidebarPill";
|
||||||
import Tooltip from "./Tooltip";
|
import Floating from "./floating/Floating";
|
||||||
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
const Wrapper = styled(Container)<{
|
const Wrapper = styled(Container)<{
|
||||||
margin?: boolean;
|
margin?: boolean;
|
||||||
@ -60,25 +61,34 @@ function SidebarAction(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<GuildSidebarListItem>
|
<GuildSidebarListItem>
|
||||||
<SidebarPill type={pillType} />
|
<SidebarPill type={pillType} />
|
||||||
<Tooltip title={props.tooltip} placement="right">
|
<Floating
|
||||||
<Wrapper
|
placement="right"
|
||||||
onClick={props.action}
|
type="tooltip"
|
||||||
onMouseEnter={() => setHovered(true)}
|
offset={20}
|
||||||
onMouseLeave={() => setHovered(false)}
|
props={{
|
||||||
margin={props.margin}
|
content: <span>{props.tooltip}</span>,
|
||||||
active={props.active}
|
}}
|
||||||
useGreenColorScheme={props.useGreenColorScheme}
|
>
|
||||||
>
|
<FloatingTrigger>
|
||||||
{props.image && <img {...props.image} loading="lazy" />}
|
<Wrapper
|
||||||
{props.icon && (
|
onClick={props.action}
|
||||||
<Icon
|
onMouseEnter={() => setHovered(true)}
|
||||||
{...props.icon}
|
onMouseLeave={() => setHovered(false)}
|
||||||
color={isHovered && props.useGreenColorScheme ? "var(--text)" : props.icon.color}
|
margin={props.margin}
|
||||||
/>
|
active={props.active}
|
||||||
)}
|
useGreenColorScheme={props.useGreenColorScheme}
|
||||||
{props.label && <span>{props.label}</span>}
|
>
|
||||||
</Wrapper>
|
{props.image && <img {...props.image} loading="lazy" />}
|
||||||
</Tooltip>
|
{props.icon && (
|
||||||
|
<Icon
|
||||||
|
{...props.icon}
|
||||||
|
color={isHovered && props.useGreenColorScheme ? "var(--text)" : props.icon.color}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{props.label && <span>{props.label}</span>}
|
||||||
|
</Wrapper>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
</GuildSidebarListItem>
|
</GuildSidebarListItem>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,21 +1,24 @@
|
|||||||
import MuiTooltip, { TooltipProps as MuiTooltipProps, tooltipClasses } from "@mui/material/Tooltip";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
import { FloatingProps } from "./floating/Floating";
|
||||||
|
|
||||||
export default styled(({ className, ...props }: MuiTooltipProps) => (
|
const Container = styled.div`
|
||||||
<MuiTooltip {...props} arrow classes={{ popper: className }} />
|
background-color: var(--background-tertiary);
|
||||||
))(() => ({
|
line-height: 16px;
|
||||||
[`& .${tooltipClasses.popper}`]: {
|
box-sizing: border-box;
|
||||||
maxWidth: 200,
|
font-size: 14px;
|
||||||
borderRadius: 5,
|
padding: 8px 12px;
|
||||||
},
|
overflow: hidden;
|
||||||
[`& .${tooltipClasses.arrow}`]: {
|
text-overflow: ellipsis;
|
||||||
color: "var(--background-tertiary)",
|
white-space: nowrap;
|
||||||
},
|
max-width: 250px;
|
||||||
[`& .${tooltipClasses.tooltip}`]: {
|
border-radius: 4px;
|
||||||
backgroundColor: "var(--background-tertiary)",
|
color: var(--text);
|
||||||
fontSize: "14px",
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5);
|
||||||
padding: "8px 12px",
|
`;
|
||||||
overflow: "hidden",
|
|
||||||
textOverflow: "ellipsis",
|
function Tooltip(props: FloatingProps<"tooltip">) {
|
||||||
},
|
if (!props) return null;
|
||||||
}));
|
return <Container aria-label={props.aria}>{props.content}</Container>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tooltip;
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
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";
|
||||||
import Tooltip from "./Tooltip";
|
import Floating from "./floating/Floating";
|
||||||
import SettingsModal from "./modals/SettingsModal";
|
import FloatingTrigger from "./floating/FloatingTrigger";
|
||||||
|
|
||||||
const Section = styled.section`
|
const Section = styled.section`
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
@ -21,13 +22,14 @@ const Container = styled.div`
|
|||||||
background-color: var(--background-secondary-alt);
|
background-color: var(--background-secondary-alt);
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const AvatarWrapper = styled.div`
|
const AvatarWrapper = styled(FloatingTrigger)`
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
min-width: 120px;
|
min-width: 120px;
|
||||||
padding-left: 2px;
|
padding-left: 2px;
|
||||||
margin-right: 8px;
|
margin-right: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
cursor: default;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: var(--background-primary-alt);
|
background-color: var(--background-primary-alt);
|
||||||
@ -45,6 +47,7 @@ const Username = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
cursor: pointer;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const Subtext = styled.div`
|
const Subtext = styled.div`
|
||||||
@ -53,6 +56,7 @@ const Subtext = styled.div`
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const ActionsWrapper = styled.div`
|
const ActionsWrapper = styled.div`
|
||||||
@ -66,32 +70,50 @@ const ActionsWrapper = styled.div`
|
|||||||
|
|
||||||
function UserPanel() {
|
function UserPanel() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const { openModal } = useModals();
|
|
||||||
|
|
||||||
const openSettingsModal = () => {
|
const openSettingsModal = () => {
|
||||||
openModal(SettingsModal);
|
modalController.push({
|
||||||
|
type: "settings",
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Section>
|
<Floating
|
||||||
<Container>
|
placement="bottom"
|
||||||
<AvatarWrapper>
|
type="userPopout"
|
||||||
<Avatar popoutPlacement="top" />
|
props={{
|
||||||
<Name>
|
user: app.account! as unknown as User,
|
||||||
<Username>{app.account?.username}</Username>
|
}}
|
||||||
<Subtext>#{app.account?.discriminator}</Subtext>
|
>
|
||||||
</Name>
|
<Section>
|
||||||
</AvatarWrapper>
|
<Container>
|
||||||
|
<AvatarWrapper>
|
||||||
|
<Avatar popoutPlacement="top" onClick={null} />
|
||||||
|
<Name>
|
||||||
|
<Username>{app.account?.username}</Username>
|
||||||
|
<Subtext>#{app.account?.discriminator}</Subtext>
|
||||||
|
</Name>
|
||||||
|
</AvatarWrapper>
|
||||||
|
|
||||||
<ActionsWrapper>
|
<ActionsWrapper>
|
||||||
<Tooltip title="Settings">
|
<Floating
|
||||||
<IconButton aria-label="settings" color="#fff" onClick={openSettingsModal}>
|
placement="top"
|
||||||
<Icon icon="mdiCog" size="20px" />
|
type="tooltip"
|
||||||
</IconButton>
|
offset={10}
|
||||||
</Tooltip>
|
props={{
|
||||||
</ActionsWrapper>
|
content: <span>Settings</span>,
|
||||||
</Container>
|
}}
|
||||||
</Section>
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<IconButton aria-label="settings" color="#fff" onClick={openSettingsModal}>
|
||||||
|
<Icon icon="mdiCog" size="20px" />
|
||||||
|
</IconButton>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
|
</ActionsWrapper>
|
||||||
|
</Container>
|
||||||
|
</Section>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,175 +0,0 @@
|
|||||||
import dayjs from "dayjs";
|
|
||||||
import styled from "styled-components";
|
|
||||||
import { ReactComponent as SpacebarLogoBlue } from "../assets/images/logo/Spacebar_Icon.svg";
|
|
||||||
import useLogger from "../hooks/useLogger";
|
|
||||||
import AccountStore from "../stores/AccountStore";
|
|
||||||
import User from "../stores/objects/User";
|
|
||||||
import Snowflake from "../utils/Snowflake";
|
|
||||||
import Avatar from "./Avatar";
|
|
||||||
import { HorizontalDivider } from "./Divider";
|
|
||||||
import Tooltip from "./Tooltip";
|
|
||||||
|
|
||||||
const Container = styled.div`
|
|
||||||
background-color: #252525;
|
|
||||||
border-radius: 4px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
width: 280px;
|
|
||||||
max-height: 600px;
|
|
||||||
overflow: hidden;
|
|
||||||
// heavy shadow
|
|
||||||
box-shadow: 0 0 0 1px rgb(0 0 0 / 15%), 0 4px 8px rgb(0 0 0 / 15%);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Top = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Bottom = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--background-tertiary);
|
|
||||||
border-radius: 4px;
|
|
||||||
margin: 0 16px 16px;
|
|
||||||
max-height: 340px;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Section = styled.div`
|
|
||||||
padding: 12px;
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UsernameWrapper = styled.div`
|
|
||||||
text-overflow: clip;
|
|
||||||
white-space: nowrap;
|
|
||||||
overflow: hidden;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const NicknameText = styled.span`
|
|
||||||
font-size: 20px;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const UsernameText = styled.span`
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
`;
|
|
||||||
|
|
||||||
const Heading = styled.span`
|
|
||||||
display: flex;
|
|
||||||
font-weight: var(--font-weight-bold);
|
|
||||||
margin-bottom: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberSinceContainer = styled.div`
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
column-gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
user-select: none;
|
|
||||||
`;
|
|
||||||
|
|
||||||
const MemberSinceText = styled.span`
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: var(--font-weight-regular);
|
|
||||||
`;
|
|
||||||
|
|
||||||
function UserProfilePopout({ user }: { user: User | AccountStore }) {
|
|
||||||
const logger = useLogger("UserProfilePopout");
|
|
||||||
// if (!member.user) {
|
|
||||||
// logger.error("member.user is undefined");
|
|
||||||
// return null;
|
|
||||||
// }
|
|
||||||
if (!user) {
|
|
||||||
logger.error("user is undefined");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const id = user.id;
|
|
||||||
const { timestamp: createdAt } = Snowflake.deconstruct(id);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<Top>
|
|
||||||
<Avatar
|
|
||||||
style={{ margin: "22px 16px" }}
|
|
||||||
size={80}
|
|
||||||
onClick={() => {
|
|
||||||
// TODO: open profile modal
|
|
||||||
}}
|
|
||||||
user={user}
|
|
||||||
/>
|
|
||||||
</Top>
|
|
||||||
<Bottom>
|
|
||||||
<Section>
|
|
||||||
<div>
|
|
||||||
<UsernameWrapper>
|
|
||||||
<NicknameText>{user.username}</NicknameText>
|
|
||||||
<div>
|
|
||||||
<UsernameText>
|
|
||||||
{user.username}#{user.discriminator}
|
|
||||||
</UsernameText>
|
|
||||||
</div>
|
|
||||||
</UsernameWrapper>
|
|
||||||
</div>
|
|
||||||
</Section>
|
|
||||||
<HorizontalDivider
|
|
||||||
style={{
|
|
||||||
margin: "0 12px",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Section>
|
|
||||||
<Heading>Member Since</Heading>
|
|
||||||
<MemberSinceContainer>
|
|
||||||
<Tooltip title="Spacebar" placement="top">
|
|
||||||
<div>
|
|
||||||
<SpacebarLogoBlue width={16} height={16} style={{ borderRadius: "50%" }} />
|
|
||||||
</div>
|
|
||||||
</Tooltip>
|
|
||||||
<MemberSinceText>{dayjs(createdAt).format("MMM D, YYYY")}</MemberSinceText>
|
|
||||||
{/* <div
|
|
||||||
style={{
|
|
||||||
height: "4px",
|
|
||||||
width: "4px",
|
|
||||||
borderRadius: "50%",
|
|
||||||
backgroundColor: "var(--text-disabled)",
|
|
||||||
}}
|
|
||||||
/> */}
|
|
||||||
|
|
||||||
{/* <Tooltip title={guild.name} placement="top">
|
|
||||||
{guild.icon ? (
|
|
||||||
<img
|
|
||||||
src={REST.makeCDNUrl(CDNRoutes.guildIcon(guild.id, guild.icon, ImageFormat.PNG))}
|
|
||||||
width={16}
|
|
||||||
height={16}
|
|
||||||
loading="lazy"
|
|
||||||
style={{
|
|
||||||
borderRadius: "50%",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<span
|
|
||||||
style={{
|
|
||||||
fontSize: "16px",
|
|
||||||
fontWeight: "bold",
|
|
||||||
cursor: "pointer",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{guild.acronym}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</Tooltip>
|
|
||||||
<MemberSinceText>{dayjs(member.joined_at).format("MMM D, YYYY")}</MemberSinceText> */}
|
|
||||||
</MemberSinceContainer>
|
|
||||||
</Section>
|
|
||||||
</Bottom>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserProfilePopout;
|
|
26
src/components/common/animations.ts
Normal file
26
src/components/common/animations.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
// https://github.com/revoltchat/components/blob/master/src/components/common/animations.ts
|
||||||
|
|
||||||
|
import { keyframes } from "styled-components";
|
||||||
|
|
||||||
|
export const animationFadeIn = keyframes`
|
||||||
|
0% {opacity: 0;}
|
||||||
|
70% {opacity: 0;}
|
||||||
|
100% {opacity: 1;}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const animationFadeOut = keyframes`
|
||||||
|
0% {opacity: 1;}
|
||||||
|
70% {opacity: 0;}
|
||||||
|
100% {opacity: 0;}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const animationZoomIn = keyframes`
|
||||||
|
0% {transform: scale(0.5);}
|
||||||
|
98% {transform: scale(1.01);}
|
||||||
|
100% {transform: scale(1);}
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const animationZoomOut = keyframes`
|
||||||
|
0% {transform: scale(1);}
|
||||||
|
100% {transform: scale(0.5);}
|
||||||
|
`;
|
72
src/components/contextMenus/ChannelContextMenu.tsx
Normal file
72
src/components/contextMenus/ChannelContextMenu.tsx
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
// loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import Channel from "../../stores/objects/Channel";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
channel: Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChannelContextMenu({ channel }: MenuProps) {
|
||||||
|
/**
|
||||||
|
* Copy id to clipboard
|
||||||
|
*/
|
||||||
|
function copyId() {
|
||||||
|
navigator.clipboard.writeText(channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy link to clipboard
|
||||||
|
*/
|
||||||
|
function copyLink() {
|
||||||
|
navigator.clipboard.writeText(`${window.location.origin}/channels/${channel.guildId}/${channel.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open invite creation modal
|
||||||
|
*/
|
||||||
|
function openInviteCreateModal() {
|
||||||
|
modalController.push({
|
||||||
|
type: "create_invite",
|
||||||
|
target: channel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuButton icon="mdiAccountPlus" onClick={openInviteCreateModal}>
|
||||||
|
Create Invite
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuButton icon="mdiLink" onClick={copyLink}>
|
||||||
|
Copy Link
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
{channel.hasPermission("MANAGE_CHANNELS") && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton disabled>Edit Channel</ContextMenuButton>
|
||||||
|
<ContextMenuButton disabled destructive>
|
||||||
|
Delete Channel
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuButton
|
||||||
|
icon="mdiIdentifier"
|
||||||
|
iconProps={{
|
||||||
|
style: {
|
||||||
|
filter: "invert(100%)",
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={copyId}
|
||||||
|
>
|
||||||
|
Copy Channel ID
|
||||||
|
</ContextMenuButton>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelContextMenu;
|
48
src/components/contextMenus/ChannelMentionContextMenu.tsx
Normal file
48
src/components/contextMenus/ChannelMentionContextMenu.tsx
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
// loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx
|
||||||
|
|
||||||
|
import Channel from "../../stores/objects/Channel";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
channel: Channel;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ChannelMentionContextMenu({ channel }: MenuProps) {
|
||||||
|
/**
|
||||||
|
* Copy id to clipboard
|
||||||
|
*/
|
||||||
|
function copyId() {
|
||||||
|
navigator.clipboard.writeText(channel.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy link to clipboard
|
||||||
|
*/
|
||||||
|
function copyLink() {
|
||||||
|
navigator.clipboard.writeText(`${window.location.origin}/channels/${channel.guildId}/${channel.id}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuButton icon="mdiLink" onClick={copyLink}>
|
||||||
|
Copy Link
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuButton
|
||||||
|
icon="mdiIdentifier"
|
||||||
|
iconProps={{
|
||||||
|
style: {
|
||||||
|
filter: "invert(100%)",
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={copyId}
|
||||||
|
>
|
||||||
|
Copy Channel ID
|
||||||
|
</ContextMenuButton>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ChannelMentionContextMenu;
|
76
src/components/contextMenus/ContextMenu.tsx
Normal file
76
src/components/contextMenus/ContextMenu.tsx
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
// modified from https://github.com/revoltchat/frontend/blob/master/components/app/menus/ContextMenu.tsx
|
||||||
|
// changed some styling
|
||||||
|
|
||||||
|
import { ComponentProps } from "react";
|
||||||
|
import styled from "styled-components";
|
||||||
|
import Icon, { IconProps } from "../Icon";
|
||||||
|
|
||||||
|
export const ContextMenu = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 6px 8px;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 300px;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 4px;
|
||||||
|
background: var(--background-tertiary);
|
||||||
|
color: var(--text);
|
||||||
|
|
||||||
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.5);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ContextMenuDivider = styled.div`
|
||||||
|
height: 1px;
|
||||||
|
margin: 4px;
|
||||||
|
background: var(--text-disabled);
|
||||||
|
`;
|
||||||
|
|
||||||
|
export const ContextMenuItem = styled("button")`
|
||||||
|
display: block;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 2px 0;
|
||||||
|
cursor: ${(props) => (props.disabled ? "not-allowed" : "pointer")};
|
||||||
|
opacity: ${(props) => (props.disabled ? 0.5 : 1)};
|
||||||
|
|
||||||
|
// remove default button styles
|
||||||
|
border: none;
|
||||||
|
background: none;
|
||||||
|
color: inherit;
|
||||||
|
outline: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const ButtonBase = styled(ContextMenuItem)<{ destructive?: boolean }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
> span {
|
||||||
|
margin-top: 1px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: ${(props) => (props.destructive ? "var(--danger)" : "var(--primary)")};
|
||||||
|
${(props) => (props.destructive ? `color: var(--text)` : "")}
|
||||||
|
}
|
||||||
|
|
||||||
|
${(props) => (props.destructive ? `fill: var(--danger); color: var(--danger)` : "")}
|
||||||
|
`;
|
||||||
|
|
||||||
|
type ButtonProps = ComponentProps<typeof ContextMenuItem> & {
|
||||||
|
icon?: IconProps["icon"];
|
||||||
|
iconProps?: Omit<IconProps, "icon" | "size">;
|
||||||
|
destructive?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function ContextMenuButton({ icon, children, iconProps, ...props }: ButtonProps) {
|
||||||
|
return (
|
||||||
|
<ButtonBase {...props}>
|
||||||
|
<span>{children}</span>
|
||||||
|
{icon && <Icon icon={icon} {...iconProps} size="18px" />}
|
||||||
|
</ButtonBase>
|
||||||
|
);
|
||||||
|
}
|
79
src/components/contextMenus/GuildContextMenu.tsx
Normal file
79
src/components/contextMenus/GuildContextMenu.tsx
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
// loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx
|
||||||
|
|
||||||
|
import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import useLogger from "../../hooks/useLogger";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import Guild from "../../stores/objects/Guild";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
guild: Guild;
|
||||||
|
}
|
||||||
|
|
||||||
|
function GuildContextMenu({ guild }: MenuProps) {
|
||||||
|
const app = useAppStore();
|
||||||
|
const logger = useLogger("GuildContextMenu");
|
||||||
|
const isNotOwner = guild.ownerId !== app.account!.id;
|
||||||
|
/**
|
||||||
|
* Copy id to clipboard
|
||||||
|
*/
|
||||||
|
function copyId() {
|
||||||
|
navigator.clipboard.writeText(guild.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Leave guild
|
||||||
|
*/
|
||||||
|
function leaveGuild() {
|
||||||
|
modalController.push({
|
||||||
|
type: "leave_server",
|
||||||
|
target: guild,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open invite creation modal
|
||||||
|
*/
|
||||||
|
function openInviteCreateModal() {
|
||||||
|
const channel = guild.channels.find((x) => x.type === ChannelType.GuildText && x.hasPermission("VIEW_CHANNEL"));
|
||||||
|
if (!channel) {
|
||||||
|
logger.error("Failed to find suitable channel for invite creation");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
modalController.push({
|
||||||
|
type: "create_invite",
|
||||||
|
target: channel,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuButton onClick={openInviteCreateModal}>Create Invite</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
{isNotOwner && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton destructive onClick={leaveGuild}>
|
||||||
|
Leave Guild
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ContextMenuButton
|
||||||
|
icon="mdiIdentifier"
|
||||||
|
iconProps={{
|
||||||
|
style: {
|
||||||
|
filter: "invert(100%)",
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={copyId}
|
||||||
|
>
|
||||||
|
Copy Guild ID
|
||||||
|
</ContextMenuButton>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GuildContextMenu;
|
67
src/components/contextMenus/MessageContextMenu.tsx
Normal file
67
src/components/contextMenus/MessageContextMenu.tsx
Normal file
@ -0,0 +1,67 @@
|
|||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import Message from "../../stores/objects/Message";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
message: Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
function MessageContextMenu({ message }: MenuProps) {
|
||||||
|
const app = useAppStore();
|
||||||
|
|
||||||
|
function copyRaw() {
|
||||||
|
navigator.clipboard.writeText(message.content);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMessage(e: MouseEvent) {
|
||||||
|
if (e.shiftKey) {
|
||||||
|
await message.delete();
|
||||||
|
} else {
|
||||||
|
modalController.push({
|
||||||
|
type: "delete_message",
|
||||||
|
target: message as Message,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function copyId() {
|
||||||
|
navigator.clipboard.writeText(message.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuButton icon="mdiReply" disabled>
|
||||||
|
Reply
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuButton icon="mdiContentCopy" onClick={copyRaw}>
|
||||||
|
Copy Raw Text
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
{(message.channel.hasPermission("MANAGE_MESSAGES") || message.author.id === app.account?.id) &&
|
||||||
|
message instanceof Message && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton icon="mdiDelete" destructive onClick={deleteMessage}>
|
||||||
|
Delete Message
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ContextMenuButton
|
||||||
|
icon="mdiIdentifier"
|
||||||
|
onClick={copyId}
|
||||||
|
iconProps={{
|
||||||
|
style: {
|
||||||
|
filter: "invert(100%)",
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Copy Message ID
|
||||||
|
</ContextMenuButton>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default MessageContextMenu;
|
101
src/components/contextMenus/UserContextMenu.tsx
Normal file
101
src/components/contextMenus/UserContextMenu.tsx
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
// loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import GuildMember from "../../stores/objects/GuildMember";
|
||||||
|
import User from "../../stores/objects/User";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||||
|
|
||||||
|
interface MenuProps {
|
||||||
|
user: User;
|
||||||
|
member?: GuildMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserContextMenu({ user, member }: MenuProps) {
|
||||||
|
const app = useAppStore();
|
||||||
|
const guild = member ? app.guilds.get(member.guild.id) : undefined;
|
||||||
|
const guildMe = guild ? guild.members.get(app.account!.id) : undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copy user id to clipboard
|
||||||
|
*/
|
||||||
|
function copyId() {
|
||||||
|
navigator.clipboard.writeText(user.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open kick modal
|
||||||
|
*/
|
||||||
|
function kick() {
|
||||||
|
if (!member) return;
|
||||||
|
modalController.push({
|
||||||
|
type: "kick_member",
|
||||||
|
target: member,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Open ban modal
|
||||||
|
*/
|
||||||
|
function ban() {
|
||||||
|
if (!member) return;
|
||||||
|
modalController.push({
|
||||||
|
type: "ban_member",
|
||||||
|
target: member,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ContextMenu>
|
||||||
|
<ContextMenuButton disabled>Profile</ContextMenuButton>
|
||||||
|
<ContextMenuButton disabled>Mention</ContextMenuButton>
|
||||||
|
<ContextMenuButton disabled>Message</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
{member && <ContextMenuButton disabled>Change Nickname</ContextMenuButton>}
|
||||||
|
<ContextMenuButton disabled>Add Friend</ContextMenuButton>
|
||||||
|
<ContextMenuButton disabled>Block</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
{member && guildMe && (
|
||||||
|
<>
|
||||||
|
{guildMe.hasPermission("KICK_MEMBERS") && (
|
||||||
|
<ContextMenuButton destructive onClick={kick}>
|
||||||
|
Kick {member?.nick ?? user.username}
|
||||||
|
</ContextMenuButton>
|
||||||
|
)}
|
||||||
|
{guildMe.hasPermission("BAN_MEMBERS") && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton destructive onClick={ban}>
|
||||||
|
Ban {member?.nick ?? user.username}
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{guildMe.hasPermission("MANAGE_ROLES") && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton disabled icon="mdiChevronRight">
|
||||||
|
Roles
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ContextMenuButton
|
||||||
|
icon="mdiIdentifier"
|
||||||
|
iconProps={{
|
||||||
|
style: {
|
||||||
|
filter: "invert(100%)",
|
||||||
|
background: "black",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
onClick={copyId}
|
||||||
|
>
|
||||||
|
Copy User ID
|
||||||
|
</ContextMenuButton>
|
||||||
|
</ContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserContextMenu;
|
95
src/components/floating/Floating.tsx
Normal file
95
src/components/floating/Floating.tsx
Normal file
@ -0,0 +1,95 @@
|
|||||||
|
import { FloatingArrow, FloatingPortal, Placement } from "@floating-ui/react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import { FloatingContext } from "../../contexts/FloatingContext";
|
||||||
|
import useFloating from "../../hooks/useFloating";
|
||||||
|
import Guild from "../../stores/objects/Guild";
|
||||||
|
import GuildMember from "../../stores/objects/GuildMember";
|
||||||
|
import User from "../../stores/objects/User";
|
||||||
|
import Tooltip from "../Tooltip";
|
||||||
|
import GuildMenuPopout from "./GuildMenuPopout";
|
||||||
|
import UserProfilePopout from "./UserProfilePopout";
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
type Components = Record<string, React.FC<any>>;
|
||||||
|
|
||||||
|
const components: Components = {
|
||||||
|
userPopout: UserProfilePopout,
|
||||||
|
tooltip: Tooltip,
|
||||||
|
guild: GuildMenuPopout,
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FloatingOptions = {
|
||||||
|
initialOpen?: boolean;
|
||||||
|
placement?: Placement;
|
||||||
|
offset?: number;
|
||||||
|
open?: boolean;
|
||||||
|
onOpenChange?: (open: boolean) => void;
|
||||||
|
} & (
|
||||||
|
| {
|
||||||
|
type: "userPopout";
|
||||||
|
props: {
|
||||||
|
user: User;
|
||||||
|
member?: GuildMember;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "tooltip";
|
||||||
|
props: {
|
||||||
|
content: JSX.Element;
|
||||||
|
aria?: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
type: "guild";
|
||||||
|
props: {
|
||||||
|
guild: Guild;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
export type FloatingProps<T extends FloatingOptions["type"]> = (FloatingOptions & {
|
||||||
|
type: T;
|
||||||
|
})["props"];
|
||||||
|
|
||||||
|
function Floating({
|
||||||
|
type,
|
||||||
|
children,
|
||||||
|
props,
|
||||||
|
...restOptions
|
||||||
|
}: {
|
||||||
|
children: React.ReactNode;
|
||||||
|
} & FloatingOptions) {
|
||||||
|
const floating = useFloating({ type, ...restOptions });
|
||||||
|
|
||||||
|
const Component = components[type];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FloatingContext.Provider value={floating}>
|
||||||
|
{children}
|
||||||
|
{Component && floating.open && (
|
||||||
|
<FloatingPortal>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.1, easing: [0.87, 0, 0.13, 1] }}
|
||||||
|
ref={floating.refs.setFloating}
|
||||||
|
style={{ ...floating.context.floatingStyles, zIndex: 1000, outline: "none" }}
|
||||||
|
{...floating.getFloatingProps()}
|
||||||
|
>
|
||||||
|
<Component {...props} />
|
||||||
|
{type === "tooltip" && (
|
||||||
|
<FloatingArrow
|
||||||
|
ref={floating.arrowRef}
|
||||||
|
context={floating.context}
|
||||||
|
fill="var(--background-tertiary)"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</motion.div>
|
||||||
|
</FloatingPortal>
|
||||||
|
)}
|
||||||
|
</FloatingContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Floating;
|
35
src/components/floating/FloatingContent.tsx
Normal file
35
src/components/floating/FloatingContent.tsx
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import { FloatingFocusManager, FloatingPortal, useMergeRefs } from "@floating-ui/react";
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import React from "react";
|
||||||
|
import useFloatingContext from "../../hooks/useFloatingContext";
|
||||||
|
|
||||||
|
export default React.forwardRef<HTMLDivElement, React.HTMLProps<HTMLDivElement>>(function PopoverContent(
|
||||||
|
{ style, ...props },
|
||||||
|
propRef,
|
||||||
|
) {
|
||||||
|
const { context: floatingContext, ...context } = useFloatingContext();
|
||||||
|
const ref = useMergeRefs([context.refs.setFloating, propRef]);
|
||||||
|
|
||||||
|
if (!floatingContext.open) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<FloatingPortal>
|
||||||
|
<FloatingFocusManager context={floatingContext}>
|
||||||
|
<motion.div
|
||||||
|
initial={{ opacity: 0 }}
|
||||||
|
animate={{ opacity: 1 }}
|
||||||
|
exit={{ opacity: 0 }}
|
||||||
|
transition={{ duration: 0.1, easing: [0.87, 0, 0.13, 1] }}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
ref={ref}
|
||||||
|
style={{ ...context.floatingStyles, ...style, zIndex: 1000, outline: "none" }}
|
||||||
|
{...context.getFloatingProps(props)}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</div>
|
||||||
|
</motion.div>
|
||||||
|
</FloatingFocusManager>
|
||||||
|
</FloatingPortal>
|
||||||
|
);
|
||||||
|
});
|
36
src/components/floating/FloatingTrigger.tsx
Normal file
36
src/components/floating/FloatingTrigger.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { useMergeRefs } from "@floating-ui/react";
|
||||||
|
import React from "react";
|
||||||
|
import useFloatingContext from "../../hooks/useFloatingContext";
|
||||||
|
|
||||||
|
interface PopoverTriggerProps {
|
||||||
|
children: React.ReactNode;
|
||||||
|
asChild?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default React.forwardRef<HTMLElement, React.HTMLProps<HTMLElement> & PopoverTriggerProps>(
|
||||||
|
function FloatingTrigger({ children, asChild = false, ...props }, propRef) {
|
||||||
|
const context = useFloatingContext();
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const childrenRef = (children as any).ref;
|
||||||
|
const ref = useMergeRefs([context.refs.setReference, propRef, childrenRef]);
|
||||||
|
|
||||||
|
// `asChild` allows the user to pass any element as the anchor
|
||||||
|
if (asChild && React.isValidElement(children)) {
|
||||||
|
return React.cloneElement(
|
||||||
|
children,
|
||||||
|
context.getReferenceProps({
|
||||||
|
ref,
|
||||||
|
...props,
|
||||||
|
...children.props,
|
||||||
|
"data-state": context.open ? "open" : "closed",
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span ref={ref} data-state={context.open ? "open" : "closed"} {...context.getReferenceProps(props)}>
|
||||||
|
{children}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
77
src/components/floating/GuildMenuPopout.tsx
Normal file
77
src/components/floating/GuildMenuPopout.tsx
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import useLogger from "../../hooks/useLogger";
|
||||||
|
|
||||||
|
import React, { useEffect } from "react";
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import { Permissions } from "../../utils/Permissions";
|
||||||
|
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "../contextMenus/ContextMenu";
|
||||||
|
|
||||||
|
const CustomContextMenu = styled(ContextMenu)`
|
||||||
|
width: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
function GuildMenuPopout() {
|
||||||
|
const { activeGuild, account } = useAppStore();
|
||||||
|
const logger = useLogger("GuildMenuPopout");
|
||||||
|
|
||||||
|
const [hasCreateChannelPermission, setHasCreateChannelPermission] = React.useState(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeGuild) return;
|
||||||
|
|
||||||
|
const permission = Permissions.getPermission(account!.id, activeGuild, undefined);
|
||||||
|
const hasPermission = permission.has("MANAGE_CHANNELS");
|
||||||
|
setHasCreateChannelPermission(hasPermission);
|
||||||
|
}, [activeGuild]);
|
||||||
|
|
||||||
|
if (!activeGuild) {
|
||||||
|
logger.error("activeGuild is undefined");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function leaveGuild() {
|
||||||
|
modalController.push({
|
||||||
|
type: "leave_server",
|
||||||
|
target: activeGuild!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onChannelCreateClick() {
|
||||||
|
modalController.push({
|
||||||
|
type: "create_channel",
|
||||||
|
guild: activeGuild!,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CustomContextMenu>
|
||||||
|
<ContextMenuButton icon="mdiCog" disabled>
|
||||||
|
Server Settings
|
||||||
|
</ContextMenuButton>
|
||||||
|
{hasCreateChannelPermission && (
|
||||||
|
<>
|
||||||
|
<ContextMenuButton icon="mdiPlusCircle" onClick={onChannelCreateClick}>
|
||||||
|
Create Channel
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuButton icon="mdiFolderPlus" disabled>
|
||||||
|
Create Category
|
||||||
|
</ContextMenuButton>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuButton icon="mdiBell" disabled>
|
||||||
|
Notification Settings
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuButton icon="mdiShieldLock" disabled>
|
||||||
|
Privacy Settings
|
||||||
|
</ContextMenuButton>
|
||||||
|
<ContextMenuDivider />
|
||||||
|
<ContextMenuButton icon="mdiLocationExit" destructive onClick={leaveGuild}>
|
||||||
|
Leave Guild
|
||||||
|
</ContextMenuButton>
|
||||||
|
</CustomContextMenu>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default GuildMenuPopout;
|
295
src/components/floating/UserProfilePopout.tsx
Normal file
295
src/components/floating/UserProfilePopout.tsx
Normal file
@ -0,0 +1,295 @@
|
|||||||
|
import styled from "styled-components";
|
||||||
|
import useLogger from "../../hooks/useLogger";
|
||||||
|
import GuildMember from "../../stores/objects/GuildMember";
|
||||||
|
import User from "../../stores/objects/User";
|
||||||
|
import Snowflake from "../../utils/Snowflake";
|
||||||
|
import Avatar from "../Avatar";
|
||||||
|
import { HorizontalDivider } from "../Divider";
|
||||||
|
|
||||||
|
import { CDNRoutes, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import dayjs from "dayjs";
|
||||||
|
import SpacebarLogoBlue from "../../assets/images/logo/Spacebar_Icon.svg?react";
|
||||||
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import REST from "../../utils/REST";
|
||||||
|
import Floating from "./Floating";
|
||||||
|
import FloatingTrigger from "./FloatingTrigger";
|
||||||
|
|
||||||
|
const Container = styled.div`
|
||||||
|
background-color: #252525;
|
||||||
|
border-radius: 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
width: 340px;
|
||||||
|
max-height: 600px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow:
|
||||||
|
0 0 0 1px rgb(0 0 0 / 15%),
|
||||||
|
0 4px 8px rgb(0 0 0 / 15%);
|
||||||
|
color: var(--text);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Top = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Bottom = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
background-color: var(--background-tertiary);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin: 0 16px 16px;
|
||||||
|
max-height: 340px;
|
||||||
|
gap: 8px;
|
||||||
|
|
||||||
|
& > :first-child {
|
||||||
|
padding: 12px 12px 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :nth-child(n + 3) {
|
||||||
|
padding: 0 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& > :last-child {
|
||||||
|
padding: 0 12px 12px 12px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Section = styled.div`
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UsernameWrapper = styled.div`
|
||||||
|
text-overflow: clip;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const NicknameText = styled.span`
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const UsernameText = styled.span`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Heading = styled.span`
|
||||||
|
display: flex;
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
margin-bottom: 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberSinceContainer = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
column-gap: 8px;
|
||||||
|
align-items: center;
|
||||||
|
user-select: none;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const MemberSinceText = styled.span`
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: var(--font-weight-regular);
|
||||||
|
`;
|
||||||
|
|
||||||
|
const Acronym = styled.div`
|
||||||
|
font-size: 8px;
|
||||||
|
background-image: none;
|
||||||
|
background-color: var(--background-secondary);
|
||||||
|
text-align: center;
|
||||||
|
width: 16px;
|
||||||
|
height: 16px;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const AcronymText = styled.div`
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleList = styled.div`
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
position: relative;
|
||||||
|
margin-top: 2px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RolePillDot = styled.span<{ color?: string }>`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0 4px;
|
||||||
|
background-color: ${(props) => props.color ?? "var(--text-disabled)"};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RolePill = styled.div`
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: var(--font-weight-medium);
|
||||||
|
background-color: var(--background-primary-alt);
|
||||||
|
border-radius: 12px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
height: 22px;
|
||||||
|
margin: 0 4px 4px 0;
|
||||||
|
padding: 8px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
const RoleName = styled.div`
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
max-width: 200px;
|
||||||
|
`;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
user: User;
|
||||||
|
member?: GuildMember;
|
||||||
|
}
|
||||||
|
|
||||||
|
function UserProfilePopout({ user, member }: Props) {
|
||||||
|
const app = useAppStore();
|
||||||
|
const logger = useLogger("UserProfilePopout");
|
||||||
|
|
||||||
|
const id = user.id;
|
||||||
|
const { timestamp: createdAt } = Snowflake.deconstruct(id);
|
||||||
|
const presence = app.presences.get(user.id);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Container>
|
||||||
|
<Top>
|
||||||
|
<Avatar
|
||||||
|
style={{ margin: "22px 16px" }}
|
||||||
|
size={80}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
// TODO: open profile modal
|
||||||
|
logger.debug("open profile modal");
|
||||||
|
}}
|
||||||
|
user={user}
|
||||||
|
presence={presence}
|
||||||
|
statusDotStyle={{
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
}}
|
||||||
|
showPresence
|
||||||
|
/>
|
||||||
|
</Top>
|
||||||
|
<Bottom>
|
||||||
|
<Section>
|
||||||
|
<div>
|
||||||
|
<UsernameWrapper>
|
||||||
|
<NicknameText>{member?.nick ?? user.username}</NicknameText>
|
||||||
|
<div>
|
||||||
|
<UsernameText>
|
||||||
|
{user.username}#{user.discriminator}
|
||||||
|
</UsernameText>
|
||||||
|
</div>
|
||||||
|
</UsernameWrapper>
|
||||||
|
</div>
|
||||||
|
</Section>
|
||||||
|
<HorizontalDivider
|
||||||
|
style={{
|
||||||
|
margin: "0 12px",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Section>
|
||||||
|
<Heading>Member Since</Heading>
|
||||||
|
<MemberSinceContainer>
|
||||||
|
<Floating
|
||||||
|
placement="top"
|
||||||
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: <span>Spacebar</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<div>
|
||||||
|
<SpacebarLogoBlue width={16} height={16} style={{ borderRadius: "50%" }} />
|
||||||
|
</div>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
|
<MemberSinceText>{dayjs(createdAt).format("MMM D, YYYY")}</MemberSinceText>
|
||||||
|
{member && (
|
||||||
|
<>
|
||||||
|
<div
|
||||||
|
style={{
|
||||||
|
height: "4px",
|
||||||
|
width: "4px",
|
||||||
|
borderRadius: "50%",
|
||||||
|
backgroundColor: "var(--text-disabled)",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Floating
|
||||||
|
placement="top"
|
||||||
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: <span>{member.guild.name}</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
{member.guild.icon ? (
|
||||||
|
<img
|
||||||
|
src={REST.makeCDNUrl(
|
||||||
|
CDNRoutes.guildIcon(
|
||||||
|
member.guild.id,
|
||||||
|
member.guild.icon,
|
||||||
|
ImageFormat.PNG,
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
width={16}
|
||||||
|
height={16}
|
||||||
|
loading="lazy"
|
||||||
|
style={{
|
||||||
|
borderRadius: "50%",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<Acronym>
|
||||||
|
<AcronymText>{member.guild.acronym}</AcronymText>
|
||||||
|
</Acronym>
|
||||||
|
)}
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
|
<MemberSinceText>{dayjs(member.joined_at).format("MMM D, YYYY")}</MemberSinceText>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</MemberSinceContainer>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{member && (
|
||||||
|
<Section>
|
||||||
|
<Heading>{member.roles.length ? "Roles" : "No Roles"}</Heading>
|
||||||
|
<RoleList>
|
||||||
|
{member.roles.map((x, i) => (
|
||||||
|
<RolePill key={i}>
|
||||||
|
<RolePillDot color={x.color} />
|
||||||
|
<RoleName>{x.name}</RoleName>
|
||||||
|
</RolePill>
|
||||||
|
))}
|
||||||
|
</RoleList>
|
||||||
|
</Section>
|
||||||
|
)}
|
||||||
|
</Bottom>
|
||||||
|
</Container>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UserProfilePopout;
|
@ -176,6 +176,10 @@ const customRenderer: Partial<ReactRenderer> = {
|
|||||||
<Mention key={i} type="role" id={match} />
|
<Mention key={i} type="role" id={match} />
|
||||||
));
|
));
|
||||||
|
|
||||||
|
replaced = reactStringReplace(replaced, /(@everyone|@here)/, (match, i) => (
|
||||||
|
<Mention key={i} type="text" id={match} />
|
||||||
|
));
|
||||||
|
|
||||||
return replaced;
|
return replaced;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -1,23 +1,29 @@
|
|||||||
import React, { memo } from "react";
|
import React, { memo } from "react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
import { useAppStore } from "../../stores/AppStore";
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import Channel from "../../stores/objects/Channel";
|
import Channel from "../../stores/objects/Channel";
|
||||||
import Role from "../../stores/objects/Role";
|
import Role from "../../stores/objects/Role";
|
||||||
import User from "../../stores/objects/User";
|
import User from "../../stores/objects/User";
|
||||||
import { hexToRGB, rgbToHsl } from "../../utils/Utils";
|
import { hexToRGB, rgbToHsl } from "../../utils/Utils";
|
||||||
import UserProfilePopout from "../UserProfilePopout";
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const Container = styled.span<{ color?: string }>`
|
const MentionText = styled.span<{ color?: string; withHover?: boolean }>`
|
||||||
padding: 0 2px;
|
padding: 0 2px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
background-color: hsl(${(props) => props.color ?? "var(--primary-hsl)"} / 0.3);
|
background-color: hsl(${(props) => props.color ?? "var(--primary-hsl)"} / 0.3);
|
||||||
|
user-select: ${(props) => (props.withHover ? "none" : "inherit")};
|
||||||
|
|
||||||
&:hover {
|
${(props) =>
|
||||||
background-color: hsl(${(props) => props.color ?? "var(--primary-hsl)"} / 0.5);
|
props.withHover &&
|
||||||
cursor: pointer;
|
`
|
||||||
}
|
&:hover {
|
||||||
|
background-color: hsl(${props.color ?? "var(--primary-hsl)"} / 0.5);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
`}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface MentionProps {
|
interface MentionProps {
|
||||||
@ -25,40 +31,34 @@ interface MentionProps {
|
|||||||
}
|
}
|
||||||
function UserMention({ id }: MentionProps) {
|
function UserMention({ id }: MentionProps) {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const popoutContext = React.useContext(PopoutContext);
|
|
||||||
const [user, setUser] = React.useState<User | null>(null);
|
const [user, setUser] = React.useState<User | null>(null);
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const contextMenu = React.useContext(ContextMenuContext);
|
||||||
|
|
||||||
const click = (e: React.MouseEvent) => {
|
|
||||||
e.preventDefault();
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
if (!user || !ref.current) return;
|
|
||||||
|
|
||||||
const rect = ref.current.getBoundingClientRect();
|
|
||||||
|
|
||||||
popoutContext.open({
|
|
||||||
element: <UserProfilePopout user={user} />,
|
|
||||||
position: rect,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const user = app.users.get(id);
|
const getUser = async () => {
|
||||||
if (user) setUser(user);
|
const resolvedUser = await app.users.resolve(id);
|
||||||
|
setUser(resolvedUser ?? null);
|
||||||
|
};
|
||||||
|
|
||||||
|
getUser();
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
if (!user)
|
if (!user) return <MentionText>@{id}</MentionText>;
|
||||||
return (
|
|
||||||
<Container ref={ref}>
|
|
||||||
<span>@{id}</span>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onClick={click} ref={ref}>
|
<Floating
|
||||||
<span>@{user.username}</span>
|
type="userPopout"
|
||||||
</Container>
|
placement="right"
|
||||||
|
props={{
|
||||||
|
user,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<MentionText withHover onContextMenu={(e) => contextMenu.onContextMenu(e, { type: "user", user })}>
|
||||||
|
@{user.username}
|
||||||
|
</MentionText>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,9 +66,11 @@ function ChannelMention({ id }: MentionProps) {
|
|||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const [channel, setChannel] = React.useState<Channel | null>(null);
|
const [channel, setChannel] = React.useState<Channel | null>(null);
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
const contextMenu = React.useContext(ContextMenuContext);
|
||||||
|
|
||||||
const click = () => {
|
const onClick = () => {
|
||||||
if (!channel) return;
|
if (!channel) return;
|
||||||
|
if (!channel.isGuildTextChannel) return;
|
||||||
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
navigate(`/channels/${channel.guildId}/${channel.id}`);
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -77,17 +79,16 @@ function ChannelMention({ id }: MentionProps) {
|
|||||||
if (channel) setChannel(channel);
|
if (channel) setChannel(channel);
|
||||||
}, [id]);
|
}, [id]);
|
||||||
|
|
||||||
if (!channel)
|
if (!channel) return <MentionText>#{id}</MentionText>;
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<span>#{id}</span>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container onClick={click}>
|
<MentionText
|
||||||
<span>#{channel.name}</span>
|
withHover
|
||||||
</Container>
|
onClick={onClick}
|
||||||
|
onContextMenu={(e) => contextMenu.onContextMenu(e, { type: "channelMention", channel })}
|
||||||
|
>
|
||||||
|
#{channel.name}
|
||||||
|
</MentionText>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,22 +111,21 @@ function RoleMention({ id }: MentionProps) {
|
|||||||
setColor(rgbToHsl(rgb.r, rgb.g, rgb.b));
|
setColor(rgbToHsl(rgb.r, rgb.g, rgb.b));
|
||||||
}, [role]);
|
}, [role]);
|
||||||
|
|
||||||
if (!role)
|
if (!role) return <MentionText>@unknown-role</MentionText>;
|
||||||
return (
|
|
||||||
<Container>
|
|
||||||
<span>@unknown-role</span>
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container color={color}>
|
<MentionText color={color} withHover>
|
||||||
<span>@{role.name}</span>
|
@{role.name}
|
||||||
</Container>
|
</MentionText>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function CustomMention({ id }: MentionProps) {
|
||||||
|
return <MentionText>{id}</MentionText>;
|
||||||
|
}
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
type: "role" | "user" | "channel";
|
type: "role" | "user" | "channel" | "text";
|
||||||
id: string;
|
id: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,6 +133,7 @@ function Mention({ type, id }: Props) {
|
|||||||
if (type === "role") return <RoleMention id={id} />;
|
if (type === "role") return <RoleMention id={id} />;
|
||||||
if (type === "user") return <UserMention id={id} />;
|
if (type === "user") return <UserMention id={id} />;
|
||||||
if (type === "channel") return <ChannelMention id={id} />;
|
if (type === "channel") return <ChannelMention id={id} />;
|
||||||
|
if (type === "text") return <CustomMention id={id} />;
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,7 +1,8 @@
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import { memo } from "react";
|
import { memo } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Tooltip from "../Tooltip";
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
background-color: hsl(var(--background-tertiary-hsl) / 0.3);
|
background-color: hsl(var(--background-tertiary-hsl) / 0.3);
|
||||||
@ -43,9 +44,17 @@ function Timestamp({ timestamp, style }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
<Tooltip title={date.format("dddd, MMMM MM, h:mm A")} placement="top">
|
<Floating
|
||||||
<span>{value}</span>
|
placement="top"
|
||||||
</Tooltip>
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: <span>{date.format("dddd, MMMM MM, h:mm A")}</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<span>{value}</span>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,10 @@
|
|||||||
code[class*="language-"],
|
code[class*="language-"],
|
||||||
pre[class*="language-"] {
|
pre[class*="language-"] {
|
||||||
color: #f92aad;
|
color: #f92aad;
|
||||||
text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3;
|
text-shadow:
|
||||||
|
0 0 2px #100c0f,
|
||||||
|
0 0 5px #dc078e33,
|
||||||
|
0 0 10px #fff3;
|
||||||
background: none;
|
background: none;
|
||||||
font-family: var(--font-family-code);
|
font-family: var(--font-family-code);
|
||||||
font-size: 1em;
|
font-size: 1em;
|
||||||
@ -74,7 +77,10 @@ pre[class*="language-"] {
|
|||||||
.token.property,
|
.token.property,
|
||||||
.token.selector {
|
.token.selector {
|
||||||
color: #72f1b8;
|
color: #72f1b8;
|
||||||
text-shadow: 0 0 2px #100c0f, 0 0 10px #257c5575, 0 0 35px #21272475;
|
text-shadow:
|
||||||
|
0 0 2px #100c0f,
|
||||||
|
0 0 10px #257c5575,
|
||||||
|
0 0 35px #21272475;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.function-name {
|
.token.function-name {
|
||||||
@ -85,18 +91,29 @@ pre[class*="language-"] {
|
|||||||
.token.selector .token.id,
|
.token.selector .token.id,
|
||||||
.token.function {
|
.token.function {
|
||||||
color: #fdfdfd;
|
color: #fdfdfd;
|
||||||
text-shadow: 0 0 2px #001716, 0 0 3px #03edf975, 0 0 5px #03edf975, 0 0 8px #03edf975;
|
text-shadow:
|
||||||
|
0 0 2px #001716,
|
||||||
|
0 0 3px #03edf975,
|
||||||
|
0 0 5px #03edf975,
|
||||||
|
0 0 8px #03edf975;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.class-name {
|
.token.class-name {
|
||||||
color: #fff5f6;
|
color: #fff5f6;
|
||||||
text-shadow: 0 0 2px #000, 0 0 10px #fc1f2c75, 0 0 5px #fc1f2c75, 0 0 25px #fc1f2c75;
|
text-shadow:
|
||||||
|
0 0 2px #000,
|
||||||
|
0 0 10px #fc1f2c75,
|
||||||
|
0 0 5px #fc1f2c75,
|
||||||
|
0 0 25px #fc1f2c75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.constant,
|
.token.constant,
|
||||||
.token.symbol {
|
.token.symbol {
|
||||||
color: #f92aad;
|
color: #f92aad;
|
||||||
text-shadow: 0 0 2px #100c0f, 0 0 5px #dc078e33, 0 0 10px #fff3;
|
text-shadow:
|
||||||
|
0 0 2px #100c0f,
|
||||||
|
0 0 5px #dc078e33,
|
||||||
|
0 0 10px #fff3;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.important,
|
.token.important,
|
||||||
@ -105,7 +122,10 @@ pre[class*="language-"] {
|
|||||||
.token.selector .token.class,
|
.token.selector .token.class,
|
||||||
.token.builtin {
|
.token.builtin {
|
||||||
color: #f4eee4;
|
color: #f4eee4;
|
||||||
text-shadow: 0 0 2px #393a33, 0 0 8px #f39f0575, 0 0 2px #f39f0575;
|
text-shadow:
|
||||||
|
0 0 2px #393a33,
|
||||||
|
0 0 8px #f39f0575,
|
||||||
|
0 0 2px #f39f0575;
|
||||||
}
|
}
|
||||||
|
|
||||||
.token.string,
|
.token.string,
|
||||||
|
@ -80,16 +80,17 @@ const Content = observer((props: Props2) => {
|
|||||||
function Chat() {
|
function Chat() {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const logger = useLogger("Messages");
|
const logger = useLogger("Messages");
|
||||||
|
const { activeChannel, activeGuild, activeChannelId, activeGuildId } = app;
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (!app.activeChannel || !app.activeGuild || app.activeChannelId === "@me") return;
|
if (!activeChannel || !activeGuild || activeChannelId === "@me") return;
|
||||||
|
|
||||||
runInAction(() => {
|
runInAction(() => {
|
||||||
app.gateway.onChannelOpen(app.activeGuildId!, app.activeChannelId!);
|
app.gateway.onChannelOpen(activeGuildId!, activeChannelId!);
|
||||||
});
|
});
|
||||||
}, [app.activeChannel, app.activeGuild]);
|
}, [activeChannel, activeGuild]);
|
||||||
|
|
||||||
if (app.activeGuildId && app.activeGuildId === "@me") {
|
if (activeGuildId && activeGuildId === "@me") {
|
||||||
return (
|
return (
|
||||||
<WrapperTwo>
|
<WrapperTwo>
|
||||||
<span>Home Section Placeholder</span>
|
<span>Home Section Placeholder</span>
|
||||||
@ -97,7 +98,7 @@ function Chat() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!app.activeGuild || !app.activeChannel) {
|
if (!activeGuild || !activeChannel) {
|
||||||
return (
|
return (
|
||||||
<WrapperTwo>
|
<WrapperTwo>
|
||||||
<span
|
<span
|
||||||
@ -113,10 +114,26 @@ function Chat() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!activeChannel.hasPermission("VIEW_CHANNEL")) {
|
||||||
|
return (
|
||||||
|
<WrapperTwo>
|
||||||
|
<span
|
||||||
|
style={{
|
||||||
|
color: "var(--text-secondary)",
|
||||||
|
fontSize: "1.5rem",
|
||||||
|
margin: "auto",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
You do not have permission to view this channel
|
||||||
|
</span>
|
||||||
|
</WrapperTwo>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WrapperTwo>
|
<WrapperTwo>
|
||||||
<ChatHeader channel={app.activeChannel} />
|
<ChatHeader channel={activeChannel} />
|
||||||
<Content channel={app.activeChannel} guild={app.activeGuild} />
|
<Content channel={activeChannel} guild={activeGuild} />
|
||||||
</WrapperTwo>
|
</WrapperTwo>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,24 +6,22 @@ import { useAppStore } from "../../stores/AppStore";
|
|||||||
import Channel from "../../stores/objects/Channel";
|
import Channel from "../../stores/objects/Channel";
|
||||||
import Icon from "../Icon";
|
import Icon from "../Icon";
|
||||||
import { SectionHeader } from "../SectionHeader";
|
import { SectionHeader } from "../SectionHeader";
|
||||||
import Tooltip from "../Tooltip";
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const IconButton = styled.button`
|
const IconButton = styled.button`
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
background-color: inherit;
|
background-color: inherit;
|
||||||
border: none;
|
border: none;
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: red;
|
|
||||||
}
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
const CustomIcon = styled(Icon)<{ $active?: boolean }>`
|
const CustomIcon = styled(Icon)<{ $active?: boolean }>`
|
||||||
color: ${(props) => (props.$active ? "#ffffff" : "var(--text-secondary)")};
|
color: ${(props) => (props.$active ? "var(--text)" : "var(--text-secondary)")};
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
color: var(--text);
|
color: ${(props) => (props.$active ? "var(--text-secondary)" : "var(--text)")};
|
||||||
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
|
|
||||||
@ -114,19 +112,35 @@ interface ActionItemProps {
|
|||||||
ariaLabel?: string;
|
ariaLabel?: string;
|
||||||
tooltip: string;
|
tooltip: string;
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
|
disabled?: boolean;
|
||||||
|
color?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemProps) {
|
function ActionItem({ icon, active, ariaLabel, tooltip, disabled, color, onClick }: ActionItemProps) {
|
||||||
const logger = useLogger("ChatHeader.tsx:ActionItem");
|
const logger = useLogger("ChatHeader.tsx:ActionItem");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip title={tooltip}>
|
<Floating
|
||||||
<IconWrapper>
|
placement="bottom"
|
||||||
<IconButton onClick={onClick}>
|
type="tooltip"
|
||||||
<CustomIcon $active={active} icon={icon} size="24px" aria-label={ariaLabel} />
|
props={{
|
||||||
</IconButton>
|
content: <span>{tooltip}</span>,
|
||||||
</IconWrapper>
|
}}
|
||||||
</Tooltip>
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<IconWrapper>
|
||||||
|
<IconButton onClick={onClick}>
|
||||||
|
<CustomIcon
|
||||||
|
$active={!disabled && active}
|
||||||
|
icon={icon}
|
||||||
|
size="24px"
|
||||||
|
aria-label={ariaLabel}
|
||||||
|
color={color}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
</IconWrapper>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -134,7 +148,7 @@ function ActionItem({ icon, active, ariaLabel, tooltip, onClick }: ActionItemPro
|
|||||||
* Top header for channel messages section
|
* Top header for channel messages section
|
||||||
*/
|
*/
|
||||||
function ChatHeader({ channel }: Props) {
|
function ChatHeader({ channel }: Props) {
|
||||||
const { memberListVisible, toggleMemberList } = useAppStore();
|
const { memberListVisible, toggleMemberList, updaterStore } = useAppStore();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container>
|
<Container>
|
||||||
@ -144,6 +158,36 @@ function ChatHeader({ channel }: Props) {
|
|||||||
<ChannelTopic channel={channel} />
|
<ChannelTopic channel={channel} />
|
||||||
{/* Action Items */}
|
{/* Action Items */}
|
||||||
<ActionItemsWrapper>
|
<ActionItemsWrapper>
|
||||||
|
{updaterStore?.checkingForUpdates && (
|
||||||
|
<ActionItem
|
||||||
|
icon="mdiCloudSync"
|
||||||
|
tooltip="Checking for Updates"
|
||||||
|
ariaLabel="Checking for Updates"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{updaterStore?.updateAvailable && (
|
||||||
|
<ActionItem icon="mdiUpdate" tooltip="Update Available" ariaLabel="Upate Available" disabled />
|
||||||
|
)}
|
||||||
|
{updaterStore?.updateDownloading && (
|
||||||
|
<ActionItem
|
||||||
|
icon="mdiCloudDownload"
|
||||||
|
tooltip="Downloading Update"
|
||||||
|
ariaLabel="Downloading Update"
|
||||||
|
disabled
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{updaterStore?.updateDownloaded && (
|
||||||
|
<ActionItem
|
||||||
|
icon="mdiDownload"
|
||||||
|
tooltip="Update Ready!"
|
||||||
|
ariaLabel="Update Ready!"
|
||||||
|
color="var(--success)"
|
||||||
|
onClick={() => {
|
||||||
|
updaterStore.quitAndInstall();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{/* <ActionItem icon="mdiPound" ariaLabel="Threads" /> */}
|
{/* <ActionItem icon="mdiPound" ariaLabel="Threads" /> */}
|
||||||
<DummySearch>
|
<DummySearch>
|
||||||
<span>Search</span>
|
<span>Search</span>
|
||||||
|
@ -17,6 +17,7 @@ iframe {
|
|||||||
|
|
||||||
.embedImage {
|
.embedImage {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.website {
|
.website {
|
||||||
@ -159,6 +160,7 @@ iframe {
|
|||||||
|
|
||||||
.embedThumbnail {
|
.embedThumbnail {
|
||||||
margin-left: 16px;
|
margin-left: 16px;
|
||||||
|
border-radius: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
img.image {
|
img.image {
|
||||||
@ -170,3 +172,19 @@ img.image {
|
|||||||
a {
|
a {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.embedGifIcon {
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
left: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.embedGifIconBg {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
left: 8px;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
background: #000000;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
@ -2,6 +2,8 @@
|
|||||||
// https://github.com/revoltchat/revite/blob/master/src/components/common/messaging/embed/Embed.tsx
|
// https://github.com/revoltchat/revite/blob/master/src/components/common/messaging/embed/Embed.tsx
|
||||||
|
|
||||||
import { APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
import { APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
|
import Icon from "../Icon";
|
||||||
import styles from "./Embed.module.css";
|
import styles from "./Embed.module.css";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -30,7 +32,7 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<iframe
|
<iframe
|
||||||
style={{ borderRadius: "12px", width: "400px", height: "80px" }}
|
style={{ width: "400px", height: "80px", borderRadius: 12 }}
|
||||||
src={`https://open.spotify.com/embed/${type}/${id}`}
|
src={`https://open.spotify.com/embed/${type}/${id}`}
|
||||||
frameBorder="0"
|
frameBorder="0"
|
||||||
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
allow="autoplay; clipboard-write; encrypted-media; fullscreen; picture-in-picture"
|
||||||
@ -92,15 +94,31 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
|||||||
const url = embed.video.url;
|
const url = embed.video.url;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<video
|
<div>
|
||||||
className={styles.embedImage}
|
<video
|
||||||
style={{ width, height }}
|
className={styles.embedImage}
|
||||||
src={url}
|
style={{ width, height }}
|
||||||
loop={embed.type === EmbedType.GIFV}
|
src={url}
|
||||||
controls={embed.type === EmbedType.GIFV}
|
loop={embed.type === EmbedType.GIFV}
|
||||||
autoPlay={embed.type === EmbedType.GIFV}
|
controls={embed.type !== EmbedType.GIFV}
|
||||||
muted={embed.type === EmbedType.GIFV ? true : undefined}
|
autoPlay={embed.type === EmbedType.GIFV}
|
||||||
/>
|
muted={embed.type === EmbedType.GIFV ? true : undefined}
|
||||||
|
onClick={() => {
|
||||||
|
modalController.push({
|
||||||
|
type: "image_viewer",
|
||||||
|
attachment: embed.video!,
|
||||||
|
isVideo: true,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{embed.type === EmbedType.GIFV && (
|
||||||
|
<div>
|
||||||
|
<div className={styles.embedGifIconBg}></div>
|
||||||
|
<Icon icon="mdiFileGifBox" size={1} className={styles.embedGifIcon} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else if (embed.image && !thumbnail) {
|
} else if (embed.image && !thumbnail) {
|
||||||
const url = embed.image.url;
|
const url = embed.image.url;
|
||||||
@ -112,9 +130,11 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width: "100%", height: "100%" }}
|
style={{ width: "100%", height: "100%" }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("preview image");
|
modalController.push({
|
||||||
|
type: "image_viewer",
|
||||||
|
attachment: embed.image!,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else if (embed.thumbnail) {
|
} else if (embed.thumbnail) {
|
||||||
@ -127,9 +147,11 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
|||||||
loading="lazy"
|
loading="lazy"
|
||||||
style={{ width, height }}
|
style={{ width, height }}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log("preview image");
|
modalController.push({
|
||||||
|
type: "image_viewer",
|
||||||
|
attachment: embed.thumbnail!,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
onMouseDown={(ev) => ev.button === 1 && window.open(url, "_blank")}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,12 +1,10 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React, { memo } from "react";
|
import { memo, useContext } from "react";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
import { useAppStore } from "../../stores/AppStore";
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import { MessageLike } from "../../stores/objects/Message";
|
import { MessageLike } from "../../stores/objects/Message";
|
||||||
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
||||||
import ContextMenus from "../../utils/ContextMenus";
|
|
||||||
import Avatar from "../Avatar";
|
import Avatar from "../Avatar";
|
||||||
import { IContextMenuItem } from "../ContextMenuItem";
|
|
||||||
import Markdown from "../markdown/MarkdownRenderer";
|
import Markdown from "../markdown/MarkdownRenderer";
|
||||||
import MessageAttachment from "./MessageAttachment";
|
import MessageAttachment from "./MessageAttachment";
|
||||||
import MessageAuthor from "./MessageAuthor";
|
import MessageAuthor from "./MessageAuthor";
|
||||||
@ -21,13 +19,27 @@ interface Props {
|
|||||||
|
|
||||||
function Message({ message, header }: Props) {
|
function Message({ message, header }: Props) {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
const contextMenuContext = useContext(ContextMenuContext);
|
||||||
const [contextMenuItems, setContextMenuItems] = React.useState<IContextMenuItem[]>([
|
|
||||||
...ContextMenus.Message(app, message, app.account),
|
const guild = message.guild_id ? app.guilds.get(message.guild_id) : undefined;
|
||||||
]);
|
const isEveryoneMentioned = "mention_everyone" in message && message.mention_everyone;
|
||||||
|
const isUserMentioned = "mentions" in message && message.mentions.some((mention) => mention.id === app.account!.id);
|
||||||
|
const isRoleMentioned =
|
||||||
|
guild &&
|
||||||
|
"mention_roles" in message &&
|
||||||
|
message.mention_roles.some((r1) => guild.members.me?.roles.some((r2) => r1 === r2.id));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageBase header={header} onContextMenu={(e) => contextMenu.open2(e, contextMenuItems)}>
|
<MessageBase
|
||||||
|
header={header}
|
||||||
|
mention={isEveryoneMentioned || isUserMentioned || isRoleMentioned}
|
||||||
|
onContextMenu={(e) =>
|
||||||
|
contextMenuContext.onContextMenu(e, {
|
||||||
|
type: "message",
|
||||||
|
message: message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
<MessageInfo>
|
<MessageInfo>
|
||||||
{header ? (
|
{header ? (
|
||||||
<Avatar key={message.author.id} user={message.author} size={40} />
|
<Avatar key={message.author.id} user={message.author} size={40} />
|
||||||
@ -38,10 +50,11 @@ function Message({ message, header }: Props) {
|
|||||||
<MessageContent>
|
<MessageContent>
|
||||||
{header && (
|
{header && (
|
||||||
<span className="message-details">
|
<span className="message-details">
|
||||||
<MessageAuthor message={message} />
|
<MessageAuthor message={message} guild={guild} />
|
||||||
<MessageDetails message={message} position="top" />
|
<MessageDetails message={message} position="top" />
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<MessageContentText
|
<MessageContentText
|
||||||
sending={"status" in message && message.status === QueuedMessageStatus.SENDING}
|
sending={"status" in message && message.status === QueuedMessageStatus.SENDING}
|
||||||
failed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
failed={"status" in message && message.status === QueuedMessageStatus.FAILED}
|
||||||
|
@ -1,17 +1,29 @@
|
|||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
|
import { APIAttachment } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import React from "react";
|
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { modalController } from "../../controllers/modals";
|
||||||
import useLogger from "../../hooks/useLogger";
|
import useLogger from "../../hooks/useLogger";
|
||||||
import ContextMenus from "../../utils/ContextMenus";
|
import { getFileDetails, zoomFit } from "../../utils/Utils";
|
||||||
import { calculateImageRatio, calculateScaledDimensions } from "../../utils/Message";
|
|
||||||
import { getFileDetails } from "../../utils/Utils";
|
|
||||||
import { IContextMenuItem } from "../ContextMenuItem";
|
|
||||||
import Audio from "../media/Audio";
|
import Audio from "../media/Audio";
|
||||||
import File from "../media/File";
|
import File from "../media/File";
|
||||||
import Video from "../media/Video";
|
import Video from "../media/Video";
|
||||||
import AttachmentPreviewModal from "../modals/AttachmentPreviewModal";
|
|
||||||
|
const MAX_ATTACHMENT_HEIGHT = 350;
|
||||||
|
|
||||||
|
function adjustDimensions(width: number, height: number): { adjustedWidth: number; adjustedHeight: number } {
|
||||||
|
const aspectRatio = width / height;
|
||||||
|
|
||||||
|
let adjustedWidth: number = width * aspectRatio;
|
||||||
|
let adjustedHeight: number = height * aspectRatio;
|
||||||
|
|
||||||
|
// Ensure the adjusted height does not exceed the maximum height
|
||||||
|
if (adjustedHeight > MAX_ATTACHMENT_HEIGHT) {
|
||||||
|
const scale = MAX_ATTACHMENT_HEIGHT / adjustedHeight;
|
||||||
|
adjustedWidth *= scale;
|
||||||
|
adjustedHeight = MAX_ATTACHMENT_HEIGHT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { adjustedWidth: Math.floor(adjustedWidth), adjustedHeight: Math.floor(adjustedHeight) };
|
||||||
|
}
|
||||||
|
|
||||||
const Attachment = styled.div<{ withPointer?: boolean }>`
|
const Attachment = styled.div<{ withPointer?: boolean }>`
|
||||||
cursor: ${(props) => (props.withPointer ? "pointer" : "default")};
|
cursor: ${(props) => (props.withPointer ? "pointer" : "default")};
|
||||||
@ -20,36 +32,33 @@ const Attachment = styled.div<{ withPointer?: boolean }>`
|
|||||||
|
|
||||||
const Image = styled.img`
|
const Image = styled.img`
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
|
width: 100%;
|
||||||
|
height: auto;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
interface AttachmentProps {
|
interface AttachmentProps {
|
||||||
attachment: APIAttachment;
|
attachment: APIAttachment;
|
||||||
contextMenuItems?: IContextMenuItem[];
|
|
||||||
maxWidth?: number;
|
|
||||||
maxHeight?: number;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function MessageAttachment({ attachment, contextMenuItems, maxWidth, maxHeight }: AttachmentProps) {
|
export default function MessageAttachment({ attachment }: AttachmentProps) {
|
||||||
const logger = useLogger("MessageAttachment");
|
const logger = useLogger("MessageAttachment");
|
||||||
|
|
||||||
const { openModal } = useModals();
|
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
|
||||||
|
|
||||||
const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url;
|
const url = attachment.proxy_url && attachment.proxy_url.length > 0 ? attachment.proxy_url : attachment.url;
|
||||||
|
|
||||||
const details = getFileDetails(attachment);
|
const details = getFileDetails(attachment);
|
||||||
let finalElement: JSX.Element = <></>;
|
let finalElement: JSX.Element = <></>;
|
||||||
if (details.isImage && details.isEmbeddable) {
|
if (details.isImage && details.isEmbeddable) {
|
||||||
const ratio = calculateImageRatio(attachment.width!, attachment.height!, maxWidth, maxHeight);
|
const width = attachment.width!;
|
||||||
const { scaledWidth, scaledHeight } = calculateScaledDimensions(
|
const height = attachment.height!;
|
||||||
attachment.width!,
|
const { adjustedWidth, adjustedHeight } = adjustDimensions(width, height);
|
||||||
attachment.height!,
|
|
||||||
ratio,
|
|
||||||
maxWidth,
|
|
||||||
maxHeight,
|
|
||||||
);
|
|
||||||
finalElement = (
|
finalElement = (
|
||||||
<Image src={url} alt={attachment.filename} width={scaledWidth} height={scaledHeight} loading="lazy" />
|
<Image
|
||||||
|
src={url}
|
||||||
|
alt={attachment.filename}
|
||||||
|
loading="lazy"
|
||||||
|
style={{ maxWidth: adjustedWidth, maxHeight: adjustedHeight }}
|
||||||
|
/>
|
||||||
);
|
);
|
||||||
} else if (details.isVideo && details.isEmbeddable) {
|
} else if (details.isVideo && details.isEmbeddable) {
|
||||||
finalElement = <Video attachment={attachment} />;
|
finalElement = <Video attachment={attachment} />;
|
||||||
@ -63,12 +72,15 @@ export default function MessageAttachment({ attachment, contextMenuItems, maxWid
|
|||||||
<Attachment
|
<Attachment
|
||||||
withPointer={attachment.content_type?.startsWith("image")}
|
withPointer={attachment.content_type?.startsWith("image")}
|
||||||
key={attachment.id}
|
key={attachment.id}
|
||||||
onContextMenu={(e) =>
|
|
||||||
contextMenu.open2(e, [...(contextMenuItems ?? []), ...ContextMenus.MessageAttachment(attachment)])
|
|
||||||
}
|
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (!attachment.content_type?.startsWith("image")) return;
|
if (!attachment.content_type?.startsWith("image")) return;
|
||||||
openModal(AttachmentPreviewModal, { attachment });
|
const { width, height } = zoomFit(attachment.width!, attachment.height!);
|
||||||
|
modalController.push({
|
||||||
|
type: "image_viewer",
|
||||||
|
attachment,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{finalElement}
|
{finalElement}
|
||||||
|
@ -1,12 +1,14 @@
|
|||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React, { useContext } from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||||
import { PopoutContext } from "../../contexts/PopoutContext";
|
import useLogger from "../../hooks/useLogger";
|
||||||
import { useAppStore } from "../../stores/AppStore";
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
|
import Guild from "../../stores/objects/Guild";
|
||||||
|
import GuildMember from "../../stores/objects/GuildMember";
|
||||||
import { MessageLike } from "../../stores/objects/Message";
|
import { MessageLike } from "../../stores/objects/Message";
|
||||||
import ContextMenus from "../../utils/ContextMenus";
|
import Floating from "../floating/Floating";
|
||||||
import UserProfilePopout from "../UserProfilePopout";
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
const Container = styled.div`
|
const Container = styled.div`
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@ -21,57 +23,59 @@ const Container = styled.div`
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
message: MessageLike;
|
message: MessageLike;
|
||||||
|
guild?: Guild;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageAuthor({ message }: Props) {
|
function MessageAuthor({ message, guild }: Props) {
|
||||||
const app = useAppStore();
|
const app = useAppStore();
|
||||||
const contextMenu = React.useContext(ContextMenuContext);
|
const logger = useLogger("MessageAuthor");
|
||||||
const popoutContext = React.useContext(PopoutContext);
|
const contextMenu = useContext(ContextMenuContext);
|
||||||
const [color, setColor] = React.useState<string | undefined>(undefined);
|
const [color, setColor] = React.useState<string | undefined>(undefined);
|
||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const [eventData, setEventData] = React.useState<React.MouseEvent<HTMLDivElement, MouseEvent> | undefined>();
|
||||||
|
const [member, setMember] = React.useState<GuildMember | undefined>(undefined);
|
||||||
|
const { members } = guild || {};
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if ("guild_id" in message && message.guild_id) {
|
if (!eventData) return;
|
||||||
const guild = app.guilds.get(message.guild_id);
|
contextMenu.onContextMenu(eventData, { type: "user", user: message.author, member });
|
||||||
if (!guild) return;
|
}, [eventData, member]);
|
||||||
const member = guild.members.get(message.author.id);
|
|
||||||
if (!member) return;
|
|
||||||
const highestRole = member.roles.reduce((prev, role) => {
|
|
||||||
if (role.position > prev.position) return role;
|
|
||||||
return prev;
|
|
||||||
}, member.roles[0]);
|
|
||||||
if (highestRole?.color === "#000000") return; // TODO: why the fk do we use black as the default color???
|
|
||||||
setColor(highestRole?.color);
|
|
||||||
}
|
|
||||||
}, [message]);
|
|
||||||
|
|
||||||
const openPopout = (e: React.MouseEvent) => {
|
const onContextMenu = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
|
||||||
if (!ref.current) return;
|
setEventData(e);
|
||||||
|
app.guilds.get(message.guild_id!)?.members.resolve(message.author.id).then(setMember);
|
||||||
const rect = ref.current.getBoundingClientRect();
|
|
||||||
if (!rect) return;
|
|
||||||
|
|
||||||
popoutContext.open({
|
|
||||||
element: <UserProfilePopout user={message.author} />,
|
|
||||||
position: rect,
|
|
||||||
placement: "right",
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
React.useEffect(() => {
|
||||||
|
if (!members) return;
|
||||||
|
|
||||||
|
const member = members.get(message.author.id);
|
||||||
|
if (!member) return;
|
||||||
|
setColor(member.roleColor);
|
||||||
|
}, [message, members]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container
|
<Floating
|
||||||
ref={ref}
|
placement="right-start"
|
||||||
style={{
|
type="userPopout"
|
||||||
color,
|
props={{
|
||||||
|
user: message.author,
|
||||||
}}
|
}}
|
||||||
onContextMenu={(e) => contextMenu.open2(e, [...ContextMenus.User(message.author)])}
|
|
||||||
onClick={openPopout}
|
|
||||||
>
|
>
|
||||||
{message.author.username}
|
<FloatingTrigger>
|
||||||
</Container>
|
<Container
|
||||||
|
style={{
|
||||||
|
color,
|
||||||
|
}}
|
||||||
|
ref={contextMenu.setReferenceElement}
|
||||||
|
onContextMenu={onContextMenu}
|
||||||
|
>
|
||||||
|
{message.author.username}
|
||||||
|
</Container>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,7 +3,8 @@ import { observer } from "mobx-react-lite";
|
|||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
import Message, { MessageLike } from "../../stores/objects/Message";
|
import Message, { MessageLike } from "../../stores/objects/Message";
|
||||||
import { calendarStrings } from "../../utils/i18n";
|
import { calendarStrings } from "../../utils/i18n";
|
||||||
import Tooltip from "../Tooltip";
|
import Floating from "../floating/Floating";
|
||||||
|
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
header?: boolean;
|
header?: boolean;
|
||||||
@ -15,8 +16,10 @@ export default styled.div<Props>`
|
|||||||
overflow: none;
|
overflow: none;
|
||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
${(props) => !props.header && "align-items: center;"}
|
${(props) => !props.header && "align-items: center;"}
|
||||||
${(props) => props.header && "margin-top: 20px;"}
|
${(props) => props.header && "margin-top: 10px;"}
|
||||||
${(props) => props.mention && "background-color: hsl(var(--warning-light-hsl)/0.1);"}
|
${(props) => props.mention && "background-color: hsl(var(--warning-light-hsl)/0.1);"}
|
||||||
|
padding-top: 0.2rem;
|
||||||
|
padding-bottom: 0.2rem;
|
||||||
|
|
||||||
.message-details {
|
.message-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -63,7 +66,6 @@ export const MessageInfo = styled.div`
|
|||||||
export const MessageContent = styled.div`
|
export const MessageContent = styled.div`
|
||||||
position: relative;
|
position: relative;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
flex-grow: 1;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -96,18 +98,33 @@ export const MessageDetails = observer(({ message, position }: { message: Messag
|
|||||||
if (message instanceof Message && message.edited_timestamp) {
|
if (message instanceof Message && message.edited_timestamp) {
|
||||||
return (
|
return (
|
||||||
<div className="messageTimestampWrapper">
|
<div className="messageTimestampWrapper">
|
||||||
<Tooltip title={dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")} placement="top">
|
<Floating
|
||||||
<time className="copyTime" dateTime={message.edited_timestamp.toISOString()}>
|
placement="top"
|
||||||
{dayjs(message.edited_timestamp).format("h:mm A")}
|
type="tooltip"
|
||||||
</time>
|
props={{
|
||||||
</Tooltip>
|
content: <span>{dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")}</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<time className="copyTime" dateTime={message.edited_timestamp.toISOString()}>
|
||||||
|
{dayjs(message.edited_timestamp).format("h:mm A")}
|
||||||
|
</time>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
<span className="edited">
|
<span className="edited">
|
||||||
<Tooltip
|
<Floating
|
||||||
title={dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")}
|
|
||||||
placement="top"
|
placement="top"
|
||||||
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: (
|
||||||
|
<span>{dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")}</span>
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<span>(edited)</span>
|
<FloatingTrigger>
|
||||||
</Tooltip>
|
<span>(edited)</span>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@ -121,15 +138,31 @@ export const MessageDetails = observer(({ message, position }: { message: Messag
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DetailBase>
|
<DetailBase>
|
||||||
<Tooltip title={dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")} placement="top">
|
<Floating
|
||||||
<time className="copyTime" dateTime={message.timestamp.toISOString()}>
|
placement="top"
|
||||||
{dayjs(message.timestamp).calendar(undefined, calendarStrings)}
|
type="tooltip"
|
||||||
</time>
|
props={{
|
||||||
</Tooltip>
|
content: <span>{dayjs(message.timestamp).format("dddd, MMMM D, YYYY h:mm A")}</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<time className="copyTime" dateTime={message.timestamp.toISOString()}>
|
||||||
|
{dayjs(message.timestamp).calendar(undefined, calendarStrings)}
|
||||||
|
</time>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
{message instanceof Message && message.edited_timestamp && (
|
{message instanceof Message && message.edited_timestamp && (
|
||||||
<Tooltip title={dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")} placement="top">
|
<Floating
|
||||||
<span className="edited">(edited)</span>
|
placement="top"
|
||||||
</Tooltip>
|
type="tooltip"
|
||||||
|
props={{
|
||||||
|
content: <span>{dayjs(message.edited_timestamp).format("dddd, MMMM D, YYYY h:mm A")}</span>,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<FloatingTrigger>
|
||||||
|
<span className="edited">(edited)</span>
|
||||||
|
</FloatingTrigger>
|
||||||
|
</Floating>
|
||||||
)}
|
)}
|
||||||
</DetailBase>
|
</DetailBase>
|
||||||
);
|
);
|
||||||
|
@ -5,11 +5,13 @@ import { APIEmbed, EmbedType } from "@spacebarchat/spacebar-api-types/v9";
|
|||||||
import classNames from "classnames";
|
import classNames from "classnames";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { decimalColorToHex } from "../../utils/Utils";
|
import { decimalColorToHex } from "../../utils/Utils";
|
||||||
|
import Markdown from "../markdown/Markdown";
|
||||||
|
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 { MESSAGE_AREA_PADDING, MessageAreaWidthContext } from "./MessageList";
|
||||||
|
|
||||||
const MAX_EMBED_WIDTH = 400;
|
const MAX_EMBED_WIDTH = 300;
|
||||||
const MAX_EMBED_HEIGHT = 640;
|
const MAX_EMBED_HEIGHT = 640;
|
||||||
const THUMBNAIL_MAX_WIDTH = 80;
|
const THUMBNAIL_MAX_WIDTH = 80;
|
||||||
const CONTAINER_PADDING = 24;
|
const CONTAINER_PADDING = 24;
|
||||||
@ -26,7 +28,6 @@ function MessageEmbed({ embed }: Props) {
|
|||||||
|
|
||||||
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.
|
||||||
@ -75,14 +76,13 @@ function MessageEmbed({ embed }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { width, height } = calculateSize(mw, mh);
|
const { width, height } = calculateSize(mw, mh);
|
||||||
if (embed.type === EmbedType.GIFV || EMBEDDABLE_PROVIDERS.includes(embed.provider?.name ?? "")) {
|
if (
|
||||||
return (
|
embed.type === EmbedType.GIFV ||
|
||||||
<EmbedMedia
|
embed.type === EmbedType.Image ||
|
||||||
embed={embed}
|
embed.type === EmbedType.Video ||
|
||||||
width={height * ((embed.image?.width ?? 0) / (embed.image?.height ?? 0))}
|
EMBEDDABLE_PROVIDERS.includes(embed.provider?.name ?? "")
|
||||||
height={height}
|
) {
|
||||||
/>
|
return <EmbedMedia embed={embed} width={height} height={height} />;
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -141,7 +141,11 @@ function MessageEmbed({ embed }: Props) {
|
|||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{embed.description && <div className={styles.embedDescription}>{embed.description}</div>}
|
{embed.description && (
|
||||||
|
<div className={styles.embedDescription}>
|
||||||
|
<MarkdownRenderer content={embed.description} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{embed.fields && (
|
{embed.fields && (
|
||||||
<div className={styles.embedFields}>
|
<div className={styles.embedFields}>
|
||||||
@ -156,7 +160,9 @@ function MessageEmbed({ embed }: Props) {
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className={styles.embedFieldName}>{field.name}</div>
|
<div className={styles.embedFieldName}>{field.name}</div>
|
||||||
<div className={styles.embedFieldValue}>{field.value}</div>
|
<div className={styles.embedFieldValue}>
|
||||||
|
<Markdown content={field.value} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,18 +1,17 @@
|
|||||||
import Channel from "../../stores/objects/Channel";
|
import Channel from "../../stores/objects/Channel";
|
||||||
|
|
||||||
import { useModals } from "@mattjennings/react-modal-stack";
|
|
||||||
import { ChannelType, MessageType, RESTPostAPIChannelMessageJSONBody } from "@spacebarchat/spacebar-api-types/v9";
|
import { ChannelType, MessageType, RESTPostAPIChannelMessageJSONBody } from "@spacebarchat/spacebar-api-types/v9";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import styled from "styled-components";
|
import styled from "styled-components";
|
||||||
|
|
||||||
|
import { modalController } from "../../controllers/modals";
|
||||||
import useLogger from "../../hooks/useLogger";
|
import useLogger from "../../hooks/useLogger";
|
||||||
import { useAppStore } from "../../stores/AppStore";
|
import { useAppStore } from "../../stores/AppStore";
|
||||||
import Guild from "../../stores/objects/Guild";
|
import Guild from "../../stores/objects/Guild";
|
||||||
import Snowflake from "../../utils/Snowflake";
|
import Snowflake from "../../utils/Snowflake";
|
||||||
import { MAX_ATTACHMENTS } from "../../utils/constants";
|
import { MAX_ATTACHMENTS } from "../../utils/constants";
|
||||||
import { debounce } from "../../utils/debounce";
|
import { debounce } from "../../utils/debounce";
|
||||||
import { isTouchscreenDevice } from "../../utils/isTouchscreenDevice";
|
|
||||||
import ErrorModal from "../modals/ErrorModal";
|
|
||||||
import MessageTextArea from "./MessageTextArea";
|
import MessageTextArea from "./MessageTextArea";
|
||||||
import AttachmentUpload from "./attachments/AttachmentUpload";
|
import AttachmentUpload from "./attachments/AttachmentUpload";
|
||||||
import AttachmentUploadList from "./attachments/AttachmentUploadPreview";
|
import AttachmentUploadList from "./attachments/AttachmentUploadPreview";
|
||||||
@ -60,7 +59,6 @@ function MessageInput({ channel }: Props) {
|
|||||||
const logger = useLogger("MessageInput");
|
const logger = useLogger("MessageInput");
|
||||||
const [content, setContent] = React.useState("");
|
const [content, setContent] = React.useState("");
|
||||||
const [attachments, setAttachments] = React.useState<File[]>([]);
|
const [attachments, setAttachments] = React.useState<File[]>([]);
|
||||||
const { openModal } = useModals();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Debounced stopTyping
|
* Debounced stopTyping
|
||||||
@ -139,7 +137,7 @@ function MessageInput({ channel }: Props) {
|
|||||||
|
|
||||||
// TODO: handle editing last message
|
// TODO: handle editing last message
|
||||||
|
|
||||||
if (!e.shiftKey && e.key === "Enter" && !isTouchscreenDevice) {
|
if (!e.shiftKey && e.key === "Enter") {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
return sendMessage();
|
return sendMessage();
|
||||||
}
|
}
|
||||||
@ -161,13 +159,10 @@ function MessageInput({ channel }: Props) {
|
|||||||
const appendAttachment = (files: File[]) => {
|
const appendAttachment = (files: File[]) => {
|
||||||
if (files.length === 0) return;
|
if (files.length === 0) return;
|
||||||
if (files.length > MAX_ATTACHMENTS || attachments.length + files.length > MAX_ATTACHMENTS) {
|
if (files.length > MAX_ATTACHMENTS || attachments.length + files.length > MAX_ATTACHMENTS) {
|
||||||
openModal(ErrorModal, {
|
modalController.push({
|
||||||
|
type: "error",
|
||||||
title: "Too many attachments",
|
title: "Too many attachments",
|
||||||
message: (
|
error: `You can only attach ${MAX_ATTACHMENTS} files at once.`,
|
||||||
<div style={{ justifyContent: "center", display: "flex" }}>
|
|
||||||
You can only attach {MAX_ATTACHMENTS} files at once.
|
|
||||||
</div>
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -202,7 +197,7 @@ function MessageInput({ channel }: Props) {
|
|||||||
channel.type === ChannelType.DM
|
channel.type === ChannelType.DM
|
||||||
? channel.recipients?.[0].username
|
? channel.recipients?.[0].username
|
||||||
: "#" + channel.name
|
: "#" + channel.name
|
||||||
}`
|
}`
|
||||||
: "You do not have permission to send messages in this channel."
|
: "You do not have permission to send messages in this channel."
|
||||||
}
|
}
|
||||||
disabled={!channel.hasPermission("SEND_MESSAGES")}
|
disabled={!channel.hasPermission("SEND_MESSAGES")}
|
||||||
|
@ -44,25 +44,34 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
const ref = React.useRef<HTMLDivElement>(null);
|
const ref = React.useRef<HTMLDivElement>(null);
|
||||||
const { width } = useResizeObserver<HTMLDivElement>({ ref });
|
const { width } = useResizeObserver<HTMLDivElement>({ ref });
|
||||||
|
|
||||||
// handles the permission check
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
const permission = Permissions.getPermission(app.account!.id, guild, channel);
|
const permission = Permissions.getPermission(app.account!.id, guild, channel);
|
||||||
setCanView(permission.has("READ_MESSAGE_HISTORY"));
|
const hasPermission = permission.has("READ_MESSAGE_HISTORY");
|
||||||
}, [guild, channel]);
|
setCanView(hasPermission);
|
||||||
|
|
||||||
|
if (!hasPermission) {
|
||||||
|
logger.debug("User cannot view this channel. Aborting initial message fetch.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// handles the initial fetch of channel messages
|
|
||||||
React.useEffect(() => {
|
|
||||||
if (!canView) return;
|
|
||||||
if (guild && channel && channel.messages.count === 0) {
|
if (guild && channel && channel.messages.count === 0) {
|
||||||
channel.getMessages(app, true).then((r) => {
|
channel.getMessages(app, true).then((r) => {
|
||||||
if (r !== 50) setHasMore(false);
|
if (r < 50) {
|
||||||
else setHasMore(true);
|
setHasMore(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, [guild, channel, canView]);
|
|
||||||
|
return () => {
|
||||||
|
logger.debug("MessageList unmounted");
|
||||||
|
setHasMore(true);
|
||||||
|
setCanView(false);
|
||||||
|
};
|
||||||
|
}, [guild, channel]);
|
||||||
|
|
||||||
const fetchMore = React.useCallback(() => {
|
const fetchMore = React.useCallback(() => {
|
||||||
if (!channel.messages.count) {
|
if (!channel.messages.count) {
|
||||||
|
logger.warn("channel has no messages, aborting!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
// get last group
|
// get last group
|
||||||
@ -77,16 +86,17 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
const before = lastGroup.messages[0].id;
|
const before = lastGroup.messages[0].id;
|
||||||
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
|
||||||
channel.getMessages(app, false, 50, before).then((r) => {
|
channel.getMessages(app, false, 50, before).then((r) => {
|
||||||
if (r !== 50) setHasMore(false);
|
if (r < 50) {
|
||||||
else setHasMore(true);
|
setHasMore(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}, [channel, messageGroups, setHasMore]);
|
}, [channel, messageGroups]);
|
||||||
|
|
||||||
const renderGroup = React.useCallback(
|
const renderGroup = React.useCallback(
|
||||||
(group: MessageGroupType) => (
|
(group: MessageGroupType) => (
|
||||||
<MessageGroup key={`messageGroup-${group.messages[group.messages.length - 1].id}`} group={group} />
|
<MessageGroup key={`messageGroup-${group.messages[group.messages.length - 1].id}`} group={group} />
|
||||||
),
|
),
|
||||||
[],
|
[messageGroups],
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -100,6 +110,7 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
flexDirection: "column-reverse",
|
flexDirection: "column-reverse",
|
||||||
marginBottom: 30,
|
marginBottom: 30,
|
||||||
|
overflow: "hidden",
|
||||||
}} // to put endMessage and loader to the top.
|
}} // to put endMessage and loader to the top.
|
||||||
hasMore={hasMore}
|
hasMore={hasMore}
|
||||||
inverse={true}
|
inverse={true}
|
||||||
@ -109,11 +120,13 @@ function MessageList({ guild, channel }: Props) {
|
|||||||
display: "flex",
|
display: "flex",
|
||||||
justifyContent: "center",
|
justifyContent: "center",
|
||||||
alignContent: "center",
|
alignContent: "center",
|
||||||
marginBottom: 30,
|
margin: 30,
|
||||||
}}
|
}}
|
||||||
color="var(--primary)"
|
color="var(--primary)"
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
|
// FIXME: seems to be broken in react-infinite-scroll-component when using inverse
|
||||||
|
scrollThreshold={0.5}
|
||||||
scrollableTarget="scrollable-div"
|
scrollableTarget="scrollable-div"
|
||||||
endMessage={
|
endMessage={
|
||||||
<EndMessageContainer>
|
<EndMessageContainer>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user