1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-24 19:32:34 +01:00

implement changing avatar

This commit is contained in:
Puyodead1 2024-07-17 20:17:18 -04:00
parent 686431e900
commit 329a14ccf5
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
50 changed files with 300 additions and 103 deletions

View File

@ -17,11 +17,11 @@ import useLogger from "./hooks/useLogger";
import AppPage from "./pages/AppPage"; import AppPage from "./pages/AppPage";
import LogoutPage from "./pages/LogoutPage"; import LogoutPage from "./pages/LogoutPage";
import ChannelPage from "./pages/subpages/ChannelPage"; import ChannelPage from "./pages/subpages/ChannelPage";
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 { bannerController } from "./controllers/banners"; import { bannerController } from "./controllers/banners";
import { useAppStore } from "./hooks/useAppStore";
import { isTauri } from "./utils/Utils"; import { isTauri } from "./utils/Utils";
function App() { function App() {

View File

@ -1,9 +1,9 @@
import { PresenceUpdateStatus } from "@spacebarchat/spacebar-api-types/v9"; 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, { useRef } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useAppStore } from "../hooks/useAppStore";
import AccountStore from "../stores/AccountStore"; import AccountStore from "../stores/AccountStore";
import { useAppStore } from "../stores/AppStore";
import Presence from "../stores/objects/Presence"; import Presence from "../stores/objects/Presence";
import User from "../stores/objects/User"; import User from "../stores/objects/User";
import Container from "./Container"; import Container from "./Container";
@ -33,7 +33,7 @@ interface Props {
function Avatar(props: Props) { function Avatar(props: Props) {
const app = useAppStore(); const app = useAppStore();
const ref = React.useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const user = props.user ?? app.account; const user = props.user ?? app.account;
if (!user) return null; if (!user) return null;

View File

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import styled from "styled-components"; import styled from "styled-components";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
import Icon, { IconProps } from "./Icon"; import Icon, { IconProps } from "./Icon";
import { SectionHeader } from "./SectionHeader"; import { SectionHeader } from "./SectionHeader";
import Floating from "./floating/Floating"; import Floating from "./floating/Floating";

View File

@ -2,7 +2,7 @@ import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
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 { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import ChannelListItem from "./ChannelListItem"; import ChannelListItem from "./ChannelListItem";
const Container = styled.div` const Container = styled.div`

View File

@ -4,13 +4,13 @@ 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 { modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import Channel from "../../stores/objects/Channel"; import Channel from "../../stores/objects/Channel";
import { Permissions } from "../../utils/Permissions"; import { Permissions } from "../../utils/Permissions";
import Icon from "../Icon"; import Icon from "../Icon";
import SidebarPill from "../SidebarPill"; import SidebarPill from "../SidebarPill";
import Floating from "../floating/Floating"; import Floating from "../floating/Floating";
import FloatingTrigger from "../floating/FloatingTrigger"; import FloatingTrigger from "../floating/FloatingTrigger";
import { useAppStore } from "../../hooks/useAppStore";
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")};

View File

@ -4,8 +4,8 @@ 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 { useAppStore } from "../hooks/useAppStore";
import useLogger from "../hooks/useLogger"; import useLogger from "../hooks/useLogger";
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";
@ -30,9 +30,7 @@ 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: transition: border-radius 0.2s ease, background-color 0.2s ease;
border-radius 0.2s ease,
background-color 0.2s ease;
&:hover { &:hover {
border-radius: 30%; border-radius: 30%;

View File

@ -4,7 +4,7 @@ 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 { modalController } from "../controllers/modals";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
import GuildItem, { GuildSidebarListItem } from "./GuildItem"; import GuildItem, { GuildSidebarListItem } from "./GuildItem";
import SidebarAction from "./SidebarAction"; import SidebarAction from "./SidebarAction";

View File

@ -1,8 +1,8 @@
import { invoke } from "@tauri-apps/api/core"; 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 { useAppStore } from "../hooks/useAppStore";
import LoadingPage from "../pages/LoadingPage"; import LoadingPage from "../pages/LoadingPage";
import { useAppStore } from "../stores/AppStore";
import { isTauri } from "../utils/Utils"; import { isTauri } from "../utils/Utils";
interface Props { interface Props {

View File

@ -2,7 +2,7 @@ import { autorun } from "mobx";
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 { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import GuildMemberListStore from "../../stores/GuildMemberListStore"; import GuildMemberListStore from "../../stores/GuildMemberListStore";
import ListSection from "../ListSection"; import ListSection from "../ListSection";
import MemberListItem from "./MemberListItem"; import MemberListItem from "./MemberListItem";

View File

@ -3,7 +3,7 @@ import { observer } from "mobx-react-lite";
import { useContext } from "react"; 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 { useAppStore } from "../../hooks/useAppStore";
import GuildMember from "../../stores/objects/GuildMember"; import GuildMember from "../../stores/objects/GuildMember";
import Avatar from "../Avatar"; import Avatar from "../Avatar";
import Floating from "../floating/Floating"; import Floating from "../floating/Floating";

View File

@ -1,7 +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 { modalController } from "../controllers/modals"; import { modalController } from "../controllers/modals";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
import Avatar from "./Avatar"; import Avatar from "./Avatar";
import Icon from "./Icon"; import Icon from "./Icon";
import IconButton from "./IconButton"; import IconButton from "./IconButton";

View File

@ -2,8 +2,8 @@
import { ChannelType } from "@spacebarchat/spacebar-api-types/v9"; import { ChannelType } from "@spacebarchat/spacebar-api-types/v9";
import { modalController } from "../../controllers/modals"; import { modalController } from "../../controllers/modals";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import Guild from "../../stores/objects/Guild"; import Guild from "../../stores/objects/Guild";
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";

View File

@ -1,5 +1,5 @@
import { modalController } from "../../controllers/modals"; import { modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import Message from "../../stores/objects/Message"; import Message from "../../stores/objects/Message";
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";

View File

@ -1,7 +1,7 @@
// loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx // loosely based on https://github.com/revoltchat/frontend/blob/master/components/app/menus/UserContextMenu.tsx
import { modalController } from "../../controllers/modals"; import { modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import GuildMember from "../../stores/objects/GuildMember"; import GuildMember from "../../stores/objects/GuildMember";
import User from "../../stores/objects/User"; import User from "../../stores/objects/User";
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu"; import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "./ContextMenu";

View File

@ -3,7 +3,7 @@ import useLogger from "../../hooks/useLogger";
import React, { useEffect } from "react"; import React, { useEffect } from "react";
import { modalController } from "../../controllers/modals"; import { modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { Permissions } from "../../utils/Permissions"; import { Permissions } from "../../utils/Permissions";
import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "../contextMenus/ContextMenu"; import { ContextMenu, ContextMenuButton, ContextMenuDivider } from "../contextMenus/ContextMenu";

View File

@ -9,7 +9,7 @@ import { HorizontalDivider } from "../Divider";
import { CDNRoutes, ImageFormat } from "@spacebarchat/spacebar-api-types/v9"; import { CDNRoutes, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
import dayjs from "dayjs"; import dayjs from "dayjs";
import SpacebarLogoBlue from "../../assets/images/logo/Spacebar_Icon.svg?react"; 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 REST from "../../utils/REST";
import Floating from "./Floating"; import Floating from "./Floating";
import FloatingTrigger from "./FloatingTrigger"; import FloatingTrigger from "./FloatingTrigger";

View File

@ -1,6 +1,6 @@
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { useAppStore } from "../../hooks/useAppStore";
import { LoadingSuspense } from "../../pages/LoadingPage"; import { LoadingSuspense } from "../../pages/LoadingPage";
import { useAppStore } from "../../stores/AppStore";
interface Props { interface Props {
component: React.FC; component: React.FC;

View File

@ -1,6 +1,6 @@
import { Navigate } from "react-router-dom"; import { Navigate } from "react-router-dom";
import { useAppStore } from "../../hooks/useAppStore";
import { LoadingSuspense } from "../../pages/LoadingPage"; import { LoadingSuspense } from "../../pages/LoadingPage";
import { useAppStore } from "../../stores/AppStore";
interface Props { interface Props {
component: React.FC; component: React.FC;

View File

@ -2,7 +2,7 @@ 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 { ContextMenuContext } from "../../contexts/ContextMenuContext"; import { ContextMenuContext } from "../../contexts/ContextMenuContext";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
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";

View File

@ -2,8 +2,8 @@ import { runInAction } from "mobx";
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 { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import Channel from "../../stores/objects/Channel"; import Channel from "../../stores/objects/Channel";
import Guild from "../../stores/objects/Guild"; import Guild from "../../stores/objects/Guild";
import MemberList from "../MemberList/MemberList"; import MemberList from "../MemberList/MemberList";

View File

@ -1,8 +1,8 @@
import * as Icons from "@mdi/js"; import * as Icons from "@mdi/js";
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 "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
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";

View File

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { memo, useContext } 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 "../../hooks/useAppStore";
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 Avatar from "../Avatar"; import Avatar from "../Avatar";

View File

@ -2,8 +2,8 @@ import { observer } from "mobx-react-lite";
import React, { useContext } 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 { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import Guild from "../../stores/objects/Guild"; import Guild from "../../stores/objects/Guild";
import GuildMember from "../../stores/objects/GuildMember"; import GuildMember from "../../stores/objects/GuildMember";
import { MessageLike } from "../../stores/objects/Message"; import { MessageLike } from "../../stores/objects/Message";
@ -36,7 +36,7 @@ function MessageAuthor({ message, guild }: Props) {
React.useEffect(() => { React.useEffect(() => {
if (!eventData) return; if (!eventData) return;
contextMenu.onContextMenu(eventData, { type: "user", user: message.author, member }); contextMenu?.onContextMenu(eventData, { type: "user", user: message.author, member });
}, [eventData, member]); }, [eventData, member]);
const onContextMenu = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => { const onContextMenu = async (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
@ -68,7 +68,7 @@ function MessageAuthor({ message, guild }: Props) {
style={{ style={{
color, color,
}} }}
ref={contextMenu.setReferenceElement} ref={contextMenu?.setReferenceElement}
onContextMenu={onContextMenu} onContextMenu={onContextMenu}
> >
{message.author.username} {message.author.username}

View File

@ -6,8 +6,8 @@ import React from "react";
import styled from "styled-components"; import styled from "styled-components";
import { modalController } from "../../controllers/modals"; import { modalController } from "../../controllers/modals";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
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";
@ -197,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")}

View File

@ -4,8 +4,8 @@ import InfiniteScroll from "react-infinite-scroll-component";
import PulseLoader from "react-spinners/PulseLoader"; import PulseLoader from "react-spinners/PulseLoader";
import styled from "styled-components"; import styled from "styled-components";
import useResizeObserver from "use-resize-observer"; import useResizeObserver from "use-resize-observer";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { MessageGroup as MessageGroupType } from "../../stores/MessageStore"; import { MessageGroup as MessageGroupType } from "../../stores/MessageStore";
import Channel from "../../stores/objects/Channel"; import Channel from "../../stores/objects/Channel";
import Guild from "../../stores/objects/Guild"; import Guild from "../../stores/objects/Guild";
@ -30,6 +30,7 @@ const EndMessageContainer = styled.div`
interface Props { interface Props {
guild: Guild; guild: Guild;
channel: Channel; channel: Channel;
before?: string;
} }
/** /**

View File

@ -1,6 +1,6 @@
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 { useAppStore } from "../../../hooks/useAppStore";
import QueuedMessage from "../../../stores/objects/QueuedMessage"; import QueuedMessage from "../../../stores/objects/QueuedMessage";
import { bytesToSize } from "../../../utils/Utils"; import { bytesToSize } from "../../../utils/Utils";
import Icon from "../../Icon"; import Icon from "../../Icon";

View File

@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
import styled from "styled-components"; import styled from "styled-components";
import * as yup from "yup"; import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { Modal } from "./ModalComponents"; import { Modal } from "./ModalComponents";
const DescriptionText = styled.p` const DescriptionText = styled.p`
@ -54,7 +54,7 @@ export function BanMemberModal({ target, type, ...props }: ModalProps<"ban_membe
data.reason data.reason
? { ? {
"X-Audit-Log-Reason": data.reason, "X-Audit-Log-Reason": data.reason,
} }
: undefined, : undefined,
) )
.then(() => { .then(() => {

View File

@ -11,7 +11,7 @@ import { useNavigate } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import * as yup from "yup"; import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText } from "../AuthComponents"; import { Input, InputErrorText } from "../AuthComponents";
import { TextDivider } from "../Divider"; import { TextDivider } from "../Divider";

View File

@ -4,8 +4,8 @@ import React from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import styled from "styled-components"; import styled from "styled-components";
import { ModalProps } from "../../controllers/modals/types"; import { ModalProps } from "../../controllers/modals/types";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents"; import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
import Button from "../Button"; import Button from "../Button";

View File

@ -4,8 +4,8 @@ import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents"; import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents";
import { TextDivider } from "../Divider"; import { TextDivider } from "../Divider";

View File

@ -3,8 +3,8 @@ import { useForm } from "react-hook-form";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import styled from "styled-components"; import styled from "styled-components";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError"; import { messageFromFieldError } from "../../utils/messageFromFieldError";
import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents"; import { Input, InputErrorText, InputLabel, LabelWrapper } from "../AuthComponents";
import { TextDivider } from "../Divider"; import { TextDivider } from "../Divider";

View File

@ -4,7 +4,7 @@ import { useForm } from "react-hook-form";
import styled from "styled-components"; import styled from "styled-components";
import * as yup from "yup"; import * as yup from "yup";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { Modal } from "./ModalComponents"; import { Modal } from "./ModalComponents";
const DescriptionText = styled.p` const DescriptionText = styled.p`
@ -54,7 +54,7 @@ export function KickMemberModal({ target, ...props }: ModalProps<"kick_member">)
data.reason data.reason
? { ? {
"X-Audit-Log-Reason": data.reason, "X-Audit-Log-Reason": data.reason,
} }
: undefined, : undefined,
) )
.then(() => { .then(() => {

View File

@ -2,8 +2,8 @@ import { Routes } from "@spacebarchat/spacebar-api-types/v9";
import { useState } from "react"; import { useState } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { Modal } from "./ModalComponents"; import { Modal } from "./ModalComponents";
export function LeaveServerModal({ target, ...props }: ModalProps<"leave_server">) { export function LeaveServerModal({ target, ...props }: ModalProps<"leave_server">) {

View File

@ -2,7 +2,7 @@ import { observer } from "mobx-react-lite";
import { useState } from "react"; import { useState } from "react";
import styled, { css } from "styled-components"; import styled, { css } from "styled-components";
import { ModalProps, modalController } from "../../controllers/modals"; import { ModalProps, modalController } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
import { isTauri } from "../../utils/Utils"; import { isTauri } from "../../utils/Utils";
import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
import Icon from "../Icon"; import Icon from "../Icon";

View File

@ -1,12 +1,17 @@
import { RESTPatchAPICurrentUserJSONBody, Routes } from "@spacebarchat/spacebar-api-types/v9";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react"; import { useEffect, useRef, useState } from "react";
import styled, { css } from "styled-components"; 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"; import SectionTitle from "../../SectionTitle";
const Content = styled.div` const Content = styled.div`
display: flex; display: flex;
flex-direction: column; flex-direction: column;
min-width: 30vw;
`; `;
const UserInfoContainer = styled.div` const UserInfoContainer = styled.div`
@ -77,25 +82,152 @@ const FieldValueToggle = styled.button`
text-rendering: optimizeLegibility; 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() { function AccountSettingsPage() {
const app = useAppStore(); const app = useAppStore();
const [shouldRedactEmail, setShouldRedactEmail] = useState(true); 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 redactEmail = (email: string) => {
const [username, domain] = email.split("@"); const [username, domain] = email.split("@");
return `${"*".repeat(username.length)}@${domain}`; return `${"*".repeat(username.length)}@${domain}`;
}; };
const refactPhoneNumber = (phoneNumber: string) => { const redactPhoneNumber = (phoneNumber: string) => {
const lastFour = phoneNumber.slice(-4); const lastFour = phoneNumber.slice(-4);
return "*".repeat(phoneNumber.length - 4) + lastFour; 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 ( return (
<div> <div>
<SectionTitle>Account</SectionTitle> <SectionTitle>Account</SectionTitle>
<Content> <Content>
<UserInfoContainer> <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> <Field spacerBottom>
<Row> <Row>
<FieldTitle>Username</FieldTitle> <FieldTitle>Username</FieldTitle>
@ -138,6 +270,20 @@ function AccountSettingsPage() {
</Row> </Row>
</Field> </Field>
</UserInfoContainer> </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> </Content>
</div> </div>
); );

View File

@ -1,7 +1,7 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useState } from "react"; import { useState } from "react";
import styled from "styled-components"; 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 { EXPERIMENT_LIST, Experiment as ExperimentType } from "../../../stores/ExperimentsStore";
import SectionTitle from "../../SectionTitle"; import SectionTitle from "../../SectionTitle";

View 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>();

View File

@ -1,45 +1,7 @@
/* eslint-disable @typescript-eslint/no-explicit-any */ import { FloatingPortal } from "@floating-ui/react";
import { FloatingPortal, useFloating } from "@floating-ui/react";
import React from "react"; import React from "react";
import useContextMenu, { ContextMenuComponents } from "../hooks/useContextMenu"; import useContextMenu, { ContextMenuComponents } from "../hooks/useContextMenu";
import Channel from "../stores/objects/Channel"; import { ContextMenuContext, ContextMenuProps } from "./ContextMenuContext";
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>();
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
export const ContextMenuContextProvider: React.FC<any> = ({ children }) => { export const ContextMenuContextProvider: React.FC<any> = ({ children }) => {
@ -53,7 +15,7 @@ export const ContextMenuContextProvider: React.FC<any> = ({ children }) => {
? ContextMenuComponents[contextMenu.props.type] ? ContextMenuComponents[contextMenu.props.type]
: () => { : () => {
return null; return null;
}; };
return ( return (
<ContextMenuContext.Provider <ContextMenuContext.Provider

View File

@ -1,6 +1,6 @@
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { createGlobalStyle } from "styled-components"; import { createGlobalStyle } from "styled-components";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
import { rgbToHsl } from "../utils/Utils"; import { rgbToHsl } from "../utils/Utils";
const font: ThemeFont["font"] = { const font: ThemeFont["font"] = {

7
src/hooks/useAppStore.ts Normal file
View File

@ -0,0 +1,7 @@
import AppStore from "../stores/AppStore";
export const appStore = new AppStore();
export function useAppStore() {
return appStore;
}

View File

@ -19,7 +19,7 @@ import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom"; import { BrowserRouter } from "react-router-dom";
import { ErrorBoundaryContext } from "react-use-error-boundary"; import { ErrorBoundaryContext } from "react-use-error-boundary";
import App from "./App"; import App from "./App";
import { ContextMenuContextProvider } from "./contexts/ContextMenuContext"; import { ContextMenuContextProvider } from "./contexts/ContextMenuContextProvider";
import Theme from "./contexts/Theme"; import Theme from "./contexts/Theme";
import ModalRenderer from "./controllers/modals/ModalRenderer"; import ModalRenderer from "./controllers/modals/ModalRenderer";
import "./index.css"; import "./index.css";

View File

@ -5,7 +5,7 @@ import styled from "styled-components";
import SpacebarLogoBlue from "../assets/images/logo/Logo-Blue.svg?react"; import SpacebarLogoBlue from "../assets/images/logo/Logo-Blue.svg?react";
import Button from "../components/Button"; import Button from "../components/Button";
import Container from "../components/Container"; import Container from "../components/Container";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
const Wrapper = styled.div` const Wrapper = styled.div`
justify-content: center; justify-content: center;

View File

@ -23,8 +23,9 @@ import {
} from "../components/AuthComponents"; } from "../components/AuthComponents";
import { TextDivider } from "../components/Divider"; import { TextDivider } from "../components/Divider";
import HCaptcha, { HeaderContainer } from "../components/HCaptcha"; import HCaptcha, { HeaderContainer } from "../components/HCaptcha";
import { useAppStore } from "../hooks/useAppStore";
import useLogger from "../hooks/useLogger"; 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 { Globals } from "../utils/Globals";
import REST from "../utils/REST"; import REST from "../utils/REST";
import { RouteSettings } from "../utils/constants"; import { RouteSettings } from "../utils/constants";

View File

@ -1,5 +1,5 @@
import React from "react"; import React from "react";
import { useAppStore } from "../stores/AppStore"; import { useAppStore } from "../hooks/useAppStore";
function LogoutPage() { function LogoutPage() {
const app = useAppStore(); const app = useAppStore();

View File

@ -24,8 +24,9 @@ import {
import DOBInput from "../components/DOBInput"; import DOBInput from "../components/DOBInput";
import { TextDivider } from "../components/Divider"; import { TextDivider } from "../components/Divider";
import HCaptcha from "../components/HCaptcha"; import HCaptcha from "../components/HCaptcha";
import { useAppStore } from "../hooks/useAppStore";
import useLogger from "../hooks/useLogger"; 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 { IAPILoginResponseSuccess, IAPIRegisterRequest, IAPIRegisterResponseError } from "../utils/interfaces/api";
import { messageFromFieldError } from "../utils/messageFromFieldError"; import { messageFromFieldError } from "../utils/messageFromFieldError";

View File

@ -10,7 +10,7 @@ import GuildSidebar from "../../components/GuildSidebar";
import SwipeableLayout from "../../components/SwipeableLayout"; import SwipeableLayout from "../../components/SwipeableLayout";
import Chat from "../../components/messaging/Chat"; import Chat from "../../components/messaging/Chat";
import BannerRenderer from "../../controllers/banners/BannerRenderer"; import BannerRenderer from "../../controllers/banners/BannerRenderer";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../hooks/useAppStore";
const Container = styled(ContainerComponent)` const Container = styled(ContainerComponent)`
display: flex; display: flex;

View File

@ -20,8 +20,8 @@ import {
Wrapper, Wrapper,
} from "../../components/AuthComponents"; } from "../../components/AuthComponents";
import { TextDivider } from "../../components/Divider"; import { TextDivider } from "../../components/Divider";
import { useAppStore } from "../../hooks/useAppStore";
import useLogger from "../../hooks/useLogger"; import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { import {
IAPIError, IAPIError,
IAPILoginResponseMFARequired, IAPILoginResponseMFARequired,

View File

@ -189,9 +189,3 @@ export default class AppStore {
this.loadUpdaterEnabled(); this.loadUpdaterEnabled();
} }
} }
export const appStore = new AppStore();
export function useAppStore() {
return appStore;
}

View File

@ -728,5 +728,9 @@ export default class GatewayConnectionStore {
private onUserUpdate = (data: GatewayUserUpdateDispatchData) => { private onUserUpdate = (data: GatewayUserUpdateDispatchData) => {
this.app.users.update(data); this.app.users.update(data);
if (data.id === this.app.account!.id) {
this.app.setUser(data);
}
}; };
} }

View File

@ -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>( public async postFormData<U>(
path: string, path: string,
body: FormData, body: FormData,