Merge branch 'beta7' into thread-and-settings-redesigns

This commit is contained in:
Marquis Kurt 2019-10-04 13:08:59 -04:00 committed by GitHub
commit 1d2c77c429
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 744 additions and 305 deletions

48
package-lock.json generated
View File

@ -3220,6 +3220,14 @@
"integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==",
"dev": true
},
"bindings": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz",
"integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==",
"requires": {
"file-uri-to-path": "1.0.0"
}
},
"bluebird": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.5.4.tgz",
@ -7262,10 +7270,13 @@
}
},
"eslint-utils": {
"version": "1.3.1",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.3.1.tgz",
"integrity": "sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==",
"dev": true
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-1.4.2.tgz",
"integrity": "sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==",
"dev": true,
"requires": {
"eslint-visitor-keys": "^1.0.0"
}
},
"eslint-visitor-keys": {
"version": "1.0.0",
@ -7325,6 +7336,11 @@
"integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=",
"dev": true
},
"event-target-shim": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-1.1.1.tgz",
"integrity": "sha1-qG5e5r2qFgVEddp5fM3fDFVphJE="
},
"eventemitter3": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-3.1.0.tgz",
@ -8022,6 +8038,11 @@
"schema-utils": "^1.0.0"
}
},
"file-uri-to-path": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz",
"integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="
},
"filename-regex": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/filename-regex/-/filename-regex-2.0.1.tgz",
@ -9448,9 +9469,9 @@
"dev": true
},
"handlebars": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.1.2.tgz",
"integrity": "sha512-nvfrjqvt9xQ8Z/w0ijewdD/vvWDTOweBUm96NTr66Wfvo1mJenBLwcYmPs3TIBP5ruzYGD7Hx/DaM9RmhroGPw==",
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.4.0.tgz",
"integrity": "sha512-xkRtOt3/3DzTKMOt3xahj2M/EqNhY988T+imYSlMgs5fVhLN2fmKVVj0LtEGmb+3UUYV5Qmm1052Mm3dIQxOvw==",
"dev": true,
"requires": {
"neo-async": "^2.6.0",
@ -12840,6 +12861,16 @@
}
}
},
"node-mac-notifier": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/node-mac-notifier/-/node-mac-notifier-1.2.0.tgz",
"integrity": "sha512-+9FZ01BbPMv3pQVRWgPlaIKbhQl35Pn3WmRg96zIrCJHb4XvClnAqc0+aPfHrWs8o1PYMAQFeYK5tF69ljkKQw==",
"requires": {
"bindings": "^1.2.1",
"event-target-shim": "^1.1.1",
"uuid": "^3.3.2"
}
},
"node-notifier": {
"version": "5.4.0",
"resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-5.4.0.tgz",
@ -20242,8 +20273,7 @@
"uuid": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz",
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==",
"dev": true
"integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA=="
},
"validate-npm-package-license": {
"version": "3.0.4",

View File

@ -1,6 +1,6 @@
{
"version": "1.0.0beta7",
"location": "desktop",
"location": "https://localhost:3000",
"branding": {
"name": "Hyperspace",
"logo": "logo.svg",

View File

@ -20,7 +20,7 @@ let mainWindow;
// to when authorizing Hyperspace.
protocol.registerSchemesAsPrivileged([
{ scheme: 'hyperspace', privileges: { standard: true, secure: true } }
])
]);
/**
* Determine whether the desktop app is on macOS
@ -217,15 +217,8 @@ function createMenubar() {
click() {
safelyGoTo("hyperspace://hyperspace/app/#compose")
}
},
{ type: 'separator' },
{
label: 'Edit Profile',
accelerator: "Shift+CmdOrCtrl+P",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/you")
}
},
}
]
},
{
@ -283,7 +276,7 @@ function createMenubar() {
]
},
{
label: "Places",
label: "Timelines",
submenu: [
{
label: 'Home',
@ -307,27 +300,53 @@ function createMenubar() {
}
},
{
label: 'Recommendations',
label: 'Messages',
accelerator: "CmdOrCtrl+3",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/messages")
}
}
]
},
{
label: "Account",
submenu: [
{
label: 'Notifications',
accelerator: "Alt+CmdOrCtrl+N",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/notifications")
}
},
{
label: 'Recommendations...',
accelerator: "Alt+CmdOrCtrl+R",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/recommended")
}
},
{ type: 'separator' },
{
label: 'Notifications',
accelerator: "CmdOrCtrl+4",
label: 'Edit Profile',
accelerator: "Shift+CmdOrCtrl+P",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/notifications")
safelyGoTo("hyperspace://hyperspace/app/#/you")
}
},
{
label: 'Messages',
accelerator: "CmdOrCtrl+5",
label: 'Blocked Servers',
accelerator: "Shift+CmdOrCtrl+B",
click() {
safelyGoTo("hyperspace://hyperspace/app/#/messages")
safelyGoTo("hyperspace://hyperspace/app/#/blocked")
}
},
{ type: 'separator'},
{
label: 'Switch Accounts...',
click() {
safelyGoTo("hyperspace://hyperspace/app/#/welcome")
}
}
]
},
{
@ -356,7 +375,7 @@ function createMenubar() {
}
]
}
]
];
if (process.platform === 'darwin') {
menuBar.unshift({
@ -385,7 +404,7 @@ function createMenubar() {
{ type: 'separator' },
{ role: 'quit' }
]
})
});
// Edit menu
menuBar[2].submenu.push(
@ -397,10 +416,10 @@ function createMenubar() {
{ role: 'stopspeaking' }
]
}
)
);
// Window menu
menuBar[5].submenu = [
menuBar[6].submenu = [
{ role: 'close' },
{ role: 'minimize' },
{ role: 'zoom' },

View File

@ -3,7 +3,7 @@ import { MuiThemeProvider, CssBaseline, withStyles } from "@material-ui/core";
import { setHyperspaceTheme, darkMode } from "./utilities/themes";
import AppLayout from "./components/AppLayout";
import { styles } from "./App.styles";
import { Route } from "react-router-dom";
import { Route, withRouter } from "react-router-dom";
import AboutPage from "./pages/About";
import Settings from "./pages/Settings";
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
@ -27,14 +27,22 @@ import { userLoggedIn } from "./utilities/accounts";
import { isDarwinApp } from "./utilities/desktop";
let theme = setHyperspaceTheme(getUserDefaultTheme());
class App extends Component<any, any> {
interface IAppState {
theme: any;
showLayout: boolean;
}
class App extends Component<any, IAppState> {
offline: any;
unlisten: any;
constructor(props: any) {
super(props);
this.state = {
theme: theme
theme: theme,
showLayout:
userLoggedIn() && !window.location.hash.includes("#/welcome")
};
}
@ -43,17 +51,33 @@ class App extends Component<any, any> {
this.state.theme,
getUserDefaultBool("darkModeEnabled")
);
this.setState({ theme: newTheme });
this.setState({
theme: newTheme,
showLayout:
userLoggedIn() && !window.location.hash.includes("#/welcome")
});
}
componentDidMount() {
this.removeBodyBackground();
this.unlisten = this.props.history.listen(
(location: Location, action: any) => {
this.setState({
showLayout:
userLoggedIn() &&
!location.pathname.includes("/welcome")
});
}
);
}
componentDidUpdate() {
this.removeBodyBackground();
}
componentWillUnmount() {
this.unlisten();
}
removeBodyBackground() {
if (isDarwinApp()) {
@ -71,7 +95,7 @@ class App extends Component<any, any> {
<CssBaseline />
<Route path="/welcome" component={WelcomePage} />
<div>
{userLoggedIn() ? <AppLayout /> : null}
{this.state.showLayout ? <AppLayout /> : null}
<PrivateRoute exact path="/" component={HomePage} />
<PrivateRoute path="/home" component={HomePage} />
<PrivateRoute path="/local" component={LocalPage} />
@ -91,7 +115,7 @@ class App extends Component<any, any> {
/>
<PrivateRoute path="/search" component={SearchPage} />
<PrivateRoute path="/settings" component={Settings} />
<PrivateRoute path="/blocked" component={Blocked}/>
<PrivateRoute path="/blocked" component={Blocked} />
<PrivateRoute path="/you" component={You} />
<PrivateRoute path="/about" component={AboutPage} />
<PrivateRoute path="/compose" component={Composer} />
@ -105,4 +129,5 @@ class App extends Component<any, any> {
}
}
export default withStyles(styles)(withSnackbar(App));
// @ts-ignore
export default withStyles(styles)(withSnackbar(withRouter(App)));

View File

@ -39,10 +39,10 @@ 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 SupervisedUserCircleIcon from "@material-ui/icons/SupervisedUserCircle";
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
import { styles } from "./AppLayout.styles";
import { UAccount } from "../../types/Account";
import { MultiAccount, UAccount } from "../../types/Account";
import {
LinkableListItem,
LinkableIconButton,
@ -53,8 +53,16 @@ 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 {
isDesktopApp,
isDarwinApp,
getElectronApp
} from "../../utilities/desktop";
import { Config } from "../../types/Config";
import {
getAccountRegistry,
removeAccountFromRegistry
} from "../../utilities/accounts";
interface IAppLayoutState {
acctMenuOpen: boolean;
@ -92,23 +100,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}
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);
});
}
this.getAccountData();
getConfig().then((result: any) => {
if (result !== undefined) {
@ -126,6 +118,24 @@ export class AppLayout extends Component<any, IAppLayoutState> {
this.streamNotifications();
}
getAccountData() {
this.client
.get("/accounts/verify_credentials")
.then((resp: any) => {
let data: UAccount = resp.data;
this.setState({ currentUser: data });
sessionStorage.setItem("id", data.id);
})
.catch((err: Error) => {
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) });
});
}
streamNotifications() {
this.streamListener = this.client.stream("/streaming/user");
@ -139,6 +149,11 @@ export class AppLayout extends Component<any, IAppLayoutState> {
this.streamListener.on("notification", (notif: Notification) => {
const notificationCount = this.state.notificationCount + 1;
this.setState({ notificationCount });
if (isDesktopApp()) {
getElectronApp().setBadgeCount(notificationCount);
}
if (!document.hasFocus()) {
let primaryMessage = "";
let secondaryMessage = "";
@ -220,16 +235,28 @@ export class AppLayout extends Component<any, IAppLayoutState> {
window.location.href = isDesktopApp()
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
: "/#/search?query=" + what;
window.location.reload;
window.location.reload();
}
logOutAndRestart() {
let loginData = localStorage.getItem("login");
if (loginData) {
let registry = getAccountRegistry();
registry.forEach((registryItem: MultiAccount, index: number) => {
if (
registryItem.access_token ===
localStorage.getItem("access_token")
) {
removeAccountFromRegistry(index);
}
});
let items = ["login", "account", "baseurl", "access_token"];
items.forEach(entry => {
localStorage.removeItem(entry);
});
window.location.reload();
}
}
@ -238,6 +265,10 @@ export class AppLayout extends Component<any, IAppLayoutState> {
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
this.setState({ notificationCount: 0 });
}
if (isDesktopApp() && getElectronApp().getBadgeCount() > 0) {
getElectronApp().setBadgeCount(0);
}
}
titlebar() {
@ -307,10 +338,22 @@ export class AppLayout extends Component<any, IAppLayoutState> {
}
/>
</LinkableListItem>
{/* <LinkableListItem button key="acctSwitch-module" to="/switchacct">
<ListItemIcon><SupervisedUserCircleIcon/></ListItemIcon>
<ListItemText primary="Switch account"/>
</LinkableListItem> */}
<LinkableListItem
button
key="acctSwitch-module"
to="/welcome"
>
<ListItemIcon>
<SupervisedUserCircleIcon />
</ListItemIcon>
<ListItemText
primary={
getAccountRegistry().length > 1
? "Switch account"
: "Add account"
}
/>
</LinkableListItem>
<ListItem
button
key="acctLogout-mobile"
@ -571,7 +614,14 @@ export class AppLayout extends Component<any, IAppLayoutState> {
Edit profile
</ListItemText>
</LinkableListItem>
{/* <MenuItem>Switch account</MenuItem> */}
<LinkableListItem to={"/welcome"}>
<ListItemText>
{getAccountRegistry()
.length > 1
? "Switch account"
: "Add account"}
</ListItemText>
</LinkableListItem>
<MenuItem
onClick={() =>
this.toggleLogOutDialog()
@ -613,48 +663,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
</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>
{this.logoutDialog()}
<Tooltip title="Create a new post">
<LinkableFab
to="/compose"
@ -668,6 +677,57 @@ export class AppLayout extends Component<any, IAppLayoutState> {
</div>
);
}
logoutDialog() {
return (
<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">
<Typography paragraph>
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"}
.
</Typography>
<Typography paragraph>
Logging out will also remove this account from the
account list.
</Typography>
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.toggleLogOutDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.logOutAndRestart();
}}
color="primary"
>
Log out
</Button>
</DialogActions>
</Dialog>
);
}
}
export default withStyles(styles)(withSnackbar(AppLayout));

