Merge pull request #137 from hyperspacedev/HD-28-back-button

HD-28 #done
This commit is contained in:
Marquis Kurt 2019-12-22 14:55:17 -05:00 committed by GitHub
commit 89c4339c85
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 147 additions and 0 deletions

View File

@ -57,6 +57,10 @@ export const styles = (theme: Theme) =>
display: "none" display: "none"
} }
}, },
appBarBackButton: {
marginLeft: -12,
marginRight: 20
},
appBarTitle: { appBarTitle: {
display: "none", display: "none",
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {

View File

@ -44,6 +44,7 @@ import SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
import ExitToAppIcon from "@material-ui/icons/ExitToApp"; import ExitToAppIcon from "@material-ui/icons/ExitToApp";
import TrendingUpIcon from "@material-ui/icons/TrendingUp"; import TrendingUpIcon from "@material-ui/icons/TrendingUp";
import BuildIcon from "@material-ui/icons/Build"; import BuildIcon from "@material-ui/icons/Build";
import ArrowBackIcon from "@material-ui/icons/ArrowBack";
import { styles } from "./AppLayout.styles"; import { styles } from "./AppLayout.styles";
import { MultiAccount, UAccount } from "../../types/Account"; import { MultiAccount, UAccount } from "../../types/Account";
@ -67,30 +68,81 @@ import {
getAccountRegistry, getAccountRegistry,
removeAccountFromRegistry removeAccountFromRegistry
} from "../../utilities/accounts"; } from "../../utilities/accounts";
import { isChildView } from "../../utilities/appbar";
/**
* The pre-define state interface for the app layout.
*/
interface IAppLayoutState { interface IAppLayoutState {
/**
* Whether the account menu is open or not.
*/
acctMenuOpen: boolean; acctMenuOpen: boolean;
/**
* Whether the drawer is open (mobile-only).
*/
drawerOpenOnMobile: boolean; drawerOpenOnMobile: boolean;
/**
* The current user signed in.
*/
currentUser?: UAccount; currentUser?: UAccount;
/**
* The number of notifications received.
*/
notificationCount: number; notificationCount: number;
/**
* Whether the log out dialog is open.
*/
logOutOpen: boolean; logOutOpen: boolean;
/**
* Whether federation has been enabled in the config.
*/
enableFederation?: boolean; enableFederation?: boolean;
/**
* The brand name of the app, if not "Hyperspace".
*/
brandName?: string; brandName?: string;
/**
* Whether the app is in development mode.
*/
developerMode?: boolean; developerMode?: boolean;
} }
/**
* The base app layout class. Responsible for the search bar, navigation menus, etc.
*/
export class AppLayout extends Component<any, IAppLayoutState> { export class AppLayout extends Component<any, IAppLayoutState> {
/**
* The Mastodon client to operate with.
*/
client: Mastodon; client: Mastodon;
/**
* A stream listener to listen for new streaming events from Mastodon.
*/
streamListener: any; streamListener: any;
/**
* Construct the app layout.
* @param props The properties to pass in.
*/
constructor(props: any) { constructor(props: any) {
super(props); super(props);
// Create the Mastodon client
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
// Initialize the state
this.state = { this.state = {
drawerOpenOnMobile: false, drawerOpenOnMobile: false,
acctMenuOpen: false, acctMenuOpen: false,
@ -98,14 +150,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
logOutOpen: false logOutOpen: false
}; };
// Bind functions as properties to this class for reference
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this); this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
this.toggleAcctMenu = this.toggleAcctMenu.bind(this); this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
this.clearBadge = this.clearBadge.bind(this); this.clearBadge = this.clearBadge.bind(this);
} }
/**
* Run post-mount tasks such as getting account data and refreshing the config file.
*/
componentDidMount() { componentDidMount() {
// Get the account data.
this.getAccountData(); this.getAccountData();
// Read the config file and then update the state.
getConfig().then((result: any) => { getConfig().then((result: any) => {
if (result !== undefined) { if (result !== undefined) {
let config: Config = result; let config: Config = result;
@ -119,18 +177,25 @@ export class AppLayout extends Component<any, IAppLayoutState> {
} }
}); });
// Listen for notifications.
this.streamNotifications(); this.streamNotifications();
} }
/**
* Get updated credentials from Mastodon or pull information from local storage.
*/
getAccountData() { getAccountData() {
// Try to get updated credentials from Mastodon.
this.client this.client
.get("/accounts/verify_credentials") .get("/accounts/verify_credentials")
.then((resp: any) => { .then((resp: any) => {
// Update the account if possible.
let data: UAccount = resp.data; let data: UAccount = resp.data;
this.setState({ currentUser: data }); this.setState({ currentUser: data });
sessionStorage.setItem("id", data.id); sessionStorage.setItem("id", data.id);
}) })
.catch((err: Error) => { .catch((err: Error) => {
// Otherwise, pull from local storage.
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"Couldn't find profile info: " + err.name "Couldn't find profile info: " + err.name
); );
@ -140,9 +205,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}); });
} }
/**
* Set up a stream listener and listen for notifications.
*/
streamNotifications() { streamNotifications() {
// Set up the stream listener.
this.streamListener = this.client.stream("/streaming/user"); this.streamListener = this.client.stream("/streaming/user");
// Set the count if the user asked to display the total count.
if (getUserDefaultBool("displayAllOnNotificationBadge")) { if (getUserDefaultBool("displayAllOnNotificationBadge")) {
this.client.get("/notifications").then((resp: any) => { this.client.get("/notifications").then((resp: any) => {
let notifArray = resp.data; let notifArray = resp.data;
@ -150,14 +220,17 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}); });
} }
// Listen for notifications.
this.streamListener.on("notification", (notif: Notification) => { this.streamListener.on("notification", (notif: Notification) => {
const notificationCount = this.state.notificationCount + 1; const notificationCount = this.state.notificationCount + 1;
this.setState({ notificationCount }); this.setState({ notificationCount });
// Update the badge on the desktop.
if (isDesktopApp()) { if (isDesktopApp()) {
getElectronApp().setBadgeCount(notificationCount); getElectronApp().setBadgeCount(notificationCount);
} }
// Set up a push notification if the window isn't in focus.
if (!document.hasFocus()) { if (!document.hasFocus()) {
let primaryMessage = ""; let primaryMessage = "";
let secondaryMessage = ""; let secondaryMessage = "";
@ -216,25 +289,39 @@ export class AppLayout extends Component<any, IAppLayoutState> {
break; break;
} }
// Respectfully send the notification request.
sendNotificationRequest(primaryMessage, secondaryMessage); sendNotificationRequest(primaryMessage, secondaryMessage);
} }
}); });
} }
/**
* Toggle the account menu.
*/
toggleAcctMenu() { toggleAcctMenu() {
this.setState({ acctMenuOpen: !this.state.acctMenuOpen }); this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
} }
/**
* Toggle the app drawer, if on mobile.
*/
toggleDrawerOnMobile() { toggleDrawerOnMobile() {
this.setState({ this.setState({
drawerOpenOnMobile: !this.state.drawerOpenOnMobile drawerOpenOnMobile: !this.state.drawerOpenOnMobile
}); });
} }
/**
* Toggle the logout dialog.
*/
toggleLogOutDialog() { toggleLogOutDialog() {
this.setState({ logOutOpen: !this.state.logOutOpen }); 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) { searchForQuery(what: string) {
what = what.replace(/^#/g, "tag:"); what = what.replace(/^#/g, "tag:");
console.log(what); console.log(what);
@ -243,9 +330,13 @@ export class AppLayout extends Component<any, IAppLayoutState> {
: "/#/search?query=" + what; : "/#/search?query=" + what;
} }
/**
* Clear login information, remove the account from the registry, and reload the web page.
*/
logOutAndRestart() { logOutAndRestart() {
let loginData = localStorage.getItem("login"); let loginData = localStorage.getItem("login");
if (loginData) { if (loginData) {
// Remove account from the registry.
let registry = getAccountRegistry(); let registry = getAccountRegistry();
registry.forEach((registryItem: MultiAccount, index: number) => { registry.forEach((registryItem: MultiAccount, index: number) => {
@ -257,15 +348,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
} }
}); });
// Clear some of the local storage fields.
let items = ["login", "account", "baseurl", "access_token"]; let items = ["login", "account", "baseurl", "access_token"];
items.forEach(entry => { items.forEach(entry => {
localStorage.removeItem(entry); localStorage.removeItem(entry);
}); });
// Finally, reload.
window.location.reload(); window.location.reload();
} }
} }
/**
* Clear the notifications badge.
*/
clearBadge() { clearBadge() {
if (!getUserDefaultBool("displayAllOnNotificationBadge")) { if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
this.setState({ notificationCount: 0 }); this.setState({ notificationCount: 0 });
@ -276,6 +372,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
} }
} }
/**
* Render the title bar.
*/
titlebar() { titlebar() {
const { classes } = this.props; const { classes } = this.props;
if (isDarwinApp()) { if (isDarwinApp()) {
@ -307,6 +406,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
} }
} }
/**
* Render the app drawer. On the desktop, this appears as a sidebar in larger layouts.
*/
appDrawer() { appDrawer() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
@ -476,6 +578,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
); );
} }
/**
* Render the entire layout.
*/
render() { render() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
@ -484,6 +589,18 @@ export class AppLayout extends Component<any, IAppLayoutState> {
{this.titlebar()} {this.titlebar()}
<AppBar className={classes.appBar} position="static"> <AppBar className={classes.appBar} position="static">
<Toolbar> <Toolbar>
{isDesktopApp() &&
isChildView(window.location.hash) ? (
<IconButton
className={classes.appBarBackButton}
color="inherit"
aria-label="Go back"
onClick={() => window.history.back()}
>
<ArrowBackIcon />
</IconButton>
) : null}
<IconButton <IconButton
className={classes.appBarMenuButton} className={classes.appBarMenuButton}
color="inherit" color="inherit"

View File

@ -1,5 +1,14 @@
import { isDarwinApp } from "./desktop"; import { isDarwinApp } from "./desktop";
/**
* A list containing the types of child views.
*
* This list is used to help determine if a back button is necessary, usually because there
* is no defined way of returning to the parent view without using the menu bar or keyboard
* shortcut in desktop apps.
*/
export const childViews = ["#/profile", "#/conversation"];
/** /**
* Determine whether the title bar is being displayed. * Determine whether the title bar is being displayed.
* This might be useful in cases where styles are dependent on the title bar's visibility, such as heights. * This might be useful in cases where styles are dependent on the title bar's visibility, such as heights.
@ -9,3 +18,20 @@ import { isDarwinApp } from "./desktop";
export function isAppbarExpanded(): boolean { export function isAppbarExpanded(): boolean {
return isDarwinApp() || process.env.NODE_ENV === "development"; return isDarwinApp() || process.env.NODE_ENV === "development";
} }
/**
* Determine whether a path is considered a "child view".
*
* This is often used to determine whether a back button should be rendered or not.
* @param path The path of the page, usually its hash
* @returns Boolean distating if the view is a child view.
*/
export function isChildView(path: string): boolean {
let protocolMatched = false;
childViews.forEach((childViewProtocol: string) => {
if (path.startsWith(childViewProtocol)) {
protocolMatched = true;
}
});
return protocolMatched;
}