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 TrendingUpIcon from "@material-ui/icons/TrendingUp"; import BuildIcon from "@material-ui/icons/Build"; import ArrowBackIcon from "@material-ui/icons/ArrowBack"; import { styles } from "./AppLayout.styles"; import { MultiAccount, 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, getElectronApp } from "../../utilities/desktop"; import { Config } from "../../types/Config"; import { getAccountRegistry, removeAccountFromRegistry } from "../../utilities/accounts"; import { isChildView } from "../../utilities/appbar"; /** * The pre-define state interface for the app layout. */ interface IAppLayoutState { /** * Whether the account menu is open or not. */ acctMenuOpen: boolean; /** * Whether the drawer is open (mobile-only). */ drawerOpenOnMobile: boolean; /** * The current user signed in. */ currentUser?: UAccount; /** * The number of notifications received. */ notificationCount: number; /** * Whether the log out dialog is open. */ logOutOpen: boolean; /** * Whether federation has been enabled in the config. */ enableFederation?: boolean; /** * The brand name of the app, if not "Hyperspace". */ brandName?: string; /** * Whether the app is in development mode. */ developerMode?: boolean; } /** * The base app layout class. Responsible for the search bar, navigation menus, etc. */ export class AppLayout extends Component { /** * The Mastodon client to operate with. */ client: Mastodon; /** * A stream listener to listen for new streaming events from Mastodon. */ streamListener: any; /** * Construct the app layout. * @param props The properties to pass in. */ constructor(props: any) { super(props); // Create the Mastodon client this.client = new Mastodon( localStorage.getItem("access_token") as string, (localStorage.getItem("baseurl") as string) + "/api/v1" ); // Initialize the state this.state = { drawerOpenOnMobile: false, acctMenuOpen: false, notificationCount: 0, logOutOpen: false }; // Bind functions as properties to this class for reference this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this); this.toggleAcctMenu = this.toggleAcctMenu.bind(this); this.clearBadge = this.clearBadge.bind(this); } /** * Run post-mount tasks such as getting account data and refreshing the config file. */ componentDidMount() { // Get the account data. this.getAccountData(); // Read the config file and then update the state. 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 }); } }); // Listen for notifications. this.streamNotifications(); } /** * Get updated credentials from Mastodon or pull information from local storage. */ getAccountData() { // Try to get updated credentials from Mastodon. this.client .get("/accounts/verify_credentials") .then((resp: any) => { // Update the account if possible. let data: UAccount = resp.data; this.setState({ currentUser: data }); sessionStorage.setItem("id", data.id); }) .catch((err: Error) => { // Otherwise, pull from local storage. this.props.enqueueSnackbar( "Couldn't find profile info: " + err.name ); console.error(err.message); let acct = localStorage.getItem("account") as string; this.setState({ currentUser: JSON.parse(acct) }); }); } /** * Set up a stream listener and listen for notifications. */ streamNotifications() { // Set up the stream listener. this.streamListener = this.client.stream("/streaming/user"); // Set the count if the user asked to display the total count. if (getUserDefaultBool("displayAllOnNotificationBadge")) { this.client.get("/notifications").then((resp: any) => { let notifArray = resp.data; this.setState({ notificationCount: notifArray.length }); }); } // Listen for notifications. this.streamListener.on("notification", (notif: Notification) => { const notificationCount = this.state.notificationCount + 1; this.setState({ notificationCount }); // Update the badge on the desktop. if (isDesktopApp()) { getElectronApp().setBadgeCount(notificationCount); } // Set up a push notification if the window isn't in focus. 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; } // Respectfully send the notification request. sendNotificationRequest(primaryMessage, secondaryMessage); } }); } /** * Toggle the account menu. */ toggleAcctMenu() { this.setState({ acctMenuOpen: !this.state.acctMenuOpen }); } /** * Toggle the app drawer, if on mobile. */ toggleDrawerOnMobile() { this.setState({ drawerOpenOnMobile: !this.state.drawerOpenOnMobile }); } /** * Toggle the logout dialog. */ toggleLogOutDialog() { this.setState({ logOutOpen: !this.state.logOutOpen }); } /** * Perform a search and redirect to the search page. * @param what The query input from the search box */ searchForQuery(what: string) { what = what.replace(/^#/g, "tag:"); console.log(what); window.location.href = isDesktopApp() ? "hyperspace://hyperspace/app/index.html#/search?query=" + what : "/#/search?query=" + what; } /** * Clear login information, remove the account from the registry, and reload the web page. */ logOutAndRestart() { let loginData = localStorage.getItem("login"); if (loginData) { // Remove account from the registry. let registry = getAccountRegistry(); registry.forEach((registryItem: MultiAccount, index: number) => { if ( registryItem.access_token === localStorage.getItem("access_token") ) { removeAccountFromRegistry(index); } }); // Clear some of the local storage fields. let items = ["login", "account", "baseurl", "access_token"]; items.forEach(entry => { localStorage.removeItem(entry); }); // Finally, reload. window.location.reload(); } } /** * Clear the notifications badge. */ clearBadge() { if (!getUserDefaultBool("displayAllOnNotificationBadge")) { this.setState({ notificationCount: 0 }); } if (isDesktopApp() && getElectronApp().getBadgeCount() > 0) { getElectronApp().setBadgeCount(0); } } /** * Render the title bar. */ titlebar() { const { classes } = this.props; if (isDarwinApp()) { return (
{this.state.brandName ? this.state.brandName : "Hyperspace"}{" "} Desktop {this.state.developerMode ? "(Beta)" : null}
); } else if (process.env.NODE_ENV === "development") { return (
{" "} Careful: you're running in developer mode.
); } } /** * Render the app drawer. On the desktop, this appears as a sidebar in larger layouts. */ appDrawer() { const { classes } = this.props; return (
1 ? "Switch account" : "Add account" } /> this.toggleLogOutDialog()} >
Timelines {this.state.enableFederation ? ( ) : ( )}
Account 0 ? this.state.notificationCount : "" } color="secondary" >
Community More
); } /** * Render the entire layout. */ render() { const { classes } = this.props; return (
{this.titlebar()} {isDesktopApp() && isChildView(window.location.hash) ? ( window.history.back()} > ) : null} {this.state.brandName ? this.state.brandName : "Hyperspace"}
{ if (event.keyCode === 13) { this.searchForQuery( event.currentTarget.value ); } }} />
0 ? this.state .notificationCount : "" } color="secondary" >
Edit profile Manage follow requests {getAccountRegistry() .length > 1 ? "Switch account" : "Add account"} this.toggleLogOutDialog() } > Log out
{this.logoutDialog()}
); } logoutDialog() { return ( this.toggleLogOutDialog()} > Log out of{" "} {this.state.brandName ? this.state.brandName : "Hyperspace"} 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"} . Logging out will also remove this account from the account list. ); } } export default withStyles(styles)(withSnackbar(AppLayout));