View File

@ -1,33 +1,32 @@
import React from "react";
import {
Typography,
IconButton,
Card,
CardHeader,
Avatar,
CardContent,
CardActions,
withStyles,
Menu,
MenuItem,
Chip,
Divider,
CardMedia,
CardActionArea,
ExpansionPanel,
ExpansionPanelSummary,
ExpansionPanelDetails,
Zoom,
Tooltip,
RadioGroup,
Radio,
FormControlLabel,
Button,
Card,
CardActionArea,
CardActions,
CardContent,
CardHeader,
CardMedia,
Dialog,
DialogTitle,
DialogActions,
DialogContent,
DialogContentText,
DialogActions
DialogTitle,
Divider,
ExpansionPanel,
ExpansionPanelDetails,
ExpansionPanelSummary,
FormControlLabel,
IconButton,
Menu,
MenuItem,
Radio,
RadioGroup,
Tooltip,
Typography,
withStyles,
Zoom
} from "@material-ui/core";
import MoreVertIcon from "@material-ui/icons/MoreVert";
import ReplyIcon from "@material-ui/icons/Reply";
@ -51,10 +50,10 @@ import moment from "moment";
import AttachmentComponent from "../Attachment";
import Mastodon from "megalodon";
import {
LinkableAvatar,
LinkableChip,
LinkableMenuItem,
LinkableIconButton,
LinkableAvatar
LinkableMenuItem
} from "../../interfaces/overrides";
import { withSnackbar } from "notistack";
import ShareMenu from "./PostShareMenu";
@ -74,6 +73,7 @@ interface IPostState {
menuIsOpen: boolean;
myVote?: [number];
deletePostDialog: boolean;
myAccount?: string;
}
export class Post extends React.Component<any, IPostState> {
@ -95,6 +95,12 @@ export class Post extends React.Component<any, IPostState> {
this.client = this.props.client;
}
componentWillMount() {
this.setState({
myAccount: sessionStorage.getItem("id") as string
});
}
togglePostMenu() {
this.setState({ menuIsOpen: !this.state.menuIsOpen });
}
@ -106,7 +112,7 @@ export class Post extends React.Component<any, IPostState> {
deletePost() {
this.client
.del("/statuses/" + this.state.post.id)
.then((resp: any) => {
.then(() => {
this.props.enqueueSnackbar(
"Post deleted. Refresh to see changes."
);
@ -262,7 +268,7 @@ export class Post extends React.Component<any, IPostState> {
<RadioGroup value={this.findBiggestVote()}>
{status.poll.options.map(
(pollOption: PollOption) => {
let x = (
return (
<FormControlLabel
disabled
value={pollOption.title}
@ -274,7 +280,6 @@ export class Post extends React.Component<any, IPostState> {
}
/>
);
return x;
}
)}
</RadioGroup>
@ -303,7 +308,7 @@ export class Post extends React.Component<any, IPostState> {
>
{status.poll.options.map(
(pollOption: PollOption) => {
let x = (
return (
<FormControlLabel
value={pollOption.title}
control={<Radio />}
@ -314,13 +319,12 @@ export class Post extends React.Component<any, IPostState> {
}
/>
);
return x;
}
)}
</RadioGroup>
<Button
color="primary"
onClick={(event: any) => this.submitVote()}
onClick={() => this.submitVote()}
>
Vote
</Button>
@ -381,7 +385,6 @@ export class Post extends React.Component<any, IPostState> {
}
getReblogOfPost(of: Status | null) {
const { classes } = this.props;
if (of !== null) {
return of.sensitive
? this.getSensitiveContent(of.spoiler_text, of)
@ -832,9 +835,8 @@ export class Post extends React.Component<any, IPostState> {
Open in Web
</MenuItem>
</div>
{post.account.id ==
JSON.parse(localStorage.getItem("account") as string)
.id ? (
{this.state.myAccount &&
post.account.id === this.state.myAccount ? (
<div>
<Divider />
<MenuItem

View File

@ -22,6 +22,12 @@ if (userLoggedIn()) {
refreshUserAccountData();
}
window.onstorage = (event: any) => {
if (event.key == "account") {
window.location.reload();
}
};
ReactDOM.render(
<HashRouter>
<SnackbarProvider

View File

@ -84,6 +84,10 @@ class Composer extends Component<any, IComposerState> {
componentDidMount() {
let state = this.getComposerParams(this.props);
let text = state.acct ? `@${state.acct}: ` : "";
this.client.get("/accounts/verify_credentials").then((resp: any) => {
let account: UAccount = resp.data;
this.setState({ account });
});
getConfig().then((config: any) => {
this.setState({
federated: config.federation.allowPublicPosts,
@ -439,7 +443,7 @@ class Composer extends Component<any, IComposerState> {
event.target.value
)
}
></TextField>
/>
</Fade>
) : null}
{this.state.visibility === "direct" ? (

View File

@ -110,7 +110,7 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
removeNotification(id: string) {
this.client
.post("/notifications/dismiss", { id: id })
.post(`/notifications/${id}/dismiss`)
.then((resp: any) => {
let notifications = this.state.notifications;
if (notifications !== undefined && notifications.length > 0) {

View File

@ -12,7 +12,13 @@ import {
Dialog,
DialogTitle,
DialogActions,
DialogContent
DialogContent,
List,
ListItem,
ListItemText,
ListItemAvatar,
ListItemSecondaryAction,
IconButton
} from "@material-ui/core";
import { styles } from "./WelcomePage.styles";
import Mastodon from "megalodon";
@ -28,6 +34,16 @@ import { isDarwinApp } from "../utilities/desktop";
import axios from "axios";
import { withSnackbar, withSnackbarProps } from "notistack";
import { Config } from "../types/Config";
import {
addAccountToRegistry,
getAccountRegistry,
loginWithAccount,
removeAccountFromRegistry
} from "../utilities/accounts";
import { Account, MultiAccount } from "../types/Account";
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
import CloseIcon from "@material-ui/icons/Close";
interface IWelcomeProps extends withSnackbarProps {
classes: any;
@ -39,7 +55,7 @@ interface IWelcomeState {
brandName?: string;
registerBase?: string;
federates?: boolean;
wantsToLogin: boolean;
proceedToGetCode: boolean;
user: string;
userInputError: boolean;
userInputErrorMessage: string;
@ -47,7 +63,7 @@ interface IWelcomeState {
clientSecret?: string;
authUrl?: string;
foundSavedLogin: boolean;
authority: boolean;
authorizing: boolean;
license?: string;
repo?: string;
defaultRedirectAddress: string;
@ -55,6 +71,7 @@ interface IWelcomeState {
authCode: string;
emergencyMode: boolean;
version: string;
willAddAccount: boolean;
}
class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
@ -64,17 +81,18 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
super(props);
this.state = {
wantsToLogin: false,
proceedToGetCode: false,
user: "",
userInputError: false,
foundSavedLogin: false,
authority: false,
authorizing: false,
userInputErrorMessage: "",
defaultRedirectAddress: "",
openAuthDialog: false,
authCode: "",
emergencyMode: false,
version: ""
version: "",
willAddAccount: false
};
getConfig()
@ -155,6 +173,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
}
}
clear() {
localStorage.removeItem("access_token");
localStorage.removeItem("baseurl");
}
getSavedSession() {
let loginData = localStorage.getItem("login");
if (loginData) {
@ -253,7 +276,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
clientId: resp.clientId,
clientSecret: resp.clientSecret,
authUrl: resp.url,
wantsToLogin: true
proceedToGetCode: true
});
});
} else {
@ -304,7 +327,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
clientSecret: session.clientSecret,
authUrl: session.authUrl,
emergencyMode: session.emergency,
wantsToLogin: true
proceedToGetCode: true
});
}
}
@ -332,8 +355,8 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
axios
.get(
"https://" +
baseUrl +
"/api/v1/timelines/public"
baseUrl +
"/api/v1/timelines/public"
)
.catch((err: Error) => {
let userInputError = true;
@ -375,7 +398,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
let location = window.location.href;
if (location.includes("?code=")) {
let code = parseUrl(location).query.code as string;
this.setState({ authority: true });
this.setState({ authorizing: true });
let loginData = localStorage.getItem("login");
if (loginData) {
let clientLoginSession: SaveClientSession = JSON.parse(
@ -389,12 +412,12 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
this.state.emergencyMode
? undefined
: clientLoginSession.authUrl.includes(
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
)
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
)
? undefined
: window.location.protocol === "hyperspace:"
? "hyperspace://hyperspace/app/"
: `https://${window.location.host}`
? "hyperspace://hyperspace/app/"
: `https://${window.location.host}`
)
.then((tokenData: any) => {
localStorage.setItem(
@ -436,6 +459,65 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
}
}
showMultiAccount() {
const { classes } = this.props;
return (
<div>
<Typography variant="h5">Select an account</Typography>
<Typography>from the list below or add a new one</Typography>
<List>
{getAccountRegistry().map(
(account: MultiAccount, index: number) => (
<ListItem
onClick={() => {
loginWithAccount(account);
window.location.href =
window.location.protocol ===
"hyperspace:"
? "hyperspace://hyperspace/app/"
: `https://${window.location.host}/#/`;
}}
button={true}
>
<ListItemAvatar>
<AccountCircleIcon color="action" />
</ListItemAvatar>
<ListItemText
primary={`@${account.username}`}
secondary={account.host}
/>
<ListItemSecondaryAction>
<IconButton
onClick={(e: any) => {
e.preventDefault();
removeAccountFromRegistry(index);
window.location.reload();
}}
>
<CloseIcon />
</IconButton>
</ListItemSecondaryAction>
</ListItem>
)
)}
</List>
<div className={classes.middlePadding} />
<Button
onClick={() => {
this.setState({ willAddAccount: true });
this.clear();
}}
color={"primary"}
variant={"contained"}
>
Add Account
</Button>
</div>
);
}
showLanding() {
const { classes } = this.props;
return (
@ -452,7 +534,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
onKeyDown={event => this.watchUsernameField(event)}
error={this.state.userInputError}
onBlur={() => this.checkForErrors()}
></TextField>
/>
{this.state.userInputError ? (
<Typography color="error">
{this.state.userInputErrorMessage}
@ -597,7 +679,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
this.updateAuthCode(event.target.value)
}
onKeyDown={event => this.watchAuthField(event)}
></TextField>
/>
</DialogContent>
<DialogActions>
<Button onClick={() => this.toggleAuthDialog()}>
@ -614,7 +696,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
);
}
showAuthority() {
showAuthorizationLoader() {
const { classes } = this.props;
return (
<div>
@ -659,11 +741,14 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
/>
<br />
<Fade in={true}>
{this.state.authority
? this.showAuthority()
: this.state.wantsToLogin
? this.showLoginAuth()
: this.showLanding()}
{this.state.authorizing
? this.showAuthorizationLoader()
: this.state.proceedToGetCode
? this.showLoginAuth()
: getAccountRegistry().length > 0 &&
!this.state.willAddAccount
? this.showMultiAccount()
: this.showLanding()}
</Fade>
<br />
<Typography variant="caption">
@ -741,4 +826,4 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
}
}
export default withStyles(styles)(withSnackbar(WelcomePage));
export default withStyles(styles)(withSnackbar(WelcomePage));

View File

@ -1,16 +1,12 @@
import React, { Component } from "react";
import {
withStyles,
Typography,
Paper,
Avatar,
Button,
CircularProgress,
Paper,
TextField,
ListItem,
ListItemText,
ListItemAvatar,
List,
Grid
Typography,
withStyles
} from "@material-ui/core";
import { withSnackbar, withSnackbarProps } from "notistack";
import { styles } from "./PageLayout.styles";
@ -18,16 +14,17 @@ import { Account } from "../types/Account";
import Mastodon from "megalodon";
import filedialog from "file-dialog";
import PersonIcon from "@material-ui/icons/Person";
interface IYouProps extends withSnackbarProps {
classes: any;
}
interface IYouState {
currentAccount: Account;
currentAccount?: Account;
newDisplayName?: string;
newBio?: string;
viewIsLoading: boolean;
viewLoaded: boolean;
viewErrored: boolean;
}
class You extends Component<IYouProps, IYouState> {
@ -42,12 +39,42 @@ class You extends Component<IYouProps, IYouState> {
);
this.state = {
currentAccount: this.getAccount()
viewIsLoading: true,
viewLoaded: false,
viewErrored: false
};
}
componentWillMount() {
this.client
.get("/accounts/verify_credentials")
.then((resp: any) => {
let currentAccount: Account = resp.data;
this.setState({
currentAccount,
viewIsLoading: false,
viewLoaded: true
});
})
.catch(() => {
if (this.getAccount()) {
this.setState({
currentAccount: this.getAccount(),
viewIsLoading: false,
viewLoaded: true
});
} else {
this.setState({
viewIsLoading: false,
viewErrored: true
});
}
});
}
getAccount() {
let acct = localStorage.getItem("account");
console.log(acct);
if (acct) {
return JSON.parse(acct);
}
@ -142,15 +169,16 @@ class You extends Component<IYouProps, IYouState> {
removeHTMLContent(text: string) {
const div = document.createElement("div");
div.innerHTML = text;
let innerContent = div.textContent || div.innerText || "";
return innerContent;
return div.textContent || div.innerText || "";
}
changeDisplayName() {
this.client
.patch("/accounts/update_credentials", {
display_name: this.state.newDisplayName
? this.state.newDisplayName
: this.state.currentAccount.display_name
: this.state.currentAccount
? this.state.currentAccount.display_name
: ""
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
@ -179,7 +207,9 @@ class You extends Component<IYouProps, IYouState> {
.patch("/accounts/update_credentials", {
note: this.state.newBio
? this.state.newBio
: this.state.currentAccount.note
: this.state.currentAccount
? this.state.currentAccount.note
: ""
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
@ -205,116 +235,155 @@ class You extends Component<IYouProps, IYouState> {
const { classes } = this.props;
return (
<div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentAccount.header_static}")`
}}
/>
<div className={classes.profileContent}>
<br />
<Avatar
className={classes.profileAvatar}
src={this.state.currentAccount.avatar_static}
/>
<div
className={classes.profileUserBox}
style={{ paddingTop: 8, paddingBottom: 8 }}
>
<Typography
variant="h4"
color="inherit"
component="h1"
>
Edit your profile
</Typography>
<Typography color="inherit">
Change information such as your display name,
bio, and images used here.
</Typography>
<div>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateAvatar()}
{this.state.viewErrored ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when trying to get your account
information.
</Typography>
</Paper>
) : (
<span />
)}
{this.state.currentAccount ? (
<div>
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentAccount.header_static}")`
}}
/>
<div className={classes.profileContent}>
<br />
<Avatar
className={classes.profileAvatar}
src={
this.state.currentAccount.avatar_static
}
/>
<div
className={classes.profileUserBox}
style={{ paddingTop: 8, paddingBottom: 8 }}
>
Change Avatar
</Button>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateHeader()}
>
Change Header
</Button>
<Typography
variant="h4"
color="inherit"
component="h1"
>
Edit your profile
</Typography>
<Typography color="inherit">
Change information such as your display
name, bio, and images used here.
</Typography>
<div>
<Button
className={
classes.pageProfileFollowButton
}
variant="contained"
onClick={() => this.updateAvatar()}
>
Change Avatar
</Button>
<Button
className={
classes.pageProfileFollowButton
}
variant="contained"
onClick={() => this.updateHeader()}
>
Change Header
</Button>
</div>
</div>
</div>
</div>
<div className={classes.pageContentLayoutConstraints}>
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
Display Name
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.display_name
}
rowsMax="1"
variant="outlined"
fullWidth
onChange={(event: any) =>
this.updateDisplayName(
event.target.value
)
}
/>
<div style={{ textAlign: "right" }}>
<Button
className={
classes.pageProfileFollowButton
}
color="primary"
onClick={() => this.changeDisplayName()}
>
Update display Name
</Button>
</div>
</Paper>
<br />
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
About you
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.note
? this.removeHTMLContent(
this.state.currentAccount.note
)
: "Tell a little bit about yourself"
}
multiline
variant="outlined"
rows="2"
rowsMax="5"
fullWidth
onChange={(event: any) =>
this.updateBio(event.target.value)
}
/>
<div style={{ textAlign: "right" }}>
<Button
className={
classes.pageProfileFollowButton
}
color="primary"
onClick={() => this.changeBio()}
>
Update biography
</Button>
</div>
</Paper>
</div>
</div>
</div>
<div className={classes.pageContentLayoutConstraints}>
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
Display Name
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.display_name
}
rowsMax="1"
variant="outlined"
fullWidth
onChange={(event: any) =>
this.updateDisplayName(event.target.value)
}
) : (
"AAA"
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeDisplayName()}
>
Update display Name
</Button>
</div>
</Paper>
<br />
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
About you
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.note
? this.removeHTMLContent(
this.state.currentAccount.note
)
: "Tell a little bit about yourself"
}
multiline
variant="outlined"
rows="2"
rowsMax="5"
fullWidth
onChange={(event: any) =>
this.updateBio(event.target.value)
}
/>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeBio()}
>
Update biography
</Button>
</div>
</Paper>
</div>
</div>
) : (
<span />
)}
</div>
);
}

View File

@ -26,9 +26,32 @@ export type Account = {
bot: boolean | null;
};
/**
* Watered-down type for Mastodon accounts
*/
export type UAccount = {
id: string;
acct: string;
display_name: string;
avatar_static: string;
};
/**
* Account type for use with multi-account support
*/
export type MultiAccount = {
/**
* The host name of the account (ex.: mastodon.social)
*/
host: string;
/**
* The username of the account (@test)
*/
username: string;
/**
* The access token generated from the login
*/
access_token: string;
};

View File

@ -1,25 +1,26 @@
import Mastodon from "megalodon";
import { MultiAccount, Account } from "../types/Account";
export function userLoggedIn(): boolean {
if (
localStorage.getItem("baseurl") &&
localStorage.getItem("access_token")
) {
return true;
} else {
return false;
}
return !!(
localStorage.getItem("baseurl") && localStorage.getItem("access_token")
);
}
export function refreshUserAccountData() {
let client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
let host = localStorage.getItem("baseurl") as string;
let token = localStorage.getItem("access_token") as string;
let client = new Mastodon(token, host + "/api/v1");
client
.get("/accounts/verify_credentials")
.then((resp: any) => {
localStorage.setItem("account", JSON.stringify(resp.data));
let account: Account = resp.data;
localStorage.setItem("account", JSON.stringify(account));
sessionStorage.setItem("id", account.id);
addAccountToRegistry(host, token, account.acct);
})
.catch((err: Error) => {
console.error(err.message);
@ -31,3 +32,111 @@ export function refreshUserAccountData() {
);
});
}
/**
* Set the access token and base URL to a given multi-account user.
* @param account The multi-account from localStorage to use
*/
export function loginWithAccount(account: MultiAccount) {
if (localStorage.getItem("access_token") !== null) {
console.info(
"Existing login detected. Removing and using assigned token..."
);
}
localStorage.setItem("access_token", account.access_token);
localStorage.setItem("baseurl", account.host);
}
/**
* Gets the account registry.
* @returns A list of accounts
*/
export function getAccountRegistry(): MultiAccount[] {
let accountRegistry: MultiAccount[] = [];
let accountRegistryString = localStorage.getItem("accountRegistry");
if (accountRegistryString !== null) {
accountRegistry = JSON.parse(accountRegistryString);
}
return accountRegistry;
}
/**
* Add an account to the multi-account registry if it doesn't exist already.
* @param base_url The base URL of the user (eg., the instance)
* @param access_token The access token for the user
* @param username The username of the user
*/
export function addAccountToRegistry(
base_url: string,
access_token: string,
username: string
) {
const newAccount: MultiAccount = {
host: base_url,
username,
access_token
};
let accountRegistry = getAccountRegistry();
const stringifiedRegistry = accountRegistry.map(account =>
JSON.stringify(account)
);
if (stringifiedRegistry.indexOf(JSON.stringify(newAccount)) === -1) {
accountRegistry.push(newAccount);
}
localStorage.setItem("accountRegistry", JSON.stringify(accountRegistry));
}
/**
* Remove an account from the multi-account registry, if possible
* @param accountIdentifier The index of the account from the registry or the MultiAccount object itself
*/
export function removeAccountFromRegistry(
accountIdentifier: number | MultiAccount
) {
let accountRegistry = getAccountRegistry();
if (typeof accountIdentifier === "number") {
if (accountRegistry.length > accountIdentifier) {
if (
localStorage.getItem("access_token") ===
accountRegistry[accountIdentifier].access_token
) {
localStorage.removeItem("baseurl");
localStorage.removeItem("access_token");
}
accountRegistry.splice(accountIdentifier);
} else {
console.log("Multi account index may be out of range");
}
} else {
const stringifiedRegistry = accountRegistry.map(account =>
JSON.stringify(account)
);
const stringifiedAccountId = JSON.stringify(accountIdentifier);
if (
stringifiedRegistry.indexOf(
JSON.stringify(stringifiedAccountId)
) !== -1
) {
if (
localStorage.getItem("access_token") ===
accountIdentifier.access_token
) {
localStorage.removeItem("baseurl");
localStorage.removeItem("access_token");
}
accountRegistry.splice(
stringifiedRegistry.indexOf(stringifiedAccountId)
);
}
}
localStorage.setItem("accountRegistry", JSON.stringify(accountRegistry));
}

View File

@ -47,3 +47,12 @@ export function getDarwinAccentColor(): number {
);
return themeInteger === "" ? -2 : parseInt(themeInteger);
}
/**
* Get the app component from the desktop app
*/
export function getElectronApp() {
const eWin = window as ElectronWindow;
const { remote } = eWin.require("electron");
return remote.app;
}

View File

@ -45,9 +45,7 @@ export function canSendNotifications() {
*/
export function sendNotificationRequest(title: string, body: string) {
if (canSendNotifications()) {
let notif = new Notification(title, {
body: body
});
let notif = new Notification(title, { body });
notif.onclick = () => {
window.focus();