hyperspace-desktop-client-w.../src/components/AppLayout/AppLayout.tsx

416 lines
18 KiB
TypeScript

import React, { Component } from 'react';
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, ListItem, Tooltip } from '@material-ui/core';
import MenuIcon from '@material-ui/icons/Menu';
import SearchIcon from '@material-ui/icons/Search';
import NotificationsIcon from '@material-ui/icons/Notifications';
import MailIcon from '@material-ui/icons/Mail';
import HomeIcon from '@material-ui/icons/Home';
import DomainIcon from '@material-ui/icons/Domain';
import PublicIcon from '@material-ui/icons/Public';
import GroupIcon from '@material-ui/icons/Group';
import SettingsIcon from '@material-ui/icons/Settings';
import InfoIcon from '@material-ui/icons/Info';
import CreateIcon from '@material-ui/icons/Create';
//import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import {styles} from './AppLayout.styles';
import { UAccount } from '../../types/Account';
import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interfaces/overrides';
import Mastodon from 'megalodon';
import { Notification } from '../../types/Notification';
import {sendNotificationRequest} from '../../utilities/notifications';
import {withSnackbar} from 'notistack';
import { getConfig, getUserDefaultBool } from '../../utilities/settings';
import { isDesktopApp, isDarwinApp } from '../../utilities/desktop';
import { Config } from '../../types/Config';
interface IAppLayoutState {
acctMenuOpen: boolean;
drawerOpenOnMobile: boolean;
currentUser?: UAccount;
notificationCount: number;
logOutOpen: boolean;
enableFederation?: boolean;
brandName?: string;
developerMode?: boolean;
}
export class AppLayout extends Component<any, IAppLayoutState> {
client: Mastodon;
streamListener: any;
constructor(props: any) {
super(props);
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
this.state = {
drawerOpenOnMobile: false,
acctMenuOpen: false,
notificationCount: 0,
logOutOpen: false
}
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
this.clearBadge = this.clearBadge.bind(this);
}
componentDidMount() {
let acct = localStorage.getItem('account');
if (acct) {
this.setState({ currentUser: JSON.parse(acct) });
} else {
this.client.get('/accounts/verify_credentials').then((resp: any) => {
let data: UAccount = resp.data;
this.setState({ currentUser: data });
}).catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't find profile info: " + err.name);
console.error(err.message);
})
}
getConfig().then((result: any) => {
if (result !== undefined) {
let config: Config = result;
this.setState({
enableFederation: config.federation.enablePublicTimeline,
brandName: config.branding? config.branding.name: "Hyperspace",
developerMode: config.developer
});
}
})
this.streamNotifications()
}
streamNotifications() {
this.streamListener = this.client.stream('/streaming/user');
if (getUserDefaultBool('displayAllOnNotificationBadge')) {
this.client.get('/notifications').then((resp: any) => {
let notifArray = resp.data;
this.setState({ notificationCount: notifArray.length });
})
}
this.streamListener.on('notification', (notif: Notification) => {
const notificationCount = this.state.notificationCount + 1;
this.setState({ notificationCount });
if (!document.hasFocus()) {
let primaryMessage = "";
let secondaryMessage = "";
switch(notif.type) {
case "favourite":
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " favorited your post.";
if (notif.status) {
const div = document.createElement('div');
div.innerHTML = notif.status.content;
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
}
break;
case "follow":
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " is now following you.";
break;
case "mention":
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " mentioned you in a post.";
if (notif.status) {
const div = document.createElement('div');
div.innerHTML = notif.status.content;
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
}
break;
case "reblog":
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " reblogged your post.";
if (notif.status) {
const div = document.createElement('div');
div.innerHTML = notif.status.content;
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
}
break;
}
sendNotificationRequest(primaryMessage, secondaryMessage);
}
});
}
toggleAcctMenu() {
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
}
toggleDrawerOnMobile() {
this.setState({
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
})
}
toggleLogOutDialog() {
this.setState({ logOutOpen: !this.state.logOutOpen });
}
searchForQuery(what: string) {
window.location.href = isDesktopApp()? "hyperspace://hyperspace/app/index.html#/search?query=" + what: "/#/search?query=" + what;
window.location.reload;
}
logOutAndRestart() {
let loginData = localStorage.getItem("login");
if (loginData) {
let items = ["login", "account", "baseurl", "access_token"];
items.forEach((entry) => {
localStorage.removeItem(entry);
})
window.location.reload();
}
}
clearBadge() {
if (!getUserDefaultBool('displayAllOnNotificationBadge')) {
this.setState({ notificationCount: 0 });
}
}
titlebar() {
const { classes } = this.props;
if (isDarwinApp()) {
return (
<div className={classes.titleBarRoot}>
<Typography className={classes.titleBarText}>{this.state.brandName? this.state.brandName: "Hyperspace"} {this.state.developerMode? "(beta)": null}</Typography>
</div>
);
} else if (this.state.developerMode || process.env.NODE_ENV === "development") {
return (
<div className={classes.titleBarRoot}>
<Typography className={classes.titleBarText}>Careful: you're running in developer mode.</Typography>
</div>
);
}
}
appDrawer() {
const { classes } = this.props;
return (
<div>
<List>
<div className={classes.drawerDisplayMobile}>
<LinkableListItem button key="profile-mobile" to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
<ListItemAvatar>
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</ListItemAvatar>
<ListItemText primary={this.state.currentUser? (this.state.currentUser.display_name || this.state.currentUser.acct): "Loading..."} secondary={this.state.currentUser? this.state.currentUser.acct: "Loading..."}/>
</LinkableListItem>
{/* <LinkableListItem button key="acctSwitch-module" to="/switchacct">
<ListItemIcon><SupervisedUserCircleIcon/></ListItemIcon>
<ListItemText primary="Switch account"/>
</LinkableListItem> */}
<ListItem button key="acctLogout-mobile" onClick={() => this.toggleLogOutDialog()}>
<ListItemIcon><ExitToAppIcon/></ListItemIcon>
<ListItemText primary="Log out"/>
</ListItem>
<Divider/>
</div>
<ListSubheader>Timelines</ListSubheader>
<LinkableListItem button key="home" to="/home">
<ListItemIcon><HomeIcon/></ListItemIcon>
<ListItemText primary="Home"/>
</LinkableListItem>
<LinkableListItem button key="local" to="/local">
<ListItemIcon><DomainIcon/></ListItemIcon>
<ListItemText primary="Local"/>
</LinkableListItem>
{
this.state.enableFederation?
<LinkableListItem button key="public" to="/public">
<ListItemIcon><PublicIcon/></ListItemIcon>
<ListItemText primary="Public"/>
</LinkableListItem>:
<ListItem disabled>
<ListItemIcon><PublicIcon/></ListItemIcon>
<ListItemText primary="Public" secondary="Disabled by admin"/>
</ListItem>
}
<Divider/>
<div className={classes.drawerDisplayMobile}>
<ListSubheader>Account</ListSubheader>
<LinkableListItem button key="notifications-mobile" to="/notifications">
<ListItemIcon>
<Badge badgeContent={this.state.notificationCount > 0? this.state.notificationCount: ""} color="secondary">
<NotificationsIcon />
</Badge>
</ListItemIcon>
<ListItemText primary="Notifications"/>
</LinkableListItem>
<LinkableListItem button key="messages-mobile" to="/messages">
<ListItemIcon><MailIcon/></ListItemIcon>
<ListItemText primary="Messages"/>
</LinkableListItem>
<Divider/>
</div>
<ListSubheader>More</ListSubheader>
<LinkableListItem button key="recommended" to="/recommended">
<ListItemIcon><GroupIcon/></ListItemIcon>
<ListItemText primary="Who to follow"/>
</LinkableListItem>
<LinkableListItem button key="settings" to="/settings">
<ListItemIcon><SettingsIcon/></ListItemIcon>
<ListItemText primary="Settings"/>
</LinkableListItem>
<LinkableListItem button key="info" to="/about">
<ListItemIcon><InfoIcon/></ListItemIcon>
<ListItemText primary="About"/>
</LinkableListItem>
</List>
</div>
);
}
render() {
const { classes } = this.props;
return (
<div className={classes.root}>
<div className={classes.stickyArea}>
{this.titlebar()}
<AppBar className={classes.appBar} position="static">
<Toolbar>
<IconButton
className={classes.appBarMenuButton}
color="inherit"
aria-label="Open drawer"
onClick={this.toggleDrawerOnMobile}
>
<MenuIcon/>
</IconButton>
<Typography className={classes.appBarTitle} variant="h6" color="inherit" noWrap>
{this.state.brandName? this.state.brandName: "Hyperspace"}
</Typography>
<div className={classes.appBarFlexGrow}/>
<div className={classes.appBarSearch}>
<div className={classes.appBarSearchIcon}>
<SearchIcon/>
</div>
<InputBase
placeholder="Search..."
classes={{
root: classes.appBarSearchInputRoot,
input: classes.appBarSearchInputInput
}}
onKeyUp={(event) => {
if (event.keyCode === 13) {
this.searchForQuery(event.currentTarget.value);
}
}}
/>
</div>
<div className={classes.appBarFlexGrow}/>
<div className={classes.appBarActionButtons}>
<Tooltip title="Notifications">
<LinkableIconButton color="inherit" to="/notifications" onClick={this.clearBadge}>
<Badge badgeContent={this.state.notificationCount > 0? this.state.notificationCount: ""} color="secondary">
<NotificationsIcon />
</Badge>
</LinkableIconButton>
</Tooltip>
<Tooltip title="Direct messages">
<LinkableIconButton color="inherit" to="/messages">
<MailIcon/>
</LinkableIconButton>
</Tooltip>
<Tooltip title="Your account">
<IconButton id="acctMenuBtn" onClick={this.toggleAcctMenu}>
<Avatar className={classes.appBarAcctMenuIcon} alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</IconButton>
</Tooltip>
<Menu
id="acct-menu"
anchorEl={document.getElementById("acctMenuBtn")}
open={this.state.acctMenuOpen}
className={classes.acctMenu}
>
<ClickAwayListener onClickAway={this.toggleAcctMenu}>
<div>
<LinkableListItem to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
<ListItemAvatar>
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
</ListItemAvatar>
<ListItemText
primary={this.state.currentUser? (this.state.currentUser.display_name || this.state.currentUser.acct): "Loading..."}
secondary={'@' + (this.state.currentUser? this.state.currentUser.acct: "Loading...")}
/>
</LinkableListItem>
<Divider/>
<LinkableListItem to={"/you"}>
<ListItemText>Edit profile</ListItemText>
</LinkableListItem>
{/* <MenuItem>Switch account</MenuItem> */}
<MenuItem onClick={() => this.toggleLogOutDialog()}>Log out</MenuItem>
</div>
</ClickAwayListener>
</Menu>
</div>
</Toolbar>
</AppBar>
<nav className={classes.drawer}>
<Hidden mdUp implementation="css">
<Drawer
container={this.props.container}
variant="temporary"
anchor={'left'}
open={this.state.drawerOpenOnMobile}
onClose={this.toggleDrawerOnMobile}
classes={{ paper: classes.drawerPaper }}
>
{this.appDrawer()}
</Drawer>
</Hidden>
<Hidden smDown implementation="css">
<Drawer
classes={{
paper: this.titlebar()? classes.drawerPaperWithTitleAndAppBar: classes.drawerPaperWithAppBar
}}
variant="permanent"
open
>
{this.appDrawer()}
</Drawer>
</Hidden>
</nav>
</div>
<Dialog
open={this.state.logOutOpen}
onClose={() => this.toggleLogOutDialog()}
>
<DialogTitle id="alert-dialog-title">Log out of {this.state.brandName? this.state.brandName: "Hyperspace"}</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
You'll need to remove {this.state.brandName? this.state.brandName: "Hyperspace"} from your list of authorized apps and log in again if you want to use {this.state.brandName? this.state.brandName: "Hyperspace"}.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button onClick={() => this.toggleLogOutDialog()} color="primary" autoFocus>
Cancel
</Button>
<Button onClick={() => {
this.logOutAndRestart();
}} color="primary">
Log out
</Button>
</DialogActions>
</Dialog>
<Tooltip title="Create a new post">
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
<CreateIcon/>
</LinkableFab>
</Tooltip>
</div>
);
}
}
export default withStyles(styles)(withSnackbar(AppLayout));