diff --git a/index.html b/index.html
index 81aee13..dc166b9 100644
--- a/index.html
+++ b/index.html
@@ -1,4 +1,4 @@
-
+
diff --git a/src/components/SectionTitle.tsx b/src/components/SectionTitle.tsx
new file mode 100644
index 0000000..6026bac
--- /dev/null
+++ b/src/components/SectionTitle.tsx
@@ -0,0 +1,26 @@
+import React from "react";
+import styled from "styled-components";
+
+const Container = styled.div`
+ display: flex;
+`;
+
+const Text = styled.h2`
+ color: var(--text);
+ margin-bottom: 20px;
+ font-size: 20px;
+ font-weight: var(--font-weight-medium);
+ flex: 1;
+`;
+
+interface Props {}
+
+function SectionTitle({ children }: React.PropsWithChildren) {
+ return (
+
+ {children}
+
+ );
+}
+
+export default SectionTitle;
diff --git a/src/components/modals/ModalComponents.tsx b/src/components/modals/ModalComponents.tsx
index eef6af9..984c969 100644
--- a/src/components/modals/ModalComponents.tsx
+++ b/src/components/modals/ModalComponents.tsx
@@ -27,6 +27,7 @@ interface ModalProps {
disabled?: boolean;
withEmptyActionBar?: boolean;
withoutCloseButton?: boolean;
+ fullScreen?: boolean;
}
/**
@@ -48,7 +49,7 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
animation-fill-mode: forwards;
display: grid;
- overflow-y: auto;
+ overflow: hidden;
place-items: center;
color: var(--text);
@@ -72,12 +73,8 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
* Wrapper for modal content, handles the sizing and positioning
*/
export const ModalWrapper = styled.div<
- Pick & { actions: boolean }
+ Pick & { actions: boolean; fullScreen?: boolean }
>`
- min-height: 200px;
- max-width: min(calc(100vw - 20px), ${(props) => props.maxWidth ?? "450px"});
- max-height: min(calc(100vh - 20px), ${(props) => props.maxHeight ?? "650px"});
-
margin: 20px;
display: flex;
flex-direction: column;
@@ -86,17 +83,28 @@ export const ModalWrapper = styled.div<
animation-duration: 0.25s;
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
+ overflow: hidden;
+ background: var(--background-tertiary);
+
${(props) =>
- !props.maxWidth &&
+ !props.fullScreen &&
+ css`
+ min-height: 200px;
+ max-width: min(calc(100vw - 20px), ${props.maxWidth ?? "450px"});
+ max-height: min(calc(100vh - 20px), ${props.maxHeight ?? "650px"});
+ `}
+
+ ${(props) =>
+ props.fullScreen &&
css`
width: 100%;
+ height: 100%;
`}
${(props) =>
!props.transparent &&
+ !props.fullScreen &&
css`
- overflow: hidden;
- background: var(--background-primary);
border-radius: 8px;
`}
`;
@@ -122,12 +130,6 @@ export const ModalContentContainer = styled.div
- !props.transparent &&
- css`
- background: var(--background-primary);
- `}
`;
const Actions = styled.div`
diff --git a/src/components/modals/SettingsModal.tsx b/src/components/modals/SettingsModal.tsx
index 1e146fb..7538ffb 100644
--- a/src/components/modals/SettingsModal.tsx
+++ b/src/components/modals/SettingsModal.tsx
@@ -1,105 +1,264 @@
-import { FormControlLabel, FormGroup, Switch } from "@mui/material";
import { observer } from "mobx-react-lite";
-import styled from "styled-components";
+import { useState } from "react";
+import styled, { css } from "styled-components";
import { ModalProps } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore";
import { isTauri } from "../../utils/Utils";
-import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
-import Button from "../Button";
+import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
+import Icon from "../Icon";
import Link from "../Link";
import { Modal } from "./ModalComponents";
+import AccountSettingsPage from "./SettingsPages/AccountSettingsPage";
+import DeveloperSettingsPage from "./SettingsPages/DeveloperSettingsPage";
+import ExperimentsPage from "./SettingsPages/ExperimentsPage";
-const Wrapper = styled.div`
- padding: 16px 0;
- gap: 8px;
+const SidebarView = styled.div`
+ display: flex;
+ flex: 1;
+ overflow: hidden;
+`;
+
+const Sidebar = styled.div`
+ display: flex;
+ flex: 1 0 220px;
+ justify-content: flex-end;
+`;
+
+const SidebarInner = styled.div`
+ overflow: hidden scroll;
+ display: flex;
+ flex: 1 0 auto;
+ flex-direction: row;
+ justify-content: flex-end;
+ align-items: flex-start;
+ background: var(--background-secondary);
+`;
+
+const SidebarNav = styled.nav`
+ width: 220px;
+ padding: 60px 6px 20px;
+ box-sizing: border-box;
+`;
+
+const SidebarNavWrapper = styled.div`
display: flex;
flex-direction: column;
`;
-const ActionWrapper = styled.div`
- margin-top: 20px;
- gap: 8px;
+const Content = styled.div`
display: flex;
+ flex: 1 1 800px;
+ align-items: flex-start;
+ background: var(--background-primary);
`;
-const VersionWrapper = styled.div`
+const ContentInner = styled.div`
+ overflow: hidden scroll;
+ justify-content: flex-start;
+ position: static;
display: flex;
- flex-direction: column;
- user-select: text;
+ flex-direction: row;
+ align-items: flex-start;
+ background: var(--background-primary);
+ box-sizing: border-box;
+`;
- & > span {
- color: var(--text-secondary);
+const ContentColumn = styled.div`
+ padding: 60px 40px 80px;
+ flex: 1 1 auto;
+ max-width: 740px;
+ min-width: 460px;
+ min-height: 100%;
+ box-sizing: border-box;
+`;
+
+const Header = styled.div`
+ padding: 6px 10px;
+ color: var(--text-secondary);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ flex-shrink: 0;
+ font-size: 14px;
+ font-weight: var(--font-weight-bold);
+ letter-spacing: 0.5px;
+`;
+
+const Item = styled.div<{ selected?: boolean; textColor?: string }>`
+ padding: 5px 10px;
+ margin-bottom: 5px;
+ border-radius: 4px;
+ font-size: 16px;
+ cursor: pointer;
+ font-weight: var(--font-weight-regular);
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ overflow: hidden;
+ flex-shrink: 0;
+ color: ${(props) => props.textColor ?? "var(--text-secondary);"};
+
+ &:hover {
+ background-color: hsl(var(--background-primary-hsl) / 0.6);
+ cursor: pointer;
}
+
+ ${(props) =>
+ props.selected &&
+ css`
+ background-color: var(--background-primary);
+ color: var(--text);
+ `}
+`;
+
+const Divider = styled.div`
+ margin: 8px 10px;
+ height: 1px;
+ background-color: var(--text-disabled);
+`;
+
+const VersionInfo = styled.div`
+ padding: 8px 10px;
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: var(--font-weight-regular);
+`;
+
+const CloseContainer = styled.div`
+ margin-right: 20px;
+ flex: 0 0 36px;
+ width: 60px;
+ padding-top: 60px;
+ position: relative;
+`;
+
+const CloseContainerInner = styled.div`
+ position: fixed;
+`;
+
+const CloseContainerWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+`;
+
+const CloseButtonWrapper = styled.div`
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ flex: 0 0 40px;
+ border: solid 1px;
+ border-radius: 50%;
+ width: 40px;
+ height: 40px;
+ cursor: pointer;
+ color: var(--text-secondary);
`;
export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => {
const app = useAppStore();
+ const [index, setIndex] = useState(0);
+
+ const onClick = (e: React.MouseEvent) => {
+ const value = e.currentTarget.getAttribute("data-value");
+ if (value) {
+ setIndex(parseInt(value));
+ }
+ };
return (
-
-
-
- app.setFpsShown(e.target.checked)} />}
- label="Show FPS Graph"
- />
-
-
- {isTauri && (
-
- app.updaterStore?.setEnabled(e.target.checked)}
- />
- }
- label="Enabled auto updater"
- />
-
- )}
-
-
-
- Client Version:{" "}
-
- {GIT_REVISION.substring(0, 7)}
-
- {` `}
-
- ({GIT_BRANCH})
-
-
-
- {isTauri && (
- <>
- App Version: {window.globals.appVersion ?? "Fetching version information..."}
-
- Tauri Version: {window.globals.tauriVersion ?? "Fetching version information..."}
-
- Platform: {window.globals.platform.name}
- Arch: {window.globals.platform.arch}
- OS Version: {window.globals.platform.version}
- Locale: {window.globals.platform.locale ?? "Unknown"}
- >
- )}
-
-
-
-
-
-
+
+
+
+
+
+
+
+ -
+ Account
+
+
+ -
+ Developer Options
+
+ -
+ Experiments
+
+
+ -
+
+ Log Out
+
+
+
+
+
+
+ {GIT_BRANCH} {APP_VERSION} (
+
+ {GIT_REVISION.substring(0, 7)}
+
+ )
+
+ {isTauri && (
+ <>
+ {/*
+ {window.globals.appVersion
+ ? `${window.globals.appVersion} (${(
+
+ {GIT_REVISION.substring(0, 7)}
+
+ )})`
+ : "Fetching version information..."}
+ */}
+
+ Tauri {window.globals.tauriVersion ?? "Fetching version information..."}
+
+ {`${window.globals.platform.name} ${window.globals.platform.arch} (${window.globals.platform.version})`}
+ {window.globals.platform.locale ?? "Unknown"}
+ >
+ )}
+
+
+
+
+
+
+
+
+ {index === 0 && }
+ {index === 1 && }
+ {index === 2 && }
+
+
+
+
+ {
+ console.log("Close modal");
+ }}
+ >
+
+
+
+
+
+
+
);
});
diff --git a/src/components/modals/SettingsPages/AccountSettingsPage.tsx b/src/components/modals/SettingsPages/AccountSettingsPage.tsx
new file mode 100644
index 0000000..b6f022e
--- /dev/null
+++ b/src/components/modals/SettingsPages/AccountSettingsPage.tsx
@@ -0,0 +1,146 @@
+import { observer } from "mobx-react-lite";
+import { useState } from "react";
+import styled, { css } from "styled-components";
+import { useAppStore } from "../../../stores/AppStore";
+import SectionTitle from "../../SectionTitle";
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const UserInfoContainer = styled.div`
+ border-radius: 8px;
+ background-color: var(--background-secondary);
+ padding: 16px;
+`;
+
+const Field = styled.div<{ spacerTop?: boolean; spacerBottom?: boolean }>`
+ display: flex;
+ flex-direction: row;
+ justify-content: space-between;
+
+ ${(props) =>
+ props.spacerTop &&
+ css`
+ margin-top: 24px;
+ `}
+
+ ${(props) =>
+ props.spacerBottom &&
+ css`
+ margin-bottom: 24px;
+ `}
+`;
+
+const Row = styled.div`
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: hidden;
+ margin-right: 16px;
+`;
+
+const FieldTitle = styled.span`
+ margin-bottom: 4px;
+ color: var(--text-secondary);
+ font-size: 12px;
+ font-weight: var(--font-weight-medium);
+ letter-spacing: 0.5px;
+`;
+
+const FieldValue = styled.div`
+ overflow: hidden;
+ text-overflow: ellipsis;
+`;
+
+const FieldValueText = styled.span`
+ color: var(--text);
+ font-size: 16px;
+ font-weight: var(--font-weight-regular);
+`;
+
+const FieldValueToggle = styled.button`
+ color: var(--text-link);
+ cursor: pointer;
+ width: auto;
+ display: inline;
+ height: auto;
+ padding: 2px 4px;
+ position: relative;
+ background: none;
+ border: none;
+ border-radius: 4px;
+ font-size: 14px;
+ font-weight: var(--font-weight-medium);
+ user-select: none;
+ text-rendering: optimizeLegibility;
+`;
+
+function AccountSettingsPage() {
+ const app = useAppStore();
+ const [shouldRedactEmail, setShouldRedactEmail] = useState(true);
+
+ const redactEmail = (email: string) => {
+ const [username, domain] = email.split("@");
+ return `${"*".repeat(username.length)}@${domain}`;
+ };
+
+ const refactPhoneNumber = (phoneNumber: string) => {
+ const lastFour = phoneNumber.slice(-4);
+ return "*".repeat(phoneNumber.length - 4) + lastFour;
+ };
+
+ return (
+
+ Account
+
+
+
+
+ Username
+
+
+
+ {app.account?.username}#{app.account?.discriminator}
+
+
+
+
+
+
+
+ Email
+
+
+
+ {app.account?.email
+ ? shouldRedactEmail
+ ? redactEmail(app.account.email)
+ : app.account.email
+ : "No email added."}
+
+ setShouldRedactEmail(!shouldRedactEmail)}>
+ {shouldRedactEmail ? "Reveal" : "Hide"}
+
+
+
+
+
+
+
+
+ Phone Number
+
+
+ No phone number added.
+
+
+
+
+
+
+ );
+}
+
+export default observer(AccountSettingsPage);
diff --git a/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx b/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx
new file mode 100644
index 0000000..5710e82
--- /dev/null
+++ b/src/components/modals/SettingsPages/DeveloperSettingsPage.tsx
@@ -0,0 +1,19 @@
+import { observer } from "mobx-react-lite";
+import styled from "styled-components";
+import SectionTitle from "../../SectionTitle";
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+function DeveloperSettingsPage() {
+ return (
+
+ Developer Options
+
+
+ );
+}
+
+export default observer(DeveloperSettingsPage);
diff --git a/src/components/modals/SettingsPages/ExperimentsPage.tsx b/src/components/modals/SettingsPages/ExperimentsPage.tsx
new file mode 100644
index 0000000..af5554b
--- /dev/null
+++ b/src/components/modals/SettingsPages/ExperimentsPage.tsx
@@ -0,0 +1,118 @@
+import { observer } from "mobx-react-lite";
+import { useState } from "react";
+import styled from "styled-components";
+import { useAppStore } from "../../../stores/AppStore";
+import { EXPERIMENT_LIST, Experiment as ExperimentType } from "../../../stores/ExperimentsStore";
+import SectionTitle from "../../SectionTitle";
+
+const Content = styled.div`
+ display: flex;
+ flex-direction: column;
+`;
+
+const ExperimentList = styled.ul`
+ display: grid;
+ list-style: none;
+ padding: 0;
+ margin: 0;
+ gap: 10px;
+`;
+
+const Experiment = styled.li`
+ display: flex;
+ flex-direction: column;
+`;
+
+const Title = styled.span`
+ font-size: 16px;
+ font-weight: var(--font-weight-medium);
+ color: var(--text);
+`;
+
+const Subtitle = styled.div`
+ color: var(--text-disabled);
+ font-size: 14px;
+ font-weight: var(--font-weight-regular);
+`;
+
+const OverrideText = styled.div`
+ color: var(--text.muted);
+ margin-bottom: 10px;
+ font-size: 12px;
+ font-weight: var(--font-weight-bold);
+`;
+
+const Select = styled.select`
+ appearance: none;
+ /* safari */
+ -webkit-appearance: none;
+ background-color: var(--background-tertiary);
+ border-color: var(--background-tertiary);
+ color: var(--text);
+ font-weight: var(--font-weight-medium);
+ border: 1px solid transparent;
+ padding: 8px 8px 8px 12px;
+ cursor: pointer;
+ display: grid;
+ grid-template-columns: 1fr auto;
+ align-items: center;
+ border-radius: 4px;
+`;
+
+function ExperimentItem({ experiment }: { experiment: ExperimentType }) {
+ const app = useAppStore();
+ const isActive = app.experiments.isExperimentEnabled(experiment.id);
+ const activeTreatment = app.experiments.getTreatment(experiment.id);
+ const [isExpanded, setExpanded] = useState(isActive);
+
+ const toggle = () => setExpanded(!isExpanded);
+
+ const onChange = (e: React.ChangeEvent) => {
+ const value = Number.parseInt(e.target.value);
+ app.experiments.setTreatment(experiment.id, value);
+ };
+
+ return (
+
+
+
{experiment.name}
+ {experiment.description}
+
+ {isExpanded && (
+
+ Treatment Override
+
+
+ )}
+
+ );
+}
+
+function ExperimentsPage() {
+ const app = useAppStore();
+
+ return (
+
+ Experiments
+
+
+ {EXPERIMENT_LIST.map((experiment) => (
+
+ ))}
+
+
+
+ );
+}
+
+export default observer(ExperimentsPage);
diff --git a/src/stores/ExperimentsStore.ts b/src/stores/ExperimentsStore.ts
index 10a8f68..c789c04 100644
--- a/src/stores/ExperimentsStore.ts
+++ b/src/stores/ExperimentsStore.ts
@@ -1,6 +1,6 @@
import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
-export type ExperimentType = "test" | "message_queue";
+export type ExperimentType = "test" | "message_queue" | "presence_rings";
export interface ExperimentTreatment {
id: number;
@@ -56,6 +56,27 @@ export const EXPERIMENT_LIST: Experiment[] = [
},
],
},
+ {
+ id: "presence_rings",
+ name: "Presence Rings",
+ description: "Use rings for presence status instead of dots",
+ treatments: [
+ {
+ id: 0,
+ name: "Control",
+ },
+ {
+ id: 1,
+ name: "Treatment 1",
+ description: "Use presence dots",
+ },
+ {
+ id: 2,
+ name: "Treatment 2",
+ description: "Use presence rings",
+ },
+ ],
+ },
];
export interface Data {
@@ -76,7 +97,7 @@ export default class ExperimentsStore {
}
@computed
- getTreatment(id: ExperimentType) {
+ getTreatment(id: ExperimentType): ExperimentTreatment | undefined {
const treatment = this.experiments.get(id);
const experiment = EXPERIMENT_LIST.find((x) => x.id === id);
return experiment?.treatments.find((x) => x.id === treatment);