1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-21 09:52:31 +01:00

start implementing proper settings modal

This commit is contained in:
Puyodead1 2024-06-03 11:53:43 -04:00
parent 34db3f9561
commit 1750b22af7
No known key found for this signature in database
GPG Key ID: A4FA4FEC0DD353FC
8 changed files with 588 additions and 97 deletions

View File

@ -1,4 +1,4 @@
<!doctype html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- Primary Meta Tags -->

View File

@ -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<Props>) {
return (
<Container>
<Text>{children}</Text>
</Container>
);
}
export default SectionTitle;

View File

@ -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<ModalProps, "transparent" | "maxWidth" | "maxHeight"> & { actions: boolean }
Pick<ModalProps, "transparent" | "maxWidth" | "maxHeight"> & { 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<Pick<ModalProps, "transparent" |
overflow-y: auto;
font-size: 0.9375rem;
${(props) =>
!props.transparent &&
css`
background: var(--background-primary);
`}
`;
const Actions = styled.div`

View File

@ -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<HTMLDivElement>) => {
const value = e.currentTarget.getAttribute("data-value");
if (value) {
setIndex(parseInt(value));
}
};
return (
<Modal {...props}>
<Wrapper>
<FormGroup>
<FormControlLabel
control={<Switch checked={app.fpsShown} onChange={(e) => app.setFpsShown(e.target.checked)} />}
label="Show FPS Graph"
/>
</FormGroup>
{isTauri && (
<FormGroup>
<FormControlLabel
control={
<Switch
checked={app.updaterStore?.enabled}
onChange={(e) => app.updaterStore?.setEnabled(e.target.checked)}
/>
}
label="Enabled auto updater"
/>
</FormGroup>
)}
<VersionWrapper>
<span>
Client Version:{" "}
<Link href={`${REPO_URL}/commit/${GIT_REVISION}`} target="_blank" rel="noreferrer">
{GIT_REVISION.substring(0, 7)}
</Link>
{` `}
<Link
href={GIT_BRANCH !== "DETACHED" ? `${REPO_URL}/tree/${GIT_BRANCH}` : undefined}
target="_blank"
rel="noreferrer"
>
({GIT_BRANCH})
</Link>
</span>
{isTauri && (
<>
<span>App Version: {window.globals.appVersion ?? "Fetching version information..."}</span>
<span>
Tauri Version: {window.globals.tauriVersion ?? "Fetching version information..."}
</span>
<span>Platform: {window.globals.platform.name}</span>
<span>Arch: {window.globals.platform.arch}</span>
<span>OS Version: {window.globals.platform.version}</span>
<span>Locale: {window.globals.platform.locale ?? "Unknown"}</span>
</>
)}
</VersionWrapper>
<ActionWrapper>
<Button
palette="danger"
onClick={() => {
app.logout();
}}
>
Logout
</Button>
</ActionWrapper>
</Wrapper>
<Modal {...props} fullScreen withoutCloseButton withEmptyActionBar padding="0">
<SidebarView>
<Sidebar>
<SidebarInner>
<SidebarNav>
<SidebarNavWrapper>
<Header>User Settings</Header>
<Item data-value="0" onClick={onClick}>
Account
</Item>
<Divider />
<Item data-value="1" onClick={onClick}>
Developer Options
</Item>
<Item data-value="2" onClick={onClick}>
Experiments
</Item>
<Divider />
<Item onClick={app.logout}>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
color: "var(--error)",
}}
>
Log Out
<Icon icon="mdiLogout" size="16px" color="var(--error)" />
</div>
</Item>
<Divider />
<VersionInfo>
<span>
{GIT_BRANCH} {APP_VERSION} (
<Link
href={`${REPO_URL}/commit/${GIT_REVISION}`}
target="_blank"
rel="noreferrer"
>
{GIT_REVISION.substring(0, 7)}
</Link>
)
</span>
{isTauri && (
<>
{/* <span>
{window.globals.appVersion
? `${window.globals.appVersion} (${(
<Link
href={`${REPO_URL}/commit/${GIT_REVISION}`}
target="_blank"
rel="noreferrer"
>
{GIT_REVISION.substring(0, 7)}
</Link>
)})`
: "Fetching version information..."}
</span> */}
<span>
Tauri {window.globals.tauriVersion ?? "Fetching version information..."}
</span>
<span>{`${window.globals.platform.name} ${window.globals.platform.arch} (${window.globals.platform.version})`}</span>
<span>{window.globals.platform.locale ?? "Unknown"}</span>
</>
)}
</VersionInfo>
</SidebarNavWrapper>
</SidebarNav>
</SidebarInner>
</Sidebar>
<Content>
<ContentInner>
<ContentColumn>
{index === 0 && <AccountSettingsPage />}
{index === 1 && <DeveloperSettingsPage />}
{index === 2 && <ExperimentsPage />}
</ContentColumn>
<CloseContainer>
<CloseContainerInner></CloseContainerInner>
<CloseContainerWrapper>
<CloseButtonWrapper
onClick={() => {
console.log("Close modal");
}}
>
<Icon icon="mdiClose" size="18px" />
</CloseButtonWrapper>
</CloseContainerWrapper>
</CloseContainer>
</ContentInner>
</Content>
</SidebarView>
</Modal>
);
});

