Merge pull request #137 from hyperspacedev/HD-28-back-button
HD-28 #done
This commit is contained in:
commit
89c4339c85
|
@ -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")]: {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in New Issue