hyperspace-desktop-client-w.../src/pages/Settings.tsx

743 lines
30 KiB
TypeScript

import React, { Component } from "react";
import {
List,
ListItem,
ListItemAvatar,
ListItemText,
ListSubheader,
ListItemSecondaryAction,
Paper,
IconButton,
withStyles,
Button,
Switch,
Dialog,
DialogTitle,
DialogContent,
RadioGroup,
FormControlLabel,
Radio,
DialogActions,
DialogContentText,
Grid,
Theme,
Typography,
Avatar,
Toolbar,
Tooltip
} from "@material-ui/core";
import { styles } from "./PageLayout.styles";
import {
setUserDefaultBool,
getUserDefaultBool,
getUserDefaultTheme,
setUserDefaultTheme,
getUserDefaultVisibility,
setUserDefaultVisibility,
getConfig
} from "../utilities/settings";
import {
canSendNotifications,
browserSupportsNotificationRequests
} from "../utilities/notifications";
import { themes, defaultTheme } from "../types/HyperspaceTheme";
import ThemePreview from "../components/ThemePreview";
import {
setHyperspaceTheme,
getHyperspaceTheme,
getDarkModeFromSystem
} from "../utilities/themes";
import { Visibility } from "../types/Visibility";
import { LinkableButton, LinkableIconButton } from "../interfaces/overrides";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import DevicesIcon from "@material-ui/icons/Devices";
import Brightness3Icon from "@material-ui/icons/Brightness3";
import PaletteIcon from "@material-ui/icons/Palette";
import AccountEditIcon from "mdi-material-ui/AccountEdit";
import MastodonIcon from "mdi-material-ui/Mastodon";
import VisibilityIcon from "@material-ui/icons/Visibility";
import NotificationsIcon from "@material-ui/icons/Notifications";
import BellAlertIcon from "mdi-material-ui/BellAlert";
import RefreshIcon from "@material-ui/icons/Refresh";
import UndoIcon from "@material-ui/icons/Undo";
import DomainDisabledIcon from "@material-ui/icons/DomainDisabled";
import { Config } from "../types/Config";
import { Account } from "../types/Account";
import Mastodon from "megalodon";
import { isDarwinApp } from "../utilities/desktop";
interface ISettingsState {
darkModeEnabled: boolean;
systemDecidesDarkMode: boolean;
pushNotificationsEnabled: boolean;
badgeDisplaysAllNotifs: boolean;
selectThemeName: string;
themeDialogOpen: boolean;
visibilityDialogOpen: boolean;
resetHyperspaceDialog: boolean;
resetSettingsDialog: boolean;
previewTheme: Theme;
defaultVisibility: Visibility;
brandName: string;
federated: boolean;
currentUser?: Account;
}
class SettingsPage extends Component<any, ISettingsState> {
client: Mastodon;
constructor(props: any) {
super(props);
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.state = {
darkModeEnabled: getUserDefaultBool("darkModeEnabled"),
systemDecidesDarkMode: getUserDefaultBool("systemDecidesDarkMode"),
pushNotificationsEnabled: canSendNotifications(),
badgeDisplaysAllNotifs: getUserDefaultBool(
"displayAllOnNotificationBadge"
),
selectThemeName: getUserDefaultTheme().key,
themeDialogOpen: false,
visibilityDialogOpen: false,
resetHyperspaceDialog: false,
resetSettingsDialog: false,
previewTheme:
setHyperspaceTheme(getUserDefaultTheme()) ||
setHyperspaceTheme(defaultTheme),
defaultVisibility: getUserDefaultVisibility() || "public",
brandName: "Hyperspace",
federated: true
};
this.toggleDarkMode = this.toggleDarkMode.bind(this);
this.toggleSystemDarkMode = this.toggleSystemDarkMode.bind(this);
this.togglePushNotifications = this.togglePushNotifications.bind(this);
this.toggleBadgeCount = this.toggleBadgeCount.bind(this);
this.toggleThemeDialog = this.toggleThemeDialog.bind(this);
this.toggleVisibilityDialog = this.toggleVisibilityDialog.bind(this);
this.changeThemeName = this.changeThemeName.bind(this);
this.changeTheme = this.changeTheme.bind(this);
this.setVisibility = this.setVisibility.bind(this);
}
componentDidMount() {
getConfig()
.then((config: any) => {
this.setState({
brandName: config.branding.name
});
})
.catch((err: Error) => {
console.error(err.message);
});
this.getFederatedStatus();
this.client
.get("/accounts/verify_credentials")
.then((resp: any) => {
let data: Account = resp.data;
this.setState({ currentUser: data });
})
.catch((err: Error) => {
let acct = localStorage.getItem("account");
if (acct) {
this.setState({ currentUser: JSON.parse(acct) });
} else {
this.props.enqueueSnackbar(
"Couldn't find profile info: " + err.name
);
console.error(err.message);
}
});
}
getFederatedStatus() {
getConfig().then((result: any) => {
if (result !== undefined) {
let config: Config = result;
console.log(!config.federation.allowPublicPosts);
this.setState({
federated: config.federation.allowPublicPosts
});
}
});
}
toggleDarkMode() {
this.setState({ darkModeEnabled: !this.state.darkModeEnabled });
setUserDefaultBool("darkModeEnabled", !this.state.darkModeEnabled);
window.location.reload();
}
toggleSystemDarkMode() {
this.setState({
systemDecidesDarkMode: !this.state.systemDecidesDarkMode
});
setUserDefaultBool(
"systemDecidesDarkMode",
!this.state.systemDecidesDarkMode
);
window.location.reload();
}
togglePushNotifications() {
this.setState({
pushNotificationsEnabled: !this.state.pushNotificationsEnabled
});
setUserDefaultBool(
"enablePushNotifications",
!this.state.pushNotificationsEnabled
);
}
toggleBadgeCount() {
this.setState({
badgeDisplaysAllNotifs: !this.state.badgeDisplaysAllNotifs
});
setUserDefaultBool(
"displayAllOnNotificationBadge",
!this.state.badgeDisplaysAllNotifs
);
}
toggleThemeDialog() {
this.setState({ themeDialogOpen: !this.state.themeDialogOpen });
}
toggleVisibilityDialog() {
this.setState({
visibilityDialogOpen: !this.state.visibilityDialogOpen
});
}
toggleResetDialog() {
this.setState({
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
});
}
toggleResetSettingsDialog() {
this.setState({ resetSettingsDialog: !this.state.resetSettingsDialog });
}
changeTheme() {
setUserDefaultTheme(this.state.selectThemeName);
window.location.reload();
}
changeThemeName(theme: string) {
let previewTheme = setHyperspaceTheme(getHyperspaceTheme(theme));
this.setState({ selectThemeName: theme, previewTheme });
}
changeVisibility(to: Visibility) {
this.setState({ defaultVisibility: to });
}
setVisibility() {
setUserDefaultVisibility(this.state.defaultVisibility);
this.toggleVisibilityDialog();
}
reset() {
localStorage.clear();
window.location.reload();
}
refresh() {
let settings = [
"darkModeEnabled",
"enablePushNotifications",
"clearNotificationsOnRead",
"theme",
"displayAllOnNotificationBadge",
"defaultVisibility"
];
settings.forEach(setting => {
localStorage.removeItem(setting);
});
window.location.reload();
}
showThemeDialog() {
const { classes } = this.props;
return (
<Dialog
open={this.state.themeDialogOpen}
disableBackdropClick
disableEscapeKeyDown
maxWidth="md"
fullWidth={true}
aria-labelledby="confirmation-dialog-title"
>
<DialogTitle id="confirmation-dialog-title">
Choose a theme
</DialogTitle>
<DialogContent>
<Grid container spacing={16}>
<Grid item xs={12} md={6}>
<RadioGroup
aria-label="Theme"
name="colorScheme"
value={this.state.selectThemeName}
onChange={(e, value) =>
this.changeThemeName(value)
}
>
{themes.map(theme => (
<FormControlLabel
value={theme.key}
key={theme.key}
control={<Radio />}
label={theme.name}
/>
))}
))}
</RadioGroup>
</Grid>
<Grid
item
xs={12}
md={6}
className={classes.desktopOnly}
>
<Typography variant="h6" component="p">
Theme preview
</Typography>
<ThemePreview theme={this.state.previewTheme} />
</Grid>
</Grid>
</DialogContent>
<DialogActions>
<Button onClick={this.toggleThemeDialog} color="default">
Cancel
</Button>
<Button onClick={this.changeTheme} color="secondary">
Set theme
</Button>
</DialogActions>
</Dialog>
);
}
showVisibilityDialog() {
return (
<Dialog
open={this.state.visibilityDialogOpen}
disableBackdropClick
disableEscapeKeyDown
maxWidth="xs"
fullWidth={true}
aria-labelledby="confirmation-dialog-title"
>
<DialogTitle id="confirmation-dialog-title">
Set your default visibility
</DialogTitle>
<DialogContent>
<RadioGroup
aria-label="Visibility"
name="visibility"
value={this.state.defaultVisibility}
onChange={(e, value) =>
this.changeVisibility(value as Visibility)
}
>
<FormControlLabel
value={"public"}
key={"public"}
control={<Radio />}
label={`Public ${
this.state.federated
? ""
: "(disabled by provider)"
}`}
disabled={!this.state.federated}
/>
<FormControlLabel
value={"unlisted"}
key={"unlisted"}
control={<Radio />}
label={"Unlisted"}
/>
<FormControlLabel
value={"private"}
key={"private"}
control={<Radio />}
label={"Private (followers only)"}
/>
<FormControlLabel
value={"direct"}
key={"direct"}
control={<Radio />}
label={"Direct"}
/>
</RadioGroup>
</DialogContent>
<DialogActions>
<Button
onClick={this.toggleVisibilityDialog}
color="default"
>
Cancel
</Button>
<Button onClick={this.setVisibility} color="secondary">
Set default
</Button>
</DialogActions>
</Dialog>
);
}
showResetSettingsDialog() {
return (
<Dialog
open={this.state.resetSettingsDialog}
onClose={() => this.toggleResetSettingsDialog()}
>
<DialogTitle id="alert-dialog-title">
Are you sure you want to refresh settings?
</DialogTitle>
<DialogActions>
<Button
onClick={() => this.toggleResetSettingsDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.refresh();
}}
color="primary"
>
Refresh
</Button>
</DialogActions>
</Dialog>
);
}
showResetDialog() {
return (
<Dialog
open={this.state.resetHyperspaceDialog}
onClose={() => this.toggleResetDialog()}
>
<DialogTitle id="alert-dialog-title">
Reset {this.state.brandName}?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to reset {this.state.brandName}?
You'll need to re-authorize {this.state.brandName}{" "}
access again.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.toggleResetDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.reset();
}}
color="primary"
>
Reset
</Button>
</DialogActions>
</Dialog>
);
}
render() {
const { classes } = this.props;
return (
<div>
<div className={classes.pageLayoutMinimalConstraints}>
{this.state.currentUser ? (
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentUser.header_static}")`
}}
/>
<div className={classes.profileContent}>
<br />
<Avatar
className={classes.profileAvatar}
src={this.state.currentUser.avatar_static}
/>
<div
className={classes.profileUserBox}
style={{ margin: "auto" }}
>
<Typography
variant="h4"
color="inherit"
component="h1"
>
{this.state.currentUser.display_name ||
this.state.currentUser.username}
</Typography>
<Typography
color="inherit"
variant="h6"
component="p"
>
@{this.state.currentUser.acct}
</Typography>
</div>
<div className={classes.pageGrow} />
<Toolbar>
<Tooltip title="Edit Profile">
<LinkableIconButton
to={"/you"}
color="inherit"
>
<AccountEditIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Manage blocked servers">
<LinkableIconButton
to={"/blocked"}
color="inherit"
>
<DomainDisabledIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Configure on Mastodon">
<IconButton
href={
(localStorage.getItem(
"baseurl"
) as string) +
"/settings/preferences"
}
target="_blank"
rel="noreferrer"
color="inherit"
>
<MastodonIcon />
</IconButton>
</Tooltip>
</Toolbar>
</div>
</div>
) : null}
<div className={classes.pageContentLayoutConstraints}>
<ListSubheader>Appearance</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<DevicesIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Match system appearance"
secondary="Obey light/dark theme from your system"
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state.systemDecidesDarkMode
}
onChange={this.toggleSystemDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
{!isDarwinApp() ||
(isDarwinApp() &&
!this.state.systemDecidesDarkMode) ? (
<ListItem>
<ListItemAvatar>
<Brightness3Icon color="action" />
</ListItemAvatar>
<ListItemText
primary="Dark mode"
secondary="Toggles light or dark theme"
/>
<ListItemSecondaryAction>
<Switch
disabled={
this.state
.systemDecidesDarkMode
}
checked={
this.state.darkModeEnabled
}
onChange={this.toggleDarkMode}
/>
</ListItemSecondaryAction>
</ListItem>
) : null}
<ListItem>
<ListItemAvatar>
<PaletteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Interface theme"
secondary="The color palette used for the interface"
/>
<ListItemSecondaryAction>
<Button
onClick={this.toggleThemeDialog}
>
Set theme
</Button>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Composer</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<VisibilityIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Default visibility"
secondary="New posts in composer will use this visiblity"
/>
<ListItemSecondaryAction>
<Button
onClick={
this.toggleVisibilityDialog
}
>
Change
</Button>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Notifications</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<NotificationsIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable push notifications"
secondary={
getUserDefaultBool(
"userDeniedNotification"
)
? "Check your browser's notification permissions."
: browserSupportsNotificationRequests()
? "Send a push notification when not focused."
: "Notifications aren't supported."
}
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state
.pushNotificationsEnabled
}
onChange={
this.togglePushNotifications
}
disabled={
!browserSupportsNotificationRequests() ||
getUserDefaultBool(
"userDeniedNotification"
)
}
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<BellAlertIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Notification badge counts all notifications"
secondary={
"Counts all notifications, read or unread."
}
/>
<ListItemSecondaryAction>
<Switch
checked={
this.state
.badgeDisplaysAllNotifs
}
onChange={this.toggleBadgeCount}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />
<ListSubheader>Advanced</ListSubheader>
<Paper className={classes.pageListConstraints}>
<List>
<ListItem>
<ListItemAvatar>
<RefreshIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Refresh settings"
secondary="Reset the settings to defaults."
/>
<ListItemSecondaryAction>
<Button
onClick={() =>
this.toggleResetSettingsDialog()
}
>
Refresh
</Button>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<UndoIcon color="action" />
</ListItemAvatar>
<ListItemText
primary={`Reset ${this.state.brandName}`}
secondary="Deletes all data and resets the app"
/>
<ListItemSecondaryAction>
<Button
onClick={() =>
this.toggleResetDialog()
}
>
Reset
</Button>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
{this.showThemeDialog()}
{this.showVisibilityDialog()}
{this.showResetDialog()}
{this.showResetSettingsDialog()}
</div>
</div>
</div>
);
}
}
export default withStyles(styles)(SettingsPage);