View File

@ -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 (
<div>
<SectionTitle>Account</SectionTitle>
<Content>
<UserInfoContainer>
<Field spacerBottom>
<Row>
<FieldTitle>Username</FieldTitle>
<FieldValue>
<FieldValueText>
{app.account?.username}#{app.account?.discriminator}
</FieldValueText>
</FieldValue>
</Row>
</Field>
<Field>
<Row>
<FieldTitle>Email</FieldTitle>
<FieldValue>
<FieldValueText>
{app.account?.email
? shouldRedactEmail
? redactEmail(app.account.email)
: app.account.email
: "No email added."}
<FieldValueToggle onClick={() => setShouldRedactEmail(!shouldRedactEmail)}>
{shouldRedactEmail ? "Reveal" : "Hide"}
</FieldValueToggle>
</FieldValueText>
</FieldValue>
</Row>
</Field>
<Field spacerTop>
<Row>
<FieldTitle>Phone Number</FieldTitle>
<FieldValue>
<FieldValueText>No phone number added.</FieldValueText>
</FieldValue>
</Row>
</Field>
</UserInfoContainer>
</Content>
</div>
);
}
export default observer(AccountSettingsPage);

View File

@ -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 (
<div>
<SectionTitle>Developer Options</SectionTitle>
<Content></Content>
</div>
);
}
export default observer(DeveloperSettingsPage);

View File

@ -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<HTMLSelectElement>) => {
const value = Number.parseInt(e.target.value);
app.experiments.setTreatment(experiment.id, value);
};
return (
<Experiment key={experiment.id}>
<div style={{ marginBottom: "10px", cursor: "pointer" }} onClick={toggle}>
<Title>{experiment.name}</Title>
<Subtitle>{experiment.description}</Subtitle>
</div>
{isExpanded && (
<div style={{ display: "flex", flexDirection: "column" }}>
<OverrideText>Treatment Override</OverrideText>
<Select onChange={onChange}>
{experiment.treatments.map((treatment) => (
<option
key={treatment.id}
value={treatment.id}
selected={(!isActive && treatment.id === 0) || activeTreatment?.id === treatment.id}
>
{`${treatment.name}${treatment.description ? ": " + treatment.description : ""}`}
</option>
))}
</Select>
</div>
)}
</Experiment>
);
}
function ExperimentsPage() {
const app = useAppStore();
return (
<div>
<SectionTitle>Experiments</SectionTitle>
<Content>
<ExperimentList>
{EXPERIMENT_LIST.map((experiment) => (
<ExperimentItem experiment={experiment} />
))}
</ExperimentList>
</Content>
</div>
);
}
export default observer(ExperimentsPage);

View File

@ -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);