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",
|
||||
"version": "1.0.0-beta6",
|
||||
"version": "1.0.0-beta7",
|
||||
"lockfileVersion": 1,
|
||||
"requires": true,
|
||||
"dependencies": {
|
||||
|
@ -17778,6 +17778,12 @@
|
|||
"integrity": "sha1-gV7R9uvGWSb4ZbMQwHE7yzMVzks=",
|
||||
"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": {
|
||||
"version": "4.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-4.0.2.tgz",
|
||||
|
|
|
@ -28,6 +28,7 @@
|
|||
"megalodon": "^0.6.4",
|
||||
"moment": "^2.24.0",
|
||||
"notistack": "^0.5.1",
|
||||
"prettier": "^1.18.2",
|
||||
"query-string": "^6.8.2",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
|
|
|
@ -1,21 +1,24 @@
|
|||
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: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
height: '100%',
|
||||
minHeight: '100vh',
|
||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.default,
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
height: "100%",
|
||||
minHeight: "100vh",
|
||||
backgroundColor: isDarwinApp()
|
||||
? "transparent"
|
||||
: theme.palette.background.default
|
||||
},
|
||||
content: {
|
||||
marginTop: 72,
|
||||
flexGrow: 1,
|
||||
padding: theme.spacing.unit * 3,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
},
|
||||
},
|
||||
marginTop: 88
|
||||
}
|
||||
}
|
||||
});
|
120
src/App.tsx
120
src/App.tsx
|
@ -1,33 +1,33 @@
|
|||
import React, { Component } from 'react';
|
||||
import {MuiThemeProvider, CssBaseline, withStyles } from '@material-ui/core';
|
||||
import { setHyperspaceTheme, darkMode } from './utilities/themes';
|
||||
import AppLayout from './components/AppLayout';
|
||||
import {styles} from './App.styles';
|
||||
import {Route} from 'react-router-dom';
|
||||
import AboutPage from './pages/About';
|
||||
import Settings from './pages/Settings';
|
||||
import { getUserDefaultBool, getUserDefaultTheme } from './utilities/settings';
|
||||
import ProfilePage from './pages/ProfilePage';
|
||||
import HomePage from './pages/Home';
|
||||
import LocalPage from './pages/Local';
|
||||
import PublicPage from './pages/Public';
|
||||
import Conversation from './pages/Conversation';
|
||||
import NotificationsPage from './pages/Notifications';
|
||||
import SearchPage from './pages/Search';
|
||||
import Composer from './pages/Compose';
|
||||
import WelcomePage from './pages/Welcome';
|
||||
import MessagesPage from './pages/Messages';
|
||||
import RecommendationsPage from './pages/Recommendations';
|
||||
import Missingno from './pages/Missingno';
|
||||
import You from './pages/You';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import {PrivateRoute} from './interfaces/overrides';
|
||||
import { userLoggedIn } from './utilities/accounts';
|
||||
import { isDarwinApp } from './utilities/desktop';
|
||||
import React, { Component } from "react";
|
||||
import { MuiThemeProvider, CssBaseline, withStyles } from "@material-ui/core";
|
||||
import { setHyperspaceTheme, darkMode } from "./utilities/themes";
|
||||
import AppLayout from "./components/AppLayout";
|
||||
import { styles } from "./App.styles";
|
||||
import { Route } from "react-router-dom";
|
||||
import AboutPage from "./pages/About";
|
||||
import Settings from "./pages/Settings";
|
||||
import { getUserDefaultBool, getUserDefaultTheme } from "./utilities/settings";
|
||||
import ProfilePage from "./pages/ProfilePage";
|
||||
import HomePage from "./pages/Home";
|
||||
import LocalPage from "./pages/Local";
|
||||
import PublicPage from "./pages/Public";
|
||||
import Conversation from "./pages/Conversation";
|
||||
import NotificationsPage from "./pages/Notifications";
|
||||
import SearchPage from "./pages/Search";
|
||||
import Composer from "./pages/Compose";
|
||||
import WelcomePage from "./pages/Welcome";
|
||||
import MessagesPage from "./pages/Messages";
|
||||
import RecommendationsPage from "./pages/Recommendations";
|
||||
import Missingno from "./pages/Missingno";
|
||||
import You from "./pages/You";
|
||||
import Blocked from "./pages/Blocked";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { PrivateRoute } from "./interfaces/overrides";
|
||||
import { userLoggedIn } from "./utilities/accounts";
|
||||
import { isDarwinApp } from "./utilities/desktop";
|
||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||
|
||||
class App extends Component<any, any> {
|
||||
|
||||
offline: any;
|
||||
|
||||
constructor(props: any) {
|
||||
|
@ -35,57 +35,73 @@ class App extends Component<any, any> {
|
|||
|
||||
this.state = {
|
||||
theme: theme
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
let newTheme = darkMode(this.state.theme, getUserDefaultBool('darkModeEnabled'));
|
||||
let newTheme = darkMode(
|
||||
this.state.theme,
|
||||
getUserDefaultBool("darkModeEnabled")
|
||||
);
|
||||
this.setState({ theme: newTheme });
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.removeBodyBackground()
|
||||
this.removeBodyBackground();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
this.removeBodyBackground()
|
||||
this.removeBodyBackground();
|
||||
}
|
||||
|
||||
removeBodyBackground() {
|
||||
if (isDarwinApp()) {
|
||||
document.body.style.backgroundColor = "transparent";
|
||||
console.log("Changed!")
|
||||
console.log(`New color: ${document.body.style.backgroundColor}`)
|
||||
console.log("Changed!");
|
||||
console.log(`New color: ${document.body.style.backgroundColor}`);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
this.removeBodyBackground()
|
||||
this.removeBodyBackground();
|
||||
|
||||
return (
|
||||
<MuiThemeProvider theme={this.state.theme}>
|
||||
<CssBaseline/>
|
||||
<Route path="/welcome" component={WelcomePage}/>
|
||||
<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>
|
||||
{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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,32 +1,35 @@
|
|||
import { Theme, createStyles } from "@material-ui/core";
|
||||
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";
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
export const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
width: "100%",
|
||||
display: "flex"
|
||||
},
|
||||
stickyArea: {
|
||||
position: 'fixed',
|
||||
width: '100%',
|
||||
position: "fixed",
|
||||
width: "100%",
|
||||
top: 0,
|
||||
left: 0,
|
||||
zIndex: 1000,
|
||||
zIndex: 1000
|
||||
},
|
||||
titleBarRoot: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 24,
|
||||
width: '100%',
|
||||
backgroundColor: isDarwinApp()? theme.palette.primary.main: theme.palette.primary.dark,
|
||||
textAlign: 'center',
|
||||
width: "100%",
|
||||
backgroundColor: isDarwinApp()
|
||||
? theme.palette.primary.main
|
||||
: theme.palette.primary.dark,
|
||||
textAlign: "center",
|
||||
zIndex: 1000,
|
||||
verticalAlign: 'middle',
|
||||
WebkitUserSelect: 'none',
|
||||
WebkitAppRegion: "drag",
|
||||
verticalAlign: "middle",
|
||||
WebkitUserSelect: "none",
|
||||
WebkitAppRegion: "drag"
|
||||
},
|
||||
titleBarText: {
|
||||
color: theme.palette.common.white,
|
||||
|
@ -36,83 +39,83 @@ export const styles = (theme: Theme) => createStyles({
|
|||
},
|
||||
appBar: {
|
||||
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,
|
||||
borderBottomColor: darken(theme.palette.primary.dark, 0.2),
|
||||
borderBottomWidth: 1,
|
||||
borderBottomStyle: isDarwinApp()? "solid": "none",
|
||||
boxShadow: isDarwinApp()? "none": "inherit"
|
||||
borderBottomStyle: isDarwinApp() ? "solid" : "none",
|
||||
boxShadow: isDarwinApp() ? "none" : "inherit"
|
||||
},
|
||||
appBarMenuButton: {
|
||||
marginLeft: -12,
|
||||
marginRight: 20,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none'
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
appBarTitle: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'block',
|
||||
display: "none",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "block"
|
||||
}
|
||||
},
|
||||
appBarSearch: {
|
||||
position: 'relative',
|
||||
position: "relative",
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
backgroundColor: fade(theme.palette.common.white, 0.15),
|
||||
'&:hover': {
|
||||
"&:hover": {
|
||||
backgroundColor: fade(theme.palette.common.white, 0.25)
|
||||
},
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
marginLeft: 0,
|
||||
marginRight: theme.spacing.unit,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: theme.spacing.unit * 6,
|
||||
width: '50%'
|
||||
width: "50%"
|
||||
}
|
||||
},
|
||||
appBarSearchIcon: {
|
||||
width: theme.spacing.unit * 9,
|
||||
height: '100%',
|
||||
position: 'absolute',
|
||||
pointerEvents: 'none',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center'
|
||||
height: "100%",
|
||||
position: "absolute",
|
||||
pointerEvents: "none",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center"
|
||||
},
|
||||
appBarSearchInputRoot: {
|
||||
color: 'inherit',
|
||||
width: '100%'
|
||||
color: "inherit",
|
||||
width: "100%"
|
||||
},
|
||||
appBarSearchInputInput: {
|
||||
paddingTop: theme.spacing.unit,
|
||||
paddingBottom: theme.spacing.unit,
|
||||
paddingLeft: theme.spacing.unit * 10,
|
||||
paddingRight: theme.spacing.unit,
|
||||
transition: theme.transitions.create('width'),
|
||||
width: '100%',
|
||||
transition: theme.transitions.create("width"),
|
||||
width: "100%"
|
||||
},
|
||||
appBarFlexGrow: {
|
||||
flexGrow: 1
|
||||
},
|
||||
appBarActionButtons: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'flex',
|
||||
},
|
||||
display: "none",
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "flex"
|
||||
}
|
||||
},
|
||||
appBarAcctMenuIcon: {
|
||||
backgroundColor: theme.palette.primary.dark
|
||||
},
|
||||
acctMenu: {
|
||||
|
||||
},
|
||||
acctMenu: {},
|
||||
drawer: {
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
width: 250,
|
||||
flexShrink: 0
|
||||
},
|
||||
zIndex: 1,
|
||||
zIndex: 1
|
||||
},
|
||||
drawerPaper: {
|
||||
width: 250,
|
||||
|
@ -122,43 +125,47 @@ export const styles = (theme: Theme) => createStyles({
|
|||
width: 250,
|
||||
zIndex: -1,
|
||||
marginTop: 64,
|
||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.paper
|
||||
backgroundColor: isDarwinApp()
|
||||
? "transparent"
|
||||
: theme.palette.background.paper
|
||||
},
|
||||
drawerPaperWithTitleAndAppBar: {
|
||||
width: 250,
|
||||
zIndex: -1,
|
||||
marginTop: 88,
|
||||
backgroundColor: isDarwinApp()? "transparent": theme.palette.background.paper
|
||||
backgroundColor: isDarwinApp()
|
||||
? "transparent"
|
||||
: theme.palette.background.paper
|
||||
},
|
||||
drawerDisplayMobile: {
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none'
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
toolbar: theme.mixins.toolbar,
|
||||
sectionDesktop: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'flex',
|
||||
},
|
||||
display: "none",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "flex"
|
||||
}
|
||||
},
|
||||
sectionMobile: {
|
||||
display: 'flex',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
display: 'none',
|
||||
},
|
||||
display: "flex",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
content: {
|
||||
padding: theme.spacing.unit * 3,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: 250
|
||||
},
|
||||
overflowY: 'auto',
|
||||
overflowY: "auto"
|
||||
},
|
||||
composeButton: {
|
||||
position: "fixed",
|
||||
bottom: theme.spacing.unit * 2,
|
||||
right: theme.spacing.unit * 2,
|
||||
zIndex: 50
|
||||
},
|
||||
}
|
||||
});
|
|
@ -1,28 +1,60 @@
|
|||
import React, { Component } from 'react';
|
||||
import { Typography, AppBar, Toolbar, IconButton, InputBase, Avatar, ListItemText, Divider, List, ListItemIcon, Hidden, Drawer, ListSubheader, ListItemAvatar, withStyles, Menu, MenuItem, ClickAwayListener, Badge, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions, Button, ListItem, Tooltip } from '@material-ui/core';
|
||||
import MenuIcon from '@material-ui/icons/Menu';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||
import MailIcon from '@material-ui/icons/Mail';
|
||||
import HomeIcon from '@material-ui/icons/Home';
|
||||
import DomainIcon from '@material-ui/icons/Domain';
|
||||
import PublicIcon from '@material-ui/icons/Public';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import SettingsIcon from '@material-ui/icons/Settings';
|
||||
import InfoIcon from '@material-ui/icons/Info';
|
||||
import CreateIcon from '@material-ui/icons/Create';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Typography,
|
||||
AppBar,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
InputBase,
|
||||
Avatar,
|
||||
ListItemText,
|
||||
Divider,
|
||||
List,
|
||||
ListItemIcon,
|
||||
Hidden,
|
||||
Drawer,
|
||||
ListSubheader,
|
||||
ListItemAvatar,
|
||||
withStyles,
|
||||
Menu,
|
||||
MenuItem,
|
||||
ClickAwayListener,
|
||||
Badge,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions,
|
||||
Button,
|
||||
ListItem,
|
||||
Tooltip
|
||||
} from "@material-ui/core";
|
||||
import MenuIcon from "@material-ui/icons/Menu";
|
||||
import SearchIcon from "@material-ui/icons/Search";
|
||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||
import MailIcon from "@material-ui/icons/Mail";
|
||||
import HomeIcon from "@material-ui/icons/Home";
|
||||
import DomainIcon from "@material-ui/icons/Domain";
|
||||
import PublicIcon from "@material-ui/icons/Public";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
import SettingsIcon from "@material-ui/icons/Settings";
|
||||
import InfoIcon from "@material-ui/icons/Info";
|
||||
import CreateIcon from "@material-ui/icons/Create";
|
||||
//import SupervisedUserCircleIcon from '@material-ui/icons/SupervisedUserCircle';
|
||||
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
|
||||
import {styles} from './AppLayout.styles';
|
||||
import { UAccount } from '../../types/Account';
|
||||
import {LinkableListItem, LinkableIconButton, LinkableFab} from '../../interfaces/overrides';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Notification } from '../../types/Notification';
|
||||
import {sendNotificationRequest} from '../../utilities/notifications';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { getConfig, getUserDefaultBool } from '../../utilities/settings';
|
||||
import { isDesktopApp, isDarwinApp } from '../../utilities/desktop';
|
||||
import { Config } from '../../types/Config';
|
||||
import ExitToAppIcon from "@material-ui/icons/ExitToApp";
|
||||
import { styles } from "./AppLayout.styles";
|
||||
import { UAccount } from "../../types/Account";
|
||||
import {
|
||||
LinkableListItem,
|
||||
LinkableIconButton,
|
||||
LinkableFab
|
||||
} from "../../interfaces/overrides";
|
||||
import Mastodon from "megalodon";
|
||||
import { Notification } from "../../types/Notification";
|
||||
import { sendNotificationRequest } from "../../utilities/notifications";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { getConfig, getUserDefaultBool } from "../../utilities/settings";
|
||||
import { isDesktopApp, isDarwinApp } from "../../utilities/desktop";
|
||||
import { Config } from "../../types/Config";
|
||||
|
||||
interface IAppLayoutState {
|
||||
acctMenuOpen: boolean;
|
||||
|
@ -36,21 +68,23 @@ interface IAppLayoutState {
|
|||
}
|
||||
|
||||
export class AppLayout extends Component<any, IAppLayoutState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
constructor(props: any) {
|
||||
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,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
this.state = {
|
||||
drawerOpenOnMobile: false,
|
||||
acctMenuOpen: false,
|
||||
notificationCount: 0,
|
||||
logOutOpen: false
|
||||
}
|
||||
};
|
||||
|
||||
this.toggleDrawerOnMobile = this.toggleDrawerOnMobile.bind(this);
|
||||
this.toggleAcctMenu = this.toggleAcctMenu.bind(this);
|
||||
|
@ -58,18 +92,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
||||
let acct = localStorage.getItem('account');
|
||||
let acct = localStorage.getItem("account");
|
||||
if (acct) {
|
||||
this.setState({ currentUser: JSON.parse(acct) });
|
||||
} else {
|
||||
this.client.get('/accounts/verify_credentials').then((resp: any) => {
|
||||
this.client
|
||||
.get("/accounts/verify_credentials")
|
||||
.then((resp: any) => {
|
||||
let data: UAccount = resp.data;
|
||||
this.setState({ currentUser: data });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't find profile info: " + err.name);
|
||||
console.error(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
getConfig().then((result: any) => {
|
||||
|
@ -77,59 +113,69 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
let config: Config = result;
|
||||
this.setState({
|
||||
enableFederation: config.federation.enablePublicTimeline,
|
||||
brandName: config.branding? config.branding.name: "Hyperspace",
|
||||
brandName: config.branding ? config.branding.name : "Hyperspace",
|
||||
developerMode: config.developer
|
||||
});
|
||||
}
|
||||
})
|
||||
|
||||
this.streamNotifications()
|
||||
});
|
||||
|
||||
this.streamNotifications();
|
||||
}
|
||||
|
||||
streamNotifications() {
|
||||
this.streamListener = this.client.stream('/streaming/user');
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
if (getUserDefaultBool('displayAllOnNotificationBadge')) {
|
||||
this.client.get('/notifications').then((resp: any) => {
|
||||
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) => {
|
||||
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) {
|
||||
switch (notif.type) {
|
||||
case "favourite":
|
||||
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " favorited your post.";
|
||||
primaryMessage =
|
||||
(notif.account.display_name || "@" + notif.account.username) +
|
||||
" favorited your post.";
|
||||
if (notif.status) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = notif.status.content;
|
||||
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
|
||||
secondaryMessage =
|
||||
(div.textContent || div.innerText || "").slice(0, 100) + "...";
|
||||
}
|
||||
break;
|
||||
case "follow":
|
||||
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " is now following you.";
|
||||
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.";
|
||||
primaryMessage =
|
||||
(notif.account.display_name || "@" + notif.account.username) +
|
||||
" mentioned you in a post.";
|
||||
if (notif.status) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = notif.status.content;
|
||||
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
|
||||
secondaryMessage =
|
||||
(div.textContent || div.innerText || "").slice(0, 100) + "...";
|
||||
}
|
||||
break;
|
||||
case "reblog":
|
||||
primaryMessage = (notif.account.display_name || "@" + notif.account.username) + " reblogged your post.";
|
||||
primaryMessage =
|
||||
(notif.account.display_name || "@" + notif.account.username) +
|
||||
" reblogged your post.";
|
||||
if (notif.status) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = notif.status.content;
|
||||
secondaryMessage = (div.textContent || div.innerText || "").slice(0, 100) + "..."
|
||||
secondaryMessage =
|
||||
(div.textContent || div.innerText || "").slice(0, 100) + "...";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
@ -146,7 +192,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
toggleDrawerOnMobile() {
|
||||
this.setState({
|
||||
drawerOpenOnMobile: !this.state.drawerOpenOnMobile
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleLogOutDialog() {
|
||||
|
@ -154,7 +200,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
}
|
||||
|
||||
searchForQuery(what: string) {
|
||||
window.location.href = isDesktopApp()? "hyperspace://hyperspace/app/index.html#/search?query=" + what: "/#/search?query=" + what;
|
||||
window.location.href = isDesktopApp()
|
||||
? "hyperspace://hyperspace/app/index.html#/search?query=" + what
|
||||
: "/#/search?query=" + what;
|
||||
window.location.reload;
|
||||
}
|
||||
|
||||
|
@ -162,15 +210,15 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
let loginData = localStorage.getItem("login");
|
||||
if (loginData) {
|
||||
let items = ["login", "account", "baseurl", "access_token"];
|
||||
items.forEach((entry) => {
|
||||
items.forEach(entry => {
|
||||
localStorage.removeItem(entry);
|
||||
})
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
}
|
||||
|
||||
clearBadge() {
|
||||
if (!getUserDefaultBool('displayAllOnNotificationBadge')) {
|
||||
if (!getUserDefaultBool("displayAllOnNotificationBadge")) {
|
||||
this.setState({ notificationCount: 0 });
|
||||
}
|
||||
}
|
||||
|
@ -180,13 +228,21 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
if (isDarwinApp()) {
|
||||
return (
|
||||
<div className={classes.titleBarRoot}>
|
||||
<Typography className={classes.titleBarText}>{this.state.brandName? this.state.brandName: "Hyperspace"} {this.state.developerMode? "(beta)": null}</Typography>
|
||||
<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") {
|
||||
} 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>
|
||||
<Typography className={classes.titleBarText}>
|
||||
Careful: you're running in developer mode.
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -198,71 +254,129 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
<div>
|
||||
<List>
|
||||
<div className={classes.drawerDisplayMobile}>
|
||||
<LinkableListItem button key="profile-mobile" to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
|
||||
<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: ""}/>
|
||||
<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..."}/>
|
||||
<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>
|
||||
<ListItemText primary="Switch account"/>
|
||||
</LinkableListItem> */}
|
||||
<ListItem button key="acctLogout-mobile" onClick={() => this.toggleLogOutDialog()}>
|
||||
<ListItemIcon><ExitToAppIcon/></ListItemIcon>
|
||||
<ListItemText primary="Log out"/>
|
||||
<ListItem
|
||||
button
|
||||
key="acctLogout-mobile"
|
||||
onClick={() => this.toggleLogOutDialog()}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<ExitToAppIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Log out" />
|
||||
</ListItem>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
</div>
|
||||
<ListSubheader>Timelines</ListSubheader>
|
||||
<LinkableListItem button key="home" to="/home">
|
||||
<ListItemIcon><HomeIcon/></ListItemIcon>
|
||||
<ListItemText primary="Home"/>
|
||||
<ListItemIcon>
|
||||
<HomeIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Home" />
|
||||
</LinkableListItem>
|
||||
<LinkableListItem button key="local" to="/local">
|
||||
<ListItemIcon><DomainIcon/></ListItemIcon>
|
||||
<ListItemText primary="Local"/>
|
||||
<ListItemIcon>
|
||||
<DomainIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Local" />
|
||||
</LinkableListItem>
|
||||
{
|
||||
this.state.enableFederation?
|
||||
{this.state.enableFederation ? (
|
||||
<LinkableListItem button key="public" to="/public">
|
||||
<ListItemIcon><PublicIcon/></ListItemIcon>
|
||||
<ListItemText primary="Public"/>
|
||||
</LinkableListItem>:
|
||||
<ListItemIcon>
|
||||
<PublicIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Public" />
|
||||
</LinkableListItem>
|
||||
) : (
|
||||
<ListItem disabled>
|
||||
<ListItemIcon><PublicIcon/></ListItemIcon>
|
||||
<ListItemText primary="Public" secondary="Disabled by admin"/>
|
||||
<ListItemIcon>
|
||||
<PublicIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Public" secondary="Disabled by admin" />
|
||||
</ListItem>
|
||||
}
|
||||
<Divider/>
|
||||
)}
|
||||
<Divider />
|
||||
<div className={classes.drawerDisplayMobile}>
|
||||
<ListSubheader>Account</ListSubheader>
|
||||
<LinkableListItem button key="notifications-mobile" to="/notifications">
|
||||
<LinkableListItem
|
||||
button
|
||||
key="notifications-mobile"
|
||||
to="/notifications"
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Badge badgeContent={this.state.notificationCount > 0? this.state.notificationCount: ""} color="secondary">
|
||||
<Badge
|
||||
badgeContent={
|
||||
this.state.notificationCount > 0
|
||||
? this.state.notificationCount
|
||||
: ""
|
||||
}
|
||||
color="secondary"
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</Badge>
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Notifications"/>
|
||||
<ListItemText primary="Notifications" />
|
||||
</LinkableListItem>
|
||||
<LinkableListItem button key="messages-mobile" to="/messages">
|
||||
<ListItemIcon><MailIcon/></ListItemIcon>
|
||||
<ListItemText primary="Messages"/>
|
||||
<ListItemIcon>
|
||||
<MailIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Messages" />
|
||||
</LinkableListItem>
|
||||
<Divider/>
|
||||
<Divider />
|
||||
</div>
|
||||
<ListSubheader>More</ListSubheader>
|
||||
<LinkableListItem button key="recommended" to="/recommended">
|
||||
<ListItemIcon><GroupIcon/></ListItemIcon>
|
||||
<ListItemText primary="Who to follow"/>
|
||||
<ListItemIcon>
|
||||
<GroupIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Who to follow" />
|
||||
</LinkableListItem>
|
||||
<LinkableListItem button key="settings" to="/settings">
|
||||
<ListItemIcon><SettingsIcon/></ListItemIcon>
|
||||
<ListItemText primary="Settings"/>
|
||||
<ListItemIcon>
|
||||
<SettingsIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="Settings" />
|
||||
</LinkableListItem>
|
||||
<LinkableListItem button key="info" to="/about">
|
||||
<ListItemIcon><InfoIcon/></ListItemIcon>
|
||||
<ListItemText primary="About"/>
|
||||
<ListItemIcon>
|
||||
<InfoIcon />
|
||||
</ListItemIcon>
|
||||
<ListItemText primary="About" />
|
||||
</LinkableListItem>
|
||||
</List>
|
||||
</div>
|
||||
|
@ -283,15 +397,20 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
aria-label="Open drawer"
|
||||
onClick={this.toggleDrawerOnMobile}
|
||||
>
|
||||
<MenuIcon/>
|
||||
<MenuIcon />
|
||||
</IconButton>
|
||||
<Typography className={classes.appBarTitle} variant="h6" color="inherit" noWrap>
|
||||
{this.state.brandName? this.state.brandName: "Hyperspace"}
|
||||
<Typography
|
||||
className={classes.appBarTitle}
|
||||
variant="h6"
|
||||
color="inherit"
|
||||
noWrap
|
||||
>
|
||||
{this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||
</Typography>
|
||||
<div className={classes.appBarFlexGrow}/>
|
||||
<div className={classes.appBarFlexGrow} />
|
||||
<div className={classes.appBarSearch}>
|
||||
<div className={classes.appBarSearchIcon}>
|
||||
<SearchIcon/>
|
||||
<SearchIcon />
|
||||
</div>
|
||||
<InputBase
|
||||
placeholder="Search..."
|
||||
|
@ -299,30 +418,49 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
root: classes.appBarSearchInputRoot,
|
||||
input: classes.appBarSearchInputInput
|
||||
}}
|
||||
onKeyUp={(event) => {
|
||||
onKeyUp={event => {
|
||||
if (event.keyCode === 13) {
|
||||
this.searchForQuery(event.currentTarget.value);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className={classes.appBarFlexGrow}/>
|
||||
<div className={classes.appBarFlexGrow} />
|
||||
<div className={classes.appBarActionButtons}>
|
||||
<Tooltip title="Notifications">
|
||||
<LinkableIconButton color="inherit" to="/notifications" onClick={this.clearBadge}>
|
||||
<Badge badgeContent={this.state.notificationCount > 0? this.state.notificationCount: ""} color="secondary">
|
||||
<LinkableIconButton
|
||||
color="inherit"
|
||||
to="/notifications"
|
||||
onClick={this.clearBadge}
|
||||
>
|
||||
<Badge
|
||||
badgeContent={
|
||||
this.state.notificationCount > 0
|
||||
? this.state.notificationCount
|
||||
: ""
|
||||
}
|
||||
color="secondary"
|
||||
>
|
||||
<NotificationsIcon />
|
||||
</Badge>
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Direct messages">
|
||||
<LinkableIconButton color="inherit" to="/messages">
|
||||
<MailIcon/>
|
||||
<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: ""}/>
|
||||
<Avatar
|
||||
className={classes.appBarAcctMenuIcon}
|
||||
alt="You"
|
||||
src={
|
||||
this.state.currentUser
|
||||
? this.state.currentUser.avatar_static
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
||||
|
@ -334,18 +472,43 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
>
|
||||
<ClickAwayListener onClickAway={this.toggleAcctMenu}>
|
||||
<div>
|
||||
<LinkableListItem to={`/profile/${this.state.currentUser? this.state.currentUser.id: "1"}`}>
|
||||
<LinkableListItem
|
||||
to={`/profile/${
|
||||
this.state.currentUser
|
||||
? this.state.currentUser.id
|
||||
: "1"
|
||||
}`}
|
||||
>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt="You" src={this.state.currentUser? this.state.currentUser.avatar_static: ""}/>
|
||||
<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...")}
|
||||
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/>
|
||||
<Divider />
|
||||
{/* <MenuItem>Switch account</MenuItem> */}
|
||||
<MenuItem onClick={() => this.toggleLogOutDialog()}>Log out</MenuItem>
|
||||
<MenuItem onClick={() => this.toggleLogOutDialog()}>
|
||||
Log out
|
||||
</MenuItem>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
</Menu>
|
||||
|
@ -357,7 +520,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
<Drawer
|
||||
container={this.props.container}
|
||||
variant="temporary"
|
||||
anchor={'left'}
|
||||
anchor={"left"}
|
||||
open={this.state.drawerOpenOnMobile}
|
||||
onClose={this.toggleDrawerOnMobile}
|
||||
classes={{ paper: classes.drawerPaper }}
|
||||
|
@ -368,7 +531,9 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
<Hidden smDown implementation="css">
|
||||
<Drawer
|
||||
classes={{
|
||||
paper: this.titlebar()? classes.drawerPaperWithTitleAndAppBar: classes.drawerPaperWithAppBar
|
||||
paper: this.titlebar()
|
||||
? classes.drawerPaperWithTitleAndAppBar
|
||||
: classes.drawerPaperWithAppBar
|
||||
}}
|
||||
variant="permanent"
|
||||
open
|
||||
|
@ -382,26 +547,44 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
open={this.state.logOutOpen}
|
||||
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>
|
||||
<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{" "}
|
||||
{this.state.brandName ? this.state.brandName : "Hyperspace"} from
|
||||
your list of authorized apps and log in again if you want to use{" "}
|
||||
{this.state.brandName ? this.state.brandName : "Hyperspace"}.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.toggleLogOutDialog()} color="primary" autoFocus>
|
||||
<Button
|
||||
onClick={() => this.toggleLogOutDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.logOutAndRestart();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
<Tooltip title="Create a new post">
|
||||
<LinkableFab to="/compose" className={classes.composeButton} color="secondary" aria-label="Compose">
|
||||
<CreateIcon/>
|
||||
<LinkableFab
|
||||
to="/compose"
|
||||
className={classes.composeButton}
|
||||
color="secondary"
|
||||
aria-label="Compose"
|
||||
>
|
||||
<CreateIcon />
|
||||
</LinkableFab>
|
||||
</Tooltip>
|
||||
</div>
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AppLayout } from './AppLayout';
|
||||
import { withStyles } from '@material-ui/core';
|
||||
import { styles } from './AppLayout.styles';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { AppLayout } from "./AppLayout";
|
||||
import { withStyles } from "@material-ui/core";
|
||||
import { styles } from "./AppLayout.styles";
|
||||
import { withSnackbar } from "notistack";
|
||||
|
||||
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: {
|
||||
padding: theme.spacing.unit * 2,
|
||||
padding: theme.spacing.unit * 2
|
||||
},
|
||||
mediaObject: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
width: "100%",
|
||||
height: "100%"
|
||||
},
|
||||
mediaSlide: {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
width: '100%',
|
||||
height: 'auto'
|
||||
width: "100%",
|
||||
height: "auto"
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,8 +1,13 @@
|
|||
import React, { Component } from 'react';
|
||||
import {withStyles, Typography, MobileStepper, Button} from '@material-ui/core';
|
||||
import { styles } from './Attachment.styles';
|
||||
import { Attachment } from '../../types/Attachment';
|
||||
import SwipeableViews from 'react-swipeable-views';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
Typography,
|
||||
MobileStepper,
|
||||
Button
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./Attachment.styles";
|
||||
import { Attachment } from "../../types/Attachment";
|
||||
import SwipeableViews from "react-swipeable-views";
|
||||
|
||||
interface IAttachmentProps {
|
||||
media: [Attachment];
|
||||
|
@ -15,7 +20,10 @@ interface IAttachmentState {
|
|||
attachments: [Attachment];
|
||||
}
|
||||
|
||||
class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState> {
|
||||
class AttachmentComponent extends Component<
|
||||
IAttachmentProps,
|
||||
IAttachmentState
|
||||
> {
|
||||
constructor(props: IAttachmentProps) {
|
||||
super(props);
|
||||
|
||||
|
@ -23,7 +31,7 @@ class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState>
|
|||
attachments: this.props.media,
|
||||
totalSteps: this.props.media.length,
|
||||
currentStep: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
moveBack() {
|
||||
|
@ -40,45 +48,61 @@ class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState>
|
|||
nextStep = this.state.totalSteps;
|
||||
}
|
||||
this.setState({ currentStep: nextStep });
|
||||
|
||||
}
|
||||
|
||||
handleStepChange(currentStep: number) {
|
||||
this.setState({
|
||||
currentStep
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
getSlide(slide: Attachment) {
|
||||
const {classes} = this.props;
|
||||
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}/>
|
||||
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 { 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}>
|
||||
<SwipeableViews index={this.state.currentStep}>
|
||||
{this.state.attachments.map((slide: Attachment) => {
|
||||
return (
|
||||
<div key={slide.id} className={classes.mediaSlide}>
|
||||
{this.getSlide(slide)}
|
||||
</div>);
|
||||
})
|
||||
}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</SwipeableViews>
|
||||
<MobileStepper
|
||||
steps={this.state.totalSteps}
|
||||
|
@ -86,19 +110,31 @@ class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState>
|
|||
activeStep={this.state.currentStep}
|
||||
className={classes.mobileStepper}
|
||||
nextButton={
|
||||
<Button size="small" onClick={() => this.moveForward()} disabled={this.state.currentStep === this.state.totalSteps - 1}>
|
||||
<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}>
|
||||
<Button
|
||||
size="small"
|
||||
onClick={() => this.moveBack()}
|
||||
disabled={this.state.currentStep === 0}
|
||||
>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Typography variant="caption">{mediaItem.description? mediaItem.description: "No description provided."}</Typography>
|
||||
<Typography variant="caption">
|
||||
{mediaItem.description
|
||||
? mediaItem.description
|
||||
: "No description provided."}
|
||||
</Typography>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {withStyles} from '@material-ui/core';
|
||||
import AttachmentComponent from './Attachment';
|
||||
import {styles} from './Attachment.styles';
|
||||
import { withStyles } from "@material-ui/core";
|
||||
import AttachmentComponent from "./Attachment";
|
||||
import { styles } from "./Attachment.styles";
|
||||
|
||||
export default withStyles(styles)(AttachmentComponent);
|
|
@ -1,6 +1,7 @@
|
|||
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: {
|
||||
height: 175,
|
||||
width: 268,
|
||||
|
@ -14,4 +15,4 @@ export const styles = (theme: Theme) => createStyles({
|
|||
backgroundColor: theme.palette.background.paper,
|
||||
opacity: 0.5
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
import {GridListTile, GridListTileBar, TextField, withStyles, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
GridListTile,
|
||||
GridListTileBar,
|
||||
TextField,
|
||||
withStyles,
|
||||
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 {
|
||||
classes: any;
|
||||
|
@ -18,8 +24,10 @@ interface IComposeMediaAttachmentState {
|
|||
attachment: Attachment;
|
||||
}
|
||||
|
||||
class ComposeMediaAttachment extends Component<IComposeMediaAttachmentProps, IComposeMediaAttachmentState> {
|
||||
|
||||
class ComposeMediaAttachment extends Component<
|
||||
IComposeMediaAttachmentProps,
|
||||
IComposeMediaAttachmentState
|
||||
> {
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: IComposeMediaAttachmentProps) {
|
||||
|
@ -29,29 +37,35 @@ class ComposeMediaAttachment extends Component<IComposeMediaAttachmentProps, ICo
|
|||
|
||||
this.state = {
|
||||
attachment: this.props.attachment
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateAttachmentText(text: string) {
|
||||
this.client.put(`/media/${this.state.attachment.id}`, { description: text }).then((resp: any) => {
|
||||
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);
|
||||
this.props.enqueueSnackbar("Description updated.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't update description: " + err.name);
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes, attachment } = this.props;
|
||||
return (
|
||||
<GridListTile className={classes.attachmentArea}>
|
||||
{
|
||||
attachment.type === "image" || attachment.type === "gifv"?
|
||||
<img src={attachment.url} alt={attachment.description? attachment.description: ""}/>:
|
||||
attachment.type === "video"?
|
||||
<video autoPlay={false} src={attachment.url}/>:
|
||||
<object data={attachment.url}/>
|
||||
}
|
||||
{attachment.type === "image" || attachment.type === "gifv" ? (
|
||||
<img
|
||||
src={attachment.url}
|
||||
alt={attachment.description ? attachment.description : ""}
|
||||
/>
|
||||
) : attachment.type === "video" ? (
|
||||
<video autoPlay={false} src={attachment.url} />
|
||||
) : (
|
||||
<object data={attachment.url} />
|
||||
)}
|
||||
<GridListTileBar
|
||||
classes={{ title: classes.attachmentBar }}
|
||||
title={
|
||||
|
@ -60,21 +74,20 @@ class ComposeMediaAttachment extends Component<IComposeMediaAttachmentProps, ICo
|
|||
label="Description"
|
||||
margin="dense"
|
||||
className={classes.attachmentText}
|
||||
onBlur={(event) => this.updateAttachmentText(event.target.value)}
|
||||
onBlur={event => this.updateAttachmentText(event.target.value)}
|
||||
></TextField>
|
||||
}
|
||||
actionIcon={
|
||||
<IconButton color="inherit"
|
||||
onClick={
|
||||
() => this.props.onDeleteCallback(this.state.attachment)
|
||||
}
|
||||
<IconButton
|
||||
color="inherit"
|
||||
onClick={() => this.props.onDeleteCallback(this.state.attachment)}
|
||||
>
|
||||
<DeleteIcon/>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
}
|
||||
/>
|
||||
</GridListTile>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import {styles} from './ComposeMediaAttachment.styles';
|
||||
import ComposeMediaAttachment from './ComposeMediaAttachment';
|
||||
import {withStyles} from '@material-ui/core';
|
||||
import { styles } from "./ComposeMediaAttachment.styles";
|
||||
import ComposeMediaAttachment from "./ComposeMediaAttachment";
|
||||
import { withStyles } from "@material-ui/core";
|
||||
|
||||
export default withStyles(styles)(ComposeMediaAttachment);
|
|
@ -1,13 +1,12 @@
|
|||
import React, { Component } from 'react';
|
||||
import {Picker, PickerProps, CustomEmoji} from 'emoji-mart';
|
||||
import 'emoji-mart/css/emoji-mart.css';
|
||||
import React, { Component } from "react";
|
||||
import { Picker, PickerProps, CustomEmoji } from "emoji-mart";
|
||||
import "emoji-mart/css/emoji-mart.css";
|
||||
|
||||
interface IEmojiPickerProps extends PickerProps {
|
||||
onGetEmoji: any;
|
||||
}
|
||||
|
||||
export class EmojiPicker extends Component<IEmojiPickerProps, any> {
|
||||
|
||||
retrieveFromLocal() {
|
||||
return JSON.parse(localStorage.getItem("emojis") as string);
|
||||
}
|
||||
|
@ -20,15 +19,14 @@ export class EmojiPicker extends Component<IEmojiPickerProps, any> {
|
|||
title=""
|
||||
onClick={this.props.onGetEmoji}
|
||||
style={{
|
||||
borderColor: 'transparent'
|
||||
borderColor: "transparent"
|
||||
}}
|
||||
perLine={10}
|
||||
emojiSize={20}
|
||||
set={"google"}
|
||||
/>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default EmojiPicker;
|
|
@ -1,13 +1,14 @@
|
|||
import { Theme, createStyles } from "@material-ui/core";
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
export const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
post: {
|
||||
marginTop: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit
|
||||
},
|
||||
postReblogChip: {
|
||||
color: theme.palette.common.white,
|
||||
'&:hover': {
|
||||
"&:hover": {
|
||||
backgroundColor: theme.palette.secondary.light
|
||||
},
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
|
@ -16,27 +17,27 @@ export const styles = (theme: Theme) => createStyles({
|
|||
postContent: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
"& a": {
|
||||
textDecoration: "none",
|
||||
color: theme.palette.secondary.light,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline'
|
||||
"&:hover": {
|
||||
textDecoration: "underline"
|
||||
},
|
||||
'&.u-url.mention': {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
fontWeight: 'bold'
|
||||
"&.u-url.mention": {
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
fontWeight: "bold"
|
||||
},
|
||||
'&.mention.hashtag': {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
fontWeight: 'bold'
|
||||
"&.mention.hashtag": {
|
||||
textDecoration: "none",
|
||||
color: "inherit",
|
||||
fontWeight: "bold"
|
||||
}
|
||||
}
|
||||
},
|
||||
postCard: {
|
||||
'& a:hover': {
|
||||
textDecoration: 'none'
|
||||
"& a:hover": {
|
||||
textDecoration: "none"
|
||||
}
|
||||
},
|
||||
postEmoji: {
|
||||
|
@ -44,7 +45,7 @@ export const styles = (theme: Theme) => createStyles({
|
|||
},
|
||||
postMedia: {
|
||||
height: 0,
|
||||
paddingTop: '56.25%', // 16:9
|
||||
paddingTop: "56.25%" // 16:9
|
||||
},
|
||||
postActionsReply: {
|
||||
marginLeft: theme.spacing.unit,
|
||||
|
@ -85,14 +86,14 @@ export const styles = (theme: Theme) => createStyles({
|
|||
color: "inherit"
|
||||
},
|
||||
mobileOnly: {
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'none'
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
desktopOnly: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'block'
|
||||
display: "none",
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "block"
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,31 +1,65 @@
|
|||
import React from 'react';
|
||||
import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip, RadioGroup, Radio, FormControlLabel, Button, Dialog, DialogTitle, DialogContent, DialogContentText, DialogActions } from '@material-ui/core';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
import ReplyIcon from '@material-ui/icons/Reply';
|
||||
import FavoriteIcon from '@material-ui/icons/Favorite';
|
||||
import AutorenewIcon from '@material-ui/icons/Autorenew';
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import PublicIcon from '@material-ui/icons/Public';
|
||||
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import ForumIcon from '@material-ui/icons/Forum';
|
||||
import AlternateEmailIcon from '@material-ui/icons/AlternateEmail';
|
||||
import LocalOfferIcon from '@material-ui/icons/LocalOffer';
|
||||
import {styles} from './Post.styles';
|
||||
import { Status } from '../../types/Status';
|
||||
import { Tag } from '../../types/Tag';
|
||||
import { Mention } from '../../types/Mention';
|
||||
import { Visibility } from '../../types/Visibility';
|
||||
import moment from 'moment';
|
||||
import AttachmentComponent from '../Attachment';
|
||||
import Mastodon from 'megalodon';
|
||||
import { LinkableChip, LinkableMenuItem, LinkableIconButton, LinkableAvatar } from '../../interfaces/overrides';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ShareMenu from './PostShareMenu';
|
||||
import {emojifyString} from '../../utilities/emojis';
|
||||
import { PollOption } from '../../types/Poll';
|
||||
import React from "react";
|
||||
import {
|
||||
Typography,
|
||||
IconButton,
|
||||
Card,
|
||||
CardHeader,
|
||||
Avatar,
|
||||
CardContent,
|
||||
CardActions,
|
||||
withStyles,
|
||||
Menu,
|
||||
MenuItem,
|
||||
Chip,
|
||||
Divider,
|
||||
CardMedia,
|
||||
CardActionArea,
|
||||
ExpansionPanel,
|
||||
ExpansionPanelSummary,
|
||||
ExpansionPanelDetails,
|
||||
Zoom,
|
||||
Tooltip,
|
||||
RadioGroup,
|
||||
Radio,
|
||||
FormControlLabel,
|
||||
Button,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogContent,
|
||||
DialogContentText,
|
||||
DialogActions
|
||||
} from "@material-ui/core";
|
||||
import MoreVertIcon from "@material-ui/icons/MoreVert";
|
||||
import ReplyIcon from "@material-ui/icons/Reply";
|
||||
import FavoriteIcon from "@material-ui/icons/Favorite";
|
||||
import AutorenewIcon from "@material-ui/icons/Autorenew";
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import PublicIcon from "@material-ui/icons/Public";
|
||||
import VisibilityOffIcon from "@material-ui/icons/VisibilityOff";
|
||||
import WarningIcon from "@material-ui/icons/Warning";
|
||||
import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
|
||||
import GroupIcon from "@material-ui/icons/Group";
|
||||
import ForumIcon from "@material-ui/icons/Forum";
|
||||
import AlternateEmailIcon from "@material-ui/icons/AlternateEmail";
|
||||
import LocalOfferIcon from "@material-ui/icons/LocalOffer";
|
||||
import { styles } from "./Post.styles";
|
||||
import { Status } from "../../types/Status";
|
||||
import { Tag } from "../../types/Tag";
|
||||
import { Mention } from "../../types/Mention";
|
||||
import { Visibility } from "../../types/Visibility";
|
||||
import moment from "moment";
|
||||
import AttachmentComponent from "../Attachment";
|
||||
import Mastodon from "megalodon";
|
||||
import {
|
||||
LinkableChip,
|
||||
LinkableMenuItem,
|
||||
LinkableIconButton,
|
||||
LinkableAvatar
|
||||
} from "../../interfaces/overrides";
|
||||
import { withSnackbar } from "notistack";
|
||||
import ShareMenu from "./PostShareMenu";
|
||||
import { emojifyString } from "../../utilities/emojis";
|
||||
import { PollOption } from "../../types/Poll";
|
||||
|
||||
interface IPostProps {
|
||||
post: Status;
|
||||
|
@ -42,7 +76,6 @@ interface IPostState {
|
|||
}
|
||||
|
||||
export class Post extends React.Component<any, IPostState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
|
@ -50,17 +83,19 @@ export class Post extends React.Component<any, IPostState> {
|
|||
|
||||
this.state = {
|
||||
post: this.props.post,
|
||||
media_slides: this.props.post.media_attachments.length > 0? this.props.post.media_attachments.length: 0,
|
||||
media_slides:
|
||||
this.props.post.media_attachments.length > 0
|
||||
? this.props.post.media_attachments.length
|
||||
: 0,
|
||||
menuIsOpen: false,
|
||||
deletePostDialog: false
|
||||
}
|
||||
};
|
||||
|
||||
this.client = this.props.client;
|
||||
|
||||
}
|
||||
|
||||
togglePostMenu() {
|
||||
this.setState({ menuIsOpen: !this.state.menuIsOpen })
|
||||
this.setState({ menuIsOpen: !this.state.menuIsOpen });
|
||||
}
|
||||
|
||||
togglePostDeleteDialog() {
|
||||
|
@ -68,12 +103,15 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
|
||||
deletePost() {
|
||||
this.client.del('/statuses/' + this.state.post.id).then((resp: any) => {
|
||||
this.props.enqueueSnackbar('Post deleted. Refresh to see changes.');
|
||||
}).catch((err: Error) => {
|
||||
this.client
|
||||
.del("/statuses/" + this.state.post.id)
|
||||
.then((resp: any) => {
|
||||
this.props.enqueueSnackbar("Post deleted. Refresh to see changes.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't delete post: " + err.name);
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
findBiggestVote() {
|
||||
|
@ -82,13 +120,13 @@ export class Post extends React.Component<any, IPostState> {
|
|||
let bv = "";
|
||||
if (poll) {
|
||||
poll.options.forEach((option: PollOption) => {
|
||||
votes.push(option.votes_count? option.votes_count: 0);
|
||||
votes.push(option.votes_count ? option.votes_count : 0);
|
||||
});
|
||||
let biggestVote = Math.max.apply(null, votes);
|
||||
poll.options.forEach((option: PollOption) => {
|
||||
if (option.votes_count === biggestVote) {
|
||||
bv = option.title;
|
||||
};
|
||||
}
|
||||
});
|
||||
return bv;
|
||||
} else {
|
||||
|
@ -104,7 +142,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
if (pollOption.title === option) {
|
||||
pollIndex = index;
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
this.setState({ myVote: [pollIndex] });
|
||||
}
|
||||
|
@ -112,117 +150,158 @@ export class Post extends React.Component<any, IPostState> {
|
|||
submitVote() {
|
||||
let poll = this.state.post.poll;
|
||||
if (poll) {
|
||||
this.client.post(`/polls/${poll.id}/votes`, {choices: this.state.myVote}).then((resp: any) => {
|
||||
this.client
|
||||
.post(`/polls/${poll.id}/votes`, { choices: this.state.myVote })
|
||||
.then((resp: any) => {
|
||||
let post = this.state.post;
|
||||
post.poll = resp.data;
|
||||
this.setState({ post });
|
||||
this.props.enqueueSnackbar("Vote submitted.");
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't vote: " + err.name);
|
||||
console.error(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
materializeContent(status: Status) {
|
||||
const { classes } = this.props;
|
||||
|
||||
const oldContent = document.createElement('div');
|
||||
const oldContent = document.createElement("div");
|
||||
oldContent.innerHTML = status.content;
|
||||
|
||||
let anchors = oldContent.getElementsByTagName("a");
|
||||
|
||||
Array.prototype.forEach.call(anchors, (link: HTMLAnchorElement) => {
|
||||
if (link.className.includes("mention") || link.className.includes("hashtag")) {
|
||||
if (
|
||||
link.className.includes("mention") ||
|
||||
link.className.includes("hashtag")
|
||||
) {
|
||||
link.removeAttribute("href");
|
||||
}
|
||||
});
|
||||
|
||||
oldContent.innerHTML = emojifyString(oldContent.innerHTML, status.emojis, classes.postEmoji);
|
||||
oldContent.innerHTML = emojifyString(
|
||||
oldContent.innerHTML,
|
||||
status.emojis,
|
||||
classes.postEmoji
|
||||
);
|
||||
|
||||
return (
|
||||
<CardContent className={classes.postContent}>
|
||||
<div className={classes.mediaContainer}>
|
||||
<Typography paragraph dangerouslySetInnerHTML={{__html: oldContent.innerHTML}}/>
|
||||
{
|
||||
status.card?
|
||||
<Typography
|
||||
paragraph
|
||||
dangerouslySetInnerHTML={{ __html: oldContent.innerHTML }}
|
||||
/>
|
||||
{status.card ? (
|
||||
<div className={classes.postCard}>
|
||||
<Divider/>
|
||||
<CardActionArea href={status.card.url} target="_blank" rel="noreferrer">
|
||||
<Divider />
|
||||
<CardActionArea
|
||||
href={status.card.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h6" component="h2">{status.card.title}</Typography>
|
||||
<Typography gutterBottom variant="h6" component="h2">
|
||||
{status.card.title}
|
||||
</Typography>
|
||||
<Typography>
|
||||
{
|
||||
status.card.description.slice(0, 500) + (status.card.description.length > 500? "...": "")
|
||||
|| "No description provided. Click with caution."
|
||||
}
|
||||
{status.card.description.slice(0, 500) +
|
||||
(status.card.description.length > 500 ? "..." : "") ||
|
||||
"No description provided. Click with caution."}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
{
|
||||
status.card.image && status.media_attachments.length <= 0?
|
||||
<CardMedia className={classes.postMedia} image={status.card.image}/>: <span/>
|
||||
}
|
||||
{status.card.image && status.media_attachments.length <= 0 ? (
|
||||
<CardMedia
|
||||
className={classes.postMedia}
|
||||
image={status.card.image}
|
||||
/>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
<CardContent>
|
||||
<Typography>{status.card.provider_url|| status.card.author_url || status.card.author_url}</Typography>
|
||||
<Typography>
|
||||
{status.card.provider_url ||
|
||||
status.card.author_url ||
|
||||
status.card.author_url}
|
||||
</Typography>
|
||||
</CardContent>
|
||||
</CardActionArea>
|
||||
<Divider/>
|
||||
</div>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
status.media_attachments.length > 0?
|
||||
<AttachmentComponent media={status.media_attachments}/>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
status.poll?
|
||||
status.poll.voted || status.poll.expired?
|
||||
<Divider />
|
||||
</div>
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{status.media_attachments.length > 0 ? (
|
||||
<AttachmentComponent media={status.media_attachments} />
|
||||
) : (
|
||||
<span />
|
||||
)}
|
||||
{status.poll ? (
|
||||
status.poll.voted || status.poll.expired ? (
|
||||
<div>
|
||||
<Typography variant="caption">You can't vote on this poll. Below are the results of the poll.</Typography>
|
||||
<RadioGroup
|
||||
value={this.findBiggestVote()}
|
||||
>
|
||||
{
|
||||
status.poll.options.map((pollOption: PollOption) => {
|
||||
let x = <FormControlLabel
|
||||
<Typography variant="caption">
|
||||
You can't vote on this poll. Below are the results of the
|
||||
poll.
|
||||
</Typography>
|
||||
<RadioGroup value={this.findBiggestVote()}>
|
||||
{status.poll.options.map((pollOption: PollOption) => {
|
||||
let x = (
|
||||
<FormControlLabel
|
||||
disabled
|
||||
value={pollOption.title}
|
||||
control={<Radio />}
|
||||
label={`${pollOption.title} (${pollOption.votes_count} votes)`}
|
||||
key={pollOption.title+pollOption.votes_count}
|
||||
/>;
|
||||
return (x);
|
||||
|
||||
})
|
||||
}
|
||||
key={pollOption.title + pollOption.votes_count}
|
||||
/>
|
||||
);
|
||||
return x;
|
||||
})}
|
||||
</RadioGroup>
|
||||
{
|
||||
status.poll && status.poll.expired?
|
||||
<Typography variant="caption">This poll has expired.</Typography>:
|
||||
<Typography variant="caption">This poll will expire on {moment(status.poll.expires_at? status.poll.expires_at: "").format('MMMM Do YYYY, [at] h:mm A')}.</Typography>
|
||||
}
|
||||
</div>:
|
||||
{status.poll && status.poll.expired ? (
|
||||
<Typography variant="caption">
|
||||
This poll has expired.
|
||||
</Typography>
|
||||
) : (
|
||||
<Typography variant="caption">
|
||||
This poll will expire on{" "}
|
||||
{moment(
|
||||
status.poll.expires_at ? status.poll.expires_at : ""
|
||||
).format("MMMM Do YYYY, [at] h:mm A")}
|
||||
.
|
||||
</Typography>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div>
|
||||
<RadioGroup
|
||||
onChange={(event: any, option: any) => this.captureVote(option)}
|
||||
onChange={(event: any, option: any) =>
|
||||
this.captureVote(option)
|
||||
}
|
||||
>
|
||||
{
|
||||
status.poll.options.map((pollOption: PollOption) => {
|
||||
let x = <FormControlLabel
|
||||
{status.poll.options.map((pollOption: PollOption) => {
|
||||
let x = (
|
||||
<FormControlLabel
|
||||
value={pollOption.title}
|
||||
control={<Radio />}
|
||||
label={pollOption.title}
|
||||
key={pollOption.title+pollOption.votes_count}
|
||||
/>;
|
||||
return (x);
|
||||
|
||||
})
|
||||
}
|
||||
key={pollOption.title + pollOption.votes_count}
|
||||
/>
|
||||
);
|
||||
return x;
|
||||
})}
|
||||
</RadioGroup>
|
||||
<Button color="primary" onClick={(event: any) => this.submitVote()}>Vote</Button>
|
||||
</div>: null
|
||||
}
|
||||
<Button
|
||||
color="primary"
|
||||
onClick={(event: any) => this.submitVote()}
|
||||
>
|
||||
Vote
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
</CardContent>
|
||||
);
|
||||
|
@ -235,7 +314,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
if (text.includes(flag)) {
|
||||
result = true;
|
||||
}
|
||||
})
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
|
@ -243,27 +322,39 @@ export class Post extends React.Component<any, IPostState> {
|
|||
const { classes } = this.props;
|
||||
const warningText = spoiler_text || "Unmarked content";
|
||||
let icon;
|
||||
if (this.spoilerContainsFlags(spoiler_text) || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") {
|
||||
icon = <WarningIcon className={classes.postWarningIcon}/>;
|
||||
if (
|
||||
this.spoilerContainsFlags(spoiler_text) ||
|
||||
spoiler_text.includes("Spoiler") ||
|
||||
warningText === "Unmarked content"
|
||||
) {
|
||||
icon = <WarningIcon className={classes.postWarningIcon} />;
|
||||
}
|
||||
return (
|
||||
<ExpansionPanel className={this.spoilerContainsFlags(spoiler_text)? classes.nsfwCard: classes.spoilerCard} color="inherit">
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} color="inherit">
|
||||
{icon}<Typography>{warningText}</Typography>
|
||||
<ExpansionPanel
|
||||
className={
|
||||
this.spoilerContainsFlags(spoiler_text)
|
||||
? classes.nsfwCard
|
||||
: classes.spoilerCard
|
||||
}
|
||||
color="inherit"
|
||||
>
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon />} color="inherit">
|
||||
{icon}
|
||||
<Typography>{warningText}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.postContent} color="inherit">
|
||||
{this.materializeContent(content)}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
getReblogOfPost(of: Status | null) {
|
||||
const { classes } = this.props;
|
||||
if (of !== null) {
|
||||
return (
|
||||
of.sensitive? this.getSensitiveContent(of.spoiler_text, of): this.materializeContent(of)
|
||||
);
|
||||
return of.sensitive
|
||||
? this.getSensitiveContent(of.spoiler_text, of)
|
||||
: this.materializeContent(of);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -273,16 +364,19 @@ export class Post extends React.Component<any, IPostState> {
|
|||
const { classes } = this.props;
|
||||
if (post.reblog) {
|
||||
let author = post.reblog.account;
|
||||
let origString = `<span>${author.display_name || author.username} (@${author.acct}) 🔄 ${post.account.display_name || post.account.username}</span>`;
|
||||
let origString = `<span>${author.display_name || author.username} (@${
|
||||
author.acct
|
||||
}) 🔄 ${post.account.display_name || post.account.username}</span>`;
|
||||
let emojis = author.emojis;
|
||||
emojis.concat(post.account.emojis);
|
||||
return emojifyString(origString, emojis, classes.postAuthorEmoji);
|
||||
} else {
|
||||
let author = post.account;
|
||||
let origString = `<span>${author.display_name || author.username} (@${author.acct})</span>`;
|
||||
let origString = `<span>${author.display_name || author.username} (@${
|
||||
author.acct
|
||||
})</span>`;
|
||||
return emojifyString(origString, author.emojis, classes.postAuthorEmoji);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getMentions(mention: [Mention]) {
|
||||
|
@ -291,12 +385,12 @@ export class Post extends React.Component<any, IPostState> {
|
|||
return (
|
||||
<CardContent className={classes.postTags}>
|
||||
<Typography variant="caption">Mentions</Typography>
|
||||
{
|
||||
mention.map((person: Mention) => {
|
||||
return <LinkableChip
|
||||
{mention.map((person: Mention) => {
|
||||
return (
|
||||
<LinkableChip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<AlternateEmailIcon/>
|
||||
<AlternateEmailIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={person.username}
|
||||
|
@ -305,10 +399,10 @@ export class Post extends React.Component<any, IPostState> {
|
|||
className={classes.postMention}
|
||||
clickable
|
||||
/>
|
||||
})
|
||||
}
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -320,12 +414,12 @@ export class Post extends React.Component<any, IPostState> {
|
|||
return (
|
||||
<CardContent className={classes.postTags}>
|
||||
<Typography variant="caption">Tags</Typography>
|
||||
{
|
||||
tags.map((tag: Tag) => {
|
||||
return <LinkableChip
|
||||
{tags.map((tag: Tag) => {
|
||||
return (
|
||||
<LinkableChip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<LocalOfferIcon/>
|
||||
<LocalOfferIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={tag.name}
|
||||
|
@ -334,10 +428,10 @@ export class Post extends React.Component<any, IPostState> {
|
|||
className={classes.postMention}
|
||||
clickable
|
||||
/>
|
||||
})
|
||||
}
|
||||
);
|
||||
})}
|
||||
</CardContent>
|
||||
)
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
|
@ -345,22 +439,34 @@ export class Post extends React.Component<any, IPostState> {
|
|||
|
||||
showVisibilityIcon(visibility: Visibility) {
|
||||
const { classes } = this.props;
|
||||
switch(visibility) {
|
||||
switch (visibility) {
|
||||
case "public":
|
||||
return <Tooltip title="Public"><PublicIcon className={classes.postTypeIcon}/></Tooltip>;
|
||||
return (
|
||||
<Tooltip title="Public">
|
||||
<PublicIcon className={classes.postTypeIcon} />
|
||||
</Tooltip>
|
||||
);
|
||||
case "private":
|
||||
return <Tooltip title="Followers only"><GroupIcon className={classes.postTypeIcon}/></Tooltip>;
|
||||
return (
|
||||
<Tooltip title="Followers only">
|
||||
<GroupIcon className={classes.postTypeIcon} />
|
||||
</Tooltip>
|
||||
);
|
||||
case "unlisted":
|
||||
return <Tooltip title="Unlisted (invisible from public timeline)"><VisibilityOffIcon className={classes.postTypeIcon}/></Tooltip>;
|
||||
return (
|
||||
<Tooltip title="Unlisted (invisible from public timeline)">
|
||||
<VisibilityOffIcon className={classes.postTypeIcon} />
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
getMastodonUrl(post: Status) {
|
||||
let url = "";
|
||||
if (post.reblog) {
|
||||
url = post.reblog.uri
|
||||
url = post.reblog.uri;
|
||||
} else {
|
||||
url = post.uri
|
||||
url = post.uri;
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
@ -368,49 +474,61 @@ export class Post extends React.Component<any, IPostState> {
|
|||
toggleFavorited(post: Status) {
|
||||
let _this = this;
|
||||
if (post.favourited) {
|
||||
this.client.post(`/statuses/${post.id}/unfavourite`).then((resp: any) => {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unfavourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(`Couldn't unfavorite post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
variant: "error"
|
||||
});
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
this.client.post(`/statuses/${post.id}/favourite`).then((resp: any) => {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/favourite`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(`Couldn't favorite post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
variant: "error"
|
||||
});
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
toggleReblogged(post: Status) {
|
||||
if (post.reblogged) {
|
||||
this.client.post(`/statuses/${post.id}/unreblog`).then((resp: any) => {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/unreblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(`Couldn't unboost post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
variant: "error"
|
||||
});
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
} else {
|
||||
this.client.post(`/statuses/${post.id}/reblog`).then((resp: any) => {
|
||||
this.client
|
||||
.post(`/statuses/${post.id}/reblog`)
|
||||
.then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(`Couldn't boost post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
variant: "error"
|
||||
});
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -423,22 +541,30 @@ export class Post extends React.Component<any, IPostState> {
|
|||
<DialogTitle id="alert-dialog-title">Delete this post?</DialogTitle>
|
||||
<DialogContent>
|
||||
<DialogContentText id="alert-dialog-description">
|
||||
Are you sure you want to delete this post? This action cannot be undone.
|
||||
Are you sure you want to delete this post? This action cannot be
|
||||
undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.togglePostDeleteDialog()} color="primary" autoFocus>
|
||||
<Button
|
||||
onClick={() => this.togglePostDeleteDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.deletePost();
|
||||
this.togglePostDeleteDialog();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -447,76 +573,119 @@ export class Post extends React.Component<any, IPostState> {
|
|||
return (
|
||||
<Zoom in={true}>
|
||||
<Card className={classes.post} id={`post_${post.id}`}>
|
||||
<CardHeader avatar={
|
||||
<LinkableAvatar to={`/profile/${post.reblog? post.reblog.account.id: post.account.id}`} src={
|
||||
post.reblog? post.reblog.account.avatar_static: post.account.avatar_static
|
||||
} />
|
||||
} action={
|
||||
<CardHeader
|
||||
avatar={
|
||||
<LinkableAvatar
|
||||
to={`/profile/${
|
||||
post.reblog ? post.reblog.account.id : post.account.id
|
||||
}`}
|
||||
src={
|
||||
post.reblog
|
||||
? post.reblog.account.avatar_static
|
||||
: post.account.avatar_static
|
||||
}
|
||||
/>
|
||||
}
|
||||
action={
|
||||
<Tooltip title="More">
|
||||
<IconButton key={`${post.id}_submenu`} id={`${post.id}_submenu`} onClick={() => this.togglePostMenu()}>
|
||||
<IconButton
|
||||
key={`${post.id}_submenu`}
|
||||
id={`${post.id}_submenu`}
|
||||
onClick={() => this.togglePostMenu()}
|
||||
>
|
||||
<MoreVertIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
}
|
||||
title={
|
||||
<Typography dangerouslySetInnerHTML={{__html: this.getReblogAuthors(post)}}></Typography>
|
||||
}
|
||||
subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
|
||||
{
|
||||
post.reblog? this.getReblogOfPost(post.reblog): null
|
||||
}
|
||||
{
|
||||
post.sensitive? this.getSensitiveContent(post.spoiler_text, post):
|
||||
post.reblog? null: this.materializeContent(post)
|
||||
}
|
||||
{
|
||||
post.reblog && post.reblog.mentions.length > 0? this.getMentions(post.reblog.mentions): this.getMentions(post.mentions)
|
||||
}
|
||||
{
|
||||
post.reblog && post.reblog.tags.length > 0? this.getTags(post.reblog.tags): this.getTags(post.tags)
|
||||
<Typography
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.getReblogAuthors(post)
|
||||
}}
|
||||
></Typography>
|
||||
}
|
||||
subheader={moment(post.created_at).format(
|
||||
"MMMM Do YYYY [at] h:mm A"
|
||||
)}
|
||||
/>
|
||||
{post.reblog ? this.getReblogOfPost(post.reblog) : null}
|
||||
{post.sensitive
|
||||
? this.getSensitiveContent(post.spoiler_text, post)
|
||||
: post.reblog
|
||||
? null
|
||||
: this.materializeContent(post)}
|
||||
{post.reblog && post.reblog.mentions.length > 0
|
||||
? this.getMentions(post.reblog.mentions)
|
||||
: this.getMentions(post.mentions)}
|
||||
{post.reblog && post.reblog.tags.length > 0
|
||||
? this.getTags(post.reblog.tags)
|
||||
: this.getTags(post.tags)}
|
||||
<CardActions>
|
||||
<Tooltip title="Reply">
|
||||
<LinkableIconButton to={`/compose?reply=${post.reblog? post.reblog.id: post.id}&visibility=${post.visibility}&acct=${post.reblog? post.reblog.account.acct: post.account.acct}`}>
|
||||
<ReplyIcon/>
|
||||
<LinkableIconButton
|
||||
to={`/compose?reply=${
|
||||
post.reblog ? post.reblog.id : post.id
|
||||
}&visibility=${post.visibility}&acct=${
|
||||
post.reblog ? post.reblog.account.acct : post.account.acct
|
||||
}`}
|
||||
>
|
||||
<ReplyIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Typography>{post.reblog? post.reblog.replies_count: post.replies_count}</Typography>
|
||||
<Typography>
|
||||
{post.reblog ? post.reblog.replies_count : post.replies_count}
|
||||
</Typography>
|
||||
<Tooltip title="Favorite">
|
||||
<IconButton onClick={() => this.toggleFavorited(post)}>
|
||||
<FavoriteIcon className={
|
||||
post.reblog?
|
||||
post.reblog.favourited?
|
||||
classes.postDidAction:
|
||||
'':
|
||||
post.favourited?
|
||||
classes.postDidAction:
|
||||
''
|
||||
}/>
|
||||
<FavoriteIcon
|
||||
className={
|
||||
post.reblog
|
||||
? post.reblog.favourited
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
: post.favourited
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{post.reblog? post.reblog.favourites_count: post.favourites_count}</Typography>
|
||||
<Typography>
|
||||
{post.reblog
|
||||
? post.reblog.favourites_count
|
||||
: post.favourites_count}
|
||||
</Typography>
|
||||
<Tooltip title="Boost">
|
||||
<IconButton onClick={() => this.toggleReblogged(post)}>
|
||||
<AutorenewIcon className={
|
||||
post.reblog?
|
||||
post.reblog.reblogged?
|
||||
classes.postDidAction:
|
||||
'':
|
||||
post.reblogged?
|
||||
classes.postDidAction:
|
||||
''
|
||||
}/>
|
||||
<AutorenewIcon
|
||||
className={
|
||||
post.reblog
|
||||
? post.reblog.reblogged
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
: post.reblogged
|
||||
? classes.postDidAction
|
||||
: ""
|
||||
}
|
||||
/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{post.reblog? post.reblog.reblogs_count: post.reblogs_count}</Typography>
|
||||
<Typography>
|
||||
{post.reblog ? post.reblog.reblogs_count : post.reblogs_count}
|
||||
</Typography>
|
||||
<Tooltip className={classes.desktopOnly} title="View thread">
|
||||
<LinkableIconButton to={`/conversation/${post.reblog? post.reblog.id: post.id}`}>
|
||||
<LinkableIconButton
|
||||
to={`/conversation/${post.reblog ? post.reblog.id : post.id}`}
|
||||
>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip className={classes.desktopOnly} title="Open in Web">
|
||||
<IconButton href={this.getMastodonUrl(post)} rel="noreferrer" target="_blank">
|
||||
<IconButton
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
|
@ -531,39 +700,65 @@ export class Post extends React.Component<any, IPostState> {
|
|||
open={this.state.menuIsOpen}
|
||||
onClose={() => this.togglePostMenu()}
|
||||
>
|
||||
<ShareMenu config={{
|
||||
<ShareMenu
|
||||
config={{
|
||||
params: {
|
||||
title: `@${post.account.username} posted on Mastodon: `,
|
||||
text: post.content,
|
||||
url: this.getMastodonUrl(post),
|
||||
url: this.getMastodonUrl(post)
|
||||
},
|
||||
onShareSuccess: () => this.props.enqueueSnackbar("Post shared!", {variant: 'success'}),
|
||||
onShareSuccess: () =>
|
||||
this.props.enqueueSnackbar("Post shared!", {
|
||||
variant: "success"
|
||||
}),
|
||||
onShareError: (error: Error) => {
|
||||
if (error.name != "AbortError")
|
||||
this.props.enqueueSnackbar(`Couldn't share post: ${error.name}`, {variant: 'error'})
|
||||
},
|
||||
}}/>
|
||||
{
|
||||
post.reblog?
|
||||
<div>
|
||||
<LinkableMenuItem to={`/profile/${post.reblog.account.id}`}>View author profile</LinkableMenuItem>
|
||||
<LinkableMenuItem to={`/profile/${post.account.id}`}>View reblogger profile</LinkableMenuItem>
|
||||
|
||||
</div>: <LinkableMenuItem to={`/profile/${post.account.id}`}>View profile</LinkableMenuItem>
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't share post: ${error.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
}
|
||||
<div className={classes.mobileOnly}>
|
||||
<Divider/>
|
||||
<LinkableMenuItem to={`/conversation/${post.reblog? post.reblog.id: post.id}`}>View thread</LinkableMenuItem>
|
||||
<MenuItem component="a" href={this.getMastodonUrl(post)} rel="noreferrer" target="_blank">Open in Web</MenuItem>
|
||||
}}
|
||||
/>
|
||||
{post.reblog ? (
|
||||
<div>
|
||||
<LinkableMenuItem to={`/profile/${post.reblog.account.id}`}>
|
||||
View author profile
|
||||
</LinkableMenuItem>
|
||||
<LinkableMenuItem to={`/profile/${post.account.id}`}>
|
||||
View reblogger profile
|
||||
</LinkableMenuItem>
|
||||
</div>
|
||||
{
|
||||
post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
|
||||
) : (
|
||||
<LinkableMenuItem to={`/profile/${post.account.id}`}>
|
||||
View profile
|
||||
</LinkableMenuItem>
|
||||
)}
|
||||
<div className={classes.mobileOnly}>
|
||||
<Divider />
|
||||
<LinkableMenuItem
|
||||
to={`/conversation/${post.reblog ? post.reblog.id : post.id}`}
|
||||
>
|
||||
View thread
|
||||
</LinkableMenuItem>
|
||||
<MenuItem
|
||||
component="a"
|
||||
href={this.getMastodonUrl(post)}
|
||||
rel="noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Open in Web
|
||||
</MenuItem>
|
||||
</div>
|
||||
{post.account.id ==
|
||||
JSON.parse(localStorage.getItem("account") as string).id ? (
|
||||
<div>
|
||||
<Divider/>
|
||||
<MenuItem onClick={() => this.togglePostDeleteDialog()}>Delete</MenuItem>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
<Divider />
|
||||
<MenuItem onClick={() => this.togglePostDeleteDialog()}>
|
||||
Delete
|
||||
</MenuItem>
|
||||
</div>
|
||||
) : null}
|
||||
{this.showDeleteDialog()}
|
||||
</Menu>
|
||||
</Card>
|
||||
|
|
|
@ -1,15 +1,20 @@
|
|||
import * as React from 'react';
|
||||
import webShare, { WebShareInterface } from 'react-web-share-api';
|
||||
import {MenuItem} from '@material-ui/core';
|
||||
import * as React from "react";
|
||||
import webShare, { WebShareInterface } from "react-web-share-api";
|
||||
import { MenuItem } from "@material-ui/core";
|
||||
|
||||
export interface OwnProps {
|
||||
style: object;
|
||||
}
|
||||
|
||||
const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({
|
||||
share, isSupported, style,
|
||||
}) => isSupported
|
||||
? <MenuItem onClick={share} style={style}>Share</MenuItem>
|
||||
: null
|
||||
share,
|
||||
isSupported,
|
||||
style
|
||||
}) =>
|
||||
isSupported ? (
|
||||
<MenuItem onClick={share} style={style}>
|
||||
Share
|
||||
</MenuItem>
|
||||
) : null;
|
||||
|
||||
export default webShare<OwnProps>()(ShareMenu);
|
|
@ -1,6 +1,6 @@
|
|||
import { Post } from './Post';
|
||||
import { withStyles } from '@material-ui/core';
|
||||
import { styles } from './Post.styles';
|
||||
import { withSnackbar } from 'notistack'
|
||||
import { Post } from "./Post";
|
||||
import { withStyles } from "@material-ui/core";
|
||||
import { styles } from "./Post.styles";
|
||||
import { withSnackbar } from "notistack";
|
||||
|
||||
export default withStyles(styles)(withSnackbar(Post));
|
|
@ -1,7 +1,16 @@
|
|||
import React, {Component} from 'react';
|
||||
import {MuiThemeProvider, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
MuiThemeProvider,
|
||||
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 {
|
||||
theme: Theme;
|
||||
|
@ -17,40 +26,62 @@ class ThemePreview extends Component<IThemePreviewProps, IThemePreviewState> {
|
|||
|
||||
this.state = {
|
||||
theme: this.props.theme
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<div style={{ position: "relative" }}>
|
||||
<MuiThemeProvider theme={this.props.theme}>
|
||||
<CssBaseline/>
|
||||
<CssBaseline />
|
||||
<Paper>
|
||||
<AppBar color="primary" position="static">
|
||||
<Toolbar>
|
||||
<MenuIcon style={{ marginRight: 20, marginLeft: -4 }}/>
|
||||
<Typography variant="h6" color="inherit">Hyperspace</Typography>
|
||||
<MenuIcon style={{ marginRight: 20, marginLeft: -4 }} />
|
||||
<Typography variant="h6" color="inherit">
|
||||
Hyperspace
|
||||
</Typography>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<div style={{ paddingLeft: 16, paddingTop: 16, paddingRight: 16, paddingBottom: 16, flexGrow: 1 }}>
|
||||
<div
|
||||
style={{
|
||||
paddingLeft: 16,
|
||||
paddingTop: 16,
|
||||
paddingRight: 16,
|
||||
paddingBottom: 16,
|
||||
flexGrow: 1
|
||||
}}
|
||||
>
|
||||
<Typography variant="h4" component="p">
|
||||
This is your theme.
|
||||
</Typography>
|
||||
<br/>
|
||||
<br />
|
||||
<Typography paragraph>
|
||||
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.
|
||||
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 style={{ textAlign: 'right' }}>
|
||||
<Fab color="secondary" style={{ marginRight: 8, marginBottom: 8 }}>
|
||||
<EditIcon/>
|
||||
<div style={{ textAlign: "right" }}>
|
||||
<Fab
|
||||
color="secondary"
|
||||
style={{ marginRight: 8, marginBottom: 8 }}
|
||||
>
|
||||
<EditIcon />
|
||||
</Fab>
|
||||
</div>
|
||||
|
||||
</Paper>
|
||||
</MuiThemeProvider>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,3 @@
|
|||
import ThemePreview from './ThemePreview';
|
||||
import ThemePreview from "./ThemePreview";
|
||||
|
||||
export default ThemePreview;
|
|
@ -1,18 +1,20 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import App from './App';
|
||||
import { HashRouter } from 'react-router-dom';
|
||||
import * as serviceWorker from './serviceWorker';
|
||||
import {createUserDefaults, getConfig} from './utilities/settings';
|
||||
import {collectEmojisFromServer} from './utilities/emojis';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
import { userLoggedIn, refreshUserAccountData } from './utilities/accounts';
|
||||
import React from "react";
|
||||
import ReactDOM from "react-dom";
|
||||
import App from "./App";
|
||||
import { HashRouter } from "react-router-dom";
|
||||
import * as serviceWorker from "./serviceWorker";
|
||||
import { createUserDefaults, getConfig } from "./utilities/settings";
|
||||
import { collectEmojisFromServer } from "./utilities/emojis";
|
||||
import { SnackbarProvider } from "notistack";
|
||||
import { userLoggedIn, refreshUserAccountData } from "./utilities/accounts";
|
||||
|
||||
getConfig().then((config: any) => {
|
||||
getConfig()
|
||||
.then((config: any) => {
|
||||
document.title = config.branding.name || "Hyperspace";
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
console.error(err);
|
||||
})
|
||||
});
|
||||
|
||||
createUserDefaults();
|
||||
if (userLoggedIn()) {
|
||||
|
@ -24,15 +26,15 @@ ReactDOM.render(
|
|||
<HashRouter>
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'left',
|
||||
vertical: "bottom",
|
||||
horizontal: "left"
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</HashRouter>,
|
||||
document.getElementById('root'));
|
||||
|
||||
document.getElementById("root")
|
||||
);
|
||||
|
||||
// If you want your app to work offline and load faster, you can change
|
||||
// unregister() to register() below. Note this comes with some pitfalls.
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import ListItem, { ListItemProps } from "@material-ui/core/ListItem";
|
||||
import IconButton, { IconButtonProps } from "@material-ui/core/IconButton";
|
||||
import { Link, Route, Redirect, RouteProps } from "react-router-dom";
|
||||
import Chip, { ChipProps } from '@material-ui/core/Chip';
|
||||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import Button, { ButtonProps } from '@material-ui/core/Button';
|
||||
import Fab, { FabProps } from '@material-ui/core/Fab';
|
||||
import Avatar, { AvatarProps } from '@material-ui/core/Avatar';
|
||||
import { userLoggedIn } from '../utilities/accounts';
|
||||
import Chip, { ChipProps } from "@material-ui/core/Chip";
|
||||
import { MenuItemProps } from "@material-ui/core/MenuItem";
|
||||
import { MenuItem } from "@material-ui/core";
|
||||
import Button, { ButtonProps } from "@material-ui/core/Button";
|
||||
import Fab, { FabProps } from "@material-ui/core/Fab";
|
||||
import Avatar, { AvatarProps } from "@material-ui/core/Avatar";
|
||||
import { userLoggedIn } from "../utilities/accounts";
|
||||
|
||||
export interface ILinkableListItemProps extends ListItemProps {
|
||||
to: string;
|
||||
|
@ -46,50 +46,52 @@ export interface ILinkableAvatarProps extends AvatarProps {
|
|||
}
|
||||
|
||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||
<ListItem {...props} component={Link as any}/>
|
||||
)
|
||||
<ListItem {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
|
||||
<IconButton {...props} component={Link as any}/>
|
||||
)
|
||||
<IconButton {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const LinkableChip = (props: ILinkableChipProps) => (
|
||||
<Chip {...props} component={Link as any}/>
|
||||
)
|
||||
<Chip {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
|
||||
<MenuItem {...props} component={Link as any}/>
|
||||
)
|
||||
<MenuItem {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const LinkableButton = (props: ILinkableButtonProps) => (
|
||||
<Button {...props} component={Link as any}/>
|
||||
)
|
||||
<Button {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const LinkableFab = (props: ILinkableFabProps) => (
|
||||
<Fab {...props} component={Link as any}/>
|
||||
)
|
||||
<Fab {...props} component={Link as any} />
|
||||
);
|
||||
export const LinkableAvatar = (props: ILinkableAvatarProps) => (
|
||||
<Avatar {...props} component={Link as any}/>
|
||||
)
|
||||
<Avatar {...props} component={Link as any} />
|
||||
);
|
||||
|
||||
export const ProfileRoute = (rest: any, component: Component) => (
|
||||
<Route {...rest} render={props => (
|
||||
<Component {...props}/>
|
||||
)}/>
|
||||
)
|
||||
<Route {...rest} render={props => <Component {...props} />} />
|
||||
);
|
||||
|
||||
export const PrivateRoute = (props: IPrivateRouteProps) => {
|
||||
const { component, render, ...rest } = props;
|
||||
return (<Route {...rest}
|
||||
render={(compProps: any) => (
|
||||
userLoggedIn()?
|
||||
React.createElement(component, compProps):
|
||||
<Redirect to="/welcome"/>
|
||||
)}
|
||||
return (
|
||||
<Route
|
||||
{...rest}
|
||||
render={(compProps: any) =>
|
||||
userLoggedIn() ? (
|
||||
React.createElement(component, compProps)
|
||||
) : (
|
||||
<Redirect to="/welcome" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
);
|
||||
};
|
||||
|
||||
interface IPrivateRouteProps extends RouteProps {
|
||||
component: any
|
||||
component: any;
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
|
@ -14,30 +14,30 @@ import {
|
|||
Link,
|
||||
Tooltip,
|
||||
Button
|
||||
} from '@material-ui/core';
|
||||
} from "@material-ui/core";
|
||||
|
||||
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
|
||||
import ChatIcon from '@material-ui/icons/Chat';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
import AssignmentIcon from '@material-ui/icons/Assignment';
|
||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
||||
import NetworkCheckIcon from '@material-ui/icons/NetworkCheck';
|
||||
import UpdateIcon from '@material-ui/icons/Update';
|
||||
import InfoIcon from '@material-ui/icons/Info';
|
||||
import NotesIcon from '@material-ui/icons/Notes';
|
||||
import CodeIcon from '@material-ui/icons/Code';
|
||||
import TicketAccountIcon from 'mdi-material-ui/TicketAccount';
|
||||
import MastodonIcon from 'mdi-material-ui/Mastodon';
|
||||
import EditIcon from '@material-ui/icons/Edit';
|
||||
import VpnKeyIcon from '@material-ui/icons/VpnKey';
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import ChatIcon from "@material-ui/icons/Chat";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import AssignmentIcon from "@material-ui/icons/Assignment";
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
import NetworkCheckIcon from "@material-ui/icons/NetworkCheck";
|
||||
import UpdateIcon from "@material-ui/icons/Update";
|
||||
import InfoIcon from "@material-ui/icons/Info";
|
||||
import NotesIcon from "@material-ui/icons/Notes";
|
||||
import CodeIcon from "@material-ui/icons/Code";
|
||||
import TicketAccountIcon from "mdi-material-ui/TicketAccount";
|
||||
import MastodonIcon from "mdi-material-ui/Mastodon";
|
||||
import EditIcon from "@material-ui/icons/Edit";
|
||||
import VpnKeyIcon from "@material-ui/icons/VpnKey";
|
||||
|
||||
import {styles} from './PageLayout.styles';
|
||||
import {Instance} from '../types/Instance';
|
||||
import {LinkableIconButton, LinkableAvatar} from '../interfaces/overrides';
|
||||
import Mastodon from 'megalodon';
|
||||
import { UAccount } from '../types/Account';
|
||||
import { getConfig } from '../utilities/settings';
|
||||
import { License, Federation } from '../types/Config';
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { Instance } from "../types/Instance";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import Mastodon from "megalodon";
|
||||
import { UAccount } from "../types/Account";
|
||||
import { getConfig } from "../utilities/settings";
|
||||
import { License, Federation } from "../types/Config";
|
||||
|
||||
interface IAboutPageState {
|
||||
instance?: Instance;
|
||||
|
@ -54,50 +54,55 @@ interface IAboutPageState {
|
|||
}
|
||||
|
||||
class AboutPage extends Component<any, IAboutPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.client = new Mastodon(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"
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.client.get('/instance').then((resp: any) => {
|
||||
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) => {
|
||||
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,
|
||||
developer: config.developer ? config.developer === "true" : false,
|
||||
versionNumber: config.version,
|
||||
brandName: config.branding? config.branding.name: "Hyperspace",
|
||||
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) => {
|
||||
.catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -109,40 +114,86 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
<div
|
||||
className={classes.instanceHeaderPaper}
|
||||
style={{
|
||||
backgroundImage: `url("${this.state.instance && this.state.instance.thumbnail? 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
|
||||
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>
|
||||
<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>
|
||||
{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: ""}/>
|
||||
<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..."
|
||||
}/>
|
||||
<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
|
||||
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
|
||||
to={`/profile/${
|
||||
this.state.instance
|
||||
? this.state.instance.contact_account.id
|
||||
: 0
|
||||
}`}
|
||||
>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>}
|
||||
</ListItem>
|
||||
)}
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<AssignmentIcon/>
|
||||
<AssignmentIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
|
@ -151,8 +202,14 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Open in browser">
|
||||
<IconButton href={localStorage.getItem("baseurl") as string + "/terms"} target="_blank" rel="noreferrer">
|
||||
<OpenInNewIcon/>
|
||||
<IconButton
|
||||
href={
|
||||
(localStorage.getItem("baseurl") as string) + "/terms"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -160,7 +217,7 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<TicketAccountIcon/>
|
||||
<TicketAccountIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
|
@ -169,7 +226,13 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Go to invite settings">
|
||||
<Button href={localStorage.getItem("baseurl") as string + "/invites"} target="_blank" rel="noreferrer">
|
||||
<Button
|
||||
href={
|
||||
(localStorage.getItem("baseurl") as string) + "/invites"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Invite
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
@ -178,57 +241,103 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<MastodonIcon/>
|
||||
<MastodonIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Mastodon version"
|
||||
secondary={this.state.instance? this.state.instance.version: "x.x.x"}
|
||||
secondary={
|
||||
this.state.instance ? this.state.instance.version : "x.x.x"
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
|
||||
<Paper>
|
||||
<div
|
||||
className={classes.instanceHeaderPaper}
|
||||
style={{
|
||||
backgroundImage: `url("${this.state.brandBg? this.state.brandBg: ""}")`
|
||||
backgroundImage: `url("${
|
||||
this.state.brandBg ? this.state.brandBg : ""
|
||||
}")`
|
||||
}}
|
||||
>
|
||||
<div className={classes.instanceToolbar}>
|
||||
{
|
||||
this.state.repository?
|
||||
{this.state.repository ? (
|
||||
<Tooltip title="View source code">
|
||||
<IconButton href={this.state.repository} target="_blank" rel="noreferrer" color="inherit">
|
||||
<CodeIcon/>
|
||||
<IconButton
|
||||
href={this.state.repository}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
color="inherit"
|
||||
>
|
||||
<CodeIcon />
|
||||
</IconButton>
|
||||
</Tooltip>: null
|
||||
}
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</div>
|
||||
<Typography className={classes.instanceHeaderText} variant="h4" component="p">
|
||||
{this.state.brandName? this.state.brandName: "Hyperspace"}
|
||||
<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
|
||||
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"}/>
|
||||
<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
|
||||
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
|
||||
to={`/profile/${
|
||||
this.state.hyperspaceAdmin
|
||||
? this.state.hyperspaceAdmin.id
|
||||
: 0
|
||||
}`}
|
||||
>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -236,14 +345,21 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<NotesIcon/>
|
||||
<NotesIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="License" secondary={this.state.license.name}/>
|
||||
<ListItemText
|
||||
primary="License"
|
||||
secondary={this.state.license.name}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title = "View license">
|
||||
<IconButton href={this.state.license.url} target="_blank" rel="noreferrer">
|
||||
<OpenInNewIcon/>
|
||||
<Tooltip title="View license">
|
||||
<IconButton
|
||||
href={this.state.license.url}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -251,66 +367,133 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<UpdateIcon/>
|
||||
<UpdateIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Release channel" secondary={
|
||||
this.state?
|
||||
this.state.developer?
|
||||
"Developer":
|
||||
"Release":
|
||||
"Loading..."
|
||||
}/>
|
||||
<ListItemText
|
||||
primary="Release channel"
|
||||
secondary={
|
||||
this.state
|
||||
? this.state.developer
|
||||
? "Developer"
|
||||
: "Release"
|
||||
: "Loading..."
|
||||
}
|
||||
/>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<InfoIcon/>
|
||||
<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)": ""}`}/>
|
||||
<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/>
|
||||
<br />
|
||||
<ListSubheader>Federation status</ListSubheader>
|
||||
<Paper>
|
||||
<List className={classes.pageListConstraints}>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Avatar>
|
||||
<NetworkCheckIcon/>
|
||||
<NetworkCheckIcon />
|
||||
</Avatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="General federation" secondary={this.state.federation && this.state.federation.enablePublicTimeline? "This instance is federated.": "This instance is not federated."}/>
|
||||
<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/>
|
||||
<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."}/>
|
||||
<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/>
|
||||
<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."}/>
|
||||
<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/>
|
||||
<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>
|
||||
<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);
|
|
@ -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,6 +1,7 @@
|
|||
import { Theme, createStyles } from "@material-ui/core";
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
export const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
dialog: {
|
||||
minHeight: 400
|
||||
},
|
||||
|
@ -18,21 +19,21 @@ export const styles = (theme: Theme) => createStyles({
|
|||
verticalAlign: "text-bottom"
|
||||
},
|
||||
composeAttachmentArea: {
|
||||
display: 'flex',
|
||||
flexWrap: 'wrap',
|
||||
justifyContent: 'space-around',
|
||||
overflow: 'hidden'
|
||||
display: "flex",
|
||||
flexWrap: "wrap",
|
||||
justifyContent: "space-around",
|
||||
overflow: "hidden"
|
||||
},
|
||||
composeAttachmentAreaGridList: {
|
||||
height: 250,
|
||||
width: '100%'
|
||||
width: "100%"
|
||||
},
|
||||
composeEmoji: {
|
||||
marginTop: theme.spacing.unit * 8
|
||||
},
|
||||
desktopOnly: {
|
||||
display: "none",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "block"
|
||||
}
|
||||
},
|
||||
|
@ -45,4 +46,4 @@ export const styles = (theme: Theme) => createStyles({
|
|||
pollWizardFlexGrow: {
|
||||
flexGrow: 1
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,26 +1,45 @@
|
|||
import React, {Component} from 'react';
|
||||
import { Dialog, DialogContent, DialogActions, withStyles, Button, CardHeader, Avatar, TextField, Toolbar, IconButton, Fade, Typography, Tooltip, Menu, MenuItem, GridList, ListSubheader, GridListTile } from '@material-ui/core';
|
||||
import {parse as parseParams, ParsedQuery} from 'query-string';
|
||||
import {styles} from './Compose.styles';
|
||||
import { UAccount } from '../types/Account';
|
||||
import { Visibility } from '../types/Visibility';
|
||||
import CameraAltIcon from '@material-ui/icons/CameraAlt';
|
||||
import TagFacesIcon from '@material-ui/icons/TagFaces';
|
||||
import HowToVoteIcon from '@material-ui/icons/HowToVote';
|
||||
import VisibilityIcon from '@material-ui/icons/Visibility';
|
||||
import WarningIcon from '@material-ui/icons/Warning';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked';
|
||||
import Mastodon from 'megalodon';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { Attachment } from '../types/Attachment';
|
||||
import { PollWizard, PollWizardOption } from '../types/Poll';
|
||||
import filedialog from 'file-dialog';
|
||||
import ComposeMediaAttachment from '../components/ComposeMediaAttachment';
|
||||
import EmojiPicker from '../components/EmojiPicker';
|
||||
import { DateTimePicker, MuiPickersUtilsProvider } from 'material-ui-pickers';
|
||||
import MomentUtils from '@date-io/moment';
|
||||
import { getUserDefaultVisibility, getConfig } from '../utilities/settings';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogActions,
|
||||
withStyles,
|
||||
Button,
|
||||
CardHeader,
|
||||
Avatar,
|
||||
TextField,
|
||||
Toolbar,
|
||||
IconButton,
|
||||
Fade,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Menu,
|
||||
MenuItem,
|
||||
GridList,
|
||||
ListSubheader,
|
||||
GridListTile
|
||||
} from "@material-ui/core";
|
||||
import { parse as parseParams, ParsedQuery } from "query-string";
|
||||
import { styles } from "./Compose.styles";
|
||||
import { UAccount } from "../types/Account";
|
||||
import { Visibility } from "../types/Visibility";
|
||||
import CameraAltIcon from "@material-ui/icons/CameraAlt";
|
||||
import TagFacesIcon from "@material-ui/icons/TagFaces";
|
||||
import HowToVoteIcon from "@material-ui/icons/HowToVote";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import WarningIcon from "@material-ui/icons/Warning";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import RadioButtonCheckedIcon from "@material-ui/icons/RadioButtonChecked";
|
||||
import Mastodon from "megalodon";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { Attachment } from "../types/Attachment";
|
||||
import { PollWizard, PollWizardOption } from "../types/Poll";
|
||||
import filedialog from "file-dialog";
|
||||
import ComposeMediaAttachment from "../components/ComposeMediaAttachment";
|
||||
import EmojiPicker from "../components/EmojiPicker";
|
||||
import { DateTimePicker, MuiPickersUtilsProvider } from "material-ui-pickers";
|
||||
import MomentUtils from "@date-io/moment";
|
||||
import { getUserDefaultVisibility, getConfig } from "../utilities/settings";
|
||||
|
||||
interface IComposerState {
|
||||
account: UAccount;
|
||||
|
@ -40,29 +59,31 @@ interface IComposerState {
|
|||
}
|
||||
|
||||
class Composer extends Component<any, IComposerState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.client = new Mastodon(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 = {
|
||||
account: JSON.parse(localStorage.getItem('account') as string),
|
||||
account: JSON.parse(localStorage.getItem("account") as string),
|
||||
visibility: getUserDefaultVisibility(),
|
||||
sensitive: false,
|
||||
visibilityMenu: false,
|
||||
text: '',
|
||||
text: "",
|
||||
remainingChars: 500,
|
||||
showEmojis: false,
|
||||
federated: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
let state = this.getComposerParams(this.props);
|
||||
let text = state.acct? `@${state.acct}: `: '';
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
getConfig().then((config: any) => {
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts,
|
||||
|
@ -72,26 +93,24 @@ class Composer extends Component<any, IComposerState> {
|
|||
text,
|
||||
remainingChars: 500 - text.length
|
||||
});
|
||||
})
|
||||
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
let state = this.getComposerParams(props);
|
||||
let text = state.acct? `@${state.acct}: `: '';
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
this.setState({
|
||||
reply: state.reply,
|
||||
acct: state.acct,
|
||||
visibility: state.visibility,
|
||||
text,
|
||||
remainingChars: 500 - text.length
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
checkComposerParams(location?: string): ParsedQuery {
|
||||
let params = "";
|
||||
if (location !== undefined && typeof(location) === "string") {
|
||||
if (location !== undefined && typeof location === "string") {
|
||||
params = location.replace("#/compose", "");
|
||||
} else {
|
||||
params = window.location.hash.replace("#/compose", "");
|
||||
|
@ -103,7 +122,7 @@ class Composer extends Component<any, IComposerState> {
|
|||
let params = this.checkComposerParams(props.location);
|
||||
let reply: string = "";
|
||||
let acct: string = "";
|
||||
let visibility= this.state.visibility;
|
||||
let visibility = this.state.visibility;
|
||||
|
||||
if (params.reply) {
|
||||
reply = params.reply.toString();
|
||||
|
@ -118,7 +137,7 @@ class Composer extends Component<any, IComposerState> {
|
|||
reply,
|
||||
acct,
|
||||
visibility
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
updateTextFromField(text: string) {
|
||||
|
@ -137,11 +156,17 @@ class Composer extends Component<any, IComposerState> {
|
|||
filedialog({
|
||||
multiple: false,
|
||||
accept: "image/*, video/*"
|
||||
}).then((media: FileList) => {
|
||||
})
|
||||
.then((media: FileList) => {
|
||||
let mediaForm = new FormData();
|
||||
mediaForm.append('file', media[0]);
|
||||
this.props.enqueueSnackbar("Uploading media...", { persist: true, key: "media-upload" })
|
||||
this.client.post('/media', mediaForm).then((resp: any) => {
|
||||
mediaForm.append("file", media[0]);
|
||||
this.props.enqueueSnackbar("Uploading media...", {
|
||||
persist: true,
|
||||
key: "media-upload"
|
||||
});
|
||||
this.client
|
||||
.post("/media", mediaForm)
|
||||
.then((resp: any) => {
|
||||
let attachment: Attachment = resp.data;
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
|
@ -151,13 +176,19 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
this.setState({ attachments });
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar('Media uploaded.');
|
||||
}).catch((err: Error) => {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar("Couldn't upload media: " + err.name, { variant: "error" });
|
||||
this.props.enqueueSnackbar("Media uploaded.");
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't get media: " + err.name, { variant: "error" });
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar("Couldn't upload media: " + err.name, {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't get media: " + err.name, {
|
||||
variant: "error"
|
||||
});
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
@ -179,7 +210,7 @@ class Composer extends Component<any, IComposerState> {
|
|||
if (attach.id === attachment.id && attachments) {
|
||||
attachments[attachments.indexOf(attach)] = attachment;
|
||||
}
|
||||
})
|
||||
});
|
||||
this.setState({ attachments });
|
||||
}
|
||||
}
|
||||
|
@ -192,20 +223,20 @@ class Composer extends Component<any, IComposerState> {
|
|||
attachments.splice(attachments.indexOf(attach), 1);
|
||||
}
|
||||
this.setState({ attachments });
|
||||
})
|
||||
});
|
||||
this.props.enqueueSnackbar("Attachment removed.");
|
||||
}
|
||||
}
|
||||
|
||||
insertEmoji(e: any) {
|
||||
if (e.custom) {
|
||||
let text = this.state.text + e.colons
|
||||
let text = this.state.text + e.colons;
|
||||
this.setState({
|
||||
text,
|
||||
remainingChars: 500 - text.length
|
||||
});
|
||||
} else {
|
||||
let text = this.state.text + e.native
|
||||
let text = this.state.text + e.native;
|
||||
this.setState({
|
||||
text,
|
||||
remainingChars: 500 - text.length
|
||||
|
@ -218,12 +249,12 @@ class Composer extends Component<any, IComposerState> {
|
|||
let expiration = new Date();
|
||||
let current = new Date();
|
||||
expiration.setMinutes(expiration.getMinutes() + 30);
|
||||
let expiryDifference = (expiration.getTime() - current.getTime() / 1000);
|
||||
let expiryDifference = expiration.getTime() - current.getTime() / 1000;
|
||||
let temporaryPoll: PollWizard = {
|
||||
expires_at: expiryDifference.toString(),
|
||||
multiple: false,
|
||||
options: [{title: 'Option 1'}, {title: 'Option 2'}]
|
||||
}
|
||||
options: [{ title: "Option 1" }, { title: "Option 2" }]
|
||||
};
|
||||
this.setState({
|
||||
poll: temporaryPoll,
|
||||
pollExpiresDate: expiration
|
||||
|
@ -233,7 +264,7 @@ class Composer extends Component<any, IComposerState> {
|
|||
|
||||
addPollItem() {
|
||||
if (this.state.poll !== undefined && this.state.poll.options.length < 4) {
|
||||
let newOption = {title: 'New option'}
|
||||
let newOption = { title: "New option" };
|
||||
let options = this.state.poll.options;
|
||||
let poll = this.state.poll;
|
||||
options.push(newOption);
|
||||
|
@ -241,9 +272,12 @@ class Composer extends Component<any, IComposerState> {
|
|||
poll.multiple = true;
|
||||
this.setState({
|
||||
poll: poll
|
||||
})
|
||||
});
|
||||
} else if (this.state.poll && this.state.poll.options.length == 4) {
|
||||
this.props.enqueueSnackbar("You've reached the options limit in your poll.", { variant: 'error' })
|
||||
this.props.enqueueSnackbar(
|
||||
"You've reached the options limit in your poll.",
|
||||
{ variant: "error" }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -260,7 +294,7 @@ class Composer extends Component<any, IComposerState> {
|
|||
this.setState({
|
||||
poll: poll
|
||||
});
|
||||
this.props.enqueueSnackbar('Option edited.');
|
||||
this.props.enqueueSnackbar("Option edited.");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -279,9 +313,11 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
this.setState({
|
||||
poll: poll
|
||||
})
|
||||
});
|
||||
} else if (this.state.poll && this.state.poll.options.length <= 2) {
|
||||
this.props.enqueueSnackbar('Polls must have at least two items.', { variant: 'error'} );
|
||||
this.props.enqueueSnackbar("Polls must have at least two items.", {
|
||||
variant: "error"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -290,14 +326,17 @@ class Composer extends Component<any, IComposerState> {
|
|||
let newDate = new Date(date);
|
||||
let poll = this.state.poll;
|
||||
if (poll) {
|
||||
let expiry = ((newDate.getTime() - currentDate.getTime()) / 1000);
|
||||
let expiry = (newDate.getTime() - currentDate.getTime()) / 1000;
|
||||
console.log(expiry);
|
||||
if (expiry >= 1800) {
|
||||
poll.expires_at = expiry.toString();
|
||||
this.setState({ poll, pollExpiresDate: date });
|
||||
this.props.enqueueSnackbar("Expiration updated.")
|
||||
this.props.enqueueSnackbar("Expiration updated.");
|
||||
} else {
|
||||
this.props.enqueueSnackbar("Expiration is too small (min. 30 minutes).", { variant: 'error' });
|
||||
this.props.enqueueSnackbar(
|
||||
"Expiration is too small (min. 30 minutes).",
|
||||
{ variant: "error" }
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -319,27 +358,32 @@ class Composer extends Component<any, IComposerState> {
|
|||
if (this.state.poll) {
|
||||
this.state.poll.options.forEach((option: PollWizardOption) => {
|
||||
pollOptions.push(option.title);
|
||||
})
|
||||
});
|
||||
}
|
||||
this.client.post('/statuses', {
|
||||
this.client
|
||||
.post("/statuses", {
|
||||
status: this.state.text,
|
||||
media_ids: this.getOnlyMediaIds(),
|
||||
visibility: this.state.visibility,
|
||||
sensitive: this.state.sensitive,
|
||||
spoiler_text: this.state.sensitiveText,
|
||||
in_reply_to_id: this.state.reply,
|
||||
poll: this.state.poll? {
|
||||
poll: this.state.poll
|
||||
? {
|
||||
options: pollOptions,
|
||||
expires_in: this.state.poll.expires_at,
|
||||
multiple: this.state.poll.multiple
|
||||
}: null
|
||||
}).then(() => {
|
||||
this.props.enqueueSnackbar('Posted!');
|
||||
}
|
||||
: null
|
||||
})
|
||||
.then(() => {
|
||||
this.props.enqueueSnackbar("Posted!");
|
||||
window.history.back();
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
||||
console.log(err.message);
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleSensitive() {
|
||||
|
@ -355,37 +399,46 @@ class Composer extends Component<any, IComposerState> {
|
|||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
console.log(this.state);
|
||||
|
||||
return (
|
||||
<Dialog open={true} maxWidth="sm" fullWidth={true} className={classes.dialog} onClose={() => window.history.back()}>
|
||||
<Dialog
|
||||
open={true}
|
||||
maxWidth="sm"
|
||||
fullWidth={true}
|
||||
className={classes.dialog}
|
||||
onClose={() => window.history.back()}
|
||||
>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar src={this.state.account.avatar_static} />
|
||||
}
|
||||
avatar={<Avatar src={this.state.account.avatar_static} />}
|
||||
title={`${this.state.account.display_name} (@${this.state.account.acct})`}
|
||||
subheader={this.state.visibility.charAt(0).toUpperCase() + this.state.visibility.substr(1)}
|
||||
subheader={
|
||||
this.state.visibility.charAt(0).toUpperCase() +
|
||||
this.state.visibility.substr(1)
|
||||
}
|
||||
/>
|
||||
<DialogContent className={classes.dialogContent}>
|
||||
{
|
||||
this.state.sensitive?
|
||||
{this.state.sensitive ? (
|
||||
<Fade in={this.state.sensitive}>
|
||||
<TextField
|
||||
variant="outlined"
|
||||
fullWidth
|
||||
label="Content warning"
|
||||
margin="dense"
|
||||
onChange={(event) => this.updateWarningFromField(event.target.value)}
|
||||
onChange={event =>
|
||||
this.updateWarningFromField(event.target.value)
|
||||
}
|
||||
></TextField>
|
||||
</Fade>: null
|
||||
}
|
||||
{
|
||||
this.state.visibility === "direct"?
|
||||
<Typography variant="caption" >
|
||||
<WarningIcon className={classes.warningCaption}/> Don't forget to add the usernames of the accounts you want to message in your post.
|
||||
</Typography>: null
|
||||
}
|
||||
</Fade>
|
||||
) : null}
|
||||
{this.state.visibility === "direct" ? (
|
||||
<Typography variant="caption">
|
||||
<WarningIcon className={classes.warningCaption} /> Don't forget to
|
||||
add the usernames of the accounts you want to message in your
|
||||
post.
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<TextField
|
||||
variant="outlined"
|
||||
|
@ -393,65 +446,97 @@ class Composer extends Component<any, IComposerState> {
|
|||
fullWidth
|
||||
placeholder="What's on your mind?"
|
||||
margin="normal"
|
||||
onChange={(event) => this.updateTextFromField(event.target.value)}
|
||||
onKeyDown={(event) => this.postViaKeyboard(event)}
|
||||
inputProps = {
|
||||
{
|
||||
onChange={event => this.updateTextFromField(event.target.value)}
|
||||
onKeyDown={event => this.postViaKeyboard(event)}
|
||||
inputProps={{
|
||||
maxLength: 500
|
||||
}
|
||||
}
|
||||
}}
|
||||
value={this.state.text}
|
||||
/>
|
||||
<Typography variant="caption" className={this.state.remainingChars <= 100? classes.charsReachingLimit: null}>
|
||||
{`${this.state.remainingChars} character${this.state.remainingChars === 1? '': 's'} remaining`}
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={
|
||||
this.state.remainingChars <= 100
|
||||
? classes.charsReachingLimit
|
||||
: null
|
||||
}
|
||||
>
|
||||
{`${this.state.remainingChars} character${
|
||||
this.state.remainingChars === 1 ? "" : "s"
|
||||
} remaining`}
|
||||
</Typography>
|
||||
{
|
||||
this.state.attachments && this.state.attachments.length > 0?
|
||||
{this.state.attachments && this.state.attachments.length > 0 ? (
|
||||
<div className={classes.composeAttachmentArea}>
|
||||
<GridList cellHeight={48} className={classes.composeAttachmentAreaGridList}>
|
||||
<GridListTile key="Subheader-composer" cols={2} style={{ height: 'auto' }}>
|
||||
<GridList
|
||||
cellHeight={48}
|
||||
className={classes.composeAttachmentAreaGridList}
|
||||
>
|
||||
<GridListTile
|
||||
key="Subheader-composer"
|
||||
cols={2}
|
||||
style={{ height: "auto" }}
|
||||
>
|
||||
<ListSubheader>Attachments</ListSubheader>
|
||||
</GridListTile>
|
||||
{
|
||||
this.state.attachments.map((attachment: Attachment) => {
|
||||
let c = <ComposeMediaAttachment
|
||||
{this.state.attachments.map((attachment: Attachment) => {
|
||||
let c = (
|
||||
<ComposeMediaAttachment
|
||||
client={this.client}
|
||||
attachment={attachment}
|
||||
onAttachmentUpdate={(attachment: Attachment) => this.fetchAttachmentAfterUpdate(attachment)}
|
||||
onDeleteCallback={(attachment: Attachment) => this.deleteMediaAttachment(attachment)}
|
||||
/>;
|
||||
return (c);
|
||||
})
|
||||
onAttachmentUpdate={(attachment: Attachment) =>
|
||||
this.fetchAttachmentAfterUpdate(attachment)
|
||||
}
|
||||
onDeleteCallback={(attachment: Attachment) =>
|
||||
this.deleteMediaAttachment(attachment)
|
||||
}
|
||||
/>
|
||||
);
|
||||
return c;
|
||||
})}
|
||||
</GridList>
|
||||
</div>: null
|
||||
}
|
||||
{
|
||||
this.state.poll?
|
||||
<div style={{ marginTop: 4}}>
|
||||
|
||||
{
|
||||
this.state.poll?
|
||||
this.state.poll.options.map((option: PollWizardOption, index: number) => {
|
||||
let c = <div style={{ display: "flex" }} key={"compose_option_" + index.toString()}>
|
||||
<RadioButtonCheckedIcon className={classes.pollWizardOptionIcon}/>
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.poll ? (
|
||||
<div style={{ marginTop: 4 }}>
|
||||
{this.state.poll
|
||||
? this.state.poll.options.map(
|
||||
(option: PollWizardOption, index: number) => {
|
||||
let c = (
|
||||
<div
|
||||
style={{ display: "flex" }}
|
||||
key={"compose_option_" + index.toString()}
|
||||
>
|
||||
<RadioButtonCheckedIcon
|
||||
className={classes.pollWizardOptionIcon}
|
||||
/>
|
||||
<TextField
|
||||
onBlur={(event: any) => this.editPollItem(index, event)}
|
||||
defaultValue={option.title}/>
|
||||
<div className={classes.pollWizardFlexGrow}/>
|
||||
onBlur={(event: any) =>
|
||||
this.editPollItem(index, event)
|
||||
}
|
||||
defaultValue={option.title}
|
||||
/>
|
||||
<div className={classes.pollWizardFlexGrow} />
|
||||
<Tooltip title="Remove poll option">
|
||||
<IconButton onClick={() => this.removePollItem(option.title)}>
|
||||
<DeleteIcon/>
|
||||
<IconButton
|
||||
onClick={() => this.removePollItem(option.title)}
|
||||
>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
return c;
|
||||
}): null
|
||||
}
|
||||
<div style={{ display: "flex"}}>
|
||||
)
|
||||
: null}
|
||||
<div style={{ display: "flex" }}>
|
||||
<MuiPickersUtilsProvider utils={MomentUtils}>
|
||||
<DateTimePicker
|
||||
value={this.state.pollExpiresDate? this.state.pollExpiresDate: new Date()}
|
||||
value={
|
||||
this.state.pollExpiresDate
|
||||
? this.state.pollExpiresDate
|
||||
: new Date()
|
||||
}
|
||||
onChange={(date: any) => {
|
||||
this.setPollExpires(date.toISOString());
|
||||
}}
|
||||
|
@ -459,62 +544,96 @@ class Composer extends Component<any, IComposerState> {
|
|||
disablePast
|
||||
/>
|
||||
</MuiPickersUtilsProvider>
|
||||
<div className={classes.pollWizardFlexGrow}/>
|
||||
<div className={classes.pollWizardFlexGrow} />
|
||||
<Button onClick={() => this.addPollItem()}>Add Option</Button>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
<Toolbar className={classes.dialogActions}>
|
||||
<Tooltip title="Add photos or videos">
|
||||
<IconButton disabled={this.state.poll !== undefined} onClick={() => this.uploadMedia()} id="compose-media">
|
||||
<CameraAltIcon/>
|
||||
<IconButton
|
||||
disabled={this.state.poll !== undefined}
|
||||
onClick={() => this.uploadMedia()}
|
||||
id="compose-media"
|
||||
>
|
||||
<CameraAltIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Insert emoji">
|
||||
<IconButton id="compose-emoji" onClick={() => this.toggleEmojis()} className={classes.desktopOnly}>
|
||||
<TagFacesIcon/>
|
||||
<IconButton
|
||||
id="compose-emoji"
|
||||
onClick={() => this.toggleEmojis()}
|
||||
className={classes.desktopOnly}
|
||||
>
|
||||
<TagFacesIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu
|
||||
open={this.state.showEmojis}
|
||||
anchorEl={document.getElementById('compose-emoji')}
|
||||
anchorEl={document.getElementById("compose-emoji")}
|
||||
onClose={() => this.toggleEmojis()}
|
||||
className={classes.composeEmoji}
|
||||
>
|
||||
<EmojiPicker onGetEmoji={(emoji: any) => this.insertEmoji(emoji)}/>
|
||||
<EmojiPicker onGetEmoji={(emoji: any) => this.insertEmoji(emoji)} />
|
||||
</Menu>
|
||||
<Tooltip title="Add/remove a poll">
|
||||
<IconButton disabled={this.state.attachments && this.state.attachments.length > 0} id="compose-poll" onClick={() => {
|
||||
this.state.poll?
|
||||
this.removePoll():
|
||||
this.createPoll()
|
||||
}}>
|
||||
<HowToVoteIcon/>
|
||||
<IconButton
|
||||
disabled={
|
||||
this.state.attachments && this.state.attachments.length > 0
|
||||
}
|
||||
id="compose-poll"
|
||||
onClick={() => {
|
||||
this.state.poll ? this.removePoll() : this.createPoll();
|
||||
}}
|
||||
>
|
||||
<HowToVoteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Change who sees your post">
|
||||
<IconButton id="compose-visibility" onClick={() => this.toggleVisibilityMenu()}>
|
||||
<VisibilityIcon/>
|
||||
<IconButton
|
||||
id="compose-visibility"
|
||||
onClick={() => this.toggleVisibilityMenu()}
|
||||
>
|
||||
<VisibilityIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Set a content warning">
|
||||
<IconButton onClick={() => this.toggleSensitive()} id="compose-warning">
|
||||
<WarningIcon/>
|
||||
<IconButton
|
||||
onClick={() => this.toggleSensitive()}
|
||||
id="compose-warning"
|
||||
>
|
||||
<WarningIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Menu open={this.state.visibilityMenu} anchorEl={document.getElementById('compose-visibility')} onClose={() => this.toggleVisibilityMenu()}>
|
||||
<MenuItem onClick={() => this.changeVisibility('direct')}>Direct (direct message)</MenuItem>
|
||||
<MenuItem onClick={() => this.changeVisibility('private')}>Private (followers only)</MenuItem>
|
||||
<MenuItem onClick={() => this.changeVisibility('unlisted')}>Unlisted</MenuItem>
|
||||
{this.state.federated? <MenuItem onClick={() => this.changeVisibility('public')}>Public</MenuItem>: null}
|
||||
<Menu
|
||||
open={this.state.visibilityMenu}
|
||||
anchorEl={document.getElementById("compose-visibility")}
|
||||
onClose={() => this.toggleVisibilityMenu()}
|
||||
>
|
||||
<MenuItem onClick={() => this.changeVisibility("direct")}>
|
||||
Direct (direct message)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => this.changeVisibility("private")}>
|
||||
Private (followers only)
|
||||
</MenuItem>
|
||||
<MenuItem onClick={() => this.changeVisibility("unlisted")}>
|
||||
Unlisted
|
||||
</MenuItem>
|
||||
{this.state.federated ? (
|
||||
<MenuItem onClick={() => this.changeVisibility("public")}>
|
||||
Public
|
||||
</MenuItem>
|
||||
) : null}
|
||||
</Menu>
|
||||
</Toolbar>
|
||||
<DialogActions>
|
||||
<Button color="secondary" onClick={() => this.post()}>Post</Button>
|
||||
<Button color="secondary" onClick={() => this.post()}>
|
||||
Post
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper} from '@material-ui/core';
|
||||
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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Paper
|
||||
} from "@material-ui/core";
|
||||
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 {
|
||||
posts?: [Status];
|
||||
|
@ -16,9 +21,7 @@ interface IConversationPageState {
|
|||
conversationId: string;
|
||||
}
|
||||
|
||||
|
||||
class Conversation extends Component<any, IConversationPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
|
@ -28,30 +31,42 @@ class Conversation extends Component<any, IConversationPageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
conversationId: props.match.params.conversationId
|
||||
}
|
||||
|
||||
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,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
}
|
||||
|
||||
getContext() {
|
||||
this.client.get(`/statuses/${this.state.conversationId}`).then((resp: any) => {
|
||||
this.client
|
||||
.get(`/statuses/${this.state.conversationId}`)
|
||||
.then((resp: any) => {
|
||||
let result: Status = resp.data;
|
||||
this.setState({ posts: [result] });
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
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) => {
|
||||
});
|
||||
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);
|
||||
array = array
|
||||
.concat(context.ancestors)
|
||||
.concat(posts)
|
||||
.concat(context.descendants);
|
||||
}
|
||||
this.setState({
|
||||
posts: array as [Status],
|
||||
|
@ -59,59 +74,75 @@ class Conversation extends Component<any, IConversationPageState> {
|
|||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.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.getContext();
|
||||
}
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.getContext()
|
||||
this.getContext();
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
const where: HTMLElement | null = document.getElementById(`post_${this.state.conversationId}`);
|
||||
if (where && this.state.posts && this.state.posts[0].id !== this.state.conversationId) {
|
||||
const where: HTMLElement | null = document.getElementById(
|
||||
`post_${this.state.conversationId}`
|
||||
);
|
||||
if (
|
||||
where &&
|
||||
this.state.posts &&
|
||||
this.state.posts[0].id !== this.state.conversationId
|
||||
) {
|
||||
window.scrollTo(0, where.getBoundingClientRect().top);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{ this.state.posts?
|
||||
{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?
|
||||
{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/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper, Button, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
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 {
|
||||
posts?: [Status];
|
||||
|
@ -16,9 +25,7 @@ interface IHomePageState {
|
|||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class HomePage extends Component<any, IHomePageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
||||
|
@ -28,68 +35,74 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, 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"
|
||||
);
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/home', {limit: 40}).then((resp: any) => {
|
||||
this.streamListener.on("connect", () => {
|
||||
this.client
|
||||
.get("/timelines/home", { limit: 40 })
|
||||
.then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
})
|
||||
}).catch((resp: any) => {
|
||||
.catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
});
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
this.streamListener.on("update", (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
||||
if (queue !== null && queue !== undefined) {
|
||||
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;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
})
|
||||
});
|
||||
this.setState({ posts });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('error', (err: Error) => {
|
||||
this.streamListener.on("error", (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
this.streamListener.on("heartbeat", () => {});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -102,14 +115,19 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
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) => {
|
||||
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) => {
|
||||
|
@ -119,37 +137,39 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
<ArrowUpwardIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
||||
label={`View ${this.state.backlogPosts.length} new post${
|
||||
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||
}`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
|
@ -157,34 +177,46 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
</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?
|
||||
{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/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper, Button, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
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 {
|
||||
posts?: [Status];
|
||||
|
@ -16,9 +25,7 @@ interface ILocalPageState {
|
|||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class LocalPage extends Component<any, ILocalPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
||||
|
@ -28,68 +35,74 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, 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"
|
||||
);
|
||||
this.streamListener = this.client.stream("/streaming/public/local");
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => {
|
||||
this.streamListener.on("connect", () => {
|
||||
this.client
|
||||
.get("/timelines/public", { limit: 40, local: true })
|
||||
.then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
})
|
||||
}).catch((resp: any) => {
|
||||
.catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
});
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
this.streamListener.on("update", (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
||||
if (queue !== null && queue !== undefined) {
|
||||
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;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
})
|
||||
});
|
||||
this.setState({ posts });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('error', (err: Error) => {
|
||||
this.streamListener.on("error", (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
this.streamListener.on("heartbeat", () => {});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -102,14 +115,20 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
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) => {
|
||||
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) => {
|
||||
|
@ -119,37 +138,39 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
<ArrowUpwardIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
||||
label={`View ${this.state.backlogPosts.length} new post${
|
||||
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||
}`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
|
@ -157,34 +178,46 @@ class LocalPage extends Component<any, ILocalPageState> {
|
|||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>
|
||||
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?
|
||||
<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/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,11 +1,23 @@
|
|||
import React, { Component } from 'react';
|
||||
import {withStyles, ListSubheader, Paper, List, ListItem, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
ListSubheader,
|
||||
Paper,
|
||||
List,
|
||||
ListItem,
|
||||
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 {
|
||||
posts?: [Status];
|
||||
|
@ -16,23 +28,24 @@ interface IMessagesState {
|
|||
}
|
||||
|
||||
class MessagesPage extends Component<any, IMessagesState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
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,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.client.get('/conversations')
|
||||
.then((resp) => {
|
||||
let data:any = resp.data;
|
||||
this.client.get("/conversations").then(resp => {
|
||||
let data: any = resp.data;
|
||||
let messages: any = [];
|
||||
|
||||
data.forEach((message: any) => {
|
||||
|
@ -50,10 +63,10 @@ class MessagesPage extends Component<any, IMessagesState> {
|
|||
}
|
||||
|
||||
removeHTMLContent(text: string) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = text;
|
||||
let innerContent = div.textContent || div.innerText || "";
|
||||
innerContent = innerContent.slice(0, 100) + "..."
|
||||
innerContent = innerContent.slice(0, 100) + "...";
|
||||
return innerContent;
|
||||
}
|
||||
|
||||
|
@ -61,46 +74,56 @@ class MessagesPage extends Component<any, IMessagesState> {
|
|||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{
|
||||
this.state.viewDidLoad?
|
||||
{this.state.viewDidLoad ? (
|
||||
<div className={classes.pageListContsraints}>
|
||||
<ListSubheader>Recent messages</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{
|
||||
this.state.posts?
|
||||
this.state.posts.map((message: Status) => {
|
||||
{this.state.posts
|
||||
? this.state.posts.map((message: Status) => {
|
||||
return (
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar to={`/profile/${message.account.id}`} alt={message.account.username} src={message.account.avatar_static}>
|
||||
<PersonIcon/>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${message.account.id}`}
|
||||
alt={message.account.username}
|
||||
src={message.account.avatar_static}
|
||||
>
|
||||
<PersonIcon />
|
||||
</LinkableAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={message.account.display_name || "@" + message.account.acct} secondary={this.removeHTMLContent(message.content)}/>
|
||||
<ListItemText
|
||||
primary={
|
||||
message.account.display_name ||
|
||||
"@" + message.account.acct
|
||||
}
|
||||
secondary={this.removeHTMLContent(message.content)}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View conversation">
|
||||
<LinkableIconButton to={`/conversation/${message.id}`}>
|
||||
<ForumIcon/>
|
||||
<LinkableIconButton
|
||||
to={`/conversation/${message.id}`}
|
||||
>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
)
|
||||
}): null
|
||||
}
|
||||
);
|
||||
})
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
</div>: null
|
||||
}
|
||||
{
|
||||
this.state.viewIsLoading?
|
||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
||||
null
|
||||
}
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
) : null}
|
||||
{this.state.viewIsLoading ? (
|
||||
<div style={{ textAlign: "center" }}>
|
||||
<CircularProgress className={classes.progress} color="primary" />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,24 +1,27 @@
|
|||
import React, {Component} from 'react';
|
||||
import {withStyles, Typography} from '@material-ui/core';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import {LinkableButton} from '../interfaces/overrides';
|
||||
import React, { Component } from "react";
|
||||
import { withStyles, Typography } from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { LinkableButton } from "../interfaces/overrides";
|
||||
|
||||
class Missingno extends Component<any, any> {
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
<div>
|
||||
<Typography variant="h4" component="h1"><b>Uh oh!</b></Typography>
|
||||
<Typography variant="h6" component="p">The part of Hyperspace you're looking for isn't here.</Typography>
|
||||
<br/>
|
||||
<Typography variant="h4" component="h1">
|
||||
<b>Uh oh!</b>
|
||||
</Typography>
|
||||
<Typography variant="h6" component="p">
|
||||
The part of Hyperspace you're looking for isn't here.
|
||||
</Typography>
|
||||
<br />
|
||||
<LinkableButton to="/home" color="primary" variant="contained">
|
||||
Go back to home timeline
|
||||
</LinkableButton>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
|
@ -18,19 +18,19 @@ import {
|
|||
DialogContentText,
|
||||
DialogActions,
|
||||
Tooltip
|
||||
} from '@material-ui/core';
|
||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
import DeleteIcon from '@material-ui/icons/Delete';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import { LinkableIconButton, LinkableAvatar } from '../interfaces/overrides';
|
||||
import ForumIcon from '@material-ui/icons/Forum';
|
||||
import ReplyIcon from '@material-ui/icons/Reply';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Notification } from '../types/Notification';
|
||||
import { Account } from '../types/Account';
|
||||
import { withSnackbar } from 'notistack';
|
||||
} from "@material-ui/core";
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
import DeleteIcon from "@material-ui/icons/Delete";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import ForumIcon from "@material-ui/icons/Forum";
|
||||
import ReplyIcon from "@material-ui/icons/Reply";
|
||||
import Mastodon from "megalodon";
|
||||
import { Notification } from "../types/Notification";
|
||||
import { Account } from "../types/Account";
|
||||
import { withSnackbar } from "notistack";
|
||||
|
||||
interface INotificationsPageState {
|
||||
notifications?: [Notification];
|
||||
|
@ -42,36 +42,41 @@ interface INotificationsPageState {
|
|||
}
|
||||
|
||||
class NotificationsPage extends Component<any, INotificationsPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.client = new Mastodon(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 = {
|
||||
viewIsLoading: true,
|
||||
deleteDialogOpen: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.client.get('/notifications').then((resp: any) => {
|
||||
this.client
|
||||
.get("/notifications")
|
||||
.then((resp: any) => {
|
||||
let notifications: [Notification] = resp.data;
|
||||
this.setState({
|
||||
notifications,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewDidLoad: true,
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -79,15 +84,15 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
|
||||
streamNotifications() {
|
||||
this.streamListener = this.client.stream('/streaming/user');
|
||||
this.streamListener = this.client.stream("/streaming/user");
|
||||
|
||||
this.streamListener.on('notification', (notif: Notification) => {
|
||||
this.streamListener.on("notification", (notif: Notification) => {
|
||||
let notifications = this.state.notifications;
|
||||
if (notifications) {
|
||||
notifications.unshift(notif);
|
||||
this.setState({ notifications });
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleDeleteDialog() {
|
||||
|
@ -95,7 +100,7 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
|
||||
removeHTMLContent(text: string) {
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = text;
|
||||
let innerContent = div.textContent || div.innerText || "";
|
||||
if (innerContent.length > 65)
|
||||
|
@ -104,33 +109,45 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
|
||||
removeNotification(id: string) {
|
||||
this.client.post('/notifications/dismiss', {id: id}).then((resp: any) => {
|
||||
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'
|
||||
});
|
||||
}
|
||||
this.setState({ notifications });
|
||||
this.props.enqueueSnackbar("Notification deleted.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't delete notification: " + err.name,
|
||||
{
|
||||
variant: "error"
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
removeAllNotifications() {
|
||||
this.client.post('/notifications/clear').then((resp: any) => {
|
||||
this.setState({ notifications: undefined })
|
||||
this.props.enqueueSnackbar('All notifications deleted.');
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't delete notifications: " + err.name, {
|
||||
variant: 'error'
|
||||
});
|
||||
this.client
|
||||
.post("/notifications/clear")
|
||||
.then((resp: any) => {
|
||||
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) {
|
||||
|
@ -139,24 +156,36 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
let secondary = "";
|
||||
switch (notif.type) {
|
||||
case "follow":
|
||||
primary = `${notif.account.display_name || notif.account.username} is now following you!`;
|
||||
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: "");
|
||||
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: "");
|
||||
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: "");
|
||||
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: "");
|
||||
secondary = this.removeHTMLContent(
|
||||
notif.status ? notif.status.content : ""
|
||||
);
|
||||
} else {
|
||||
primary = "A magical thing happened!";
|
||||
}
|
||||
|
@ -165,11 +194,17 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
return (
|
||||
<ListItem key={notif.id}>
|
||||
<ListItemAvatar>
|
||||
<LinkableAvatar alt={notif.account.username} src={notif.account.avatar_static} to={`/profile/${notif.account.id}`}>
|
||||
<PersonIcon/>
|
||||
<LinkableAvatar
|
||||
alt={notif.account.username}
|
||||
src={notif.account.avatar_static}
|
||||
to={`/profile/${notif.account.id}`}
|
||||
>
|
||||
<PersonIcon />
|
||||
</LinkableAvatar>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={primary} secondary={
|
||||
<ListItemText
|
||||
primary={primary}
|
||||
secondary={
|
||||
<span>
|
||||
<Typography color="textSecondary" className={classes.mobileOnly}>
|
||||
{secondary.slice(0, 35) + "..."}
|
||||
|
@ -178,44 +213,51 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
{secondary}
|
||||
</Typography>
|
||||
</span>
|
||||
}/>
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
{
|
||||
notif.type === "follow"?
|
||||
{notif.type === "follow" ? (
|
||||
<span>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton to={`/profile/${notif.account.id}`}>
|
||||
<AssignmentIndIcon/>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Follow account">
|
||||
<IconButton onClick={() => this.followMember(notif.account)}>
|
||||
<PersonAddIcon/>
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</span>:
|
||||
|
||||
notif.status?
|
||||
</span>
|
||||
) : notif.status ? (
|
||||
<span>
|
||||
<Tooltip title="View conversation">
|
||||
<LinkableIconButton to={`/conversation/${notif.status.id}`}>
|
||||
<ForumIcon/>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
{
|
||||
notif.type === "mention"?
|
||||
{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
|
||||
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>
|
||||
) : null}
|
||||
</span>
|
||||
) : null}
|
||||
<Tooltip title="Remove notification">
|
||||
<IconButton onClick={() => this.removeNotification(notif.id)}>
|
||||
<DeleteIcon/>
|
||||
<DeleteIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -224,73 +266,104 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
}
|
||||
|
||||
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);
|
||||
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?
|
||||
{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>
|
||||
<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)
|
||||
})
|
||||
{this.state.notifications.map(
|
||||
(notification: Notification) => {
|
||||
return this.createNotification(notification);
|
||||
}
|
||||
)}
|
||||
</List>
|
||||
</Paper>
|
||||
</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?
|
||||
<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/>
|
||||
}
|
||||
<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>
|
||||
<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.
|
||||
Are you sure you want to delete all notifications? This action
|
||||
cannot be undone.
|
||||
</DialogContentText>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.toggleDeleteDialog()} color="primary" autoFocus>
|
||||
<Button
|
||||
onClick={() => this.toggleDeleteDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.removeAllNotifications();
|
||||
this.toggleDeleteDialog();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -298,7 +371,6 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(withSnackbar(NotificationsPage));
|
|
@ -2,11 +2,12 @@ import { Theme, createStyles } from "@material-ui/core";
|
|||
import { isDarwinApp } from "../utilities/desktop";
|
||||
import { isAppbarExpanded } from "../utilities/appbar";
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
export const styles = (theme: Theme) =>
|
||||
createStyles({
|
||||
root: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
height: '100%'
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
height: "100%"
|
||||
},
|
||||
pageLayoutConstraints: {
|
||||
marginTop: 72,
|
||||
|
@ -14,87 +15,87 @@ export const styles = (theme: Theme) => createStyles({
|
|||
padding: theme.spacing.unit * 3,
|
||||
paddingLeft: theme.spacing.unit,
|
||||
paddingRight: theme.spacing.unit,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
paddingLeft: theme.spacing.unit * 24,
|
||||
paddingRight: theme.spacing.unit * 24
|
||||
},
|
||||
backgroundColor: theme.palette.background.default,
|
||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
||||
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||
},
|
||||
pageLayoutMaxConstraints: {
|
||||
marginTop: 72,
|
||||
flexGrow: 1,
|
||||
paddingTop: theme.spacing.unit * 2,
|
||||
padding: theme.spacing.unit,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
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,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
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,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
paddingLeft: theme.spacing.unit * 40,
|
||||
paddingRight: theme.spacing.unit * 40,
|
||||
paddingRight: theme.spacing.unit * 40
|
||||
},
|
||||
backgroundColor: theme.palette.background.default,
|
||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
||||
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||
},
|
||||
pageLayoutMinimalConstraints: {
|
||||
flexGrow: 1,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginLeft: 250,
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginLeft: 250
|
||||
},
|
||||
backgroundColor: theme.palette.background.default,
|
||||
minHeight: isDarwinApp()? "100vh": 'auto',
|
||||
minHeight: isDarwinApp() ? "100vh" : "auto"
|
||||
},
|
||||
pageLayoutEmptyTextConstraints: {
|
||||
paddingLeft: theme.spacing.unit * 2,
|
||||
paddingRight: theme.spacing.unit * 2
|
||||
},
|
||||
pageHeroBackground: {
|
||||
position: 'relative',
|
||||
height: 'intrinsic',
|
||||
position: "relative",
|
||||
height: "intrinsic",
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
color: theme.palette.common.white,
|
||||
zIndex: 1,
|
||||
top: isAppbarExpanded()? 80: 64,
|
||||
top: isAppbarExpanded() ? 80 : 64
|
||||
},
|
||||
pageHeroBackgroundImage: {
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
height: '100%',
|
||||
width: '100%',
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
height: "100%",
|
||||
width: "100%",
|
||||
opacity: 0.35,
|
||||
zIndex: -1,
|
||||
filter: 'blur(2px)'
|
||||
filter: "blur(2px)"
|
||||
},
|
||||
pageHeroContent: {
|
||||
padding: 16,
|
||||
paddingTop: 8,
|
||||
textAlign: 'center',
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
paddingLeft: '5%',
|
||||
paddingRight: '5%',
|
||||
textAlign: "center",
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
paddingLeft: "5%",
|
||||
paddingRight: "5%"
|
||||
},
|
||||
position: "relative",
|
||||
zIndex: 1
|
||||
|
@ -102,43 +103,43 @@ export const styles = (theme: Theme) => createStyles({
|
|||
pageHeroToolbar: {
|
||||
position: "absolute",
|
||||
right: theme.spacing.unit * 2,
|
||||
marginTop: -16,
|
||||
marginTop: -16
|
||||
},
|
||||
pageListConstraints: {
|
||||
paddingLeft: theme.spacing.unit,
|
||||
paddingRight: theme.spacing.unit,
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
paddingLeft: theme.spacing.unit * 2,
|
||||
paddingRight: theme.spacing.unit * 2
|
||||
},
|
||||
}
|
||||
//backgroundColor: theme.palette.background.default
|
||||
},
|
||||
profileToolbar: {
|
||||
zIndex: 2,
|
||||
paddingTop: 8,
|
||||
paddingTop: 8
|
||||
},
|
||||
profileContent: {
|
||||
padding: 16,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
paddingLeft: '5%',
|
||||
paddingRight: '5%',
|
||||
[theme.breakpoints.up("md")]: {
|
||||
paddingLeft: "5%",
|
||||
paddingRight: "5%",
|
||||
paddingBottom: 48,
|
||||
paddingTop: 24,
|
||||
paddingTop: 24
|
||||
},
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
position: 'relative',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
position: "relative",
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
display: "flex",
|
||||
paddingBottom: 24,
|
||||
paddingTop: 24,
|
||||
paddingTop: 24
|
||||
},
|
||||
profileAvatar: {
|
||||
width: 64,
|
||||
height: 64,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
width: 128,
|
||||
height: 128,
|
||||
height: 128
|
||||
},
|
||||
backgroundColor: theme.palette.primary.main
|
||||
},
|
||||
|
@ -148,19 +149,19 @@ export const styles = (theme: Theme) => createStyles({
|
|||
pageProfileAvatar: {
|
||||
width: 128,
|
||||
height: 128,
|
||||
marginLeft: 'auto',
|
||||
marginRight: 'auto',
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginBottom: theme.spacing.unit,
|
||||
backgroundColor: theme.palette.primary.main
|
||||
},
|
||||
pageProfileNameEmoji: {
|
||||
height: theme.typography.h4.fontSize,
|
||||
fontWeight: theme.typography.fontWeightMedium,
|
||||
fontWeight: theme.typography.fontWeightMedium
|
||||
},
|
||||
pageProfileStatsDiv: {
|
||||
display: 'inline-flex',
|
||||
display: "inline-flex",
|
||||
marginTop: theme.spacing.unit * 2,
|
||||
marginBottom: theme.spacing.unit * 2,
|
||||
marginBottom: theme.spacing.unit * 2
|
||||
},
|
||||
pageProfileStat: {
|
||||
marginLeft: theme.spacing.unit,
|
||||
|
@ -177,28 +178,28 @@ export const styles = (theme: Theme) => createStyles({
|
|||
paddingRight: theme.spacing.unit,
|
||||
paddingTop: theme.spacing.unit * 12,
|
||||
paddingBottom: theme.spacing.unit * 2,
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
paddingLeft: theme.spacing.unit * 32,
|
||||
paddingRight: theme.spacing.unit * 32
|
||||
},
|
||||
}
|
||||
//backgroundColor: theme.palette.background.default,
|
||||
},
|
||||
errorCard: {
|
||||
padding: theme.spacing.unit * 4,
|
||||
backgroundColor: theme.palette.error.main,
|
||||
backgroundColor: theme.palette.error.main
|
||||
},
|
||||
pageTopChipContainer: {
|
||||
zIndex: 24,
|
||||
position: "fixed",
|
||||
width: '100%'
|
||||
width: "100%"
|
||||
},
|
||||
pageTopChips: {
|
||||
textAlign: 'center',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginRight: '55%'
|
||||
textAlign: "center",
|
||||
[theme.breakpoints.up("md")]: {
|
||||
marginRight: "55%"
|
||||
},
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
marginRight: '50%'
|
||||
[theme.breakpoints.up("xl")]: {
|
||||
marginRight: "50%"
|
||||
}
|
||||
},
|
||||
pageTopChip: {
|
||||
|
@ -206,27 +207,27 @@ export const styles = (theme: Theme) => createStyles({
|
|||
},
|
||||
clearAllButton: {
|
||||
zIndex: 3,
|
||||
position: 'absolute',
|
||||
position: "absolute",
|
||||
right: 24,
|
||||
top: 100,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
[theme.breakpoints.up("md")]: {
|
||||
top: 116,
|
||||
right: theme.spacing.unit * 24,
|
||||
right: theme.spacing.unit * 24
|
||||
}
|
||||
},
|
||||
mobileOnly: {
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'none'
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "none"
|
||||
}
|
||||
},
|
||||
desktopOnly: {
|
||||
display: 'none',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
display: 'block'
|
||||
display: "none",
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
display: "block"
|
||||
}
|
||||
},
|
||||
pageLayoutFooter: {
|
||||
'& a': {
|
||||
"& a": {
|
||||
color: theme.palette.primary.light
|
||||
}
|
||||
},
|
||||
|
@ -235,13 +236,13 @@ export const styles = (theme: Theme) => createStyles({
|
|||
width: 88
|
||||
},
|
||||
youPaper: {
|
||||
padding: theme.spacing.unit * 2,
|
||||
padding: theme.spacing.unit * 2
|
||||
},
|
||||
youGrid: {
|
||||
textAlign: "center",
|
||||
'& *': {
|
||||
"& *": {
|
||||
marginLeft: "auto",
|
||||
marginRight: "auto",
|
||||
marginRight: "auto"
|
||||
}
|
||||
},
|
||||
youGridAvatar: {
|
||||
|
@ -249,7 +250,7 @@ export const styles = (theme: Theme) => createStyles({
|
|||
width: 128
|
||||
},
|
||||
youGridImage: {
|
||||
width: 'auto',
|
||||
width: "auto",
|
||||
height: 128
|
||||
},
|
||||
instanceHeaderPaper: {
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, {Component} from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
Typography,
|
||||
|
@ -15,27 +15,25 @@ import {
|
|||
DialogActions,
|
||||
Toolbar,
|
||||
IconButton
|
||||
} from '@material-ui/core';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Account } from '../types/Account';
|
||||
import { Status } from '../types/Status';
|
||||
import { Relationship } from '../types/Relationship';
|
||||
import Post from '../components/Post';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { LinkableIconButton } from '../interfaces/overrides';
|
||||
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';
|
||||
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import Mastodon from "megalodon";
|
||||
import { Account } from "../types/Account";
|
||||
import { Status } from "../types/Status";
|
||||
import { Relationship } from "../types/Relationship";
|
||||
import Post from "../components/Post";
|
||||
import { withSnackbar } from "notistack";
|
||||
import { LinkableIconButton } from "../interfaces/overrides";
|
||||
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";
|
||||
|
||||
interface IProfilePageState {
|
||||
account?: Account;
|
||||
|
@ -49,60 +47,66 @@ interface IProfilePageState {
|
|||
}
|
||||
|
||||
class ProfilePage extends Component<any, IProfilePageState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
||||
this.client = new Mastodon(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 = {
|
||||
viewIsLoading: true,
|
||||
blockDialogOpen: false
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
toggleBlockDialog() {
|
||||
if (this.state.relationship && !this.state.relationship.blocking)
|
||||
this.setState({ blockDialogOpen: !this.state.blockDialogOpen })
|
||||
else
|
||||
this.toggleBlock()
|
||||
this.setState({ blockDialogOpen: !this.state.blockDialogOpen });
|
||||
else this.toggleBlock();
|
||||
}
|
||||
|
||||
getAccountData(id: string) {
|
||||
this.client.get(`/accounts/${id}`).then((resp: any) => {
|
||||
this.client
|
||||
.get(`/accounts/${id}`)
|
||||
.then((resp: any) => {
|
||||
let profile: Account = resp.data;
|
||||
|
||||
const div = document.createElement('div');
|
||||
const div = document.createElement("div");
|
||||
div.innerHTML = profile.note;
|
||||
profile.note = div.textContent || div.innerText || "";
|
||||
|
||||
this.setState({
|
||||
account: profile
|
||||
});
|
||||
})
|
||||
}).catch((error: Error) => {
|
||||
.catch((error: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
});
|
||||
});
|
||||
this.getRelationships();
|
||||
this.client.get(`/accounts/${id}/statuses`).then((resp: any) => {
|
||||
this.client
|
||||
.get(`/accounts/${id}/statuses`)
|
||||
.then((resp: any) => {
|
||||
this.setState({
|
||||
posts: resp.data,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
})
|
||||
}).catch( (err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -112,41 +116,56 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { match: { params }} = this.props;
|
||||
const {
|
||||
match: { params }
|
||||
} = this.props;
|
||||
this.getAccountData(params.profileId);
|
||||
}
|
||||
|
||||
isItMe(): boolean {
|
||||
if (this.state.account) {
|
||||
return this.state.account.id === JSON.parse(localStorage.getItem('account') as string).id;
|
||||
return (
|
||||
this.state.account.id ===
|
||||
JSON.parse(localStorage.getItem("account") as string).id
|
||||
);
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
getRelationships() {
|
||||
this.client.get("/accounts/relationships", {id: this.props.match.params.profileId }).then((resp: any) => {
|
||||
this.client
|
||||
.get("/accounts/relationships", { id: this.props.match.params.profileId })
|
||||
.then((resp: any) => {
|
||||
let relationship: Relationship = resp.data[0];
|
||||
this.setState({ relationship });
|
||||
}).catch((error: Error) => {
|
||||
})
|
||||
.catch((error: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
const { match: {params}} = this.props;
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
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) => {
|
||||
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'
|
||||
variant: "error"
|
||||
});
|
||||
} else {
|
||||
newPosts.forEach((post: Status) => {
|
||||
|
@ -157,198 +176,321 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
} else {
|
||||
this.props.enqueueSnackbar("Reached end of posts", { variant: 'error'} );
|
||||
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) => {
|
||||
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.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);
|
||||
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) => {
|
||||
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);
|
||||
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() {
|
||||
if (this.state.relationship) {
|
||||
if (this.state.relationship.blocking) {
|
||||
this.client.post(`/accounts/${this.state.account? this.state.account.id: this.props.match.params.profileId}/unblock`).then((resp: any) => {
|
||||
this.client
|
||||
.post(
|
||||
`/accounts/${
|
||||
this.state.account
|
||||
? this.state.account.id
|
||||
: this.props.match.params.profileId
|
||||
}/unblock`
|
||||
)
|
||||
.then((resp: any) => {
|
||||
let relationship: Relationship = resp.data;
|
||||
this.setState({ relationship });
|
||||
this.props.enqueueSnackbar('You are no longer blocking this account.');
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't unblock account: " + err.name, { variant: 'error' });
|
||||
console.error(err.message);
|
||||
this.props.enqueueSnackbar(
|
||||
"You are no longer blocking this account."
|
||||
);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't unblock 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}/block`).then((resp: any) => {
|
||||
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;
|
||||
this.setState({ relationship });
|
||||
this.props.enqueueSnackbar('You are now blocking this account.');
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't block account: " + err.name, { variant: 'error' });
|
||||
console.error(err.message);
|
||||
this.props.enqueueSnackbar("You are now blocking this account.");
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't block account: " + err.name, {
|
||||
variant: "error"
|
||||
});
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return(
|
||||
return (
|
||||
<div className={classes.pageLayoutMinimalConstraints}>
|
||||
<div className={classes.pageHeroBackground}>
|
||||
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
|
||||
<div
|
||||
className={classes.pageHeroBackgroundImage}
|
||||
style={{
|
||||
backgroundImage: this.state.account
|
||||
? `url("${this.state.account.header}")`
|
||||
: `url("")`
|
||||
}}
|
||||
/>
|
||||
<Toolbar className={classes.profileToolbar}>
|
||||
<div className={classes.pageGrow}/>
|
||||
<Tooltip title={
|
||||
this.isItMe()?
|
||||
"You can't follow yourself.":
|
||||
this.state.relationship && this.state.relationship.following?
|
||||
"Unfollow":
|
||||
"Follow"
|
||||
}>
|
||||
<IconButton color={"inherit"} disabled={this.isItMe()} onClick={() => this.toggleFollow()}>
|
||||
{
|
||||
this.isItMe()?
|
||||
<PersonAddDisabledIcon/>:
|
||||
this.state.relationship && this.state.relationship.following?
|
||||
<AccountMinusIcon/>:
|
||||
<PersonAddIcon/>
|
||||
<div className={classes.pageGrow} />
|
||||
<Tooltip
|
||||
title={
|
||||
this.isItMe()
|
||||
? "You can't follow yourself."
|
||||
: this.state.relationship && this.state.relationship.following
|
||||
? "Unfollow"
|
||||
: "Follow"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
color={"inherit"}
|
||||
disabled={this.isItMe()}
|
||||
onClick={() => this.toggleFollow()}
|
||||
>
|
||||
{this.isItMe() ? (
|
||||
<PersonAddDisabledIcon />
|
||||
) : this.state.relationship &&
|
||||
this.state.relationship.following ? (
|
||||
<AccountMinusIcon />
|
||||
) : (
|
||||
<PersonAddIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title={"Send a message or post"}>
|
||||
<LinkableIconButton to={`/compose?acct=${this.state.account? this.state.account.acct: ""}`} color={"inherit"}>
|
||||
<ChatIcon/>
|
||||
<LinkableIconButton
|
||||
to={`/compose?acct=${
|
||||
this.state.account ? this.state.account.acct : ""
|
||||
}`}
|
||||
color={"inherit"}
|
||||
>
|
||||
<ChatIcon />
|
||||
</LinkableIconButton>
|
||||
</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()}>
|
||||
{
|
||||
this.state.relationship && this.state.relationship.blocking? <AccountHeartIcon/>: <AccountRemoveIcon/>
|
||||
<Tooltip
|
||||
title={
|
||||
this.state.relationship && this.state.relationship.blocking
|
||||
? "Unblock this account"
|
||||
: "Block this account"
|
||||
}
|
||||
>
|
||||
<IconButton
|
||||
color={"inherit"}
|
||||
disabled={this.isItMe()}
|
||||
onClick={() => this.toggleBlockDialog()}
|
||||
>
|
||||
{this.state.relationship && this.state.relationship.blocking ? (
|
||||
<AccountHeartIcon />
|
||||
) : (
|
||||
<AccountRemoveIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open in web">
|
||||
<IconButton href={this.state.account? this.state.account.url: ""} target="_blank" rel={"nofollower noreferrer noopener"} color={"inherit"}>
|
||||
<OpenInNewIcon/>
|
||||
<IconButton
|
||||
href={this.state.account ? this.state.account.url : ""}
|
||||
target="_blank"
|
||||
rel={"nofollower noreferrer noopener"}
|
||||
color={"inherit"}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
{
|
||||
this.isItMe()?
|
||||
{this.isItMe() ? (
|
||||
<Tooltip title="Edit profile">
|
||||
<LinkableIconButton to="/you" color="inherit">
|
||||
<AccountEditIcon/>
|
||||
<AccountEditIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>: null
|
||||
}
|
||||
</Tooltip>
|
||||
) : null}
|
||||
</Toolbar>
|
||||
<div className={classes.profileContent}>
|
||||
<Avatar className={classes.profileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
|
||||
<Avatar
|
||||
className={classes.profileAvatar}
|
||||
src={this.state.account ? this.state.account.avatar : ""}
|
||||
/>
|
||||
<div className={classes.profileUserBox}>
|
||||
<Typography variant="h4" color="inherit" dangerouslySetInnerHTML={
|
||||
{__html: this.state.account?
|
||||
this.state.account.display_name?
|
||||
emojifyString(this.state.account.display_name, this.state.account.emojis, classes.pageProfileNameEmoji)
|
||||
<Typography
|
||||
variant="h4"
|
||||
color="inherit"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: this.state.account
|
||||
? this.state.account.display_name
|
||||
? emojifyString(
|
||||
this.state.account.display_name,
|
||||
this.state.account.emojis,
|
||||
classes.pageProfileNameEmoji
|
||||
)
|
||||
: this.state.account.username
|
||||
: ""}}
|
||||
className={classes.pageProfileNameEmoji}/>
|
||||
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
|
||||
<Typography paragraph color="inherit">{
|
||||
this.state.account ?
|
||||
this.state.account.note?
|
||||
this.state.account.note
|
||||
: ""
|
||||
}}
|
||||
className={classes.pageProfileNameEmoji}
|
||||
/>
|
||||
<Typography variant="caption" color="inherit">
|
||||
{this.state.account ? "@" + this.state.account.acct : ""}
|
||||
</Typography>
|
||||
<Typography paragraph color="inherit">
|
||||
{this.state.account
|
||||
? this.state.account.note
|
||||
? this.state.account.note
|
||||
: "No bio provided by user."
|
||||
: "No bio available."
|
||||
}</Typography>
|
||||
: "No bio available."}
|
||||
</Typography>
|
||||
<Typography 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
|
||||
{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
|
||||
</Typography>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
{
|
||||
this.state.viewDidError?
|
||||
{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?
|
||||
<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/>
|
||||
}
|
||||
{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>
|
||||
<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.
|
||||
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>
|
||||
<Button
|
||||
onClick={() => this.toggleBlockDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.toggleBlock();
|
||||
this.toggleBlockDialog();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Block
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
|
|
@ -1,11 +1,20 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper, Button, 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';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
CircularProgress,
|
||||
Typography,
|
||||
Paper,
|
||||
Button,
|
||||
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 {
|
||||
posts?: [Status];
|
||||
|
@ -16,9 +25,7 @@ interface IPublicPageState {
|
|||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class PublicPage extends Component<any, IPublicPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: StreamListener;
|
||||
|
||||
|
@ -28,67 +35,74 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, 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"
|
||||
);
|
||||
this.streamListener = this.client.stream("/streaming/public");
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/public', {limit: 40}).then((resp: any) => {
|
||||
this.streamListener.on("connect", () => {
|
||||
this.client
|
||||
.get("/timelines/public", { limit: 40 })
|
||||
.then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
});
|
||||
})
|
||||
}).catch((resp: any) => {
|
||||
.catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
});
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
this.streamListener.on("update", (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.unshift(status); } else { queue = [status] }
|
||||
if (queue !== null && queue !== undefined) {
|
||||
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;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
})
|
||||
});
|
||||
this.setState({ posts });
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('error', (err: Error) => {
|
||||
this.streamListener.on("error", (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
this.streamListener.on("heartbeat", () => {});
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
|
@ -101,14 +115,19 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
this.setState({ posts: push as [Status], backlogPosts: null });
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
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) => {
|
||||
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) => {
|
||||
|
@ -118,37 +137,39 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: "error"
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
{this.state.backlogPosts ? (
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
<ArrowUpwardIcon />
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new post${this.state.backlogPosts.length > 1? "s": ""}`}
|
||||
label={`View ${this.state.backlogPosts.length} new post${
|
||||
this.state.backlogPosts.length > 1 ? "s" : ""
|
||||
}`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
|
@ -156,34 +177,46 @@ class PublicPage extends Component<any, IPublicPageState> {
|
|||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
</div>
|
||||
) : null}
|
||||
{this.state.posts ? (
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>
|
||||
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?
|
||||
<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/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,15 +1,30 @@
|
|||
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 {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';
|
||||
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 { 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 {
|
||||
classes: any;
|
||||
|
@ -24,51 +39,64 @@ interface IRecommendationsPageState {
|
|||
followSuggestions?: [Account];
|
||||
}
|
||||
|
||||
class RecommendationsPage extends Component<IRecommendationsPageProps, IRecommendationsPageState> {
|
||||
|
||||
class RecommendationsPage extends Component<
|
||||
IRecommendationsPageProps,
|
||||
IRecommendationsPageState
|
||||
> {
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
this.client = new Mastodon(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 = {
|
||||
viewIsLoading: true
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.client.get('/follow_requests').then((resp: any) => {
|
||||
this.client
|
||||
.get("/follow_requests")
|
||||
.then((resp: any) => {
|
||||
let requestedFollows: [Account] = resp.data;
|
||||
this.setState({ requestedFollows })
|
||||
}).catch((err: Error) => {
|
||||
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) => {
|
||||
this.client
|
||||
.get("/suggestions")
|
||||
.then((resp: any) => {
|
||||
let followSuggestions: [Account] = resp.data;
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
followSuggestions
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.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
|
||||
.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) {
|
||||
|
@ -79,158 +107,203 @@ class RecommendationsPage extends Component<IRecommendationsPageProps, IRecommen
|
|||
});
|
||||
this.setState({ followSuggestions });
|
||||
}
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
|
||||
.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) => {
|
||||
|
||||
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});
|
||||
}
|
||||
this.setState({ requestedFollows });
|
||||
|
||||
let verb: string = type;
|
||||
verb === "authorize"? verb = "authorized": verb = "rejected";
|
||||
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);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't ${type} this request: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
showFollowRequests() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Follow requests</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{
|
||||
this.state.requestedFollows?
|
||||
this.state.requestedFollows.map((request: Account) => {
|
||||
{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}/>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${request.id}`}
|
||||
alt={request.username}
|
||||
src={request.avatar_static}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={request.display_name || request.acct} secondary={request.acct}/>
|
||||
<ListItemText
|
||||
primary={request.display_name || request.acct}
|
||||
secondary={request.acct}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="Accept request">
|
||||
<IconButton onClick={() => this.handleFollowRequest(request, "authorize")}>
|
||||
<CheckIcon/>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(request, "authorize")
|
||||
}
|
||||
>
|
||||
<CheckIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Reject request">
|
||||
<IconButton onClick={() => this.handleFollowRequest(request, "reject")}>
|
||||
<CloseIcon/>
|
||||
<IconButton
|
||||
onClick={() =>
|
||||
this.handleFollowRequest(request, "reject")
|
||||
}
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton to={`/profile/${request.id}`}>
|
||||
<AccountCircleIcon/>
|
||||
<AccountCircleIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}): null
|
||||
}
|
||||
})
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
showFollowSuggestions() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Suggested accounts</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{
|
||||
this.state.followSuggestions?
|
||||
this.state.followSuggestions.map((suggestion: Account) => {
|
||||
{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}/>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${suggestion.id}`}
|
||||
alt={suggestion.username}
|
||||
src={suggestion.avatar_static}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={suggestion.display_name || suggestion.acct} secondary={suggestion.acct}/>
|
||||
<ListItemText
|
||||
primary={suggestion.display_name || suggestion.acct}
|
||||
secondary={suggestion.acct}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton to={`/profile/${suggestion.id}`}>
|
||||
<AssignmentIndIcon/>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Follow">
|
||||
<IconButton onClick={() => this.followMember(suggestion)}>
|
||||
<PersonAddIcon/>
|
||||
<IconButton
|
||||
onClick={() => this.followMember(suggestion)}
|
||||
>
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
);
|
||||
}): null
|
||||
}
|
||||
})
|
||||
: null}
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{
|
||||
this.state.viewDidLoad?
|
||||
{this.state.viewDidLoad ? (
|
||||
<div>
|
||||
{
|
||||
this.state.requestedFollows && this.state.requestedFollows.length > 0?
|
||||
this.showFollowRequests():
|
||||
{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/>
|
||||
<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():
|
||||
)}
|
||||
<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>
|
||||
<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?
|
||||
)}
|
||||
</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/>
|
||||
}
|
||||
<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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
|
@ -13,19 +13,19 @@ import {
|
|||
CircularProgress,
|
||||
Tooltip,
|
||||
IconButton
|
||||
} from '@material-ui/core';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
import AssignmentIndIcon from '@material-ui/icons/AssignmentInd';
|
||||
import PersonAddIcon from '@material-ui/icons/PersonAdd';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import {LinkableIconButton, LinkableAvatar} from '../interfaces/overrides';
|
||||
import Mastodon from 'megalodon';
|
||||
import {parse as parseParams, ParsedQuery} from 'query-string';
|
||||
import { Results } from '../types/Search';
|
||||
import { withSnackbar } from 'notistack';
|
||||
import Post from '../components/Post';
|
||||
import { Status } from '../types/Status';
|
||||
import { Account } from '../types/Account';
|
||||
} from "@material-ui/core";
|
||||
import PersonIcon from "@material-ui/icons/Person";
|
||||
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
|
||||
import PersonAddIcon from "@material-ui/icons/PersonAdd";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
|
||||
import Mastodon from "megalodon";
|
||||
import { parse as parseParams, ParsedQuery } from "query-string";
|
||||
import { Results } from "../types/Search";
|
||||
import { withSnackbar } from "notistack";
|
||||
import Post from "../components/Post";
|
||||
import { Status } from "../types/Status";
|
||||
import { Account } from "../types/Account";
|
||||
|
||||
interface ISearchPageState {
|
||||
query: string[] | string;
|
||||
|
@ -39,13 +39,15 @@ interface ISearchPageState {
|
|||
}
|
||||
|
||||
class SearchPage extends Component<any, ISearchPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.client = new Mastodon(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);
|
||||
|
||||
|
@ -53,18 +55,23 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
viewIsLoading: true,
|
||||
query: searchParams.query,
|
||||
type: searchParams.type
|
||||
}
|
||||
};
|
||||
|
||||
if (searchParams.type === "tag") {
|
||||
this.searchForPostsWithTags(searchParams.query);
|
||||
} else {
|
||||
this.searchQuery(searchParams.query);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true, viewDidError: false, viewDidErrorCode: '', results: undefined});
|
||||
this.setState({
|
||||
viewDidLoad: false,
|
||||
viewIsLoading: true,
|
||||
viewDidError: false,
|
||||
viewDidErrorCode: "",
|
||||
results: undefined
|
||||
});
|
||||
let searchParams = this.getQueryAndType(props);
|
||||
this.setState({ query: searchParams.query, type: searchParams.type });
|
||||
if (searchParams.type === "tag") {
|
||||
|
@ -76,7 +83,7 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
|
||||
runQueryCheck(newLocation?: string): ParsedQuery {
|
||||
let searchParams = "";
|
||||
if (newLocation !== undefined && typeof(newLocation) === "string") {
|
||||
if (newLocation !== undefined && typeof newLocation === "string") {
|
||||
searchParams = newLocation.replace("#/search", "");
|
||||
} else {
|
||||
searchParams = location.hash.replace("#/search", "");
|
||||
|
@ -110,27 +117,38 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
}
|
||||
|
||||
searchQuery(query: string | string[]) {
|
||||
this.client.get('/search', {q: query}).then((resp: any) => {
|
||||
this.client
|
||||
.get("/search", { q: query })
|
||||
.then((resp: any) => {
|
||||
let results: Results = resp.data;
|
||||
this.setState({
|
||||
results,
|
||||
viewDidLoad: true,
|
||||
viewIsLoading: false
|
||||
});
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.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' });
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't search for ${this.state.query}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
searchForPostsWithTags(query: string | string[]) {
|
||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
client.get(`/timelines/tag/${query}`).then((resp: any) => {
|
||||
let client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
client
|
||||
.get(`/timelines/tag/${query}`)
|
||||
.then((resp: any) => {
|
||||
let tagResults: [Status] = resp.data;
|
||||
this.setState({
|
||||
tagResults,
|
||||
|
@ -138,25 +156,37 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
viewIsLoading: false
|
||||
});
|
||||
console.log(this.state.tagResults);
|
||||
}).catch((err: Error) => {
|
||||
})
|
||||
.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' });
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't search for posts with tag ${this.state.query}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
followMemberFromQuery(acct: Account) {
|
||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
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);
|
||||
let client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
showAllAccountsFromQuery() {
|
||||
|
@ -165,26 +195,34 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
<div>
|
||||
<ListSubheader>Accounts</ListSubheader>
|
||||
|
||||
{
|
||||
this.state.results && this.state.results.accounts.length > 0?
|
||||
{this.state.results && this.state.results.accounts.length > 0 ? (
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
{ this.state.results.accounts.map((acct: Account) => {
|
||||
{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}/>
|
||||
<LinkableAvatar
|
||||
to={`/profile/${acct.id}`}
|
||||
alt={acct.username}
|
||||
src={acct.avatar_static}
|
||||
/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={acct.display_name || acct.acct} secondary={acct.acct}/>
|
||||
<ListItemText
|
||||
primary={acct.display_name || acct.acct}
|
||||
secondary={acct.acct}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Tooltip title="View profile">
|
||||
<LinkableIconButton to={`/profile/${acct.id}`}>
|
||||
<AssignmentIndIcon/>
|
||||
<AssignmentIndIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Follow">
|
||||
<IconButton onClick={() => this.followMemberFromQuery(acct)}>
|
||||
<PersonAddIcon/>
|
||||
<IconButton
|
||||
onClick={() => this.followMemberFromQuery(acct)}
|
||||
>
|
||||
<PersonAddIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -192,43 +230,63 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
);
|
||||
})}
|
||||
</List>
|
||||
</Paper>: <Typography variant="caption" className={classes.pageLayoutEmptyTextConstraints}>No results found</Typography>
|
||||
}
|
||||
</Paper>
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={classes.pageLayoutEmptyTextConstraints}
|
||||
>
|
||||
No results found
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
|
||||
<br/>
|
||||
<br />
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
showAllPostsFromQuery() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Posts</ListSubheader>
|
||||
{
|
||||
this.state.results?
|
||||
this.state.results.statuses.length > 0?
|
||||
{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
|
||||
}
|
||||
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;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<ListSubheader>Tagged posts</ListSubheader>
|
||||
{
|
||||
this.state.tagResults?
|
||||
this.state.tagResults.length > 0?
|
||||
{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
|
||||
}
|
||||
return <Post key={post.id} post={post} client={this.client} />;
|
||||
})
|
||||
) : (
|
||||
<Typography
|
||||
variant="caption"
|
||||
className={classes.pageLayoutEmptyTextConstraints}
|
||||
>
|
||||
No results found.
|
||||
</Typography>
|
||||
)
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -237,32 +295,37 @@ class SearchPage extends Component<any, ISearchPageState> {
|
|||
const { classes } = this.props;
|
||||
return (
|
||||
<div className={classes.pageLayoutConstraints}>
|
||||
{
|
||||
this.state.type && this.state.type === "tag"?
|
||||
this.showAllPostsWithTag():
|
||||
{this.state.type && this.state.type === "tag" ? (
|
||||
this.showAllPostsWithTag()
|
||||
) : (
|
||||
<div>
|
||||
{this.showAllAccountsFromQuery()}
|
||||
{this.showAllPostsFromQuery()}
|
||||
</div>
|
||||
}
|
||||
{
|
||||
this.state.viewDidError?
|
||||
)}
|
||||
{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/>
|
||||
}
|
||||
<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));
|
|
@ -1,4 +1,4 @@
|
|||
import React, { Component } from 'react';
|
||||
import React, { Component } from "react";
|
||||
import {
|
||||
List,
|
||||
ListItem,
|
||||
|
@ -22,28 +22,44 @@ import {
|
|||
Grid,
|
||||
Theme,
|
||||
Typography
|
||||
} from '@material-ui/core';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import {setUserDefaultBool, getUserDefaultBool, getUserDefaultTheme, setUserDefaultTheme, getUserDefaultVisibility, 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';
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./PageLayout.styles";
|
||||
import {
|
||||
setUserDefaultBool,
|
||||
getUserDefaultBool,
|
||||
getUserDefaultTheme,
|
||||
setUserDefaultTheme,
|
||||
getUserDefaultVisibility,
|
||||
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 DevicesIcon from '@material-ui/icons/Devices';
|
||||
import Brightness3Icon from '@material-ui/icons/Brightness3';
|
||||
import PaletteIcon from '@material-ui/icons/Palette';
|
||||
import AccountEditIcon from 'mdi-material-ui/AccountEdit';
|
||||
import MastodonIcon from 'mdi-material-ui/Mastodon';
|
||||
import VisibilityIcon from '@material-ui/icons/Visibility';
|
||||
import NotificationsIcon from '@material-ui/icons/Notifications';
|
||||
import BellAlertIcon from 'mdi-material-ui/BellAlert';
|
||||
import RefreshIcon from '@material-ui/icons/Refresh';
|
||||
import UndoIcon from '@material-ui/icons/Undo';
|
||||
import { Config } from '../types/Config';
|
||||
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
|
||||
import DevicesIcon from "@material-ui/icons/Devices";
|
||||
import Brightness3Icon from "@material-ui/icons/Brightness3";
|
||||
import PaletteIcon from "@material-ui/icons/Palette";
|
||||
import AccountEditIcon from "mdi-material-ui/AccountEdit";
|
||||
import MastodonIcon from "mdi-material-ui/Mastodon";
|
||||
import VisibilityIcon from "@material-ui/icons/Visibility";
|
||||
import NotificationsIcon from "@material-ui/icons/Notifications";
|
||||
import BellAlertIcon from "mdi-material-ui/BellAlert";
|
||||
import RefreshIcon from "@material-ui/icons/Refresh";
|
||||
import UndoIcon from "@material-ui/icons/Undo";
|
||||
import CancelIcon from "@material-ui/icons/Cancel";
|
||||
|
||||
interface ISettingsState {
|
||||
darkModeEnabled: boolean;
|
||||
|
@ -62,25 +78,28 @@ interface ISettingsState {
|
|||
}
|
||||
|
||||
class SettingsPage extends Component<any, ISettingsState> {
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
darkModeEnabled: getUserDefaultBool('darkModeEnabled'),
|
||||
systemDecidesDarkMode: getUserDefaultBool('systemDecidesDarkMode'),
|
||||
darkModeEnabled: getUserDefaultBool("darkModeEnabled"),
|
||||
systemDecidesDarkMode: getUserDefaultBool("systemDecidesDarkMode"),
|
||||
pushNotificationsEnabled: canSendNotifications(),
|
||||
badgeDisplaysAllNotifs: getUserDefaultBool('displayAllOnNotificationBadge'),
|
||||
badgeDisplaysAllNotifs: getUserDefaultBool(
|
||||
"displayAllOnNotificationBadge"
|
||||
),
|
||||
selectThemeName: getUserDefaultTheme().key,
|
||||
themeDialogOpen: false,
|
||||
visibilityDialogOpen: false,
|
||||
resetHyperspaceDialog: false,
|
||||
resetSettingsDialog: false,
|
||||
previewTheme: setHyperspaceTheme(getUserDefaultTheme()) || setHyperspaceTheme(defaultTheme),
|
||||
previewTheme:
|
||||
setHyperspaceTheme(getUserDefaultTheme()) ||
|
||||
setHyperspaceTheme(defaultTheme),
|
||||
defaultVisibility: getUserDefaultVisibility() || "public",
|
||||
brandName: "Hyperspace",
|
||||
federated: true
|
||||
}
|
||||
};
|
||||
|
||||
this.toggleDarkMode = this.toggleDarkMode.bind(this);
|
||||
this.toggleSystemDarkMode = this.toggleSystemDarkMode.bind(this);
|
||||
|
@ -94,11 +113,13 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
}
|
||||
|
||||
componentDidMount() {
|
||||
getConfig().then((config: any) => {
|
||||
getConfig()
|
||||
.then((config: any) => {
|
||||
this.setState({
|
||||
brandName: config.branding.name
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.catch((err: Error) => {
|
||||
console.error(err.message);
|
||||
});
|
||||
this.getFederatedStatus();
|
||||
|
@ -109,32 +130,49 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
getConfig().then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
console.log(config.federation.allowPublicPosts === false)
|
||||
this.setState({ federated: config.federation.allowPublicPosts });
|
||||
console.log(config.federation.allowPublicPosts === false);
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
toggleDarkMode() {
|
||||
this.setState({ darkModeEnabled: !this.state.darkModeEnabled });
|
||||
setUserDefaultBool('darkModeEnabled', !this.state.darkModeEnabled);
|
||||
setUserDefaultBool("darkModeEnabled", !this.state.darkModeEnabled);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
toggleSystemDarkMode() {
|
||||
this.setState({ systemDecidesDarkMode: !this.state.systemDecidesDarkMode });
|
||||
setUserDefaultBool('systemDecidesDarkMode', !this.state.systemDecidesDarkMode);
|
||||
this.setState({
|
||||
systemDecidesDarkMode: !this.state.systemDecidesDarkMode
|
||||
});
|
||||
setUserDefaultBool(
|
||||
"systemDecidesDarkMode",
|
||||
!this.state.systemDecidesDarkMode
|
||||
);
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
togglePushNotifications() {
|
||||
this.setState({ pushNotificationsEnabled: !this.state.pushNotificationsEnabled });
|
||||
setUserDefaultBool('enablePushNotifications', !this.state.pushNotificationsEnabled);
|
||||
this.setState({
|
||||
pushNotificationsEnabled: !this.state.pushNotificationsEnabled
|
||||
});
|
||||
setUserDefaultBool(
|
||||
"enablePushNotifications",
|
||||
!this.state.pushNotificationsEnabled
|
||||
);
|
||||
}
|
||||
|
||||
toggleBadgeCount() {
|
||||
this.setState({ badgeDisplaysAllNotifs: !this.state.badgeDisplaysAllNotifs });
|
||||
setUserDefaultBool('displayAllOnNotificationBadge', !this.state.badgeDisplaysAllNotifs);
|
||||
this.setState({
|
||||
badgeDisplaysAllNotifs: !this.state.badgeDisplaysAllNotifs
|
||||
});
|
||||
setUserDefaultBool(
|
||||
"displayAllOnNotificationBadge",
|
||||
!this.state.badgeDisplaysAllNotifs
|
||||
);
|
||||
}
|
||||
|
||||
toggleThemeDialog() {
|
||||
|
@ -142,11 +180,15 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
}
|
||||
|
||||
toggleVisibilityDialog() {
|
||||
this.setState({ visibilityDialogOpen: !this.state.visibilityDialogOpen });
|
||||
this.setState({
|
||||
visibilityDialogOpen: !this.state.visibilityDialogOpen
|
||||
});
|
||||
}
|
||||
|
||||
toggleResetDialog() {
|
||||
this.setState({ resetHyperspaceDialog: !this.state.resetHyperspaceDialog });
|
||||
this.setState({
|
||||
resetHyperspaceDialog: !this.state.resetHyperspaceDialog
|
||||
});
|
||||
}
|
||||
|
||||
toggleResetSettingsDialog() {
|
||||
|
@ -178,15 +220,22 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
}
|
||||
|
||||
refresh() {
|
||||
let settings = ['darkModeEnabled', 'enablePushNotifications', 'clearNotificationsOnRead', 'theme', 'displayAllOnNotificationBadge', 'defaultVisibility'];
|
||||
let settings = [
|
||||
"darkModeEnabled",
|
||||
"enablePushNotifications",
|
||||
"clearNotificationsOnRead",
|
||||
"theme",
|
||||
"displayAllOnNotificationBadge",
|
||||
"defaultVisibility"
|
||||
];
|
||||
settings.forEach(setting => {
|
||||
localStorage.removeItem(setting);
|
||||
})
|
||||
});
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
showThemeDialog() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.themeDialogOpen}
|
||||
|
@ -196,7 +245,9 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
fullWidth={true}
|
||||
aria-labelledby="confirmation-dialog-title"
|
||||
>
|
||||
<DialogTitle id="confirmation-dialog-title">Choose a theme</DialogTitle>
|
||||
<DialogTitle id="confirmation-dialog-title">
|
||||
Choose a theme
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Grid container spacing={16}>
|
||||
<Grid item xs={12} md={6}>
|
||||
|
@ -204,17 +255,31 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
aria-label="Theme"
|
||||
name="colorScheme"
|
||||
value={this.state.selectThemeName}
|
||||
onChange={(e, value) => this.changeThemeName(value)}
|
||||
onChange={(e, value) =>
|
||||
this.changeThemeName(value)
|
||||
}
|
||||
>
|
||||
{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>
|
||||
</Grid>
|
||||
<Grid item xs={12} md={6} className={classes.desktopOnly}>
|
||||
<Typography variant="h6" component="p">Theme preview</Typography>
|
||||
<ThemePreview theme={this.state.previewTheme}/>
|
||||
<Grid
|
||||
item
|
||||
xs={12}
|
||||
md={6}
|
||||
className={classes.desktopOnly}
|
||||
>
|
||||
<Typography variant="h6" component="p">
|
||||
Theme preview
|
||||
</Typography>
|
||||
<ThemePreview theme={this.state.previewTheme} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</DialogContent>
|
||||
|
@ -240,22 +305,54 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
fullWidth={true}
|
||||
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>
|
||||
<RadioGroup
|
||||
aria-label="Visibility"
|
||||
name="visibility"
|
||||
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 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"} />
|
||||
<FormControlLabel
|
||||
value={"public"}
|
||||
key={"public"}
|
||||
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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={this.toggleVisibilityDialog} color="default">
|
||||
<Button
|
||||
onClick={this.toggleVisibilityDialog}
|
||||
color="default"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={this.setVisibility} color="secondary">
|
||||
|
@ -272,14 +369,23 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
open={this.state.resetSettingsDialog}
|
||||
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>
|
||||
<Button onClick={() => this.toggleResetSettingsDialog()} color="primary" autoFocus>
|
||||
<Button
|
||||
onClick={() => this.toggleResetSettingsDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.refresh();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -293,19 +399,30 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
open={this.state.resetHyperspaceDialog}
|
||||
onClose={() => this.toggleResetDialog()}
|
||||
>
|
||||
<DialogTitle id="alert-dialog-title">Reset {this.state.brandName}?</DialogTitle>
|
||||
<DialogTitle id="alert-dialog-title">
|
||||
Reset {this.state.brandName}?
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<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>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.toggleResetDialog()} color="primary" autoFocus>
|
||||
<Button
|
||||
onClick={() => this.toggleResetDialog()}
|
||||
color="primary"
|
||||
autoFocus
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={() => {
|
||||
<Button
|
||||
onClick={() => {
|
||||
this.reset();
|
||||
}} color="primary">
|
||||
}}
|
||||
color="primary"
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</DialogActions>
|
||||
|
@ -322,9 +439,12 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<DevicesIcon color="action"/>
|
||||
<DevicesIcon color="action" />
|
||||
</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>
|
||||
<Switch
|
||||
checked={this.state.systemDecidesDarkMode}
|
||||
|
@ -334,9 +454,12 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<Brightness3Icon color="action"/>
|
||||
<Brightness3Icon color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Dark mode" secondary="Toggles light or dark theme"/>
|
||||
<ListItemText
|
||||
primary="Dark mode"
|
||||
secondary="Toggles light or dark theme"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
disabled={this.state.systemDecidesDarkMode}
|
||||
|
@ -347,9 +470,12 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<PaletteIcon color="action"/>
|
||||
<PaletteIcon color="action" />
|
||||
</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>
|
||||
<Button onClick={this.toggleThemeDialog}>
|
||||
Set theme
|
||||
|
@ -358,41 +484,69 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
<ListSubheader>Your Account</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<AccountEditIcon color="action"/>
|
||||
<AccountEditIcon color="action" />
|
||||
</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>
|
||||
<LinkableButton to="/you">Edit</LinkableButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<MastodonIcon color="action"/>
|
||||
<CancelIcon color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Configure on Mastodon"/>
|
||||
<ListItemText
|
||||
primary="Manage blocked servers"
|
||||
secondary="View and manage servers that you've blocked"
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton href={(localStorage.getItem("baseurl") as string) + "/settings/preferences"} target="_blank" rel="noreferrer">
|
||||
<OpenInNewIcon/>
|
||||
<LinkableButton to="/blocked">
|
||||
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>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
<ListSubheader>Composer</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<VisibilityIcon color="action"/>
|
||||
<VisibilityIcon color="action" />
|
||||
</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>
|
||||
<Button onClick={this.toggleVisibilityDialog}>
|
||||
Change
|
||||
|
@ -401,35 +555,42 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
<ListSubheader>Notifications</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<NotificationsIcon color="action"/>
|
||||
<NotificationsIcon color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Enable push notifications"
|
||||
secondary={
|
||||
getUserDefaultBool('userDeniedNotification')?
|
||||
"Check your browser's notification permissions.":
|
||||
browserSupportsNotificationRequests()?
|
||||
"Send a push notification when not focused.":
|
||||
"Notifications aren't supported."
|
||||
getUserDefaultBool("userDeniedNotification")
|
||||
? "Check your browser's notification permissions."
|
||||
: browserSupportsNotificationRequests()
|
||||
? "Send a push notification when not focused."
|
||||
: "Notifications aren't supported."
|
||||
}
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Switch
|
||||
checked={this.state.pushNotificationsEnabled}
|
||||
checked={
|
||||
this.state.pushNotificationsEnabled
|
||||
}
|
||||
onChange={this.togglePushNotifications}
|
||||
disabled={!browserSupportsNotificationRequests() || getUserDefaultBool('userDeniedNotification')}
|
||||
disabled={
|
||||
!browserSupportsNotificationRequests() ||
|
||||
getUserDefaultBool(
|
||||
"userDeniedNotification"
|
||||
)
|
||||
}
|
||||
/>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<BellAlertIcon color="action"/>
|
||||
<BellAlertIcon color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText
|
||||
primary="Notification badge counts all notifications"
|
||||
|
@ -446,28 +607,40 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</ListItem>
|
||||
</List>
|
||||
</Paper>
|
||||
<br/>
|
||||
<br />
|
||||
<ListSubheader>Advanced</ListSubheader>
|
||||
<Paper className={classes.pageListConstraints}>
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<RefreshIcon color="action"/>
|
||||
<RefreshIcon color="action" />
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary="Refresh settings" secondary="Reset the settings to defaults."/>
|
||||
<ListItemText
|
||||
primary="Refresh settings"
|
||||
secondary="Reset the settings to defaults."
|
||||
/>
|
||||
<ListItemSecondaryAction>
|
||||
<Button onClick={() => this.toggleResetSettingsDialog()}>
|
||||
<Button
|
||||
onClick={() =>
|
||||
this.toggleResetSettingsDialog()
|
||||
}
|
||||
>
|
||||
Refresh
|
||||
</Button>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
<ListItem>
|
||||
<ListItemAvatar>
|
||||
<UndoIcon color="action"/>
|
||||
<UndoIcon color="action" />
|
||||
</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>
|
||||
<Button onClick={() => this.toggleResetDialog()}>
|
||||
<Button
|
||||
onClick={() => this.toggleResetDialog()}
|
||||
>
|
||||
Reset
|
||||
</Button>
|
||||
</ListItemSecondaryAction>
|
||||
|
@ -481,7 +654,6 @@ class SettingsPage extends Component<any, ISettingsState> {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export default withStyles(styles)(SettingsPage);
|
|
@ -1,15 +1,29 @@
|
|||
import React, { Component, ChangeEvent } from 'react';
|
||||
import {withStyles, Paper, Typography, Button, TextField, Fade, Link, CircularProgress, Tooltip, Dialog, DialogTitle, DialogActions, DialogContent} from '@material-ui/core';
|
||||
import {styles} from './WelcomePage.styles';
|
||||
import Mastodon from 'megalodon';
|
||||
import {SaveClientSession} from '../types/SessionData';
|
||||
import { createHyperspaceApp, getRedirectAddress } from '../utilities/login';
|
||||
import {parseUrl} from 'query-string';
|
||||
import { getConfig } from '../utilities/settings';
|
||||
import { isDarwinApp } from '../utilities/desktop';
|
||||
import axios from 'axios';
|
||||
import {withSnackbar, withSnackbarProps} from 'notistack';
|
||||
import { Config } from '../types/Config';
|
||||
import React, { Component, ChangeEvent } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
Paper,
|
||||
Typography,
|
||||
Button,
|
||||
TextField,
|
||||
Fade,
|
||||
Link,
|
||||
CircularProgress,
|
||||
Tooltip,
|
||||
Dialog,
|
||||
DialogTitle,
|
||||
DialogActions,
|
||||
DialogContent
|
||||
} from "@material-ui/core";
|
||||
import { styles } from "./WelcomePage.styles";
|
||||
import Mastodon from "megalodon";
|
||||
import { SaveClientSession } from "../types/SessionData";
|
||||
import { createHyperspaceApp, getRedirectAddress } from "../utilities/login";
|
||||
import { parseUrl } from "query-string";
|
||||
import { getConfig } from "../utilities/settings";
|
||||
import { isDarwinApp } from "../utilities/desktop";
|
||||
import axios from "axios";
|
||||
import { withSnackbar, withSnackbarProps } from "notistack";
|
||||
import { Config } from "../types/Config";
|
||||
|
||||
interface IWelcomeProps extends withSnackbarProps {
|
||||
classes: any;
|
||||
|
@ -40,7 +54,6 @@ interface IWelcomeState {
|
|||
}
|
||||
|
||||
class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
||||
|
||||
client: any;
|
||||
|
||||
constructor(props: any) {
|
||||
|
@ -52,35 +65,48 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
userInputError: false,
|
||||
foundSavedLogin: false,
|
||||
authority: false,
|
||||
userInputErrorMessage: '',
|
||||
defaultRedirectAddress: '',
|
||||
userInputErrorMessage: "",
|
||||
defaultRedirectAddress: "",
|
||||
openAuthDialog: false,
|
||||
authCode: '',
|
||||
authCode: "",
|
||||
emergencyMode: false,
|
||||
version: ''
|
||||
}
|
||||
version: ""
|
||||
};
|
||||
|
||||
getConfig().then((result: any) => {
|
||||
getConfig()
|
||||
.then((result: any) => {
|
||||
if (result !== undefined) {
|
||||
let config: Config = result;
|
||||
if (result.location === "dynamic") {
|
||||
console.warn("Recirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!");
|
||||
console.warn(
|
||||
"Recirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!"
|
||||
);
|
||||
}
|
||||
this.setState({
|
||||
logoUrl: config.branding? result.branding.logo: "logo.png",
|
||||
backgroundUrl: config.branding? result.branding.background: "background.png",
|
||||
brandName: config.branding? result.branding.name: "Hyperspace",
|
||||
registerBase: config.registration? result.registration.defaultInstance: "",
|
||||
logoUrl: config.branding ? result.branding.logo : "logo.png",
|
||||
backgroundUrl: config.branding
|
||||
? result.branding.background
|
||||
: "background.png",
|
||||
brandName: config.branding ? result.branding.name : "Hyperspace",
|
||||
registerBase: config.registration
|
||||
? result.registration.defaultInstance
|
||||
: "",
|
||||
federates: config.federation.universalLogin,
|
||||
license: config.license.url,
|
||||
repo: config.repository,
|
||||
defaultRedirectAddress: config.location != "dynamic"? config.location: `https://${window.location.host}`,
|
||||
defaultRedirectAddress:
|
||||
config.location != "dynamic"
|
||||
? config.location
|
||||
: `https://${window.location.host}`,
|
||||
version: config.version
|
||||
});
|
||||
}
|
||||
}).catch(() => {
|
||||
console.error('config.json is missing. If you want to customize Hyperspace, please include config.json');
|
||||
})
|
||||
.catch(() => {
|
||||
console.error(
|
||||
"config.json is missing. If you want to customize Hyperspace, please include config.json"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -88,7 +114,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
this.getSavedSession();
|
||||
this.setState({
|
||||
foundSavedLogin: true
|
||||
})
|
||||
});
|
||||
this.checkForToken();
|
||||
}
|
||||
}
|
||||
|
@ -106,7 +132,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
|
||||
readyForAuth() {
|
||||
if (localStorage.getItem('baseurl')) {
|
||||
if (localStorage.getItem("baseurl")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -122,14 +148,14 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
clientSecret: session.clientSecret,
|
||||
authUrl: session.authUrl,
|
||||
emergencyMode: session.emergency
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
startEmergencyLogin() {
|
||||
if (!this.state.emergencyMode) {
|
||||
this.createEmergencyLogin();
|
||||
};
|
||||
}
|
||||
this.toggleAuthDialog();
|
||||
}
|
||||
|
||||
|
@ -142,13 +168,11 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
}
|
||||
|
||||
watchUsernameField(event: any) {
|
||||
if (event.keyCode === 13)
|
||||
this.startLogin()
|
||||
if (event.keyCode === 13) this.startLogin();
|
||||
}
|
||||
|
||||
watchAuthField(event: any) {
|
||||
if (event.keyCode === 13)
|
||||
this.authorizeEmergencyLogin()
|
||||
if (event.keyCode === 13) this.authorizeEmergencyLogin();
|
||||
}
|
||||
|
||||
getLoginUser(user: string) {
|
||||
|
@ -158,25 +182,37 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
this.setState({ user: newUser });
|
||||
return "https://" + newUser.split("@")[1];
|
||||
} else {
|
||||
let newUser = `${user}@${this.state.registerBase? this.state.registerBase: "mastodon.social"}`;
|
||||
let newUser = `${user}@${
|
||||
this.state.registerBase ? this.state.registerBase : "mastodon.social"
|
||||
}`;
|
||||
this.setState({ user: newUser });
|
||||
return "https://" + (this.state.registerBase? this.state.registerBase: "mastodon.social");
|
||||
return (
|
||||
"https://" +
|
||||
(this.state.registerBase
|
||||
? this.state.registerBase
|
||||
: "mastodon.social")
|
||||
);
|
||||
}
|
||||
} else {
|
||||
let newUser = `${user}@${this.state.registerBase? this.state.registerBase: "mastodon.social"}`;
|
||||
let newUser = `${user}@${
|
||||
this.state.registerBase ? this.state.registerBase : "mastodon.social"
|
||||
}`;
|
||||
this.setState({ user: newUser });
|
||||
return "https://" + (this.state.registerBase? this.state.registerBase: "mastodon.social");
|
||||
return (
|
||||
"https://" +
|
||||
(this.state.registerBase ? this.state.registerBase : "mastodon.social")
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
startLogin() {
|
||||
let error = this.checkForErrors();
|
||||
if (!error) {
|
||||
const scopes = 'read write follow';
|
||||
const scopes = "read write follow";
|
||||
const baseurl = this.getLoginUser(this.state.user);
|
||||
localStorage.setItem("baseurl", baseurl);
|
||||
createHyperspaceApp(
|
||||
this.state.brandName? this.state.brandName: "Hyperspace",
|
||||
this.state.brandName ? this.state.brandName : "Hyperspace",
|
||||
scopes,
|
||||
baseurl,
|
||||
getRedirectAddress(this.state.defaultRedirectAddress)
|
||||
|
@ -186,26 +222,26 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
clientSecret: resp.clientSecret,
|
||||
authUrl: resp.url,
|
||||
emergency: false
|
||||
}
|
||||
};
|
||||
localStorage.setItem("login", JSON.stringify(saveSessionForCrashing));
|
||||
this.setState({
|
||||
clientId: resp.clientId,
|
||||
clientSecret: resp.clientSecret,
|
||||
authUrl: resp.url,
|
||||
wantsToLogin: true
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
} else {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
createEmergencyLogin() {
|
||||
console.log("Creating an emergency login...")
|
||||
console.log("Creating an emergency login...");
|
||||
const scopes = "read write follow";
|
||||
const baseurl = localStorage.getItem('baseurl') || this.getLoginUser(this.state.user);
|
||||
const baseurl =
|
||||
localStorage.getItem("baseurl") || this.getLoginUser(this.state.user);
|
||||
Mastodon.registerApp(
|
||||
this.state.brandName? this.state.brandName: "Hyperspace",
|
||||
this.state.brandName ? this.state.brandName : "Hyperspace",
|
||||
{
|
||||
scopes: scopes
|
||||
},
|
||||
|
@ -240,7 +276,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
authUrl: session.authUrl,
|
||||
emergencyMode: session.emergency,
|
||||
wantsToLogin: true
|
||||
})
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -255,15 +291,23 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
return true;
|
||||
} else {
|
||||
if (this.state.user.includes("@")) {
|
||||
if (this.state.federates && (this.state.federates === true)) {
|
||||
if (this.state.federates && this.state.federates === true) {
|
||||
let baseUrl = this.state.user.split("@")[1];
|
||||
axios.get("https://" + baseUrl + "/api/v1/timelines/public").catch((err: Error) => {
|
||||
axios
|
||||
.get("https://" + baseUrl + "/api/v1/timelines/public")
|
||||
.catch((err: Error) => {
|
||||
let userInputError = true;
|
||||
let userInputErrorMessage = "Instance name is invalid.";
|
||||
this.setState({ userInputError, userInputErrorMessage });
|
||||
return true;
|
||||
});
|
||||
} else if (this.state.user.includes(this.state.registerBase? this.state.registerBase: "mastodon.social")) {
|
||||
} else if (
|
||||
this.state.user.includes(
|
||||
this.state.registerBase
|
||||
? this.state.registerBase
|
||||
: "mastodon.social"
|
||||
)
|
||||
) {
|
||||
this.setState({ userInputError, userInputErrorMessage });
|
||||
return false;
|
||||
} else {
|
||||
|
@ -279,7 +323,6 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
this.setState({ userInputError, userInputErrorMessage });
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
checkForToken() {
|
||||
|
@ -294,19 +337,33 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
clientLoginSession.clientId,
|
||||
clientLoginSession.clientSecret,
|
||||
code,
|
||||
(localStorage.getItem("baseurl") as string),
|
||||
this.state.emergencyMode?
|
||||
undefined:
|
||||
clientLoginSession.authUrl.includes("urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob")?
|
||||
undefined:
|
||||
window.location.protocol === "hyperspace:"? "hyperspace://hyperspace/app/": `https://${window.location.host}`,
|
||||
).then((tokenData: any) => {
|
||||
localStorage.getItem("baseurl") as string,
|
||||
this.state.emergencyMode
|
||||
? undefined
|
||||
: clientLoginSession.authUrl.includes(
|
||||
"urn%3Aietf%3Awg%3Aoauth%3A2.0%3Aoob"
|
||||
)
|
||||
? undefined
|
||||
: window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}`
|
||||
)
|
||||
.then((tokenData: any) => {
|
||||
localStorage.setItem("access_token", tokenData.access_token);
|
||||
window.location.href = window.location.protocol === "hyperspace:"? "hyperspace://hyperspace/app/": `https://${window.location.host}/#/`;
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(`Couldn't authorize ${this.state.brandName? this.state.brandName: "Hyperspace"}: ${err.name}`, {variant: 'error'});
|
||||
console.error(err.message);
|
||||
window.location.href =
|
||||
window.location.protocol === "hyperspace:"
|
||||
? "hyperspace://hyperspace/app/"
|
||||
: `https://${window.location.host}/#/`;
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(
|
||||
`Couldn't authorize ${
|
||||
this.state.brandName ? this.state.brandName : "Hyperspace"
|
||||
}: ${err.name}`,
|
||||
{ variant: "error" }
|
||||
);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -316,7 +373,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
if (isDarwinApp()) {
|
||||
return (
|
||||
<div className={classes.titleBarRoot}>
|
||||
<Typography className={classes.titleBarText}>{this.state.brandName? this.state.brandName: "Hyperspace"}</Typography>
|
||||
<Typography className={classes.titleBarText}>
|
||||
{this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -328,46 +387,76 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
<div>
|
||||
<Typography variant="h5">Sign in</Typography>
|
||||
<Typography>with your Mastodon account</Typography>
|
||||
<div className={classes.middlePadding}/>
|
||||
<div className={classes.middlePadding} />
|
||||
<TextField
|
||||
variant="outlined"
|
||||
label="Username"
|
||||
fullWidth
|
||||
placeholder="example@mastodon.example"
|
||||
onChange={(event) => this.updateUserInfo(event.target.value)}
|
||||
onKeyDown={(event) => this.watchUsernameField(event)}
|
||||
onChange={event => this.updateUserInfo(event.target.value)}
|
||||
onKeyDown={event => this.watchUsernameField(event)}
|
||||
error={this.state.userInputError}
|
||||
onBlur={() => this.checkForErrors()}
|
||||
></TextField>
|
||||
{
|
||||
this.state.userInputError? <Typography color="error">{this.state.userInputErrorMessage}</Typography> : null
|
||||
}
|
||||
<br/>
|
||||
{
|
||||
this.state.registerBase && this.state.federates? <Typography variant="caption">Not from <b>{this.state.registerBase? this.state.registerBase: "noinstance"}</b>? Sign in with your <Link href="https://docs.joinmastodon.org/usage/decentralization/#addressing-people" target="_blank" rel="noopener noreferrer" color="secondary">full username</Link>.</Typography>: null
|
||||
}
|
||||
<br/>
|
||||
{
|
||||
this.state.foundSavedLogin?
|
||||
{this.state.userInputError ? (
|
||||
<Typography color="error">
|
||||
{this.state.userInputErrorMessage}
|
||||
</Typography>
|
||||
) : null}
|
||||
<br />
|
||||
{this.state.registerBase && this.state.federates ? (
|
||||
<Typography variant="caption">
|
||||
Not from{" "}
|
||||
<b>
|
||||
{this.state.registerBase ? this.state.registerBase : "noinstance"}
|
||||
</b>
|
||||
? Sign in with your{" "}
|
||||
<Link
|
||||
href="https://docs.joinmastodon.org/usage/decentralization/#addressing-people"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
color="secondary"
|
||||
>
|
||||
full username
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
) : null}
|
||||
<br />
|
||||
{this.state.foundSavedLogin ? (
|
||||
<Typography>
|
||||
Signing in from a previous session? <Link className={classes.welcomeLink} onClick={() => this.resumeLogin()}>Continue login</Link>.
|
||||
</Typography>: null
|
||||
}
|
||||
Signing in from a previous session?{" "}
|
||||
<Link
|
||||
className={classes.welcomeLink}
|
||||
onClick={() => this.resumeLogin()}
|
||||
>
|
||||
Continue login
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
) : null}
|
||||
|
||||
<div className={classes.middlePadding}/>
|
||||
<div className={classes.middlePadding} />
|
||||
<div style={{ display: "flex" }}>
|
||||
<Tooltip title="Create account on site">
|
||||
<Button
|
||||
href={this.startRegistration()}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>Create account</Button>
|
||||
>
|
||||
Create account
|
||||
</Button>
|
||||
</Tooltip>
|
||||
<div className={classes.flexGrow}/>
|
||||
<div className={classes.flexGrow} />
|
||||
<Tooltip title="Continue sign-in">
|
||||
<Button color="primary" variant="contained" onClick={() => this.startLogin()}>Next</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
onClick={() => this.startLogin()}
|
||||
>
|
||||
Next
|
||||
</Button>
|
||||
</Tooltip>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -377,29 +466,43 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
const { classes } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<Typography variant="h5">Howdy, {this.state.user? this.state.user.split("@")[0]: "user"}</Typography>
|
||||
<Typography>To continue, finish signing in on your instance's website and authorize {this.state.brandName? this.state.brandName: "Hyperspace"}.</Typography>
|
||||
<div className={classes.middlePadding}/>
|
||||
<Typography variant="h5">
|
||||
Howdy, {this.state.user ? this.state.user.split("@")[0] : "user"}
|
||||
</Typography>
|
||||
<Typography>
|
||||
To continue, finish signing in on your instance's website and
|
||||
authorize {this.state.brandName ? this.state.brandName : "Hyperspace"}
|
||||
.
|
||||
</Typography>
|
||||
<div className={classes.middlePadding} />
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className={classes.flexGrow}/>
|
||||
<div className={classes.flexGrow} />
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
size="large"
|
||||
href={this.state.authUrl? this.state.authUrl: ""}
|
||||
href={this.state.authUrl ? this.state.authUrl : ""}
|
||||
>
|
||||
Authorize
|
||||
</Button>
|
||||
<div className={classes.flexGrow}/>
|
||||
<div className={classes.flexGrow} />
|
||||
</div>
|
||||
<div className={classes.middlePadding}/>
|
||||
<Typography>Having trouble signing in? <Link onClick={() => this.startEmergencyLogin()} className={classes.welcomeLink}>Sign in with a code.</Link></Typography>
|
||||
<div className={classes.middlePadding} />
|
||||
<Typography>
|
||||
Having trouble signing in?{" "}
|
||||
<Link
|
||||
onClick={() => this.startEmergencyLogin()}
|
||||
className={classes.welcomeLink}
|
||||
>
|
||||
Sign in with a code.
|
||||
</Link>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
showAuthDialog() {
|
||||
const {classes} = this.props;
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Dialog
|
||||
open={this.state.openAuthDialog}
|
||||
|
@ -408,32 +511,40 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
maxWidth="sm"
|
||||
fullWidth={true}
|
||||
>
|
||||
<DialogTitle>
|
||||
Authorize with a code
|
||||
</DialogTitle>
|
||||
<DialogTitle>Authorize with a code</DialogTitle>
|
||||
<DialogContent>
|
||||
<Typography paragraph>
|
||||
If you're having trouble authorizing Hyperspace, you can manually request for an authorization code. Click 'Request Code' and then paste the code in the authorization code box to continue.
|
||||
If you're having trouble authorizing Hyperspace, you can manually
|
||||
request for an authorization code. Click 'Request Code' and then
|
||||
paste the code in the authorization code box to continue.
|
||||
</Typography>
|
||||
<Button
|
||||
color="primary"
|
||||
variant="contained"
|
||||
href={this.state.authUrl? this.state.authUrl: ""}
|
||||
href={this.state.authUrl ? this.state.authUrl : ""}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>Request Code</Button>
|
||||
<br/><br/>
|
||||
>
|
||||
Request Code
|
||||
</Button>
|
||||
<br />
|
||||
<br />
|
||||
<TextField
|
||||
variant="outlined"
|
||||
label="Authorization code"
|
||||
fullWidth
|
||||
onChange={(event) => this.updateAuthCode(event.target.value)}
|
||||
onKeyDown={(event) => this.watchAuthField(event)}
|
||||
onChange={event => this.updateAuthCode(event.target.value)}
|
||||
onKeyDown={event => this.watchAuthField(event)}
|
||||
></TextField>
|
||||
</DialogContent>
|
||||
<DialogActions>
|
||||
<Button onClick={() => this.toggleAuthDialog()}>Cancel</Button>
|
||||
<Button color="secondary" onClick={() => this.authorizeEmergencyLogin()}>Authorize</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
onClick={() => this.authorizeEmergencyLogin()}
|
||||
>
|
||||
Authorize
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
|
@ -444,14 +555,17 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
return (
|
||||
<div>
|
||||
<Typography variant="h5">Authorizing</Typography>
|
||||
<Typography>Please wait while Hyperspace authorizes with Mastodon. This shouldn't take long...</Typography>
|
||||
<div className={classes.middlePadding}/>
|
||||
<Typography>
|
||||
Please wait while Hyperspace authorizes with Mastodon. This shouldn't
|
||||
take long...
|
||||
</Typography>
|
||||
<div className={classes.middlePadding} />
|
||||
<div style={{ display: "flex" }}>
|
||||
<div className={classes.flexGrow}/>
|
||||
<CircularProgress/>
|
||||
<div className={classes.flexGrow}/>
|
||||
<div className={classes.flexGrow} />
|
||||
<CircularProgress />
|
||||
<div className={classes.flexGrow} />
|
||||
</div>
|
||||
<div className={classes.middlePadding}/>
|
||||
<div className={classes.middlePadding} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -461,31 +575,90 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
|
|||
return (
|
||||
<div>
|
||||
{this.titlebar()}
|
||||
<div className={classes.root} style={{ backgroundImage: `url(${this.state !== null? this.state.backgroundUrl: "background.png"})`}}>
|
||||
<div
|
||||
className={classes.root}
|
||||
style={{
|
||||
backgroundImage: `url(${
|
||||
this.state !== null ? this.state.backgroundUrl : "background.png"
|
||||
})`
|
||||
}}
|
||||
>
|
||||
<Paper className={classes.paper}>
|
||||
<img className={classes.logo} alt={this.state? this.state.brandName: "Hyperspace"} src={this.state? this.state.logoUrl: "logo.png"}/>
|
||||
<br/>
|
||||
<img
|
||||
className={classes.logo}
|
||||
alt={this.state ? this.state.brandName : "Hyperspace"}
|
||||
src={this.state ? this.state.logoUrl : "logo.png"}
|
||||
/>
|
||||
<br />
|
||||
<Fade in={true}>
|
||||
{
|
||||
this.state.authority?
|
||||
this.showAuthority():
|
||||
this.state.wantsToLogin?
|
||||
this.showLoginAuth():
|
||||
this.showLanding()
|
||||
}
|
||||
{this.state.authority
|
||||
? this.showAuthority()
|
||||
: this.state.wantsToLogin
|
||||
? this.showLoginAuth()
|
||||
: this.showLanding()}
|
||||
</Fade>
|
||||
<br/>
|
||||
<br />
|
||||
<Typography variant="caption">
|
||||
© {new Date().getFullYear()} {this.state.brandName && this.state.brandName !== "Hyperspace"? `${this.state.brandName} developers and the `: ""} <Link className={classes.welcomeLink} href="https://hyperspace.marquiskurt.net" target="_blank" rel="noreferrer">Hyperspace</Link> developers. All rights reserved.
|
||||
© {new Date().getFullYear()}{" "}
|
||||
{this.state.brandName && this.state.brandName !== "Hyperspace"
|
||||
? `${this.state.brandName} developers and the `
|
||||
: ""}{" "}
|
||||
<Link
|
||||
className={classes.welcomeLink}
|
||||
href="https://hyperspace.marquiskurt.net"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Hyperspace
|
||||
</Link>{" "}
|
||||
developers. All rights reserved.
|
||||
</Typography>
|
||||
<Typography variant="caption">
|
||||
{ this.state.repo? <span>
|
||||
<Link className={classes.welcomeLink} href={this.state.repo? this.state.repo: "https://github.com/hyperspacedev"} target="_blank" rel="noreferrer">Source code</Link> | </span>: null}
|
||||
<Link className={classes.welcomeLink} href={this.state.license? this.state.license: "https://www.apache.org/licenses/LICENSE-2.0"} target="_blank" rel="noreferrer">License</Link> |
|
||||
<Link className={classes.welcomeLink} href="https://github.com/hyperspacedev/hyperspace/issues/new" target="_blank" rel="noreferrer">File an Issue</Link>
|
||||
{this.state.repo ? (
|
||||
<span>
|
||||
<Link
|
||||
className={classes.welcomeLink}
|
||||
href={
|
||||
this.state.repo
|
||||
? this.state.repo
|
||||
: "https://github.com/hyperspacedev"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
Source code
|
||||
</Link>{" "}
|
||||
|{" "}
|
||||
</span>
|
||||
) : null}
|
||||
<Link
|
||||
className={classes.welcomeLink}
|
||||
href={
|
||||
this.state.license
|
||||
? this.state.license
|
||||
: "https://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
License
|
||||
</Link>{" "}
|
||||
|
|
||||
<Link
|
||||
className={classes.welcomeLink}
|
||||
href="https://github.com/hyperspacedev/hyperspace/issues/new"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
>
|
||||
File an Issue
|
||||
</Link>
|
||||
</Typography>
|
||||
<Typography variant="caption" color="textSecondary">
|
||||
{this.state.brandName? this.state.brandName: "Hypersapce"} v.{this.state.version} {this.state.brandName && this.state.brandName !== "Hyperspace"? "(Hyperspace-like)": null}
|
||||
{this.state.brandName ? this.state.brandName : "Hypersapce"} v.
|
||||
{this.state.version}{" "}
|
||||
{this.state.brandName && this.state.brandName !== "Hyperspace"
|
||||
? "(Hyperspace-like)"
|
||||
: null}
|
||||
</Typography>
|
||||
</Paper>
|
||||
{this.showAuthDialog()}
|
||||
|
|
|
@ -1,36 +1,37 @@
|
|||
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: {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
backgroundPosition: 'center',
|
||||
backgroundRepeat: 'no-repeat',
|
||||
backgroundSize: 'cover',
|
||||
width: "100%",
|
||||
height: "100%",
|
||||
backgroundPosition: "center",
|
||||
backgroundRepeat: "no-repeat",
|
||||
backgroundSize: "cover",
|
||||
top: 0,
|
||||
left: 0,
|
||||
position: "absolute",
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
paddingTop: theme.spacing.unit * 4,
|
||||
paddingLeft: '25%',
|
||||
paddingRight: '25%',
|
||||
paddingLeft: "25%",
|
||||
paddingRight: "25%"
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
[theme.breakpoints.up("lg")]: {
|
||||
paddingTop: theme.spacing.unit * 12,
|
||||
paddingLeft: '35%',
|
||||
paddingRight: '35%',
|
||||
paddingLeft: "35%",
|
||||
paddingRight: "35%"
|
||||
}
|
||||
},
|
||||
titleBarRoot: {
|
||||
top: 0,
|
||||
left: 0,
|
||||
height: 24,
|
||||
width: '100%',
|
||||
width: "100%",
|
||||
backgroundColor: "rgba(0, 0, 0, 0.2)",
|
||||
textAlign: 'center',
|
||||
textAlign: "center",
|
||||
zIndex: 1000,
|
||||
verticalAlign: 'middle',
|
||||
WebkitUserSelect: 'none',
|
||||
verticalAlign: "middle",
|
||||
WebkitUserSelect: "none",
|
||||
WebkitAppRegion: "drag",
|
||||
position: "absolute"
|
||||
},
|
||||
|
@ -41,18 +42,18 @@ export const styles = (theme: Theme) => createStyles({
|
|||
paddingBottom: 1
|
||||
},
|
||||
paper: {
|
||||
height: '100%',
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
height: 'auto',
|
||||
height: "100%",
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
height: "auto",
|
||||
paddingLeft: theme.spacing.unit * 8,
|
||||
paddingRight: theme.spacing.unit * 8,
|
||||
paddingTop: theme.spacing.unit * 6,
|
||||
paddingTop: theme.spacing.unit * 6
|
||||
},
|
||||
paddingTop: theme.spacing.unit * 12,
|
||||
paddingLeft: theme.spacing.unit * 4,
|
||||
paddingRight: theme.spacing.unit * 4,
|
||||
paddingBottom: theme.spacing.unit * 6,
|
||||
textAlign: 'center',
|
||||
textAlign: "center"
|
||||
},
|
||||
welcomeLink: {
|
||||
color: theme.palette.primary.light
|
||||
|
@ -64,9 +65,9 @@ export const styles = (theme: Theme) => createStyles({
|
|||
height: theme.spacing.unit * 6
|
||||
},
|
||||
logo: {
|
||||
[theme.breakpoints.up('sm')]: {
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
height: 64,
|
||||
width: "auto"
|
||||
},
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -1,12 +1,24 @@
|
|||
import React, {Component} from 'react';
|
||||
import {withStyles, Typography, Paper, Avatar, 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 React, { Component } from "react";
|
||||
import {
|
||||
withStyles,
|
||||
Typography,
|
||||
Paper,
|
||||
Avatar,
|
||||
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 {
|
||||
classes: any;
|
||||
|
@ -19,21 +31,23 @@ interface IYouState {
|
|||
}
|
||||
|
||||
class You extends Component<IYouProps, IYouState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
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,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
|
||||
this.state = {
|
||||
currentAccount: this.getAccount()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
getAccount() {
|
||||
let acct = localStorage.getItem('account');
|
||||
let acct = localStorage.getItem("account");
|
||||
if (acct) {
|
||||
return JSON.parse(acct);
|
||||
}
|
||||
|
@ -43,144 +57,229 @@ class You extends Component<IYouProps, IYouState> {
|
|||
filedialog({
|
||||
multiple: false,
|
||||
accept: "image/*"
|
||||
}).then((images: FileList) => {
|
||||
})
|
||||
.then((images: FileList) => {
|
||||
if (images.length > 0) {
|
||||
this.props.enqueueSnackbar("Updating avatar...", { persist: true, key: "persistAvatar" });
|
||||
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) => {
|
||||
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) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("persistAvatar");
|
||||
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name, { variant: "error" });
|
||||
})
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't update avatar: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
|
||||
});
|
||||
}
|
||||
|
||||
updateHeader() {
|
||||
filedialog({
|
||||
multiple: false,
|
||||
accept: "image/*"
|
||||
}).then((images: FileList) => {
|
||||
})
|
||||
.then((images: FileList) => {
|
||||
if (images.length > 0) {
|
||||
this.props.enqueueSnackbar("Updating header...", { persist: true, key: "persistHeader" });
|
||||
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) => {
|
||||
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) => {
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("persistHeader");
|
||||
this.props.enqueueSnackbar("Couldn't update header: " + err.name, { variant: "error" });
|
||||
})
|
||||
this.props.enqueueSnackbar(
|
||||
"Couldn't update header: " + err.name,
|
||||
{ variant: "error" }
|
||||
);
|
||||
});
|
||||
}
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
|
||||
})
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
|
||||
});
|
||||
}
|
||||
|
||||
removeHTMLContent(text: string) {
|
||||
const div = document.createElement('div');
|
||||
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
|
||||
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));
|
||||
.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" })
|
||||
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.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"});
|
||||
})
|
||||
.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})
|
||||
updateBio(bio: string) {
|
||||
this.setState({ newBio: bio });
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
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.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/>
|
||||
<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>
|
||||
<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/>
|
||||
<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"
|
||||
<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>
|
||||
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/>
|
||||
<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"}
|
||||
<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"
|
||||
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>
|
||||
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>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { MastodonEmoji } from './Emojis';
|
||||
import { Field } from './Field';
|
||||
import { MastodonEmoji } from "./Emojis";
|
||||
import { Field } from "./Field";
|
||||
|
||||
/**
|
||||
* Basic type for an account on Mastodon
|
||||
|
@ -24,11 +24,11 @@ export type Account = {
|
|||
moved: Account | null;
|
||||
fields: [Field];
|
||||
bot: boolean | null;
|
||||
}
|
||||
};
|
||||
|
||||
export type UAccount = {
|
||||
id: string;
|
||||
acct: string;
|
||||
display_name: string;
|
||||
avatar_static: string;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -10,4 +10,4 @@ export type Attachment = {
|
|||
text_url: string | null;
|
||||
meta: any | null;
|
||||
description: string | null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -14,4 +14,4 @@ export type Card = {
|
|||
html: string | null;
|
||||
width: number | null;
|
||||
height: number | null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -17,15 +17,15 @@ export type Config = {
|
|||
};
|
||||
license: License;
|
||||
repository?: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type License = {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
};
|
||||
|
||||
export type Federation = {
|
||||
universalLogin: boolean;
|
||||
allowPublicPosts: boolean;
|
||||
enablePublicTimeline: boolean;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Status } from './Status';
|
||||
import { Status } from "./Status";
|
||||
|
||||
export type Context = {
|
||||
ancestors: [Status];
|
||||
descendants: [Status];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -5,4 +5,4 @@ export type Field = {
|
|||
name: string;
|
||||
value: string;
|
||||
verified_at: string | null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,6 +1,21 @@
|
|||
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 { isDarwinApp } from '../utilities/desktop';
|
||||
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 { isDarwinApp } from "../utilities/desktop";
|
||||
|
||||
/**
|
||||
* Basic theme colors for Hyperspace.
|
||||
|
@ -9,14 +24,18 @@ export type HyperspaceTheme = {
|
|||
key: string;
|
||||
name: string;
|
||||
palette: {
|
||||
primary: {
|
||||
primary:
|
||||
| {
|
||||
main: string;
|
||||
} | Color;
|
||||
secondary: {
|
||||
main: string;
|
||||
} | Color;
|
||||
}
|
||||
}
|
||||
| Color;
|
||||
secondary:
|
||||
| {
|
||||
main: string;
|
||||
}
|
||||
| Color;
|
||||
};
|
||||
};
|
||||
|
||||
export const defaultTheme: HyperspaceTheme = {
|
||||
key: "defaultTheme",
|
||||
|
@ -25,7 +44,7 @@ export const defaultTheme: HyperspaceTheme = {
|
|||
primary: deepPurple,
|
||||
secondary: red
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const gardenerTheme: HyperspaceTheme = {
|
||||
key: "gardnerTheme",
|
||||
|
@ -34,7 +53,7 @@ export const gardenerTheme: HyperspaceTheme = {
|
|||
primary: lightGreen,
|
||||
secondary: yellow
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const teacherTheme: HyperspaceTheme = {
|
||||
key: "teacherTheme",
|
||||
|
@ -43,7 +62,7 @@ export const teacherTheme: HyperspaceTheme = {
|
|||
primary: purple,
|
||||
secondary: deepOrange
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const jokerTheme: HyperspaceTheme = {
|
||||
key: "jokerTheme",
|
||||
|
@ -52,7 +71,7 @@ export const jokerTheme: HyperspaceTheme = {
|
|||
primary: indigo,
|
||||
secondary: lightBlue
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const guardTheme: HyperspaceTheme = {
|
||||
key: "guardTheme",
|
||||
|
@ -61,7 +80,7 @@ export const guardTheme: HyperspaceTheme = {
|
|||
primary: blue,
|
||||
secondary: deepOrange
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const entertainerTheme: HyperspaceTheme = {
|
||||
key: "entertainerTheme",
|
||||
|
@ -70,7 +89,7 @@ export const entertainerTheme: HyperspaceTheme = {
|
|||
primary: pink,
|
||||
secondary: purple
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const classicTheme: HyperspaceTheme = {
|
||||
key: "classicTheme",
|
||||
|
@ -83,7 +102,7 @@ export const classicTheme: HyperspaceTheme = {
|
|||
main: "#5c2d91"
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const dragonTheme: HyperspaceTheme = {
|
||||
key: "dragonTheme",
|
||||
|
@ -92,7 +111,7 @@ export const dragonTheme: HyperspaceTheme = {
|
|||
primary: purple,
|
||||
secondary: purple
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const memoriumTheme: HyperspaceTheme = {
|
||||
key: "memoriumTheme",
|
||||
|
@ -101,7 +120,7 @@ export const memoriumTheme: HyperspaceTheme = {
|
|||
primary: red,
|
||||
secondary: red
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const blissTheme: HyperspaceTheme = {
|
||||
key: "blissTheme",
|
||||
|
@ -112,19 +131,31 @@ export const blissTheme: HyperspaceTheme = {
|
|||
},
|
||||
secondary: lightBlue
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const attractTheme: HyperspaceTheme = {
|
||||
key: "attractTheme",
|
||||
name: "Attract",
|
||||
palette: {
|
||||
primary: {
|
||||
main: '#E57373',
|
||||
main: "#E57373"
|
||||
},
|
||||
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
|
||||
];
|
||||
|
|
|
@ -12,4 +12,4 @@ export type Instance = {
|
|||
stats: Field;
|
||||
languages: [string];
|
||||
contact_account: Account;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,4 +6,4 @@ export type Mention = {
|
|||
username: string;
|
||||
acct: string;
|
||||
id: string;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -7,4 +7,4 @@ export type Notification = {
|
|||
created_at: string;
|
||||
account: Account;
|
||||
status: Status | null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,7 +9,7 @@ export type Poll = {
|
|||
votes_count: number;
|
||||
options: [PollOption];
|
||||
voted: boolean | null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Basic type for a Poll option in a Poll
|
||||
|
@ -17,14 +17,14 @@ export type Poll = {
|
|||
export type PollOption = {
|
||||
title: string;
|
||||
votes_count: number | null;
|
||||
}
|
||||
};
|
||||
|
||||
export type PollWizard = {
|
||||
expires_at: string;
|
||||
multiple: boolean;
|
||||
options: PollWizardOption[];
|
||||
}
|
||||
};
|
||||
|
||||
export type PollWizardOption = {
|
||||
title: string;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -9,4 +9,4 @@ export type Relationship = {
|
|||
domain_blocking: boolean;
|
||||
showing_reblogs: boolean;
|
||||
endorsed: boolean;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -6,4 +6,4 @@ export type Results = {
|
|||
accounts: [Account];
|
||||
statuses: [Status];
|
||||
hashtags: [Tag];
|
||||
}
|
||||
};
|
||||
|
|
|
@ -3,4 +3,4 @@ export type SaveClientSession = {
|
|||
clientSecret: string;
|
||||
authUrl: string;
|
||||
emergency: boolean;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,11 +1,11 @@
|
|||
import { MastodonEmoji } from './Emojis';
|
||||
import { Visibility } from './Visibility';
|
||||
import { Account } from './Account';
|
||||
import { Attachment } from './Attachment';
|
||||
import { Mention } from './Mention';
|
||||
import { Poll } from './Poll';
|
||||
import { Card } from './Card';
|
||||
import { Tag } from './Tag';
|
||||
import { MastodonEmoji } from "./Emojis";
|
||||
import { Visibility } from "./Visibility";
|
||||
import { Account } from "./Account";
|
||||
import { Attachment } from "./Attachment";
|
||||
import { Mention } from "./Mention";
|
||||
import { Poll } from "./Poll";
|
||||
import { Card } from "./Card";
|
||||
import { Tag } from "./Tag";
|
||||
|
||||
/**
|
||||
* Basic type for a status on Mastodon
|
||||
|
@ -37,4 +37,4 @@ export type Status = {
|
|||
poll: Poll | null;
|
||||
application: any;
|
||||
pinned: boolean | null;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import Mastodon from "megalodon";
|
||||
|
||||
export function userLoggedIn(): boolean {
|
||||
if (localStorage.getItem('baseurl') && localStorage.getItem('access_token')) {
|
||||
if (localStorage.getItem("baseurl") && localStorage.getItem("access_token")) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
|
@ -9,13 +9,22 @@ export function userLoggedIn(): boolean {
|
|||
}
|
||||
|
||||
export function refreshUserAccountData() {
|
||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
||||
client.get('/accounts/verify_credentials').then((resp: any) => {
|
||||
localStorage.setItem('account', JSON.stringify(resp.data));
|
||||
}).catch((err: Error) => {
|
||||
let client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
(localStorage.getItem("baseurl") as string) + "/api/v1"
|
||||
);
|
||||
client
|
||||
.get("/accounts/verify_credentials")
|
||||
.then((resp: any) => {
|
||||
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"))
|
||||
})
|
||||
client.get("/instance").then((resp: any) => {
|
||||
localStorage.setItem(
|
||||
"isPleroma",
|
||||
resp.data.version.match(/Pleroma/) ? "true" : "false"
|
||||
);
|
||||
});
|
||||
}
|
|
@ -17,7 +17,7 @@ export function isDesktopApp(): boolean {
|
|||
* Determines whether the app is the macOS application
|
||||
*/
|
||||
export function isDarwinApp(): boolean {
|
||||
return isDesktopApp() && navigator.userAgent.includes("Macintosh")
|
||||
return isDesktopApp() && navigator.userAgent.includes("Macintosh");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,6 +26,6 @@ export function isDarwinApp(): boolean {
|
|||
export function isDarkMode() {
|
||||
// Lift window to an ElectronWindow and add use require()
|
||||
const eWin = window as ElectronWindow;
|
||||
const {remote} = eWin.require('electron');
|
||||
return remote.systemPreferences.isDarkMode()
|
||||
const { remote } = eWin.require("electron");
|
||||
return remote.systemPreferences.isDarkMode();
|
||||
}
|
|
@ -1,5 +1,5 @@
|
|||
import {MastodonEmoji} from '../types/Emojis';
|
||||
import Mastodon from 'megalodon';
|
||||
import { MastodonEmoji } from "../types/Emojis";
|
||||
import Mastodon from "megalodon";
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @returns String with image tags for emojis
|
||||
*/
|
||||
export function emojifyString(contents: string, emojis: [MastodonEmoji], className?: any): string {
|
||||
export function emojifyString(
|
||||
contents: string,
|
||||
emojis: [MastodonEmoji],
|
||||
className?: any
|
||||
): string {
|
||||
let newContents: string = contents;
|
||||
|
||||
emojis.forEach((emoji: MastodonEmoji) => {
|
||||
let filter = new RegExp(`:${emoji.shortcode}:`, 'g');
|
||||
newContents = newContents.replace(filter, `<img src=${emoji.static_url} ${className? `class="${className}"`: ""}/>`)
|
||||
})
|
||||
let filter = new RegExp(`:${emoji.shortcode}:`, "g");
|
||||
newContents = newContents.replace(
|
||||
filter,
|
||||
`<img src=${emoji.static_url} ${
|
||||
className ? `class="${className}"` : ""
|
||||
}/>`
|
||||
);
|
||||
});
|
||||
|
||||
return newContents;
|
||||
}
|
||||
|
||||
export function collectEmojisFromServer() {
|
||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
let emojisPath = localStorage.getItem('emojis');
|
||||
let client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
let emojisPath = localStorage.getItem("emojis");
|
||||
let emojis: any[] = [];
|
||||
if (emojisPath === null) {
|
||||
client.get('/custom_emojis').then((resp: any) => {
|
||||
client
|
||||
.get("/custom_emojis")
|
||||
.then((resp: any) => {
|
||||
resp.data.forEach((emoji: MastodonEmoji) => {
|
||||
let customEmoji = {
|
||||
name: emoji.shortcode,
|
||||
emoticons: [''],
|
||||
emoticons: [""],
|
||||
short_names: [emoji.shortcode],
|
||||
imageUrl: emoji.static_url,
|
||||
keywords: ['mastodon', 'custom']
|
||||
}
|
||||
keywords: ["mastodon", "custom"]
|
||||
};
|
||||
emojis.push(customEmoji);
|
||||
localStorage.setItem("emojis", JSON.stringify(emojis));
|
||||
});
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
.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
|
||||
|
@ -7,29 +7,46 @@ import Mastodon from 'megalodon';
|
|||
* @param baseurl The base URL of the instance
|
||||
* @param redirect_uri The URL to redirect to when authorizing
|
||||
*/
|
||||
export function createHyperspaceApp(name: string, scopes: string, baseurl: string, redirect_uri: string) {
|
||||
let appName = name === "Hyperspace"? "Hyperspace": `${name} (Hyperspace-like)`
|
||||
return Mastodon.createApp(appName, {
|
||||
export function createHyperspaceApp(
|
||||
name: string,
|
||||
scopes: string,
|
||||
baseurl: string,
|
||||
redirect_uri: string
|
||||
) {
|
||||
let appName =
|
||||
name === "Hyperspace" ? "Hyperspace" : `${name} (Hyperspace-like)`;
|
||||
return Mastodon.createApp(
|
||||
appName,
|
||||
{
|
||||
scopes: scopes,
|
||||
redirect_uris: redirect_uri,
|
||||
website: 'https://hyperspace.marquiskurt.net',
|
||||
}, baseurl).then(appData => {
|
||||
return Mastodon.generateAuthUrl(appData.clientId, appData.clientSecret, {
|
||||
website: "https://hyperspace.marquiskurt.net"
|
||||
},
|
||||
baseurl
|
||||
).then(appData => {
|
||||
return Mastodon.generateAuthUrl(
|
||||
appData.clientId,
|
||||
appData.clientSecret,
|
||||
{
|
||||
redirect_uri: redirect_uri,
|
||||
scope: scopes
|
||||
}, baseurl).then(url => {
|
||||
},
|
||||
baseurl
|
||||
).then(url => {
|
||||
appData.url = url;
|
||||
return appData;
|
||||
})
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the appropriate redirect address.
|
||||
* @param type The address or configuration to use
|
||||
*/
|
||||
export function getRedirectAddress(type: "desktop" | "dynamic" | string): string {
|
||||
switch(type) {
|
||||
export function getRedirectAddress(
|
||||
type: "desktop" | "dynamic" | string
|
||||
): string {
|
||||
switch (type) {
|
||||
case "desktop":
|
||||
return "hyperspace://hyperspace/app/";
|
||||
case "dynamic":
|
||||
|
|
|
@ -1,22 +1,24 @@
|
|||
import {getUserDefaultBool, setUserDefaultBool} from './settings';
|
||||
import { getUserDefaultBool, setUserDefaultBool } from "./settings";
|
||||
|
||||
/**
|
||||
* Get the person's permission to send notification requests.
|
||||
*/
|
||||
export function getNotificationRequestPermission() {
|
||||
if ('Notification' in window) {
|
||||
if ("Notification" in window) {
|
||||
Notification.requestPermission();
|
||||
let request = Notification.permission;
|
||||
if (request === "granted") {
|
||||
setUserDefaultBool('enablePushNotifications', true);
|
||||
setUserDefaultBool('userDeniedNotification', false);
|
||||
setUserDefaultBool("enablePushNotifications", true);
|
||||
setUserDefaultBool("userDeniedNotification", false);
|
||||
} else {
|
||||
setUserDefaultBool('enablePushNotifications', false);
|
||||
setUserDefaultBool('userDeniedNotification', true);
|
||||
setUserDefaultBool("enablePushNotifications", false);
|
||||
setUserDefaultBool("userDeniedNotification", true);
|
||||
}
|
||||
} else {
|
||||
console.warn("Notifications aren't supported in this browser. The setting will be disabled.");
|
||||
setUserDefaultBool('enablePushNotifications', false);
|
||||
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
|
||||
*/
|
||||
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`
|
||||
*/
|
||||
export function canSendNotifications() {
|
||||
return getUserDefaultBool('enablePushNotifications');
|
||||
return getUserDefaultBool("enablePushNotifications");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -51,6 +53,6 @@ export function sendNotificationRequest(title: string, body: string) {
|
|||
window.focus();
|
||||
};
|
||||
} 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 { getNotificationRequestPermission } from './notifications';
|
||||
import axios from 'axios';
|
||||
import { getNotificationRequestPermission } from "./notifications";
|
||||
import axios from "axios";
|
||||
import { Config } from "../types/Config";
|
||||
import { Visibility } from "../types/Visibility";
|
||||
|
||||
type SettingsTemplate = {
|
||||
[key:string]: any;
|
||||
[key: string]: any;
|
||||
darkModeEnabled: boolean;
|
||||
systemDecidesDarkMode: boolean;
|
||||
enablePushNotifications: boolean;
|
||||
clearNotificationsOnRead: boolean;
|
||||
displayAllOnNotificationBadge: boolean;
|
||||
defaultVisibility: string;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Gets the user default from localStorage
|
||||
|
@ -21,7 +21,9 @@ type SettingsTemplate = {
|
|||
*/
|
||||
export function getUserDefaultBool(key: string): boolean {
|
||||
if (localStorage.getItem(key) === null) {
|
||||
console.warn('This key has not been set before, so the default value is FALSE for now.');
|
||||
console.warn(
|
||||
"This key has not been set before, so the default value is FALSE for now."
|
||||
);
|
||||
return false;
|
||||
} else {
|
||||
return localStorage.getItem(key) === "true";
|
||||
|
@ -35,7 +37,7 @@ export function getUserDefaultBool(key: string): boolean {
|
|||
*/
|
||||
export function setUserDefaultBool(key: string, value: boolean) {
|
||||
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());
|
||||
}
|
||||
|
@ -46,7 +48,9 @@ export function setUserDefaultBool(key: string, value: boolean) {
|
|||
*/
|
||||
export function getUserDefaultVisibility(): Visibility {
|
||||
if (localStorage.getItem("defaultVisibility") === null) {
|
||||
console.warn('This key has not been set before, so the default value is PUBLIC for now.');
|
||||
console.warn(
|
||||
"This key has not been set before, so the default value is PUBLIC for now."
|
||||
);
|
||||
return "public";
|
||||
} else {
|
||||
return localStorage.getItem("defaultVisibility") as Visibility;
|
||||
|
@ -59,7 +63,7 @@ export function getUserDefaultVisibility(): Visibility {
|
|||
*/
|
||||
export function setUserDefaultVisibility(key: string) {
|
||||
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());
|
||||
}
|
||||
|
@ -69,8 +73,8 @@ export function setUserDefaultVisibility(key: string) {
|
|||
*/
|
||||
export function getUserDefaultTheme() {
|
||||
let returnTheme = defaultTheme;
|
||||
themes.forEach((theme) => {
|
||||
if(theme.key === localStorage.getItem('theme')) {
|
||||
themes.forEach(theme => {
|
||||
if (theme.key === localStorage.getItem("theme")) {
|
||||
returnTheme = theme;
|
||||
}
|
||||
});
|
||||
|
@ -82,7 +86,7 @@ export function getUserDefaultTheme() {
|
|||
* @param themeName The name of the theme
|
||||
*/
|
||||
export function setUserDefaultTheme(themeName: string) {
|
||||
localStorage.setItem('theme', themeName);
|
||||
localStorage.setItem("theme", themeName);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -96,9 +100,15 @@ export function createUserDefaults() {
|
|||
clearNotificationsOnRead: false,
|
||||
displayAllOnNotificationBadge: false,
|
||||
defaultVisibility: "public"
|
||||
}
|
||||
};
|
||||
|
||||
let settings = ["darkModeEnabled", "systemDecidesDarkMode", "clearNotificationsOnRead", "displayAllOnNotificationBadge", "defaultVisibility"];
|
||||
let settings = [
|
||||
"darkModeEnabled",
|
||||
"systemDecidesDarkMode",
|
||||
"clearNotificationsOnRead",
|
||||
"displayAllOnNotificationBadge",
|
||||
"defaultVisibility"
|
||||
];
|
||||
|
||||
migrateExistingSettings();
|
||||
|
||||
|
@ -109,9 +119,8 @@ export function createUserDefaults() {
|
|||
} else {
|
||||
localStorage.setItem(setting, defaults[setting].toString());
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
});
|
||||
getNotificationRequestPermission();
|
||||
}
|
||||
|
||||
|
@ -121,17 +130,21 @@ export function createUserDefaults() {
|
|||
*/
|
||||
export async function getConfig(): Promise<Config | undefined> {
|
||||
try {
|
||||
const resp = await axios.get('config.json');
|
||||
const resp = await axios.get("config.json");
|
||||
let config: Config = resp.data;
|
||||
return config;
|
||||
}
|
||||
catch (err) {
|
||||
console.error("Couldn't configure Hyperspace with the config file. Reason: " + err.name);
|
||||
} catch (err) {
|
||||
console.error(
|
||||
"Couldn't configure Hyperspace with the config file. Reason: " + err.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export function migrateExistingSettings() {
|
||||
if (localStorage.getItem('prefers-dark-mode')) {
|
||||
setUserDefaultBool('darkModeEnabled', localStorage.getItem('prefers-dark-mode') === "true")
|
||||
if (localStorage.getItem("prefers-dark-mode")) {
|
||||
setUserDefaultBool(
|
||||
"darkModeEnabled",
|
||||
localStorage.getItem("prefers-dark-mode") === "true"
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,7 +1,11 @@
|
|||
import { createMuiTheme, Theme } from '@material-ui/core';
|
||||
import { HyperspaceTheme, themes, defaultTheme } from '../types/HyperspaceTheme';
|
||||
import { getUserDefaultBool } from './settings';
|
||||
import { isDarwinApp, isDarkMode } from './desktop';
|
||||
import { createMuiTheme, Theme } from "@material-ui/core";
|
||||
import {
|
||||
HyperspaceTheme,
|
||||
themes,
|
||||
defaultTheme
|
||||
} from "../types/HyperspaceTheme";
|
||||
import { getUserDefaultBool } from "./settings";
|
||||
import { isDarwinApp, isDarkMode } from "./desktop";
|
||||
|
||||
/**
|
||||
* Locates a Hyperspace theme from the themes catalog
|
||||
|
@ -27,35 +31,38 @@ export function setHyperspaceTheme(theme: HyperspaceTheme): Theme {
|
|||
return createMuiTheme({
|
||||
typography: {
|
||||
fontFamily: [
|
||||
'-apple-system',
|
||||
'BlinkMacSystemFont',
|
||||
"-apple-system",
|
||||
"BlinkMacSystemFont",
|
||||
'"Segoe UI"',
|
||||
'Roboto',
|
||||
"Roboto",
|
||||
'"Helvetica Neue"',
|
||||
'Arial',
|
||||
'sans-serif',
|
||||
"Arial",
|
||||
"sans-serif",
|
||||
'"Apple Color Emoji"',
|
||||
'"Segoe UI Emoji"',
|
||||
'"Segoe UI Symbol"',
|
||||
].join(','),
|
||||
useNextVariants: true,
|
||||
'"Segoe UI Symbol"'
|
||||
].join(","),
|
||||
useNextVariants: true
|
||||
},
|
||||
palette: {
|
||||
primary: theme.palette.primary,
|
||||
secondary: theme.palette.secondary,
|
||||
type: getUserDefaultBool('darkModeEnabled')? "dark":
|
||||
getDarkModeFromSystem() === "dark"? "dark": "light"
|
||||
type: getUserDefaultBool("darkModeEnabled")
|
||||
? "dark"
|
||||
: getDarkModeFromSystem() === "dark"
|
||||
? "dark"
|
||||
: "light"
|
||||
}
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
export function getDarkModeFromSystem(): string {
|
||||
if (getUserDefaultBool('systemDecidesDarkMode')) {
|
||||
if (getUserDefaultBool("systemDecidesDarkMode")) {
|
||||
if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
|
||||
return "dark";
|
||||
} else {
|
||||
if (isDarwinApp()) {
|
||||
return isDarkMode()? "dark": "light";
|
||||
return isDarkMode() ? "dark" : "light";
|
||||
} else {
|
||||
return "light";
|
||||
}
|
||||
|
@ -72,9 +79,9 @@ export function getDarkModeFromSystem(): string {
|
|||
*/
|
||||
export function darkMode(theme: Theme, setting: boolean): Theme {
|
||||
if (setting) {
|
||||
theme.palette.type = 'dark';
|
||||
theme.palette.type = "dark";
|
||||
} else {
|
||||
theme.palette.type = 'light';
|
||||
theme.palette.type = "light";
|
||||
}
|
||||
return theme;
|
||||
}
|
Loading…
Reference in New Issue