1
0
mirror of https://github.com/spacebarchat/client.git synced 2024-11-22 02:12:38 +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"> <html lang="en">
<head> <head>
<!-- Primary Meta Tags --> <!-- 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; disabled?: boolean;
withEmptyActionBar?: boolean; withEmptyActionBar?: boolean;
withoutCloseButton?: boolean; withoutCloseButton?: boolean;
fullScreen?: boolean;
} }
/** /**
@ -48,7 +49,7 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
animation-fill-mode: forwards; animation-fill-mode: forwards;
display: grid; display: grid;
overflow-y: auto; overflow: hidden;
place-items: center; place-items: center;
color: var(--text); color: var(--text);
@ -72,12 +73,8 @@ export const ModalBase = styled.div<{ closing?: boolean }>`
* Wrapper for modal content, handles the sizing and positioning * Wrapper for modal content, handles the sizing and positioning
*/ */
export const ModalWrapper = styled.div< 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; margin: 20px;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -86,17 +83,28 @@ export const ModalWrapper = styled.div<
animation-duration: 0.25s; animation-duration: 0.25s;
animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1); animation-timing-function: cubic-bezier(0.3, 0.3, 0.18, 1.1);
overflow: hidden;
background: var(--background-tertiary);
${(props) => ${(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` css`
width: 100%; width: 100%;
height: 100%;
`} `}
${(props) => ${(props) =>
!props.transparent && !props.transparent &&
!props.fullScreen &&
css` css`
overflow: hidden;
background: var(--background-primary);
border-radius: 8px; border-radius: 8px;
`} `}
`; `;
@ -122,12 +130,6 @@ export const ModalContentContainer = styled.div<Pick<ModalProps, "transparent" |
overflow-y: auto; overflow-y: auto;
font-size: 0.9375rem; font-size: 0.9375rem;
${(props) =>
!props.transparent &&
css`
background: var(--background-primary);
`}
`; `;
const Actions = styled.div` const Actions = styled.div`

View File

@ -1,105 +1,264 @@
import { FormControlLabel, FormGroup, Switch } from "@mui/material";
import { observer } from "mobx-react-lite"; 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 { ModalProps } from "../../controllers/modals";
import { useAppStore } from "../../stores/AppStore"; import { useAppStore } from "../../stores/AppStore";
import { isTauri } from "../../utils/Utils"; import { isTauri } from "../../utils/Utils";
import { GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison"; import { APP_VERSION, GIT_BRANCH, GIT_REVISION, REPO_URL } from "../../utils/revison";
import Button from "../Button"; import Icon from "../Icon";
import Link from "../Link"; import Link from "../Link";
import { Modal } from "./ModalComponents"; import { Modal } from "./ModalComponents";
import AccountSettingsPage from "./SettingsPages/AccountSettingsPage";
import DeveloperSettingsPage from "./SettingsPages/DeveloperSettingsPage";
import ExperimentsPage from "./SettingsPages/ExperimentsPage";
const Wrapper = styled.div` const SidebarView = styled.div`
padding: 16px 0; display: flex;
gap: 8px; 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; display: flex;
flex-direction: column; flex-direction: column;
`; `;
const ActionWrapper = styled.div` const Content = styled.div`
margin-top: 20px;
gap: 8px;
display: flex; 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; display: flex;
flex-direction: column; flex-direction: row;
user-select: text; align-items: flex-start;
background: var(--background-primary);
box-sizing: border-box;
`;
& > span { 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); 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">) => { export const SettingsModal = observer(({ ...props }: ModalProps<"settings">) => {
const app = useAppStore(); 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 ( return (
<Modal {...props}> <Modal {...props} fullScreen withoutCloseButton withEmptyActionBar padding="0">
<Wrapper> <SidebarView>
<FormGroup> <Sidebar>
<FormControlLabel <SidebarInner>
control={<Switch checked={app.fpsShown} onChange={(e) => app.setFpsShown(e.target.checked)} />} <SidebarNav>
label="Show FPS Graph" <SidebarNavWrapper>
/> <Header>User Settings</Header>
</FormGroup> <Item data-value="0" onClick={onClick}>
Account
{isTauri && ( </Item>
<FormGroup> <Divider />
<FormControlLabel <Item data-value="1" onClick={onClick}>
control={ Developer Options
<Switch </Item>
checked={app.updaterStore?.enabled} <Item data-value="2" onClick={onClick}>
onChange={(e) => app.updaterStore?.setEnabled(e.target.checked)} Experiments
/> </Item>
} <Divider />
label="Enabled auto updater" <Item onClick={app.logout}>
/> <div
</FormGroup> style={{
)} display: "flex",
justifyContent: "space-between",
<VersionWrapper> alignItems: "center",
color: "var(--error)",
}}
>
Log Out
<Icon icon="mdiLogout" size="16px" color="var(--error)" />
</div>
</Item>
<Divider />
<VersionInfo>
<span> <span>
Client Version:{" "} {GIT_BRANCH} {APP_VERSION} (
<Link href={`${REPO_URL}/commit/${GIT_REVISION}`} target="_blank" rel="noreferrer">
{GIT_REVISION.substring(0, 7)}
</Link>
{` `}
<Link <Link
href={GIT_BRANCH !== "DETACHED" ? `${REPO_URL}/tree/${GIT_BRANCH}` : undefined} href={`${REPO_URL}/commit/${GIT_REVISION}`}
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
({GIT_BRANCH}) {GIT_REVISION.substring(0, 7)}
</Link> </Link>
)
</span> </span>
{isTauri && ( {isTauri && (
<> <>
<span>App Version: {window.globals.appVersion ?? "Fetching version information..."}</span> {/* <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> <span>
Tauri Version: {window.globals.tauriVersion ?? "Fetching version information..."} Tauri {window.globals.tauriVersion ?? "Fetching version information..."}
</span> </span>
<span>Platform: {window.globals.platform.name}</span> <span>{`${window.globals.platform.name} ${window.globals.platform.arch} (${window.globals.platform.version})`}</span>
<span>Arch: {window.globals.platform.arch}</span> <span>{window.globals.platform.locale ?? "Unknown"}</span>
<span>OS Version: {window.globals.platform.version}</span>
<span>Locale: {window.globals.platform.locale ?? "Unknown"}</span>
</> </>
)} )}
</VersionWrapper> </VersionInfo>
</SidebarNavWrapper>
<ActionWrapper> </SidebarNav>
<Button </SidebarInner>
palette="danger" </Sidebar>
<Content>
<ContentInner>
<ContentColumn>
{index === 0 && <AccountSettingsPage />}
{index === 1 && <DeveloperSettingsPage />}
{index === 2 && <ExperimentsPage />}
</ContentColumn>
<CloseContainer>
<CloseContainerInner></CloseContainerInner>
<CloseContainerWrapper>
<CloseButtonWrapper
onClick={() => { onClick={() => {
app.logout(); console.log("Close modal");
}} }}
> >
Logout <Icon icon="mdiClose" size="18px" />
</Button> </CloseButtonWrapper>
</ActionWrapper> </CloseContainerWrapper>
</Wrapper> </CloseContainer>
</ContentInner>
</Content>
</SidebarView>
</Modal> </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"; import { action, computed, makeAutoObservable, ObservableMap } from "mobx";
export type ExperimentType = "test" | "message_queue"; export type ExperimentType = "test" | "message_queue" | "presence_rings";
export interface ExperimentTreatment { export interface ExperimentTreatment {
id: number; 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 { export interface Data {
@ -76,7 +97,7 @@ export default class ExperimentsStore {
} }
@computed @computed
getTreatment(id: ExperimentType) { getTreatment(id: ExperimentType): ExperimentTreatment | undefined {
const treatment = this.experiments.get(id); const treatment = this.experiments.get(id);
const experiment = EXPERIMENT_LIST.find((x) => x.id === id); const experiment = EXPERIMENT_LIST.find((x) => x.id === id);
return experiment?.treatments.find((x) => x.id === treatment); return experiment?.treatments.find((x) => x.id === treatment);