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

Merge branch 'main' into feat/joinguild

This commit is contained in:
Madeline 2023-08-12 18:27:12 +10:00 committed by GitHub
commit 0509aec987
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
45 changed files with 16071 additions and 12021 deletions

View File

@ -7,5 +7,6 @@
"bracketSpacing": true,
"quoteProps": "as-needed",
"useTabs": true,
"singleQuote": false
"singleQuote": false,
"printWidth": 120
}

View File

@ -1,61 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1682258674,
"narHash": "sha256-nSqXN+dBgw8+DeE3vBhdHB7C2p+2QBDsA27ZptjJz/Y=",
"owner": "lilyinstarlight",
"repo": "nixpkgs",
"rev": "47c77aa566f5114b4d759280702b9b910d3c7f0f",
"type": "github"
},
"original": {
"owner": "lilyinstarlight",
"ref": "unheck/nodejs",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1681202837,
"narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "cfacdce06f30d2b68473a46042957675eebb3401",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1682258674,
"narHash": "sha256-nSqXN+dBgw8+DeE3vBhdHB7C2p+2QBDsA27ZptjJz/Y=",
"owner": "lilyinstarlight",
"repo": "nixpkgs",
"rev": "47c77aa566f5114b4d759280702b9b910d3c7f0f",
"type": "github"
},
"original": {
"owner": "lilyinstarlight",
"ref": "unheck/nodejs",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

View File

@ -38,6 +38,7 @@
"react-dom": "^18.2.0",
"react-error-boundary": "^3.1.4",
"react-hook-form": "^7.43.9",
"react-infinite-scroll-component": "^6.1.0",
"react-loading-skeleton": "^3.3.1",
"react-moment": "^1.1.3",
"react-router-dom": "^6.11.1",
@ -55,9 +56,8 @@
"start": "craco start",
"build": "craco build",
"test": "craco test",
"eject": "react-scripts eject",
"lint": "eslint .",
"lint:fix": "npx prettier . --write"
"lint:fix": "pnpx prettier . --write"
},
"eslintConfig": {
"extends": [

File diff suppressed because it is too large Load Diff

View File

@ -32,17 +32,12 @@ function App() {
app.setGatewayReady(false);
app.gateway.connect(Globals.routeSettings.gateway);
} else {
logger.debug(
"Gateway connect called but socket is not closed",
);
logger.debug("Gateway connect called but socket is not closed");
}
} else {
logger.debug("user no longer authenticated");
if (app.gateway.readyState === WebSocket.OPEN) {
app.gateway.disconnect(
1000,
"user is no longer authenticated",
);
app.gateway.disconnect(1000, "user is no longer authenticated");
}
navigate("/");
@ -62,33 +57,15 @@ function App() {
return (
<Loader>
<Routes>
<Route
index
path="/"
element={<AuthenticationGuard component={AppPage} />}
/>
<Route
path="/app"
element={<AuthenticationGuard component={AppPage} />}
/>
<Route index path="/" element={<AuthenticationGuard component={AppPage} />} />
<Route path="/app" element={<AuthenticationGuard component={AppPage} />} />
<Route
path="/channels/:guildId/:channelId?"
element={<AuthenticationGuard component={ChannelPage} />}
/>
<Route
path="/login"
element={<UnauthenticatedGuard component={LoginPage} />}
/>
<Route
path="/register"
element={
<UnauthenticatedGuard component={RegistrationPage} />
}
/>
<Route
path="/logout"
element={<AuthenticationGuard component={LogoutPage} />}
/>
<Route path="/login" element={<UnauthenticatedGuard component={LoginPage} />} />
<Route path="/register" element={<UnauthenticatedGuard component={RegistrationPage} />} />
<Route path="/logout" element={<AuthenticationGuard component={LogoutPage} />} />
<Route path="*" element={<NotFoundPage />} />
</Routes>
</Loader>

View File

@ -24,8 +24,7 @@ const FirstWrapper = styled.div<{ isCategory?: boolean; active?: boolean }>`
align-items: center;
display: flex;
padding: 0 8px;
background-color: ${(props) =>
props.active ? "var(--background-primary-alt)" : "transparent"};
background-color: ${(props) => (props.active ? "var(--background-primary-alt)" : "transparent")};
&:hover {
background-color: var(--background-primary-alt);
@ -66,14 +65,12 @@ function ChannelList() {
<ListItem
key={channel.id}
isCategory={isCategory}
onClick={() =>
{
// prevent navigating to non-text channels
if (!channel.isTextChannel) return;
onClick={() => {
// prevent navigating to non-text channels
if (!channel.isTextChannel) return;
navigate(`/channels/${guild.id}/${channel.id}`)
}
}
navigate(`/channels/${guild.id}/${channel.id}`);
}}
>
<FirstWrapper isCategory={isCategory} active={active}>
{channel.channelIcon && (

View File

@ -1,7 +1,9 @@
import { observer } from "mobx-react-lite";
import React from "react";
import InfiniteScroll from "react-infinite-scroll-component";
import { useParams } from "react-router-dom";
import styled from "styled-components";
import useLogger from "../hooks/useLogger";
import { useAppStore } from "../stores/AppStore";
import { QueuedMessageStatus } from "../stores/MessageQueue";
import ChatHeader from "./ChatHeader";
@ -22,12 +24,6 @@ const MessageListWrapper = styled.div`
flex-direction: column-reverse;
`;
const List = styled.ul`
list-style: none;
padding: 0;
margin: 0;
`;
const Container = styled.div`
display: flex;
flex-direction: column;
@ -43,6 +39,7 @@ const Spacer = styled.div`
function Chat() {
const app = useAppStore();
const logger = useLogger("Chat");
const { guildId, channelId } = useParams<{
guildId: string;
channelId: string;
@ -65,48 +62,61 @@ function Chat() {
);
}
const messages = [
...(channel.messages.messages ?? []),
...(channel ? app.queue.get(channel.id) ?? [] : []),
];
const messages = [...(channel.messages.messages ?? []), ...(channel ? app.queue.get(channel.id) ?? [] : [])];
const fetchMore = async () => {
if (!channel.messages.count) {
return;
}
// get first message in the list to use as before
const before = channel.messages.messages[0].id;
logger.debug(`Fetching 50 messages before ${before} for channel ${channel.id}`);
await channel.getMessages(app, false, 50, before);
};
return (
<Wrapper>
<ChatHeader channel={channel} />
<Container>
<MessageListWrapper>
<List>
{messages.map((message, index, arr) => {
const t = 1 * 24 * 60 * 60 * 1000;
<MessageListWrapper id="scrollable-div">
<InfiniteScroll
dataLength={messages.length}
next={fetchMore}
inverse={true}
// TODO: change this to false when we have a fetch that returns less than 50 messages
hasMore={true}
loader={<h4>Loading...</h4>}
scrollableTarget="scrollable-div"
>
{messages.map((message, index, arr) => {
// calculate max ms between messages to determine if they should be grouped (if from same author). 7 minutes
const maxTimeDifference = 1000 * 60 * 7;
const isHeader =
index === 0 ||
message.author.id !==
arr[index - 1].author.id ||
message.timestamp.getTime() -
arr[index - 1].timestamp.getTime() >
t;
const isHeader =
// always show header for first message
index === 0 ||
// show header if author is different from previous message
message.author.id !== arr[index - 1].author.id ||
// show header if time difference is greater than maxTimeDifference
message.timestamp.getTime() - arr[index - 1].timestamp.getTime() >
maxTimeDifference;
return (
<Message
key={message.id}
message={message}
isHeader={isHeader}
isSending={
"status" in message &&
message.status ===
QueuedMessageStatus.SENDING
}
isFailed={
"status" in message &&
message.status ===
QueuedMessageStatus.FAILED
}
/>
);
})}
<Spacer />
</List>
return (
<Message
key={message.id}
message={message}
isHeader={isHeader}
isSending={
"status" in message && message.status === QueuedMessageStatus.SENDING
}
isFailed={"status" in message && message.status === QueuedMessageStatus.FAILED}
/>
);
})}
<Spacer />
</InfiniteScroll>
</MessageListWrapper>
</MessageListWrapper>
<MessageInput channel={channel} />
</Container>

View File

@ -48,8 +48,7 @@
display: none;
}
.select-search-container:not(.select-search-is-multiple).select-search-has-focus
.select-search-select {
.select-search-container:not(.select-search-is-multiple).select-search-has-focus .select-search-select {
display: block;
}

View File

@ -79,11 +79,7 @@ const MONTHS = [
interface Props {
onChange: (value: string) => void;
onErrorChange: (errors: {
month?: string;
day?: string;
year?: string;
}) => void;
onErrorChange: (errors: { month?: string; day?: string; year?: string }) => void;
error: boolean;
disabled?: boolean;
}
@ -121,21 +117,34 @@ export class DOBInput extends Component<Props, State> {
}
}
onInputChange =
(type: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
onInputChange = (type: string) => (e: React.ChangeEvent<HTMLInputElement>) => {
const value = e.target.value;
// clear error for field
this.setState(
{
...this.state,
errors: { ...this.state.errors, [type]: undefined },
},
() => {
// ensure only numbers
if (isNaN(Number(value))) {
// clear error for field
this.setState(
{
...this.state,
errors: { ...this.state.errors, [type]: undefined },
},
() => {
// ensure only numbers
if (isNaN(Number(value))) {
this.setState({
...this.state,
errors: {
...this.state.errors,
[type]: "Invalid Date",
},
});
return;
}
if (type === "day") {
// day should be a number between 1-31 and not more than 2 digits
if (value !== "" && (value.length > 2 || Number(value) > 31 || Number(value) < 1)) {
this.setState({
...this.state,
day: value,
errors: {
...this.state.errors,
[type]: "Invalid Date",
@ -144,53 +153,32 @@ export class DOBInput extends Component<Props, State> {
return;
}
if (type === "day") {
// day should be a number between 1-31 and not more than 2 digits
if (
value !== "" &&
(value.length > 2 ||
Number(value) > 31 ||
Number(value) < 1)
) {
this.setState({
...this.state,
day: value,
errors: {
...this.state.errors,
[type]: "Invalid Date",
},
});
return;
}
this.setState({ ...this.state, day: value });
}
this.setState({ ...this.state, day: value });
if (type === "year") {
// year must be between now-min and now-max
if (
value.length === 4 &&
(Number(value) > new Date().getFullYear() - MIN_AGE ||
Number(value) < new Date().getFullYear() - MAX_AGE)
) {
this.setState({
...this.state,
year: value,
errors: {
...this.state.errors,
[type]: "Invalid Date",
},
});
return;
}
if (type === "year") {
// year must be between now-min and now-max
if (
value.length === 4 &&
(Number(value) >
new Date().getFullYear() - MIN_AGE ||
Number(value) <
new Date().getFullYear() - MAX_AGE)
) {
this.setState({
...this.state,
year: value,
errors: {
...this.state.errors,
[type]: "Invalid Date",
},
});
return;
}
this.setState({ ...this.state, year: value });
}
},
);
};
this.setState({ ...this.state, year: value });
}
},
);
};
constructDate = (values: { month: string; day: string; year: string }) => {
const { month, day, year } = values;
@ -206,9 +194,7 @@ export class DOBInput extends Component<Props, State> {
placeholder="Month"
search
options={MONTHS}
onChange={(e) =>
this.setState({ ...this.state, month: e as string })
}
onChange={(e) => this.setState({ ...this.state, month: e as string })}
value={this.state.month}
disabled={this.props.disabled}
/>

View File

@ -1,8 +1,4 @@
import {
CDNRoutes,
ChannelType,
ImageFormat,
} from "@spacebarchat/spacebar-api-types/v9";
import { CDNRoutes, ChannelType, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
import React from "react";
import { useNavigate } from "react-router-dom";
import styled from "styled-components";
@ -21,18 +17,15 @@ const Wrapper = styled(Container)<{ active?: boolean; hasImage?: boolean }>`
height: 48px;
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;
props.hasImage ? "transparent" : props.active ? "var(--primary)" : "var(--background-secondary)"};
transition:
border-radius 0.2s ease,
background-color 0.2s ease;
cursor: pointer;
&:hover {
border-radius: 30%;
background-color: ${(props) =>
props.hasImage ? "transparent" : "var(--primary)"};
background-color: ${(props) => (props.hasImage ? "transparent" : "var(--primary)")};
}
`;
@ -54,12 +47,8 @@ function GuildItem(props: Props) {
if (!guild) return null;
const doNavigate = () => {
const channel = guild.channels.mapped.find(
(x) => x.type !== ChannelType.GuildCategory,
);
navigate(
`/channels/${props.guildId}${channel ? `/${channel.id}` : ""}`,
);
const channel = guild.channels.mapped.find((x) => x.type !== ChannelType.GuildCategory);
navigate(`/channels/${props.guildId}${channel ? `/${channel.id}` : ""}`);
};
React.useEffect(() => {
@ -82,13 +71,7 @@ function GuildItem(props: Props) {
>
{guild.icon ? (
<img
src={REST.makeCDNUrl(
CDNRoutes.guildIcon(
props.guildId,
guild?.icon,
ImageFormat.PNG,
),
)}
src={REST.makeCDNUrl(CDNRoutes.guildIcon(props.guildId, guild?.icon, ImageFormat.PNG))}
width={48}
height={48}
/>

View File

@ -52,11 +52,7 @@ function GuildSidebar() {
</GuildSidebarListItem>
<div aria-label="Servers">
{app.guilds.getAll().map((guild) => (
<GuildItem
key={guild.id}
guildId={guild.id}
active={guild.id === guildId}
/>
<GuildItem key={guild.id} guildId={guild.id} active={guild.id === guildId} />
))}
</div>
@ -66,7 +62,7 @@ function GuildSidebar() {
icon={{
icon: "mdiPlus",
size: "24px",
color: "var(--success)"
color: "var(--success)",
}}
action={() => {
openModal(AddServerModal);

View File

@ -12,9 +12,13 @@ const MessageListItem = styled.li``;
const Container = styled.div<{ isHeader?: boolean }>`
display: flex;
flex-direction: row;
padding: 2px 12px;
padding: ${(props) => (props.isHeader ? "4" : "2")}px 12px;
align-items: center;
margin-top: ${(props) => (props.isHeader ? "20px" : undefined)};
&:hover {
background-color: var(--background-primary-highlight);
}
`;
const MessageContentContainer = styled.div<{ isHeader?: boolean }>`
@ -71,15 +75,10 @@ function Message({ message, isHeader, isSending, isFailed }: Props) {
<MessageContentContainer isHeader={isHeader}>
{isHeader && (
<MessageHeader>
<MessageAuthor>
{message.author.username}
</MessageAuthor>
<MessageAuthor>{message.author.username}</MessageAuthor>
<MessageTimestamp>
<Moment
calendar={calendarStrings}
date={new Date(message.timestamp)}
/>
<Moment calendar={calendarStrings} date={new Date(message.timestamp)} />
</MessageTimestamp>
</MessageHeader>
)}

View File

@ -52,8 +52,7 @@ function MessageInput(props: Props) {
}
// controls the placeholder visibility
if (!content.length)
placeholderRef.current!.style.setProperty("display", "block");
if (!content.length) placeholderRef.current!.style.setProperty("display", "block");
else placeholderRef.current!.style.setProperty("display", "none");
// update the input content
@ -78,8 +77,7 @@ function MessageInput(props: Props) {
if (!wrapperRef.current) return;
wrapperRef.current.style.height = "44px";
wrapperRef.current.style.height =
wrapperRef.current.scrollHeight + "px";
wrapperRef.current.style.height = wrapperRef.current.scrollHeight + "px";
}
function resetInput() {
@ -103,14 +101,8 @@ function MessageInput(props: Props) {
if (e.key === "Enter") {
e.preventDefault();
const shouldFail = app.experiments.isTreatmentEnabled(
"message_queue",
2,
);
const shouldSend = !app.experiments.isTreatmentEnabled(
"message_queue",
1,
);
const shouldFail = app.experiments.isTreatmentEnabled("message_queue", 2);
const shouldSend = !app.experiments.isTreatmentEnabled("message_queue", 1);
if (!props.channel.canSendMessage(content) && !shouldFail) return;

View File

@ -16,8 +16,7 @@ const Wrapper = styled(Container)<{
width: 48px;
height: 48px;
border-radius: ${(props) => (props.active ? "30%" : "50%")};
background-color: ${(props) =>
props.active ? "var(--primary)" : "var(--background-secondary)"};
background-color: ${(props) => (props.active ? "var(--primary)" : "var(--background-secondary)")};
display: flex;
align-items: center;
justify-content: center;
@ -25,10 +24,7 @@ const Wrapper = styled(Container)<{
&:hover {
border-radius: 30%;
background-color: ${(props) =>
props.useGreenColorScheme
? "var(--success)"
: "var(--primary)"};
background-color: ${(props) => (props.useGreenColorScheme ? "var(--success)" : "var(--primary)")};
}
`;
@ -36,10 +32,7 @@ const Wrapper = styled(Container)<{
interface Props {
tooltip?: string;
action?: () => void;
image?: React.DetailedHTMLProps<
React.ImgHTMLAttributes<HTMLImageElement>,
HTMLImageElement
>;
image?: React.DetailedHTMLProps<React.ImgHTMLAttributes<HTMLImageElement>, HTMLImageElement>;
icon?: IconProps;
label?: string;
margin?: boolean;
@ -50,9 +43,7 @@ interface Props {
function SidebarAction(props: Props) {
if (props.image && props.icon && props.label)
throw new Error(
"SidebarAction can only have one of image, icon, or label",
);
throw new Error("SidebarAction can only have one of image, icon, or label");
const [pillType, setPillType] = React.useState<PillType>("none");
const [isHovered, setHovered] = React.useState(false);
@ -79,9 +70,12 @@ function SidebarAction(props: Props) {
useGreenColorScheme={props.useGreenColorScheme}
>
{props.image && <img {...props.image} />}
{props.icon && <Icon {...props.icon} color={
isHovered && props.useGreenColorScheme ? "#fff" : props.icon.color
} />}
{props.icon && (
<Icon
{...props.icon}
color={isHovered && props.useGreenColorScheme ? "#fff" : props.icon.color}
/>
)}
{props.label && <span>{props.label}</span>}
</Wrapper>
</Tooltip>

View File

@ -1,7 +1,4 @@
import MuiTooltip, {
TooltipProps as MuiTooltipProps,
tooltipClasses,
} from "@mui/material/Tooltip";
import MuiTooltip, { TooltipProps as MuiTooltipProps, tooltipClasses } from "@mui/material/Tooltip";
import styled from "styled-components";
export default styled(({ className, ...props }: MuiTooltipProps) => (

View File

@ -67,20 +67,14 @@ function UserPanel() {
<div>
<Username>{app.account?.username}</Username>
</div>
<Discriminator>
#{app.account?.discriminator}
</Discriminator>
<Discriminator>#{app.account?.discriminator}</Discriminator>
</Name>
</AvatarWrapper>
<ActionsWrapper>
<Tooltip title="Settings">
<span>
<IconButton
aria-label="settings"
disabled
color="#fff"
>
<IconButton aria-label="settings" disabled color="#fff">
<Icon icon="mdiCog" size="20px" />
</IconButton>
</span>

View File

@ -22,12 +22,7 @@ function HCaptchaModal({ open, siteKey, onVerify }: Props) {
return open ? (
<Wrapper>
<HCaptchaLib
sitekey={siteKey}
onLoad={onLoad}
onVerify={onVerify}
ref={ref}
/>
<HCaptchaLib sitekey={siteKey} onLoad={onLoad} onVerify={onVerify} ref={ref} />
</Wrapper>
) : null;
}

View File

@ -69,9 +69,7 @@ function AddServerModal() {
<ModalHeader>
<ModalHeaderText>Add a Guild</ModalHeaderText>
<ModalSubHeaderText>
Lorem ipsum dolor sit amet, consectetur adipiscing elit.
</ModalSubHeaderText>
<ModalSubHeaderText>Lorem ipsum dolor sit amet, consectetur adipiscing elit.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>

View File

@ -7,13 +7,7 @@ import styled from "styled-components";
import useLogger from "../../hooks/useLogger";
import { useAppStore } from "../../stores/AppStore";
import { messageFromFieldError } from "../../utils/messageFromFieldError";
import {
Input,
InputErrorText,
InputLabel,
InputWrapper,
LabelWrapper,
} from "../AuthComponents";
import { Input, InputErrorText, InputLabel, InputWrapper, LabelWrapper } from "../AuthComponents";
import { Divider } from "../Divider";
import Icon from "../Icon";
import AddServerModal from "./AddServerModal";
@ -169,20 +163,14 @@ function CreateServerModal() {
<ModalHeader>
<ModalHeaderText>Customize your guild</ModalHeaderText>
<ModalSubHeaderText>
Give your new guild a personality with a name and an
icon. You can always change it later.
Give your new guild a personality with a name and an icon. You can always change it later.
</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>
<UploadIcon>
<IconContainer>
<svg
width="80"
height="80"
viewBox="0 0 80 80"
fill="none"
>
<svg width="80" height="80" viewBox="0 0 80 80" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
@ -244,12 +232,7 @@ function CreateServerModal() {
</ModelContentContainer>
<ModalFooter>
<ModalActionItem
variant="filled"
size="med"
onClick={onSubmit}
disabled={isLoading}
>
<ModalActionItem variant="filled" size="med" onClick={onSubmit} disabled={isLoading}>
Create
</ModalActionItem>

View File

@ -59,17 +59,12 @@ function ForgotPasswordModal() {
</ModalHeader>
<ModelContentContainer>
We sent instructions to change your password to
user@example.com, please check both your inbox and spam
folder.
We sent instructions to change your password to user@example.com, please check both your inbox and
spam folder.
</ModelContentContainer>
<ModalFooter>
<SubmitButton
variant="filled"
size="med"
onClick={closeModal}
>
<SubmitButton variant="filled" size="med" onClick={closeModal}>
Okay
</SubmitButton>
</ModalFooter>

View File

@ -139,9 +139,7 @@ function JoinServerModal() {
<ModalHeader>
<ModalHeaderText>Join a Guild</ModalHeaderText>
<ModalSubHeaderText>
Enter an invite below to join an existing guild.
</ModalSubHeaderText>
<ModalSubHeaderText>Enter an invite below to join an existing guild.</ModalSubHeaderText>
</ModalHeader>
<ModelContentContainer>

View File

@ -25,7 +25,8 @@ export const ModalWrapper = styled.div`
width: 440px;
border-radius: 10px;
background-color: var(--background-secondary);
box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.05),
box-shadow:
0 1px 2px 0 rgba(0, 0, 0, 0.05),
0 1px 3px 1px rgba(0, 0, 0, 0.05);
position: relative;
display: flex;

View File

@ -5,6 +5,7 @@ import { useAppStore } from "../stores/AppStore";
export type ThemeVariables =
| "backgroundPrimary"
| "backgroundPrimaryAlt"
| "backgroundPrimaryHighlight"
| "backgroundSecondary"
| "backgroundSecondaryAlt"
| "backgroundSecondaryHighlight"
@ -52,6 +53,7 @@ export const ThemePresets: Record<string, Theme> = {
light: {
backgroundPrimary: "#ffffff",
backgroundPrimaryAlt: "",
backgroundPrimaryHighlight: "",
backgroundSecondary: "#ebe5e4",
backgroundSecondaryAlt: "#ebe5e4",
backgroundSecondaryHighlight: "#ebe5e4",
@ -90,6 +92,7 @@ export const ThemePresets: Record<string, Theme> = {
dark: {
backgroundPrimary: "#2e2e2e",
backgroundPrimaryAlt: "#2a2a2a",
backgroundPrimaryHighlight: "#262626",
backgroundSecondary: "#232323",
backgroundSecondaryAlt: "#1e1e1e",
backgroundSecondaryHighlight: "#383838",
@ -133,8 +136,7 @@ const GlobalTheme = createGlobalStyle<{ theme: Theme }>`
}
`;
const toDashed = (str: string) =>
str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
const toDashed = (str: string) => str.replace(/[A-Z]/g, (m) => "-" + m.toLowerCase());
export const generateVariables = (theme: Theme) => {
return (Object.keys(theme) as ThemeVariables[]).map((key) => {
@ -143,9 +145,7 @@ export const generateVariables = (theme: Theme) => {
const r = parseInt(colour.substring(1, 3), 16);
const g = parseInt(colour.substring(3, 5), 16);
const b = parseInt(colour.substring(5, 7), 16);
return `--${toDashed(key)}: ${theme[key]}; --${toDashed(
key,
)}-rgb: rgb(${r}, ${g}, ${b});`;
return `--${toDashed(key)}: ${theme[key]}; --${toDashed(key)}-rgb: rgb(${r}, ${g}, ${b});`;
} catch {
return `--${toDashed(key)}: ${theme[key]};`;
}

View File

@ -7,9 +7,7 @@ import App from "./App";
import Theme from "./contexts/Theme";
import "./index.css";
const root = ReactDOM.createRoot(
document.getElementById("root") as HTMLElement,
);
const root = ReactDOM.createRoot(document.getElementById("root") as HTMLElement);
root.render(
<BrowserRouter>
<ModalStack>

View File

@ -51,8 +51,7 @@ function LoginPage() {
const navigate = useNavigate();
const [loading, setLoading] = React.useState(false);
const [captchaSiteKey, setCaptchaSiteKey] = React.useState<string>();
const [mfaData, setMfaData] =
React.useState<IAPILoginResponseMFARequired>();
const [mfaData, setMfaData] = React.useState<IAPILoginResponseMFARequired>();
const captchaRef = React.useRef<HCaptchaLib>(null);
const [debounce, setDebounce] = React.useState<NodeJS.Timeout | null>(null);
const [isCheckingInstance, setCheckingInstance] = React.useState(false);
@ -196,9 +195,7 @@ function LoginPage() {
type: "manual",
message:
(e instanceof Error &&
(e?.message?.length > 60
? e.message.slice(0, 60) + "..."
: e.message)) ||
(e?.message?.length > 60 ? e.message.slice(0, 60) + "..." : e.message)) ||
"Instance could not be resolved",
});
}
@ -217,13 +214,7 @@ function LoginPage() {
};
if (captchaSiteKey) {
return (
<HCaptcha
captchaRef={captchaRef}
sitekey={captchaSiteKey}
onVerify={onCaptchaVerify}
/>
);
return <HCaptcha captchaRef={captchaRef} sitekey={captchaSiteKey} onVerify={onCaptchaVerify} />;
}
if (mfaData) {
@ -247,10 +238,7 @@ function LoginPage() {
</HeaderContainer>
<FormContainer onSubmit={onSubmit}>
<InputContainer
marginBottom={true}
style={{ marginTop: 0 }}
>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.instance}>
<InputLabel>Instance</InputLabel>
{isCheckingInstance != false && (
@ -337,18 +325,12 @@ function LoginPage() {
Forgot your password?
</PasswordResetLink> */}
<SubmitButton
variant="primary"
type="submit"
disabled={loading}
>
<SubmitButton variant="primary" type="submit" disabled={loading}>
Login
</SubmitButton>
<AuthSwitchPageContainer>
<AuthSwitchPageLabel>
New to Spacebar?&nbsp;
</AuthSwitchPageLabel>
<AuthSwitchPageLabel>New to Spacebar?&nbsp;</AuthSwitchPageLabel>
<AuthSwitchPageLink
onClick={() => {
navigate("/register");

View File

@ -26,11 +26,7 @@ import { Divider } from "../components/Divider";
import HCaptcha from "../components/HCaptcha";
import useLogger from "../hooks/useLogger";
import { AUTH_NO_BRANDING, useAppStore } from "../stores/AppStore";
import {
IAPILoginResponseSuccess,
IAPIRegisterRequest,
IAPIRegisterResponseError,
} from "../utils/interfaces/api";
import { IAPILoginResponseSuccess, IAPIRegisterRequest, IAPIRegisterResponseError } from "../utils/interfaces/api";
import { messageFromFieldError } from "../utils/messageFromFieldError";
type FormValues = {
@ -77,13 +73,10 @@ function RegistrationPage() {
setValue("captcha_key", undefined);
app.rest
.post<IAPIRegisterRequest, IAPILoginResponseSuccess>(
Routes.register(),
{
...data,
consent: true,
},
)
.post<IAPIRegisterRequest, IAPILoginResponseSuccess>(Routes.register(), {
...data,
consent: true,
})
.then((r) => {
if ("token" in r) {
// success
@ -163,13 +156,7 @@ function RegistrationPage() {
};
if (captchaSiteKey) {
return (
<HCaptcha
captchaRef={captchaRef}
sitekey={captchaSiteKey}
onVerify={onCaptchaVerify}
/>
);
return <HCaptcha captchaRef={captchaRef} sitekey={captchaSiteKey} onVerify={onCaptchaVerify} />;
}
return (
@ -187,10 +174,7 @@ function RegistrationPage() {
)}
<FormContainer onSubmit={onSubmit}>
<InputContainer
marginBottom={true}
style={{ marginTop: 0 }}
>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.email}>
<InputLabel>Email</InputLabel>
{errors.email && (
@ -214,10 +198,7 @@ function RegistrationPage() {
</InputWrapper>
</InputContainer>
<InputContainer
marginBottom={true}
style={{ marginTop: 0 }}
>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.username}>
<InputLabel>Username</InputLabel>
{errors.username && (
@ -277,20 +258,14 @@ function RegistrationPage() {
<InputWrapper>
<DOBInput
onChange={(value) =>
setValue("date_of_birth", value)
}
onChange={(value) => setValue("date_of_birth", value)}
onErrorChange={(errors) => {
const hasError = Object.values(errors).some(
(error) => error,
);
const hasError = Object.values(errors).some((error) => error);
if (hasError) {
// set to first error
setError("date_of_birth", {
type: "manual",
message: Object.values(
errors,
).filter((x) => x)[0],
message: Object.values(errors).filter((x) => x)[0],
});
} else clearErrors("date_of_birth");
}}
@ -300,18 +275,12 @@ function RegistrationPage() {
</InputWrapper>
</InputContainer>
<SubmitButton
variant="primary"
type="submit"
disabled={loading}
>
<SubmitButton variant="primary" type="submit" disabled={loading}>
Create Account
</SubmitButton>
<AuthSwitchPageContainer>
<AuthSwitchPageLabel>
Already have an account?&nbsp;
</AuthSwitchPageLabel>
<AuthSwitchPageLabel>Already have an account?&nbsp;</AuthSwitchPageLabel>
<AuthSwitchPageLink
onClick={() => {
navigate("/login");

View File

@ -99,16 +99,10 @@ function MFA(props: IAPILoginResponseMFARequired) {
<HeaderContainer>
<SpacebarLogoBlue height={48} width="auto" />
<Header>Two-factor authentication</Header>
<SubHeader>
You can use a backup code or your two-factor
authentication mobile app.
</SubHeader>
<SubHeader>You can use a backup code or your two-factor authentication mobile app.</SubHeader>
<FormContainer onSubmit={onSubmit}>
<InputContainer
marginBottom={true}
style={{ marginTop: 0 }}
>
<InputContainer marginBottom={true} style={{ marginTop: 0 }}>
<LabelWrapper error={!!errors.code}>
<InputLabel>Enter 2FA/Backup Code</InputLabel>
{errors.code && (
@ -132,11 +126,7 @@ function MFA(props: IAPILoginResponseMFARequired) {
</InputWrapper>
</InputContainer>
<SubmitButton
variant="primary"
type="submit"
disabled={loading}
>
<SubmitButton variant="primary" type="submit" disabled={loading}>
Log In
</SubmitButton>
@ -154,10 +144,7 @@ function MFA(props: IAPILoginResponseMFARequired) {
<Link
onClick={() => {
window.open(
"https://youtu.be/dQw4w9WgXcQ",
"_blank",
);
window.open("https://youtu.be/dQw4w9WgXcQ", "_blank");
}}
type="button"
>

View File

@ -20,10 +20,7 @@ export default class AccountStore {
@observable bot = false;
@observable system = false;
@observable mfaEnabled = false;
@observable premiumType?:
| UserPremiumType.NitroClassic
| UserPremiumType.Nitro
| UserPremiumType.NitroBasic;
@observable premiumType?: UserPremiumType.NitroClassic | UserPremiumType.Nitro | UserPremiumType.NitroBasic;
@observable flags?: UserFlags;
@observable publicFlags?: UserFlags;
// phone: string | null;
@ -69,9 +66,7 @@ export default class AccountStore {
*/
get defaultAvatarUrl(): string {
return REST.makeCDNUrl(
CDNRoutes.defaultUserAvatar(
(Number(this.discriminator) % 5) as DefaultUserAvatarAssets,
),
CDNRoutes.defaultUserAvatar((Number(this.discriminator) % 5) as DefaultUserAvatarAssets),
);
}
@ -80,10 +75,7 @@ export default class AccountStore {
* @returns The URL to the user's avatar or the default avatar if they don't have one.
*/
get avatarUrl(): string {
if (this.avatar)
return REST.makeCDNUrl(
CDNRoutes.userAvatar(this.id, this.avatar, ImageFormat.PNG),
);
if (this.avatar) return REST.makeCDNUrl(CDNRoutes.userAvatar(this.id, this.avatar, ImageFormat.PNG));
else return this.defaultAvatarUrl;
}
}

View File

@ -96,9 +96,7 @@ export default class AppStore {
* Whether the app is done loading and ready to be displayed
*/
get isReady() {
return (
!this.isAppLoading && this.isGatewayReady && this.isNetworkConnected
);
return !this.isAppLoading && this.isGatewayReady && this.isNetworkConnected;
}
}

View File

@ -55,28 +55,17 @@ export default class ChannelStore {
category: Channel | null;
}[] = [];
const categories = this.sortPosition(
channels.filter((x) => x.type === ChannelType.GuildCategory),
);
const categorizedChannels = channels.filter(
(x) => x.type !== ChannelType.GuildCategory && x.parentId !== null,
);
const categories = this.sortPosition(channels.filter((x) => x.type === ChannelType.GuildCategory));
const categorizedChannels = channels.filter((x) => x.type !== ChannelType.GuildCategory && x.parentId !== null);
const uncategorizedChannels = this.sortPosition(
channels.filter(
(x) =>
x.type !== ChannelType.GuildCategory && x.parentId === null,
),
channels.filter((x) => x.type !== ChannelType.GuildCategory && x.parentId === null),
);
// for each category, add an object containing the category and its children
categories.forEach((category) => {
result.push({
id: category.id,
children: this.sortPosition(
categorizedChannels.filter(
(x) => x.parentId === category.id,
),
),
children: this.sortPosition(categorizedChannels.filter((x) => x.parentId === category.id)),
category: category,
});
});

View File

@ -96,61 +96,24 @@ export default class GatewayConnectionStore {
private setupDispatchHandler() {
this.dispatchHandlers.set(GatewayDispatchEvents.Ready, this.onReady);
this.dispatchHandlers.set(
GatewayDispatchEvents.Resumed,
this.onResumed,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.GuildCreate,
this.onGuildCreate,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.GuildUpdate,
this.onGuildUpdate,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.GuildDelete,
this.onGuildDelete,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.GuildMemberListUpdate,
this.onGuildMemberListUpdate,
);
this.dispatchHandlers.set(GatewayDispatchEvents.Resumed, this.onResumed);
this.dispatchHandlers.set(GatewayDispatchEvents.GuildCreate, this.onGuildCreate);
this.dispatchHandlers.set(GatewayDispatchEvents.GuildUpdate, this.onGuildUpdate);
this.dispatchHandlers.set(GatewayDispatchEvents.GuildDelete, this.onGuildDelete);
this.dispatchHandlers.set(GatewayDispatchEvents.GuildMemberListUpdate, this.onGuildMemberListUpdate);
this.dispatchHandlers.set(
GatewayDispatchEvents.ChannelCreate,
this.onChannelCreate,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.ChannelDelete,
this.onChannelDelete,
);
this.dispatchHandlers.set(GatewayDispatchEvents.ChannelCreate, this.onChannelCreate);
this.dispatchHandlers.set(GatewayDispatchEvents.ChannelDelete, this.onChannelDelete);
this.dispatchHandlers.set(
GatewayDispatchEvents.MessageCreate,
this.onMessageCreate,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.MessageUpdate,
this.onMessageUpdate,
);
this.dispatchHandlers.set(
GatewayDispatchEvents.MessageDelete,
this.onMessageDelete,
);
this.dispatchHandlers.set(GatewayDispatchEvents.MessageCreate, this.onMessageCreate);
this.dispatchHandlers.set(GatewayDispatchEvents.MessageUpdate, this.onMessageUpdate);
this.dispatchHandlers.set(GatewayDispatchEvents.MessageDelete, this.onMessageDelete);
this.dispatchHandlers.set(
GatewayDispatchEvents.PresenceUpdate,
this.onPresenceUpdate,
);
this.dispatchHandlers.set(GatewayDispatchEvents.PresenceUpdate, this.onPresenceUpdate);
}
private onopen = () => {
this.logger.debug(
`[Connected] ${this.url} (took ${
Date.now() - this.connectionStartTime!
}ms)`,
);
this.logger.debug(`[Connected] ${this.url} (took ${Date.now() - this.connectionStartTime!}ms)`);
this.readyState = WebSocket.OPEN;
this.handleIdentify();
@ -204,9 +167,7 @@ export default class GatewayConnectionStore {
}
if (this.socket.readyState !== WebSocket.OPEN) {
this.logger.error(
`Socket is not open; readyState: ${this.socket.readyState}`,
);
this.logger.error(`Socket is not open; readyState: ${this.socket.readyState}`);
return;
}
this.logger.debug(`[Gateway] <- ${payload.op}`, payload);
@ -287,9 +248,7 @@ export default class GatewayConnectionStore {
private handleHello = (data: GatewayHelloData) => {
this.heartbeatInterval = data.heartbeat_interval;
this.logger.info(
`[Hello] heartbeat interval: ${data.heartbeat_interval} (took ${
Date.now() - this.connectionStartTime!
}ms)`,
`[Hello] heartbeat interval: ${data.heartbeat_interval} (took ${Date.now() - this.connectionStartTime!}ms)`,
);
this.startHeartbeater();
};
@ -355,14 +314,14 @@ export default class GatewayConnectionStore {
}
};
this.initialHeartbeatTimeout = setTimeout(() => {
this.initialHeartbeatTimeout = null;
this.heartbeater = setInterval(
heartbeaterFn,
this.heartbeatInterval!,
);
heartbeaterFn();
}, Math.floor(Math.random() * this.heartbeatInterval!));
this.initialHeartbeatTimeout = setTimeout(
() => {
this.initialHeartbeatTimeout = null;
this.heartbeater = setInterval(heartbeaterFn, this.heartbeatInterval!);
heartbeaterFn();
},
Math.floor(Math.random() * this.heartbeatInterval!),
);
};
/**
@ -385,9 +344,7 @@ export default class GatewayConnectionStore {
*/
private handleHeartbeatTimeout = () => {
this.logger.warn(
`[Heartbeat ACK Timeout] should reconnect in ${(
this.heartbeatInterval! / 1000
).toFixed(2)} seconds`,
`[Heartbeat ACK Timeout] should reconnect in ${(this.heartbeatInterval! / 1000).toFixed(2)} seconds`,
);
this.socket?.close(4009);
@ -453,9 +410,7 @@ export default class GatewayConnectionStore {
* Processes a ready dispatch event
*/
private onReady = (data: GatewayReadyDispatchData) => {
this.logger.info(
`[Ready] took ${Date.now() - this.connectionStartTime!}ms`,
);
this.logger.info(`[Ready] took ${Date.now() - this.connectionStartTime!}ms`);
const { session_id, guilds, users, user, private_channels } = data;
this.sessionId = session_id;
@ -540,17 +495,13 @@ export default class GatewayConnectionStore {
});
};
private onGuildMemberListUpdate = (
data: GatewayGuildMemberListUpdateDispatchData,
) => {
private onGuildMemberListUpdate = (data: GatewayGuildMemberListUpdateDispatchData) => {
this.logger.debug("Received GuildMemberListUpdate event");
const { guild_id } = data;
const guild = this.app.guilds.get(guild_id);
if (!guild) {
this.logger.warn(
`[GuildMemberListUpdate] Guild ${guild_id} not found`,
);
this.logger.warn(`[GuildMemberListUpdate] Guild ${guild_id} not found`);
return;
}
@ -565,9 +516,7 @@ export default class GatewayConnectionStore {
const guild = this.app.guilds.get(data.guild_id!);
if (!guild) {
this.logger.warn(
`[ChannelCreate] Guild ${data.guild_id} not found for channel ${data.id}`,
);
this.logger.warn(`[ChannelCreate] Guild ${data.guild_id} not found for channel ${data.id}`);
return;
}
guild.channels.add(data);
@ -581,9 +530,7 @@ export default class GatewayConnectionStore {
const guild = this.app.guilds.get(data.guild_id!);
if (!guild) {
this.logger.warn(
`[ChannelDelete] Guild ${data.guild_id} not found for channel ${data.id}`,
);
this.logger.warn(`[ChannelDelete] Guild ${data.guild_id} not found for channel ${data.id}`);
return;
}
guild.channels.remove(data.id);
@ -592,16 +539,12 @@ export default class GatewayConnectionStore {
private onMessageCreate = (data: GatewayMessageCreateDispatchData) => {
const guild = this.app.guilds.get(data.guild_id!);
if (!guild) {
this.logger.warn(
`[MessageCreate] Guild ${data.guild_id} not found for channel ${data.id}`,
);
this.logger.warn(`[MessageCreate] Guild ${data.guild_id} not found for channel ${data.id}`);
return;
}
const channel = guild.channels.get(data.channel_id);
if (!channel) {
this.logger.warn(
`[MessageCreate] Channel ${data.channel_id} not found for message ${data.id}`,
);
this.logger.warn(`[MessageCreate] Channel ${data.channel_id} not found for message ${data.id}`);
return;
}
@ -612,16 +555,12 @@ export default class GatewayConnectionStore {
private onMessageUpdate = (data: GatewayMessageUpdateDispatchData) => {
const guild = this.app.guilds.get(data.guild_id!);
if (!guild) {
this.logger.warn(
`[MessageUpdate] Guild ${data.guild_id} not found for channel ${data.id}`,
);
this.logger.warn(`[MessageUpdate] Guild ${data.guild_id} not found for channel ${data.id}`);
return;
}
const channel = guild.channels.get(data.channel_id);
if (!channel) {
this.logger.warn(
`[MessageUpdate] Channel ${data.channel_id} not found for message ${data.id}`,
);
this.logger.warn(`[MessageUpdate] Channel ${data.channel_id} not found for message ${data.id}`);
return;
}
@ -631,16 +570,12 @@ export default class GatewayConnectionStore {
private onMessageDelete = (data: GatewayMessageDeleteDispatchData) => {
const guild = this.app.guilds.get(data.guild_id!);
if (!guild) {
this.logger.warn(
`[MessageDelete] Guild ${data.guild_id} not found for channel ${data.id}`,
);
this.logger.warn(`[MessageDelete] Guild ${data.guild_id} not found for channel ${data.id}`);
return;
}
const channel = guild.channels.get(data.channel_id);
if (!channel) {
this.logger.warn(
`[MessageDelete] Channel ${data.channel_id} not found for message ${data.id}`,
);
this.logger.warn(`[MessageDelete] Channel ${data.channel_id} not found for message ${data.id}`);
return;
}

View File

@ -20,11 +20,7 @@ export default class GuildMemberListStore {
@observable online_count: number;
@observable list: (string | GuildMember)[] = [];
constructor(
app: AppStore,
guild: Guild,
data: GatewayGuildMemberListUpdateDispatchData,
) {
constructor(app: AppStore, guild: Guild, data: GatewayGuildMemberListUpdateDispatchData) {
this.app = app;
this.guild = guild;
@ -48,9 +44,7 @@ export default class GuildMemberListStore {
this.computeListData(data.ops);
}
private computeListData(
ops: GatewayGuildMemberListUpdateDispatchData["ops"],
) {
private computeListData(ops: GatewayGuildMemberListUpdateDispatchData["ops"]) {
for (const i of ops) {
const { op, items, range, item, index } = i;
switch (op) {
@ -65,17 +59,13 @@ export default class GuildMemberListStore {
const role = this.guild.roles.get(item.group.id);
listData.push({
title: `${(
role?.name ?? item.group.id
).toUpperCase()}`,
title: `${(role?.name ?? item.group.id).toUpperCase()}`,
data: [],
});
} else {
// try to get the existing member
if (item.member.user?.id) {
const member = this.guild.members.get(
item.member.user.id,
);
const member = this.guild.members.get(item.member.user.id);
if (member) {
listData[listData.length - 1].data.push({
member,
@ -85,11 +75,7 @@ export default class GuildMemberListStore {
}
}
listData[listData.length - 1].data.push({
member: new GuildMember(
this.app,
this.guild,
item.member,
),
member: new GuildMember(this.app, this.guild, item.member),
index: item.member.index,
});
}
@ -105,11 +91,7 @@ export default class GuildMemberListStore {
// hide offline group if it has more than 100 members
listData = listData.filter(
(i) =>
!(
i.title.toLowerCase().startsWith("offline") &&
i.data.length >= 100
),
(i) => !(i.title.toLowerCase().startsWith("offline") && i.data.length >= 100),
);
// sort the list by the index
@ -125,9 +107,7 @@ export default class GuildMemberListStore {
const ua = a.member.user?.username;
const ub = b.member.user?.username;
if (ua && ub) {
return ua.toLowerCase() > ub.toLowerCase()
? 1
: -1;
return ua.toLowerCase() > ub.toLowerCase() ? 1 : -1;
}
return 0;

View File

@ -9,10 +9,7 @@ export default class GuildMemberStore {
private readonly app: AppStore;
private readonly guild: Guild;
@observable private readonly members = new ObservableMap<
Snowflake,
GuildMember
>();
@observable private readonly members = new ObservableMap<Snowflake, GuildMember>();
constructor(app: AppStore, guild: Guild) {
this.app = app;
@ -29,10 +26,7 @@ export default class GuildMemberStore {
if (this.members.has(member.user.id)) {
return;
}
this.members.set(
member.user.id,
new GuildMember(this.app, this.guild, member),
);
this.members.set(member.user.id, new GuildMember(this.app, this.guild, member));
}
@action

View File

@ -17,19 +17,13 @@ export default class PresenceStore {
Snowflake,
Map<
Snowflake,
Pick<
GatewayPresenceUpdate,
"activities" | "client_status" | "status"
> & {
Pick<GatewayPresenceUpdate, "activities" | "client_status" | "status"> & {
timestamp: number;
}
>
>();
@observable activities = observable.map<Snowflake, GatewayActivity[]>();
@observable clientStatuses = observable.map<
Snowflake,
OneKeyFrom<GatewayPresenceClientStatus>
>();
@observable clientStatuses = observable.map<Snowflake, OneKeyFrom<GatewayPresenceClientStatus>>();
constructor(app: AppStore) {
this.app = app;
@ -38,11 +32,7 @@ export default class PresenceStore {
}
@action
add(
presence:
| GatewayPresenceUpdate
| GatewayGuildMemberListUpdateMember["presence"],
) {
add(presence: GatewayPresenceUpdate | GatewayGuildMemberListUpdateMember["presence"]) {
if (presence.status) {
this.presences.set(presence.user.id, presence.status);
}

View File

@ -86,8 +86,7 @@ export default class Channel {
this.readStates = channel.read_states;
this.webhooks = channel.webhooks;
this.flags = channel.flags;
this.defaultThreadRateLimitPerUser =
channel.default_thread_rate_limit_per_user;
this.defaultThreadRateLimitPerUser = channel.default_thread_rate_limit_per_user;
if (channel.messages) {
this.messages.addAll(channel.messages);
@ -174,10 +173,7 @@ export default class Channel {
this.hasFetchedMessages = true;
this.logger.info(`Fetching messags for ${this.id}`);
app.rest
.get<RESTGetAPIChannelMessagesResult | APIError>(
Routes.channelMessages(this.id),
opts,
)
.get<RESTGetAPIChannelMessagesResult | APIError>(Routes.channelMessages(this.id), opts)
.then((res) => {
if ("code" in res) {
this.logger.error(res);
@ -200,10 +196,10 @@ export default class Channel {
@action
async sendMessage(data: RESTPostAPIChannelMessageJSONBody) {
// TODO: handle errors, highlight message as failed
return this.app.rest.post<
RESTPostAPIChannelMessageJSONBody,
RESTPostAPIChannelMessageResult
>(Routes.channelMessages(this.id), data);
return this.app.rest.post<RESTPostAPIChannelMessageJSONBody, RESTPostAPIChannelMessageResult>(
Routes.channelMessages(this.id),
data,
);
}
canSendMessage(content: string) {

View File

@ -86,8 +86,7 @@ export default class Guild {
this.systemChannelId = data.properties.system_channel_id;
this.verificationLevel = data.properties.verification_level;
this.explicitContentFilter = data.properties.explicit_content_filter;
this.defaultMessageNotifications =
data.properties.default_message_notifications;
this.defaultMessageNotifications = data.properties.default_message_notifications;
this.mfaLevel = data.properties.mfa_level;
this.vanityUrlCode = data.properties.vanity_url_code;
this.premiumTier = data.properties.premium_tier;
@ -125,11 +124,7 @@ export default class Guild {
if (this.memberListStore) {
this.memberListStore.update(data);
} else {
this.memberListStore = new GuildMemberListStore(
this.app,
this,
data,
);
this.memberListStore = new GuildMemberListStore(this.app, this, data);
}
}

View File

@ -25,20 +25,14 @@ export default class GuildMember {
@observable pending?: boolean | undefined;
@observable communication_disabled_until?: string | null | undefined;
constructor(
app: AppStore,
guild: Guild,
data: APIGuildMember | GatewayGuildMemberListUpdateMember,
) {
constructor(app: AppStore, guild: Guild, data: APIGuildMember | GatewayGuildMemberListUpdateMember) {
this.app = app;
this.guild = guild;
this.user = data.user;
this.nick = data.nick;
this.avatar = data.avatar;
this.roles = data.roles
.map((role) => guild.roles.get(role))
.filter(Boolean) as Role[];
this.roles = data.roles.map((role) => guild.roles.get(role)).filter(Boolean) as Role[];
this.joined_at = data.joined_at;
this.premium_since = data.premium_since;
this.deaf = data.deaf;

View File

@ -238,9 +238,7 @@ export default class Message {
// this.member = message.member ? new GuildMember(message.member) : undefined;
this.content = message.content;
this.timestamp = new Date(message.timestamp);
this.edited_timestamp = message.edited_timestamp
? new Date(message.edited_timestamp)
: null;
this.edited_timestamp = message.edited_timestamp ? new Date(message.edited_timestamp) : null;
this.tts = message.tts;
this.mention_everyone = message.mention_everyone;
this.mentions = message.mentions; // TODO: user object?
@ -280,8 +278,6 @@ export default class Message {
Object.assign(this, message);
this.timestamp = new Date(message.timestamp);
this.edited_timestamp = message.edited_timestamp
? new Date(message.edited_timestamp)
: null;
this.edited_timestamp = message.edited_timestamp ? new Date(message.edited_timestamp) : null;
}
}

View File

@ -1,10 +1,5 @@
import { Snowflake } from "@spacebarchat/spacebar-api-types/globals";
import {
APIUser,
CDNRoutes,
DefaultUserAvatarAssets,
ImageFormat,
} from "@spacebarchat/spacebar-api-types/v9";
import { APIUser, CDNRoutes, DefaultUserAvatarAssets, ImageFormat } from "@spacebarchat/spacebar-api-types/v9";
import { makeObservable, observable } from "mobx";
import REST from "../../utils/REST";
@ -55,9 +50,7 @@ export default class User {
*/
get defaultAvatarUrl(): string {
return REST.makeCDNUrl(
CDNRoutes.defaultUserAvatar(
(Number(this.discriminator) % 5) as DefaultUserAvatarAssets,
),
CDNRoutes.defaultUserAvatar((Number(this.discriminator) % 5) as DefaultUserAvatarAssets),
);
}
@ -66,10 +59,7 @@ export default class User {
* @returns The URL to the user's avatar or the default avatar if they don't have one.
*/
get avatarUrl(): string {
if (this.avatar)
return REST.makeCDNUrl(
CDNRoutes.userAvatar(this.id, this.avatar, ImageFormat.PNG),
);
if (this.avatar) return REST.makeCDNUrl(CDNRoutes.userAvatar(this.id, this.avatar, ImageFormat.PNG));
else return this.defaultAvatarUrl;
}
}

View File

@ -33,10 +33,7 @@ export const Globals: {
logger.info("Loaded route settings from storage");
},
save: () => {
localStorage.setItem(
"routeSettings",
JSON.stringify(Globals.routeSettings),
);
localStorage.setItem("routeSettings", JSON.stringify(Globals.routeSettings));
},
routeSettings: DefaultRouteSettings,
};

View File

@ -6,34 +6,18 @@ export default class Logger {
}
debug(...args: any[]) {
console.debug(
`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`,
`color: LimeGreen`,
...args,
);
console.debug(`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`, `color: LimeGreen`, ...args);
}
info(...args: any[]) {
console.debug(
`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`,
`color: DodgerBlue`,
...args,
);
console.debug(`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`, `color: DodgerBlue`, ...args);
}
warn(...args: any[]) {
console.debug(
`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`,
`color: Tomato`,
...args,
);
console.debug(`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`, `color: Tomato`, ...args);
}
error(...args: any[]) {
console.debug(
`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`,
`color: Red`,
...args,
);
console.debug(`%c${new Date().toLocaleTimeString()} | ${this.name} | DEBUG |`, `color: Red`, ...args);
}
}

View File

@ -29,9 +29,7 @@ export default class REST {
}
}
public static async getEndpointsFromDomain(
url: URL,
): Promise<RouteSettings> {
public static async getEndpointsFromDomain(url: URL): Promise<RouteSettings> {
try {
return await this.getInstanceDomains(url, url);
} catch (e) {
@ -47,14 +45,9 @@ export default class REST {
return await this.getInstanceDomains(wellKnown, url);
}
static async getInstanceDomains(
url: URL,
knownas: URL,
): Promise<RouteSettings> {
static async getInstanceDomains(url: URL, knownas: URL): Promise<RouteSettings> {
const endpoints = await fetch(
`${url.toString()}${
url.pathname.includes("api") ? "" : "api"
}/policies/instance/domains`,
`${url.toString()}${url.pathname.includes("api") ? "" : "api"}/policies/instance/domains`,
).then((x) => x.json());
return {
api: endpoints.apiEndpoint,
@ -125,11 +118,7 @@ export default class REST {
resolve(await res.json());
} else {
// reject with json if content type is json
if (
res.headers
.get("content-type")
?.includes("application/json")
) {
if (res.headers.get("content-type")?.includes("application/json")) {
return reject(await res.json());
}

View File

@ -17,9 +17,7 @@ export default class Snowflake {
static workerId = BigInt(0 % 31); // max 31
constructor() {
throw new Error(
`The ${this.constructor.name} class may not be instantiated.`,
);
throw new Error(`The ${this.constructor.name} class may not be instantiated.`);
}
/**
@ -114,9 +112,7 @@ export default class Snowflake {
* @returns {DeconstructedSnowflake} Deconstructed snowflake
*/
static deconstruct(snowflake) {
const BINARY = Snowflake.idToBinary(snowflake)
.toString(2)
.padStart(64, "0");
const BINARY = Snowflake.idToBinary(snowflake).toString(2).padStart(64, "0");
const res = {
timestamp: parseInt(BINARY.substring(0, 42), 2) + Snowflake.EPOCH,
workerID: parseInt(BINARY.substring(42, 47), 2),

View File

@ -60,14 +60,9 @@ export interface IAPIError {
};
}
export type IAPILoginResponse =
| IAPILoginResponseSuccess
| IAPILoginResponseMFARequired;
export type IAPILoginResponse = IAPILoginResponseSuccess | IAPILoginResponseMFARequired;
export type IAPILoginResponseError =
| IAPILoginResponseMFARequired
| IAPIResponseCaptchaRequired
| IAPIError;
export type IAPILoginResponseError = IAPILoginResponseMFARequired | IAPIResponseCaptchaRequired | IAPIError;
export interface IAPILoginRequest {
login: string;
@ -118,9 +113,9 @@ export interface APIError {
_errors: {
code: string;
message: string;
}
}
}
};
};
};
}
// export type RESTAPIPostInviteResponse = {} | IAPIError;

View File

@ -4,9 +4,7 @@ export type OneKeyFrom<
K extends keyof T = keyof T,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
> = K extends any
? M &
Pick<Required<T>, K> &
Partial<Record<Exclude<keyof T, K>, never>> extends infer O
? M & Pick<Required<T>, K> & Partial<Record<Exclude<keyof T, K>, never>> extends infer O
? { [P in keyof O]: O[P] }
: never
: never;