mirror of
https://github.com/spacebarchat/client.git
synced 2024-11-24 11:22:29 +01:00
Merge branch 'dev'
This commit is contained in:
commit
c480f86799
100
package.json
100
package.json
@ -4,64 +4,64 @@
|
||||
"url": "https://github.com/spacebarchat/client/issues"
|
||||
},
|
||||
"dependencies": {
|
||||
"@emotion/react": "^11.11.4",
|
||||
"@emotion/styled": "^11.11.0",
|
||||
"@floating-ui/react": "^0.26.17",
|
||||
"@fontsource/roboto": "^5.0.8",
|
||||
"@fontsource/roboto-mono": "^5.0.16",
|
||||
"@hcaptcha/react-hcaptcha": "^1.10.1",
|
||||
"@hookform/resolvers": "^3.3.4",
|
||||
"@emotion/react": "^11.13.3",
|
||||
"@emotion/styled": "^11.13.0",
|
||||
"@floating-ui/react": "^0.26.24",
|
||||
"@fontsource/roboto": "^5.1.0",
|
||||
"@fontsource/roboto-mono": "^5.1.0",
|
||||
"@hcaptcha/react-hcaptcha": "^1.11.0",
|
||||
"@hookform/resolvers": "^3.9.0",
|
||||
"@mattjennings/react-modal-stack": "^1.0.4",
|
||||
"@mdi/js": "^7.4.47",
|
||||
"@mdi/react": "^1.6.1",
|
||||
"@mui/material": "^5.15.14",
|
||||
"@mui/material": "^6.1.3",
|
||||
"@originjs/vite-plugin-commonjs": "^1.0.3",
|
||||
"@rollup/plugin-replace": "^5.0.5",
|
||||
"@rollup/plugin-replace": "^6.0.1",
|
||||
"@spacebarchat/spacebar-api-types": "0.37.51",
|
||||
"@tauri-apps/api": "2.0.0-beta.7",
|
||||
"@tauri-apps/plugin-authenticator": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0-beta.5",
|
||||
"@tauri-apps/plugin-log": "2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-notification": "2.0.0-beta.1",
|
||||
"@tauri-apps/plugin-os": "2.0.0-beta.1",
|
||||
"@tauri-apps/plugin-process": "2.0.0-beta.2",
|
||||
"@tauri-apps/plugin-stronghold": "2.0.0-beta.3",
|
||||
"@tauri-apps/plugin-updater": "2.0.0-beta.1",
|
||||
"@testing-library/jest-dom": "^6.4.2",
|
||||
"@testing-library/react": "^15.0.2",
|
||||
"@tauri-apps/api": "2.0.2",
|
||||
"@tauri-apps/plugin-authenticator": "2.0.0-rc.1",
|
||||
"@tauri-apps/plugin-autostart": "2.0.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "2.0.0",
|
||||
"@tauri-apps/plugin-dialog": "2.0.0",
|
||||
"@tauri-apps/plugin-log": "2.0.0",
|
||||
"@tauri-apps/plugin-notification": "2.0.0",
|
||||
"@tauri-apps/plugin-os": "2.0.0",
|
||||
"@tauri-apps/plugin-process": "2.0.0",
|
||||
"@tauri-apps/plugin-stronghold": "2.0.0",
|
||||
"@tauri-apps/plugin-updater": "2.0.0",
|
||||
"@testing-library/jest-dom": "^6.5.0",
|
||||
"@testing-library/react": "^16.0.1",
|
||||
"@testing-library/user-event": "^14.5.2",
|
||||
"@types/react-measure": "^2.0.12",
|
||||
"@types/react-portal": "^4.0.7",
|
||||
"@uidotdev/usehooks": "^2.4.1",
|
||||
"classnames": "^2.5.1",
|
||||
"csstype": "^3.1.3",
|
||||
"dayjs": "^1.11.10",
|
||||
"framer-motion": "^11.1.3",
|
||||
"dayjs": "^1.11.13",
|
||||
"framer-motion": "^11.11.4",
|
||||
"marked-react": "^2.0.0",
|
||||
"missing-native-js-functions": "^1.4.3",
|
||||
"mobx": "^6.12.4",
|
||||
"mobx": "^6.13.3",
|
||||
"mobx-react-lite": "^4.0.7",
|
||||
"murmurhash-js": "^1.0.0",
|
||||
"prismjs": "^1.29.0",
|
||||
"react": "^18.2.0",
|
||||
"react-advanced-cropper": "^0.19.6",
|
||||
"react": "^18.3.1",
|
||||
"react-advanced-cropper": "^0.20.0",
|
||||
"react-colorful": "^5.6.1",
|
||||
"react-device-detect": "^2.2.3",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-fps-stats": "^0.3.1",
|
||||
"react-hook-form": "^7.51.3",
|
||||
"react-hook-form": "^7.53.0",
|
||||
"react-infinite-scroll-component": "^6.1.0",
|
||||
"react-loading-skeleton": "^3.4.0",
|
||||
"react-loading-skeleton": "^3.5.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-measure": "^2.5.2",
|
||||
"react-portal": "^4.2.2",
|
||||
"react-router-dom": "^6.22.3",
|
||||
"react-router-dom": "^6.26.2",
|
||||
"react-secure-storage": "^1.3.2",
|
||||
"react-select-search": "^4.1.7",
|
||||
"react-spinners": "^0.13.8",
|
||||
"react-spring": "^9.7.3",
|
||||
"react-select-search": "^4.1.8",
|
||||
"react-spinners": "^0.14.1",
|
||||
"react-spring": "^9.7.4",
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-syntax-highlighter": "^15.5.0",
|
||||
"react-use-error-boundary": "^3.0.0",
|
||||
@ -75,26 +75,25 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@craco/craco": "^7.1.0",
|
||||
"@tauri-apps/cli": "2.0.0-beta.5",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@tauri-apps/cli": "2.0.2",
|
||||
"@types/jest": "^29.5.13",
|
||||
"@types/loadable__component": "^5.13.9",
|
||||
"@types/murmurhash-js": "^1.0.6",
|
||||
"@types/node": "^20.14.2",
|
||||
"@types/react": "^18.2.79",
|
||||
"@types/react-dom": "^18.2.25",
|
||||
"@types/react-syntax-highlighter": "^15.5.11",
|
||||
"@types/react-virtualized": "^9.21.29",
|
||||
"@types/node": "^22.7.5",
|
||||
"@types/react": "^18.3.11",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@types/styled-components": "^5.1.34",
|
||||
"@typescript-eslint/eslint-plugin": "^7.1.0",
|
||||
"@typescript-eslint/parser": "^7.8.0",
|
||||
"@vitejs/plugin-react": "^4.2.1",
|
||||
"@typescript-eslint/eslint-plugin": "^8.7.0",
|
||||
"@typescript-eslint/parser": "^8.7.0",
|
||||
"@vitejs/plugin-react": "^4.3.2",
|
||||
"cross-env": "^7.0.3",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.6",
|
||||
"internal-ip": "^8.0.0",
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.14",
|
||||
"eslint": "^8.57.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"eslint-plugin-react-refresh": "^0.4.12",
|
||||
"typescript": "^5.6.3",
|
||||
"vite": "^5.4.8",
|
||||
"vite-plugin-chunk-split": "^0.5.0",
|
||||
"vite-plugin-clean": "^1.0.0",
|
||||
"vite-plugin-html": "^3.2.2",
|
||||
@ -104,7 +103,6 @@
|
||||
"homepage": "https://spacebar.chat",
|
||||
"license": "AGPL-3.0-only",
|
||||
"name": "spacebar-client",
|
||||
"packageManager": "pnpm@8.14.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/spacebarchat/client.git"
|
||||
|
23595
pnpm-lock.yaml
23595
pnpm-lock.yaml
File diff suppressed because it is too large
Load Diff
1737
src-tauri/Cargo.lock
generated
1737
src-tauri/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -14,26 +14,26 @@ crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2.0.0-beta.15", features = [] }
|
||||
tauri-build = { version = "2.0.1", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2.0.0-beta.19", features = ["devtools", "tray-icon"] }
|
||||
tauri-plugin-updater = "2.0.0-beta.4"
|
||||
tauri-plugin-process = "2.0.0-alpha"
|
||||
tauri-plugin-log = "2.0.0-alpha"
|
||||
tauri-plugin-os = "2.0.0-alpha"
|
||||
reqwest = { version = "0.12.4", default-features = false, features = [
|
||||
tauri = { version = "2.0.2", features = ["devtools", "tray-icon"] }
|
||||
tauri-plugin-updater = "2.0.2"
|
||||
tauri-plugin-process = "2.0.1"
|
||||
tauri-plugin-log = "2.0.1"
|
||||
tauri-plugin-os = "2.0.1"
|
||||
tauri-plugin-notification = "2.0.1"
|
||||
tauri-plugin-single-instance = "2.0.1"
|
||||
tauri-plugin-autostart = "2.0.1"
|
||||
reqwest = { version = "0.12.8", default-features = false, features = [
|
||||
"json",
|
||||
"rustls-tls",
|
||||
] }
|
||||
url = "2.4.1"
|
||||
url = "2.5.2"
|
||||
chrono = "0.4"
|
||||
log = "0.4.20"
|
||||
log = "0.4.22"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
tauri-plugin-notification = "2.0.0-beta.5"
|
||||
tauri-plugin-single-instance = "2.0.0-beta.6"
|
||||
tauri-plugin-autostart = "2.0.0-beta.4"
|
||||
|
||||
[features]
|
||||
# this feature is used for production builds or when `devPath` points to the filesystem
|
||||
|
@ -3,13 +3,13 @@
|
||||
"description": "base",
|
||||
"windows": ["main", "splashscreen"],
|
||||
"permissions": [
|
||||
"path:default",
|
||||
"event:default",
|
||||
"window:default",
|
||||
"app:default",
|
||||
"resources:default",
|
||||
"menu:default",
|
||||
"tray:default",
|
||||
"core:path:default",
|
||||
"core:event:default",
|
||||
"core:window:default",
|
||||
"core:app:default",
|
||||
"core:resources:default",
|
||||
"core:menu:default",
|
||||
"core:tray:default",
|
||||
"updater:default",
|
||||
"notification:default",
|
||||
"os:allow-platform",
|
||||
@ -18,7 +18,7 @@
|
||||
"os:allow-locale",
|
||||
"os:allow-os-type",
|
||||
"os:allow-version",
|
||||
"webview:allow-internal-toggle-devtools"
|
||||
"core:webview:allow-internal-toggle-devtools"
|
||||
],
|
||||
"platforms": ["linux", "macOS", "windows", "android", "iOS"]
|
||||
}
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +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"]}}
|
||||
{"base":{"identifier":"base","description":"base","local":true,"windows":["main","splashscreen"],"permissions":["core:path:default","core:event:default","core:window:default","core:app:default","core:resources:default","core:menu:default","core: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","core:webview:allow-internal-toggle-devtools"],"platforms":["linux","macOS","windows","android","iOS"]}}
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -1,6 +1,7 @@
|
||||
use reqwest;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Mutex;
|
||||
use tauri::Emitter;
|
||||
use tauri::{Manager, Runtime};
|
||||
use tauri_plugin_updater::{Update, UpdaterExt};
|
||||
use url::Url;
|
||||
@ -201,6 +202,7 @@ pub fn check_for_updates<R: Runtime>(ignore_prereleases: bool, window: tauri::Wi
|
||||
return false;
|
||||
})
|
||||
.endpoints(vec![tauri_release_endpoint])
|
||||
.unwrap()
|
||||
.header("User-Agent", "spacebar-client")
|
||||
{
|
||||
Ok(updater_builder) => updater_builder,
|
||||
|
@ -30,7 +30,7 @@
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": ["deb", "rpm", "appimage", "nsis", "app", "dmg", "updater"],
|
||||
"targets": ["deb", "rpm", "appimage", "nsis", "app", "dmg"],
|
||||
"icon": ["icons/32x32.png", "icons/128x128.png", "icons/128x128@2x.png", "icons/icon.icns", "icons/icon.ico"],
|
||||
"publisher": "Spacebar",
|
||||
"category": "SocialNetworking",
|
||||
@ -41,7 +41,8 @@
|
||||
"sidebarImage": "./icons/sidebar.bmp",
|
||||
"installerIcon": "./icons/icon.ico"
|
||||
}
|
||||
}
|
||||
},
|
||||
"createUpdaterArtifacts": true
|
||||
},
|
||||
"plugins": {
|
||||
"shell": {
|
||||
|
@ -17,11 +17,11 @@ import useLogger from "./hooks/useLogger";
|
||||
import AppPage from "./pages/AppPage";
|
||||
import LogoutPage from "./pages/LogoutPage";
|
||||
import ChannelPage from "./pages/subpages/ChannelPage";
|
||||
import { useAppStore } from "./stores/AppStore";
|
||||
import { Globals } from "./utils/Globals";
|
||||
// @ts-expect-error no types
|
||||
import FPSStats from "react-fps-stats";
|
||||
import { bannerController } from "./controllers/banners";
|
||||
import { useAppStore } from "./hooks/useAppStore";
|
||||
import { isTauri } from "./utils/Utils";
|
||||
|
||||
function App() {
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import React, { useRef } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import AccountStore from "../stores/AccountStore";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Presence from "../stores/objects/Presence";
|
||||
import User from "../stores/objects/User";
|
||||
import Container from "./Container";
|
||||
@ -12,6 +12,7 @@ const Wrapper = styled(Container)<{ size: number; hasClick?: boolean }>`
|
||||
background-color: transparent;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
user-select: none;
|
||||
`;
|
||||
|
||||
interface Props {
|
||||
@ -32,7 +33,7 @@ interface Props {
|
||||
function Avatar(props: Props) {
|
||||
const app = useAppStore();
|
||||
|
||||
const ref = React.useRef<HTMLDivElement>(null);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const user = props.user ?? app.account;
|
||||
if (!user) return null;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import Icon, { IconProps } from "./Icon";
|
||||
import { SectionHeader } from "./SectionHeader";
|
||||
import Floating from "./floating/Floating";
|
||||
|
@ -2,7 +2,7 @@ import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { AutoSizer, List, ListRowProps } from "react-virtualized";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import ChannelListItem from "./ChannelListItem";
|
||||
|
||||
const Container = styled.div`
|
||||
|
@ -1,12 +1,14 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React, { useContext, useEffect } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import { Permissions } from "../../utils/Permissions";
|
||||
import Icon from "../Icon";
|
||||
import SidebarPill from "../SidebarPill";
|
||||
import Floating from "../floating/Floating";
|
||||
import FloatingTrigger from "../floating/FloatingTrigger";
|
||||
|
||||
@ -88,6 +90,7 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<SidebarPill type={channel.unread ? "unread" : "none"} />
|
||||
{channel.channelIcon && !isCategory && (
|
||||
<Icon
|
||||
icon={channel.channelIcon}
|
||||
@ -163,4 +166,4 @@ function ChannelListItem({ channel, isCategory, active }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
export default ChannelListItem;
|
||||
export default observer(ChannelListItem);
|
||||
|
@ -188,6 +188,8 @@ export class DOBInput extends Component<Props, State> {
|
||||
onChange={(e) => this.setState({ ...this.state, month: e as string })}
|
||||
value={this.state.month}
|
||||
disabled={this.props.disabled}
|
||||
onBlur={() => {}}
|
||||
onFocus={() => {}}
|
||||
/>
|
||||
<CustomInput
|
||||
placeholder="Day"
|
||||
|
@ -4,8 +4,8 @@ import React, { useContext } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import { Permissions } from "../utils/Permissions";
|
||||
import REST from "../utils/REST";
|
||||
@ -30,9 +30,7 @@ const Wrapper = styled(Container)<{ active?: boolean; hasImage?: boolean }>`
|
||||
border-radius: ${(props) => (props.active ? "30%" : "50%")};
|
||||
background-color: ${(props) =>
|
||||
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 {
|
||||
border-radius: 30%;
|
||||
|
@ -4,7 +4,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import { AutoSizer, List, ListRowProps } from "react-virtualized";
|
||||
import styled from "styled-components";
|
||||
import { modalController } from "../controllers/modals";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import GuildItem, { GuildSidebarListItem } from "./GuildItem";
|
||||
import SidebarAction from "./SidebarAction";
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { invoke } from "@tauri-apps/api/core";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import LoadingPage from "../pages/LoadingPage";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { isTauri } from "../utils/Utils";
|
||||
|
||||
interface Props {
|
||||
|
@ -2,7 +2,7 @@ import { autorun } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import GuildMemberListStore from "../../stores/GuildMemberListStore";
|
||||
import ListSection from "../ListSection";
|
||||
import MemberListItem from "./MemberListItem";
|
||||
|
@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { useContext } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import GuildMember from "../../stores/objects/GuildMember";
|
||||
import Avatar from "../Avatar";
|
||||
import Floating from "../floating/Floating";
|
||||
|
@ -5,7 +5,7 @@ export type PillType = "none" | "unread" | "hover" | "active";
|
||||
|
||||
const Wrapper = styled(Container)`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
// top: 0;
|
||||
left: 0;
|
||||
width: 8px;
|
||||
height: 48px;
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components";
|
||||
import { modalController } from "../controllers/modals";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import Avatar from "./Avatar";
|
||||
import Icon from "./Icon";
|
||||
import IconButton from "./IconButton";
|
||||
|
@ -2,8 +2,8 @@
|
||||
|
||||
import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import Message from "../../stores/objects/Message";
|
||||
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
// 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 { useAppStore } from "../../hooks/useAppStore";
|
||||
import GuildMember from "../../stores/objects/GuildMember";
|
||||
import User from "../../stores/objects/User";
|
||||
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";
|
||||
|
@ -3,7 +3,7 @@ import useLogger from "../../hooks/useLogger";
|
||||
|
||||
import React, { useEffect } from "react";
|
||||
import { modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { Permissions } from "../../utils/Permissions";
|
||||
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "../contextMenus/ContextMenu";
|
||||
|
||||
|
@ -9,7 +9,7 @@ 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 { useAppStore } from "../../hooks/useAppStore";
|
||||
import REST from "../../utils/REST";
|
||||
import Floating from "./Floating";
|
||||
import FloatingTrigger from "./FloatingTrigger";
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { LoadingSuspense } from "../../pages/LoadingPage";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
|
||||
interface Props {
|
||||
component: React.FC;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { Navigate } from "react-router-dom";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { LoadingSuspense } from "../../pages/LoadingPage";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
|
||||
interface Props {
|
||||
component: React.FC;
|
||||
|
@ -2,7 +2,7 @@ import React, { memo } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Role from "../../stores/objects/Role";
|
||||
import User from "../../stores/objects/User";
|
||||
|
@ -1,9 +1,9 @@
|
||||
import { runInAction } from "mobx";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import React from "react";
|
||||
import React, { useEffect } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import MemberList from "../MemberList/MemberList";
|
||||
@ -54,9 +54,16 @@ interface Props2 {
|
||||
}
|
||||
|
||||
function ChatContent({ channel, guild }: Props2) {
|
||||
const app = useAppStore();
|
||||
const readstate = app.readStateStore.get(channel.id);
|
||||
|
||||
useEffect(() => {
|
||||
channel.markAsRead();
|
||||
}, [channel, guild]);
|
||||
|
||||
return (
|
||||
<Container>
|
||||
<MessageList guild={guild} channel={channel} />
|
||||
<MessageList guild={guild} channel={channel} before={readstate?.lastMessageId} />
|
||||
<MessageInput channel={channel} guild={guild} />
|
||||
<TypingIndicator channel={channel} />
|
||||
</Container>
|
||||
|
@ -1,8 +1,8 @@
|
||||
import * as Icons from "@mdi/js";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Icon from "../Icon";
|
||||
import { SectionHeader } from "../SectionHeader";
|
||||
|
@ -70,7 +70,7 @@ function EmbedMedia({ embed, width, height, thumbnail }: Props) {
|
||||
height = newHeight;
|
||||
}
|
||||
|
||||
console.log(`Original size: ${originalWidth}x${originalHeight} - Scaled size: ${width}x${height}`);
|
||||
// console.log(`Original size: ${originalWidth}x${originalHeight} - Scaled size: ${width}x${height}`);
|
||||
|
||||
switch (embed.provider?.name) {
|
||||
case "YouTube": {
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { memo, useContext } from "react";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { MessageLike } from "../../stores/objects/Message";
|
||||
import { QueuedMessageStatus } from "../../stores/objects/QueuedMessage";
|
||||
import Avatar from "../Avatar";
|
||||
|
@ -2,8 +2,8 @@ import { observer } from "mobx-react-lite";
|
||||
import React, { useContext } from "react";
|
||||
import styled from "styled-components";
|
||||
import { ContextMenuContext } from "../../contexts/ContextMenuContext";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import GuildMember from "../../stores/objects/GuildMember";
|
||||
import { MessageLike } from "../../stores/objects/Message";
|
||||
@ -13,7 +13,6 @@ import FloatingTrigger from "../floating/FloatingTrigger";
|
||||
const Container = styled.div`
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
user-select: none;
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
@ -37,7 +36,7 @@ function MessageAuthor({ message, guild }: Props) {
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!eventData) return;
|
||||
contextMenu.onContextMenu(eventData, { type: "user", user: message.author, member });
|
||||
contextMenu?.onContextMenu(eventData, { type: "user", user: message.author, member });
|
||||
}, [eventData, member]);
|
||||
|
||||
const onContextMenu = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
@ -69,7 +68,7 @@ function MessageAuthor({ message, guild }: Props) {
|
||||
style={{
|
||||
color,
|
||||
}}
|
||||
ref={contextMenu.setReferenceElement}
|
||||
ref={contextMenu?.setReferenceElement}
|
||||
onContextMenu={onContextMenu}
|
||||
>
|
||||
{message.author.username}
|
||||
|
@ -6,8 +6,8 @@ import React from "react";
|
||||
import styled from "styled-components";
|
||||
|
||||
import { modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
import Snowflake from "../../utils/Snowflake";
|
||||
import { MAX_ATTACHMENTS } from "../../utils/constants";
|
||||
@ -197,7 +197,7 @@ function MessageInput({ channel }: Props) {
|
||||
channel.type === ChannelType.DM
|
||||
? channel.recipients?.[0].username
|
||||
: "#" + channel.name
|
||||
}`
|
||||
}`
|
||||
: "You do not have permission to send messages in this channel."
|
||||
}
|
||||
disabled={!channel.hasPermission("SEND_MESSAGES")}
|
||||
|
@ -4,8 +4,8 @@ import InfiniteScroll from "react-infinite-scroll-component";
|
||||
import PulseLoader from "react-spinners/PulseLoader";
|
||||
import styled from "styled-components";
|
||||
import useResizeObserver from "use-resize-observer";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { MessageGroup as MessageGroupType } from "../../stores/MessageStore";
|
||||
import Channel from "../../stores/objects/Channel";
|
||||
import Guild from "../../stores/objects/Guild";
|
||||
@ -30,6 +30,7 @@ const EndMessageContainer = styled.div`
|
||||
interface Props {
|
||||
guild: Guild;
|
||||
channel: Channel;
|
||||
before?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../../stores/AppStore";
|
||||
import { useAppStore } from "../../../hooks/useAppStore";
|
||||
import QueuedMessage from "../../../stores/objects/QueuedMessage";
|
||||
import { bytesToSize } from "../../../utils/Utils";
|
||||
import Icon from "../../Icon";
|
||||
|
@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
|
||||
import styled from "styled-components";
|
||||
import * as yup from "yup";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { Modal } from "./ModalComponents";
|
||||
|
||||
const DescriptionText = styled.p`
|
||||
@ -54,7 +54,7 @@ export function BanMemberModal({ target, type, ...props }: ModalProps<"ban_membe
|
||||
data.reason
|
||||
? {
|
||||
"X-Audit-Log-Reason": data.reason,
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.then(() => {
|
||||
|
@ -11,7 +11,7 @@ import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import * as yup from "yup";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText } from "../AuthComponents";
|
||||
import { TextDivider } from "../Divider";
|
||||
|
@ -4,8 +4,8 @@ import React from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import styled from "styled-components";
|
||||
import { ModalProps } from "../../controllers/modals/types";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
|
||||
import Button from "../Button";
|
||||
|
@ -4,8 +4,8 @@ import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents";
|
||||
import { TextDivider } from "../Divider";
|
||||
|
@ -3,8 +3,8 @@ import { useForm } from "react-hook-form";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import styled from "styled-components";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { messageFromFieldError } from "../../utils/messageFromFieldError";
|
||||
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
|
||||
import { TextDivider } from "../Divider";
|
||||
|
@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
|
||||
import styled from "styled-components";
|
||||
import * as yup from "yup";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { Modal } from "./ModalComponents";
|
||||
|
||||
const DescriptionText = styled.p`
|
||||
@ -54,7 +54,7 @@ export function KickMemberModal({ target, ...props }: ModalProps<"kick_member">)
|
||||
data.reason
|
||||
? {
|
||||
"X-Audit-Log-Reason": data.reason,
|
||||
}
|
||||
}
|
||||
: undefined,
|
||||
)
|
||||
.then(() => {
|
||||
|
@ -2,8 +2,8 @@ import { Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { useState } from "react";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { Modal } from "./ModalComponents";
|
||||
|
||||
export function LeaveServerModal({ target, ...props }: ModalProps<"leave_server">) {
|
||||
|
@ -1,8 +1,8 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import { ModalProps } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { ModalProps, modalController } from "../../controllers/modals";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import { isTauri } from "../../utils/Utils";
|
||||
import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
|
||||
import Icon from "../Icon";
|
||||
@ -247,11 +247,7 @@ export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) =>
|
||||
<CloseContainer>
|
||||
<CloseContainerInner></CloseContainerInner>
|
||||
<CloseContainerWrapper>
|
||||
<CloseButtonWrapper
|
||||
onClick={() => {
|
||||
console.log("Close modal");
|
||||
}}
|
||||
>
|
||||
<CloseButtonWrapper onClick={() => modalController.close()}>
|
||||
<Icon icon="mdiClose" size="18px" />
|
||||
</CloseButtonWrapper>
|
||||
</CloseContainerWrapper>
|
||||
|
@ -1,12 +1,17 @@
|
||||
import { RESTPatchAPICurrentUserJSONBody, Routes } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import styled, { css } from "styled-components";
|
||||
import { useAppStore } from "../../../stores/AppStore";
|
||||
import { useAppStore } from "../../../hooks/useAppStore";
|
||||
import Avatar from "../../Avatar";
|
||||
import Button from "../../Button";
|
||||
import SectionTitle from "../../SectionTitle";
|
||||
|
||||
const Content = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
min-width: 30vw;
|
||||
`;
|
||||
|
||||
const UserInfoContainer = styled.div`
|
||||
@ -77,25 +82,152 @@ const FieldValueToggle = styled.button`
|
||||
text-rendering: optimizeLegibility;
|
||||
`;
|
||||
|
||||
const IconContainer = styled.div`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
`;
|
||||
|
||||
const IconInput = styled.input`
|
||||
display: none;
|
||||
`;
|
||||
|
||||
const FileInput = styled.div`
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0;
|
||||
cursor: pointer;
|
||||
font-size: 0px;
|
||||
`;
|
||||
|
||||
const UnsavedChangesBar = styled.div`
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
background-color: var(--background-tertiary);
|
||||
padding: 10px 16px;
|
||||
border-radius: 8px;
|
||||
margin-top: 24px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const UnsavedChangedActions = styled.div`
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
`;
|
||||
|
||||
const Text = styled.p`
|
||||
color: var(--text-secondary);
|
||||
font-size: 16px;
|
||||
font-weight: var(--font-weight-medium);
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
`;
|
||||
|
||||
function AccountSettingsPage() {
|
||||
const app = useAppStore();
|
||||
const [shouldRedactEmail, setShouldRedactEmail] = useState(true);
|
||||
const [selectedFile, setSelectedFile] = useState<File>();
|
||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||
const [hasUnsavedChangd, setHasUnsavedChanged] = useState(false);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const redactEmail = (email: string) => {
|
||||
const [username, domain] = email.split("@");
|
||||
return `${"*".repeat(username.length)}@${domain}`;
|
||||
};
|
||||
|
||||
const refactPhoneNumber = (phoneNumber: string) => {
|
||||
const redactPhoneNumber = (phoneNumber: string) => {
|
||||
const lastFour = phoneNumber.slice(-4);
|
||||
return "*".repeat(phoneNumber.length - 4) + lastFour;
|
||||
};
|
||||
|
||||
const onAvatarChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
if (!event.target.files) return;
|
||||
setSelectedFile(event.target.files[0]);
|
||||
};
|
||||
|
||||
const discardChanges = () => {
|
||||
setSelectedFile(undefined);
|
||||
};
|
||||
|
||||
const save = async () => {
|
||||
setLoading(true);
|
||||
if (!selectedFile) return;
|
||||
const reader = new FileReader();
|
||||
reader.onload = async () => {
|
||||
const payload: RESTPatchAPICurrentUserJSONBody = {
|
||||
// @ts-expect-error broken types or whatever
|
||||
avatar: reader.result,
|
||||
};
|
||||
app.rest
|
||||
.patch<RESTPatchAPICurrentUserJSONBody, RESTPatchAPICurrentUserJSONBody>(Routes.user(), payload)
|
||||
.then((r) => {
|
||||
// runInAction(() => {
|
||||
// if (r.username) app.account!.username = r.username;
|
||||
// if (r.avatar) app.account!.avatar = r.avatar;
|
||||
// });
|
||||
setSelectedFile(undefined);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
reader.readAsDataURL(selectedFile);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
// handle unsaved changes state
|
||||
if (selectedFile) {
|
||||
setHasUnsavedChanged(true);
|
||||
} else {
|
||||
// Reset state if there is nothing changed
|
||||
setHasUnsavedChanged(false);
|
||||
}
|
||||
}, [selectedFile]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<SectionTitle>Account</SectionTitle>
|
||||
<Content>
|
||||
<UserInfoContainer>
|
||||
<Field spacerBottom>
|
||||
<IconContainer>
|
||||
{selectedFile ? (
|
||||
<img
|
||||
src={URL.createObjectURL(selectedFile)}
|
||||
alt="Avatar"
|
||||
width="80px"
|
||||
height="80px"
|
||||
style={{
|
||||
borderRadius: "50%",
|
||||
pointerEvents: "none",
|
||||
objectFit: "cover",
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Avatar user={app.account!} size={80} />
|
||||
)}
|
||||
<IconInput
|
||||
ref={fileInputRef}
|
||||
type="file"
|
||||
name="avatar"
|
||||
accept="image/*"
|
||||
onChange={onAvatarChange}
|
||||
disabled={loading}
|
||||
/>
|
||||
<FileInput
|
||||
role="button"
|
||||
onClick={() => fileInputRef.current?.click()}
|
||||
aria-disabled={loading}
|
||||
/>
|
||||
</IconContainer>
|
||||
</Field>
|
||||
|
||||
<Field spacerBottom>
|
||||
<Row>
|
||||
<FieldTitle>Username</FieldTitle>
|
||||
@ -138,6 +270,20 @@ function AccountSettingsPage() {
|
||||
</Row>
|
||||
</Field>
|
||||
</UserInfoContainer>
|
||||
|
||||
{hasUnsavedChangd && (
|
||||
<UnsavedChangesBar>
|
||||
<Text>You have unsaved changes.</Text>
|
||||
<UnsavedChangedActions>
|
||||
<Button palette="link" onClick={discardChanges} disabled={loading}>
|
||||
Discard
|
||||
</Button>
|
||||
<Button palette="primary" disabled={loading} onClick={save}>
|
||||
{loading ? "Saving..." : "Save"}
|
||||
</Button>
|
||||
</UnsavedChangedActions>
|
||||
</UnsavedChangesBar>
|
||||
)}
|
||||
</Content>
|
||||
</div>
|
||||
);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useState } from "react";
|
||||
import styled from "styled-components";
|
||||
import { useAppStore } from "../../../stores/AppStore";
|
||||
import { useAppStore } from "../../../hooks/useAppStore";
|
||||
import { EXPERIMENT_LIST, Experiment as ExperimentType } from "../../../stores/ExperimentsStore";
|
||||
import SectionTitle from "../../SectionTitle";
|
||||
|
||||
|
41
src/contexts/ContextMenuContext.ts
Normal file
41
src/contexts/ContextMenuContext.ts
Normal file
@ -0,0 +1,41 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { useFloating } from "@floating-ui/react";
|
||||
import React from "react";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import GuildMember from "../stores/objects/GuildMember";
|
||||
import { MessageLike } from "../stores/objects/Message";
|
||||
import User from "../stores/objects/User";
|
||||
|
||||
export type ContextMenuProps =
|
||||
| {
|
||||
type: "user";
|
||||
user: User;
|
||||
member?: GuildMember;
|
||||
}
|
||||
| {
|
||||
type: "message";
|
||||
message: MessageLike;
|
||||
}
|
||||
| {
|
||||
type: "channel";
|
||||
channel: Channel;
|
||||
}
|
||||
| {
|
||||
type: "channelMention";
|
||||
channel: Channel;
|
||||
}
|
||||
| {
|
||||
type: "guild";
|
||||
guild: Guild;
|
||||
};
|
||||
|
||||
export type ContextMenuContextType = {
|
||||
setReferenceElement: ReturnType<typeof useFloating>["refs"]["setReference"];
|
||||
onContextMenu: (e: React.MouseEvent, props: ContextMenuProps) => void;
|
||||
close: () => void;
|
||||
open: (props: ContextMenuProps) => void;
|
||||
};
|
||||
|
||||
// @ts-expect-error not specifying a default value here
|
||||
export const ContextMenuContext = React.createContext<ContextMenuContextType>();
|
@ -1,45 +1,7 @@
|
||||
/* eslint-disable @typescript-eslint/no-explicit-any */
|
||||
import { FloatingPortal, useFloating } from "@floating-ui/react";
|
||||
import { FloatingPortal } from "@floating-ui/react";
|
||||
import React from "react";
|
||||
import useContextMenu, { ContextMenuComponents } from "../hooks/useContextMenu";
|
||||
import Channel from "../stores/objects/Channel";
|
||||
import Guild from "../stores/objects/Guild";
|
||||
import GuildMember from "../stores/objects/GuildMember";
|
||||
import { MessageLike } from "../stores/objects/Message";
|
||||
import User from "../stores/objects/User";
|
||||
|
||||
export type ContextMenuProps =
|
||||
| {
|
||||
type: "user";
|
||||
user: User;
|
||||
member?: GuildMember;
|
||||
}
|
||||
| {
|
||||
type: "message";
|
||||
message: MessageLike;
|
||||
}
|
||||
| {
|
||||
type: "channel";
|
||||
channel: Channel;
|
||||
}
|
||||
| {
|
||||
type: "channelMention";
|
||||
channel: Channel;
|
||||
}
|
||||
| {
|
||||
type: "guild";
|
||||
guild: Guild;
|
||||
};
|
||||
|
||||
export type ContextMenuContextType = {
|
||||
setReferenceElement: ReturnType<typeof useFloating>["refs"]["setReference"];
|
||||
onContextMenu: (e: React.MouseEvent, props: ContextMenuProps) => void;
|
||||
close: () => void;
|
||||
open: (props: ContextMenuProps) => void;
|
||||
};
|
||||
|
||||
// @ts-expect-error not specifying a default value here
|
||||
export const ContextMenuContext = React.createContext<ContextMenuContextType>();
|
||||
import { ContextMenuContext, ContextMenuProps } from "./ContextMenuContext";
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
export const ContextMenuContextProvider: React.FC<any> = ({ children }) => {
|
||||
@ -53,7 +15,7 @@ export const ContextMenuContextProvider: React.FC<any> = ({ children }) => {
|
||||
? ContextMenuComponents[contextMenu.props.type]
|
||||
: () => {
|
||||
return null;
|
||||
};
|
||||
};
|
||||
|
||||
return (
|
||||
<ContextMenuContext.Provider
|
@ -1,6 +1,6 @@
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { createGlobalStyle } from "styled-components";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import { rgbToHsl } from "../utils/Utils";
|
||||
|
||||
const font: ThemeFont["font"] = {
|
||||
|
7
src/hooks/useAppStore.ts
Normal file
7
src/hooks/useAppStore.ts
Normal file
@ -0,0 +1,7 @@
|
||||
import AppStore from "../stores/AppStore";
|
||||
|
||||
export const appStore = new AppStore();
|
||||
|
||||
export function useAppStore() {
|
||||
return appStore;
|
||||
}
|
@ -19,7 +19,7 @@ import ReactDOM from "react-dom/client";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import { ErrorBoundaryContext } from "react-use-error-boundary";
|
||||
import App from "./App";
|
||||
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext";
|
||||
import { ContextMenuContextProvider } from "./contexts/ContextMenuContextProvider";
|
||||
import Theme from "./contexts/Theme";
|
||||
import ModalRenderer from "./controllers/modals/ModalRenderer";
|
||||
import "./index.css";
|
||||
|
@ -5,7 +5,7 @@ import styled from "styled-components";
|
||||
import SpacebarLogoBlue from "../assets/images/logo/Logo-Blue.svg?react";
|
||||
import Button from "../components/Button";
|
||||
import Container from "../components/Container";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
|
||||
const Wrapper = styled.div`
|
||||
justify-content: center;
|
||||
|
@ -23,8 +23,9 @@ import {
|
||||
} from "../components/AuthComponents";
|
||||
import { TextDivider } from "../components/Divider";
|
||||
import HCaptcha, { HeaderContainer } from "../components/HCaptcha";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { AUTH_NO_BRANDING, useAppStore } from "../stores/AppStore";
|
||||
import { AUTH_NO_BRANDING } from "../stores/AppStore";
|
||||
import { Globals } from "../utils/Globals";
|
||||
import REST from "../utils/REST";
|
||||
import { RouteSettings } from "../utils/constants";
|
||||
|
@ -1,5 +1,5 @@
|
||||
import React from "react";
|
||||
import { useAppStore } from "../stores/AppStore";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
|
||||
function LogoutPage() {
|
||||
const app = useAppStore();
|
||||
|
@ -24,8 +24,9 @@ import {
|
||||
import DOBInput from "../components/DOBInput";
|
||||
import { TextDivider } from "../components/Divider";
|
||||
import HCaptcha from "../components/HCaptcha";
|
||||
import { useAppStore } from "../hooks/useAppStore";
|
||||
import useLogger from "../hooks/useLogger";
|
||||
import { AUTH_NO_BRANDING, useAppStore } from "../stores/AppStore";
|
||||
import { AUTH_NO_BRANDING } from "../stores/AppStore";
|
||||
import { IAPILoginResponseSuccess, IAPIRegisterRequest, IAPIRegisterResponseError } from "../utils/interfaces/api";
|
||||
import { messageFromFieldError } from "../utils/messageFromFieldError";
|
||||
|
||||
|
@ -10,7 +10,7 @@ import GuildSidebar from "../../components/GuildSidebar";
|
||||
import SwipeableLayout from "../../components/SwipeableLayout";
|
||||
import Chat from "../../components/messaging/Chat";
|
||||
import BannerRenderer from "../../controllers/banners/BannerRenderer";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
|
||||
const Container = styled(ContainerComponent)`
|
||||
display: flex;
|
||||
|
@ -20,8 +20,8 @@ import {
|
||||
Wrapper,
|
||||
} from "../../components/AuthComponents";
|
||||
import { TextDivider } from "../../components/Divider";
|
||||
import { useAppStore } from "../../hooks/useAppStore";
|
||||
import useLogger from "../../hooks/useLogger";
|
||||
import { useAppStore } from "../../stores/AppStore";
|
||||
import {
|
||||
IAPIError,
|
||||
IAPILoginResponseMFARequired,
|
||||
|
@ -13,6 +13,7 @@ import GuildStore from "./GuildStore";
|
||||
import MessageQueue from "./MessageQueue";
|
||||
import PresenceStore from "./PresenceStore";
|
||||
import PrivateChannelStore from "./PrivateChannelStore";
|
||||
import ReadStateStore from "./ReadStateStore";
|
||||
import RoleStore from "./RoleStore";
|
||||
import ThemeStore from "./ThemeStore";
|
||||
import UpdaterStore from "./UpdaterStore";
|
||||
@ -47,6 +48,7 @@ export default class AppStore {
|
||||
@observable rest = new REST(this);
|
||||
@observable experiments = new ExperimentsStore();
|
||||
@observable presences = new PresenceStore(this);
|
||||
@observable readStateStore = new ReadStateStore(this);
|
||||
@observable queue = new MessageQueue(this);
|
||||
@observable updaterStore: UpdaterStore | null = null;
|
||||
|
||||
@ -187,9 +189,3 @@ export default class AppStore {
|
||||
this.loadUpdaterEnabled();
|
||||
}
|
||||
}
|
||||
|
||||
export const appStore = new AppStore();
|
||||
|
||||
export function useAppStore() {
|
||||
return appStore;
|
||||
}
|
||||
|
@ -143,6 +143,8 @@ export default class GatewayConnectionStore {
|
||||
this.dispatchHandlers.set(GatewayDispatchEvents.ChannelCreate, this.onChannelCreate);
|
||||
this.dispatchHandlers.set(GatewayDispatchEvents.ChannelUpdate, this.onChannelUpdate);
|
||||
this.dispatchHandlers.set(GatewayDispatchEvents.ChannelDelete, this.onChannelDelete);
|
||||
// @ts-expect-error missing event in typings
|
||||
this.dispatchHandlers.set("MESSAGE_ACK", this.onMessageAck);
|
||||
|
||||
this.dispatchHandlers.set(GatewayDispatchEvents.MessageCreate, this.onMessageCreate);
|
||||
this.dispatchHandlers.set(GatewayDispatchEvents.MessageUpdate, this.onMessageUpdate);
|
||||
@ -469,7 +471,7 @@ export default class GatewayConnectionStore {
|
||||
*/
|
||||
private onReady = (data: GatewayReadyDispatchData) => {
|
||||
this.logger.info(`[Ready] took ${Date.now() - this.connectionStartTime!}ms`);
|
||||
const { session_id, guilds, users, user, private_channels, sessions } = data;
|
||||
const { session_id, guilds, users, user, private_channels, sessions, read_state } = data;
|
||||
this.sessionId = session_id;
|
||||
this.session = (sessions as GatewaySession[]).find((x) => x.session_id === session_id);
|
||||
|
||||
@ -480,8 +482,9 @@ export default class GatewayConnectionStore {
|
||||
if (users) {
|
||||
this.app.users.addAll(users);
|
||||
}
|
||||
|
||||
// TODO: store relationships
|
||||
// TODO: store readstates
|
||||
this.app.readStateStore.addAll(read_state.entries);
|
||||
this.app.privateChannels.addAll(private_channels);
|
||||
|
||||
if (data.merged_members) {
|
||||
@ -632,6 +635,23 @@ export default class GatewayConnectionStore {
|
||||
guild.removeChannel(data.id);
|
||||
};
|
||||
|
||||
private onMessageAck = (data: { channel_id: string; message_id: string; version: number }) => {
|
||||
// get readstate for channel
|
||||
const readstate = this.app.readStateStore.get(data.channel_id);
|
||||
if (!readstate) {
|
||||
this.logger.warn(`[MessageAck] Readstate not found for channel ${data.channel_id}`);
|
||||
return;
|
||||
}
|
||||
|
||||
runInAction(() => {
|
||||
readstate.lastMessageId = data.message_id;
|
||||
});
|
||||
|
||||
this.logger.debug(
|
||||
`[MessageAck] Updated last message id for channel readstate ${data.channel_id} to ${data.message_id}`,
|
||||
);
|
||||
};
|
||||
|
||||
private onMessageCreate = (data: GatewayMessageCreateDispatchData) => {
|
||||
const guild = this.app.guilds.get(data.guild_id!);
|
||||
if (!guild) {
|
||||
@ -727,5 +747,9 @@ export default class GatewayConnectionStore {
|
||||
|
||||
private onUserUpdate = (data: GatewayUserUpdateDispatchData) => {
|
||||
this.app.users.update(data);
|
||||
|
||||
if (data.id === this.app.account!.id) {
|
||||
this.app.setUser(data);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
61
src/stores/ReadStateStore.ts
Normal file
61
src/stores/ReadStateStore.ts
Normal file
@ -0,0 +1,61 @@
|
||||
import type { APIReadState } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { ObservableMap, action, computed, observable } from "mobx";
|
||||
import AppStore from "./AppStore";
|
||||
import ReadState from "./objects/ReadState";
|
||||
|
||||
export default class ReadStateStore {
|
||||
private readonly app: AppStore;
|
||||
@observable readonly readstates = new ObservableMap<string, ReadState>();
|
||||
|
||||
constructor(app: AppStore) {
|
||||
this.app = app;
|
||||
}
|
||||
|
||||
@action
|
||||
add(readstate: APIReadState) {
|
||||
this.readstates.set(readstate.id, new ReadState(this.app, readstate));
|
||||
}
|
||||
|
||||
@action
|
||||
update(readstate: APIReadState) {
|
||||
const existing = this.readstates.get(readstate.id);
|
||||
if (existing) {
|
||||
existing.update(readstate);
|
||||
} else {
|
||||
this.add(readstate);
|
||||
}
|
||||
}
|
||||
|
||||
@action
|
||||
addAll(readstates: APIReadState[]) {
|
||||
readstates.forEach((readstate) => this.add(readstate));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a channels readstate
|
||||
* @param id channel id
|
||||
* @returns
|
||||
*/
|
||||
get(id: string) {
|
||||
return this.readstates.get(id);
|
||||
}
|
||||
|
||||
@computed
|
||||
get all() {
|
||||
return Array.from(this.readstates.values());
|
||||
}
|
||||
|
||||
@action
|
||||
remove(id: string) {
|
||||
this.readstates.delete(id);
|
||||
}
|
||||
|
||||
@computed
|
||||
get count() {
|
||||
return this.readstates.size;
|
||||
}
|
||||
|
||||
has(id: string) {
|
||||
return this.readstates.has(id);
|
||||
}
|
||||
}
|
@ -3,7 +3,6 @@ import type {
|
||||
APIChannel,
|
||||
APIInvite,
|
||||
APIOverwrite,
|
||||
APIReadState,
|
||||
APIUser,
|
||||
APIWebhook,
|
||||
GatewayVoiceState,
|
||||
@ -53,7 +52,7 @@ export default class Channel {
|
||||
@observable retentionPolicyId?: string;
|
||||
@observable messages: MessageStore;
|
||||
@observable voiceStates?: GatewayVoiceState[];
|
||||
@observable readStates?: APIReadState[];
|
||||
// @observable readStates?: APIReadState[]; ????? this seems wrong
|
||||
@observable webhooks?: APIWebhook[];
|
||||
@observable flags: number;
|
||||
@observable defaultThreadRateLimitPerUser: number;
|
||||
@ -89,7 +88,7 @@ export default class Channel {
|
||||
this.invites = channel.invites;
|
||||
this.retentionPolicyId = channel.retention_policy_id;
|
||||
this.voiceStates = channel.voice_states;
|
||||
this.readStates = channel.read_states;
|
||||
// this.readStates = channel.read_states;
|
||||
this.webhooks = channel.webhooks;
|
||||
this.flags = channel.flags;
|
||||
this.defaultThreadRateLimitPerUser = channel.default_thread_rate_limit_per_user;
|
||||
@ -311,4 +310,34 @@ export default class Channel {
|
||||
|
||||
return listId;
|
||||
}
|
||||
|
||||
@computed
|
||||
get unread() {
|
||||
const readState = this.app.readStateStore.get(this.id);
|
||||
if (!readState) {
|
||||
// this.logger.warn(`Failed to find readstate for channel ${this.id}`); // this just causes unnecessary spam
|
||||
return false;
|
||||
}
|
||||
|
||||
return readState.lastMessageId !== this.lastMessageId;
|
||||
}
|
||||
|
||||
markAsRead() {
|
||||
const readState = this.app.readStateStore.get(this.id);
|
||||
if (!readState) {
|
||||
this.logger.warn(`Failed to find readstate for channel ${this.id}`); // this just causes unnecessary spam
|
||||
return;
|
||||
}
|
||||
|
||||
this.app.rest
|
||||
.post(Routes.channelMessage(this.id, readState.lastMessageId) + "/ack", {
|
||||
mention_count: readState.mentionCount,
|
||||
})
|
||||
.then((r) => {
|
||||
this.logger.debug(`Acked ${this.lastMessageId} for channel ${this.id}`, r);
|
||||
})
|
||||
.catch((e) => {
|
||||
this.logger.error(`Failed to ack ${this.lastMessageId} for channel ${this.id}`, e);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
29
src/stores/objects/ReadState.ts
Normal file
29
src/stores/objects/ReadState.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { type APIReadState } from "@spacebarchat/spacebar-api-types/v9";
|
||||
import { action, observable } from "mobx";
|
||||
import Logger from "../../utils/Logger";
|
||||
import AppStore from "../AppStore";
|
||||
|
||||
export default class ReadState {
|
||||
private readonly logger: Logger;
|
||||
private readonly app: AppStore;
|
||||
|
||||
id: string;
|
||||
@observable lastMessageId: string;
|
||||
@observable lastPinTimestamp: string | null;
|
||||
@observable mentionCount: number | null;
|
||||
|
||||
constructor(app: AppStore, data: APIReadState) {
|
||||
this.logger = new Logger("ReadState");
|
||||
this.app = app;
|
||||
|
||||
this.id = data.id; // channel id
|
||||
this.lastMessageId = data.last_message_id;
|
||||
this.lastPinTimestamp = data.last_pin_timestamp;
|
||||
this.mentionCount = data.mention_count;
|
||||
}
|
||||
|
||||
@action
|
||||
update(role: APIReadState) {
|
||||
Object.assign(this, role);
|
||||
}
|
||||
}
|
@ -200,6 +200,48 @@ export default class REST {
|
||||
});
|
||||
}
|
||||
|
||||
public async patch<T, U>(
|
||||
path: string,
|
||||
body?: T,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
queryParams: Record<string, any> = {},
|
||||
headers: Record<string, string> = {},
|
||||
): Promise<U> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = REST.makeAPIUrl(path, queryParams);
|
||||
this.logger.debug(`PATCH ${url}; payload:`, body);
|
||||
return fetch(url, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
...headers,
|
||||
...this.headers,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: body ? JSON.stringify(body) : undefined,
|
||||
mode: "cors",
|
||||
})
|
||||
.then(async (res) => {
|
||||
// handle json if content type is json
|
||||
if (res.headers.get("content-type")?.includes("application/json")) {
|
||||
const data = await res.json();
|
||||
if (res.ok) return resolve(data);
|
||||
else return reject(data);
|
||||
}
|
||||
|
||||
// if theres content, handle text
|
||||
if (res.headers.get("content-length") !== "0") {
|
||||
const data = await res.text();
|
||||
if (res.ok) return resolve(data as U);
|
||||
else return reject(data as U);
|
||||
}
|
||||
|
||||
if (res.ok) return resolve(res.status as U);
|
||||
else return reject(res.statusText);
|
||||
})
|
||||
.catch(reject);
|
||||
});
|
||||
}
|
||||
|
||||
public async postFormData<U>(
|
||||
path: string,
|
||||
body: FormData,
|
||||
|
@ -84,7 +84,9 @@ export const getFileDetails = (fileOrAttachment: File | APIAttachment) => {
|
||||
export const isTauri = !!window.__TAURI_INTERNALS__;
|
||||
|
||||
export function rgbToHsl(r: number, g: number, b: number) {
|
||||
(r /= 255), (g /= 255), (b /= 255);
|
||||
r /= 255;
|
||||
g /= 255;
|
||||
b /= 255;
|
||||
|
||||
const max = Math.max(r, g, b),
|
||||
min = Math.min(r, g, b);
|
||||
|
@ -1,7 +1,6 @@
|
||||
import replace from "@rollup/plugin-replace";
|
||||
import react from "@vitejs/plugin-react";
|
||||
import fs, { readFileSync } from "fs";
|
||||
import { internalIpV4 } from "internal-ip";
|
||||
import path, { resolve } from "path";
|
||||
import type { Plugin } from "vite";
|
||||
import { defineConfig } from "vite";
|
||||
@ -44,7 +43,7 @@ function getVersion() {
|
||||
return JSON.parse(readFileSync("package.json").toString()).version;
|
||||
}
|
||||
|
||||
const mobile = !!/android|ios/.exec(process.env.TAURI_ENV_PLATFORM);
|
||||
const host = process.env.TAURI_DEV_HOST;
|
||||
|
||||
// https://vitejs.dev/config/
|
||||
export default defineConfig(async () => ({
|
||||
@ -72,14 +71,14 @@ export default defineConfig(async () => ({
|
||||
clearScreen: false,
|
||||
// 2. tauri expects a fixed port, fail if that port is not available
|
||||
server: {
|
||||
host: mobile ? "0.0.0.0" : false,
|
||||
host: host || false,
|
||||
port: 1420,
|
||||
hmr: mobile
|
||||
hmr: host
|
||||
? {
|
||||
protocol: "ws",
|
||||
host: await internalIpV4(),
|
||||
port: 1421,
|
||||
}
|
||||
host: host,
|
||||
port: 1430,
|
||||
}
|
||||
: undefined,
|
||||
strictPort: true,
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user