Set up basic UI for server blocking
Signed-off-by: Marquis Kurt <software@marquiskurt.net>
This commit is contained in:
parent
ee2843eff4
commit
32f9940ea2
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"tabWidth": 4
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "hyperspace",
|
"name": "hyperspace",
|
||||||
"version": "1.0.0-beta6",
|
"version": "1.0.0-beta7",
|
||||||
"lockfileVersion": 1,
|
"lockfileVersion": 1,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -17778,6 +17778,12 @@
|
||||||
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
|
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
|
"prettier": {
|
||||||
|
"version": "1.18.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz",
|
||||||
|
"integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"pretty-bytes": {
|
"pretty-bytes": {
|
||||||
"version": "4.0.2",
|
"version": "4.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
|
||||||
|
|
|
@ -28,6 +28,7 @@
|
||||||
"megalodon": "^0.6.4",
|
"megalodon": "^0.6.4",
|
||||||
"moment": "^2.24.0",
|
"moment": "^2.24.0",
|
||||||
"notistack": "^0.5.1",
|
"notistack": "^0.5.1",
|
||||||
|
"prettier": "^1.18.2",
|
||||||
"query-string": "^6.8.2",
|
"query-string": "^6.8.2",
|
||||||
"react": "^16.8.6",
|
"react": "^16.8.6",
|
||||||
"react-dom": "^16.8.6",
|
"react-dom": "^16.8.6",
|
||||||
|
|
|
@ -1,21 +1,24 @@
|
||||||
import { Theme, createStyles } from "@material-ui/core";
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
import { isDarwinApp } from './utilities/desktop';
|
import { isDarwinApp } from "./utilities/desktop";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
minHeight: '100vh',
|
minHeight: "100vh",
|
||||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.default,
|
backgroundColor: isDarwinApp()
|
||||||
|
? "transparent"
|
||||||
|
: theme.palette.background.default
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
marginTop: 72,
|
marginTop: 72,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250,
|
||||||
marginTop: 88,
|
marginTop: 88
|
||||||
},
|
}
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
176
src/App.tsx
176
src/App.tsx
|
@ -1,94 +1,110 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {MuiThemeProvider, CssBaseline, withStyles } from '@material-ui/core';
|
import { MuiThemeProvider, CssBaseline, withStyles } from "@material-ui/core";
|
||||||
import { setHyperspaceTheme, darkMode } from './utilities/themes';
|
import { setHyperspaceTheme, darkMode } from "./utilities/themes";
|
||||||
import AppLayout from './components/AppLayout';
|
import AppLayout from "./components/AppLayout";
|
||||||
import {styles} from './App.styles';
|
import { styles } from "./App.styles";
|
||||||
import {Route} from 'react-router-dom';
|
import { Route } from "react-router-dom";
|
||||||
import AboutPage from './pages/About';
|
import AboutPage from "./pages/About";
|
||||||
import Settings from './pages/Settings';
|
import Settings from "./pages/Settings";
|
||||||
import { getUserDefaultBool, getUserDefaultTheme } from './utilities/settings';
|
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
|
||||||
import ProfilePage from './pages/ProfilePage';
|
import ProfilePage from "./pages/ProfilePage";
|
||||||
import HomePage from './pages/Home';
|
import HomePage from "./pages/Home";
|
||||||
import LocalPage from './pages/Local';
|
import LocalPage from "./pages/Local";
|
||||||
import PublicPage from './pages/Public';
|
import PublicPage from "./pages/Public";
|
||||||
import Conversation from './pages/Conversation';
|
import Conversation from "./pages/Conversation";
|
||||||
import NotificationsPage from './pages/Notifications';
|
import NotificationsPage from "./pages/Notifications";
|
||||||
import SearchPage from './pages/Search';
|
import SearchPage from "./pages/Search";
|
||||||
import Composer from './pages/Compose';
|
import Composer from "./pages/Compose";
|
||||||
import WelcomePage from './pages/Welcome';
|
import WelcomePage from "./pages/Welcome";
|
||||||
import MessagesPage from './pages/Messages';
|
import MessagesPage from "./pages/Messages";
|
||||||
import RecommendationsPage from './pages/Recommendations';
|
import RecommendationsPage from "./pages/Recommendations";
|
||||||
import Missingno from './pages/Missingno';
|
import Missingno from "./pages/Missingno";
|
||||||
import You from './pages/You';
|
import You from "./pages/You";
|
||||||
import {withSnackbar} from 'notistack';
|
import Blocked from "./pages/Blocked";
|
||||||
import {PrivateRoute} from './interfaces/overrides';
|
import { withSnackbar } from "notistack";
|
||||||
import { userLoggedIn } from './utilities/accounts';
|
import { PrivateRoute } from "./interfaces/overrides";
|
||||||
import { isDarwinApp } from './utilities/desktop';
|
import { userLoggedIn } from "./utilities/accounts";
|
||||||
|
import { isDarwinApp } from "./utilities/desktop";
|
||||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||||
|
|
||||||
class App extends Component<any, any> {
|
class App extends Component<any, any> {
|
||||||
|
offline: any;
|
||||||
|
|
||||||
offline: any;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
theme: theme
|
||||||
|
};
|
||||||
this.state = {
|
|
||||||
theme: theme
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
let newTheme = darkMode(this.state.theme, getUserDefaultBool('darkModeEnabled'));
|
let newTheme = darkMode(
|
||||||
this.setState({ theme: newTheme });
|
this.state.theme,
|
||||||
}
|
getUserDefaultBool("darkModeEnabled")
|
||||||
|
);
|
||||||
componentDidMount() {
|
this.setState({ theme: newTheme });
|
||||||
this.removeBodyBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidUpdate() {
|
|
||||||
this.removeBodyBackground()
|
|
||||||
}
|
|
||||||
|
|
||||||
removeBodyBackground() {
|
|
||||||
if (isDarwinApp()) {
|
|
||||||
document.body.style.backgroundColor = "transparent";
|
|
||||||
console.log("Changed!")
|
|
||||||
console.log(`New color: ${document.body.style.backgroundColor}`)
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
componentDidMount() {
|
||||||
const { classes } = this.props;
|
this.removeBodyBackground();
|
||||||
|
}
|
||||||
|
|
||||||
this.removeBodyBackground()
|
componentDidUpdate() {
|
||||||
|
this.removeBodyBackground();
|
||||||
return (
|
}
|
||||||
<MuiThemeProvider theme={this.state.theme}>
|
|
||||||
<CssBaseline/>
|
|
||||||
<Route path="/welcome" component={WelcomePage}/>
|
|
||||||
<div>
|
|
||||||
{ userLoggedIn()? <AppLayout/>: null}
|
|
||||||
<PrivateRoute exact path="/" component={HomePage}/>
|
|
||||||
<PrivateRoute path="/home" component={HomePage}/>
|
|
||||||
<PrivateRoute path="/local" component={LocalPage}/>
|
|
||||||
<PrivateRoute path="/public" component={PublicPage}/>
|
|
||||||
<PrivateRoute path="/messages" component={MessagesPage}/>
|
|
||||||
<PrivateRoute path="/notifications" component={NotificationsPage}/>
|
|
||||||
<PrivateRoute path="/profile/:profileId" component={ProfilePage}/>
|
|
||||||
<PrivateRoute path="/conversation/:conversationId" component={Conversation}/>
|
|
||||||
<PrivateRoute path="/search" component={SearchPage}/>
|
|
||||||
<PrivateRoute path="/settings" component={Settings}/>
|
|
||||||
<PrivateRoute path="/you" component={You}/>
|
|
||||||
<PrivateRoute path="/about" component={AboutPage}/>
|
|
||||||
<PrivateRoute path="/compose" component={Composer}/>
|
|
||||||
<PrivateRoute path="/recommended" component={RecommendationsPage}/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</MuiThemeProvider>
|
removeBodyBackground() {
|
||||||
);
|
if (isDarwinApp()) {
|
||||||
}
|
document.body.style.backgroundColor = "transparent";
|
||||||
|
console.log("Changed!");
|
||||||
|
console.log(`New color: ${document.body.style.backgroundColor}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
|
||||||
|
this.removeBodyBackground();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MuiThemeProvider theme={this.state.theme}>
|
||||||
|
<CssBaseline />
|
||||||
|
<Route path="/welcome" component={WelcomePage} />
|
||||||
|
<div>
|
||||||
|
{userLoggedIn() ? <AppLayout /> : null}
|
||||||
|
<PrivateRoute exact path="/" component={HomePage} />
|
||||||
|
<PrivateRoute path="/home" component={HomePage} />
|
||||||
|
<PrivateRoute path="/local" component={LocalPage} />
|
||||||
|
<PrivateRoute path="/public" component={PublicPage} />
|
||||||
|
<PrivateRoute path="/messages" component={MessagesPage} />
|
||||||
|
<PrivateRoute
|
||||||
|
path="/notifications"
|
||||||
|
component={NotificationsPage}
|
||||||
|
/>
|
||||||
|
<PrivateRoute
|
||||||
|
path="/profile/:profileId"
|
||||||
|
component={ProfilePage}
|
||||||
|
/>
|
||||||
|
<PrivateRoute
|
||||||
|
path="/conversation/:conversationId"
|
||||||
|
component={Conversation}
|
||||||
|
/>
|
||||||
|
<PrivateRoute path="/search" component={SearchPage} />
|
||||||
|
<PrivateRoute path="/blocked" component={Blocked} />
|
||||||
|
<PrivateRoute path="/settings" component={Settings} />
|
||||||
|
|
||||||
|
<PrivateRoute path="/you" component={You} />
|
||||||
|
<PrivateRoute path="/about" component={AboutPage} />
|
||||||
|
<PrivateRoute path="/compose" component={Composer} />
|
||||||
|
<PrivateRoute
|
||||||
|
path="/recommended"
|
||||||
|
component={RecommendationsPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(App));
|
export default withStyles(styles)(withSnackbar(App));
|
||||||
|
|
|
@ -1,32 +1,35 @@
|
||||||
import { Theme, createStyles } from "@material-ui/core";
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
import { darken } from "@material-ui/core/styles/colorManipulator";
|
import { darken } from "@material-ui/core/styles/colorManipulator";
|
||||||
import { isDarwinApp } from '../../utilities/desktop';
|
import { isDarwinApp } from "../../utilities/desktop";
|
||||||
|
|
||||||
import { fade } from "@material-ui/core/styles/colorManipulator";
|
import { fade } from "@material-ui/core/styles/colorManipulator";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
display: 'flex',
|
display: "flex"
|
||||||
},
|
},
|
||||||
stickyArea: {
|
stickyArea: {
|
||||||
position: 'fixed',
|
position: "fixed",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
zIndex: 1000,
|
zIndex: 1000
|
||||||
},
|
},
|
||||||
titleBarRoot: {
|
titleBarRoot: {
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
backgroundColor: isDarwinApp()? theme.palette.primary.main: theme.palette.primary.dark,
|
backgroundColor: isDarwinApp()
|
||||||
textAlign: 'center',
|
? theme.palette.primary.main
|
||||||
|
: theme.palette.primary.dark,
|
||||||
|
textAlign: "center",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: "middle",
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: "none",
|
||||||
WebkitAppRegion: "drag",
|
WebkitAppRegion: "drag"
|
||||||
},
|
},
|
||||||
titleBarText: {
|
titleBarText: {
|
||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white,
|
||||||
|
@ -36,83 +39,83 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
},
|
},
|
||||||
appBar: {
|
appBar: {
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
backgroundImage: isDarwinApp()? `linear-gradient(${theme.palette.primary.main}, ${theme.palette.primary.dark})`: undefined,
|
backgroundImage: isDarwinApp()
|
||||||
|
? `linear-gradient(${theme.palette.primary.main}, ${theme.palette.primary.dark})`
|
||||||
|
: undefined,
|
||||||
backgroundColor: theme.palette.primary.main,
|
backgroundColor: theme.palette.primary.main,
|
||||||
borderBottomColor: darken(theme.palette.primary.dark, 0.2),
|
borderBottomColor: darken(theme.palette.primary.dark, 0.2),
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomStyle: isDarwinApp()? "solid": "none",
|
borderBottomStyle: isDarwinApp() ? "solid" : "none",
|
||||||
boxShadow: isDarwinApp()? "none": "inherit"
|
boxShadow: isDarwinApp() ? "none" : "inherit"
|
||||||
},
|
},
|
||||||
appBarMenuButton: {
|
appBarMenuButton: {
|
||||||
marginLeft: -12,
|
marginLeft: -12,
|
||||||
marginRight: 20,
|
marginRight: 20,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
display: 'none'
|
display: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
appBarTitle: {
|
appBarTitle: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
display: 'block',
|
display: "block"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
appBarSearch: {
|
appBarSearch: {
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
borderRadius: theme.shape.borderRadius,
|
borderRadius: theme.shape.borderRadius,
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: fade(theme.palette.common.white, 0.25)
|
backgroundColor: fade(theme.palette.common.white, 0.25)
|
||||||
},
|
},
|
||||||
width: '100%',
|
width: "100%",
|
||||||
marginLeft: 0,
|
marginLeft: 0,
|
||||||
marginRight: theme.spacing.unit,
|
marginRight: theme.spacing.unit,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: theme.spacing.unit * 6,
|
marginLeft: theme.spacing.unit * 6,
|
||||||
width: '50%'
|
width: "50%"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
appBarSearchIcon: {
|
appBarSearchIcon: {
|
||||||
width: theme.spacing.unit * 9,
|
width: theme.spacing.unit * 9,
|
||||||
height: '100%',
|
height: "100%",
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
pointerEvents: 'none',
|
pointerEvents: "none",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
alignItems: 'center',
|
alignItems: "center",
|
||||||
justifyContent: 'center'
|
justifyContent: "center"
|
||||||
},
|
},
|
||||||
appBarSearchInputRoot: {
|
appBarSearchInputRoot: {
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
width: '100%'
|
width: "100%"
|
||||||
},
|
},
|
||||||
appBarSearchInputInput: {
|
appBarSearchInputInput: {
|
||||||
paddingTop: theme.spacing.unit,
|
paddingTop: theme.spacing.unit,
|
||||||
paddingBottom: theme.spacing.unit,
|
paddingBottom: theme.spacing.unit,
|
||||||
paddingLeft: theme.spacing.unit * 10,
|
paddingLeft: theme.spacing.unit * 10,
|
||||||
paddingRight: theme.spacing.unit,
|
paddingRight: theme.spacing.unit,
|
||||||
transition: theme.transitions.create('width'),
|
transition: theme.transitions.create("width"),
|
||||||
width: '100%',
|
width: "100%"
|
||||||
},
|
},
|
||||||
appBarFlexGrow: {
|
appBarFlexGrow: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
appBarActionButtons: {
|
appBarActionButtons: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: 'flex',
|
display: "flex"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
appBarAcctMenuIcon: {
|
appBarAcctMenuIcon: {
|
||||||
backgroundColor: theme.palette.primary.dark
|
backgroundColor: theme.palette.primary.dark
|
||||||
},
|
},
|
||||||
acctMenu: {
|
acctMenu: {},
|
||||||
|
|
||||||
},
|
|
||||||
drawer: {
|
drawer: {
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
width: 250,
|
width: 250,
|
||||||
flexShrink: 0
|
flexShrink: 0
|
||||||
},
|
},
|
||||||
zIndex: 1,
|
zIndex: 1
|
||||||
},
|
},
|
||||||
drawerPaper: {
|
drawerPaper: {
|
||||||
width: 250,
|
width: 250,
|
||||||
|
@ -122,43 +125,47 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
width: 250,
|
width: 250,
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
marginTop: 64,
|
marginTop: 64,
|
||||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.paper
|
backgroundColor: isDarwinApp()
|
||||||
|
? "transparent"
|
||||||
|
: theme.palette.background.paper
|
||||||
},
|
},
|
||||||
drawerPaperWithTitleAndAppBar: {
|
drawerPaperWithTitleAndAppBar: {
|
||||||
width: 250,
|
width: 250,
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
marginTop: 88,
|
marginTop: 88,
|
||||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.paper
|
backgroundColor: isDarwinApp()
|
||||||
|
? "transparent"
|
||||||
|
: theme.palette.background.paper
|
||||||
},
|
},
|
||||||
drawerDisplayMobile: {
|
drawerDisplayMobile: {
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
display: 'none'
|
display: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toolbar: theme.mixins.toolbar,
|
toolbar: theme.mixins.toolbar,
|
||||||
sectionDesktop: {
|
sectionDesktop: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
display: 'flex',
|
display: "flex"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
sectionMobile: {
|
sectionMobile: {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
display: 'none',
|
display: "none"
|
||||||
},
|
}
|
||||||
},
|
},
|
||||||
content: {
|
content: {
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: 250
|
marginLeft: 250
|
||||||
},
|
},
|
||||||
overflowY: 'auto',
|
overflowY: "auto"
|
||||||
},
|
},
|
||||||
composeButton: {
|
composeButton: {
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
bottom: theme.spacing.unit * 2,
|
bottom: theme.spacing.unit * 2,
|
||||||
right: theme.spacing.unit * 2,
|
right: theme.spacing.unit * 2,
|
||||||
zIndex: 50
|
zIndex: 50
|
||||||
},
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,297 +1,416 @@
|
||||||
import React, { Component } from 'react';
|
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 {
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
Typography,
|
||||||
import SearchIcon from '@material-ui/icons/Search';
|
AppBar,
|
||||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
Toolbar,
|
||||||
import MailIcon from '@material-ui/icons/Mail';
|
IconButton,
|
||||||
import HomeIcon from '@material-ui/icons/Home';
|
InputBase,
|
||||||
import DomainIcon from '@material-ui/icons/Domain';
|
Avatar,
|
||||||
import PublicIcon from '@material-ui/icons/Public';
|
ListItemText,
|
||||||
import GroupIcon from '@material-ui/icons/Group';
|
Divider,
|
||||||
import SettingsIcon from '@material-ui/icons/Settings';
|
List,
|
||||||
import InfoIcon from '@material-ui/icons/Info';
|
ListItemIcon,
|
||||||
import CreateIcon from '@material-ui/icons/Create';
|
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 SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
|
||||||
import {styles} from './AppLayout.styles';
|
import { styles } from "./AppLayout.styles";
|
||||||
import { UAccount } from '../../types/Account';
|
import { UAccount } from "../../types/Account";
|
||||||
import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interfaces/overrides';
|
import {
|
||||||
import Mastodon from 'megalodon';
|
LinkableListItem,
|
||||||
import { Notification } from '../../types/Notification';
|
LinkableIconButton,
|
||||||
import {sendNotificationRequest} from '../../utilities/notifications';
|
LinkableFab
|
||||||
import {withSnackbar} from 'notistack';
|
} from "../../interfaces/overrides";
|
||||||
import { getConfig, getUserDefaultBool } from '../../utilities/settings';
|
import Mastodon from "megalodon";
|
||||||
import { isDesktopApp, isDarwinApp } from '../../utilities/desktop';
|
import { Notification } from "../../types/Notification";
|
||||||
import { Config } from '../../types/Config';
|
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 {
|
interface IAppLayoutState {
|
||||||
acctMenuOpen: boolean;
|
acctMenuOpen: boolean;
|
||||||
drawerOpenOnMobile: boolean;
|
drawerOpenOnMobile: boolean;
|
||||||
currentUser?: UAccount;
|
currentUser?: UAccount;
|
||||||
notificationCount: number;
|
notificationCount: number;
|
||||||
logOutOpen: boolean;
|
logOutOpen: boolean;
|
||||||
enableFederation?: boolean;
|
enableFederation?: boolean;
|
||||||
brandName?: string;
|
brandName?: string;
|
||||||
developerMode?: boolean;
|
developerMode?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AppLayout extends Component<any, IAppLayoutState> {
|
export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
|
client: Mastodon;
|
||||||
client: Mastodon;
|
streamListener: any;
|
||||||
streamListener: any;
|
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
this.client = new Mastodon(
|
||||||
|
localStorage.getItem("access_token") as string,
|
||||||
this.state = {
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
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() {
|
this.state = {
|
||||||
|
drawerOpenOnMobile: false,
|
||||||
let acct = localStorage.getItem('account');
|
acctMenuOpen: false,
|
||||||
if (acct) {
|
notificationCount: 0,
|
||||||
this.setState({ currentUser: JSON.parse(acct) });
|
logOutOpen: false
|
||||||
} 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) => {
|
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
||||||
if (result !== undefined) {
|
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
||||||
let config: Config = result;
|
this.clearBadge = this.clearBadge.bind(this);
|
||||||
this.setState({
|
}
|
||||||
enableFederation: config.federation.enablePublicTimeline,
|
|
||||||
brandName: config.branding? config.branding.name: "Hyperspace",
|
componentDidMount() {
|
||||||
developerMode: config.developer
|
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.streamNotifications()
|
getConfig().then((result: any) => {
|
||||||
|
if (result !== undefined) {
|
||||||
}
|
let config: Config = result;
|
||||||
|
this.setState({
|
||||||
streamNotifications() {
|
enableFederation: config.federation.enablePublicTimeline,
|
||||||
this.streamListener = this.client.stream('/streaming/user');
|
brandName: config.branding ? config.branding.name : "Hyperspace",
|
||||||
|
developerMode: config.developer
|
||||||
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.streamNotifications();
|
||||||
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
}
|
||||||
}
|
|
||||||
|
|
||||||
toggleDrawerOnMobile() {
|
streamNotifications() {
|
||||||
this.setState({
|
this.streamListener = this.client.stream("/streaming/user");
|
||||||
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleLogOutDialog() {
|
if (getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||||
this.setState({ logOutOpen: !this.state.logOutOpen });
|
this.client.get("/notifications").then((resp: any) => {
|
||||||
}
|
let notifArray = resp.data;
|
||||||
|
this.setState({ notificationCount: notifArray.length });
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
searchForQuery(what: string) {
|
this.streamListener.on("notification", (notif: Notification) => {
|
||||||
window.location.href = isDesktopApp()? "hyperspace://hyperspace/app/index.html#/search?query=" + what: "/#/search?query=" + what;
|
const notificationCount = this.state.notificationCount + 1;
|
||||||
window.location.reload;
|
this.setState({ notificationCount });
|
||||||
}
|
if (!document.hasFocus()) {
|
||||||
|
let primaryMessage = "";
|
||||||
|
let secondaryMessage = "";
|
||||||
|
|
||||||
logOutAndRestart() {
|
switch (notif.type) {
|
||||||
let loginData = localStorage.getItem("login");
|
case "favourite":
|
||||||
if (loginData) {
|
primaryMessage =
|
||||||
let items = ["login", "account", "baseurl", "access_token"];
|
(notif.account.display_name || "@" + notif.account.username) +
|
||||||
items.forEach((entry) => {
|
" favorited your post.";
|
||||||
localStorage.removeItem(entry);
|
if (notif.status) {
|
||||||
})
|
const div = document.createElement("div");
|
||||||
window.location.reload();
|
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;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
clearBadge() {
|
sendNotificationRequest(primaryMessage, secondaryMessage);
|
||||||
if (!getUserDefaultBool('displayAllOnNotificationBadge')) {
|
|
||||||
this.setState({ notificationCount: 0 });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
titlebar() {
|
toggleAcctMenu() {
|
||||||
const { classes } = this.props;
|
this.setState({ acctMenuOpen: !this.state.acctMenuOpen });
|
||||||
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() {
|
toggleDrawerOnMobile() {
|
||||||
const { classes } = this.props;
|
this.setState({
|
||||||
return (
|
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
||||||
<div>
|
});
|
||||||
<List>
|
}
|
||||||
<div className={classes.drawerDisplayMobile}>
|
|
||||||
<LinkableListItem button key="profile-mobile" to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
|
toggleLogOutDialog() {
|
||||||
<ListItemAvatar>
|
this.setState({ logOutOpen: !this.state.logOutOpen });
|
||||||
<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..."}/>
|
searchForQuery(what: string) {
|
||||||
</LinkableListItem>
|
window.location.href = isDesktopApp()
|
||||||
{/* <LinkableListItem button key="acctSwitch-module" to="/switchacct">
|
? "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>
|
<ListItemIcon><SupervisedUserCircleIcon/></ListItemIcon>
|
||||||
<ListItemText primary="Switch account"/>
|
<ListItemText primary="Switch account"/>
|
||||||
</LinkableListItem> */}
|
</LinkableListItem> */}
|
||||||
<ListItem button key="acctLogout-mobile" onClick={() => this.toggleLogOutDialog()}>
|
<ListItem
|
||||||
<ListItemIcon><ExitToAppIcon/></ListItemIcon>
|
button
|
||||||
<ListItemText primary="Log out"/>
|
key="acctLogout-mobile"
|
||||||
</ListItem>
|
onClick={() => this.toggleLogOutDialog()}
|
||||||
<Divider/>
|
>
|
||||||
</div>
|
<ListItemIcon>
|
||||||
<ListSubheader>Timelines</ListSubheader>
|
<ExitToAppIcon />
|
||||||
<LinkableListItem button key="home" to="/home">
|
</ListItemIcon>
|
||||||
<ListItemIcon><HomeIcon/></ListItemIcon>
|
<ListItemText primary="Log out" />
|
||||||
<ListItemText primary="Home"/>
|
</ListItem>
|
||||||
</LinkableListItem>
|
<Divider />
|
||||||
<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>
|
</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() {
|
render() {
|
||||||
const { classes } = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<div className={classes.root}>
|
<div className={classes.root}>
|
||||||
<div className={classes.stickyArea}>
|
<div className={classes.stickyArea}>
|
||||||
{this.titlebar()}
|
{this.titlebar()}
|
||||||
<AppBar className={classes.appBar} position="static">
|
<AppBar className={classes.appBar} position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<IconButton
|
<IconButton
|
||||||
className={classes.appBarMenuButton}
|
className={classes.appBarMenuButton}
|
||||||
color="inherit"
|
color="inherit"
|
||||||
aria-label="Open drawer"
|
aria-label="Open drawer"
|
||||||
onClick={this.toggleDrawerOnMobile}
|
onClick={this.toggleDrawerOnMobile}
|
||||||
>
|
>
|
||||||
<MenuIcon/>
|
<MenuIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Typography className={classes.appBarTitle} variant="h6" color="inherit" noWrap>
|
<Typography
|
||||||
{this.state.brandName? this.state.brandName: "Hyperspace"}
|
className={classes.appBarTitle}
|
||||||
|
variant="h6"
|
||||||
|
color="inherit"
|
||||||
|
noWrap
|
||||||
|
>
|
||||||
|
{this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||||
</Typography>
|
</Typography>
|
||||||
<div className={classes.appBarFlexGrow}/>
|
<div className={classes.appBarFlexGrow} />
|
||||||
<div className={classes.appBarSearch}>
|
<div className={classes.appBarSearch}>
|
||||||
<div className={classes.appBarSearchIcon}>
|
<div className={classes.appBarSearchIcon}>
|
||||||
<SearchIcon/>
|
<SearchIcon />
|
||||||
</div>
|
</div>
|
||||||
<InputBase
|
<InputBase
|
||||||
placeholder="Search..."
|
placeholder="Search..."
|
||||||
|
@ -299,112 +418,176 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
||||||
root: classes.appBarSearchInputRoot,
|
root: classes.appBarSearchInputRoot,
|
||||||
input: classes.appBarSearchInputInput
|
input: classes.appBarSearchInputInput
|
||||||
}}
|
}}
|
||||||
onKeyUp={(event) => {
|
onKeyUp={event => {
|
||||||
if (event.keyCode === 13) {
|
if (event.keyCode === 13) {
|
||||||
this.searchForQuery(event.currentTarget.value);
|
this.searchForQuery(event.currentTarget.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className={classes.appBarFlexGrow}/>
|
<div className={classes.appBarFlexGrow} />
|
||||||
<div className={classes.appBarActionButtons}>
|
<div className={classes.appBarActionButtons}>
|
||||||
<Tooltip title="Notifications">
|
<Tooltip title="Notifications">
|
||||||
<LinkableIconButton color="inherit" to="/notifications" onClick={this.clearBadge}>
|
<LinkableIconButton
|
||||||
<Badge badgeContent={this.state.notificationCount > 0? this.state.notificationCount: ""} color="secondary">
|
color="inherit"
|
||||||
<NotificationsIcon />
|
to="/notifications"
|
||||||
</Badge>
|
onClick={this.clearBadge}
|
||||||
</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}>
|
<Badge
|
||||||
<div>
|
badgeContent={
|
||||||
<LinkableListItem to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
|
this.state.notificationCount > 0
|
||||||
<ListItemAvatar>
|
? this.state.notificationCount
|
||||||
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
|
: ""
|
||||||
</ListItemAvatar>
|
}
|
||||||
<ListItemText
|
color="secondary"
|
||||||
primary={this.state.currentUser? (this.state.currentUser.display_name || this.state.currentUser.acct): "Loading..."}
|
>
|
||||||
secondary={'@' + (this.state.currentUser? this.state.currentUser.acct: "Loading...")}
|
<NotificationsIcon />
|
||||||
/>
|
</Badge>
|
||||||
</LinkableListItem>
|
</LinkableIconButton>
|
||||||
<Divider/>
|
</Tooltip>
|
||||||
{/* <MenuItem>Switch account</MenuItem> */}
|
<Tooltip title="Direct messages">
|
||||||
<MenuItem onClick={() => this.toggleLogOutDialog()}>Log out</MenuItem>
|
<LinkableIconButton color="inherit" to="/messages">
|
||||||
</div>
|
<MailIcon />
|
||||||
</ClickAwayListener>
|
</LinkableIconButton>
|
||||||
</Menu>
|
</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 />
|
||||||
|
{/* <MenuItem>Switch account</MenuItem> */}
|
||||||
|
<MenuItem onClick={() => this.toggleLogOutDialog()}>
|
||||||
|
Log out
|
||||||
|
</MenuItem>
|
||||||
|
</div>
|
||||||
|
</ClickAwayListener>
|
||||||
|
</Menu>
|
||||||
</div>
|
</div>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
</AppBar>
|
</AppBar>
|
||||||
<nav className={classes.drawer}>
|
<nav className={classes.drawer}>
|
||||||
<Hidden mdUp implementation="css">
|
<Hidden mdUp implementation="css">
|
||||||
<Drawer
|
<Drawer
|
||||||
container={this.props.container}
|
container={this.props.container}
|
||||||
variant="temporary"
|
variant="temporary"
|
||||||
anchor={'left'}
|
anchor={"left"}
|
||||||
open={this.state.drawerOpenOnMobile}
|
open={this.state.drawerOpenOnMobile}
|
||||||
onClose={this.toggleDrawerOnMobile}
|
onClose={this.toggleDrawerOnMobile}
|
||||||
classes={{ paper: classes.drawerPaper }}
|
classes={{ paper: classes.drawerPaper }}
|
||||||
>
|
>
|
||||||
{this.appDrawer()}
|
{this.appDrawer()}
|
||||||
</Drawer>
|
</Drawer>
|
||||||
</Hidden>
|
</Hidden>
|
||||||
<Hidden smDown implementation="css">
|
<Hidden smDown implementation="css">
|
||||||
<Drawer
|
<Drawer
|
||||||
classes={{
|
classes={{
|
||||||
paper: this.titlebar()? classes.drawerPaperWithTitleAndAppBar: classes.drawerPaperWithAppBar
|
paper: this.titlebar()
|
||||||
}}
|
? classes.drawerPaperWithTitleAndAppBar
|
||||||
variant="permanent"
|
: classes.drawerPaperWithAppBar
|
||||||
open
|
}}
|
||||||
>
|
variant="permanent"
|
||||||
{this.appDrawer()}
|
open
|
||||||
</Drawer>
|
>
|
||||||
|
{this.appDrawer()}
|
||||||
|
</Drawer>
|
||||||
</Hidden>
|
</Hidden>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.logOutOpen}
|
open={this.state.logOutOpen}
|
||||||
onClose={() => this.toggleLogOutDialog()}
|
onClose={() => this.toggleLogOutDialog()}
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">Log out of {this.state.brandName? this.state.brandName: "Hyperspace"}</DialogTitle>
|
<DialogTitle id="alert-dialog-title">
|
||||||
|
Log out of{" "}
|
||||||
|
{this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">
|
<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"}.
|
You'll need to remove{" "}
|
||||||
</DialogContentText>
|
{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>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => this.toggleLogOutDialog()} color="primary" autoFocus>
|
<Button
|
||||||
|
onClick={() => this.toggleLogOutDialog()}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => {
|
<Button
|
||||||
this.logOutAndRestart();
|
onClick={() => {
|
||||||
}} color="primary">
|
this.logOutAndRestart();
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
Log out
|
Log out
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<Tooltip title="Create a new post">
|
<Tooltip title="Create a new post">
|
||||||
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
|
<LinkableFab
|
||||||
<CreateIcon/>
|
to="/compose"
|
||||||
|
className={classes.composeButton}
|
||||||
|
color="secondary"
|
||||||
|
aria-label="Compose"
|
||||||
|
>
|
||||||
|
<CreateIcon />
|
||||||
</LinkableFab>
|
</LinkableFab>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { AppLayout } from './AppLayout';
|
import { AppLayout } from "./AppLayout";
|
||||||
import { withStyles } from '@material-ui/core';
|
import { withStyles } from "@material-ui/core";
|
||||||
import { styles } from './AppLayout.styles';
|
import { styles } from "./AppLayout.styles";
|
||||||
import {withSnackbar} from 'notistack';
|
import { withSnackbar } from "notistack";
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(AppLayout));
|
export default withStyles(styles)(withSnackbar(AppLayout));
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
import { Theme, createStyles } from '@material-ui/core';
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
mediaContainer: {
|
mediaContainer: {
|
||||||
padding: theme.spacing.unit * 2,
|
padding: theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
mediaObject: {
|
mediaObject: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%'
|
height: "100%"
|
||||||
},
|
},
|
||||||
mediaSlide: {
|
mediaSlide: {
|
||||||
backgroundColor: theme.palette.primary.light,
|
backgroundColor: theme.palette.primary.light,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: 'auto'
|
height: "auto"
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,105 +1,141 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {withStyles, Typography, MobileStepper, Button} from '@material-ui/core';
|
import {
|
||||||
import { styles } from './Attachment.styles';
|
withStyles,
|
||||||
import { Attachment } from '../../types/Attachment';
|
Typography,
|
||||||
import SwipeableViews from 'react-swipeable-views';
|
MobileStepper,
|
||||||
|
Button
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./Attachment.styles";
|
||||||
|
import { Attachment } from "../../types/Attachment";
|
||||||
|
import SwipeableViews from "react-swipeable-views";
|
||||||
|
|
||||||
interface IAttachmentProps {
|
interface IAttachmentProps {
|
||||||
media: [Attachment];
|
media: [Attachment];
|
||||||
classes?: any;
|
classes?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAttachmentState {
|
interface IAttachmentState {
|
||||||
totalSteps: number;
|
totalSteps: number;
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
attachments: [Attachment];
|
attachments: [Attachment];
|
||||||
}
|
}
|
||||||
|
|
||||||
class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState> {
|
class AttachmentComponent extends Component<
|
||||||
constructor(props: IAttachmentProps) {
|
IAttachmentProps,
|
||||||
super(props);
|
IAttachmentState
|
||||||
|
> {
|
||||||
|
constructor(props: IAttachmentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
attachments: this.props.media,
|
attachments: this.props.media,
|
||||||
totalSteps: this.props.media.length,
|
totalSteps: this.props.media.length,
|
||||||
currentStep: 0
|
currentStep: 0
|
||||||
}
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
moveBack() {
|
||||||
|
let nextStep = this.state.currentStep - 1;
|
||||||
|
if (nextStep < 0) {
|
||||||
|
nextStep = 0;
|
||||||
}
|
}
|
||||||
|
this.setState({ currentStep: nextStep });
|
||||||
|
}
|
||||||
|
|
||||||
moveBack() {
|
moveForward() {
|
||||||
let nextStep = this.state.currentStep - 1;
|
let nextStep = this.state.currentStep + 1;
|
||||||
if (nextStep < 0) {
|
if (nextStep > this.state.totalSteps) {
|
||||||
nextStep = 0;
|
nextStep = this.state.totalSteps;
|
||||||
}
|
|
||||||
this.setState({ currentStep: nextStep });
|
|
||||||
}
|
}
|
||||||
|
this.setState({ currentStep: nextStep });
|
||||||
|
}
|
||||||
|
|
||||||
moveForward() {
|
handleStepChange(currentStep: number) {
|
||||||
let nextStep = this.state.currentStep + 1;
|
this.setState({
|
||||||
if (nextStep > this.state.totalSteps) {
|
currentStep
|
||||||
nextStep = this.state.totalSteps;
|
});
|
||||||
}
|
}
|
||||||
this.setState({ currentStep: nextStep });
|
|
||||||
|
|
||||||
}
|
getSlide(slide: Attachment) {
|
||||||
|
const { classes } = this.props;
|
||||||
handleStepChange(currentStep: number) {
|
switch (slide.type) {
|
||||||
this.setState({
|
case "image":
|
||||||
currentStep
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
getSlide(slide: Attachment) {
|
|
||||||
const {classes} = this.props;
|
|
||||||
switch (slide.type) {
|
|
||||||
case 'image':
|
|
||||||
return <img src={slide.url} alt={slide.description? slide.description: ''} className={classes.mediaObject}/>
|
|
||||||
case 'video':
|
|
||||||
return <video controls autoPlay={false} src={slide.url} className={classes.mediaObject}/>
|
|
||||||
case 'gifv':
|
|
||||||
return <img src={slide.url} alt={slide.description? slide.description: ''} className={classes.mediaObject}/>
|
|
||||||
case 'unknown':
|
|
||||||
return <object data={slide.url} className={classes.mediaObject}/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
const step = this.state.currentStep;
|
|
||||||
const mediaItem = this.state.attachments[step];
|
|
||||||
return (
|
return (
|
||||||
<div className={classes.mediaContainer}>
|
<img
|
||||||
<SwipeableViews
|
src={slide.url}
|
||||||
index={this.state.currentStep}
|
alt={slide.description ? slide.description : ""}
|
||||||
>
|
className={classes.mediaObject}
|
||||||
{
|
/>
|
||||||
this.state.attachments.map((slide: Attachment) => {
|
);
|
||||||
return (<div key={slide.id} className={classes.mediaSlide}>
|
case "video":
|
||||||
{this.getSlide(slide)}
|
return (
|
||||||
</div>);
|
<video
|
||||||
})
|
controls
|
||||||
}
|
autoPlay={false}
|
||||||
</SwipeableViews>
|
src={slide.url}
|
||||||
<MobileStepper
|
className={classes.mediaObject}
|
||||||
steps={this.state.totalSteps}
|
/>
|
||||||
position="static"
|
);
|
||||||
activeStep={this.state.currentStep}
|
case "gifv":
|
||||||
className={classes.mobileStepper}
|
return (
|
||||||
nextButton={
|
<img
|
||||||
<Button size="small" onClick={() => this.moveForward()} disabled={this.state.currentStep === this.state.totalSteps - 1}>
|
src={slide.url}
|
||||||
Next
|
alt={slide.description ? slide.description : ""}
|
||||||
</Button>
|
className={classes.mediaObject}
|
||||||
}
|
/>
|
||||||
backButton={
|
);
|
||||||
<Button size="small" onClick={() => this.moveBack()} disabled={this.state.currentStep === 0}>
|
case "unknown":
|
||||||
Back
|
return <object data={slide.url} className={classes.mediaObject} />;
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Typography variant="caption">{mediaItem.description? mediaItem.description: "No description provided."}</Typography>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
const step = this.state.currentStep;
|
||||||
|
const mediaItem = this.state.attachments[step];
|
||||||
|
return (
|
||||||
|
<div className={classes.mediaContainer}>
|
||||||
|
<SwipeableViews index={this.state.currentStep}>
|
||||||
|
{this.state.attachments.map((slide: Attachment) => {
|
||||||
|
return (
|
||||||
|
<div key={slide.id} className={classes.mediaSlide}>
|
||||||
|
{this.getSlide(slide)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</SwipeableViews>
|
||||||
|
<MobileStepper
|
||||||
|
steps={this.state.totalSteps}
|
||||||
|
position="static"
|
||||||
|
activeStep={this.state.currentStep}
|
||||||
|
className={classes.mobileStepper}
|
||||||
|
nextButton={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.moveForward()}
|
||||||
|
disabled={this.state.currentStep === this.state.totalSteps - 1}
|
||||||
|
>
|
||||||
|
Next
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
backButton={
|
||||||
|
<Button
|
||||||
|
size="small"
|
||||||
|
onClick={() => this.moveBack()}
|
||||||
|
disabled={this.state.currentStep === 0}
|
||||||
|
>
|
||||||
|
Back
|
||||||
|
</Button>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<Typography variant="caption">
|
||||||
|
{mediaItem.description
|
||||||
|
? mediaItem.description
|
||||||
|
: "No description provided."}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(AttachmentComponent);
|
export default withStyles(styles)(AttachmentComponent);
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {withStyles} from '@material-ui/core';
|
import { withStyles } from "@material-ui/core";
|
||||||
import AttachmentComponent from './Attachment';
|
import AttachmentComponent from "./Attachment";
|
||||||
import {styles} from './Attachment.styles';
|
import { styles } from "./Attachment.styles";
|
||||||
|
|
||||||
export default withStyles(styles)(AttachmentComponent);
|
export default withStyles(styles)(AttachmentComponent);
|
||||||
|
|
|
@ -1,17 +1,18 @@
|
||||||
import {Theme, createStyles} from '@material-ui/core';
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
attachmentArea: {
|
attachmentArea: {
|
||||||
height: 175,
|
height: 175,
|
||||||
width: 268,
|
width: 268,
|
||||||
backgroundColor: theme.palette.primary.light,
|
backgroundColor: theme.palette.primary.light,
|
||||||
color: theme.palette.common.white
|
color: theme.palette.common.white
|
||||||
},
|
},
|
||||||
attachmentBar: {
|
attachmentBar: {
|
||||||
marginLeft: 0
|
marginLeft: 0
|
||||||
},
|
},
|
||||||
attachmentText: {
|
attachmentText: {
|
||||||
backgroundColor: theme.palette.background.paper,
|
backgroundColor: theme.palette.background.paper,
|
||||||
opacity: 0.5
|
opacity: 0.5
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,81 +1,94 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {GridListTile, GridListTileBar, TextField, withStyles, IconButton} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './ComposeMediaAttachment.styles';
|
GridListTile,
|
||||||
import {withSnackbar, withSnackbarProps} from 'notistack';
|
GridListTileBar,
|
||||||
import Mastodon from 'megalodon';
|
TextField,
|
||||||
import { Attachment } from '../../types/Attachment';
|
withStyles,
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
IconButton
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./ComposeMediaAttachment.styles";
|
||||||
|
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||||
|
import Mastodon from "megalodon";
|
||||||
|
import { Attachment } from "../../types/Attachment";
|
||||||
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
|
|
||||||
interface IComposeMediaAttachmentProps extends withSnackbarProps {
|
interface IComposeMediaAttachmentProps extends withSnackbarProps {
|
||||||
classes: any;
|
classes: any;
|
||||||
client: Mastodon;
|
client: Mastodon;
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
onDeleteCallback: any;
|
onDeleteCallback: any;
|
||||||
onAttachmentUpdate: any;
|
onAttachmentUpdate: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IComposeMediaAttachmentState {
|
interface IComposeMediaAttachmentState {
|
||||||
attachment: Attachment;
|
attachment: Attachment;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComposeMediaAttachment extends Component<IComposeMediaAttachmentProps, IComposeMediaAttachmentState> {
|
class ComposeMediaAttachment extends Component<
|
||||||
|
IComposeMediaAttachmentProps,
|
||||||
|
IComposeMediaAttachmentState
|
||||||
|
> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: IComposeMediaAttachmentProps) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: IComposeMediaAttachmentProps) {
|
this.client = this.props.client;
|
||||||
super(props);
|
|
||||||
|
|
||||||
this.client = this.props.client;
|
this.state = {
|
||||||
|
attachment: this.props.attachment
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
updateAttachmentText(text: string) {
|
||||||
attachment: this.props.attachment
|
this.client
|
||||||
}
|
.put(`/media/${this.state.attachment.id}`, { description: text })
|
||||||
}
|
.then((resp: any) => {
|
||||||
|
this.props.onAttachmentUpdate(resp.data);
|
||||||
|
this.props.enqueueSnackbar("Description updated.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't update description: " + err.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateAttachmentText(text: string) {
|
render() {
|
||||||
this.client.put(`/media/${this.state.attachment.id}`, { description: text }).then((resp: any) => {
|
const { classes, attachment } = this.props;
|
||||||
this.props.onAttachmentUpdate(resp.data);
|
return (
|
||||||
this.props.enqueueSnackbar("Description updated.")
|
<GridListTile className={classes.attachmentArea}>
|
||||||
}).catch((err: Error) => {
|
{attachment.type === "image" || attachment.type === "gifv" ? (
|
||||||
this.props.enqueueSnackbar("Couldn't update description: " + err.name);
|
<img
|
||||||
})
|
src={attachment.url}
|
||||||
}
|
alt={attachment.description ? attachment.description : ""}
|
||||||
|
/>
|
||||||
render() {
|
) : attachment.type === "video" ? (
|
||||||
const { classes, attachment } = this.props;
|
<video autoPlay={false} src={attachment.url} />
|
||||||
return (
|
) : (
|
||||||
<GridListTile className={classes.attachmentArea}>
|
<object data={attachment.url} />
|
||||||
{
|
)}
|
||||||
attachment.type === "image" || attachment.type === "gifv"?
|
<GridListTileBar
|
||||||
<img src={attachment.url} alt={attachment.description? attachment.description: ""}/>:
|
classes={{ title: classes.attachmentBar }}
|
||||||
attachment.type === "video"?
|
title={
|
||||||
<video autoPlay={false} src={attachment.url}/>:
|
<TextField
|
||||||
<object data={attachment.url}/>
|
variant="filled"
|
||||||
}
|
label="Description"
|
||||||
<GridListTileBar
|
margin="dense"
|
||||||
classes={{ title: classes.attachmentBar }}
|
className={classes.attachmentText}
|
||||||
title={
|
onBlur={event => this.updateAttachmentText(event.target.value)}
|
||||||
<TextField
|
></TextField>
|
||||||
variant="filled"
|
}
|
||||||
label="Description"
|
actionIcon={
|
||||||
margin="dense"
|
<IconButton
|
||||||
className={classes.attachmentText}
|
color="inherit"
|
||||||
onBlur={(event) => this.updateAttachmentText(event.target.value)}
|
onClick={() => this.props.onDeleteCallback(this.state.attachment)}
|
||||||
></TextField>
|
>
|
||||||
}
|
<DeleteIcon />
|
||||||
actionIcon={
|
</IconButton>
|
||||||
<IconButton color="inherit"
|
}
|
||||||
onClick={
|
/>
|
||||||
() => this.props.onDeleteCallback(this.state.attachment)
|
</GridListTile>
|
||||||
}
|
);
|
||||||
>
|
}
|
||||||
<DeleteIcon/>
|
|
||||||
</IconButton>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</GridListTile>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(ComposeMediaAttachment));
|
export default withStyles(styles)(withSnackbar(ComposeMediaAttachment));
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {styles} from './ComposeMediaAttachment.styles';
|
import { styles } from "./ComposeMediaAttachment.styles";
|
||||||
import ComposeMediaAttachment from './ComposeMediaAttachment';
|
import ComposeMediaAttachment from "./ComposeMediaAttachment";
|
||||||
import {withStyles} from '@material-ui/core';
|
import { withStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export default withStyles(styles)(ComposeMediaAttachment);
|
export default withStyles(styles)(ComposeMediaAttachment);
|
||||||
|
|
|
@ -1,34 +1,32 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {Picker, PickerProps, CustomEmoji} from 'emoji-mart';
|
import { Picker, PickerProps, CustomEmoji } from "emoji-mart";
|
||||||
import 'emoji-mart/css/emoji-mart.css';
|
import "emoji-mart/css/emoji-mart.css";
|
||||||
|
|
||||||
interface IEmojiPickerProps extends PickerProps {
|
interface IEmojiPickerProps extends PickerProps {
|
||||||
onGetEmoji: any;
|
onGetEmoji: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class EmojiPicker extends Component<IEmojiPickerProps, any> {
|
export class EmojiPicker extends Component<IEmojiPickerProps, any> {
|
||||||
|
retrieveFromLocal() {
|
||||||
retrieveFromLocal() {
|
return JSON.parse(localStorage.getItem("emojis") as string);
|
||||||
return JSON.parse(localStorage.getItem("emojis") as string);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
return (
|
|
||||||
<Picker
|
|
||||||
custom={this.retrieveFromLocal()}
|
|
||||||
emoji=""
|
|
||||||
title=""
|
|
||||||
onClick={this.props.onGetEmoji}
|
|
||||||
style={{
|
|
||||||
borderColor: 'transparent'
|
|
||||||
}}
|
|
||||||
perLine={10}
|
|
||||||
emojiSize={20}
|
|
||||||
set={"google"}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
return (
|
||||||
|
<Picker
|
||||||
|
custom={this.retrieveFromLocal()}
|
||||||
|
emoji=""
|
||||||
|
title=""
|
||||||
|
onClick={this.props.onGetEmoji}
|
||||||
|
style={{
|
||||||
|
borderColor: "transparent"
|
||||||
|
}}
|
||||||
|
perLine={10}
|
||||||
|
emojiSize={20}
|
||||||
|
set={"google"}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default EmojiPicker;
|
export default EmojiPicker;
|
||||||
|
|
|
@ -1,13 +1,14 @@
|
||||||
import { Theme, createStyles } from "@material-ui/core";
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
post: {
|
post: {
|
||||||
marginTop: theme.spacing.unit,
|
marginTop: theme.spacing.unit,
|
||||||
marginBottom: theme.spacing.unit
|
marginBottom: theme.spacing.unit
|
||||||
},
|
},
|
||||||
postReblogChip: {
|
postReblogChip: {
|
||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white,
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
backgroundColor: theme.palette.secondary.light
|
backgroundColor: theme.palette.secondary.light
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.secondary.main,
|
backgroundColor: theme.palette.secondary.main,
|
||||||
|
@ -16,27 +17,27 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
postContent: {
|
postContent: {
|
||||||
paddingTop: 0,
|
paddingTop: 0,
|
||||||
paddingBottom: 0,
|
paddingBottom: 0,
|
||||||
'& a': {
|
"& a": {
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
color: theme.palette.secondary.light,
|
color: theme.palette.secondary.light,
|
||||||
'&:hover': {
|
"&:hover": {
|
||||||
textDecoration: 'underline'
|
textDecoration: "underline"
|
||||||
},
|
},
|
||||||
'&.u-url.mention': {
|
"&.u-url.mention": {
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
fontWeight: 'bold'
|
fontWeight: "bold"
|
||||||
},
|
},
|
||||||
'&.mention.hashtag': {
|
"&.mention.hashtag": {
|
||||||
textDecoration: 'none',
|
textDecoration: "none",
|
||||||
color: 'inherit',
|
color: "inherit",
|
||||||
fontWeight: 'bold'
|
fontWeight: "bold"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
postCard: {
|
postCard: {
|
||||||
'& a:hover': {
|
"& a:hover": {
|
||||||
textDecoration: 'none'
|
textDecoration: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
postEmoji: {
|
postEmoji: {
|
||||||
|
@ -44,7 +45,7 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
},
|
},
|
||||||
postMedia: {
|
postMedia: {
|
||||||
height: 0,
|
height: 0,
|
||||||
paddingTop: '56.25%', // 16:9
|
paddingTop: "56.25%" // 16:9
|
||||||
},
|
},
|
||||||
postActionsReply: {
|
postActionsReply: {
|
||||||
marginLeft: theme.spacing.unit,
|
marginLeft: theme.spacing.unit,
|
||||||
|
@ -85,14 +86,14 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
color: "inherit"
|
color: "inherit"
|
||||||
},
|
},
|
||||||
mobileOnly: {
|
mobileOnly: {
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: 'none'
|
display: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
desktopOnly: {
|
desktopOnly: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: 'block'
|
display: "block"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,15 +1,20 @@
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import webShare, { WebShareInterface } from 'react-web-share-api';
|
import webShare, { WebShareInterface } from "react-web-share-api";
|
||||||
import {MenuItem} from '@material-ui/core';
|
import { MenuItem } from "@material-ui/core";
|
||||||
|
|
||||||
export interface OwnProps {
|
export interface OwnProps {
|
||||||
style: object;
|
style: object;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({
|
const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({
|
||||||
share, isSupported, style,
|
share,
|
||||||
}) => isSupported
|
isSupported,
|
||||||
? <MenuItem onClick={share} style={style}>Share</MenuItem>
|
style
|
||||||
: null
|
}) =>
|
||||||
|
isSupported ? (
|
||||||
|
<MenuItem onClick={share} style={style}>
|
||||||
|
Share
|
||||||
|
</MenuItem>
|
||||||
|
) : null;
|
||||||
|
|
||||||
export default webShare<OwnProps>()(ShareMenu);
|
export default webShare<OwnProps>()(ShareMenu);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Post } from './Post';
|
import { Post } from "./Post";
|
||||||
import { withStyles } from '@material-ui/core';
|
import { withStyles } from "@material-ui/core";
|
||||||
import { styles } from './Post.styles';
|
import { styles } from "./Post.styles";
|
||||||
import { withSnackbar } from 'notistack'
|
import { withSnackbar } from "notistack";
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(Post));
|
export default withStyles(styles)(withSnackbar(Post));
|
||||||
|
|
|
@ -1,57 +1,88 @@
|
||||||
import React, {Component} from 'react';
|
import React, { Component } from "react";
|
||||||
import {MuiThemeProvider, Theme, AppBar, Typography, CssBaseline, Toolbar, Fab, Paper} from '@material-ui/core';
|
import {
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
MuiThemeProvider,
|
||||||
import MenuIcon from '@material-ui/icons/Menu';
|
Theme,
|
||||||
|
AppBar,
|
||||||
|
Typography,
|
||||||
|
CssBaseline,
|
||||||
|
Toolbar,
|
||||||
|
Fab,
|
||||||
|
Paper
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import EditIcon from "@material-ui/icons/Edit";
|
||||||
|
import MenuIcon from "@material-ui/icons/Menu";
|
||||||
|
|
||||||
interface IThemePreviewProps {
|
interface IThemePreviewProps {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IThemePreviewState {
|
interface IThemePreviewState {
|
||||||
theme: Theme;
|
theme: Theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ThemePreview extends Component<IThemePreviewProps, IThemePreviewState> {
|
class ThemePreview extends Component<IThemePreviewProps, IThemePreviewState> {
|
||||||
constructor(props: IThemePreviewProps) {
|
constructor(props: IThemePreviewProps) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
theme: this.props.theme
|
theme: this.props.theme
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div style={{ position: 'relative' }}>
|
<div style={{ position: "relative" }}>
|
||||||
<MuiThemeProvider theme={this.props.theme}>
|
<MuiThemeProvider theme={this.props.theme}>
|
||||||
<CssBaseline/>
|
<CssBaseline />
|
||||||
<Paper>
|
<Paper>
|
||||||
<AppBar color="primary" position="static">
|
<AppBar color="primary" position="static">
|
||||||
<Toolbar>
|
<Toolbar>
|
||||||
<MenuIcon style={{ marginRight: 20, marginLeft: -4 }}/>
|
<MenuIcon style={{ marginRight: 20, marginLeft: -4 }} />
|
||||||
<Typography variant="h6" color="inherit">Hyperspace</Typography>
|
<Typography variant="h6" color="inherit">
|
||||||
</Toolbar>
|
Hyperspace
|
||||||
</AppBar>
|
</Typography>
|
||||||
<div style={{ paddingLeft: 16, paddingTop: 16, paddingRight: 16, paddingBottom: 16, flexGrow: 1 }}>
|
</Toolbar>
|
||||||
<Typography variant="h4" component="p">
|
</AppBar>
|
||||||
This is your theme.
|
<div
|
||||||
</Typography>
|
style={{
|
||||||
<br/>
|
paddingLeft: 16,
|
||||||
<Typography paragraph>
|
paddingTop: 16,
|
||||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc vestibulum congue sem ac ornare. In nec imperdiet neque. In eleifend laoreet efficitur. Vestibulum vel odio mattis, scelerisque nibh a, ornare lectus. Phasellus sollicitudin erat et turpis pellentesque consequat. In maximus luctus purus, eu molestie elit euismod eu. Pellentesque quam lectus, sagittis eget accumsan in, consequat ut sapien. Morbi aliquet ligula erat, id dapibus nunc laoreet at. Integer sodales lacinia finibus. Aliquam augue nibh, eleifend quis consectetur et, rhoncus ut odio. Lorem ipsum dolor sit amet, consectetur adipiscing elit.
|
paddingRight: 16,
|
||||||
</Typography>
|
paddingBottom: 16,
|
||||||
</div>
|
flexGrow: 1
|
||||||
<div style={{ textAlign: 'right' }}>
|
}}
|
||||||
<Fab color="secondary" style={{ marginRight: 8, marginBottom: 8 }}>
|
>
|
||||||
<EditIcon/>
|
<Typography variant="h4" component="p">
|
||||||
</Fab>
|
This is your theme.
|
||||||
</div>
|
</Typography>
|
||||||
|
<br />
|
||||||
</Paper>
|
<Typography paragraph>
|
||||||
</MuiThemeProvider>
|
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc
|
||||||
|
vestibulum congue sem ac ornare. In nec imperdiet neque. In
|
||||||
|
eleifend laoreet efficitur. Vestibulum vel odio mattis,
|
||||||
|
scelerisque nibh a, ornare lectus. Phasellus sollicitudin erat
|
||||||
|
et turpis pellentesque consequat. In maximus luctus purus, eu
|
||||||
|
molestie elit euismod eu. Pellentesque quam lectus, sagittis
|
||||||
|
eget accumsan in, consequat ut sapien. Morbi aliquet ligula
|
||||||
|
erat, id dapibus nunc laoreet at. Integer sodales lacinia
|
||||||
|
finibus. Aliquam augue nibh, eleifend quis consectetur et,
|
||||||
|
rhoncus ut odio. Lorem ipsum dolor sit amet, consectetur
|
||||||
|
adipiscing elit.
|
||||||
|
</Typography>
|
||||||
</div>
|
</div>
|
||||||
)
|
<div style={{ textAlign: "right" }}>
|
||||||
}
|
<Fab
|
||||||
|
color="secondary"
|
||||||
|
style={{ marginRight: 8, marginBottom: 8 }}
|
||||||
|
>
|
||||||
|
<EditIcon />
|
||||||
|
</Fab>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</MuiThemeProvider>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ThemePreview;
|
export default ThemePreview;
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
import ThemePreview from './ThemePreview';
|
import ThemePreview from "./ThemePreview";
|
||||||
|
|
||||||
export default ThemePreview;
|
export default ThemePreview;
|
||||||
|
|
|
@ -1,38 +1,40 @@
|
||||||
import React from 'react';
|
import React from "react";
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from "react-dom";
|
||||||
import App from './App';
|
import App from "./App";
|
||||||
import { HashRouter } from 'react-router-dom';
|
import { HashRouter } from "react-router-dom";
|
||||||
import * as serviceWorker from './serviceWorker';
|
import * as serviceWorker from "./serviceWorker";
|
||||||
import {createUserDefaults, getConfig} from './utilities/settings';
|
import { createUserDefaults, getConfig } from "./utilities/settings";
|
||||||
import {collectEmojisFromServer} from './utilities/emojis';
|
import { collectEmojisFromServer } from "./utilities/emojis";
|
||||||
import {SnackbarProvider} from 'notistack';
|
import { SnackbarProvider } from "notistack";
|
||||||
import { userLoggedIn, refreshUserAccountData } from './utilities/accounts';
|
import { userLoggedIn, refreshUserAccountData } from "./utilities/accounts";
|
||||||
|
|
||||||
getConfig().then((config: any) => {
|
getConfig()
|
||||||
|
.then((config: any) => {
|
||||||
document.title = config.branding.name || "Hyperspace";
|
document.title = config.branding.name || "Hyperspace";
|
||||||
}).catch((err: Error) => {
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
})
|
});
|
||||||
|
|
||||||
createUserDefaults();
|
createUserDefaults();
|
||||||
if (userLoggedIn()) {
|
if (userLoggedIn()) {
|
||||||
collectEmojisFromServer();
|
collectEmojisFromServer();
|
||||||
refreshUserAccountData();
|
refreshUserAccountData();
|
||||||
}
|
}
|
||||||
|
|
||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
<HashRouter>
|
<HashRouter>
|
||||||
<SnackbarProvider
|
<SnackbarProvider
|
||||||
anchorOrigin={{
|
anchorOrigin={{
|
||||||
vertical: 'bottom',
|
vertical: "bottom",
|
||||||
horizontal: 'left',
|
horizontal: "left"
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<App />
|
<App />
|
||||||
</SnackbarProvider>
|
</SnackbarProvider>
|
||||||
</HashRouter>,
|
</HashRouter>,
|
||||||
document.getElementById('root'));
|
document.getElementById("root")
|
||||||
|
);
|
||||||
|
|
||||||
// If you want your app to work offline and load faster, you can change
|
// If you want your app to work offline and load faster, you can change
|
||||||
// unregister() to register() below. Note this comes with some pitfalls.
|
// unregister() to register() below. Note this comes with some pitfalls.
|
||||||
|
|
|
@ -1,95 +1,97 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import ListItem, { ListItemProps } from "@material-ui/core/ListItem";
|
import ListItem, { ListItemProps } from "@material-ui/core/ListItem";
|
||||||
import IconButton, { IconButtonProps } from "@material-ui/core/IconButton";
|
import IconButton, { IconButtonProps } from "@material-ui/core/IconButton";
|
||||||
import { Link, Route, Redirect, RouteProps } from "react-router-dom";
|
import { Link, Route, Redirect, RouteProps } from "react-router-dom";
|
||||||
import Chip, { ChipProps } from '@material-ui/core/Chip';
|
import Chip, { ChipProps } from "@material-ui/core/Chip";
|
||||||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
import { MenuItemProps } from "@material-ui/core/MenuItem";
|
||||||
import { MenuItem } from '@material-ui/core';
|
import { MenuItem } from "@material-ui/core";
|
||||||
import Button, { ButtonProps } from '@material-ui/core/Button';
|
import Button, { ButtonProps } from "@material-ui/core/Button";
|
||||||
import Fab, { FabProps } from '@material-ui/core/Fab';
|
import Fab, { FabProps } from "@material-ui/core/Fab";
|
||||||
import Avatar, { AvatarProps } from '@material-ui/core/Avatar';
|
import Avatar, { AvatarProps } from "@material-ui/core/Avatar";
|
||||||
import { userLoggedIn } from '../utilities/accounts';
|
import { userLoggedIn } from "../utilities/accounts";
|
||||||
|
|
||||||
export interface ILinkableListItemProps extends ListItemProps {
|
export interface ILinkableListItemProps extends ListItemProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableIconButtonProps extends IconButtonProps {
|
export interface ILinkableIconButtonProps extends IconButtonProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableChipProps extends ChipProps {
|
export interface ILinkableChipProps extends ChipProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableMenuItemProps extends MenuItemProps {
|
export interface ILinkableMenuItemProps extends MenuItemProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableButtonProps extends ButtonProps {
|
export interface ILinkableButtonProps extends ButtonProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableFabProps extends FabProps {
|
export interface ILinkableFabProps extends FabProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ILinkableAvatarProps extends AvatarProps {
|
export interface ILinkableAvatarProps extends AvatarProps {
|
||||||
to: string;
|
to: string;
|
||||||
replace?: boolean;
|
replace?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||||
<ListItem {...props} component={Link as any}/>
|
<ListItem {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
|
export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
|
||||||
<IconButton {...props} component={Link as any}/>
|
<IconButton {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const LinkableChip = (props: ILinkableChipProps) => (
|
export const LinkableChip = (props: ILinkableChipProps) => (
|
||||||
<Chip {...props} component={Link as any}/>
|
<Chip {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
|
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
|
||||||
<MenuItem {...props} component={Link as any}/>
|
<MenuItem {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const LinkableButton = (props: ILinkableButtonProps) => (
|
export const LinkableButton = (props: ILinkableButtonProps) => (
|
||||||
<Button {...props} component={Link as any}/>
|
<Button {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const LinkableFab = (props: ILinkableFabProps) => (
|
export const LinkableFab = (props: ILinkableFabProps) => (
|
||||||
<Fab {...props} component={Link as any}/>
|
<Fab {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
export const LinkableAvatar = (props: ILinkableAvatarProps) => (
|
export const LinkableAvatar = (props: ILinkableAvatarProps) => (
|
||||||
<Avatar {...props} component={Link as any}/>
|
<Avatar {...props} component={Link as any} />
|
||||||
)
|
);
|
||||||
|
|
||||||
export const ProfileRoute = (rest: any, component: Component) => (
|
export const ProfileRoute = (rest: any, component: Component) => (
|
||||||
<Route {...rest} render={props => (
|
<Route {...rest} render={props => <Component {...props} />} />
|
||||||
<Component {...props}/>
|
);
|
||||||
)}/>
|
|
||||||
)
|
|
||||||
|
|
||||||
export const PrivateRoute = (props: IPrivateRouteProps) => {
|
export const PrivateRoute = (props: IPrivateRouteProps) => {
|
||||||
const { component, render, ...rest } = props;
|
const { component, render, ...rest } = props;
|
||||||
return (<Route {...rest}
|
return (
|
||||||
render={(compProps: any) => (
|
<Route
|
||||||
userLoggedIn()?
|
{...rest}
|
||||||
React.createElement(component, compProps):
|
render={(compProps: any) =>
|
||||||
<Redirect to="/welcome"/>
|
userLoggedIn() ? (
|
||||||
)}
|
React.createElement(component, compProps)
|
||||||
|
) : (
|
||||||
|
<Redirect to="/welcome" />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
);
|
||||||
|
};
|
||||||
|
|
||||||
interface IPrivateRouteProps extends RouteProps {
|
interface IPrivateRouteProps extends RouteProps {
|
||||||
component: any
|
component: any;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,316 +1,499 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
Avatar,
|
Avatar,
|
||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
IconButton,
|
||||||
withStyles,
|
withStyles,
|
||||||
Typography,
|
Typography,
|
||||||
Link,
|
Link,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Button
|
Button
|
||||||
} from '@material-ui/core';
|
} from "@material-ui/core";
|
||||||
|
|
||||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||||
import ChatIcon from '@material-ui/icons/Chat';
|
import ChatIcon from "@material-ui/icons/Chat";
|
||||||
import PersonIcon from '@material-ui/icons/Person';
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
import AssignmentIcon from '@material-ui/icons/Assignment';
|
import AssignmentIcon from "@material-ui/icons/Assignment";
|
||||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
|
import NetworkCheckIcon from "@material-ui/icons/NetworkCheck";
|
||||||
import UpdateIcon from '@material-ui/icons/Update';
|
import UpdateIcon from "@material-ui/icons/Update";
|
||||||
import InfoIcon from '@material-ui/icons/Info';
|
import InfoIcon from "@material-ui/icons/Info";
|
||||||
import NotesIcon from '@material-ui/icons/Notes';
|
import NotesIcon from "@material-ui/icons/Notes";
|
||||||
import CodeIcon from '@material-ui/icons/Code';
|
import CodeIcon from "@material-ui/icons/Code";
|
||||||
import TicketAccountIcon from 'mdi-material-ui/TicketAccount';
|
import TicketAccountIcon from "mdi-material-ui/TicketAccount";
|
||||||
import MastodonIcon from 'mdi-material-ui/Mastodon';
|
import MastodonIcon from "mdi-material-ui/Mastodon";
|
||||||
import EditIcon from '@material-ui/icons/Edit';
|
import EditIcon from "@material-ui/icons/Edit";
|
||||||
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
import VpnKeyIcon from "@material-ui/icons/VpnKey";
|
||||||
|
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import {Instance} from '../types/Instance';
|
import { Instance } from "../types/Instance";
|
||||||
import {LinkableIconButton, LinkableAvatar} from '../interfaces/overrides';
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
import { UAccount } from '../types/Account';
|
import { UAccount } from "../types/Account";
|
||||||
import { getConfig } from '../utilities/settings';
|
import { getConfig } from "../utilities/settings";
|
||||||
import { License, Federation } from '../types/Config';
|
import { License, Federation } from "../types/Config";
|
||||||
|
|
||||||
interface IAboutPageState {
|
interface IAboutPageState {
|
||||||
instance?: Instance;
|
instance?: Instance;
|
||||||
federated?: boolean;
|
federated?: boolean;
|
||||||
federation?: Federation;
|
federation?: Federation;
|
||||||
developer?: boolean;
|
developer?: boolean;
|
||||||
hyperspaceAdmin?: UAccount;
|
hyperspaceAdmin?: UAccount;
|
||||||
hyperspaceAdminName?: string;
|
hyperspaceAdminName?: string;
|
||||||
versionNumber?: string;
|
versionNumber?: string;
|
||||||
brandName?: string;
|
brandName?: string;
|
||||||
brandBg?: string;
|
brandBg?: string;
|
||||||
license: License;
|
license: License;
|
||||||
repository?: string;
|
repository?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class AboutPage extends Component<any, IAboutPageState> {
|
class AboutPage extends Component<any, IAboutPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.client = new Mastodon(
|
||||||
super(props);
|
localStorage.getItem("access_token") as string,
|
||||||
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
|
);
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
this.state = {
|
||||||
|
license: {
|
||||||
|
name: "Apache 2.0 License (inherited)",
|
||||||
|
url: "https://www.apache.org/licenses/LICENSE-2.0"
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
componentWillMount() {
|
||||||
|
this.client.get("/instance").then((resp: any) => {
|
||||||
|
this.setState({
|
||||||
|
instance: resp.data as Instance
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
getConfig().then((config: any) => {
|
||||||
|
this.client
|
||||||
|
.get("/accounts/" + (config.admin ? config.admin.account : "0"))
|
||||||
|
.then((resp: any) => {
|
||||||
|
let account = resp.data;
|
||||||
|
this.setState({
|
||||||
|
hyperspaceAdmin: account,
|
||||||
|
hyperspaceAdminName: config.admin.name,
|
||||||
|
federation: config.federation,
|
||||||
|
developer: config.developer ? config.developer === "true" : false,
|
||||||
|
versionNumber: config.version,
|
||||||
|
brandName: config.branding ? config.branding.name : "Hyperspace",
|
||||||
|
brandBg: config.branding.background,
|
||||||
license: {
|
license: {
|
||||||
name: "Apache 2.0 License (inherited)",
|
name: config.license.name,
|
||||||
url: "https://www.apache.org/licenses/LICENSE-2.0"
|
url: config.license.url
|
||||||
}
|
},
|
||||||
}
|
repository: config.repository
|
||||||
}
|
});
|
||||||
|
|
||||||
componentWillMount() {
|
|
||||||
this.client.get('/instance').then((resp: any) => {
|
|
||||||
this.setState({
|
|
||||||
instance: resp.data as Instance
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getConfig().then((config: any) => {
|
render() {
|
||||||
this.client.get('/accounts/' + (config.admin? config.admin.account: "0")).then((resp: any) => {
|
const { classes } = this.props;
|
||||||
let account = resp.data;
|
|
||||||
this.setState({
|
|
||||||
hyperspaceAdmin: account,
|
|
||||||
hyperspaceAdminName: config.admin.name,
|
|
||||||
federation: config.federation,
|
|
||||||
developer: config.developer? config.developer === "true": false,
|
|
||||||
versionNumber: config.version,
|
|
||||||
brandName: config.branding? config.branding.name: "Hyperspace",
|
|
||||||
brandBg: config.branding.background,
|
|
||||||
license: {
|
|
||||||
name: config.license.name,
|
|
||||||
url: config.license.url
|
|
||||||
},
|
|
||||||
repository: config.repository
|
|
||||||
})
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
return (
|
||||||
const { classes } = this.props;
|
<div className={classes.pageLayoutConstraints}>
|
||||||
|
<Paper>
|
||||||
return (
|
<div
|
||||||
<div className={classes.pageLayoutConstraints}>
|
className={classes.instanceHeaderPaper}
|
||||||
<Paper>
|
style={{
|
||||||
<div
|
backgroundImage: `url("${
|
||||||
className={classes.instanceHeaderPaper}
|
this.state.instance && this.state.instance.thumbnail
|
||||||
style={{
|
? this.state.instance.thumbnail
|
||||||
backgroundImage: `url("${this.state.instance && this.state.instance.thumbnail? this.state.instance.thumbnail: ""}")`
|
: ""
|
||||||
}}
|
}")`
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconButton
|
||||||
|
className={classes.instanceToolbar}
|
||||||
|
href={localStorage.getItem("baseurl") as string}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
color="inherit"
|
||||||
|
>
|
||||||
|
<OpenInNewIcon />
|
||||||
|
</IconButton>
|
||||||
|
<Typography
|
||||||
|
className={classes.instanceHeaderText}
|
||||||
|
variant="h4"
|
||||||
|
component="p"
|
||||||
|
>
|
||||||
|
{this.state.instance ? this.state.instance.uri : "Loading..."}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<List className={classes.pageListConstraints}>
|
||||||
|
{localStorage["isPleroma"] == "false" && (
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
to={`/profile/${
|
||||||
|
this.state.instance
|
||||||
|
? this.state.instance.contact_account.id
|
||||||
|
: 0
|
||||||
|
}`}
|
||||||
|
alt="Instance admin"
|
||||||
|
src={
|
||||||
|
this.state.instance
|
||||||
|
? this.state.instance.contact_account.avatar_static
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="Instance admin"
|
||||||
|
secondary={
|
||||||
|
this.state.instance
|
||||||
|
? `${this.state.instance.contact_account.display_name} (@${this.state.instance.contact_account.acct})`
|
||||||
|
: "Loading..."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="Send a post or message">
|
||||||
|
<LinkableIconButton
|
||||||
|
to={`/compose?visibility=public&acct=${
|
||||||
|
this.state.instance
|
||||||
|
? this.state.instance.contact_account.acct
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
>
|
>
|
||||||
<IconButton className={classes.instanceToolbar} href={localStorage.getItem("baseurl") as string} target="_blank" rel="noreferrer" color="inherit">
|
<ChatIcon />
|
||||||
<OpenInNewIcon/>
|
</LinkableIconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
<Typography className={classes.instanceHeaderText} variant="h4" component="p">{this.state.instance ? this.state.instance.uri: "Loading..."}</Typography>
|
<Tooltip title="View profile">
|
||||||
</div>
|
<LinkableIconButton
|
||||||
<List className={classes.pageListConstraints}>
|
to={`/profile/${
|
||||||
{(localStorage['isPleroma'] == "false") && <ListItem>
|
this.state.instance
|
||||||
<ListItemAvatar>
|
? this.state.instance.contact_account.id
|
||||||
<LinkableAvatar to={`/profile/${this.state.instance? this.state.instance.contact_account.id: 0}`} alt="Instance admin" src={this.state.instance? this.state.instance.contact_account.avatar_static: ""}/>
|
: 0
|
||||||
</ListItemAvatar>
|
}`}
|
||||||
<ListItemText primary="Instance admin" secondary={
|
|
||||||
this.state.instance ? `${this.state.instance.contact_account.display_name} (@${this.state.instance.contact_account.acct})`:
|
|
||||||
"Loading..."
|
|
||||||
}/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="Send a post or message">
|
|
||||||
<LinkableIconButton to={`/compose?visibility=public&acct=${this.state.instance? this.state.instance.contact_account.acct: ""}`}>
|
|
||||||
<ChatIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="View profile">
|
|
||||||
<LinkableIconButton to={`/profile/${this.state.instance? this.state.instance.contact_account.id: 0}`}>
|
|
||||||
<AssignmentIndIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>}
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<AssignmentIcon/>
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary="Terms of service"
|
|
||||||
secondary="View the rules and privacy policies"
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="Open in browser">
|
|
||||||
<IconButton href={localStorage.getItem("baseurl") as string + "/terms"} target="_blank" rel="noreferrer">
|
|
||||||
<OpenInNewIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<TicketAccountIcon/>
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary="Invite a friend"
|
|
||||||
secondary="Invite a friend to this instance"
|
|
||||||
/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="Go to invite settings">
|
|
||||||
<Button href={localStorage.getItem("baseurl") as string + "/invites"} target="_blank" rel="noreferrer">
|
|
||||||
Invite
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<MastodonIcon/>
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText
|
|
||||||
primary="Mastodon version"
|
|
||||||
secondary={this.state.instance? this.state.instance.version: "x.x.x"}
|
|
||||||
/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
|
|
||||||
<Paper>
|
|
||||||
<div
|
|
||||||
className={classes.instanceHeaderPaper}
|
|
||||||
style={{
|
|
||||||
backgroundImage: `url("${this.state.brandBg? this.state.brandBg: ""}")`
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className={classes.instanceToolbar}>
|
<AssignmentIndIcon />
|
||||||
{
|
</LinkableIconButton>
|
||||||
this.state.repository?
|
</Tooltip>
|
||||||
<Tooltip title="View source code">
|
</ListItemSecondaryAction>
|
||||||
<IconButton href={this.state.repository} target="_blank" rel="noreferrer" color="inherit">
|
</ListItem>
|
||||||
<CodeIcon/>
|
)}
|
||||||
</IconButton>
|
<ListItem>
|
||||||
</Tooltip>: null
|
<ListItemAvatar>
|
||||||
}
|
<Avatar>
|
||||||
</div>
|
<AssignmentIcon />
|
||||||
<Typography className={classes.instanceHeaderText} variant="h4" component="p">
|
</Avatar>
|
||||||
{this.state.brandName? this.state.brandName: "Hyperspace"}
|
</ListItemAvatar>
|
||||||
</Typography>
|
<ListItemText
|
||||||
</div>
|
primary="Terms of service"
|
||||||
<List className={classes.pageListConstraints}>
|
secondary="View the rules and privacy policies"
|
||||||
<ListItem>
|
/>
|
||||||
<ListItemAvatar>
|
<ListItemSecondaryAction>
|
||||||
<LinkableAvatar to={`/profile/${this.state.hyperspaceAdmin? this.state.hyperspaceAdmin.id: 0}`} src={this.state.hyperspaceAdmin? this.state.hyperspaceAdmin.avatar_static: ""}>
|
<Tooltip title="Open in browser">
|
||||||
<PersonIcon/>
|
<IconButton
|
||||||
</LinkableAvatar>
|
href={
|
||||||
</ListItemAvatar>
|
(localStorage.getItem("baseurl") as string) + "/terms"
|
||||||
<ListItemText primary="App provider" secondary={this.state.hyperspaceAdmin && this.state.hyperspaceAdminName? (this.state.hyperspaceAdminName || this.state.hyperspaceAdmin.display_name || "@" + this.state.hyperspaceAdmin.acct): "No provider set in config"}/>
|
}
|
||||||
<ListItemSecondaryAction>
|
target="_blank"
|
||||||
<Tooltip title="Send a post or message">
|
rel="noreferrer"
|
||||||
<LinkableIconButton to={`/compose?visibility=${this.state.federated? "public": "private"}&acct=${this.state.hyperspaceAdmin? this.state.hyperspaceAdmin.acct: ""}`}>
|
>
|
||||||
<ChatIcon/>
|
<OpenInNewIcon />
|
||||||
</LinkableIconButton>
|
</IconButton>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Tooltip title="View profile">
|
</ListItemSecondaryAction>
|
||||||
<LinkableIconButton to={`/profile/${this.state.hyperspaceAdmin? this.state.hyperspaceAdmin.id: 0}`}>
|
</ListItem>
|
||||||
<AssignmentIndIcon/>
|
<ListItem>
|
||||||
</LinkableIconButton>
|
<ListItemAvatar>
|
||||||
</Tooltip>
|
<Avatar>
|
||||||
</ListItemSecondaryAction>
|
<TicketAccountIcon />
|
||||||
</ListItem>
|
</Avatar>
|
||||||
<ListItem>
|
</ListItemAvatar>
|
||||||
<ListItemAvatar>
|
<ListItemText
|
||||||
<Avatar>
|
primary="Invite a friend"
|
||||||
<NotesIcon/>
|
secondary="Invite a friend to this instance"
|
||||||
</Avatar>
|
/>
|
||||||
</ListItemAvatar>
|
<ListItemSecondaryAction>
|
||||||
<ListItemText primary="License" secondary={this.state.license.name}/>
|
<Tooltip title="Go to invite settings">
|
||||||
<ListItemSecondaryAction>
|
<Button
|
||||||
<Tooltip title = "View license">
|
href={
|
||||||
<IconButton href={this.state.license.url} target="_blank" rel="noreferrer">
|
(localStorage.getItem("baseurl") as string) + "/invites"
|
||||||
<OpenInNewIcon/>
|
}
|
||||||
</IconButton>
|
target="_blank"
|
||||||
</Tooltip>
|
rel="noreferrer"
|
||||||
</ListItemSecondaryAction>
|
>
|
||||||
</ListItem>
|
Invite
|
||||||
<ListItem>
|
</Button>
|
||||||
<ListItemAvatar>
|
</Tooltip>
|
||||||
<Avatar>
|
</ListItemSecondaryAction>
|
||||||
<UpdateIcon/>
|
</ListItem>
|
||||||
</Avatar>
|
<ListItem>
|
||||||
</ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<ListItemText primary="Release channel" secondary={
|
<Avatar>
|
||||||
this.state?
|
<MastodonIcon />
|
||||||
this.state.developer?
|
</Avatar>
|
||||||
"Developer":
|
</ListItemAvatar>
|
||||||
"Release":
|
<ListItemText
|
||||||
"Loading..."
|
primary="Mastodon version"
|
||||||
}/>
|
secondary={
|
||||||
</ListItem>
|
this.state.instance ? this.state.instance.version : "x.x.x"
|
||||||
<ListItem>
|
}
|
||||||
<ListItemAvatar>
|
/>
|
||||||
<Avatar>
|
</ListItem>
|
||||||
<InfoIcon/>
|
</List>
|
||||||
</Avatar>
|
</Paper>
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="App version" secondary={`${this.state? this.state.brandName: "Hyperspace"} v${this.state? this.state.versionNumber: "1.0.x"} ${this.state && this.state.brandName !== "Hyperspace"? "(Hyperspace-like)": ""}`}/>
|
<br />
|
||||||
</ListItem>
|
|
||||||
</List>
|
<Paper>
|
||||||
</Paper>
|
<div
|
||||||
<br/>
|
className={classes.instanceHeaderPaper}
|
||||||
<ListSubheader>Federation status</ListSubheader>
|
style={{
|
||||||
<Paper>
|
backgroundImage: `url("${
|
||||||
<List className={classes.pageListConstraints}>
|
this.state.brandBg ? this.state.brandBg : ""
|
||||||
<ListItem>
|
}")`
|
||||||
<ListItemAvatar>
|
}}
|
||||||
<Avatar>
|
>
|
||||||
<NetworkCheckIcon/>
|
<div className={classes.instanceToolbar}>
|
||||||
</Avatar>
|
{this.state.repository ? (
|
||||||
</ListItemAvatar>
|
<Tooltip title="View source code">
|
||||||
<ListItemText primary="General federation" secondary={this.state.federation && this.state.federation.enablePublicTimeline? "This instance is federated.": "This instance is not federated."}/>
|
<IconButton
|
||||||
</ListItem>
|
href={this.state.repository}
|
||||||
<ListItem>
|
target="_blank"
|
||||||
<ListItemAvatar>
|
rel="noreferrer"
|
||||||
<Avatar>
|
color="inherit"
|
||||||
<VpnKeyIcon/>
|
>
|
||||||
</Avatar>
|
<CodeIcon />
|
||||||
</ListItemAvatar>
|
</IconButton>
|
||||||
<ListItemText primary="Universal login" secondary={this.state.federation && this.state.federation.universalLogin? "This instance supports universal login.": "This instance does not support universal login."}/>
|
</Tooltip>
|
||||||
</ListItem>
|
) : null}
|
||||||
<ListItem>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<Avatar>
|
|
||||||
<EditIcon/>
|
|
||||||
</Avatar>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary="Public posting" secondary={this.state.federation && this.state.federation.allowPublicPosts? "This instance allows posting publicly.": "This instance does not allow posting publicly."}/>
|
|
||||||
</ListItem>
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
<br/>
|
|
||||||
<div className={classes.pageLayoutFooter}>
|
|
||||||
<Typography variant="caption">(C) {new Date().getFullYear()} {this.state? this.state.brandName: "Hyperspace"} developers. All rights reserved.</Typography>
|
|
||||||
<Typography variant="caption" paragraph>{this.state? this.state.brandName: "Hyperspace"} is made possible by the <Link href={"https://material-ui.com"} target="_blank" rel="noreferrer">Material UI</Link> project, <Link href={"https://www.npmjs.com/package/megalodon"} target="_blank" rel="noreferrer">Megalodon</Link> library, and other <Link href={"https://github.com/hyperspacedev/hyperspace/blob/master/package.json"} target="_blank" rel="noreferrer">open source software</Link>.</Typography>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
<Typography
|
||||||
}
|
className={classes.instanceHeaderText}
|
||||||
|
variant="h4"
|
||||||
|
component="p"
|
||||||
|
>
|
||||||
|
{this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
<List className={classes.pageListConstraints}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
to={`/profile/${
|
||||||
|
this.state.hyperspaceAdmin
|
||||||
|
? this.state.hyperspaceAdmin.id
|
||||||
|
: 0
|
||||||
|
}`}
|
||||||
|
src={
|
||||||
|
this.state.hyperspaceAdmin
|
||||||
|
? this.state.hyperspaceAdmin.avatar_static
|
||||||
|
: ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
</LinkableAvatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="App provider"
|
||||||
|
secondary={
|
||||||
|
this.state.hyperspaceAdmin && this.state.hyperspaceAdminName
|
||||||
|
? this.state.hyperspaceAdminName ||
|
||||||
|
this.state.hyperspaceAdmin.display_name ||
|
||||||
|
"@" + this.state.hyperspaceAdmin.acct
|
||||||
|
: "No provider set in config"
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="Send a post or message">
|
||||||
|
<LinkableIconButton
|
||||||
|
to={`/compose?visibility=${
|
||||||
|
this.state.federated ? "public" : "private"
|
||||||
|
}&acct=${
|
||||||
|
this.state.hyperspaceAdmin
|
||||||
|
? this.state.hyperspaceAdmin.acct
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ChatIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="View profile">
|
||||||
|
<LinkableIconButton
|
||||||
|
to={`/profile/${
|
||||||
|
this.state.hyperspaceAdmin
|
||||||
|
? this.state.hyperspaceAdmin.id
|
||||||
|
: 0
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<AssignmentIndIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<NotesIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="License"
|
||||||
|
secondary={this.state.license.name}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="View license">
|
||||||
|
<IconButton
|
||||||
|
href={this.state.license.url}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<OpenInNewIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<UpdateIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="Release channel"
|
||||||
|
secondary={
|
||||||
|
this.state
|
||||||
|
? this.state.developer
|
||||||
|
? "Developer"
|
||||||
|
: "Release"
|
||||||
|
: "Loading..."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<InfoIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="App version"
|
||||||
|
secondary={`${
|
||||||
|
this.state ? this.state.brandName : "Hyperspace"
|
||||||
|
} v${this.state ? this.state.versionNumber : "1.0.x"} ${
|
||||||
|
this.state && this.state.brandName !== "Hyperspace"
|
||||||
|
? "(Hyperspace-like)"
|
||||||
|
: ""
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<br />
|
||||||
|
<ListSubheader>Federation status</ListSubheader>
|
||||||
|
<Paper>
|
||||||
|
<List className={classes.pageListConstraints}>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<NetworkCheckIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="General federation"
|
||||||
|
secondary={
|
||||||
|
this.state.federation &&
|
||||||
|
this.state.federation.enablePublicTimeline
|
||||||
|
? "This instance is federated."
|
||||||
|
: "This instance is not federated."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<VpnKeyIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="Universal login"
|
||||||
|
secondary={
|
||||||
|
this.state.federation && this.state.federation.universalLogin
|
||||||
|
? "This instance supports universal login."
|
||||||
|
: "This instance does not support universal login."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<Avatar>
|
||||||
|
<EditIcon />
|
||||||
|
</Avatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="Public posting"
|
||||||
|
secondary={
|
||||||
|
this.state.federation &&
|
||||||
|
this.state.federation.allowPublicPosts
|
||||||
|
? "This instance allows posting publicly."
|
||||||
|
: "This instance does not allow posting publicly."
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<br />
|
||||||
|
<div className={classes.pageLayoutFooter}>
|
||||||
|
<Typography variant="caption">
|
||||||
|
(C) {new Date().getFullYear()}{" "}
|
||||||
|
{this.state ? this.state.brandName : "Hyperspace"} developers. All
|
||||||
|
rights reserved.
|
||||||
|
</Typography>
|
||||||
|
<Typography variant="caption" paragraph>
|
||||||
|
{this.state ? this.state.brandName : "Hyperspace"} is made possible
|
||||||
|
by the{" "}
|
||||||
|
<Link
|
||||||
|
href={"https://material-ui.com"}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Material UI
|
||||||
|
</Link>{" "}
|
||||||
|
project,{" "}
|
||||||
|
<Link
|
||||||
|
href={"https://www.npmjs.com/package/megalodon"}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
Megalodon
|
||||||
|
</Link>{" "}
|
||||||
|
library, and other{" "}
|
||||||
|
<Link
|
||||||
|
href={
|
||||||
|
"https://github.com/hyperspacedev/hyperspace/blob/master/package.json"
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
open source software
|
||||||
|
</Link>
|
||||||
|
.
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(AboutPage);
|
export default withStyles(styles)(AboutPage);
|
||||||
|
|
|
@ -0,0 +1,47 @@
|
||||||
|
import React, { Component } from "react";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
IconButton,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemAvatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
ListItemText,
|
||||||
|
ListSubheader,
|
||||||
|
Paper,
|
||||||
|
Switch,
|
||||||
|
withStyles
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import DevicesIcon from "@material-ui/core/SvgIcon/SvgIcon";
|
||||||
|
|
||||||
|
class Blocked extends Component<any, any> {
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.pageLayoutConstraints}>
|
||||||
|
<ListSubheader>Blocked servers</ListSubheader>
|
||||||
|
<Button className={classes.clearAllButton} variant="text">
|
||||||
|
{" "}
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
<List>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<DevicesIcon color="action" />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary="Match system appearance"
|
||||||
|
secondary="Obey light/dark theme from your system"
|
||||||
|
/>
|
||||||
|
</ListItem>
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default withStyles(styles)(Blocked);
|
|
@ -1,48 +1,49 @@
|
||||||
import { Theme, createStyles } from "@material-ui/core";
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
dialog: {
|
dialog: {
|
||||||
minHeight: 400
|
minHeight: 400
|
||||||
},
|
},
|
||||||
dialogContent: {
|
dialogContent: {
|
||||||
paddingBottom: 0
|
paddingBottom: 0
|
||||||
},
|
},
|
||||||
dialogActions: {
|
dialogActions: {
|
||||||
paddingLeft: theme.spacing.unit * 1.25
|
paddingLeft: theme.spacing.unit * 1.25
|
||||||
},
|
},
|
||||||
charsReachingLimit: {
|
charsReachingLimit: {
|
||||||
color: theme.palette.error.main
|
color: theme.palette.error.main
|
||||||
},
|
},
|
||||||
warningCaption: {
|
warningCaption: {
|
||||||
height: 16,
|
height: 16,
|
||||||
verticalAlign: "text-bottom"
|
verticalAlign: "text-bottom"
|
||||||
},
|
},
|
||||||
composeAttachmentArea: {
|
composeAttachmentArea: {
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
flexWrap: 'wrap',
|
flexWrap: "wrap",
|
||||||
justifyContent: 'space-around',
|
justifyContent: "space-around",
|
||||||
overflow: 'hidden'
|
overflow: "hidden"
|
||||||
},
|
},
|
||||||
composeAttachmentAreaGridList: {
|
composeAttachmentAreaGridList: {
|
||||||
height: 250,
|
height: 250,
|
||||||
width: '100%'
|
width: "100%"
|
||||||
},
|
},
|
||||||
composeEmoji: {
|
composeEmoji: {
|
||||||
marginTop: theme.spacing.unit * 8
|
marginTop: theme.spacing.unit * 8
|
||||||
},
|
},
|
||||||
desktopOnly: {
|
desktopOnly: {
|
||||||
display: "none",
|
display: "none",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: "block"
|
display: "block"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pollWizardOptionIcon: {
|
pollWizardOptionIcon: {
|
||||||
marginRight: theme.spacing.unit * 2,
|
marginRight: theme.spacing.unit * 2,
|
||||||
marginTop: 4,
|
marginTop: 4,
|
||||||
marginBottom: 4,
|
marginBottom: 4,
|
||||||
color: theme.palette.grey[700]
|
color: theme.palette.grey[700]
|
||||||
},
|
},
|
||||||
pollWizardFlexGrow: {
|
pollWizardFlexGrow: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,120 +1,151 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import { withStyles, CircularProgress, Typography, Paper} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './PageLayout.styles';
|
withStyles,
|
||||||
import Post from '../components/Post';
|
CircularProgress,
|
||||||
import { Status } from '../types/Status';
|
Typography,
|
||||||
import { Context } from '../types/Context';
|
Paper
|
||||||
import Mastodon from 'megalodon';
|
} from "@material-ui/core";
|
||||||
import {withSnackbar} from 'notistack';
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Post from "../components/Post";
|
||||||
|
import { Status } from "../types/Status";
|
||||||
|
import { Context } from "../types/Context";
|
||||||
|
import Mastodon from "megalodon";
|
||||||
|
import { withSnackbar } from "notistack";
|
||||||
|
|
||||||
interface IConversationPageState {
|
interface IConversationPageState {
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
viewDidErrorCode?: any;
|
||||||
conversationId: string;
|
conversationId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class Conversation extends Component<any, IConversationPageState> {
|
class Conversation extends Component<any, IConversationPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
streamListener: any;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
streamListener: any;
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
viewIsLoading: true,
|
||||||
|
conversationId: props.match.params.conversationId
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.client = new Mastodon(
|
||||||
viewIsLoading: true,
|
localStorage.getItem("access_token") as string,
|
||||||
conversationId: props.match.params.conversationId
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
}
|
);
|
||||||
|
}
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
getContext() {
|
||||||
|
this.client
|
||||||
}
|
.get(`/statuses/${this.state.conversationId}`)
|
||||||
|
.then((resp: any) => {
|
||||||
getContext() {
|
let result: Status = resp.data;
|
||||||
this.client.get(`/statuses/${this.state.conversationId}`).then((resp: any) => {
|
this.setState({ posts: [result] });
|
||||||
let result: Status = resp.data;
|
})
|
||||||
this.setState({ posts: [result] });
|
.catch((err: Error) => {
|
||||||
}).catch((err: Error) => {
|
this.setState({
|
||||||
this.setState({
|
viewIsLoading: false,
|
||||||
viewIsLoading: false,
|
viewDidError: true,
|
||||||
viewDidError: true,
|
viewDidErrorCode: err.message
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, { variant: 'error' });
|
|
||||||
})
|
|
||||||
this.client.get(`/statuses/${this.state.conversationId}/context`).then((resp: any) => {
|
|
||||||
let context: Context = resp.data;
|
|
||||||
let posts = this.state.posts;
|
|
||||||
let array: any[] = [];
|
|
||||||
if (posts) {
|
|
||||||
array = array.concat(context.ancestors).concat(posts).concat(context.descendants);
|
|
||||||
}
|
|
||||||
this.setState({
|
|
||||||
posts: array as [Status],
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidLoad: true,
|
|
||||||
viewDidError: false
|
|
||||||
});
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, { variant: 'error' });
|
|
||||||
});
|
});
|
||||||
}
|
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
componentWillReceiveProps(props: any) {
|
});
|
||||||
if (props.match.params.conversationId !== this.state.conversationId) {
|
});
|
||||||
this.getContext()
|
this.client
|
||||||
|
.get(`/statuses/${this.state.conversationId}/context`)
|
||||||
|
.then((resp: any) => {
|
||||||
|
let context: Context = resp.data;
|
||||||
|
let posts = this.state.posts;
|
||||||
|
let array: any[] = [];
|
||||||
|
if (posts) {
|
||||||
|
array = array
|
||||||
|
.concat(context.ancestors)
|
||||||
|
.concat(posts)
|
||||||
|
.concat(context.descendants);
|
||||||
}
|
}
|
||||||
}
|
this.setState({
|
||||||
|
posts: array as [Status],
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
viewDidError: false
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillReceiveProps(props: any) {
|
||||||
this.getContext()
|
if (props.match.params.conversationId !== this.state.conversationId) {
|
||||||
|
this.getContext();
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentWillMount() {
|
||||||
const where: HTMLElement | null = document.getElementById(`post_${this.state.conversationId}`);
|
this.getContext();
|
||||||
if (where && this.state.posts && this.state.posts[0].id !== this.state.conversationId) {
|
}
|
||||||
window.scrollTo(0, where.getBoundingClientRect().top);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
componentDidUpdate() {
|
||||||
const {classes} = this.props;
|
const where: HTMLElement | null = document.getElementById(
|
||||||
return (
|
`post_${this.state.conversationId}`
|
||||||
<div className={classes.pageLayoutMaxConstraints}>
|
);
|
||||||
{ this.state.posts?
|
if (
|
||||||
<div>
|
where &&
|
||||||
{ this.state.posts.map((post: Status) => {
|
this.state.posts &&
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
this.state.posts[0].id !== this.state.conversationId
|
||||||
}) }
|
) {
|
||||||
</div>:
|
window.scrollTo(0, where.getBoundingClientRect().top);
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this conversation.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
|
{this.state.posts ? (
|
||||||
|
<div>
|
||||||
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this conversation.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(Conversation));
|
export default withStyles(styles)(withSnackbar(Conversation));
|
||||||
|
|
|
@ -1,193 +1,225 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, Slide} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './PageLayout.styles';
|
withStyles,
|
||||||
import Post from '../components/Post';
|
CircularProgress,
|
||||||
import { Status } from '../types/Status';
|
Typography,
|
||||||
import Mastodon, { StreamListener } from 'megalodon';
|
Paper,
|
||||||
import {withSnackbar} from 'notistack';
|
Button,
|
||||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Slide
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Post from "../components/Post";
|
||||||
|
import { Status } from "../types/Status";
|
||||||
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
|
import { withSnackbar } from "notistack";
|
||||||
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface IHomePageState {
|
interface IHomePageState {
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
backlogPosts?: [Status] | null;
|
backlogPosts?: [Status] | null;
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
viewDidErrorCode?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HomePage extends Component<any, IHomePageState> {
|
class HomePage extends Component<any, IHomePageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
streamListener: StreamListener;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
streamListener: StreamListener;
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
viewIsLoading: true,
|
||||||
|
backlogPosts: null
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.client = new Mastodon(
|
||||||
viewIsLoading: true,
|
localStorage.getItem("access_token") as string,
|
||||||
backlogPosts: null
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
}
|
);
|
||||||
|
this.streamListener = this.client.stream("/streaming/user");
|
||||||
|
}
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
componentWillMount() {
|
||||||
this.streamListener = this.client.stream('/streaming/user');
|
this.streamListener.on("connect", () => {
|
||||||
|
this.client
|
||||||
}
|
.get("/timelines/home", { limit: 40 })
|
||||||
|
.then((resp: any) => {
|
||||||
componentWillMount() {
|
let statuses: [Status] = resp.data;
|
||||||
|
this.setState({
|
||||||
this.streamListener.on('connect', () => {
|
posts: statuses,
|
||||||
this.client.get('/timelines/home', {limit: 40}).then((resp: any) => {
|
viewIsLoading: false,
|
||||||
let statuses: [Status] = resp.data;
|
viewDidLoad: true,
|
||||||
this.setState({
|
viewDidError: false
|
||||||
posts: statuses,
|
});
|
||||||
viewIsLoading: false,
|
})
|
||||||
viewDidLoad: true,
|
.catch((resp: any) => {
|
||||||
viewDidError: false
|
this.setState({
|
||||||
})
|
viewIsLoading: false,
|
||||||
}).catch((resp: any) => {
|
viewDidLoad: true,
|
||||||
this.setState({
|
viewDidError: true,
|
||||||
viewIsLoading: false,
|
viewDidErrorCode: String(resp)
|
||||||
viewDidLoad: true,
|
});
|
||||||
viewDidError: true,
|
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||||
viewDidErrorCode: String(resp)
|
variant: "error"
|
||||||
})
|
});
|
||||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('update', (status: Status) => {
|
this.streamListener.on("update", (status: Status) => {
|
||||||
let queue = this.state.backlogPosts;
|
let queue = this.state.backlogPosts;
|
||||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
if (queue !== null && queue !== undefined) {
|
||||||
this.setState({ backlogPosts: queue });
|
queue.unshift(status);
|
||||||
})
|
} else {
|
||||||
|
queue = [status];
|
||||||
|
}
|
||||||
|
this.setState({ backlogPosts: queue });
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('delete', (id: number) => {
|
this.streamListener.on("delete", (id: number) => {
|
||||||
let posts = this.state.posts;
|
let posts = this.state.posts;
|
||||||
if (posts) {
|
if (posts) {
|
||||||
posts.forEach((post: Status) => {
|
posts.forEach((post: Status) => {
|
||||||
if (posts && parseInt(post.id) === id) {
|
if (posts && parseInt(post.id) === id) {
|
||||||
posts.splice(posts.indexOf(post), 1);
|
posts.splice(posts.indexOf(post), 1);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
this.setState({ posts });
|
this.setState({ posts });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('error', (err: Error) => {
|
this.streamListener.on("error", (err: Error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
viewDidError: true,
|
viewDidError: true,
|
||||||
viewDidErrorCode: err.message
|
viewDidErrorCode: err.message
|
||||||
})
|
});
|
||||||
this.props.enqueueSnackbar("An error occured.", {
|
this.props.enqueueSnackbar("An error occured.", {
|
||||||
variant: 'error',
|
variant: "error"
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('heartbeat', () => {
|
this.streamListener.on("heartbeat", () => {});
|
||||||
|
}
|
||||||
})
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.streamListener.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBacklog() {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
let posts = this.state.posts;
|
||||||
|
let backlog = this.state.backlogPosts;
|
||||||
|
if (posts && backlog && backlog.length > 0) {
|
||||||
|
let push = backlog.concat(posts);
|
||||||
|
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
loadMoreTimelinePieces() {
|
||||||
this.streamListener.stop();
|
this.setState({ viewDidLoad: false, viewIsLoading: true });
|
||||||
|
if (this.state.posts) {
|
||||||
|
this.client
|
||||||
|
.get("/timelines/home", {
|
||||||
|
max_id: this.state.posts[this.state.posts.length - 1].id,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
.then((resp: any) => {
|
||||||
|
let newPosts: [Status] = resp.data;
|
||||||
|
let posts = this.state.posts as [Status];
|
||||||
|
newPosts.forEach((post: Status) => {
|
||||||
|
posts.push(post);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
posts
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("Failed to get posts", {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
insertBacklog() {
|
render() {
|
||||||
window.scrollTo(0, 0);
|
const { classes } = this.props;
|
||||||
let posts = this.state.posts;
|
|
||||||
let backlog = this.state.backlogPosts;
|
|
||||||
if (posts && backlog && backlog.length > 0) {
|
|
||||||
let push = backlog.concat(posts);
|
|
||||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreTimelinePieces() {
|
return (
|
||||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
if (this.state.posts) {
|
{this.state.backlogPosts ? (
|
||||||
this.client.get('/timelines/home', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
<div className={classes.pageTopChipContainer}>
|
||||||
let newPosts: [Status] = resp.data;
|
<div className={classes.pageTopChips}>
|
||||||
let posts = this.state.posts as [Status];
|
<Slide direction="down" in={true}>
|
||||||
newPosts.forEach((post: Status) => {
|
<Chip
|
||||||
posts.push(post);
|
avatar={
|
||||||
});
|
<Avatar>
|
||||||
this.setState({
|
<ArrowUpwardIcon />
|
||||||
viewIsLoading: false,
|
</Avatar>
|
||||||
viewDidLoad: true,
|
}
|
||||||
posts
|
label={`View ${this.state.backlogPosts.length} new post${
|
||||||
})
|
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||||
}).catch((err: Error) => {
|
}`}
|
||||||
this.setState({
|
color="primary"
|
||||||
viewIsLoading: false,
|
className={classes.pageTopChip}
|
||||||
viewDidError: true,
|
onClick={() => this.insertBacklog()}
|
||||||
viewDidErrorCode: err.message
|
clickable
|
||||||
})
|
/>
|
||||||
this.props.enqueueSnackbar("Failed to get posts", {
|
</Slide>
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.pageLayoutMaxConstraints}>
|
|
||||||
{
|
|
||||||
this.state.backlogPosts?
|
|
||||||
<div className={classes.pageTopChipContainer}>
|
|
||||||
<div className={classes.pageTopChips}>
|
|
||||||
<Slide direction="down" in={true}>
|
|
||||||
<Chip
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ArrowUpwardIcon/>
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
|
||||||
color="primary"
|
|
||||||
className={classes.pageTopChip}
|
|
||||||
onClick={() => this.insertBacklog()}
|
|
||||||
clickable
|
|
||||||
/>
|
|
||||||
</Slide>
|
|
||||||
</div>
|
|
||||||
</div>: null
|
|
||||||
}
|
|
||||||
{ this.state.posts?
|
|
||||||
<div>
|
|
||||||
{ this.state.posts.map((post: Status) => {
|
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
|
||||||
}) }
|
|
||||||
<br/>
|
|
||||||
{
|
|
||||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
|
||||||
}
|
|
||||||
</div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
) : null}
|
||||||
|
{this.state.posts ? (
|
||||||
|
<div>
|
||||||
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
|
<div
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
onClick={() => this.loadMoreTimelinePieces()}
|
||||||
|
>
|
||||||
|
<Button variant="contained">Load more</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(HomePage));
|
export default withStyles(styles)(withSnackbar(HomePage));
|
||||||
|
|
|
@ -1,193 +1,226 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, Slide} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './PageLayout.styles';
|
withStyles,
|
||||||
import Post from '../components/Post';
|
CircularProgress,
|
||||||
import { Status } from '../types/Status';
|
Typography,
|
||||||
import Mastodon, { StreamListener } from 'megalodon';
|
Paper,
|
||||||
import {withSnackbar} from 'notistack';
|
Button,
|
||||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Slide
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Post from "../components/Post";
|
||||||
|
import { Status } from "../types/Status";
|
||||||
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
|
import { withSnackbar } from "notistack";
|
||||||
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface ILocalPageState {
|
interface ILocalPageState {
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
backlogPosts?: [Status] | null;
|
backlogPosts?: [Status] | null;
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
viewDidErrorCode?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class LocalPage extends Component<any, ILocalPageState> {
|
class LocalPage extends Component<any, ILocalPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
streamListener: StreamListener;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
streamListener: StreamListener;
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
viewIsLoading: true,
|
||||||
|
backlogPosts: null
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.client = new Mastodon(
|
||||||
viewIsLoading: true,
|
localStorage.getItem("access_token") as string,
|
||||||
backlogPosts: null
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
}
|
);
|
||||||
|
this.streamListener = this.client.stream("/streaming/public/local");
|
||||||
|
}
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
componentWillMount() {
|
||||||
this.streamListener = this.client.stream('/streaming/public/local');
|
this.streamListener.on("connect", () => {
|
||||||
|
this.client
|
||||||
}
|
.get("/timelines/public", { limit: 40, local: true })
|
||||||
|
.then((resp: any) => {
|
||||||
componentWillMount() {
|
let statuses: [Status] = resp.data;
|
||||||
|
this.setState({
|
||||||
this.streamListener.on('connect', () => {
|
posts: statuses,
|
||||||
this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => {
|
viewIsLoading: false,
|
||||||
let statuses: [Status] = resp.data;
|
viewDidLoad: true,
|
||||||
this.setState({
|
viewDidError: false
|
||||||
posts: statuses,
|
});
|
||||||
viewIsLoading: false,
|
})
|
||||||
viewDidLoad: true,
|
.catch((resp: any) => {
|
||||||
viewDidError: false
|
this.setState({
|
||||||
})
|
viewIsLoading: false,
|
||||||
}).catch((resp: any) => {
|
viewDidLoad: true,
|
||||||
this.setState({
|
viewDidError: true,
|
||||||
viewIsLoading: false,
|
viewDidErrorCode: String(resp)
|
||||||
viewDidLoad: true,
|
});
|
||||||
viewDidError: true,
|
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||||
viewDidErrorCode: String(resp)
|
variant: "error"
|
||||||
})
|
});
|
||||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('update', (status: Status) => {
|
this.streamListener.on("update", (status: Status) => {
|
||||||
let queue = this.state.backlogPosts;
|
let queue = this.state.backlogPosts;
|
||||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
if (queue !== null && queue !== undefined) {
|
||||||
this.setState({ backlogPosts: queue });
|
queue.unshift(status);
|
||||||
})
|
} else {
|
||||||
|
queue = [status];
|
||||||
|
}
|
||||||
|
this.setState({ backlogPosts: queue });
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('delete', (id: number) => {
|
this.streamListener.on("delete", (id: number) => {
|
||||||
let posts = this.state.posts;
|
let posts = this.state.posts;
|
||||||
if (posts) {
|
if (posts) {
|
||||||
posts.forEach((post: Status) => {
|
posts.forEach((post: Status) => {
|
||||||
if (posts && parseInt(post.id) === id) {
|
if (posts && parseInt(post.id) === id) {
|
||||||
posts.splice(posts.indexOf(post), 1);
|
posts.splice(posts.indexOf(post), 1);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
this.setState({ posts });
|
this.setState({ posts });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('error', (err: Error) => {
|
this.streamListener.on("error", (err: Error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
viewDidError: true,
|
viewDidError: true,
|
||||||
viewDidErrorCode: err.message
|
viewDidErrorCode: err.message
|
||||||
})
|
});
|
||||||
this.props.enqueueSnackbar("An error occured.", {
|
this.props.enqueueSnackbar("An error occured.", {
|
||||||
variant: 'error',
|
variant: "error"
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('heartbeat', () => {
|
this.streamListener.on("heartbeat", () => {});
|
||||||
|
}
|
||||||
})
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.streamListener.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBacklog() {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
let posts = this.state.posts;
|
||||||
|
let backlog = this.state.backlogPosts;
|
||||||
|
if (posts && backlog && backlog.length > 0) {
|
||||||
|
let push = backlog.concat(posts);
|
||||||
|
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
loadMoreTimelinePieces() {
|
||||||
this.streamListener.stop();
|
this.setState({ viewDidLoad: false, viewIsLoading: true });
|
||||||
|
if (this.state.posts) {
|
||||||
|
this.client
|
||||||
|
.get("/timelines/public", {
|
||||||
|
max_id: this.state.posts[this.state.posts.length - 1].id,
|
||||||
|
limit: 20,
|
||||||
|
local: true
|
||||||
|
})
|
||||||
|
.then((resp: any) => {
|
||||||
|
let newPosts: [Status] = resp.data;
|
||||||
|
let posts = this.state.posts as [Status];
|
||||||
|
newPosts.forEach((post: Status) => {
|
||||||
|
posts.push(post);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
posts
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("Failed to get posts", {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
insertBacklog() {
|
render() {
|
||||||
window.scrollTo(0, 0);
|
const { classes } = this.props;
|
||||||
let posts = this.state.posts;
|
|
||||||
let backlog = this.state.backlogPosts;
|
|
||||||
if (posts && backlog && backlog.length > 0) {
|
|
||||||
let push = backlog.concat(posts);
|
|
||||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreTimelinePieces() {
|
return (
|
||||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
if (this.state.posts) {
|
{this.state.backlogPosts ? (
|
||||||
this.client.get('/timelines/public', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20, local: true }).then((resp: any) => {
|
<div className={classes.pageTopChipContainer}>
|
||||||
let newPosts: [Status] = resp.data;
|
<div className={classes.pageTopChips}>
|
||||||
let posts = this.state.posts as [Status];
|
<Slide direction="down" in={true}>
|
||||||
newPosts.forEach((post: Status) => {
|
<Chip
|
||||||
posts.push(post);
|
avatar={
|
||||||
});
|
<Avatar>
|
||||||
this.setState({
|
<ArrowUpwardIcon />
|
||||||
viewIsLoading: false,
|
</Avatar>
|
||||||
viewDidLoad: true,
|
}
|
||||||
posts
|
label={`View ${this.state.backlogPosts.length} new post${
|
||||||
})
|
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||||
}).catch((err: Error) => {
|
}`}
|
||||||
this.setState({
|
color="primary"
|
||||||
viewIsLoading: false,
|
className={classes.pageTopChip}
|
||||||
viewDidError: true,
|
onClick={() => this.insertBacklog()}
|
||||||
viewDidErrorCode: err.message
|
clickable
|
||||||
})
|
/>
|
||||||
this.props.enqueueSnackbar("Failed to get posts", {
|
</Slide>
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.pageLayoutMaxConstraints}>
|
|
||||||
{
|
|
||||||
this.state.backlogPosts?
|
|
||||||
<div className={classes.pageTopChipContainer}>
|
|
||||||
<div className={classes.pageTopChips}>
|
|
||||||
<Slide direction="down" in={true}>
|
|
||||||
<Chip
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ArrowUpwardIcon/>
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
|
||||||
color="primary"
|
|
||||||
className={classes.pageTopChip}
|
|
||||||
onClick={() => this.insertBacklog()}
|
|
||||||
clickable
|
|
||||||
/>
|
|
||||||
</Slide>
|
|
||||||
</div>
|
|
||||||
</div>: null
|
|
||||||
}
|
|
||||||
{ this.state.posts?
|
|
||||||
<div>
|
|
||||||
{this.state.posts.map((post: Status) => {
|
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
|
||||||
})}
|
|
||||||
<br/>
|
|
||||||
{
|
|
||||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
|
||||||
}
|
|
||||||
</div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
) : null}
|
||||||
|
{this.state.posts ? (
|
||||||
|
<div>
|
||||||
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
|
<div
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
onClick={() => this.loadMoreTimelinePieces()}
|
||||||
|
>
|
||||||
|
<Button variant="contained">Load more</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(LocalPage));
|
export default withStyles(styles)(withSnackbar(LocalPage));
|
||||||
|
|
|
@ -1,107 +1,130 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {withStyles, ListSubheader, Paper, List, ListItem, ListItemText, CircularProgress, ListItemAvatar, Avatar, ListItemSecondaryAction, Tooltip} from '@material-ui/core';
|
import {
|
||||||
import PersonIcon from '@material-ui/icons/Person';
|
withStyles,
|
||||||
import ForumIcon from '@material-ui/icons/Forum';
|
ListSubheader,
|
||||||
import {styles} from './PageLayout.styles';
|
Paper,
|
||||||
import Mastodon from 'megalodon';
|
List,
|
||||||
import { Status } from '../types/Status';
|
ListItem,
|
||||||
import { LinkableIconButton, LinkableAvatar } from '../interfaces/overrides';
|
ListItemText,
|
||||||
|
CircularProgress,
|
||||||
|
ListItemAvatar,
|
||||||
|
Avatar,
|
||||||
|
ListItemSecondaryAction,
|
||||||
|
Tooltip
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
|
import ForumIcon from "@material-ui/icons/Forum";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Mastodon from "megalodon";
|
||||||
|
import { Status } from "../types/Status";
|
||||||
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
|
|
||||||
interface IMessagesState {
|
interface IMessagesState {
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
viewDidErrorCode?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
class MessagesPage extends Component<any, IMessagesState> {
|
class MessagesPage extends Component<any, IMessagesState> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.client = new Mastodon(
|
||||||
super(props);
|
localStorage.getItem("access_token") as string,
|
||||||
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
|
);
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
this.state = {
|
||||||
|
viewIsLoading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
componentWillMount() {
|
||||||
viewIsLoading: true
|
this.client.get("/conversations").then(resp => {
|
||||||
|
let data: any = resp.data;
|
||||||
|
let messages: any = [];
|
||||||
|
|
||||||
|
data.forEach((message: any) => {
|
||||||
|
if (message.last_status !== null) {
|
||||||
|
messages.push(message.last_status);
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
componentWillMount() {
|
this.setState({
|
||||||
this.client.get('/conversations')
|
posts: messages,
|
||||||
.then((resp) => {
|
viewIsLoading: false,
|
||||||
let data:any = resp.data;
|
viewDidLoad: true
|
||||||
let messages: any = [];
|
});
|
||||||
|
});
|
||||||
data.forEach((message: any) => {
|
}
|
||||||
if (message.last_status !== null) {
|
|
||||||
messages.push(message.last_status);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.setState({
|
removeHTMLContent(text: string) {
|
||||||
posts: messages,
|
const div = document.createElement("div");
|
||||||
viewIsLoading: false,
|
div.innerHTML = text;
|
||||||
viewDidLoad: true
|
let innerContent = div.textContent || div.innerText || "";
|
||||||
});
|
innerContent = innerContent.slice(0, 100) + "...";
|
||||||
});
|
return innerContent;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeHTMLContent(text: string) {
|
render() {
|
||||||
const div = document.createElement('div');
|
const { classes } = this.props;
|
||||||
div.innerHTML = text;
|
return (
|
||||||
let innerContent = div.textContent || div.innerText || "";
|
<div className={classes.pageLayoutConstraints}>
|
||||||
innerContent = innerContent.slice(0, 100) + "..."
|
{this.state.viewDidLoad ? (
|
||||||
return innerContent;
|
<div className={classes.pageListContsraints}>
|
||||||
}
|
<ListSubheader>Recent messages</ListSubheader>
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
render() {
|
<List>
|
||||||
const { classes } = this.props;
|
{this.state.posts
|
||||||
return (
|
? this.state.posts.map((message: Status) => {
|
||||||
<div className={classes.pageLayoutConstraints}>
|
return (
|
||||||
{
|
<ListItem>
|
||||||
this.state.viewDidLoad?
|
<ListItemAvatar>
|
||||||
<div className={classes.pageListContsraints}>
|
<LinkableAvatar
|
||||||
<ListSubheader>Recent messages</ListSubheader>
|
to={`/profile/${message.account.id}`}
|
||||||
<Paper className={classes.pageListConstraints}>
|
alt={message.account.username}
|
||||||
<List>
|
src={message.account.avatar_static}
|
||||||
{
|
>
|
||||||
this.state.posts?
|
<PersonIcon />
|
||||||
this.state.posts.map((message: Status) => {
|
</LinkableAvatar>
|
||||||
return (
|
</ListItemAvatar>
|
||||||
<ListItem>
|
<ListItemText
|
||||||
<ListItemAvatar>
|
primary={
|
||||||
<LinkableAvatar to={`/profile/${message.account.id}`} alt={message.account.username} src={message.account.avatar_static}>
|
message.account.display_name ||
|
||||||
<PersonIcon/>
|
"@" + message.account.acct
|
||||||
</LinkableAvatar>
|
}
|
||||||
</ListItemAvatar>
|
secondary={this.removeHTMLContent(message.content)}
|
||||||
<ListItemText primary={message.account.display_name || "@" + message.account.acct} secondary={this.removeHTMLContent(message.content)}/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Tooltip title="View conversation">
|
<Tooltip title="View conversation">
|
||||||
<LinkableIconButton to={`/conversation/${message.id}`}>
|
<LinkableIconButton
|
||||||
<ForumIcon/>
|
to={`/conversation/${message.id}`}
|
||||||
</LinkableIconButton>
|
>
|
||||||
</Tooltip>
|
<ForumIcon />
|
||||||
</ListItemSecondaryAction>
|
</LinkableIconButton>
|
||||||
</ListItem>
|
</Tooltip>
|
||||||
)
|
</ListItemSecondaryAction>
|
||||||
}): null
|
</ListItem>
|
||||||
}
|
);
|
||||||
</List>
|
})
|
||||||
</Paper>
|
: null}
|
||||||
<br/>
|
</List>
|
||||||
</div>: null
|
</Paper>
|
||||||
}
|
<br />
|
||||||
{
|
</div>
|
||||||
this.state.viewIsLoading?
|
) : null}
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
{this.state.viewIsLoading ? (
|
||||||
null
|
<div style={{ textAlign: "center" }}>
|
||||||
}
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
</div>
|
</div>
|
||||||
)
|
) : null}
|
||||||
}
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(MessagesPage);
|
export default withStyles(styles)(MessagesPage);
|
||||||
|
|
|
@ -1,25 +1,28 @@
|
||||||
import React, {Component} from 'react';
|
import React, { Component } from "react";
|
||||||
import {withStyles, Typography} from '@material-ui/core';
|
import { withStyles, Typography } from "@material-ui/core";
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import {LinkableButton} from '../interfaces/overrides';
|
import { LinkableButton } from "../interfaces/overrides";
|
||||||
|
|
||||||
class Missingno extends Component<any, any> {
|
class Missingno extends Component<any, any> {
|
||||||
|
render() {
|
||||||
render() {
|
const { classes } = this.props;
|
||||||
const {classes} = this.props;
|
return (
|
||||||
return (
|
<div className={classes.pageLayoutConstraints}>
|
||||||
<div className={classes.pageLayoutConstraints}>
|
<div>
|
||||||
<div>
|
<Typography variant="h4" component="h1">
|
||||||
<Typography variant="h4" component="h1"><b>Uh oh!</b></Typography>
|
<b>Uh oh!</b>
|
||||||
<Typography variant="h6" component="p">The part of Hyperspace you're looking for isn't here.</Typography>
|
</Typography>
|
||||||
<br/>
|
<Typography variant="h6" component="p">
|
||||||
<LinkableButton to="/home" color="primary" variant="contained">
|
The part of Hyperspace you're looking for isn't here.
|
||||||
Go back to home timeline
|
</Typography>
|
||||||
</LinkableButton>
|
<br />
|
||||||
</div>
|
<LinkableButton to="/home" color="primary" variant="contained">
|
||||||
</div>
|
Go back to home timeline
|
||||||
)
|
</LinkableButton>
|
||||||
}
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(Missingno);
|
export default withStyles(styles)(Missingno);
|
||||||
|
|
|
@ -1,304 +1,376 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
IconButton,
|
||||||
withStyles,
|
withStyles,
|
||||||
Typography,
|
Typography,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Button,
|
Button,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Tooltip
|
Tooltip
|
||||||
} from '@material-ui/core';
|
} from "@material-ui/core";
|
||||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
import PersonIcon from '@material-ui/icons/Person';
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
import DeleteIcon from '@material-ui/icons/Delete';
|
import DeleteIcon from "@material-ui/icons/Delete";
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import { LinkableIconButton, LinkableAvatar } from '../interfaces/overrides';
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
import ForumIcon from '@material-ui/icons/Forum';
|
import ForumIcon from "@material-ui/icons/Forum";
|
||||||
import ReplyIcon from '@material-ui/icons/Reply';
|
import ReplyIcon from "@material-ui/icons/Reply";
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
import { Notification } from '../types/Notification';
|
import { Notification } from "../types/Notification";
|
||||||
import { Account } from '../types/Account';
|
import { Account } from "../types/Account";
|
||||||
import { withSnackbar } from 'notistack';
|
import { withSnackbar } from "notistack";
|
||||||
|
|
||||||
interface INotificationsPageState {
|
interface INotificationsPageState {
|
||||||
notifications?: [Notification];
|
notifications?: [Notification];
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
deleteDialogOpen: boolean;
|
deleteDialogOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class NotificationsPage extends Component<any, INotificationsPageState> {
|
class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
streamListener: any;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
streamListener: any;
|
super(props);
|
||||||
|
this.client = new Mastodon(
|
||||||
|
localStorage.getItem("access_token") as string,
|
||||||
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
|
);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
viewIsLoading: true,
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
deleteDialogOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
componentWillMount() {
|
||||||
viewIsLoading: true,
|
this.client
|
||||||
deleteDialogOpen: false
|
.get("/notifications")
|
||||||
}
|
.then((resp: any) => {
|
||||||
}
|
let notifications: [Notification] = resp.data;
|
||||||
|
this.setState({
|
||||||
componentWillMount() {
|
notifications,
|
||||||
this.client.get('/notifications').then((resp: any) => {
|
viewIsLoading: false,
|
||||||
let notifications: [Notification] = resp.data;
|
viewDidLoad: true
|
||||||
this.setState({
|
|
||||||
notifications,
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidLoad: true
|
|
||||||
})
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewDidLoad: true,
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
componentDidMount() {
|
|
||||||
this.streamNotifications();
|
|
||||||
}
|
|
||||||
|
|
||||||
streamNotifications() {
|
|
||||||
this.streamListener = this.client.stream('/streaming/user');
|
|
||||||
|
|
||||||
this.streamListener.on('notification', (notif: Notification) => {
|
|
||||||
let notifications = this.state.notifications;
|
|
||||||
if (notifications) {
|
|
||||||
notifications.unshift(notif);
|
|
||||||
this.setState({ notifications });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
toggleDeleteDialog() {
|
|
||||||
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHTMLContent(text: string) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = text;
|
|
||||||
let innerContent = div.textContent || div.innerText || "";
|
|
||||||
if (innerContent.length > 65)
|
|
||||||
innerContent = innerContent.slice(0, 65) + "...";
|
|
||||||
return innerContent;
|
|
||||||
}
|
|
||||||
|
|
||||||
removeNotification(id: string) {
|
|
||||||
this.client.post('/notifications/dismiss', {id: id}).then((resp: any) => {
|
|
||||||
let notifications = this.state.notifications;
|
|
||||||
if (notifications !== undefined && notifications.length > 0) {
|
|
||||||
notifications.forEach((notification: Notification) => {
|
|
||||||
if (notifications !== undefined && notification.id === id) {
|
|
||||||
notifications.splice(notifications.indexOf(notification), 1);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
this.setState({ notifications })
|
|
||||||
this.props.enqueueSnackbar("Notification deleted.");
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't delete notification: " + err.name, {
|
|
||||||
variant: 'error'
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewDidLoad: true,
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
removeAllNotifications() {
|
componentDidMount() {
|
||||||
this.client.post('/notifications/clear').then((resp: any) => {
|
this.streamNotifications();
|
||||||
this.setState({ notifications: undefined })
|
}
|
||||||
this.props.enqueueSnackbar('All notifications deleted.');
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't delete notifications: " + err.name, {
|
|
||||||
variant: 'error'
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
createNotification(notif: Notification) {
|
streamNotifications() {
|
||||||
const { classes } = this.props;
|
this.streamListener = this.client.stream("/streaming/user");
|
||||||
let primary = "";
|
|
||||||
let secondary = "";
|
this.streamListener.on("notification", (notif: Notification) => {
|
||||||
switch (notif.type) {
|
let notifications = this.state.notifications;
|
||||||
case "follow":
|
if (notifications) {
|
||||||
primary = `${notif.account.display_name || notif.account.username} is now following you!`;
|
notifications.unshift(notif);
|
||||||
break;
|
this.setState({ notifications });
|
||||||
case "mention":
|
}
|
||||||
primary = `${notif.account.display_name || notif.account.username} mentioned you in a post.`;
|
});
|
||||||
secondary = this.removeHTMLContent(notif.status? notif.status.content: "");
|
}
|
||||||
break;
|
|
||||||
case "reblog":
|
toggleDeleteDialog() {
|
||||||
primary = `${notif.account.display_name || notif.account.username} reblogged your post.`;
|
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
|
||||||
secondary = this.removeHTMLContent(notif.status? notif.status.content: "");
|
}
|
||||||
break;
|
|
||||||
case "favourite":
|
removeHTMLContent(text: string) {
|
||||||
primary = `${notif.account.display_name || notif.account.username} favorited your post.`;
|
const div = document.createElement("div");
|
||||||
secondary = this.removeHTMLContent(notif.status? notif.status.content: "");
|
div.innerHTML = text;
|
||||||
break;
|
let innerContent = div.textContent || div.innerText || "";
|
||||||
default:
|
if (innerContent.length > 65)
|
||||||
if (notif.status && notif.status.poll) {
|
innerContent = innerContent.slice(0, 65) + "...";
|
||||||
primary = "A poll you voted in or created has ended.";
|
return innerContent;
|
||||||
secondary = this.removeHTMLContent(notif.status? notif.status.content: "");
|
}
|
||||||
} else {
|
|
||||||
primary = "A magical thing happened!";
|
removeNotification(id: string) {
|
||||||
}
|
this.client
|
||||||
break;
|
.post("/notifications/dismiss", { id: id })
|
||||||
|
.then((resp: any) => {
|
||||||
|
let notifications = this.state.notifications;
|
||||||
|
if (notifications !== undefined && notifications.length > 0) {
|
||||||
|
notifications.forEach((notification: Notification) => {
|
||||||
|
if (notifications !== undefined && notification.id === id) {
|
||||||
|
notifications.splice(notifications.indexOf(notification), 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return (
|
this.setState({ notifications });
|
||||||
<ListItem key={notif.id}>
|
this.props.enqueueSnackbar("Notification deleted.");
|
||||||
<ListItemAvatar>
|
})
|
||||||
<LinkableAvatar alt={notif.account.username} src={notif.account.avatar_static} to={`/profile/${notif.account.id}`}>
|
.catch((err: Error) => {
|
||||||
<PersonIcon/>
|
this.props.enqueueSnackbar(
|
||||||
</LinkableAvatar>
|
"Couldn't delete notification: " + err.name,
|
||||||
</ListItemAvatar>
|
{
|
||||||
<ListItemText primary={primary} secondary={
|
variant: "error"
|
||||||
<span>
|
}
|
||||||
<Typography color="textSecondary" className={classes.mobileOnly}>
|
);
|
||||||
{secondary.slice(0, 35) + "..."}
|
});
|
||||||
</Typography>
|
}
|
||||||
<Typography color="textSecondary" className={classes.desktopOnly}>
|
|
||||||
{secondary}
|
|
||||||
</Typography>
|
|
||||||
</span>
|
|
||||||
}/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
{
|
|
||||||
notif.type === "follow"?
|
|
||||||
<span>
|
|
||||||
<Tooltip title="View profile">
|
|
||||||
<LinkableIconButton to={`/profile/${notif.account.id}`}>
|
|
||||||
<AssignmentIndIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Follow account">
|
|
||||||
<IconButton onClick={() => this.followMember(notif.account)}>
|
|
||||||
<PersonAddIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</span>:
|
|
||||||
|
|
||||||
notif.status?
|
removeAllNotifications() {
|
||||||
<span>
|
this.client
|
||||||
<Tooltip title="View conversation">
|
.post("/notifications/clear")
|
||||||
<LinkableIconButton to={`/conversation/${notif.status.id}`}>
|
.then((resp: any) => {
|
||||||
<ForumIcon/>
|
this.setState({ notifications: undefined });
|
||||||
</LinkableIconButton>
|
this.props.enqueueSnackbar("All notifications deleted.");
|
||||||
</Tooltip>
|
})
|
||||||
{
|
.catch((err: Error) => {
|
||||||
notif.type === "mention"?
|
this.props.enqueueSnackbar(
|
||||||
<Tooltip title="Reply">
|
"Couldn't delete notifications: " + err.name,
|
||||||
<LinkableIconButton to={`/compose?reply=${notif.status.reblog? notif.status.reblog.id: notif.status.id}&visibility=${notif.status.visibility}&acct=${notif.status.reblog? notif.status.reblog.account.acct: notif.status.account.acct}`}>
|
{
|
||||||
<ReplyIcon/>
|
variant: "error"
|
||||||
</LinkableIconButton>
|
}
|
||||||
</Tooltip>: null
|
);
|
||||||
}
|
});
|
||||||
</span>:
|
}
|
||||||
null
|
|
||||||
|
createNotification(notif: Notification) {
|
||||||
|
const { classes } = this.props;
|
||||||
|
let primary = "";
|
||||||
|
let secondary = "";
|
||||||
|
switch (notif.type) {
|
||||||
|
case "follow":
|
||||||
|
primary = `${notif.account.display_name ||
|
||||||
|
notif.account.username} is now following you!`;
|
||||||
|
break;
|
||||||
|
case "mention":
|
||||||
|
primary = `${notif.account.display_name ||
|
||||||
|
notif.account.username} mentioned you in a post.`;
|
||||||
|
secondary = this.removeHTMLContent(
|
||||||
|
notif.status ? notif.status.content : ""
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "reblog":
|
||||||
|
primary = `${notif.account.display_name ||
|
||||||
|
notif.account.username} reblogged your post.`;
|
||||||
|
secondary = this.removeHTMLContent(
|
||||||
|
notif.status ? notif.status.content : ""
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
case "favourite":
|
||||||
|
primary = `${notif.account.display_name ||
|
||||||
|
notif.account.username} favorited your post.`;
|
||||||
|
secondary = this.removeHTMLContent(
|
||||||
|
notif.status ? notif.status.content : ""
|
||||||
|
);
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
if (notif.status && notif.status.poll) {
|
||||||
|
primary = "A poll you voted in or created has ended.";
|
||||||
|
secondary = this.removeHTMLContent(
|
||||||
|
notif.status ? notif.status.content : ""
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
primary = "A magical thing happened!";
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<ListItem key={notif.id}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
alt={notif.account.username}
|
||||||
|
src={notif.account.avatar_static}
|
||||||
|
to={`/profile/${notif.account.id}`}
|
||||||
|
>
|
||||||
|
<PersonIcon />
|
||||||
|
</LinkableAvatar>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={primary}
|
||||||
|
secondary={
|
||||||
|
<span>
|
||||||
|
<Typography color="textSecondary" className={classes.mobileOnly}>
|
||||||
|
{secondary.slice(0, 35) + "..."}
|
||||||
|
</Typography>
|
||||||
|
<Typography color="textSecondary" className={classes.desktopOnly}>
|
||||||
|
{secondary}
|
||||||
|
</Typography>
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
{notif.type === "follow" ? (
|
||||||
|
<span>
|
||||||
|
<Tooltip title="View profile">
|
||||||
|
<LinkableIconButton to={`/profile/${notif.account.id}`}>
|
||||||
|
<AssignmentIndIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Follow account">
|
||||||
|
<IconButton onClick={() => this.followMember(notif.account)}>
|
||||||
|
<PersonAddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</span>
|
||||||
|
) : notif.status ? (
|
||||||
|
<span>
|
||||||
|
<Tooltip title="View conversation">
|
||||||
|
<LinkableIconButton to={`/conversation/${notif.status.id}`}>
|
||||||
|
<ForumIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
{notif.type === "mention" ? (
|
||||||
|
<Tooltip title="Reply">
|
||||||
|
<LinkableIconButton
|
||||||
|
to={`/compose?reply=${
|
||||||
|
notif.status.reblog
|
||||||
|
? notif.status.reblog.id
|
||||||
|
: notif.status.id
|
||||||
|
}&visibility=${notif.status.visibility}&acct=${
|
||||||
|
notif.status.reblog
|
||||||
|
? notif.status.reblog.account.acct
|
||||||
|
: notif.status.account.acct
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<ReplyIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
<Tooltip title="Remove notification">
|
||||||
|
<IconButton onClick={() => this.removeNotification(notif.id)}>
|
||||||
|
<DeleteIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
followMember(acct: Account) {
|
||||||
|
this.client
|
||||||
|
.post(`/accounts/${acct.id}/follow`)
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.props.enqueueSnackbar("You are now following this account.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.pageLayoutConstraints}>
|
||||||
|
{this.state.viewDidLoad ? (
|
||||||
|
this.state.notifications && this.state.notifications.length > 0 ? (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Recent notifications</ListSubheader>
|
||||||
|
<Button
|
||||||
|
className={classes.clearAllButton}
|
||||||
|
variant="text"
|
||||||
|
onClick={() => this.toggleDeleteDialog()}
|
||||||
|
>
|
||||||
|
{" "}
|
||||||
|
Clear All
|
||||||
|
</Button>
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
<List>
|
||||||
|
{this.state.notifications.map(
|
||||||
|
(notification: Notification) => {
|
||||||
|
return this.createNotification(notification);
|
||||||
}
|
}
|
||||||
<Tooltip title="Remove notification">
|
)}
|
||||||
<IconButton onClick={() => this.removeNotification(notif.id)}>
|
</List>
|
||||||
<DeleteIcon/>
|
</Paper>
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
followMember(acct: Account) {
|
|
||||||
this.client.post(`/accounts/${acct.id}/follow`).then((resp: any) => {
|
|
||||||
this.props.enqueueSnackbar('You are now following this account.');
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const { classes } = this.props;
|
|
||||||
return (
|
|
||||||
<div className={classes.pageLayoutConstraints}>
|
|
||||||
{
|
|
||||||
this.state.viewDidLoad?
|
|
||||||
this.state.notifications && this.state.notifications.length > 0?
|
|
||||||
<div>
|
|
||||||
<ListSubheader>Recent notifications</ListSubheader>
|
|
||||||
<Button className={classes.clearAllButton} variant="text" onClick={() => this.toggleDeleteDialog()}> Clear All</Button>
|
|
||||||
<Paper className={classes.pageListConstraints}>
|
|
||||||
<List>
|
|
||||||
{
|
|
||||||
this.state.notifications.map((notification: Notification) => {
|
|
||||||
return this.createNotification(notification)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
</div>:
|
|
||||||
<div className={classes.pageLayoutEmptyTextConstraints}>
|
|
||||||
<Typography variant="h4">All clear!</Typography>
|
|
||||||
<Typography paragraph>It looks like you have no notifications. Why not get the conversation going with a new post?</Typography>
|
|
||||||
</div>:
|
|
||||||
null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
|
|
||||||
<Dialog
|
|
||||||
open={this.state.deleteDialogOpen}
|
|
||||||
onClose={() => this.toggleDeleteDialog()}
|
|
||||||
>
|
|
||||||
<DialogTitle id="alert-dialog-title">Delete all notifications?</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText id="alert-dialog-description">
|
|
||||||
Are you sure you want to delete all notifications? This action cannot be undone.
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => this.toggleDeleteDialog()} color="primary" autoFocus>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
this.removeAllNotifications();
|
|
||||||
this.toggleDeleteDialog();
|
|
||||||
}} color="primary">
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
) : (
|
||||||
}
|
<div className={classes.pageLayoutEmptyTextConstraints}>
|
||||||
|
<Typography variant="h4">All clear!</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
It looks like you have no notifications. Why not get the
|
||||||
|
conversation going with a new post?
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={this.state.deleteDialogOpen}
|
||||||
|
onClose={() => this.toggleDeleteDialog()}
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">
|
||||||
|
Delete all notifications?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Are you sure you want to delete all notifications? This action
|
||||||
|
cannot be undone.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => this.toggleDeleteDialog()}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
this.removeAllNotifications();
|
||||||
|
this.toggleDeleteDialog();
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(NotificationsPage));
|
export default withStyles(styles)(withSnackbar(NotificationsPage));
|
||||||
|
|
|
@ -2,11 +2,12 @@ import { Theme, createStyles } from "@material-ui/core";
|
||||||
import { isDarwinApp } from "../utilities/desktop";
|
import { isDarwinApp } from "../utilities/desktop";
|
||||||
import { isAppbarExpanded } from "../utilities/appbar";
|
import { isAppbarExpanded } from "../utilities/appbar";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
height: '100%'
|
height: "100%"
|
||||||
},
|
},
|
||||||
pageLayoutConstraints: {
|
pageLayoutConstraints: {
|
||||||
marginTop: 72,
|
marginTop: 72,
|
||||||
|
@ -14,87 +15,87 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
paddingLeft: theme.spacing.unit,
|
paddingLeft: theme.spacing.unit,
|
||||||
paddingRight: theme.spacing.unit,
|
paddingRight: theme.spacing.unit,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250,
|
||||||
marginTop: 88,
|
marginTop: 88,
|
||||||
paddingLeft: theme.spacing.unit * 24,
|
paddingLeft: theme.spacing.unit * 24,
|
||||||
paddingRight: theme.spacing.unit * 24
|
paddingRight: theme.spacing.unit * 24
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||||
},
|
},
|
||||||
pageLayoutMaxConstraints: {
|
pageLayoutMaxConstraints: {
|
||||||
marginTop: 72,
|
marginTop: 72,
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
paddingTop: theme.spacing.unit * 2,
|
paddingTop: theme.spacing.unit * 2,
|
||||||
padding: theme.spacing.unit,
|
padding: theme.spacing.unit,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250,
|
||||||
marginTop: 88,
|
marginTop: 88,
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
paddingLeft: theme.spacing.unit * 16,
|
paddingLeft: theme.spacing.unit * 16,
|
||||||
paddingRight: theme.spacing.unit * 16,
|
paddingRight: theme.spacing.unit * 16
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('lg')]: {
|
[theme.breakpoints.up("lg")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250,
|
||||||
marginTop: 88,
|
marginTop: 88,
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
paddingLeft: theme.spacing.unit * 32,
|
paddingLeft: theme.spacing.unit * 32,
|
||||||
paddingRight: theme.spacing.unit * 32,
|
paddingRight: theme.spacing.unit * 32
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('xl')]: {
|
[theme.breakpoints.up("xl")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250,
|
||||||
marginTop: 88,
|
marginTop: 88,
|
||||||
padding: theme.spacing.unit * 3,
|
padding: theme.spacing.unit * 3,
|
||||||
paddingLeft: theme.spacing.unit * 40,
|
paddingLeft: theme.spacing.unit * 40,
|
||||||
paddingRight: theme.spacing.unit * 40,
|
paddingRight: theme.spacing.unit * 40
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||||
},
|
},
|
||||||
pageLayoutMinimalConstraints: {
|
pageLayoutMinimalConstraints: {
|
||||||
flexGrow: 1,
|
flexGrow: 1,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginLeft: 250,
|
marginLeft: 250
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.background.default,
|
backgroundColor: theme.palette.background.default,
|
||||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||||
},
|
},
|
||||||
pageLayoutEmptyTextConstraints: {
|
pageLayoutEmptyTextConstraints: {
|
||||||
paddingLeft: theme.spacing.unit * 2,
|
paddingLeft: theme.spacing.unit * 2,
|
||||||
paddingRight: theme.spacing.unit * 2
|
paddingRight: theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
pageHeroBackground: {
|
pageHeroBackground: {
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
height: 'intrinsic',
|
height: "intrinsic",
|
||||||
backgroundColor: theme.palette.primary.dark,
|
backgroundColor: theme.palette.primary.dark,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white,
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
top: isAppbarExpanded()? 80: 64,
|
top: isAppbarExpanded() ? 80 : 64
|
||||||
},
|
},
|
||||||
pageHeroBackgroundImage: {
|
pageHeroBackgroundImage: {
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
bottom: 0,
|
bottom: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: "center",
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: "no-repeat",
|
||||||
backgroundSize: 'cover',
|
backgroundSize: "cover",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
opacity: 0.35,
|
opacity: 0.35,
|
||||||
zIndex: -1,
|
zIndex: -1,
|
||||||
filter: 'blur(2px)'
|
filter: "blur(2px)"
|
||||||
},
|
},
|
||||||
pageHeroContent: {
|
pageHeroContent: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
paddingTop: 8,
|
paddingTop: 8,
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
paddingLeft: '5%',
|
paddingLeft: "5%",
|
||||||
paddingRight: '5%',
|
paddingRight: "5%"
|
||||||
},
|
},
|
||||||
position: "relative",
|
position: "relative",
|
||||||
zIndex: 1
|
zIndex: 1
|
||||||
|
@ -102,65 +103,65 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
pageHeroToolbar: {
|
pageHeroToolbar: {
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
right: theme.spacing.unit * 2,
|
right: theme.spacing.unit * 2,
|
||||||
marginTop: -16,
|
marginTop: -16
|
||||||
},
|
},
|
||||||
pageListConstraints: {
|
pageListConstraints: {
|
||||||
paddingLeft: theme.spacing.unit,
|
paddingLeft: theme.spacing.unit,
|
||||||
paddingRight: theme.spacing.unit,
|
paddingRight: theme.spacing.unit,
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
paddingLeft: theme.spacing.unit * 2,
|
paddingLeft: theme.spacing.unit * 2,
|
||||||
paddingRight: theme.spacing.unit * 2
|
paddingRight: theme.spacing.unit * 2
|
||||||
},
|
}
|
||||||
//backgroundColor: theme.palette.background.default
|
//backgroundColor: theme.palette.background.default
|
||||||
},
|
},
|
||||||
profileToolbar: {
|
profileToolbar: {
|
||||||
zIndex: 2,
|
zIndex: 2,
|
||||||
paddingTop: 8,
|
paddingTop: 8
|
||||||
},
|
},
|
||||||
profileContent: {
|
profileContent: {
|
||||||
padding: 16,
|
padding: 16,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
paddingLeft: '5%',
|
paddingLeft: "5%",
|
||||||
paddingRight: '5%',
|
paddingRight: "5%",
|
||||||
paddingBottom: 48,
|
paddingBottom: 48,
|
||||||
paddingTop: 24,
|
paddingTop: 24
|
||||||
},
|
},
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
position: 'relative',
|
position: "relative",
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
display: 'flex',
|
display: "flex",
|
||||||
paddingBottom: 24,
|
paddingBottom: 24,
|
||||||
paddingTop: 24,
|
paddingTop: 24
|
||||||
},
|
},
|
||||||
profileAvatar: {
|
profileAvatar: {
|
||||||
width: 64,
|
width: 64,
|
||||||
height: 64,
|
height: 64,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
width: 128,
|
width: 128,
|
||||||
height: 128,
|
height: 128
|
||||||
},
|
},
|
||||||
backgroundColor: theme.palette.primary.main
|
backgroundColor: theme.palette.primary.main
|
||||||
},
|
},
|
||||||
profileUserBox: {
|
profileUserBox: {
|
||||||
paddingLeft: theme.spacing.unit * 2
|
paddingLeft: theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
pageProfileAvatar: {
|
pageProfileAvatar: {
|
||||||
width: 128,
|
width: 128,
|
||||||
height: 128,
|
height: 128,
|
||||||
marginLeft: 'auto',
|
marginLeft: "auto",
|
||||||
marginRight: 'auto',
|
marginRight: "auto",
|
||||||
marginBottom: theme.spacing.unit,
|
marginBottom: theme.spacing.unit,
|
||||||
backgroundColor: theme.palette.primary.main
|
backgroundColor: theme.palette.primary.main
|
||||||
},
|
},
|
||||||
pageProfileNameEmoji: {
|
pageProfileNameEmoji: {
|
||||||
height: theme.typography.h4.fontSize,
|
height: theme.typography.h4.fontSize,
|
||||||
fontWeight: theme.typography.fontWeightMedium,
|
fontWeight: theme.typography.fontWeightMedium
|
||||||
},
|
},
|
||||||
pageProfileStatsDiv: {
|
pageProfileStatsDiv: {
|
||||||
display: 'inline-flex',
|
display: "inline-flex",
|
||||||
marginTop: theme.spacing.unit * 2,
|
marginTop: theme.spacing.unit * 2,
|
||||||
marginBottom: theme.spacing.unit * 2,
|
marginBottom: theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
pageProfileStat: {
|
pageProfileStat: {
|
||||||
marginLeft: theme.spacing.unit,
|
marginLeft: theme.spacing.unit,
|
||||||
|
@ -177,28 +178,28 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
paddingRight: theme.spacing.unit,
|
paddingRight: theme.spacing.unit,
|
||||||
paddingTop: theme.spacing.unit * 12,
|
paddingTop: theme.spacing.unit * 12,
|
||||||
paddingBottom: theme.spacing.unit * 2,
|
paddingBottom: theme.spacing.unit * 2,
|
||||||
[theme.breakpoints.up('lg')]: {
|
[theme.breakpoints.up("lg")]: {
|
||||||
paddingLeft: theme.spacing.unit * 32,
|
paddingLeft: theme.spacing.unit * 32,
|
||||||
paddingRight: theme.spacing.unit * 32
|
paddingRight: theme.spacing.unit * 32
|
||||||
},
|
}
|
||||||
//backgroundColor: theme.palette.background.default,
|
//backgroundColor: theme.palette.background.default,
|
||||||
},
|
},
|
||||||
errorCard: {
|
errorCard: {
|
||||||
padding: theme.spacing.unit * 4,
|
padding: theme.spacing.unit * 4,
|
||||||
backgroundColor: theme.palette.error.main,
|
backgroundColor: theme.palette.error.main
|
||||||
},
|
},
|
||||||
pageTopChipContainer: {
|
pageTopChipContainer: {
|
||||||
zIndex: 24,
|
zIndex: 24,
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
width: '100%'
|
width: "100%"
|
||||||
},
|
},
|
||||||
pageTopChips: {
|
pageTopChips: {
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
marginRight: '55%'
|
marginRight: "55%"
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('xl')]: {
|
[theme.breakpoints.up("xl")]: {
|
||||||
marginRight: '50%'
|
marginRight: "50%"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pageTopChip: {
|
pageTopChip: {
|
||||||
|
@ -206,27 +207,27 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
},
|
},
|
||||||
clearAllButton: {
|
clearAllButton: {
|
||||||
zIndex: 3,
|
zIndex: 3,
|
||||||
position: 'absolute',
|
position: "absolute",
|
||||||
right: 24,
|
right: 24,
|
||||||
top: 100,
|
top: 100,
|
||||||
[theme.breakpoints.up('md')]: {
|
[theme.breakpoints.up("md")]: {
|
||||||
top: 116,
|
top: 116,
|
||||||
right: theme.spacing.unit * 24,
|
right: theme.spacing.unit * 24
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mobileOnly: {
|
mobileOnly: {
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: 'none'
|
display: "none"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
desktopOnly: {
|
desktopOnly: {
|
||||||
display: 'none',
|
display: "none",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
display: 'block'
|
display: "block"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
pageLayoutFooter: {
|
pageLayoutFooter: {
|
||||||
'& a': {
|
"& a": {
|
||||||
color: theme.palette.primary.light
|
color: theme.palette.primary.light
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -235,13 +236,13 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
width: 88
|
width: 88
|
||||||
},
|
},
|
||||||
youPaper: {
|
youPaper: {
|
||||||
padding: theme.spacing.unit * 2,
|
padding: theme.spacing.unit * 2
|
||||||
},
|
},
|
||||||
youGrid: {
|
youGrid: {
|
||||||
textAlign: "center",
|
textAlign: "center",
|
||||||
'& *': {
|
"& *": {
|
||||||
marginLeft: "auto",
|
marginLeft: "auto",
|
||||||
marginRight: "auto",
|
marginRight: "auto"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
youGridAvatar: {
|
youGridAvatar: {
|
||||||
|
@ -249,7 +250,7 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
width: 128
|
width: 128
|
||||||
},
|
},
|
||||||
youGridImage: {
|
youGridImage: {
|
||||||
width: 'auto',
|
width: "auto",
|
||||||
height: 128
|
height: 128
|
||||||
},
|
},
|
||||||
instanceHeaderPaper: {
|
instanceHeaderPaper: {
|
||||||
|
@ -277,6 +278,6 @@ export const styles = (theme: Theme) => createStyles({
|
||||||
color: theme.palette.common.white
|
color: theme.palette.common.white
|
||||||
},
|
},
|
||||||
pageGrow: {
|
pageGrow: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,362 +1,504 @@
|
||||||
import React, {Component} from 'react';
|
import React, { Component } from "react";
|
||||||
import {
|
import {
|
||||||
withStyles,
|
withStyles,
|
||||||
Typography,
|
Typography,
|
||||||
Avatar,
|
Avatar,
|
||||||
Divider,
|
Divider,
|
||||||
Button,
|
Button,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Paper,
|
Paper,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
DialogContentText,
|
DialogContentText,
|
||||||
DialogActions,
|
DialogActions,
|
||||||
Toolbar,
|
Toolbar,
|
||||||
IconButton
|
IconButton
|
||||||
} from '@material-ui/core';
|
} from "@material-ui/core";
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
import { Account } from '../types/Account';
|
import { Account } from "../types/Account";
|
||||||
import { Status } from '../types/Status';
|
import { Status } from "../types/Status";
|
||||||
import { Relationship } from '../types/Relationship';
|
import { Relationship } from "../types/Relationship";
|
||||||
import Post from '../components/Post';
|
import Post from "../components/Post";
|
||||||
import {withSnackbar} from 'notistack';
|
import { withSnackbar } from "notistack";
|
||||||
import { LinkableIconButton } from '../interfaces/overrides';
|
import { LinkableIconButton } from "../interfaces/overrides";
|
||||||
import { emojifyString } from '../utilities/emojis';
|
import { emojifyString } from "../utilities/emojis";
|
||||||
|
|
||||||
import AccountEditIcon from 'mdi-material-ui/AccountEdit';
|
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
|
||||||
import PersonAddDisabledIcon from '@material-ui/icons/PersonAddDisabled';
|
|
||||||
import AccountMinusIcon from 'mdi-material-ui/AccountMinus';
|
|
||||||
import ChatIcon from '@material-ui/icons/Chat';
|
|
||||||
import AccountRemoveIcon from 'mdi-material-ui/AccountRemove';
|
|
||||||
import AccountHeartIcon from 'mdi-material-ui/AccountHeart';
|
|
||||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
|
||||||
|
|
||||||
|
|
||||||
|
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||||
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
|
import PersonAddDisabledIcon from "@material-ui/icons/PersonAddDisabled";
|
||||||
|
import AccountMinusIcon from "mdi-material-ui/AccountMinus";
|
||||||
|
import ChatIcon from "@material-ui/icons/Chat";
|
||||||
|
import AccountRemoveIcon from "mdi-material-ui/AccountRemove";
|
||||||
|
import AccountHeartIcon from "mdi-material-ui/AccountHeart";
|
||||||
|
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||||
|
|
||||||
interface IProfilePageState {
|
interface IProfilePageState {
|
||||||
account?: Account;
|
account?: Account;
|
||||||
relationship?: Relationship;
|
relationship?: Relationship;
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
blockDialogOpen: boolean;
|
blockDialogOpen: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
class ProfilePage extends Component<any, IProfilePageState> {
|
class ProfilePage extends Component<any, IProfilePageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.client = new Mastodon(
|
||||||
super(props);
|
localStorage.getItem("access_token") as string,
|
||||||
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.state = {
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
viewIsLoading: true,
|
||||||
|
blockDialogOpen: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
toggleBlockDialog() {
|
||||||
viewIsLoading: true,
|
if (this.state.relationship && !this.state.relationship.blocking)
|
||||||
blockDialogOpen: false
|
this.setState({ blockDialogOpen: !this.state.blockDialogOpen });
|
||||||
}
|
else this.toggleBlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBlockDialog() {
|
getAccountData(id: string) {
|
||||||
if (this.state.relationship && !this.state.relationship.blocking)
|
this.client
|
||||||
this.setState({ blockDialogOpen: !this.state.blockDialogOpen })
|
.get(`/accounts/${id}`)
|
||||||
else
|
.then((resp: any) => {
|
||||||
this.toggleBlock()
|
let profile: Account = resp.data;
|
||||||
}
|
|
||||||
|
|
||||||
getAccountData(id: string) {
|
const div = document.createElement("div");
|
||||||
this.client.get(`/accounts/${id}`).then((resp: any) => {
|
div.innerHTML = profile.note;
|
||||||
let profile: Account = resp.data;
|
profile.note = div.textContent || div.innerText || "";
|
||||||
|
|
||||||
const div = document.createElement('div');
|
this.setState({
|
||||||
div.innerHTML = profile.note;
|
account: profile
|
||||||
profile.note = div.textContent || div.innerText || "";
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
account: profile
|
|
||||||
})
|
|
||||||
}).catch((error: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: error.message
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
this.getRelationships();
|
})
|
||||||
this.client.get(`/accounts/${id}/statuses`).then((resp: any) => {
|
.catch((error: Error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
posts: resp.data,
|
viewIsLoading: false,
|
||||||
viewIsLoading: false,
|
viewDidError: true,
|
||||||
viewDidLoad: true,
|
viewDidErrorCode: error.message
|
||||||
viewDidError: false
|
|
||||||
})
|
|
||||||
}).catch( (err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
this.getRelationships();
|
||||||
|
this.client
|
||||||
|
.get(`/accounts/${id}/statuses`)
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.setState({
|
||||||
|
posts: resp.data,
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
viewDidError: false
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props: any) {
|
componentWillReceiveProps(props: any) {
|
||||||
this.getAccountData(props.match.params.profileId);
|
this.getAccountData(props.match.params.profileId);
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillMount() {
|
componentWillMount() {
|
||||||
const { match: { params }} = this.props;
|
const {
|
||||||
this.getAccountData(params.profileId);
|
match: { params }
|
||||||
}
|
} = this.props;
|
||||||
|
this.getAccountData(params.profileId);
|
||||||
|
}
|
||||||
|
|
||||||
isItMe(): boolean {
|
isItMe(): boolean {
|
||||||
if (this.state.account) {
|
if (this.state.account) {
|
||||||
return this.state.account.id === JSON.parse(localStorage.getItem('account') as string).id;
|
return (
|
||||||
} else {
|
this.state.account.id ===
|
||||||
return false;
|
JSON.parse(localStorage.getItem("account") as string).id
|
||||||
}
|
);
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getRelationships() {
|
getRelationships() {
|
||||||
this.client.get("/accounts/relationships", {id: this.props.match.params.profileId }).then((resp: any) => {
|
this.client
|
||||||
let relationship: Relationship = resp.data[0];
|
.get("/accounts/relationships", { id: this.props.match.params.profileId })
|
||||||
|
.then((resp: any) => {
|
||||||
|
let relationship: Relationship = resp.data[0];
|
||||||
|
this.setState({ relationship });
|
||||||
|
})
|
||||||
|
.catch((error: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: error.message
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMoreTimelinePieces() {
|
||||||
|
const {
|
||||||
|
match: { params }
|
||||||
|
} = this.props;
|
||||||
|
this.setState({ viewDidLoad: false, viewIsLoading: true });
|
||||||
|
if (this.state.posts && this.state.posts.length > 0) {
|
||||||
|
this.client
|
||||||
|
.get(`/accounts/${params.profileId}/statuses`, {
|
||||||
|
max_id: this.state.posts[this.state.posts.length - 1].id,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
.then((resp: any) => {
|
||||||
|
let newPosts: [Status] = resp.data;
|
||||||
|
let posts = this.state.posts as [Status];
|
||||||
|
if (newPosts.length <= 0) {
|
||||||
|
this.props.enqueueSnackbar("Reached end of posts", {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
newPosts.forEach((post: Status) => {
|
||||||
|
posts.push(post);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
posts
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("Failed to get posts", {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.props.enqueueSnackbar("Reached end of posts", { variant: "error" });
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleFollow() {
|
||||||
|
if (this.state.relationship) {
|
||||||
|
if (this.state.relationship.following) {
|
||||||
|
this.client
|
||||||
|
.post(
|
||||||
|
`/accounts/${
|
||||||
|
this.state.account
|
||||||
|
? this.state.account.id
|
||||||
|
: this.props.match.params.profileId
|
||||||
|
}/unfollow`
|
||||||
|
)
|
||||||
|
.then((resp: any) => {
|
||||||
|
let relationship: Relationship = resp.data;
|
||||||
this.setState({ relationship });
|
this.setState({ relationship });
|
||||||
}).catch((error: Error) => {
|
this.props.enqueueSnackbar(
|
||||||
this.setState({
|
"You are no longer following this account."
|
||||||
viewIsLoading: false,
|
);
|
||||||
viewDidError: true,
|
})
|
||||||
viewDidErrorCode: error.message
|
.catch((err: Error) => {
|
||||||
})
|
this.props.enqueueSnackbar(
|
||||||
});
|
"Couldn't unfollow account: " + err.name,
|
||||||
|
{ variant: "error" }
|
||||||
|
);
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.client
|
||||||
|
.post(
|
||||||
|
`/accounts/${
|
||||||
|
this.state.account
|
||||||
|
? this.state.account.id
|
||||||
|
: this.props.match.params.profileId
|
||||||
|
}/follow`
|
||||||
|
)
|
||||||
|
.then((resp: any) => {
|
||||||
|
let relationship: Relationship = resp.data;
|
||||||
|
this.setState({ relationship });
|
||||||
|
this.props.enqueueSnackbar("You are now following this account.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
loadMoreTimelinePieces() {
|
toggleBlock() {
|
||||||
const { match: {params}} = this.props;
|
if (this.state.relationship) {
|
||||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
if (this.state.relationship.blocking) {
|
||||||
if (this.state.posts && this.state.posts.length > 0) {
|
this.client
|
||||||
this.client.get(`/accounts/${params.profileId}/statuses`, { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
.post(
|
||||||
let newPosts: [Status] = resp.data;
|
`/accounts/${
|
||||||
let posts = this.state.posts as [Status];
|
this.state.account
|
||||||
if (newPosts.length <= 0) {
|
? this.state.account.id
|
||||||
this.props.enqueueSnackbar("Reached end of posts", {
|
: this.props.match.params.profileId
|
||||||
variant: 'error'
|
}/unblock`
|
||||||
});
|
)
|
||||||
} else {
|
.then((resp: any) => {
|
||||||
newPosts.forEach((post: Status) => {
|
let relationship: Relationship = resp.data;
|
||||||
posts.push(post);
|
this.setState({ relationship });
|
||||||
});
|
this.props.enqueueSnackbar(
|
||||||
}
|
"You are no longer blocking this account."
|
||||||
this.setState({
|
);
|
||||||
viewIsLoading: false,
|
})
|
||||||
viewDidLoad: true,
|
.catch((err: Error) => {
|
||||||
posts
|
this.props.enqueueSnackbar(
|
||||||
})
|
"Couldn't unblock account: " + err.name,
|
||||||
}).catch((err: Error) => {
|
{ variant: "error" }
|
||||||
this.setState({
|
);
|
||||||
viewIsLoading: false,
|
console.error(err.message);
|
||||||
viewDidError: true,
|
});
|
||||||
viewDidErrorCode: err.message
|
} else {
|
||||||
})
|
this.client
|
||||||
this.props.enqueueSnackbar("Failed to get posts", {
|
.post(
|
||||||
variant: 'error',
|
`/accounts/${
|
||||||
});
|
this.state.account
|
||||||
})
|
? this.state.account.id
|
||||||
} else {
|
: this.props.match.params.profileId
|
||||||
this.props.enqueueSnackbar("Reached end of posts", { variant: 'error'} );
|
}/block`
|
||||||
this.setState({
|
)
|
||||||
viewIsLoading: false,
|
.then((resp: any) => {
|
||||||
viewDidLoad: true
|
let relationship: Relationship = resp.data;
|
||||||
})
|
this.setState({ relationship });
|
||||||
}
|
this.props.enqueueSnackbar("You are now blocking this account.");
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
toggleFollow() {
|
this.props.enqueueSnackbar("Couldn't block account: " + err.name, {
|
||||||
if (this.state.relationship) {
|
variant: "error"
|
||||||
if (this.state.relationship.following) {
|
});
|
||||||
this.client.post(`/accounts/${this.state.account? this.state.account.id: this.props.match.params.profileId}/unfollow`).then((resp: any) => {
|
console.error(err.message);
|
||||||
let relationship: Relationship = resp.data;
|
});
|
||||||
this.setState({ relationship });
|
}
|
||||||
this.props.enqueueSnackbar('You are no longer following this account.');
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't unfollow account: " + err.name, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
this.client.post(`/accounts/${this.state.account? this.state.account.id: this.props.match.params.profileId}/follow`).then((resp: any) => {
|
|
||||||
let relationship: Relationship = resp.data;
|
|
||||||
this.setState({ relationship });
|
|
||||||
this.props.enqueueSnackbar('You are now following this account.');
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
toggleBlock() {
|
render() {
|
||||||
if (this.state.relationship) {
|
const { classes } = this.props;
|
||||||
if (this.state.relationship.blocking) {
|
return (
|
||||||
this.client.post(`/accounts/${this.state.account? this.state.account.id: this.props.match.params.profileId}/unblock`).then((resp: any) => {
|
<div className={classes.pageLayoutMinimalConstraints}>
|
||||||
let relationship: Relationship = resp.data;
|
<div className={classes.pageHeroBackground}>
|
||||||
this.setState({ relationship });
|
<div
|
||||||
this.props.enqueueSnackbar('You are no longer blocking this account.');
|
className={classes.pageHeroBackgroundImage}
|
||||||
}).catch((err: Error) => {
|
style={{
|
||||||
this.props.enqueueSnackbar("Couldn't unblock account: " + err.name, { variant: 'error' });
|
backgroundImage: this.state.account
|
||||||
console.error(err.message);
|
? `url("${this.state.account.header}")`
|
||||||
})
|
: `url("")`
|
||||||
} else {
|
}}
|
||||||
this.client.post(`/accounts/${this.state.account? this.state.account.id: this.props.match.params.profileId}/block`).then((resp: any) => {
|
/>
|
||||||
let relationship: Relationship = resp.data;
|
<Toolbar className={classes.profileToolbar}>
|
||||||
this.setState({ relationship });
|
<div className={classes.pageGrow} />
|
||||||
this.props.enqueueSnackbar('You are now blocking this account.');
|
<Tooltip
|
||||||
}).catch((err: Error) => {
|
title={
|
||||||
this.props.enqueueSnackbar("Couldn't block account: " + err.name, { variant: 'error' });
|
this.isItMe()
|
||||||
console.error(err.message);
|
? "You can't follow yourself."
|
||||||
})
|
: this.state.relationship && this.state.relationship.following
|
||||||
}
|
? "Unfollow"
|
||||||
}
|
: "Follow"
|
||||||
|
}
|
||||||
}
|
>
|
||||||
|
<IconButton
|
||||||
render() {
|
color={"inherit"}
|
||||||
const { classes } = this.props;
|
disabled={this.isItMe()}
|
||||||
return(
|
onClick={() => this.toggleFollow()}
|
||||||
<div className={classes.pageLayoutMinimalConstraints}>
|
>
|
||||||
<div className={classes.pageHeroBackground}>
|
{this.isItMe() ? (
|
||||||
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
|
<PersonAddDisabledIcon />
|
||||||
<Toolbar className={classes.profileToolbar}>
|
) : this.state.relationship &&
|
||||||
<div className={classes.pageGrow}/>
|
this.state.relationship.following ? (
|
||||||
<Tooltip title={
|
<AccountMinusIcon />
|
||||||
this.isItMe()?
|
) : (
|
||||||
"You can't follow yourself.":
|
<PersonAddIcon />
|
||||||
this.state.relationship && this.state.relationship.following?
|
)}
|
||||||
"Unfollow":
|
</IconButton>
|
||||||
"Follow"
|
</Tooltip>
|
||||||
}>
|
<Tooltip title={"Send a message or post"}>
|
||||||
<IconButton color={"inherit"} disabled={this.isItMe()} onClick={() => this.toggleFollow()}>
|
<LinkableIconButton
|
||||||
{
|
to={`/compose?acct=${
|
||||||
this.isItMe()?
|
this.state.account ? this.state.account.acct : ""
|
||||||
<PersonAddDisabledIcon/>:
|
}`}
|
||||||
this.state.relationship && this.state.relationship.following?
|
color={"inherit"}
|
||||||
<AccountMinusIcon/>:
|
>
|
||||||
<PersonAddIcon/>
|
<ChatIcon />
|
||||||
}
|
</LinkableIconButton>
|
||||||
</IconButton>
|
</Tooltip>
|
||||||
</Tooltip>
|
<Tooltip
|
||||||
<Tooltip title={"Send a message or post"}>
|
title={
|
||||||
<LinkableIconButton to={`/compose?acct=${this.state.account? this.state.account.acct: ""}`} color={"inherit"}>
|
this.state.relationship && this.state.relationship.blocking
|
||||||
<ChatIcon/>
|
? "Unblock this account"
|
||||||
</LinkableIconButton>
|
: "Block this account"
|
||||||
</Tooltip>
|
}
|
||||||
<Tooltip title={this.state.relationship && this.state.relationship.blocking? "Unblock this account": "Block this account"}>
|
>
|
||||||
<IconButton color={"inherit"} disabled={this.isItMe()} onClick={() => this.toggleBlockDialog()}>
|
<IconButton
|
||||||
{
|
color={"inherit"}
|
||||||
this.state.relationship && this.state.relationship.blocking? <AccountHeartIcon/>: <AccountRemoveIcon/>
|
disabled={this.isItMe()}
|
||||||
}
|
onClick={() => this.toggleBlockDialog()}
|
||||||
</IconButton>
|
>
|
||||||
</Tooltip>
|
{this.state.relationship && this.state.relationship.blocking ? (
|
||||||
<Tooltip title="Open in web">
|
<AccountHeartIcon />
|
||||||
<IconButton href={this.state.account? this.state.account.url: ""} target="_blank" rel={"nofollower noreferrer noopener"} color={"inherit"}>
|
) : (
|
||||||
<OpenInNewIcon/>
|
<AccountRemoveIcon />
|
||||||
</IconButton>
|
)}
|
||||||
</Tooltip>
|
</IconButton>
|
||||||
{
|
</Tooltip>
|
||||||
this.isItMe()?
|
<Tooltip title="Open in web">
|
||||||
<Tooltip title="Edit profile">
|
<IconButton
|
||||||
<LinkableIconButton to="/you" color="inherit">
|
href={this.state.account ? this.state.account.url : ""}
|
||||||
<AccountEditIcon/>
|
target="_blank"
|
||||||
</LinkableIconButton>
|
rel={"nofollower noreferrer noopener"}
|
||||||
</Tooltip>: null
|
color={"inherit"}
|
||||||
}
|
>
|
||||||
</Toolbar>
|
<OpenInNewIcon />
|
||||||
<div className={classes.profileContent}>
|
</IconButton>
|
||||||
<Avatar className={classes.profileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
|
</Tooltip>
|
||||||
<div className={classes.profileUserBox}>
|
{this.isItMe() ? (
|
||||||
<Typography variant="h4" color="inherit" dangerouslySetInnerHTML={
|
<Tooltip title="Edit profile">
|
||||||
{__html: this.state.account?
|
<LinkableIconButton to="/you" color="inherit">
|
||||||
this.state.account.display_name?
|
<AccountEditIcon />
|
||||||
emojifyString(this.state.account.display_name, this.state.account.emojis, classes.pageProfileNameEmoji)
|
</LinkableIconButton>
|
||||||
: this.state.account.username
|
</Tooltip>
|
||||||
: ""}}
|
) : null}
|
||||||
className={classes.pageProfileNameEmoji}/>
|
</Toolbar>
|
||||||
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
|
<div className={classes.profileContent}>
|
||||||
<Typography paragraph color="inherit">{
|
<Avatar
|
||||||
this.state.account ?
|
className={classes.profileAvatar}
|
||||||
this.state.account.note?
|
src={this.state.account ? this.state.account.avatar : ""}
|
||||||
this.state.account.note
|
/>
|
||||||
: "No bio provided by user."
|
<div className={classes.profileUserBox}>
|
||||||
: "No bio available."
|
<Typography
|
||||||
}</Typography>
|
variant="h4"
|
||||||
<Typography color={"inherit"}>
|
color="inherit"
|
||||||
{this.state.account? this.state.account.followers_count: 0} followers | {this.state.account? this.state.account.following_count: 0} following | {this.state.account? this.state.account.statuses_count: 0} posts
|
dangerouslySetInnerHTML={{
|
||||||
</Typography>
|
__html: this.state.account
|
||||||
</div>
|
? this.state.account.display_name
|
||||||
</div>
|
? emojifyString(
|
||||||
</div>
|
this.state.account.display_name,
|
||||||
<div className={classes.pageContentLayoutConstraints}>
|
this.state.account.emojis,
|
||||||
{
|
classes.pageProfileNameEmoji
|
||||||
this.state.viewDidError?
|
)
|
||||||
<Paper className={classes.errorCard}>
|
: this.state.account.username
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
: ""
|
||||||
<Typography variant="h6">Something went wrong when loading this profile.</Typography>
|
}}
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
className={classes.pageProfileNameEmoji}
|
||||||
</Paper>:
|
/>
|
||||||
<span/>
|
<Typography variant="caption" color="inherit">
|
||||||
}
|
{this.state.account ? "@" + this.state.account.acct : ""}
|
||||||
{
|
</Typography>
|
||||||
this.state.posts?
|
<Typography paragraph color="inherit">
|
||||||
<div>
|
{this.state.account
|
||||||
{
|
? this.state.account.note
|
||||||
this.state.posts.map((post: Status) => {
|
? this.state.account.note
|
||||||
return <Post key={post.id} post={post} client={this.client}/>;
|
: "No bio provided by user."
|
||||||
})
|
: "No bio available."}
|
||||||
}
|
</Typography>
|
||||||
<br/>
|
<Typography color={"inherit"}>
|
||||||
{
|
{this.state.account ? this.state.account.followers_count : 0}{" "}
|
||||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
followers |{" "}
|
||||||
}
|
{this.state.account ? this.state.account.following_count : 0}{" "}
|
||||||
</div>: <span/>
|
following |{" "}
|
||||||
}
|
{this.state.account ? this.state.account.statuses_count : 0}{" "}
|
||||||
{
|
posts
|
||||||
this.state.viewIsLoading?
|
</Typography>
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
<Dialog
|
|
||||||
open={this.state.blockDialogOpen}
|
|
||||||
onClose={() => this.toggleBlockDialog()}
|
|
||||||
>
|
|
||||||
<DialogTitle id="alert-dialog-title">Block this person?</DialogTitle>
|
|
||||||
<DialogContent>
|
|
||||||
<DialogContentText id="alert-dialog-description">
|
|
||||||
Are you sure you want to block this person? You won't see their posts on your home feed, local timeline, or public timeline.
|
|
||||||
</DialogContentText>
|
|
||||||
</DialogContent>
|
|
||||||
<DialogActions>
|
|
||||||
<Button onClick={() => this.toggleBlockDialog()} color="primary" autoFocus>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button onClick={() => {
|
|
||||||
this.toggleBlock();
|
|
||||||
this.toggleBlockDialog();
|
|
||||||
}} color="primary">
|
|
||||||
Block
|
|
||||||
</Button>
|
|
||||||
</DialogActions>
|
|
||||||
</Dialog>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
</div>
|
||||||
|
<div className={classes.pageContentLayoutConstraints}>
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this profile.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.posts ? (
|
||||||
|
<div>
|
||||||
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
|
<div
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
onClick={() => this.loadMoreTimelinePieces()}
|
||||||
|
>
|
||||||
|
<Button variant="contained">Load more</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
<Dialog
|
||||||
|
open={this.state.blockDialogOpen}
|
||||||
|
onClose={() => this.toggleBlockDialog()}
|
||||||
|
>
|
||||||
|
<DialogTitle id="alert-dialog-title">
|
||||||
|
Block this person?
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogContentText id="alert-dialog-description">
|
||||||
|
Are you sure you want to block this person? You won't see their
|
||||||
|
posts on your home feed, local timeline, or public timeline.
|
||||||
|
</DialogContentText>
|
||||||
|
</DialogContent>
|
||||||
|
<DialogActions>
|
||||||
|
<Button
|
||||||
|
onClick={() => this.toggleBlockDialog()}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
this.toggleBlock();
|
||||||
|
this.toggleBlockDialog();
|
||||||
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Block
|
||||||
|
</Button>
|
||||||
|
</DialogActions>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(ProfilePage));
|
export default withStyles(styles)(withSnackbar(ProfilePage));
|
||||||
|
|
|
@ -1,192 +1,225 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, Slide} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './PageLayout.styles';
|
withStyles,
|
||||||
import Post from '../components/Post';
|
CircularProgress,
|
||||||
import { Status } from '../types/Status';
|
Typography,
|
||||||
import Mastodon, { StreamListener } from 'megalodon';
|
Paper,
|
||||||
import {withSnackbar} from 'notistack';
|
Button,
|
||||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
Chip,
|
||||||
|
Avatar,
|
||||||
|
Slide
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Post from "../components/Post";
|
||||||
|
import { Status } from "../types/Status";
|
||||||
|
import Mastodon, { StreamListener } from "megalodon";
|
||||||
|
import { withSnackbar } from "notistack";
|
||||||
|
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
|
||||||
|
|
||||||
interface IPublicPageState {
|
interface IPublicPageState {
|
||||||
posts?: [Status];
|
posts?: [Status];
|
||||||
backlogPosts?: [Status] | null;
|
backlogPosts?: [Status] | null;
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: any;
|
viewDidErrorCode?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class PublicPage extends Component<any, IPublicPageState> {
|
class PublicPage extends Component<any, IPublicPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
streamListener: StreamListener;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
streamListener: StreamListener;
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.state = {
|
||||||
super(props);
|
viewIsLoading: true,
|
||||||
|
backlogPosts: null
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
this.client = new Mastodon(
|
||||||
viewIsLoading: true,
|
localStorage.getItem("access_token") as string,
|
||||||
backlogPosts: null
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
}
|
);
|
||||||
|
this.streamListener = this.client.stream("/streaming/public");
|
||||||
|
}
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
componentWillMount() {
|
||||||
this.streamListener = this.client.stream('/streaming/public');
|
this.streamListener.on("connect", () => {
|
||||||
|
this.client
|
||||||
}
|
.get("/timelines/public", { limit: 40 })
|
||||||
|
.then((resp: any) => {
|
||||||
componentWillMount() {
|
let statuses: [Status] = resp.data;
|
||||||
this.streamListener.on('connect', () => {
|
this.setState({
|
||||||
this.client.get('/timelines/public', {limit: 40}).then((resp: any) => {
|
posts: statuses,
|
||||||
let statuses: [Status] = resp.data;
|
viewIsLoading: false,
|
||||||
this.setState({
|
viewDidLoad: true,
|
||||||
posts: statuses,
|
viewDidError: false
|
||||||
viewIsLoading: false,
|
});
|
||||||
viewDidLoad: true,
|
})
|
||||||
viewDidError: false
|
.catch((resp: any) => {
|
||||||
})
|
this.setState({
|
||||||
}).catch((resp: any) => {
|
viewIsLoading: false,
|
||||||
this.setState({
|
viewDidLoad: true,
|
||||||
viewIsLoading: false,
|
viewDidError: true,
|
||||||
viewDidLoad: true,
|
viewDidErrorCode: String(resp)
|
||||||
viewDidError: true,
|
});
|
||||||
viewDidErrorCode: String(resp)
|
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||||
})
|
variant: "error"
|
||||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
});
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('update', (status: Status) => {
|
this.streamListener.on("update", (status: Status) => {
|
||||||
let queue = this.state.backlogPosts;
|
let queue = this.state.backlogPosts;
|
||||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
if (queue !== null && queue !== undefined) {
|
||||||
this.setState({ backlogPosts: queue });
|
queue.unshift(status);
|
||||||
})
|
} else {
|
||||||
|
queue = [status];
|
||||||
|
}
|
||||||
|
this.setState({ backlogPosts: queue });
|
||||||
|
});
|
||||||
|
|
||||||
this.streamListener.on('delete', (id: number) => {
|
this.streamListener.on("delete", (id: number) => {
|
||||||
let posts = this.state.posts;
|
let posts = this.state.posts;
|
||||||
if (posts) {
|
if (posts) {
|
||||||
posts.forEach((post: Status) => {
|
posts.forEach((post: Status) => {
|
||||||
if (posts && parseInt(post.id) === id) {
|
if (posts && parseInt(post.id) === id) {
|
||||||
posts.splice(posts.indexOf(post), 1);
|
posts.splice(posts.indexOf(post), 1);
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
this.setState({ posts });
|
this.setState({ posts });
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('error', (err: Error) => {
|
this.streamListener.on("error", (err: Error) => {
|
||||||
this.setState({
|
this.setState({
|
||||||
viewDidError: true,
|
viewDidError: true,
|
||||||
viewDidErrorCode: err.message
|
viewDidErrorCode: err.message
|
||||||
})
|
});
|
||||||
this.props.enqueueSnackbar("An error occured.", {
|
this.props.enqueueSnackbar("An error occured.", {
|
||||||
variant: 'error',
|
variant: "error"
|
||||||
});
|
});
|
||||||
})
|
});
|
||||||
|
|
||||||
this.streamListener.on('heartbeat', () => {
|
this.streamListener.on("heartbeat", () => {});
|
||||||
|
}
|
||||||
})
|
|
||||||
|
componentWillUnmount() {
|
||||||
|
this.streamListener.stop();
|
||||||
|
}
|
||||||
|
|
||||||
|
insertBacklog() {
|
||||||
|
window.scrollTo(0, 0);
|
||||||
|
let posts = this.state.posts;
|
||||||
|
let backlog = this.state.backlogPosts;
|
||||||
|
if (posts && backlog && backlog.length > 0) {
|
||||||
|
let push = backlog.concat(posts);
|
||||||
|
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
loadMoreTimelinePieces() {
|
||||||
this.streamListener.stop();
|
this.setState({ viewDidLoad: false, viewIsLoading: true });
|
||||||
|
if (this.state.posts) {
|
||||||
|
this.client
|
||||||
|
.get("/timelines/public", {
|
||||||
|
max_id: this.state.posts[this.state.posts.length - 1].id,
|
||||||
|
limit: 20
|
||||||
|
})
|
||||||
|
.then((resp: any) => {
|
||||||
|
let newPosts: [Status] = resp.data;
|
||||||
|
let posts = this.state.posts as [Status];
|
||||||
|
newPosts.forEach((post: Status) => {
|
||||||
|
posts.push(post);
|
||||||
|
});
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
posts
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
this.props.enqueueSnackbar("Failed to get posts", {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
insertBacklog() {
|
render() {
|
||||||
window.scrollTo(0, 0);
|
const { classes } = this.props;
|
||||||
let posts = this.state.posts;
|
|
||||||
let backlog = this.state.backlogPosts;
|
|
||||||
if (posts && backlog && backlog.length > 0) {
|
|
||||||
let push = backlog.concat(posts);
|
|
||||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadMoreTimelinePieces() {
|
return (
|
||||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
<div className={classes.pageLayoutMaxConstraints}>
|
||||||
if (this.state.posts) {
|
{this.state.backlogPosts ? (
|
||||||
this.client.get('/timelines/public', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
<div className={classes.pageTopChipContainer}>
|
||||||
let newPosts: [Status] = resp.data;
|
<div className={classes.pageTopChips}>
|
||||||
let posts = this.state.posts as [Status];
|
<Slide direction="down" in={true}>
|
||||||
newPosts.forEach((post: Status) => {
|
<Chip
|
||||||
posts.push(post);
|
avatar={
|
||||||
});
|
<Avatar>
|
||||||
this.setState({
|
<ArrowUpwardIcon />
|
||||||
viewIsLoading: false,
|
</Avatar>
|
||||||
viewDidLoad: true,
|
}
|
||||||
posts
|
label={`View ${this.state.backlogPosts.length} new post${
|
||||||
})
|
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||||
}).catch((err: Error) => {
|
}`}
|
||||||
this.setState({
|
color="primary"
|
||||||
viewIsLoading: false,
|
className={classes.pageTopChip}
|
||||||
viewDidError: true,
|
onClick={() => this.insertBacklog()}
|
||||||
viewDidErrorCode: err.message
|
clickable
|
||||||
})
|
/>
|
||||||
this.props.enqueueSnackbar("Failed to get posts", {
|
</Slide>
|
||||||
variant: 'error',
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={classes.pageLayoutMaxConstraints}>
|
|
||||||
{
|
|
||||||
this.state.backlogPosts?
|
|
||||||
<div className={classes.pageTopChipContainer}>
|
|
||||||
<div className={classes.pageTopChips}>
|
|
||||||
<Slide direction="down" in={true}>
|
|
||||||
<Chip
|
|
||||||
avatar={
|
|
||||||
<Avatar>
|
|
||||||
<ArrowUpwardIcon/>
|
|
||||||
</Avatar>
|
|
||||||
}
|
|
||||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
|
||||||
color="primary"
|
|
||||||
className={classes.pageTopChip}
|
|
||||||
onClick={() => this.insertBacklog()}
|
|
||||||
clickable
|
|
||||||
/>
|
|
||||||
</Slide>
|
|
||||||
</div>
|
|
||||||
</div>: null
|
|
||||||
}
|
|
||||||
{ this.state.posts?
|
|
||||||
<div>
|
|
||||||
{this.state.posts.map((post: Status) => {
|
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
|
||||||
})}
|
|
||||||
<br/>
|
|
||||||
{
|
|
||||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
|
||||||
}
|
|
||||||
</div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
</div>
|
||||||
}
|
) : null}
|
||||||
|
{this.state.posts ? (
|
||||||
|
<div>
|
||||||
|
{this.state.posts.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})}
|
||||||
|
<br />
|
||||||
|
{this.state.viewDidLoad && !this.state.viewDidError ? (
|
||||||
|
<div
|
||||||
|
style={{ textAlign: "center" }}
|
||||||
|
onClick={() => this.loadMoreTimelinePieces()}
|
||||||
|
>
|
||||||
|
<Button variant="contained">Load more</Button>
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(PublicPage));
|
export default withStyles(styles)(withSnackbar(PublicPage));
|
||||||
|
|
|
@ -1,239 +1,312 @@
|
||||||
import React, {Component} from 'react';
|
import React, { Component } from "react";
|
||||||
import {withStyles, Typography, List, ListItem, Paper, ListItemText, Avatar, ListItemSecondaryAction, ListItemAvatar, ListSubheader, CircularProgress, IconButton, Divider, Tooltip} from '@material-ui/core';
|
import {
|
||||||
import {styles} from './PageLayout.styles';
|
withStyles,
|
||||||
import Mastodon from 'megalodon';
|
Typography,
|
||||||
import {Account} from '../types/Account';
|
List,
|
||||||
import { LinkableIconButton, LinkableAvatar } from '../interfaces/overrides';
|
ListItem,
|
||||||
import AccountCircleIcon from '@material-ui/icons/AccountCircle';
|
Paper,
|
||||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
ListItemText,
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
Avatar,
|
||||||
import CheckIcon from '@material-ui/icons/Check';
|
ListItemSecondaryAction,
|
||||||
import CloseIcon from '@material-ui/icons/Close';
|
ListItemAvatar,
|
||||||
import {withSnackbar, withSnackbarProps} from 'notistack';
|
ListSubheader,
|
||||||
|
CircularProgress,
|
||||||
|
IconButton,
|
||||||
|
Divider,
|
||||||
|
Tooltip
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import Mastodon from "megalodon";
|
||||||
|
import { Account } from "../types/Account";
|
||||||
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
|
import AccountCircleIcon from "@material-ui/icons/AccountCircle";
|
||||||
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
|
import CheckIcon from "@material-ui/icons/Check";
|
||||||
|
import CloseIcon from "@material-ui/icons/Close";
|
||||||
|
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||||
|
|
||||||
interface IRecommendationsPageProps extends withSnackbarProps {
|
interface IRecommendationsPageProps extends withSnackbarProps {
|
||||||
classes: any;
|
classes: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IRecommendationsPageState {
|
interface IRecommendationsPageState {
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: Boolean;
|
viewDidError?: Boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
requestedFollows?: [Account];
|
requestedFollows?: [Account];
|
||||||
followSuggestions?: [Account];
|
followSuggestions?: [Account];
|
||||||
}
|
}
|
||||||
|
|
||||||
class RecommendationsPage extends Component<IRecommendationsPageProps, IRecommendationsPageState> {
|
class RecommendationsPage extends Component<
|
||||||
|
IRecommendationsPageProps,
|
||||||
|
IRecommendationsPageState
|
||||||
|
> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
this.client = new Mastodon(
|
||||||
|
localStorage.getItem("access_token") as string,
|
||||||
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
|
);
|
||||||
|
this.state = {
|
||||||
|
viewIsLoading: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
constructor(props: any) {
|
componentDidMount() {
|
||||||
super(props);
|
this.client
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
.get("/follow_requests")
|
||||||
this.state = {
|
.then((resp: any) => {
|
||||||
viewIsLoading: true
|
let requestedFollows: [Account] = resp.data;
|
||||||
|
this.setState({ requestedFollows });
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.name
|
||||||
|
});
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.client
|
||||||
|
.get("/suggestions")
|
||||||
|
.then((resp: any) => {
|
||||||
|
let followSuggestions: [Account] = resp.data;
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidLoad: true,
|
||||||
|
followSuggestions
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.name
|
||||||
|
});
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
followMember(acct: Account) {
|
||||||
|
this.client
|
||||||
|
.post(`/accounts/${acct.id}/follow`)
|
||||||
|
.then((resp: any) => {
|
||||||
|
this.props.enqueueSnackbar("You are now following this account.");
|
||||||
|
this.client.del(`/suggestions/${acct.id}`).then((resp: any) => {
|
||||||
|
let followSuggestions = this.state.followSuggestions;
|
||||||
|
if (followSuggestions) {
|
||||||
|
followSuggestions.forEach((suggestion: Account, index: number) => {
|
||||||
|
if (followSuggestions && suggestion.id === acct.id) {
|
||||||
|
followSuggestions.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
this.setState({ followSuggestions });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
|
||||||
|
this.client
|
||||||
|
.post(`/follow_requests/${acct.id}/${type}`)
|
||||||
|
.then((resp: any) => {
|
||||||
|
let requestedFollows = this.state.requestedFollows;
|
||||||
|
if (requestedFollows) {
|
||||||
|
requestedFollows.forEach((request: Account, index: number) => {
|
||||||
|
if (requestedFollows && request.id === acct.id) {
|
||||||
|
requestedFollows.splice(index, 1);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
this.setState({ requestedFollows });
|
||||||
|
|
||||||
componentDidMount() {
|
let verb: string = type;
|
||||||
this.client.get('/follow_requests').then((resp: any) => {
|
verb === "authorize" ? (verb = "authorized") : (verb = "rejected");
|
||||||
let requestedFollows: [Account] = resp.data;
|
this.props.enqueueSnackbar(`You have ${verb} this request.`);
|
||||||
this.setState({ requestedFollows })
|
})
|
||||||
}).catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
this.setState({
|
this.props.enqueueSnackbar(
|
||||||
viewIsLoading: false,
|
`Couldn't ${type} this request: ${err.name}`,
|
||||||
viewDidError: true,
|
{ variant: "error" }
|
||||||
viewDidErrorCode: err.name
|
|
||||||
});
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
|
|
||||||
this.client.get('/suggestions').then((resp: any) => {
|
|
||||||
let followSuggestions: [Account] = resp.data;
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidLoad: true,
|
|
||||||
followSuggestions
|
|
||||||
})
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.name
|
|
||||||
});
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
followMember(acct: Account) {
|
|
||||||
this.client.post(`/accounts/${acct.id}/follow`).then((resp: any) => {
|
|
||||||
this.props.enqueueSnackbar('You are now following this account.');
|
|
||||||
this.client.del(`/suggestions/${acct.id}`).then((resp: any) => {
|
|
||||||
let followSuggestions = this.state.followSuggestions;
|
|
||||||
if (followSuggestions) {
|
|
||||||
followSuggestions.forEach((suggestion: Account, index: number) => {
|
|
||||||
if (followSuggestions && suggestion.id === acct.id) {
|
|
||||||
followSuggestions.splice(index, 1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.setState({ followSuggestions });
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
|
|
||||||
this.client.post(`/follow_requests/${acct.id}/${type}`).then((resp: any) => {
|
|
||||||
|
|
||||||
let requestedFollows = this.state.requestedFollows;
|
|
||||||
if (requestedFollows) {
|
|
||||||
requestedFollows.forEach((request: Account, index: number) => {
|
|
||||||
if (requestedFollows && request.id === acct.id) {
|
|
||||||
requestedFollows.splice(index, 1);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
this.setState({requestedFollows});
|
|
||||||
|
|
||||||
let verb: string = type;
|
|
||||||
verb === "authorize"? verb = "authorized": verb = "rejected";
|
|
||||||
this.props.enqueueSnackbar(`You have ${verb} this request.`);
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar(`Couldn't ${type} this request: ${err.name}`, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
showFollowRequests() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ListSubheader>Follow requests</ListSubheader>
|
|
||||||
<Paper className={classes.pageListConstraints}>
|
|
||||||
<List>
|
|
||||||
{
|
|
||||||
this.state.requestedFollows?
|
|
||||||
this.state.requestedFollows.map((request: Account) => {
|
|
||||||
return (
|
|
||||||
<ListItem key={request.id}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<LinkableAvatar to={`/profile/${request.id}`} alt={request.username} src={request.avatar_static}/>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={request.display_name || request.acct} secondary={request.acct}/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="Accept request">
|
|
||||||
<IconButton onClick={() => this.handleFollowRequest(request, "authorize")}>
|
|
||||||
<CheckIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Reject request">
|
|
||||||
<IconButton onClick={() => this.handleFollowRequest(request, "reject")}>
|
|
||||||
<CloseIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="View profile">
|
|
||||||
<LinkableIconButton to={`/profile/${request.id}`}>
|
|
||||||
<AccountCircleIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}): null
|
|
||||||
}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
<br/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
showFollowSuggestions() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ListSubheader>Suggested accounts</ListSubheader>
|
|
||||||
<Paper className={classes.pageListConstraints}>
|
|
||||||
<List>
|
|
||||||
{
|
|
||||||
this.state.followSuggestions?
|
|
||||||
this.state.followSuggestions.map((suggestion: Account) => {
|
|
||||||
return (
|
|
||||||
<ListItem key={suggestion.id}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<LinkableAvatar to={`/profile/${suggestion.id}`} alt={suggestion.username} src={suggestion.avatar_static}/>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={suggestion.display_name || suggestion.acct} secondary={suggestion.acct}/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="View profile">
|
|
||||||
<LinkableIconButton to={`/profile/${suggestion.id}`}>
|
|
||||||
<AssignmentIndIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Follow">
|
|
||||||
<IconButton onClick={() => this.followMember(suggestion)}>
|
|
||||||
<PersonAddIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
}): null
|
|
||||||
}
|
|
||||||
</List>
|
|
||||||
</Paper>
|
|
||||||
<br/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
return (
|
|
||||||
<div className={classes.pageLayoutConstraints}>
|
|
||||||
{
|
|
||||||
this.state.viewDidLoad?
|
|
||||||
<div>
|
|
||||||
{
|
|
||||||
this.state.requestedFollows && this.state.requestedFollows.length > 0?
|
|
||||||
this.showFollowRequests():
|
|
||||||
<div className={classes.pageLayoutEmptyTextConstraints}>
|
|
||||||
<Typography variant="h6">You don't have any follow requests.</Typography>
|
|
||||||
<br/>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
<Divider/>
|
|
||||||
<br/>
|
|
||||||
{
|
|
||||||
this.state.followSuggestions && this.state.followSuggestions.length > 0? this.showFollowSuggestions():
|
|
||||||
<div className={classes.pageLayoutEmptyTextConstraints}>
|
|
||||||
<Typography variant="h5">We don't have any suggestions for you.</Typography>
|
|
||||||
<Typography paragraph>Why not interact with the fediverse a bit by creating a new post?</Typography>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>: null
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewDidError?
|
|
||||||
<Paper className={classes.errorCard}>
|
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showFollowRequests() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Follow requests</ListSubheader>
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
<List>
|
||||||
|
{this.state.requestedFollows
|
||||||
|
? this.state.requestedFollows.map((request: Account) => {
|
||||||
|
return (
|
||||||
|
<ListItem key={request.id}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
to={`/profile/${request.id}`}
|
||||||
|
alt={request.username}
|
||||||
|
src={request.avatar_static}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={request.display_name || request.acct}
|
||||||
|
secondary={request.acct}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="Accept request">
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
this.handleFollowRequest(request, "authorize")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CheckIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Reject request">
|
||||||
|
<IconButton
|
||||||
|
onClick={() =>
|
||||||
|
this.handleFollowRequest(request, "reject")
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<CloseIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="View profile">
|
||||||
|
<LinkableIconButton to={`/profile/${request.id}`}>
|
||||||
|
<AccountCircleIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showFollowSuggestions() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Suggested accounts</ListSubheader>
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
<List>
|
||||||
|
{this.state.followSuggestions
|
||||||
|
? this.state.followSuggestions.map((suggestion: Account) => {
|
||||||
|
return (
|
||||||
|
<ListItem key={suggestion.id}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
to={`/profile/${suggestion.id}`}
|
||||||
|
alt={suggestion.username}
|
||||||
|
src={suggestion.avatar_static}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={suggestion.display_name || suggestion.acct}
|
||||||
|
secondary={suggestion.acct}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="View profile">
|
||||||
|
<LinkableIconButton to={`/profile/${suggestion.id}`}>
|
||||||
|
<AssignmentIndIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Follow">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => this.followMember(suggestion)}
|
||||||
|
>
|
||||||
|
<PersonAddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})
|
||||||
|
: null}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.pageLayoutConstraints}>
|
||||||
|
{this.state.viewDidLoad ? (
|
||||||
|
<div>
|
||||||
|
{this.state.requestedFollows &&
|
||||||
|
this.state.requestedFollows.length > 0 ? (
|
||||||
|
this.showFollowRequests()
|
||||||
|
) : (
|
||||||
|
<div className={classes.pageLayoutEmptyTextConstraints}>
|
||||||
|
<Typography variant="h6">
|
||||||
|
You don't have any follow requests.
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Divider />
|
||||||
|
<br />
|
||||||
|
{this.state.followSuggestions &&
|
||||||
|
this.state.followSuggestions.length > 0 ? (
|
||||||
|
this.showFollowSuggestions()
|
||||||
|
) : (
|
||||||
|
<div className={classes.pageLayoutEmptyTextConstraints}>
|
||||||
|
<Typography variant="h5">
|
||||||
|
We don't have any suggestions for you.
|
||||||
|
</Typography>
|
||||||
|
<Typography paragraph>
|
||||||
|
Why not interact with the fediverse a bit by creating a new
|
||||||
|
post?
|
||||||
|
</Typography>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
) : null}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(RecommendationsPage));
|
export default withStyles(styles)(withSnackbar(RecommendationsPage));
|
||||||
|
|
|
@ -1,268 +1,331 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
Avatar,
|
Avatar,
|
||||||
Paper,
|
Paper,
|
||||||
withStyles,
|
withStyles,
|
||||||
Typography,
|
Typography,
|
||||||
CircularProgress,
|
CircularProgress,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
IconButton
|
IconButton
|
||||||
} from '@material-ui/core';
|
} from "@material-ui/core";
|
||||||
import PersonIcon from '@material-ui/icons/Person';
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import {LinkableIconButton, LinkableAvatar} from '../interfaces/overrides';
|
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
import {parse as parseParams, ParsedQuery} from 'query-string';
|
import { parse as parseParams, ParsedQuery } from "query-string";
|
||||||
import { Results } from '../types/Search';
|
import { Results } from "../types/Search";
|
||||||
import { withSnackbar } from 'notistack';
|
import { withSnackbar } from "notistack";
|
||||||
import Post from '../components/Post';
|
import Post from "../components/Post";
|
||||||
import { Status } from '../types/Status';
|
import { Status } from "../types/Status";
|
||||||
import { Account } from '../types/Account';
|
import { Account } from "../types/Account";
|
||||||
|
|
||||||
interface ISearchPageState {
|
interface ISearchPageState {
|
||||||
query: string[] | string;
|
query: string[] | string;
|
||||||
type?: string[] | string;
|
type?: string[] | string;
|
||||||
results?: Results;
|
results?: Results;
|
||||||
tagResults?: [Status];
|
tagResults?: [Status];
|
||||||
viewIsLoading: boolean;
|
viewIsLoading: boolean;
|
||||||
viewDidLoad?: boolean;
|
viewDidLoad?: boolean;
|
||||||
viewDidError?: boolean;
|
viewDidError?: boolean;
|
||||||
viewDidErrorCode?: string;
|
viewDidErrorCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class SearchPage extends Component<any, ISearchPageState> {
|
class SearchPage extends Component<any, ISearchPageState> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.client = new Mastodon(
|
||||||
super(props);
|
localStorage.getItem("access_token") as string,
|
||||||
|
localStorage.getItem("baseurl") + "/api/v2"
|
||||||
|
);
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v2");
|
let searchParams = this.getQueryAndType(props);
|
||||||
|
|
||||||
let searchParams = this.getQueryAndType(props);
|
this.state = {
|
||||||
|
viewIsLoading: true,
|
||||||
|
query: searchParams.query,
|
||||||
|
type: searchParams.type
|
||||||
|
};
|
||||||
|
|
||||||
this.state = {
|
if (searchParams.type === "tag") {
|
||||||
viewIsLoading: true,
|
this.searchForPostsWithTags(searchParams.query);
|
||||||
query: searchParams.query,
|
} else {
|
||||||
type: searchParams.type
|
this.searchQuery(searchParams.query);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (searchParams.type === "tag") {
|
componentWillReceiveProps(props: any) {
|
||||||
this.searchForPostsWithTags(searchParams.query);
|
this.setState({
|
||||||
} else {
|
viewDidLoad: false,
|
||||||
this.searchQuery(searchParams.query);
|
viewIsLoading: true,
|
||||||
}
|
viewDidError: false,
|
||||||
|
viewDidErrorCode: "",
|
||||||
|
results: undefined
|
||||||
|
});
|
||||||
|
let searchParams = this.getQueryAndType(props);
|
||||||
|
this.setState({ query: searchParams.query, type: searchParams.type });
|
||||||
|
if (searchParams.type === "tag") {
|
||||||
|
this.searchForPostsWithTags(searchParams.query);
|
||||||
|
} else {
|
||||||
|
this.searchQuery(searchParams.query);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
runQueryCheck(newLocation?: string): ParsedQuery {
|
||||||
|
let searchParams = "";
|
||||||
|
if (newLocation !== undefined && typeof newLocation === "string") {
|
||||||
|
searchParams = newLocation.replace("#/search", "");
|
||||||
|
} else {
|
||||||
|
searchParams = location.hash.replace("#/search", "");
|
||||||
|
}
|
||||||
|
return parseParams(searchParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
getQueryAndType(props: any) {
|
||||||
|
let newSearch = this.runQueryCheck(props.location);
|
||||||
|
let query: string | string[];
|
||||||
|
let type;
|
||||||
|
|
||||||
|
if (newSearch.query) {
|
||||||
|
if (newSearch.query.toString().startsWith("tag:")) {
|
||||||
|
type = "tag";
|
||||||
|
query = newSearch.query.toString().replace("tag:", "");
|
||||||
|
} else {
|
||||||
|
query = newSearch.query;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
query = "";
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillReceiveProps(props: any) {
|
if (newSearch.type && newSearch.type !== undefined) {
|
||||||
this.setState({ viewDidLoad: false, viewIsLoading: true, viewDidError: false, viewDidErrorCode: '', results: undefined});
|
type = newSearch.type;
|
||||||
let searchParams = this.getQueryAndType(props);
|
|
||||||
this.setState({ query: searchParams.query, type: searchParams.type });
|
|
||||||
if (searchParams.type === "tag") {
|
|
||||||
this.searchForPostsWithTags(searchParams.query);
|
|
||||||
} else {
|
|
||||||
this.searchQuery(searchParams.query);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return {
|
||||||
|
query: query,
|
||||||
|
type: type
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
runQueryCheck(newLocation?: string): ParsedQuery {
|
searchQuery(query: string | string[]) {
|
||||||
let searchParams = "";
|
this.client
|
||||||
if (newLocation !== undefined && typeof(newLocation) === "string") {
|
.get("/search", { q: query })
|
||||||
searchParams = newLocation.replace("#/search", "");
|
.then((resp: any) => {
|
||||||
} else {
|
let results: Results = resp.data;
|
||||||
searchParams = location.hash.replace("#/search", "");
|
this.setState({
|
||||||
}
|
results,
|
||||||
return parseParams(searchParams);
|
viewDidLoad: true,
|
||||||
}
|
viewIsLoading: false
|
||||||
|
|
||||||
getQueryAndType(props: any) {
|
|
||||||
let newSearch = this.runQueryCheck(props.location);
|
|
||||||
let query: string | string[];
|
|
||||||
let type;
|
|
||||||
|
|
||||||
if (newSearch.query) {
|
|
||||||
if (newSearch.query.toString().startsWith("tag:")) {
|
|
||||||
type = "tag";
|
|
||||||
query = newSearch.query.toString().replace("tag:", "");
|
|
||||||
} else {
|
|
||||||
query = newSearch.query;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
query = "";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (newSearch.type && newSearch.type !== undefined) {
|
|
||||||
type = newSearch.type;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
query: query,
|
|
||||||
type: type
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
searchQuery(query: string | string[]) {
|
|
||||||
this.client.get('/search', {q: query}).then((resp: any) => {
|
|
||||||
let results: Results = resp.data;
|
|
||||||
this.setState({
|
|
||||||
results,
|
|
||||||
viewDidLoad: true,
|
|
||||||
viewIsLoading: false
|
|
||||||
});
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
|
|
||||||
this.props.enqueueSnackbar(`Couldn't search for ${this.state.query}: ${err.name}`, { variant: 'error' });
|
|
||||||
});
|
});
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
searchForPostsWithTags(query: string | string[]) {
|
this.setState({
|
||||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
viewIsLoading: false,
|
||||||
client.get(`/timelines/tag/${query}`).then((resp: any) => {
|
viewDidError: true,
|
||||||
let tagResults: [Status] = resp.data;
|
viewDidErrorCode: err.message
|
||||||
this.setState({
|
|
||||||
tagResults,
|
|
||||||
viewDidLoad: true,
|
|
||||||
viewIsLoading: false
|
|
||||||
});
|
|
||||||
console.log(this.state.tagResults);
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.setState({
|
|
||||||
viewIsLoading: false,
|
|
||||||
viewDidError: true,
|
|
||||||
viewDidErrorCode: err.message
|
|
||||||
})
|
|
||||||
|
|
||||||
this.props.enqueueSnackbar(`Couldn't search for posts with tag ${this.state.query}: ${err.name}`, { variant: 'error' });
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
followMemberFromQuery(acct: Account) {
|
this.props.enqueueSnackbar(
|
||||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
`Couldn't search for ${this.state.query}: ${err.name}`,
|
||||||
client.post(`/accounts/${acct.id}/follow`).then((resp: any) => {
|
{ variant: "error" }
|
||||||
this.props.enqueueSnackbar('You are now following this account.');
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
|
|
||||||
console.error(err.message);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
showAllAccountsFromQuery() {
|
|
||||||
const { classes } = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ListSubheader>Accounts</ListSubheader>
|
|
||||||
|
|
||||||
{
|
|
||||||
this.state.results && this.state.results.accounts.length > 0?
|
|
||||||
<Paper className={classes.pageListConstraints}>
|
|
||||||
<List>
|
|
||||||
{ this.state.results.accounts.map((acct: Account) => {
|
|
||||||
return (
|
|
||||||
<ListItem key={acct.id}>
|
|
||||||
<ListItemAvatar>
|
|
||||||
<LinkableAvatar to={`/profile/${acct.id}`} alt={acct.username} src={acct.avatar_static}/>
|
|
||||||
</ListItemAvatar>
|
|
||||||
<ListItemText primary={acct.display_name || acct.acct} secondary={acct.acct}/>
|
|
||||||
<ListItemSecondaryAction>
|
|
||||||
<Tooltip title="View profile">
|
|
||||||
<LinkableIconButton to={`/profile/${acct.id}`}>
|
|
||||||
<AssignmentIndIcon/>
|
|
||||||
</LinkableIconButton>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip title="Follow">
|
|
||||||
<IconButton onClick={() => this.followMemberFromQuery(acct)}>
|
|
||||||
<PersonAddIcon/>
|
|
||||||
</IconButton>
|
|
||||||
</Tooltip>
|
|
||||||
</ListItemSecondaryAction>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</List>
|
|
||||||
</Paper>: <Typography variant="caption" className={classes.pageLayoutEmptyTextConstraints}>No results found</Typography>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
<br/>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
showAllPostsFromQuery() {
|
|
||||||
const {classes} = this.props;
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<ListSubheader>Posts</ListSubheader>
|
|
||||||
{
|
|
||||||
this.state.results?
|
|
||||||
this.state.results.statuses.length > 0?
|
|
||||||
this.state.results.statuses.map((post: Status) => {
|
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
|
||||||
}): <Typography variant="caption" className={classes.pageLayoutEmptyTextConstraints}>No results found.</Typography>: null
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
showAllPostsWithTag() {
|
searchForPostsWithTags(query: string | string[]) {
|
||||||
const {classes} = this.props;
|
let client = new Mastodon(
|
||||||
return (
|
localStorage.getItem("access_token") as string,
|
||||||
<div>
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
<ListSubheader>Tagged posts</ListSubheader>
|
);
|
||||||
{
|
client
|
||||||
this.state.tagResults?
|
.get(`/timelines/tag/${query}`)
|
||||||
this.state.tagResults.length > 0?
|
.then((resp: any) => {
|
||||||
this.state.tagResults.map((post: Status) => {
|
let tagResults: [Status] = resp.data;
|
||||||
return <Post key={post.id} post={post} client={this.client}/>
|
this.setState({
|
||||||
}): <Typography variant="caption" className={classes.pageLayoutEmptyTextConstraints}>No results found.</Typography>: null
|
tagResults,
|
||||||
}
|
viewDidLoad: true,
|
||||||
</div>
|
viewIsLoading: false
|
||||||
|
});
|
||||||
|
console.log(this.state.tagResults);
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.setState({
|
||||||
|
viewIsLoading: false,
|
||||||
|
viewDidError: true,
|
||||||
|
viewDidErrorCode: err.message
|
||||||
|
});
|
||||||
|
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
`Couldn't search for posts with tag ${this.state.query}: ${err.name}`,
|
||||||
|
{ variant: "error" }
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
followMemberFromQuery(acct: Account) {
|
||||||
const { classes } = this.props;
|
let client = new Mastodon(
|
||||||
return (
|
localStorage.getItem("access_token") as string,
|
||||||
<div className={classes.pageLayoutConstraints}>
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
{
|
);
|
||||||
this.state.type && this.state.type === "tag"?
|
client
|
||||||
this.showAllPostsWithTag():
|
.post(`/accounts/${acct.id}/follow`)
|
||||||
<div>
|
.then((resp: any) => {
|
||||||
{this.showAllAccountsFromQuery()}
|
this.props.enqueueSnackbar("You are now following this account.");
|
||||||
{this.showAllPostsFromQuery()}
|
})
|
||||||
</div>
|
.catch((err: Error) => {
|
||||||
}
|
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
|
||||||
{
|
variant: "error"
|
||||||
this.state.viewDidError?
|
});
|
||||||
<Paper className={classes.errorCard}>
|
console.error(err.message);
|
||||||
<Typography variant="h4">Bummer.</Typography>
|
});
|
||||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
}
|
||||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
|
||||||
</Paper>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
{
|
|
||||||
this.state.viewIsLoading?
|
|
||||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
|
||||||
<span/>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
showAllAccountsFromQuery() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Accounts</ListSubheader>
|
||||||
|
|
||||||
|
{this.state.results && this.state.results.accounts.length > 0 ? (
|
||||||
|
<Paper className={classes.pageListConstraints}>
|
||||||
|
<List>
|
||||||
|
{this.state.results.accounts.map((acct: Account) => {
|
||||||
|
return (
|
||||||
|
<ListItem key={acct.id}>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<LinkableAvatar
|
||||||
|
to={`/profile/${acct.id}`}
|
||||||
|
alt={acct.username}
|
||||||
|
src={acct.avatar_static}
|
||||||
|
/>
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText
|
||||||
|
primary={acct.display_name || acct.acct}
|
||||||
|
secondary={acct.acct}
|
||||||
|
/>
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<Tooltip title="View profile">
|
||||||
|
<LinkableIconButton to={`/profile/${acct.id}`}>
|
||||||
|
<AssignmentIndIcon />
|
||||||
|
</LinkableIconButton>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip title="Follow">
|
||||||
|
<IconButton
|
||||||
|
onClick={() => this.followMemberFromQuery(acct)}
|
||||||
|
>
|
||||||
|
<PersonAddIcon />
|
||||||
|
</IconButton>
|
||||||
|
</Tooltip>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</List>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className={classes.pageLayoutEmptyTextConstraints}
|
||||||
|
>
|
||||||
|
No results found
|
||||||
|
</Typography>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<br />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAllPostsFromQuery() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Posts</ListSubheader>
|
||||||
|
{this.state.results ? (
|
||||||
|
this.state.results.statuses.length > 0 ? (
|
||||||
|
this.state.results.statuses.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className={classes.pageLayoutEmptyTextConstraints}
|
||||||
|
>
|
||||||
|
No results found.
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showAllPostsWithTag() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ListSubheader>Tagged posts</ListSubheader>
|
||||||
|
{this.state.tagResults ? (
|
||||||
|
this.state.tagResults.length > 0 ? (
|
||||||
|
this.state.tagResults.map((post: Status) => {
|
||||||
|
return <Post key={post.id} post={post} client={this.client} />;
|
||||||
|
})
|
||||||
|
) : (
|
||||||
|
<Typography
|
||||||
|
variant="caption"
|
||||||
|
className={classes.pageLayoutEmptyTextConstraints}
|
||||||
|
>
|
||||||
|
No results found.
|
||||||
|
</Typography>
|
||||||
|
)
|
||||||
|
) : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
const { classes } = this.props;
|
||||||
|
return (
|
||||||
|
<div className={classes.pageLayoutConstraints}>
|
||||||
|
{this.state.type && this.state.type === "tag" ? (
|
||||||
|
this.showAllPostsWithTag()
|
||||||
|
) : (
|
||||||
|
<div>
|
||||||
|
{this.showAllAccountsFromQuery()}
|
||||||
|
{this.showAllPostsFromQuery()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{this.state.viewDidError ? (
|
||||||
|
<Paper className={classes.errorCard}>
|
||||||
|
<Typography variant="h4">Bummer.</Typography>
|
||||||
|
<Typography variant="h6">
|
||||||
|
Something went wrong when loading this timeline.
|
||||||
|
</Typography>
|
||||||
|
<Typography>
|
||||||
|
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
|
||||||
|
</Typography>
|
||||||
|
</Paper>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
{this.state.viewIsLoading ? (
|
||||||
|
<div style={{ textAlign: "center" }}>
|
||||||
|
<CircularProgress className={classes.progress} color="primary" />
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(SearchPage));
|
export default withStyles(styles)(withSnackbar(SearchPage));
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
import React, { Component } from 'react';
|
import React, { Component } from "react";
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListItem,
|
ListItem,
|
||||||
ListItemAvatar,
|
ListItemAvatar,
|
||||||
ListItemText,
|
ListItemText,
|
||||||
ListSubheader,
|
ListSubheader,
|
||||||
ListItemSecondaryAction,
|
ListItemSecondaryAction,
|
||||||
Paper,
|
Paper,
|
||||||
IconButton,
|
IconButton,
|
||||||
withStyles,
|
withStyles,
|
||||||
Button,
|
Button,
|
||||||
Switch,
|
Switch,
|
||||||
Dialog,
|
Dialog,
|
||||||
|
@ -22,28 +22,44 @@ import {
|
||||||
Grid,
|
Grid,
|
||||||
Theme,
|
Theme,
|
||||||
Typography
|
Typography
|
||||||
} from '@material-ui/core';
|
} from "@material-ui/core";
|
||||||
import {styles} from './PageLayout.styles';
|
import { styles } from "./PageLayout.styles";
|
||||||
import {setUserDefaultBool, getUserDefaultBool, getUserDefaultTheme, setUserDefaultTheme, getUserDefaultVisibility, setUserDefaultVisibility, getConfig} from '../utilities/settings';
|
import {
|
||||||
import {canSendNotifications, browserSupportsNotificationRequests} from '../utilities/notifications';
|
setUserDefaultBool,
|
||||||
import {themes, defaultTheme} from '../types/HyperspaceTheme';
|
getUserDefaultBool,
|
||||||
import ThemePreview from '../components/ThemePreview';
|
getUserDefaultTheme,
|
||||||
import {setHyperspaceTheme, getHyperspaceTheme, getDarkModeFromSystem} from '../utilities/themes';
|
setUserDefaultTheme,
|
||||||
import { Visibility } from '../types/Visibility';
|
getUserDefaultVisibility,
|
||||||
import {LinkableButton} from '../interfaces/overrides';
|
setUserDefaultVisibility,
|
||||||
|
getConfig
|
||||||
|
} from "../utilities/settings";
|
||||||
|
import {
|
||||||
|
canSendNotifications,
|
||||||
|
browserSupportsNotificationRequests
|
||||||
|
} from "../utilities/notifications";
|
||||||
|
import { themes, defaultTheme } from "../types/HyperspaceTheme";
|
||||||
|
import ThemePreview from "../components/ThemePreview";
|
||||||
|
import {
|
||||||
|
setHyperspaceTheme,
|
||||||
|
getHyperspaceTheme,
|
||||||
|
getDarkModeFromSystem
|
||||||
|
} from "../utilities/themes";
|
||||||
|
import { Visibility } from "../types/Visibility";
|
||||||
|
import { LinkableButton } from "../interfaces/overrides";
|
||||||
|
import { Config } from "../types/Config";
|
||||||
|
|
||||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||||
import DevicesIcon from '@material-ui/icons/Devices';
|
import DevicesIcon from "@material-ui/icons/Devices";
|
||||||
import Brightness3Icon from '@material-ui/icons/Brightness3';
|
import Brightness3Icon from "@material-ui/icons/Brightness3";
|
||||||
import PaletteIcon from '@material-ui/icons/Palette';
|
import PaletteIcon from "@material-ui/icons/Palette";
|
||||||
import AccountEditIcon from 'mdi-material-ui/AccountEdit';
|
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||||
import MastodonIcon from 'mdi-material-ui/Mastodon';
|
import MastodonIcon from "mdi-material-ui/Mastodon";
|
||||||
import VisibilityIcon from '@material-ui/icons/Visibility';
|
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||||
import BellAlertIcon from 'mdi-material-ui/BellAlert';
|
import BellAlertIcon from "mdi-material-ui/BellAlert";
|
||||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||||
import UndoIcon from '@material-ui/icons/Undo';
|
import UndoIcon from "@material-ui/icons/Undo";
|
||||||
import { Config } from '../types/Config';
|
import CancelIcon from "@material-ui/icons/Cancel";
|
||||||
|
|
||||||
interface ISettingsState {
|
interface ISettingsState {
|
||||||
darkModeEnabled: boolean;
|
darkModeEnabled: boolean;
|
||||||
|
@ -62,25 +78,28 @@ interface ISettingsState {
|
||||||
}
|
}
|
||||||
|
|
||||||
class SettingsPage extends Component<any, ISettingsState> {
|
class SettingsPage extends Component<any, ISettingsState> {
|
||||||
|
|
||||||
constructor(props: any) {
|
constructor(props: any) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
darkModeEnabled: getUserDefaultBool('darkModeEnabled'),
|
darkModeEnabled: getUserDefaultBool("darkModeEnabled"),
|
||||||
systemDecidesDarkMode: getUserDefaultBool('systemDecidesDarkMode'),
|
systemDecidesDarkMode: getUserDefaultBool("systemDecidesDarkMode"),
|
||||||
pushNotificationsEnabled: canSendNotifications(),
|
pushNotificationsEnabled: canSendNotifications(),
|
||||||
badgeDisplaysAllNotifs: getUserDefaultBool('displayAllOnNotificationBadge'),
|
badgeDisplaysAllNotifs: getUserDefaultBool(
|
||||||
|
"displayAllOnNotificationBadge"
|
||||||
|
),
|
||||||
selectThemeName: getUserDefaultTheme().key,
|
selectThemeName: getUserDefaultTheme().key,
|
||||||
themeDialogOpen: false,
|
themeDialogOpen: false,
|
||||||
visibilityDialogOpen: false,
|
visibilityDialogOpen: false,
|
||||||
resetHyperspaceDialog: false,
|
resetHyperspaceDialog: false,
|
||||||
resetSettingsDialog: false,
|
resetSettingsDialog: false,
|
||||||
previewTheme: setHyperspaceTheme(getUserDefaultTheme()) || setHyperspaceTheme(defaultTheme),
|
previewTheme:
|
||||||
|
setHyperspaceTheme(getUserDefaultTheme()) ||
|
||||||
|
setHyperspaceTheme(defaultTheme),
|
||||||
defaultVisibility: getUserDefaultVisibility() || "public",
|
defaultVisibility: getUserDefaultVisibility() || "public",
|
||||||
brandName: "Hyperspace",
|
brandName: "Hyperspace",
|
||||||
federated: true
|
federated: true
|
||||||
}
|
};
|
||||||
|
|
||||||
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
||||||
this.toggleSystemDarkMode = this.toggleSystemDarkMode.bind(this);
|
this.toggleSystemDarkMode = this.toggleSystemDarkMode.bind(this);
|
||||||
|
@ -94,13 +113,15 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
getConfig().then((config: any) => {
|
getConfig()
|
||||||
this.setState({
|
.then((config: any) => {
|
||||||
brandName: config.branding.name
|
this.setState({
|
||||||
|
brandName: config.branding.name
|
||||||
|
});
|
||||||
})
|
})
|
||||||
}).catch((err: Error) => {
|
.catch((err: Error) => {
|
||||||
console.error(err.message);
|
console.error(err.message);
|
||||||
});
|
});
|
||||||
this.getFederatedStatus();
|
this.getFederatedStatus();
|
||||||
console.log(getDarkModeFromSystem());
|
console.log(getDarkModeFromSystem());
|
||||||
}
|
}
|
||||||
|
@ -109,32 +130,49 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
getConfig().then((result: any) => {
|
getConfig().then((result: any) => {
|
||||||
if (result !== undefined) {
|
if (result !== undefined) {
|
||||||
let config: Config = result;
|
let config: Config = result;
|
||||||
console.log(config.federation.allowPublicPosts === false)
|
console.log(config.federation.allowPublicPosts === false);
|
||||||
this.setState({ federated: config.federation.allowPublicPosts });
|
this.setState({
|
||||||
|
federated: config.federation.allowPublicPosts
|
||||||
|
});
|
||||||
}
|
}
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleDarkMode() {
|
toggleDarkMode() {
|
||||||
this.setState({ darkModeEnabled: !this.state.darkModeEnabled });
|
this.setState({ darkModeEnabled: !this.state.darkModeEnabled });
|
||||||
setUserDefaultBool('darkModeEnabled', !this.state.darkModeEnabled);
|
setUserDefaultBool("darkModeEnabled", !this.state.darkModeEnabled);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleSystemDarkMode() {
|
toggleSystemDarkMode() {
|
||||||
this.setState({ systemDecidesDarkMode: !this.state.systemDecidesDarkMode });
|
this.setState({
|
||||||
setUserDefaultBool('systemDecidesDarkMode', !this.state.systemDecidesDarkMode);
|
systemDecidesDarkMode: !this.state.systemDecidesDarkMode
|
||||||
|
});
|
||||||
|
setUserDefaultBool(
|
||||||
|
"systemDecidesDarkMode",
|
||||||
|
!this.state.systemDecidesDarkMode
|
||||||
|
);
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
togglePushNotifications() {
|
togglePushNotifications() {
|
||||||
this.setState({ pushNotificationsEnabled: !this.state.pushNotificationsEnabled });
|
this.setState({
|
||||||
setUserDefaultBool('enablePushNotifications', !this.state.pushNotificationsEnabled);
|
pushNotificationsEnabled: !this.state.pushNotificationsEnabled
|
||||||
|
});
|
||||||
|
setUserDefaultBool(
|
||||||
|
"enablePushNotifications",
|
||||||
|
!this.state.pushNotificationsEnabled
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleBadgeCount() {
|
toggleBadgeCount() {
|
||||||
this.setState({ badgeDisplaysAllNotifs: !this.state.badgeDisplaysAllNotifs });
|
this.setState({
|
||||||
setUserDefaultBool('displayAllOnNotificationBadge', !this.state.badgeDisplaysAllNotifs);
|
badgeDisplaysAllNotifs: !this.state.badgeDisplaysAllNotifs
|
||||||
|
});
|
||||||
|
setUserDefaultBool(
|
||||||
|
"displayAllOnNotificationBadge",
|
||||||
|
!this.state.badgeDisplaysAllNotifs
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleThemeDialog() {
|
toggleThemeDialog() {
|
||||||
|
@ -142,11 +180,15 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleVisibilityDialog() {
|
toggleVisibilityDialog() {
|
||||||
this.setState({ visibilityDialogOpen: !this.state.visibilityDialogOpen });
|
this.setState({
|
||||||
|
visibilityDialogOpen: !this.state.visibilityDialogOpen
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleResetDialog() {
|
toggleResetDialog() {
|
||||||
this.setState({ resetHyperspaceDialog: !this.state.resetHyperspaceDialog });
|
this.setState({
|
||||||
|
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleResetSettingsDialog() {
|
toggleResetSettingsDialog() {
|
||||||
|
@ -178,15 +220,22 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh() {
|
refresh() {
|
||||||
let settings = ['darkModeEnabled', 'enablePushNotifications', 'clearNotificationsOnRead', 'theme', 'displayAllOnNotificationBadge', 'defaultVisibility'];
|
let settings = [
|
||||||
|
"darkModeEnabled",
|
||||||
|
"enablePushNotifications",
|
||||||
|
"clearNotificationsOnRead",
|
||||||
|
"theme",
|
||||||
|
"displayAllOnNotificationBadge",
|
||||||
|
"defaultVisibility"
|
||||||
|
];
|
||||||
settings.forEach(setting => {
|
settings.forEach(setting => {
|
||||||
localStorage.removeItem(setting);
|
localStorage.removeItem(setting);
|
||||||
})
|
});
|
||||||
window.location.reload();
|
window.location.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
showThemeDialog() {
|
showThemeDialog() {
|
||||||
const {classes} = this.props;
|
const { classes } = this.props;
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.themeDialogOpen}
|
open={this.state.themeDialogOpen}
|
||||||
|
@ -196,7 +245,9 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
aria-labelledby="confirmation-dialog-title"
|
aria-labelledby="confirmation-dialog-title"
|
||||||
>
|
>
|
||||||
<DialogTitle id="confirmation-dialog-title">Choose a theme</DialogTitle>
|
<DialogTitle id="confirmation-dialog-title">
|
||||||
|
Choose a theme
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<Grid container spacing={16}>
|
<Grid container spacing={16}>
|
||||||
<Grid item xs={12} md={6}>
|
<Grid item xs={12} md={6}>
|
||||||
|
@ -204,17 +255,31 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
aria-label="Theme"
|
aria-label="Theme"
|
||||||
name="colorScheme"
|
name="colorScheme"
|
||||||
value={this.state.selectThemeName}
|
value={this.state.selectThemeName}
|
||||||
onChange={(e, value) => this.changeThemeName(value)}
|
onChange={(e, value) =>
|
||||||
|
this.changeThemeName(value)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{themes.map(theme => (
|
{themes.map(theme => (
|
||||||
<FormControlLabel value={theme.key} key={theme.key} control={<Radio />} label={theme.name} />
|
<FormControlLabel
|
||||||
|
value={theme.key}
|
||||||
|
key={theme.key}
|
||||||
|
control={<Radio />}
|
||||||
|
label={theme.name}
|
||||||
|
/>
|
||||||
))}
|
))}
|
||||||
))}
|
))}
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Grid item xs={12} md={6} className={classes.desktopOnly}>
|
<Grid
|
||||||
<Typography variant="h6" component="p">Theme preview</Typography>
|
item
|
||||||
<ThemePreview theme={this.state.previewTheme}/>
|
xs={12}
|
||||||
|
md={6}
|
||||||
|
className={classes.desktopOnly}
|
||||||
|
>
|
||||||
|
<Typography variant="h6" component="p">
|
||||||
|
Theme preview
|
||||||
|
</Typography>
|
||||||
|
<ThemePreview theme={this.state.previewTheme} />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
|
@ -240,22 +305,54 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
fullWidth={true}
|
fullWidth={true}
|
||||||
aria-labelledby="confirmation-dialog-title"
|
aria-labelledby="confirmation-dialog-title"
|
||||||
>
|
>
|
||||||
<DialogTitle id="confirmation-dialog-title">Set your default visibility</DialogTitle>
|
<DialogTitle id="confirmation-dialog-title">
|
||||||
|
Set your default visibility
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<RadioGroup
|
<RadioGroup
|
||||||
aria-label="Visibility"
|
aria-label="Visibility"
|
||||||
name="visibility"
|
name="visibility"
|
||||||
value={this.state.defaultVisibility}
|
value={this.state.defaultVisibility}
|
||||||
onChange={(e, value) => this.changeVisibility(value as Visibility)}
|
onChange={(e, value) =>
|
||||||
|
this.changeVisibility(value as Visibility)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<FormControlLabel value={"public"} key={"public"} control={<Radio />} label={`Public ${this.state.federated? "": "(disabled by provider)"}`} disabled={!this.state.federated}/>
|
<FormControlLabel
|
||||||
<FormControlLabel value={"unlisted"} key={"unlisted"} control={<Radio />} label={"Unlisted"} />
|
value={"public"}
|
||||||
<FormControlLabel value={"private"} key={"private"} control={<Radio />} label={"Private (followers only)"} />
|
key={"public"}
|
||||||
<FormControlLabel value={"direct"} key={"direct"} control={<Radio />} label={"Direct"} />
|
control={<Radio />}
|
||||||
|
label={`Public ${
|
||||||
|
this.state.federated
|
||||||
|
? ""
|
||||||
|
: "(disabled by provider)"
|
||||||
|
}`}
|
||||||
|
disabled={!this.state.federated}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value={"unlisted"}
|
||||||
|
key={"unlisted"}
|
||||||
|
control={<Radio />}
|
||||||
|
label={"Unlisted"}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value={"private"}
|
||||||
|
key={"private"}
|
||||||
|
control={<Radio />}
|
||||||
|
label={"Private (followers only)"}
|
||||||
|
/>
|
||||||
|
<FormControlLabel
|
||||||
|
value={"direct"}
|
||||||
|
key={"direct"}
|
||||||
|
control={<Radio />}
|
||||||
|
label={"Direct"}
|
||||||
|
/>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={this.toggleVisibilityDialog} color="default">
|
<Button
|
||||||
|
onClick={this.toggleVisibilityDialog}
|
||||||
|
color="default"
|
||||||
|
>
|
||||||
Cancel
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={this.setVisibility} color="secondary">
|
<Button onClick={this.setVisibility} color="secondary">
|
||||||
|
@ -271,19 +368,28 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.resetSettingsDialog}
|
open={this.state.resetSettingsDialog}
|
||||||
onClose={() => this.toggleResetSettingsDialog()}
|
onClose={() => this.toggleResetSettingsDialog()}
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">Are you sure you want to refresh settings?</DialogTitle>
|
<DialogTitle id="alert-dialog-title">
|
||||||
|
Are you sure you want to refresh settings?
|
||||||
|
</DialogTitle>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => this.toggleResetSettingsDialog()} color="primary" autoFocus>
|
<Button
|
||||||
Cancel
|
onClick={() => this.toggleResetSettingsDialog()}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => {
|
<Button
|
||||||
this.refresh();
|
onClick={() => {
|
||||||
}} color="primary">
|
this.refresh();
|
||||||
Refresh
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,24 +398,35 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
<Dialog
|
<Dialog
|
||||||
open={this.state.resetHyperspaceDialog}
|
open={this.state.resetHyperspaceDialog}
|
||||||
onClose={() => this.toggleResetDialog()}
|
onClose={() => this.toggleResetDialog()}
|
||||||
>
|
>
|
||||||
<DialogTitle id="alert-dialog-title">Reset {this.state.brandName}?</DialogTitle>
|
<DialogTitle id="alert-dialog-title">
|
||||||
|
Reset {this.state.brandName}?
|
||||||
|
</DialogTitle>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogContentText id="alert-dialog-description">
|
<DialogContentText id="alert-dialog-description">
|
||||||
Are you sure you want to reset {this.state.brandName}? You'll need to re-authorize {this.state.brandName} access again.
|
Are you sure you want to reset {this.state.brandName}?
|
||||||
|
You'll need to re-authorize {this.state.brandName}{" "}
|
||||||
|
access again.
|
||||||
</DialogContentText>
|
</DialogContentText>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
<DialogActions>
|
<DialogActions>
|
||||||
<Button onClick={() => this.toggleResetDialog()} color="primary" autoFocus>
|
<Button
|
||||||
Cancel
|
onClick={() => this.toggleResetDialog()}
|
||||||
|
color="primary"
|
||||||
|
autoFocus
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
</Button>
|
</Button>
|
||||||
<Button onClick={() => {
|
<Button
|
||||||
this.reset();
|
onClick={() => {
|
||||||
}} color="primary">
|
this.reset();
|
||||||
Reset
|
}}
|
||||||
|
color="primary"
|
||||||
|
>
|
||||||
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</DialogActions>
|
</DialogActions>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -322,34 +439,43 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<DevicesIcon color="action"/>
|
<DevicesIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Match system appearance" secondary="Obey light/dark theme from your system"/>
|
<ListItemText
|
||||||
|
primary="Match system appearance"
|
||||||
|
secondary="Obey light/dark theme from your system"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
checked={this.state.systemDecidesDarkMode}
|
checked={this.state.systemDecidesDarkMode}
|
||||||
onChange={this.toggleSystemDarkMode}
|
onChange={this.toggleSystemDarkMode}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<Brightness3Icon color="action"/>
|
<Brightness3Icon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Dark mode" secondary="Toggles light or dark theme"/>
|
<ListItemText
|
||||||
|
primary="Dark mode"
|
||||||
|
secondary="Toggles light or dark theme"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
disabled={this.state.systemDecidesDarkMode}
|
disabled={this.state.systemDecidesDarkMode}
|
||||||
checked={this.state.darkModeEnabled}
|
checked={this.state.darkModeEnabled}
|
||||||
onChange={this.toggleDarkMode}
|
onChange={this.toggleDarkMode}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<PaletteIcon color="action"/>
|
<PaletteIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Interface theme" secondary="The color palette used for the interface"/>
|
<ListItemText
|
||||||
|
primary="Interface theme"
|
||||||
|
secondary="The color palette used for the interface"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Button onClick={this.toggleThemeDialog}>
|
<Button onClick={this.toggleThemeDialog}>
|
||||||
Set theme
|
Set theme
|
||||||
|
@ -358,41 +484,69 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br/>
|
<br />
|
||||||
<ListSubheader>Your Account</ListSubheader>
|
<ListSubheader>Your Account</ListSubheader>
|
||||||
<Paper className={classes.pageListConstraints}>
|
<Paper className={classes.pageListConstraints}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<AccountEditIcon color="action"/>
|
<AccountEditIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Edit your profile" secondary="Change your bio, display name, and images"/>
|
<ListItemText
|
||||||
|
primary="Edit your profile"
|
||||||
|
secondary="Change your bio, display name, and images"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<LinkableButton to="/you">Edit</LinkableButton>
|
<LinkableButton to="/you">Edit</LinkableButton>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<MastodonIcon color="action"/>
|
<CancelIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Configure on Mastodon"/>
|
<ListItemText
|
||||||
|
primary="Manage blocked servers"
|
||||||
|
secondary="View and manage servers that you've blocked"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<IconButton href={(localStorage.getItem("baseurl") as string) + "/settings/preferences"} target="_blank" rel="noreferrer">
|
<LinkableButton to="/blocked">
|
||||||
<OpenInNewIcon/>
|
Manage
|
||||||
|
</LinkableButton>
|
||||||
|
</ListItemSecondaryAction>
|
||||||
|
</ListItem>
|
||||||
|
<ListItem>
|
||||||
|
<ListItemAvatar>
|
||||||
|
<MastodonIcon color="action" />
|
||||||
|
</ListItemAvatar>
|
||||||
|
<ListItemText primary="Configure on Mastodon" />
|
||||||
|
<ListItemSecondaryAction>
|
||||||
|
<IconButton
|
||||||
|
href={
|
||||||
|
(localStorage.getItem(
|
||||||
|
"baseurl"
|
||||||
|
) as string) + "/settings/preferences"
|
||||||
|
}
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<OpenInNewIcon />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br/>
|
<br />
|
||||||
<ListSubheader>Composer</ListSubheader>
|
<ListSubheader>Composer</ListSubheader>
|
||||||
<Paper className={classes.pageListConstraints}>
|
<Paper className={classes.pageListConstraints}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<VisibilityIcon color="action"/>
|
<VisibilityIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Default visibility" secondary="New posts in composer will use this visiblity"/>
|
<ListItemText
|
||||||
|
primary="Default visibility"
|
||||||
|
secondary="New posts in composer will use this visiblity"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Button onClick={this.toggleVisibilityDialog}>
|
<Button onClick={this.toggleVisibilityDialog}>
|
||||||
Change
|
Change
|
||||||
|
@ -401,73 +555,92 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br/>
|
<br />
|
||||||
<ListSubheader>Notifications</ListSubheader>
|
<ListSubheader>Notifications</ListSubheader>
|
||||||
<Paper className={classes.pageListConstraints}>
|
<Paper className={classes.pageListConstraints}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<NotificationsIcon color="action"/>
|
<NotificationsIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Enable push notifications"
|
primary="Enable push notifications"
|
||||||
secondary={
|
secondary={
|
||||||
getUserDefaultBool('userDeniedNotification')?
|
getUserDefaultBool("userDeniedNotification")
|
||||||
"Check your browser's notification permissions.":
|
? "Check your browser's notification permissions."
|
||||||
browserSupportsNotificationRequests()?
|
: browserSupportsNotificationRequests()
|
||||||
"Send a push notification when not focused.":
|
? "Send a push notification when not focused."
|
||||||
"Notifications aren't supported."
|
: "Notifications aren't supported."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
checked={this.state.pushNotificationsEnabled}
|
checked={
|
||||||
|
this.state.pushNotificationsEnabled
|
||||||
|
}
|
||||||
onChange={this.togglePushNotifications}
|
onChange={this.togglePushNotifications}
|
||||||
disabled={!browserSupportsNotificationRequests() || getUserDefaultBool('userDeniedNotification')}
|
disabled={
|
||||||
|
!browserSupportsNotificationRequests() ||
|
||||||
|
getUserDefaultBool(
|
||||||
|
"userDeniedNotification"
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<BellAlertIcon color="action"/>
|
<BellAlertIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText
|
<ListItemText
|
||||||
primary="Notification badge counts all notifications"
|
primary="Notification badge counts all notifications"
|
||||||
secondary={
|
secondary={
|
||||||
"Counts all notifications, read or unread."
|
"Counts all notifications, read or unread."
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Switch
|
<Switch
|
||||||
checked={this.state.badgeDisplaysAllNotifs}
|
checked={this.state.badgeDisplaysAllNotifs}
|
||||||
onChange={this.toggleBadgeCount}
|
onChange={this.toggleBadgeCount}
|
||||||
/>
|
/>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
</List>
|
</List>
|
||||||
</Paper>
|
</Paper>
|
||||||
<br/>
|
<br />
|
||||||
<ListSubheader>Advanced</ListSubheader>
|
<ListSubheader>Advanced</ListSubheader>
|
||||||
<Paper className={classes.pageListConstraints}>
|
<Paper className={classes.pageListConstraints}>
|
||||||
<List>
|
<List>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<RefreshIcon color="action"/>
|
<RefreshIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary="Refresh settings" secondary="Reset the settings to defaults."/>
|
<ListItemText
|
||||||
|
primary="Refresh settings"
|
||||||
|
secondary="Reset the settings to defaults."
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Button onClick={() => this.toggleResetSettingsDialog()}>
|
<Button
|
||||||
|
onClick={() =>
|
||||||
|
this.toggleResetSettingsDialog()
|
||||||
|
}
|
||||||
|
>
|
||||||
Refresh
|
Refresh
|
||||||
</Button>
|
</Button>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
<ListItem>
|
<ListItem>
|
||||||
<ListItemAvatar>
|
<ListItemAvatar>
|
||||||
<UndoIcon color="action"/>
|
<UndoIcon color="action" />
|
||||||
</ListItemAvatar>
|
</ListItemAvatar>
|
||||||
<ListItemText primary={`Reset ${this.state.brandName}`} secondary="Deletes all data and resets the app"/>
|
<ListItemText
|
||||||
|
primary={`Reset ${this.state.brandName}`}
|
||||||
|
secondary="Deletes all data and resets the app"
|
||||||
|
/>
|
||||||
<ListItemSecondaryAction>
|
<ListItemSecondaryAction>
|
||||||
<Button onClick={() => this.toggleResetDialog()}>
|
<Button
|
||||||
|
onClick={() => this.toggleResetDialog()}
|
||||||
|
>
|
||||||
Reset
|
Reset
|
||||||
</Button>
|
</Button>
|
||||||
</ListItemSecondaryAction>
|
</ListItemSecondaryAction>
|
||||||
|
@ -481,7 +654,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(SettingsPage);
|
export default withStyles(styles)(SettingsPage);
|
||||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,72 +1,73 @@
|
||||||
import { Theme, createStyles } from '@material-ui/core';
|
import { Theme, createStyles } from "@material-ui/core";
|
||||||
|
|
||||||
export const styles = (theme: Theme) => createStyles({
|
export const styles = (theme: Theme) =>
|
||||||
|
createStyles({
|
||||||
root: {
|
root: {
|
||||||
width: '100%',
|
width: "100%",
|
||||||
height: '100%',
|
height: "100%",
|
||||||
backgroundPosition: 'center',
|
backgroundPosition: "center",
|
||||||
backgroundRepeat: 'no-repeat',
|
backgroundRepeat: "no-repeat",
|
||||||
backgroundSize: 'cover',
|
backgroundSize: "cover",
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
position: "absolute",
|
position: "absolute",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
paddingTop: theme.spacing.unit * 4,
|
paddingTop: theme.spacing.unit * 4,
|
||||||
paddingLeft: '25%',
|
paddingLeft: "25%",
|
||||||
paddingRight: '25%',
|
paddingRight: "25%"
|
||||||
},
|
},
|
||||||
[theme.breakpoints.up('lg')]: {
|
[theme.breakpoints.up("lg")]: {
|
||||||
paddingTop: theme.spacing.unit * 12,
|
paddingTop: theme.spacing.unit * 12,
|
||||||
paddingLeft: '35%',
|
paddingLeft: "35%",
|
||||||
paddingRight: '35%',
|
paddingRight: "35%"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
titleBarRoot: {
|
titleBarRoot: {
|
||||||
top: 0,
|
top: 0,
|
||||||
left: 0,
|
left: 0,
|
||||||
height: 24,
|
height: 24,
|
||||||
width: '100%',
|
width: "100%",
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
||||||
textAlign: 'center',
|
textAlign: "center",
|
||||||
zIndex: 1000,
|
zIndex: 1000,
|
||||||
verticalAlign: 'middle',
|
verticalAlign: "middle",
|
||||||
WebkitUserSelect: 'none',
|
WebkitUserSelect: "none",
|
||||||
WebkitAppRegion: "drag",
|
WebkitAppRegion: "drag",
|
||||||
position: "absolute"
|
position: "absolute"
|
||||||
},
|
},
|
||||||
titleBarText: {
|
titleBarText: {
|
||||||
color: theme.palette.common.white,
|
color: theme.palette.common.white,
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
paddingTop: 2,
|
paddingTop: 2,
|
||||||
paddingBottom: 1
|
paddingBottom: 1
|
||||||
},
|
},
|
||||||
paper: {
|
paper: {
|
||||||
height: '100%',
|
height: "100%",
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
height: 'auto',
|
height: "auto",
|
||||||
paddingLeft: theme.spacing.unit * 8,
|
paddingLeft: theme.spacing.unit * 8,
|
||||||
paddingRight: theme.spacing.unit * 8,
|
paddingRight: theme.spacing.unit * 8,
|
||||||
paddingTop: theme.spacing.unit * 6,
|
paddingTop: theme.spacing.unit * 6
|
||||||
},
|
},
|
||||||
paddingTop: theme.spacing.unit * 12,
|
paddingTop: theme.spacing.unit * 12,
|
||||||
paddingLeft: theme.spacing.unit * 4,
|
paddingLeft: theme.spacing.unit * 4,
|
||||||
paddingRight: theme.spacing.unit * 4,
|
paddingRight: theme.spacing.unit * 4,
|
||||||
paddingBottom: theme.spacing.unit * 6,
|
paddingBottom: theme.spacing.unit * 6,
|
||||||
textAlign: 'center',
|
textAlign: "center"
|
||||||
},
|
},
|
||||||
welcomeLink: {
|
welcomeLink: {
|
||||||
color: theme.palette.primary.light
|
color: theme.palette.primary.light
|
||||||
},
|
},
|
||||||
flexGrow: {
|
flexGrow: {
|
||||||
flexGrow: 1
|
flexGrow: 1
|
||||||
},
|
},
|
||||||
middlePadding: {
|
middlePadding: {
|
||||||
height: theme.spacing.unit * 6
|
height: theme.spacing.unit * 6
|
||||||
},
|
},
|
||||||
logo: {
|
logo: {
|
||||||
[theme.breakpoints.up('sm')]: {
|
[theme.breakpoints.up("sm")]: {
|
||||||
height: 64,
|
height: 64,
|
||||||
width: "auto"
|
width: "auto"
|
||||||
},
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,192 +1,291 @@
|
||||||
import React, {Component} from 'react';
|
import React, { Component } from "react";
|
||||||
import {withStyles, Typography, Paper, Avatar, Button, TextField, ListItem, ListItemText, ListItemAvatar, List, Grid} from '@material-ui/core';
|
import {
|
||||||
import {withSnackbar, withSnackbarProps} from 'notistack';
|
withStyles,
|
||||||
import {styles} from './PageLayout.styles';
|
Typography,
|
||||||
import { Account } from '../types/Account';
|
Paper,
|
||||||
import Mastodon from 'megalodon';
|
Avatar,
|
||||||
import filedialog from 'file-dialog';
|
Button,
|
||||||
|
TextField,
|
||||||
|
ListItem,
|
||||||
|
ListItemText,
|
||||||
|
ListItemAvatar,
|
||||||
|
List,
|
||||||
|
Grid
|
||||||
|
} from "@material-ui/core";
|
||||||
|
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||||
|
import { styles } from "./PageLayout.styles";
|
||||||
|
import { Account } from "../types/Account";
|
||||||
|
import Mastodon from "megalodon";
|
||||||
|
import filedialog from "file-dialog";
|
||||||
|
|
||||||
import PersonIcon from '@material-ui/icons/Person';
|
import PersonIcon from "@material-ui/icons/Person";
|
||||||
|
|
||||||
interface IYouProps extends withSnackbarProps {
|
interface IYouProps extends withSnackbarProps {
|
||||||
classes: any;
|
classes: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IYouState {
|
interface IYouState {
|
||||||
currentAccount: Account;
|
currentAccount: Account;
|
||||||
newDisplayName?: string;
|
newDisplayName?: string;
|
||||||
newBio?: string;
|
newBio?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
class You extends Component<IYouProps, IYouState> {
|
class You extends Component<IYouProps, IYouState> {
|
||||||
|
client: Mastodon;
|
||||||
|
|
||||||
client: Mastodon;
|
constructor(props: any) {
|
||||||
|
super(props);
|
||||||
|
|
||||||
constructor(props: any) {
|
this.client = new Mastodon(
|
||||||
super(props);
|
localStorage.getItem("access_token") as string,
|
||||||
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
|
);
|
||||||
|
|
||||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
this.state = {
|
||||||
|
currentAccount: this.getAccount()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
this.state = {
|
getAccount() {
|
||||||
currentAccount: this.getAccount()
|
let acct = localStorage.getItem("account");
|
||||||
|
if (acct) {
|
||||||
|
return JSON.parse(acct);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updateAvatar() {
|
||||||
|
filedialog({
|
||||||
|
multiple: false,
|
||||||
|
accept: "image/*"
|
||||||
|
})
|
||||||
|
.then((images: FileList) => {
|
||||||
|
if (images.length > 0) {
|
||||||
|
this.props.enqueueSnackbar("Updating avatar...", {
|
||||||
|
persist: true,
|
||||||
|
key: "persistAvatar"
|
||||||
|
});
|
||||||
|
let upload = new FormData();
|
||||||
|
upload.append("avatar", images[0]);
|
||||||
|
this.client
|
||||||
|
.patch("/accounts/update_credentials", upload)
|
||||||
|
.then((acct: any) => {
|
||||||
|
let currentAccount: Account = acct.data;
|
||||||
|
this.setState({ currentAccount });
|
||||||
|
localStorage.setItem("account", JSON.stringify(currentAccount));
|
||||||
|
this.props.closeSnackbar("persistAvatar");
|
||||||
|
this.props.enqueueSnackbar("Avatar updated successfully.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.closeSnackbar("persistAvatar");
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
"Couldn't update avatar: " + err.name,
|
||||||
|
{ variant: "error" }
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
getAccount() {
|
updateHeader() {
|
||||||
let acct = localStorage.getItem('account');
|
filedialog({
|
||||||
if (acct) {
|
multiple: false,
|
||||||
return JSON.parse(acct);
|
accept: "image/*"
|
||||||
|
})
|
||||||
|
.then((images: FileList) => {
|
||||||
|
if (images.length > 0) {
|
||||||
|
this.props.enqueueSnackbar("Updating header...", {
|
||||||
|
persist: true,
|
||||||
|
key: "persistHeader"
|
||||||
|
});
|
||||||
|
let upload = new FormData();
|
||||||
|
upload.append("header", images[0]);
|
||||||
|
this.client
|
||||||
|
.patch("/accounts/update_credentials", upload)
|
||||||
|
.then((acct: any) => {
|
||||||
|
let currentAccount: Account = acct.data;
|
||||||
|
this.setState({ currentAccount });
|
||||||
|
localStorage.setItem("account", JSON.stringify(currentAccount));
|
||||||
|
this.props.closeSnackbar("persistHeader");
|
||||||
|
this.props.enqueueSnackbar("Header updated successfully.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.closeSnackbar("persistHeader");
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
"Couldn't update header: " + err.name,
|
||||||
|
{ variant: "error" }
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
updateAvatar() {
|
removeHTMLContent(text: string) {
|
||||||
filedialog({
|
const div = document.createElement("div");
|
||||||
multiple: false,
|
div.innerHTML = text;
|
||||||
accept: "image/*"
|
let innerContent = div.textContent || div.innerText || "";
|
||||||
}).then((images: FileList) => {
|
return innerContent;
|
||||||
if (images.length > 0) {
|
}
|
||||||
this.props.enqueueSnackbar("Updating avatar...", { persist: true, key: "persistAvatar" });
|
changeDisplayName() {
|
||||||
let upload = new FormData();
|
this.client
|
||||||
upload.append("avatar", images[0]);
|
.patch("/accounts/update_credentials", {
|
||||||
this.client.patch("/accounts/update_credentials", upload).then((acct: any) => {
|
display_name: this.state.newDisplayName
|
||||||
let currentAccount: Account = acct.data;
|
? this.state.newDisplayName
|
||||||
this.setState({ currentAccount });
|
: this.state.currentAccount.display_name
|
||||||
localStorage.setItem("account", JSON.stringify(currentAccount));
|
})
|
||||||
this.props.closeSnackbar("persistAvatar");
|
.then((acct: any) => {
|
||||||
this.props.enqueueSnackbar("Avatar updated successfully.");
|
let currentAccount: Account = acct.data;
|
||||||
}).catch((err: Error) => {
|
this.setState({ currentAccount });
|
||||||
this.props.closeSnackbar("persistAvatar");
|
localStorage.setItem("account", JSON.stringify(currentAccount));
|
||||||
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name, { variant: "error" });
|
this.props.closeSnackbar("persistHeader");
|
||||||
})
|
this.props.enqueueSnackbar(
|
||||||
}
|
"Display name updated to " + this.state.newDisplayName
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateHeader() {
|
|
||||||
filedialog({
|
|
||||||
multiple: false,
|
|
||||||
accept: "image/*"
|
|
||||||
}).then((images: FileList) => {
|
|
||||||
if (images.length > 0) {
|
|
||||||
this.props.enqueueSnackbar("Updating header...", { persist: true, key: "persistHeader" });
|
|
||||||
let upload = new FormData();
|
|
||||||
upload.append("header", images[0]);
|
|
||||||
this.client.patch("/accounts/update_credentials", upload).then((acct: any) => {
|
|
||||||
let currentAccount: Account = acct.data;
|
|
||||||
this.setState({ currentAccount });
|
|
||||||
localStorage.setItem("account", JSON.stringify(currentAccount));
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Header updated successfully.");
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Couldn't update header: " + err.name, { variant: "error" });
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
removeHTMLContent(text: string) {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.innerHTML = text;
|
|
||||||
let innerContent = div.textContent || div.innerText || "";
|
|
||||||
return innerContent;
|
|
||||||
}
|
|
||||||
changeDisplayName() {
|
|
||||||
this.client.patch('/accounts/update_credentials', {
|
|
||||||
display_name: this.state.newDisplayName? this.state.newDisplayName: this.state.currentAccount.display_name
|
|
||||||
})
|
|
||||||
.then((acct: any) =>{
|
|
||||||
let currentAccount: Account = acct.data
|
|
||||||
this.setState({currentAccount});
|
|
||||||
localStorage.setItem('account', JSON.stringify(currentAccount));
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Display name updated to " + this.state.newDisplayName);
|
|
||||||
} ).catch((err:Error) => {
|
|
||||||
console.error(err.name)
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Couldn't update display name: " + err.name, { variant: "error" })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateDisplayname(name: string) {
|
|
||||||
this.setState({ newDisplayName: name });
|
|
||||||
};
|
|
||||||
changeBio() {
|
|
||||||
this.client.patch('/accounts/update_credentials', {note: this.state.newBio? this.state.newBio: this.state.currentAccount.note})
|
|
||||||
.then((acct:any) => {
|
|
||||||
let currentAccount: Account = acct.data
|
|
||||||
this.setState({currentAccount});
|
|
||||||
localStorage.setItem('account', JSON.stringify(currentAccount));
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Bio updated successfully.");
|
|
||||||
}).catch((err: Error) => {
|
|
||||||
console.error(err.name)
|
|
||||||
this.props.closeSnackbar("persistHeader");
|
|
||||||
this.props.enqueueSnackbar("Couldn't update bio: " + err.name, { variant: "error"});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
updateBio(bio:string){
|
|
||||||
this.setState({newBio:bio})
|
|
||||||
}
|
|
||||||
|
|
||||||
render() {
|
|
||||||
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.pageHeroContent}>
|
|
||||||
<Avatar className={classes.pageProfileAvatar} src={this.state.currentAccount.avatar_static}/>
|
|
||||||
<Typography variant="h4" color="inherit" component="h1">Edit your profile</Typography>
|
|
||||||
<br/>
|
|
||||||
<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>
|
|
||||||
<br/>
|
|
||||||
</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)}>
|
|
||||||
</TextField>
|
|
||||||
<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)}>
|
|
||||||
</TextField>
|
|
||||||
<div style={{textAlign: "right"}}>
|
|
||||||
<Button className={classes.pageProfileFollowButton} color = "primary" onClick = {() => this.changeBio()}>Update biography</Button>
|
|
||||||
</div>
|
|
||||||
</Paper>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
console.error(err.name);
|
||||||
|
this.props.closeSnackbar("persistHeader");
|
||||||
|
this.props.enqueueSnackbar(
|
||||||
|
"Couldn't update display name: " + err.name,
|
||||||
|
{ variant: "error" }
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateDisplayname(name: string) {
|
||||||
|
this.setState({ newDisplayName: name });
|
||||||
|
}
|
||||||
|
changeBio() {
|
||||||
|
this.client
|
||||||
|
.patch("/accounts/update_credentials", {
|
||||||
|
note: this.state.newBio
|
||||||
|
? this.state.newBio
|
||||||
|
: this.state.currentAccount.note
|
||||||
|
})
|
||||||
|
.then((acct: any) => {
|
||||||
|
let currentAccount: Account = acct.data;
|
||||||
|
this.setState({ currentAccount });
|
||||||
|
localStorage.setItem("account", JSON.stringify(currentAccount));
|
||||||
|
this.props.closeSnackbar("persistHeader");
|
||||||
|
this.props.enqueueSnackbar("Bio updated successfully.");
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
console.error(err.name);
|
||||||
|
this.props.closeSnackbar("persistHeader");
|
||||||
|
this.props.enqueueSnackbar("Couldn't update bio: " + err.name, {
|
||||||
|
variant: "error"
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateBio(bio: string) {
|
||||||
|
this.setState({ newBio: bio });
|
||||||
|
}
|
||||||
|
|
||||||
|
render() {
|
||||||
|
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.pageHeroContent}>
|
||||||
|
<Avatar
|
||||||
|
className={classes.pageProfileAvatar}
|
||||||
|
src={this.state.currentAccount.avatar_static}
|
||||||
|
/>
|
||||||
|
<Typography variant="h4" color="inherit" component="h1">
|
||||||
|
Edit your profile
|
||||||
|
</Typography>
|
||||||
|
<br />
|
||||||
|
<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>
|
||||||
|
<br />
|
||||||
|
</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)
|
||||||
|
}
|
||||||
|
></TextField>
|
||||||
|
<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)}
|
||||||
|
></TextField>
|
||||||
|
<div style={{ textAlign: "right" }}>
|
||||||
|
<Button
|
||||||
|
className={classes.pageProfileFollowButton}
|
||||||
|
color="primary"
|
||||||
|
onClick={() => this.changeBio()}
|
||||||
|
>
|
||||||
|
Update biography
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Paper>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default withStyles(styles)(withSnackbar(You));
|
export default withStyles(styles)(withSnackbar(You));
|
||||||
|
|
|
@ -1,34 +1,34 @@
|
||||||
import { MastodonEmoji } from './Emojis';
|
import { MastodonEmoji } from "./Emojis";
|
||||||
import { Field } from './Field';
|
import { Field } from "./Field";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic type for an account on Mastodon
|
* Basic type for an account on Mastodon
|
||||||
*/
|
*/
|
||||||
export type Account = {
|
export type Account = {
|
||||||
id: string;
|
id: string;
|
||||||
username: string;
|
username: string;
|
||||||
acct: string;
|
acct: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
locked: boolean;
|
locked: boolean;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
followers_count: number;
|
followers_count: number;
|
||||||
following_count: number;
|
following_count: number;
|
||||||
statuses_count: number;
|
statuses_count: number;
|
||||||
note: string;
|
note: string;
|
||||||
url: string;
|
url: string;
|
||||||
avatar: string;
|
avatar: string;
|
||||||
avatar_static: string;
|
avatar_static: string;
|
||||||
header: string;
|
header: string;
|
||||||
header_static: string;
|
header_static: string;
|
||||||
emojis: [MastodonEmoji];
|
emojis: [MastodonEmoji];
|
||||||
moved: Account | null;
|
moved: Account | null;
|
||||||
fields: [Field];
|
fields: [Field];
|
||||||
bot: boolean | null;
|
bot: boolean | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type UAccount = {
|
export type UAccount = {
|
||||||
id: string;
|
id: string;
|
||||||
acct: string;
|
acct: string;
|
||||||
display_name: string;
|
display_name: string;
|
||||||
avatar_static: string;
|
avatar_static: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,12 +2,12 @@
|
||||||
* Basic type for an attachment, usually on Statuses
|
* Basic type for an attachment, usually on Statuses
|
||||||
*/
|
*/
|
||||||
export type Attachment = {
|
export type Attachment = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "unknown" | "image" | "gifv" | "video";
|
type: "unknown" | "image" | "gifv" | "video";
|
||||||
url: string;
|
url: string;
|
||||||
remote_url: string | null;
|
remote_url: string | null;
|
||||||
preview_url: string;
|
preview_url: string;
|
||||||
text_url: string | null;
|
text_url: string | null;
|
||||||
meta: any | null;
|
meta: any | null;
|
||||||
description: string | null;
|
description: string | null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
* Basic type for Cards, usually in Statuses
|
* Basic type for Cards, usually in Statuses
|
||||||
*/
|
*/
|
||||||
export type Card = {
|
export type Card = {
|
||||||
url: string;
|
url: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
image: string | null;
|
image: string | null;
|
||||||
type: "link" | "photo" | "video" | "rich";
|
type: "link" | "photo" | "video" | "rich";
|
||||||
author_name: string | null;
|
author_name: string | null;
|
||||||
author_url: string | null;
|
author_url: string | null;
|
||||||
provider_name: string | null;
|
provider_name: string | null;
|
||||||
provider_url: string | null;
|
provider_url: string | null;
|
||||||
html: string | null;
|
html: string | null;
|
||||||
width: number | null;
|
width: number | null;
|
||||||
height: number | null;
|
height: number | null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,31 +1,31 @@
|
||||||
export type Config = {
|
export type Config = {
|
||||||
version: string;
|
version: string;
|
||||||
location: string;
|
location: string;
|
||||||
branding?: {
|
branding?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
logo?: string;
|
logo?: string;
|
||||||
background?: string;
|
background?: string;
|
||||||
};
|
};
|
||||||
developer?: boolean;
|
developer?: boolean;
|
||||||
federation: Federation;
|
federation: Federation;
|
||||||
registration?: {
|
registration?: {
|
||||||
defaultInstance?: string;
|
defaultInstance?: string;
|
||||||
};
|
};
|
||||||
admin?: {
|
admin?: {
|
||||||
name?: string;
|
name?: string;
|
||||||
account?: string;
|
account?: string;
|
||||||
};
|
};
|
||||||
license: License;
|
license: License;
|
||||||
repository?: string;
|
repository?: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type License = {
|
export type License = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type Federation = {
|
export type Federation = {
|
||||||
universalLogin: boolean;
|
universalLogin: boolean;
|
||||||
allowPublicPosts: boolean;
|
allowPublicPosts: boolean;
|
||||||
enablePublicTimeline: boolean;
|
enablePublicTimeline: boolean;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { Status } from './Status';
|
import { Status } from "./Status";
|
||||||
|
|
||||||
export type Context = {
|
export type Context = {
|
||||||
ancestors: [Status];
|
ancestors: [Status];
|
||||||
descendants: [Status];
|
descendants: [Status];
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,16 +2,16 @@
|
||||||
* Basic type for Emojis on Mastodon.
|
* Basic type for Emojis on Mastodon.
|
||||||
*/
|
*/
|
||||||
export type MastodonEmoji = {
|
export type MastodonEmoji = {
|
||||||
shortcode: string;
|
shortcode: string;
|
||||||
static_url: string;
|
static_url: string;
|
||||||
url: string;
|
url: string;
|
||||||
visible_in_picker: boolean;
|
visible_in_picker: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Trimmed type of Emoji from emoji-mart
|
* Trimmed type of Emoji from emoji-mart
|
||||||
*/
|
*/
|
||||||
export type Emoji = {
|
export type Emoji = {
|
||||||
name: string;
|
name: string;
|
||||||
imageUrl: string;
|
imageUrl: string;
|
||||||
};
|
};
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* Basic type for a table entry, usually in Account
|
* Basic type for a table entry, usually in Account
|
||||||
*/
|
*/
|
||||||
export type Field = {
|
export type Field = {
|
||||||
name: string;
|
name: string;
|
||||||
value: string;
|
value: string;
|
||||||
verified_at: string | null;
|
verified_at: string | null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,130 +1,161 @@
|
||||||
import {Color} from '@material-ui/core';
|
import { Color } from "@material-ui/core";
|
||||||
import { deepPurple, red, lightGreen, yellow, purple, deepOrange, indigo, lightBlue, orange, blue, amber, pink, brown, blueGrey } from '@material-ui/core/colors';
|
import {
|
||||||
import { isDarwinApp } from '../utilities/desktop';
|
deepPurple,
|
||||||
|
red,
|
||||||
|
lightGreen,
|
||||||
|
yellow,
|
||||||
|
purple,
|
||||||
|
deepOrange,
|
||||||
|
indigo,
|
||||||
|
lightBlue,
|
||||||
|
orange,
|
||||||
|
blue,
|
||||||
|
amber,
|
||||||
|
pink,
|
||||||
|
brown,
|
||||||
|
blueGrey
|
||||||
|
} from "@material-ui/core/colors";
|
||||||
|
import { isDarwinApp } from "../utilities/desktop";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic theme colors for Hyperspace.
|
* Basic theme colors for Hyperspace.
|
||||||
*/
|
*/
|
||||||
export type HyperspaceTheme = {
|
export type HyperspaceTheme = {
|
||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary:
|
||||||
main: string;
|
| {
|
||||||
} | Color;
|
main: string;
|
||||||
secondary: {
|
}
|
||||||
main: string;
|
| Color;
|
||||||
} | Color;
|
secondary:
|
||||||
}
|
| {
|
||||||
}
|
main: string;
|
||||||
|
}
|
||||||
|
| Color;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
export const defaultTheme: HyperspaceTheme = {
|
export const defaultTheme: HyperspaceTheme = {
|
||||||
key: "defaultTheme",
|
key: "defaultTheme",
|
||||||
name: "Royal (Default)",
|
name: "Royal (Default)",
|
||||||
palette: {
|
palette: {
|
||||||
primary: deepPurple,
|
primary: deepPurple,
|
||||||
secondary: red
|
secondary: red
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const gardenerTheme: HyperspaceTheme = {
|
export const gardenerTheme: HyperspaceTheme = {
|
||||||
key: "gardnerTheme",
|
key: "gardnerTheme",
|
||||||
name: "Botanical",
|
name: "Botanical",
|
||||||
palette: {
|
palette: {
|
||||||
primary: lightGreen,
|
primary: lightGreen,
|
||||||
secondary: yellow
|
secondary: yellow
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const teacherTheme: HyperspaceTheme = {
|
export const teacherTheme: HyperspaceTheme = {
|
||||||
key: "teacherTheme",
|
key: "teacherTheme",
|
||||||
name: "Compassionate",
|
name: "Compassionate",
|
||||||
palette: {
|
palette: {
|
||||||
primary: purple,
|
primary: purple,
|
||||||
secondary: deepOrange
|
secondary: deepOrange
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const jokerTheme: HyperspaceTheme = {
|
export const jokerTheme: HyperspaceTheme = {
|
||||||
key: "jokerTheme",
|
key: "jokerTheme",
|
||||||
name: "Joker",
|
name: "Joker",
|
||||||
palette: {
|
palette: {
|
||||||
primary: indigo,
|
primary: indigo,
|
||||||
secondary: lightBlue
|
secondary: lightBlue
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const guardTheme: HyperspaceTheme = {
|
export const guardTheme: HyperspaceTheme = {
|
||||||
key: "guardTheme",
|
key: "guardTheme",
|
||||||
name: "Enthusiastic",
|
name: "Enthusiastic",
|
||||||
palette: {
|
palette: {
|
||||||
primary: blue,
|
primary: blue,
|
||||||
secondary: deepOrange
|
secondary: deepOrange
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const entertainerTheme: HyperspaceTheme = {
|
export const entertainerTheme: HyperspaceTheme = {
|
||||||
key: "entertainerTheme",
|
key: "entertainerTheme",
|
||||||
name: "Animated",
|
name: "Animated",
|
||||||
palette: {
|
palette: {
|
||||||
primary: pink,
|
primary: pink,
|
||||||
secondary: purple
|
secondary: purple
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const classicTheme: HyperspaceTheme = {
|
export const classicTheme: HyperspaceTheme = {
|
||||||
key: "classicTheme",
|
key: "classicTheme",
|
||||||
name: "Classic",
|
name: "Classic",
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: "#555555"
|
main: "#555555"
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#5c2d91"
|
main: "#5c2d91"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const dragonTheme: HyperspaceTheme = {
|
export const dragonTheme: HyperspaceTheme = {
|
||||||
key: "dragonTheme",
|
key: "dragonTheme",
|
||||||
name: "Adventurous",
|
name: "Adventurous",
|
||||||
palette: {
|
palette: {
|
||||||
primary: purple,
|
primary: purple,
|
||||||
secondary: purple
|
secondary: purple
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const memoriumTheme: HyperspaceTheme = {
|
export const memoriumTheme: HyperspaceTheme = {
|
||||||
key: "memoriumTheme",
|
key: "memoriumTheme",
|
||||||
name: "Memorial",
|
name: "Memorial",
|
||||||
palette: {
|
palette: {
|
||||||
primary: red,
|
primary: red,
|
||||||
secondary: red
|
secondary: red
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const blissTheme: HyperspaceTheme = {
|
export const blissTheme: HyperspaceTheme = {
|
||||||
key: "blissTheme",
|
key: "blissTheme",
|
||||||
name: "Bliss",
|
name: "Bliss",
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: "#3e2723"
|
main: "#3e2723"
|
||||||
},
|
},
|
||||||
secondary: lightBlue
|
secondary: lightBlue
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
export const attractTheme: HyperspaceTheme = {
|
export const attractTheme: HyperspaceTheme = {
|
||||||
key: "attractTheme",
|
key: "attractTheme",
|
||||||
name: "Attract",
|
name: "Attract",
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
main: '#E57373',
|
main: "#E57373"
|
||||||
},
|
},
|
||||||
secondary: {
|
secondary: {
|
||||||
main: "#78909C",
|
main: "#78909C"
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const themes = [defaultTheme, gardenerTheme, teacherTheme, jokerTheme, guardTheme, entertainerTheme, classicTheme, dragonTheme, memoriumTheme, blissTheme, attractTheme]
|
export const themes = [
|
||||||
|
defaultTheme,
|
||||||
|
gardenerTheme,
|
||||||
|
teacherTheme,
|
||||||
|
jokerTheme,
|
||||||
|
guardTheme,
|
||||||
|
entertainerTheme,
|
||||||
|
classicTheme,
|
||||||
|
dragonTheme,
|
||||||
|
memoriumTheme,
|
||||||
|
blissTheme,
|
||||||
|
attractTheme
|
||||||
|
];
|
||||||
|
|
|
@ -2,14 +2,14 @@ import { Field } from "./Field";
|
||||||
import { Account } from "./Account";
|
import { Account } from "./Account";
|
||||||
|
|
||||||
export type Instance = {
|
export type Instance = {
|
||||||
uri: string;
|
uri: string;
|
||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
email: string;
|
email: string;
|
||||||
version: string;
|
version: string;
|
||||||
thumbnail: string | null;
|
thumbnail: string | null;
|
||||||
urls: Field;
|
urls: Field;
|
||||||
stats: Field;
|
stats: Field;
|
||||||
languages: [string];
|
languages: [string];
|
||||||
contact_account: Account;
|
contact_account: Account;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,8 +2,8 @@
|
||||||
* Basic type for a person mentioned in a Status
|
* Basic type for a person mentioned in a Status
|
||||||
*/
|
*/
|
||||||
export type Mention = {
|
export type Mention = {
|
||||||
url: string;
|
url: string;
|
||||||
username: string;
|
username: string;
|
||||||
acct: string;
|
acct: string;
|
||||||
id: string;
|
id: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,9 +2,9 @@ import { Account } from "./Account";
|
||||||
import { Status } from "./Status";
|
import { Status } from "./Status";
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id: string;
|
id: string;
|
||||||
type: "follow" | "mention" | "reblog" | "favourite";
|
type: "follow" | "mention" | "reblog" | "favourite";
|
||||||
created_at: string;
|
created_at: string;
|
||||||
account: Account;
|
account: Account;
|
||||||
status: Status | null;
|
status: Status | null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -2,29 +2,29 @@
|
||||||
* Basic type for a Poll on Mastodon
|
* Basic type for a Poll on Mastodon
|
||||||
*/
|
*/
|
||||||
export type Poll = {
|
export type Poll = {
|
||||||
id: string;
|
id: string;
|
||||||
expires_at: string | null;
|
expires_at: string | null;
|
||||||
expired: boolean;
|
expired: boolean;
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
votes_count: number;
|
votes_count: number;
|
||||||
options: [PollOption];
|
options: [PollOption];
|
||||||
voted: boolean | null;
|
voted: boolean | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic type for a Poll option in a Poll
|
* Basic type for a Poll option in a Poll
|
||||||
*/
|
*/
|
||||||
export type PollOption = {
|
export type PollOption = {
|
||||||
title: string;
|
title: string;
|
||||||
votes_count: number | null;
|
votes_count: number | null;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PollWizard = {
|
export type PollWizard = {
|
||||||
expires_at: string;
|
expires_at: string;
|
||||||
multiple: boolean;
|
multiple: boolean;
|
||||||
options: PollWizardOption[];
|
options: PollWizardOption[];
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PollWizardOption = {
|
export type PollWizardOption = {
|
||||||
title: string;
|
title: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
export type Relationship = {
|
export type Relationship = {
|
||||||
id: string;
|
id: string;
|
||||||
following: boolean;
|
following: boolean;
|
||||||
followed_by: boolean;
|
followed_by: boolean;
|
||||||
blocking: boolean;
|
blocking: boolean;
|
||||||
muting: boolean;
|
muting: boolean;
|
||||||
muting_notifications: boolean;
|
muting_notifications: boolean;
|
||||||
requested: boolean;
|
requested: boolean;
|
||||||
domain_blocking: boolean;
|
domain_blocking: boolean;
|
||||||
showing_reblogs: boolean;
|
showing_reblogs: boolean;
|
||||||
endorsed: boolean;
|
endorsed: boolean;
|
||||||
}
|
};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { Status } from "./Status";
|
||||||
import { Tag } from "./Tag";
|
import { Tag } from "./Tag";
|
||||||
|
|
||||||
export type Results = {
|
export type Results = {
|
||||||
accounts: [Account];
|
accounts: [Account];
|
||||||
statuses: [Status];
|
statuses: [Status];
|
||||||
hashtags: [Tag];
|
hashtags: [Tag];
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
export type SaveClientSession = {
|
export type SaveClientSession = {
|
||||||
clientId: string;
|
clientId: string;
|
||||||
clientSecret: string;
|
clientSecret: string;
|
||||||
authUrl: string;
|
authUrl: string;
|
||||||
emergency: boolean;
|
emergency: boolean;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,40 +1,40 @@
|
||||||
import { MastodonEmoji } from './Emojis';
|
import { MastodonEmoji } from "./Emojis";
|
||||||
import { Visibility } from './Visibility';
|
import { Visibility } from "./Visibility";
|
||||||
import { Account } from './Account';
|
import { Account } from "./Account";
|
||||||
import { Attachment } from './Attachment';
|
import { Attachment } from "./Attachment";
|
||||||
import { Mention } from './Mention';
|
import { Mention } from "./Mention";
|
||||||
import { Poll } from './Poll';
|
import { Poll } from "./Poll";
|
||||||
import { Card } from './Card';
|
import { Card } from "./Card";
|
||||||
import { Tag } from './Tag';
|
import { Tag } from "./Tag";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Basic type for a status on Mastodon
|
* Basic type for a status on Mastodon
|
||||||
*/
|
*/
|
||||||
export type Status = {
|
export type Status = {
|
||||||
id: string;
|
id: string;
|
||||||
uri: string;
|
uri: string;
|
||||||
url: string | null;
|
url: string | null;
|
||||||
account: Account;
|
account: Account;
|
||||||
in_reply_to_id: string | null;
|
in_reply_to_id: string | null;
|
||||||
in_reply_to_account_id: string | null;
|
in_reply_to_account_id: string | null;
|
||||||
reblog: Status | null;
|
reblog: Status | null;
|
||||||
content: string;
|
content: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
emojis: [MastodonEmoji];
|
emojis: [MastodonEmoji];
|
||||||
replies_count: number;
|
replies_count: number;
|
||||||
reblogs_count: number;
|
reblogs_count: number;
|
||||||
favourites_count: number;
|
favourites_count: number;
|
||||||
reblogged: boolean | null;
|
reblogged: boolean | null;
|
||||||
favourited: boolean | null;
|
favourited: boolean | null;
|
||||||
muted: boolean | null;
|
muted: boolean | null;
|
||||||
sensitive: boolean;
|
sensitive: boolean;
|
||||||
spoiler_text: string;
|
spoiler_text: string;
|
||||||
visibility: Visibility;
|
visibility: Visibility;
|
||||||
media_attachments: [Attachment];
|
media_attachments: [Attachment];
|
||||||
mentions: [Mention];
|
mentions: [Mention];
|
||||||
tags: [Tag];
|
tags: [Tag];
|
||||||
card: Card | null;
|
card: Card | null;
|
||||||
poll: Poll | null;
|
poll: Poll | null;
|
||||||
application: any;
|
application: any;
|
||||||
pinned: boolean | null;
|
pinned: boolean | null;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
export type Tag = {
|
export type Tag = {
|
||||||
name: string;
|
name: string;
|
||||||
url: string;
|
url: string;
|
||||||
}
|
};
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
/**
|
/**
|
||||||
* Types of a post's visibility on Mastodon.
|
* Types of a post's visibility on Mastodon.
|
||||||
*/
|
*/
|
||||||
export type Visibility = "direct" | "private" | "unlisted" | "public";
|
export type Visibility = "direct" | "private" | "unlisted" | "public";
|
||||||
|
|
|
@ -1,21 +1,30 @@
|
||||||
import Mastodon from "megalodon";
|
import Mastodon from "megalodon";
|
||||||
|
|
||||||
export function userLoggedIn(): boolean {
|
export function userLoggedIn(): boolean {
|
||||||
if (localStorage.getItem('baseurl') && localStorage.getItem('access_token')) {
|
if (localStorage.getItem("baseurl") && localStorage.getItem("access_token")) {
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function refreshUserAccountData() {
|
export function refreshUserAccountData() {
|
||||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
let client = new Mastodon(
|
||||||
client.get('/accounts/verify_credentials').then((resp: any) => {
|
localStorage.getItem("access_token") as string,
|
||||||
localStorage.setItem('account', JSON.stringify(resp.data));
|
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||||
}).catch((err: Error) => {
|
);
|
||||||
console.error(err.message);
|
client
|
||||||
});
|
.get("/accounts/verify_credentials")
|
||||||
client.get('/instance').then((resp: any) => {
|
.then((resp: any) => {
|
||||||
localStorage.setItem('isPleroma', (resp.data.version.match(/Pleroma/) ? "true" : "false"))
|
localStorage.setItem("account", JSON.stringify(resp.data));
|
||||||
})
|
})
|
||||||
}
|
.catch((err: Error) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
client.get("/instance").then((resp: any) => {
|
||||||
|
localStorage.setItem(
|
||||||
|
"isPleroma",
|
||||||
|
resp.data.version.match(/Pleroma/) ? "true" : "false"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
|
@ -7,5 +7,5 @@ import { isDarwinApp } from "./desktop";
|
||||||
* @returns Boolean dictating if the title bar is visible
|
* @returns Boolean dictating if the title bar is visible
|
||||||
*/
|
*/
|
||||||
export function isAppbarExpanded(): boolean {
|
export function isAppbarExpanded(): boolean {
|
||||||
return isDarwinApp() || process.env.NODE_ENV === "development";
|
return isDarwinApp() || process.env.NODE_ENV === "development";
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
* A Window interface with extra properties for Electron
|
* A Window interface with extra properties for Electron
|
||||||
*/
|
*/
|
||||||
interface ElectronWindow extends Window {
|
interface ElectronWindow extends Window {
|
||||||
require?: any;
|
require?: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -10,22 +10,22 @@ interface ElectronWindow extends Window {
|
||||||
* @returns Boolean of whether it is in desktop mode or not
|
* @returns Boolean of whether it is in desktop mode or not
|
||||||
*/
|
*/
|
||||||
export function isDesktopApp(): boolean {
|
export function isDesktopApp(): boolean {
|
||||||
return navigator.userAgent.includes("Hyperspace" || "Electron");
|
return navigator.userAgent.includes("Hyperspace" || "Electron");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines whether the app is the macOS application
|
* Determines whether the app is the macOS application
|
||||||
*/
|
*/
|
||||||
export function isDarwinApp(): boolean {
|
export function isDarwinApp(): boolean {
|
||||||
return isDesktopApp() && navigator.userAgent.includes("Macintosh")
|
return isDesktopApp() && navigator.userAgent.includes("Macintosh");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determine whether the system is in dark mode or not (macOS)
|
* Determine whether the system is in dark mode or not (macOS)
|
||||||
*/
|
*/
|
||||||
export function isDarkMode() {
|
export function isDarkMode() {
|
||||||
// Lift window to an ElectronWindow and add use require()
|
// Lift window to an ElectronWindow and add use require()
|
||||||
const eWin = window as ElectronWindow;
|
const eWin = window as ElectronWindow;
|
||||||
const {remote} = eWin.require('electron');
|
const { remote } = eWin.require("electron");
|
||||||
return remote.systemPreferences.isDarkMode()
|
return remote.systemPreferences.isDarkMode();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import {MastodonEmoji} from '../types/Emojis';
|
import { MastodonEmoji } from "../types/Emojis";
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Takes a given string and replaces emoji codes with their respective image tags.
|
* Takes a given string and replaces emoji codes with their respective image tags.
|
||||||
|
@ -8,36 +8,51 @@ import Mastodon from 'megalodon';
|
||||||
* @param className The associated class for the string
|
* @param className The associated class for the string
|
||||||
* @returns String with image tags for emojis
|
* @returns String with image tags for emojis
|
||||||
*/
|
*/
|
||||||
export function emojifyString(contents: string, emojis: [MastodonEmoji], className?: any): string {
|
export function emojifyString(
|
||||||
let newContents: string = contents;
|
contents: string,
|
||||||
|
emojis: [MastodonEmoji],
|
||||||
|
className?: any
|
||||||
|
): string {
|
||||||
|
let newContents: string = contents;
|
||||||
|
|
||||||
emojis.forEach((emoji: MastodonEmoji) => {
|
emojis.forEach((emoji: MastodonEmoji) => {
|
||||||
let filter = new RegExp(`:${emoji.shortcode}:`, 'g');
|
let filter = new RegExp(`:${emoji.shortcode}:`, "g");
|
||||||
newContents = newContents.replace(filter, `<img src=${emoji.static_url} ${className? `class="${className}"`: ""}/>`)
|
newContents = newContents.replace(
|
||||||
})
|
filter,
|
||||||
|
`<img src=${emoji.static_url} ${
|
||||||
|
className ? `class="${className}"` : ""
|
||||||
|
}/>`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
return newContents;
|
return newContents;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function collectEmojisFromServer() {
|
export function collectEmojisFromServer() {
|
||||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
let client = new Mastodon(
|
||||||
let emojisPath = localStorage.getItem('emojis');
|
localStorage.getItem("access_token") as string,
|
||||||
let emojis: any[] = [];
|
localStorage.getItem("baseurl") + "/api/v1"
|
||||||
if (emojisPath === null) {
|
);
|
||||||
client.get('/custom_emojis').then((resp: any) => {
|
let emojisPath = localStorage.getItem("emojis");
|
||||||
resp.data.forEach((emoji: MastodonEmoji) => {
|
let emojis: any[] = [];
|
||||||
let customEmoji = {
|
if (emojisPath === null) {
|
||||||
name: emoji.shortcode,
|
client
|
||||||
emoticons: [''],
|
.get("/custom_emojis")
|
||||||
short_names: [emoji.shortcode],
|
.then((resp: any) => {
|
||||||
imageUrl: emoji.static_url,
|
resp.data.forEach((emoji: MastodonEmoji) => {
|
||||||
keywords: ['mastodon', 'custom']
|
let customEmoji = {
|
||||||
}
|
name: emoji.shortcode,
|
||||||
emojis.push(customEmoji);
|
emoticons: [""],
|
||||||
localStorage.setItem("emojis", JSON.stringify(emojis));
|
short_names: [emoji.shortcode],
|
||||||
})
|
imageUrl: emoji.static_url,
|
||||||
}).catch((err: Error) => {
|
keywords: ["mastodon", "custom"]
|
||||||
console.error(err.message);
|
};
|
||||||
})
|
emojis.push(customEmoji);
|
||||||
}
|
localStorage.setItem("emojis", JSON.stringify(emojis));
|
||||||
}
|
});
|
||||||
|
})
|
||||||
|
.catch((err: Error) => {
|
||||||
|
console.error(err.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import Mastodon from 'megalodon';
|
import Mastodon from "megalodon";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the Hyperspace app with the appropriate Redirect URI
|
* Creates the Hyperspace app with the appropriate Redirect URI
|
||||||
|
@ -7,34 +7,51 @@ import Mastodon from 'megalodon';
|
||||||
* @param baseurl The base URL of the instance
|
* @param baseurl The base URL of the instance
|
||||||
* @param redirect_uri The URL to redirect to when authorizing
|
* @param redirect_uri The URL to redirect to when authorizing
|
||||||
*/
|
*/
|
||||||
export function createHyperspaceApp(name: string, scopes: string, baseurl: string, redirect_uri: string) {
|
export function createHyperspaceApp(
|
||||||
let appName = name === "Hyperspace"? "Hyperspace": `${name} (Hyperspace-like)`
|
name: string,
|
||||||
return Mastodon.createApp(appName, {
|
scopes: string,
|
||||||
scopes: scopes,
|
baseurl: string,
|
||||||
redirect_uris: redirect_uri,
|
redirect_uri: string
|
||||||
website: 'https://hyperspace.marquiskurt.net',
|
) {
|
||||||
}, baseurl).then(appData => {
|
let appName =
|
||||||
return Mastodon.generateAuthUrl(appData.clientId, appData.clientSecret, {
|
name === "Hyperspace" ? "Hyperspace" : `${name} (Hyperspace-like)`;
|
||||||
redirect_uri: redirect_uri,
|
return Mastodon.createApp(
|
||||||
scope: scopes
|
appName,
|
||||||
}, baseurl).then(url => {
|
{
|
||||||
appData.url = url;
|
scopes: scopes,
|
||||||
return appData;
|
redirect_uris: redirect_uri,
|
||||||
})
|
website: "https://hyperspace.marquiskurt.net"
|
||||||
})
|
},
|
||||||
|
baseurl
|
||||||
|
).then(appData => {
|
||||||
|
return Mastodon.generateAuthUrl(
|
||||||
|
appData.clientId,
|
||||||
|
appData.clientSecret,
|
||||||
|
{
|
||||||
|
redirect_uri: redirect_uri,
|
||||||
|
scope: scopes
|
||||||
|
},
|
||||||
|
baseurl
|
||||||
|
).then(url => {
|
||||||
|
appData.url = url;
|
||||||
|
return appData;
|
||||||
|
});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the appropriate redirect address.
|
* Gets the appropriate redirect address.
|
||||||
* @param type The address or configuration to use
|
* @param type The address or configuration to use
|
||||||
*/
|
*/
|
||||||
export function getRedirectAddress(type: "desktop" | "dynamic" | string): string {
|
export function getRedirectAddress(
|
||||||
switch(type) {
|
type: "desktop" | "dynamic" | string
|
||||||
case "desktop":
|
): string {
|
||||||
return "hyperspace://hyperspace/app/";
|
switch (type) {
|
||||||
case "dynamic":
|
case "desktop":
|
||||||
return `https://${window.location.host}`;
|
return "hyperspace://hyperspace/app/";
|
||||||
default:
|
case "dynamic":
|
||||||
return type;
|
return `https://${window.location.host}`;
|
||||||
}
|
default:
|
||||||
}
|
return type;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,23 +1,25 @@
|
||||||
import {getUserDefaultBool, setUserDefaultBool} from './settings';
|
import { getUserDefaultBool, setUserDefaultBool } from "./settings";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the person's permission to send notification requests.
|
* Get the person's permission to send notification requests.
|
||||||
*/
|
*/
|
||||||
export function getNotificationRequestPermission() {
|
export function getNotificationRequestPermission() {
|
||||||
if ('Notification' in window) {
|
if ("Notification" in window) {
|
||||||
Notification.requestPermission();
|
Notification.requestPermission();
|
||||||
let request = Notification.permission;
|
let request = Notification.permission;
|
||||||
if (request === "granted") {
|
if (request === "granted") {
|
||||||
setUserDefaultBool('enablePushNotifications', true);
|
setUserDefaultBool("enablePushNotifications", true);
|
||||||
setUserDefaultBool('userDeniedNotification', false);
|
setUserDefaultBool("userDeniedNotification", false);
|
||||||
} else {
|
|
||||||
setUserDefaultBool('enablePushNotifications', false);
|
|
||||||
setUserDefaultBool('userDeniedNotification', true);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
console.warn("Notifications aren't supported in this browser. The setting will be disabled.");
|
setUserDefaultBool("enablePushNotifications", false);
|
||||||
setUserDefaultBool('enablePushNotifications', false);
|
setUserDefaultBool("userDeniedNotification", true);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
console.warn(
|
||||||
|
"Notifications aren't supported in this browser. The setting will be disabled."
|
||||||
|
);
|
||||||
|
setUserDefaultBool("enablePushNotifications", false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +27,7 @@ export function getNotificationRequestPermission() {
|
||||||
* @returns Boolean value that determines whether the browser supports the Notification API
|
* @returns Boolean value that determines whether the browser supports the Notification API
|
||||||
*/
|
*/
|
||||||
export function browserSupportsNotificationRequests(): boolean {
|
export function browserSupportsNotificationRequests(): boolean {
|
||||||
return ('Notification' in window);
|
return "Notification" in window;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -33,7 +35,7 @@ export function browserSupportsNotificationRequests(): boolean {
|
||||||
* @returns Boolean value of `enablePushNotifications`
|
* @returns Boolean value of `enablePushNotifications`
|
||||||
*/
|
*/
|
||||||
export function canSendNotifications() {
|
export function canSendNotifications() {
|
||||||
return getUserDefaultBool('enablePushNotifications');
|
return getUserDefaultBool("enablePushNotifications");
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -42,15 +44,15 @@ export function canSendNotifications() {
|
||||||
* @param body The contents of the push notification
|
* @param body The contents of the push notification
|
||||||
*/
|
*/
|
||||||
export function sendNotificationRequest(title: string, body: string) {
|
export function sendNotificationRequest(title: string, body: string) {
|
||||||
if (canSendNotifications()) {
|
if (canSendNotifications()) {
|
||||||
let notif = new Notification(title, {
|
let notif = new Notification(title, {
|
||||||
body: body
|
body: body
|
||||||
});
|
});
|
||||||
|
|
||||||
notif.onclick = () => {
|
notif.onclick = () => {
|
||||||
window.focus();
|
window.focus();
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
console.warn('The person has opted to not receive push notifications.');
|
console.warn("The person has opted to not receive push notifications.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,18 +1,18 @@
|
||||||
import { defaultTheme, themes } from "../types/HyperspaceTheme";
|
import { defaultTheme, themes } from "../types/HyperspaceTheme";
|
||||||
import { getNotificationRequestPermission } from './notifications';
|
import { getNotificationRequestPermission } from "./notifications";
|
||||||
import axios from 'axios';
|
import axios from "axios";
|
||||||
import { Config } from "../types/Config";
|
import { Config } from "../types/Config";
|
||||||
import { Visibility } from "../types/Visibility";
|
import { Visibility } from "../types/Visibility";
|
||||||
|
|
||||||
type SettingsTemplate = {
|
type SettingsTemplate = {
|
||||||
[key:string]: any;
|
[key: string]: any;
|
||||||
darkModeEnabled: boolean;
|
darkModeEnabled: boolean;
|
||||||
systemDecidesDarkMode: boolean;
|
systemDecidesDarkMode: boolean;
|
||||||
enablePushNotifications: boolean;
|
enablePushNotifications: boolean;
|
||||||
clearNotificationsOnRead: boolean;
|
clearNotificationsOnRead: boolean;
|
||||||
displayAllOnNotificationBadge: boolean;
|
displayAllOnNotificationBadge: boolean;
|
||||||
defaultVisibility: string;
|
defaultVisibility: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the user default from localStorage
|
* Gets the user default from localStorage
|
||||||
|
@ -20,12 +20,14 @@ type SettingsTemplate = {
|
||||||
* @returns The boolean value associated with the key
|
* @returns The boolean value associated with the key
|
||||||
*/
|
*/
|
||||||
export function getUserDefaultBool(key: string): boolean {
|
export function getUserDefaultBool(key: string): boolean {
|
||||||
if (localStorage.getItem(key) === null) {
|
if (localStorage.getItem(key) === null) {
|
||||||
console.warn('This key has not been set before, so the default value is FALSE for now.');
|
console.warn(
|
||||||
return false;
|
"This key has not been set before, so the default value is FALSE for now."
|
||||||
} else {
|
);
|
||||||
return localStorage.getItem(key) === "true";
|
return false;
|
||||||
}
|
} else {
|
||||||
|
return localStorage.getItem(key) === "true";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,10 +36,10 @@ export function getUserDefaultBool(key: string): boolean {
|
||||||
* @param value The boolean value for the key
|
* @param value The boolean value for the key
|
||||||
*/
|
*/
|
||||||
export function setUserDefaultBool(key: string, value: boolean) {
|
export function setUserDefaultBool(key: string, value: boolean) {
|
||||||
if (localStorage.getItem(key) === null) {
|
if (localStorage.getItem(key) === null) {
|
||||||
console.warn('This key has not been set before.');
|
console.warn("This key has not been set before.");
|
||||||
}
|
}
|
||||||
localStorage.setItem(key, value.toString());
|
localStorage.setItem(key, value.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -45,12 +47,14 @@ export function setUserDefaultBool(key: string, value: boolean) {
|
||||||
* @returns The Visibility value associated with the key
|
* @returns The Visibility value associated with the key
|
||||||
*/
|
*/
|
||||||
export function getUserDefaultVisibility(): Visibility {
|
export function getUserDefaultVisibility(): Visibility {
|
||||||
if (localStorage.getItem("defaultVisibility") === null) {
|
if (localStorage.getItem("defaultVisibility") === null) {
|
||||||
console.warn('This key has not been set before, so the default value is PUBLIC for now.');
|
console.warn(
|
||||||
return "public";
|
"This key has not been set before, so the default value is PUBLIC for now."
|
||||||
} else {
|
);
|
||||||
return localStorage.getItem("defaultVisibility") as Visibility;
|
return "public";
|
||||||
}
|
} else {
|
||||||
|
return localStorage.getItem("defaultVisibility") as Visibility;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -58,23 +62,23 @@ export function getUserDefaultVisibility(): Visibility {
|
||||||
* @param key The settings key in localStorage to change
|
* @param key The settings key in localStorage to change
|
||||||
*/
|
*/
|
||||||
export function setUserDefaultVisibility(key: string) {
|
export function setUserDefaultVisibility(key: string) {
|
||||||
if (localStorage.getItem("defaultVisibility") === null) {
|
if (localStorage.getItem("defaultVisibility") === null) {
|
||||||
console.warn('This key has not been set before.');
|
console.warn("This key has not been set before.");
|
||||||
}
|
}
|
||||||
localStorage.setItem("defaultVisibility", key.toString());
|
localStorage.setItem("defaultVisibility", key.toString());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the user's default theme or the default theme
|
* Gets the user's default theme or the default theme
|
||||||
*/
|
*/
|
||||||
export function getUserDefaultTheme() {
|
export function getUserDefaultTheme() {
|
||||||
let returnTheme = defaultTheme;
|
let returnTheme = defaultTheme;
|
||||||
themes.forEach((theme) => {
|
themes.forEach(theme => {
|
||||||
if(theme.key === localStorage.getItem('theme')) {
|
if (theme.key === localStorage.getItem("theme")) {
|
||||||
returnTheme = theme;
|
returnTheme = theme;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return returnTheme;
|
return returnTheme;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,37 +86,42 @@ export function getUserDefaultTheme() {
|
||||||
* @param themeName The name of the theme
|
* @param themeName The name of the theme
|
||||||
*/
|
*/
|
||||||
export function setUserDefaultTheme(themeName: string) {
|
export function setUserDefaultTheme(themeName: string) {
|
||||||
localStorage.setItem('theme', themeName);
|
localStorage.setItem("theme", themeName);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates the user defaults if they do not exist already.
|
* Creates the user defaults if they do not exist already.
|
||||||
*/
|
*/
|
||||||
export function createUserDefaults() {
|
export function createUserDefaults() {
|
||||||
let defaults: SettingsTemplate = {
|
let defaults: SettingsTemplate = {
|
||||||
darkModeEnabled: false,
|
darkModeEnabled: false,
|
||||||
systemDecidesDarkMode: true,
|
systemDecidesDarkMode: true,
|
||||||
enablePushNotifications: true,
|
enablePushNotifications: true,
|
||||||
clearNotificationsOnRead: false,
|
clearNotificationsOnRead: false,
|
||||||
displayAllOnNotificationBadge: false,
|
displayAllOnNotificationBadge: false,
|
||||||
defaultVisibility: "public"
|
defaultVisibility: "public"
|
||||||
|
};
|
||||||
|
|
||||||
|
let settings = [
|
||||||
|
"darkModeEnabled",
|
||||||
|
"systemDecidesDarkMode",
|
||||||
|
"clearNotificationsOnRead",
|
||||||
|
"displayAllOnNotificationBadge",
|
||||||
|
"defaultVisibility"
|
||||||
|
];
|
||||||
|
|
||||||
|
migrateExistingSettings();
|
||||||
|
|
||||||
|
settings.forEach((setting: string) => {
|
||||||
|
if (localStorage.getItem(setting) === null) {
|
||||||
|
if (typeof defaults[setting] === "boolean") {
|
||||||
|
setUserDefaultBool(setting, defaults[setting]);
|
||||||
|
} else {
|
||||||
|
localStorage.setItem(setting, defaults[setting].toString());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
});
|
||||||
let settings = ["darkModeEnabled", "systemDecidesDarkMode", "clearNotificationsOnRead", "displayAllOnNotificationBadge", "defaultVisibility"];
|
getNotificationRequestPermission();
|
||||||
|
|
||||||
migrateExistingSettings();
|
|
||||||
|
|
||||||
settings.forEach((setting: string) => {
|
|
||||||
if (localStorage.getItem(setting) === null) {
|
|
||||||
if (typeof defaults[setting] === "boolean") {
|
|
||||||
setUserDefaultBool(setting, defaults[setting]);
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(setting, defaults[setting].toString());
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
getNotificationRequestPermission();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -120,18 +129,22 @@ export function createUserDefaults() {
|
||||||
* @returns The Promise data from getting the config.
|
* @returns The Promise data from getting the config.
|
||||||
*/
|
*/
|
||||||
export async function getConfig(): Promise<Config | undefined> {
|
export async function getConfig(): Promise<Config | undefined> {
|
||||||
try {
|
try {
|
||||||
const resp = await axios.get('config.json');
|
const resp = await axios.get("config.json");
|
||||||
let config: Config = resp.data;
|
let config: Config = resp.data;
|
||||||
return config;
|
return config;
|
||||||
}
|
} catch (err) {
|
||||||
catch (err) {
|
console.error(
|
||||||
console.error("Couldn't configure Hyperspace with the config file. Reason: " + err.name);
|
"Couldn't configure Hyperspace with the config file. Reason: " + err.name
|
||||||
}
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function migrateExistingSettings() {
|
export function migrateExistingSettings() {
|
||||||
if (localStorage.getItem('prefers-dark-mode')) {
|
if (localStorage.getItem("prefers-dark-mode")) {
|
||||||
setUserDefaultBool('darkModeEnabled', localStorage.getItem('prefers-dark-mode') === "true")
|
setUserDefaultBool(
|
||||||
}
|
"darkModeEnabled",
|
||||||
}
|
localStorage.getItem("prefers-dark-mode") === "true"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,7 +1,11 @@
|
||||||
import { createMuiTheme, Theme } from '@material-ui/core';
|
import { createMuiTheme, Theme } from "@material-ui/core";
|
||||||
import { HyperspaceTheme, themes, defaultTheme } from '../types/HyperspaceTheme';
|
import {
|
||||||
import { getUserDefaultBool } from './settings';
|
HyperspaceTheme,
|
||||||
import { isDarwinApp, isDarkMode } from './desktop';
|
themes,
|
||||||
|
defaultTheme
|
||||||
|
} from "../types/HyperspaceTheme";
|
||||||
|
import { getUserDefaultBool } from "./settings";
|
||||||
|
import { isDarwinApp, isDarkMode } from "./desktop";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Locates a Hyperspace theme from the themes catalog
|
* Locates a Hyperspace theme from the themes catalog
|
||||||
|
@ -9,13 +13,13 @@ import { isDarwinApp, isDarkMode } from './desktop';
|
||||||
* @returns Hyperspace theme with name or the default
|
* @returns Hyperspace theme with name or the default
|
||||||
*/
|
*/
|
||||||
export function getHyperspaceTheme(name: string): HyperspaceTheme {
|
export function getHyperspaceTheme(name: string): HyperspaceTheme {
|
||||||
let theme: HyperspaceTheme = defaultTheme;
|
let theme: HyperspaceTheme = defaultTheme;
|
||||||
themes.forEach((themeItem: HyperspaceTheme) => {
|
themes.forEach((themeItem: HyperspaceTheme) => {
|
||||||
if (themeItem.key === name) {
|
if (themeItem.key === name) {
|
||||||
theme = themeItem;
|
theme = themeItem;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -24,45 +28,48 @@ export function getHyperspaceTheme(name: string): HyperspaceTheme {
|
||||||
* @returns A Material-UI theme with the Hyperspace theme's palette colors
|
* @returns A Material-UI theme with the Hyperspace theme's palette colors
|
||||||
*/
|
*/
|
||||||
export function setHyperspaceTheme(theme: HyperspaceTheme): Theme {
|
export function setHyperspaceTheme(theme: HyperspaceTheme): Theme {
|
||||||
return createMuiTheme({
|
return createMuiTheme({
|
||||||
typography: {
|
typography: {
|
||||||
fontFamily: [
|
fontFamily: [
|
||||||
'-apple-system',
|
"-apple-system",
|
||||||
'BlinkMacSystemFont',
|
"BlinkMacSystemFont",
|
||||||
'"Segoe UI"',
|
'"Segoe UI"',
|
||||||
'Roboto',
|
"Roboto",
|
||||||
'"Helvetica Neue"',
|
'"Helvetica Neue"',
|
||||||
'Arial',
|
"Arial",
|
||||||
'sans-serif',
|
"sans-serif",
|
||||||
'"Apple Color Emoji"',
|
'"Apple Color Emoji"',
|
||||||
'"Segoe UI Emoji"',
|
'"Segoe UI Emoji"',
|
||||||
'"Segoe UI Symbol"',
|
'"Segoe UI Symbol"'
|
||||||
].join(','),
|
].join(","),
|
||||||
useNextVariants: true,
|
useNextVariants: true
|
||||||
},
|
},
|
||||||
palette: {
|
palette: {
|
||||||
primary: theme.palette.primary,
|
primary: theme.palette.primary,
|
||||||
secondary: theme.palette.secondary,
|
secondary: theme.palette.secondary,
|
||||||
type: getUserDefaultBool('darkModeEnabled')? "dark":
|
type: getUserDefaultBool("darkModeEnabled")
|
||||||
getDarkModeFromSystem() === "dark"? "dark": "light"
|
? "dark"
|
||||||
}
|
: getDarkModeFromSystem() === "dark"
|
||||||
})
|
? "dark"
|
||||||
|
: "light"
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getDarkModeFromSystem(): string {
|
export function getDarkModeFromSystem(): string {
|
||||||
if (getUserDefaultBool('systemDecidesDarkMode')) {
|
if (getUserDefaultBool("systemDecidesDarkMode")) {
|
||||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||||
return "dark";
|
return "dark";
|
||||||
} else {
|
|
||||||
if (isDarwinApp()) {
|
|
||||||
return isDarkMode()? "dark": "light";
|
|
||||||
} else {
|
|
||||||
return "light";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
|
if (isDarwinApp()) {
|
||||||
|
return isDarkMode() ? "dark" : "light";
|
||||||
|
} else {
|
||||||
return "light";
|
return "light";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "light";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,10 +78,10 @@ export function getDarkModeFromSystem(): string {
|
||||||
* @param setting Whether dark mode should be on (`true`) or off (`false`)
|
* @param setting Whether dark mode should be on (`true`) or off (`false`)
|
||||||
*/
|
*/
|
||||||
export function darkMode(theme: Theme, setting: boolean): Theme {
|
export function darkMode(theme: Theme, setting: boolean): Theme {
|
||||||
if (setting) {
|
if (setting) {
|
||||||
theme.palette.type = 'dark';
|
theme.palette.type = "dark";
|
||||||
} else {
|
} else {
|
||||||
theme.palette.type = 'light';
|
theme.palette.type = "light";
|
||||||
}
|
}
|
||||||
return theme;
|
return theme;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue