Work on adding basic posts
This commit is contained in:
parent
911a25506d
commit
b8f9035be1
|
@ -945,9 +945,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"@material-ui/core": {
|
||||
"version": "3.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.2.tgz",
|
||||
"integrity": "sha512-aukR3mSH3g115St2OnqoeMRtmxzxxx+Mch7pFKRV3Tz3URExBlZwOolimjxKZpG4LGec8HlhREawafLsDzjVWQ==",
|
||||
"version": "3.9.3",
|
||||
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.3.tgz",
|
||||
"integrity": "sha512-REIj62+zEvTgI/C//YL4fZxrCVIySygmpZglsu/Nl5jPqy3CDjZv1F9ubBYorHqmRgeVPh64EghMMWqk4egmfg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.2.0",
|
||||
|
@ -1272,6 +1272,15 @@
|
|||
"@types/react-router": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-swipeable-views": {
|
||||
"version": "0.13.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-swipeable-views/-/react-swipeable-views-0.13.0.tgz",
|
||||
"integrity": "sha512-orrreCcXev6IUXDuHf07RDDCAoIZRMSr95eyWmYNRfjic7w/O+68iPu0NCysVls+UygRNvoqZMuXI72N/58E1w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
},
|
||||
"@types/react-transition-group": {
|
||||
"version": "2.0.16",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.0.16.tgz",
|
||||
|
@ -4494,6 +4503,17 @@
|
|||
"sha.js": "^2.4.8"
|
||||
}
|
||||
},
|
||||
"create-react-class": {
|
||||
"version": "15.6.3",
|
||||
"resolved": "https://registry.npmjs.org/create-react-class/-/create-react-class-15.6.3.tgz",
|
||||
"integrity": "sha512-M+/3Q6E6DLO6Yx3OwrWjwHBnvfXXYA7W+dFjt/ZDBemHO1DDZhsalX/NUtnTYclN6GfnBDRh4qRHjcDHmlJBJg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fbjs": "^0.8.9",
|
||||
"loose-envify": "^1.3.1",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"create-react-context": {
|
||||
"version": "0.2.3",
|
||||
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.3.tgz",
|
||||
|
@ -10424,6 +10444,12 @@
|
|||
"array-includes": "^3.0.3"
|
||||
}
|
||||
},
|
||||
"keycode": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.0.tgz",
|
||||
"integrity": "sha1-PQr1bce4uOXLqNCpfxByBO7CKwQ=",
|
||||
"dev": true
|
||||
},
|
||||
"killable": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz",
|
||||
|
@ -10786,9 +10812,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"megalodon": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.5.1.tgz",
|
||||
"integrity": "sha512-gnE12mA2zClg0fsPo6ARszJiUGpbM48hL1kWtK/3M8KrrnG7V33cyg9qwaCZNkOmmu+aUxZjesgTlG3PT1mI0A==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.6.0.tgz",
|
||||
"integrity": "sha512-SXk6DqM02NJGLppr4XpCKDX3HqsPBS1fyuxm19IPmMlHRerChVyIDoWlmxlAEq3jdcCQrMmVVC/E5QuoO5uc0Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/oauth": "^0.9.0",
|
||||
|
@ -11094,6 +11120,12 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"moment": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/moment/-/moment-2.24.0.tgz",
|
||||
"integrity": "sha512-bV7f+6l2QigeBBZSM/6yTNq4P2fNpSWj/0e7jQcy87A8e7o2nAfP/34/2ky5Vw4B9S446EtIhodAzkFCcR4dQg==",
|
||||
"dev": true
|
||||
},
|
||||
"move-concurrently": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz",
|
||||
|
@ -11362,6 +11394,16 @@
|
|||
"integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==",
|
||||
"dev": true
|
||||
},
|
||||
"notistack": {
|
||||
"version": "0.5.1",
|
||||
"resolved": "https://registry.npmjs.org/notistack/-/notistack-0.5.1.tgz",
|
||||
"integrity": "sha512-Pym0iWYCDFsC3imDL+zx4lWjkeEu6s/mlIDq6m+VRpHPPNFJErNcHCqo+xakvkkpOOR3fz4Y802GtMk8GDFVIA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"classnames": "^2.2.6",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"npm-run-path": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz",
|
||||
|
@ -14846,15 +14888,15 @@
|
|||
}
|
||||
},
|
||||
"react": {
|
||||
"version": "16.8.5",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.8.5.tgz",
|
||||
"integrity": "sha512-daCb9TD6FZGvJ3sg8da1tRAtIuw29PbKZW++NN4wqkbEvxL+bZpaaYb4xuftW/SpXmgacf1skXl/ddX6CdOlDw==",
|
||||
"version": "16.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.8.6.tgz",
|
||||
"integrity": "sha512-pC0uMkhLaHm11ZSJULfOBqV4tIZkx87ZLvbbQYunNixAAvjnC+snJCg0XQXn9VIsttVsbZP/H/ewzgsd5fxKXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.5"
|
||||
"scheduler": "^0.13.6"
|
||||
}
|
||||
},
|
||||
"react-app-polyfill": {
|
||||
|
@ -15004,15 +15046,15 @@
|
|||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.8.5",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.5.tgz",
|
||||
"integrity": "sha512-VIEIvZLpFafsfu4kgmftP5L8j7P1f0YThfVTrANMhZUFMDOsA6e0kfR6wxw/8xxKs4NB59TZYbxNdPCDW34x4w==",
|
||||
"version": "16.8.6",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.8.6.tgz",
|
||||
"integrity": "sha512-1nL7PIq9LTL3fthPqwkvr2zY7phIPjYrT0jp4HjyEQrEROnw4dG41VVwi/wfoCneoleqrNX7iAD+pXebJZwrwA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.13.5"
|
||||
"scheduler": "^0.13.6"
|
||||
}
|
||||
},
|
||||
"react-error-overlay": {
|
||||
|
@ -15149,10 +15191,99 @@
|
|||
"workbox-webpack-plugin": "3.6.3"
|
||||
}
|
||||
},
|
||||
"react-swipeable-views": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-swipeable-views/-/react-swipeable-views-0.13.1.tgz",
|
||||
"integrity": "sha512-zZIRBD+HFO0P5z3TCkDMsSG/Sc3LXGGtojO+dEAGxsPvZjc3ji4Z/oOcLjZ4ozl0xIlLQJZ89o1rR82H/MDBHw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "7.0.0",
|
||||
"dom-helpers": "^3.2.1",
|
||||
"prop-types": "^15.5.4",
|
||||
"react-swipeable-views-core": "^0.13.1",
|
||||
"react-swipeable-views-utils": "^0.13.1",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
|
||||
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
|
||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-swipeable-views-core": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-swipeable-views-core/-/react-swipeable-views-core-0.13.1.tgz",
|
||||
"integrity": "sha512-EP8sCvvD7VDiZLglPt9icMuMNu8qLRLk0ab/fB1HXv7lX8ClnwF3UMCM0ZrN3sguSY7CsX3LevducGGsT1VcDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "7.0.0",
|
||||
"warning": "^4.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
|
||||
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
|
||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-swipeable-views-utils": {
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/react-swipeable-views-utils/-/react-swipeable-views-utils-0.13.1.tgz",
|
||||
"integrity": "sha512-r5eyPIIHnlvILvFHXGnASDXa3RwC6X2YevegA2II5fhzYqwQRpJ81+lnzknNg5hiIN6ucXc5vrkqRRjabAhMpA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/runtime": "7.0.0",
|
||||
"fbjs": "^0.8.4",
|
||||
"keycode": "^2.1.7",
|
||||
"prop-types": "^15.6.0",
|
||||
"react-event-listener": "^0.6.0",
|
||||
"react-swipeable-views-core": "^0.13.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.0.0.tgz",
|
||||
"integrity": "sha512-7hGhzlcmg01CvH1EHdSPVXYX1aJ8KCEyz6I9xYIi/asDtzBPMyMhVibhM/K6g/5qnKBwjZtp10bNZIEFTRW1MA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"regenerator-runtime": "^0.12.0"
|
||||
}
|
||||
},
|
||||
"regenerator-runtime": {
|
||||
"version": "0.12.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.12.1.tgz",
|
||||
"integrity": "sha512-odxIc1/vDlo4iZcfXqRYFj0vpXFNoGdKMAUieAlFYO6m/nl5e9KR/beGf41z4a1FI+aQgtjhuaSlDxQ0hmkrHg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"react-transition-group": {
|
||||
"version": "2.7.0",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.0.tgz",
|
||||
"integrity": "sha512-CzF22K0x6arjQO4AxkasMaiYcFG/QH0MhPNs45FmNsfWsQmsO9jv52sIZJAalnlryD5RgrrbLtV5CMJSokrrMA==",
|
||||
"version": "2.7.1",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.1.tgz",
|
||||
"integrity": "sha512-b0VJTzNRnXxRpCuxng6QJbAzmmrhBn1BZJfPPnHbH2PIo8msdkajqwtfdyGm/OypPXZNfAHKEqeN15wjMXrRJQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dom-helpers": "^3.3.1",
|
||||
|
@ -15161,6 +15292,43 @@
|
|||
"react-lifecycles-compat": "^3.0.4"
|
||||
}
|
||||
},
|
||||
"react-web-share-api": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-web-share-api/-/react-web-share-api-0.0.2.tgz",
|
||||
"integrity": "sha1-pp7BMEYXHnb3JIq8XU7ufdj3slo=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"react": "^15.5.0",
|
||||
"react-dom": "^15.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-15.6.2.tgz",
|
||||
"integrity": "sha1-26BDSrQ5z+gvEI8PURZjkIF5qnI=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"create-react-class": "^15.6.0",
|
||||
"fbjs": "^0.8.9",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "15.6.2",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-15.6.2.tgz",
|
||||
"integrity": "sha1-Qc+t9pO3V/rycIRDodH9WgK+9zA=",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fbjs": "^0.8.9",
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"prop-types": "^15.5.10"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz",
|
||||
|
@ -16375,9 +16543,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.13.5",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.5.tgz",
|
||||
"integrity": "sha512-K98vjkQX9OIt/riLhp6F+XtDPtMQhqNcf045vsh+pcuvHq+PHy1xCrH3pq1P40m6yR46lpVvVhKdEOtnimuUJw==",
|
||||
"version": "0.13.6",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.13.6.tgz",
|
||||
"integrity": "sha512-IWnObHt413ucAYKsD9J1QShUKkbKLQQHdxRyw73sw4FN26iWr3DY/H34xGPe4nmL1DwXyWmSWmMrA9TfQbE/XQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
|
|
17
package.json
17
package.json
|
@ -3,22 +3,27 @@
|
|||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@material-ui/core": "^3.9.2",
|
||||
"@material-ui/core": "^3.9.3",
|
||||
"@material-ui/icons": "^3.0.2",
|
||||
"@types/jest": "24.0.11",
|
||||
"@types/node": "11.11.6",
|
||||
"@types/react": "16.8.8",
|
||||
"@types/react-dom": "16.8.3",
|
||||
"@types/react-router-dom": "^4.3.1",
|
||||
"megalodon": "latest",
|
||||
"react": "^16.8.5",
|
||||
"react-dom": "^16.8.5",
|
||||
"@types/react-swipeable-views": "latest",
|
||||
"megalodon": "^0.6.0",
|
||||
"moment": "^2.24.0",
|
||||
"react": "^16.8.6",
|
||||
"react-dom": "^16.8.6",
|
||||
"react-router-dom": "^5.0.0",
|
||||
"react-scripts": "2.1.8",
|
||||
"typescript": "3.3.4000"
|
||||
"react-swipeable-views": "^0.13.1",
|
||||
"typescript": "3.3.4000",
|
||||
"notistack": "^0.5.1",
|
||||
"react-web-share-api": "^0.0.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"start": "BROWSER='Safari Technology Preview' react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
|
|
16
src/App.tsx
16
src/App.tsx
|
@ -1,5 +1,5 @@
|
|||
import React, { Component } from 'react';
|
||||
import {MuiThemeProvider, CssBaseline, withStyles, Typography } from '@material-ui/core';
|
||||
import {MuiThemeProvider, CssBaseline, withStyles } from '@material-ui/core';
|
||||
import { setHyperspaceTheme, darkMode } from './utilities/themes';
|
||||
import AppLayout from './components/AppLayout';
|
||||
import {styles} from './App.styles';
|
||||
|
@ -8,11 +8,16 @@ 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 {withSnackbar} from 'notistack';
|
||||
import {ProfileRoute} from './interfaces/overrides';
|
||||
|
||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||
|
||||
class App extends Component<any, any> {
|
||||
|
||||
offline: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
|
@ -28,17 +33,18 @@ class App extends Component<any, any> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return (
|
||||
<MuiThemeProvider theme={this.state.theme}>
|
||||
<CssBaseline/>
|
||||
<AppLayout/>
|
||||
<Route exact path="/"/>
|
||||
<Route path="/home"/>
|
||||
<Route exact path="/" component={HomePage}/>
|
||||
<Route path="/home" component={HomePage}/>
|
||||
<Route path="/local"/>
|
||||
<Route path="/public"/>
|
||||
<Route path="/messages"/>
|
||||
<Route path="/notifications"/>
|
||||
<Route path="/profile/:profileId" component={ProfilePage}/>
|
||||
<Route path="/profile/:profileId" render={props => <ProfilePage {...props}></ProfilePage>}/>
|
||||
<Route path="/conversation/:conversationId"/>
|
||||
<Route path="/settings" component={Settings}/>
|
||||
<Route path="/about" component={AboutPage}/>
|
||||
|
@ -47,4 +53,4 @@ class App extends Component<any, any> {
|
|||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(App);
|
||||
export default withStyles(styles)(withSnackbar(App));
|
||||
|
|
|
@ -6,7 +6,6 @@ export const styles = (theme: Theme) => createStyles({
|
|||
root: {
|
||||
width: '100%',
|
||||
display: 'flex',
|
||||
height: '100%'
|
||||
},
|
||||
stickyArea: {
|
||||
position: 'fixed',
|
||||
|
|
|
@ -177,15 +177,17 @@ export class AppLayout extends Component<any, IAppLayoutState> {
|
|||
className={classes.acctMenu}
|
||||
>
|
||||
<ClickAwayListener onClickAway={this.toggleAcctMenu}>
|
||||
<LinkableListItem to={`/profile/${this.state.currentUser.id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt="You" src={this.state.currentUser.avatar_static}/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.state.currentUser.display_name} secondary={this.state.currentUser.acct}/>
|
||||
</LinkableListItem>
|
||||
<Divider/>
|
||||
<MenuItem>Switch account</MenuItem>
|
||||
<MenuItem>Log out</MenuItem>
|
||||
<div>
|
||||
<LinkableListItem to={`/profile/${this.state.currentUser.id}`}>
|
||||
<ListItemAvatar>
|
||||
<Avatar alt="You" src={this.state.currentUser.avatar_static}/>
|
||||
</ListItemAvatar>
|
||||
<ListItemText primary={this.state.currentUser.display_name} secondary={this.state.currentUser.acct}/>
|
||||
</LinkableListItem>
|
||||
<Divider/>
|
||||
<MenuItem>Switch account</MenuItem>
|
||||
<MenuItem>Log out</MenuItem>
|
||||
</div>
|
||||
</ClickAwayListener>
|
||||
</Menu>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import { Theme, createStyles } from '@material-ui/core';
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
mediaContainer: {
|
||||
padding: theme.spacing.unit * 2,
|
||||
},
|
||||
mediaObject: {
|
||||
width: '100%',
|
||||
height: '100%'
|
||||
},
|
||||
mediaSlide: {
|
||||
backgroundColor: theme.palette.primary.light,
|
||||
width: '100%',
|
||||
height: 'auto'
|
||||
}
|
||||
});
|
|
@ -0,0 +1,105 @@
|
|||
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];
|
||||
classes?: any;
|
||||
}
|
||||
|
||||
interface IAttachmentState {
|
||||
totalSteps: number;
|
||||
currentStep: number;
|
||||
attachments: [Attachment];
|
||||
}
|
||||
|
||||
class AttachmentComponent extends Component<IAttachmentProps, IAttachmentState> {
|
||||
constructor(props: IAttachmentProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
attachments: this.props.media,
|
||||
totalSteps: this.props.media.length,
|
||||
currentStep: 0
|
||||
}
|
||||
}
|
||||
|
||||
moveBack() {
|
||||
let nextStep = this.state.currentStep - 1;
|
||||
if (nextStep < 0) {
|
||||
nextStep = 0;
|
||||
}
|
||||
this.setState({ currentStep: nextStep });
|
||||
}
|
||||
|
||||
moveForward() {
|
||||
let nextStep = this.state.currentStep + 1;
|
||||
if (nextStep > this.state.totalSteps) {
|
||||
nextStep = this.state.totalSteps;
|
||||
}
|
||||
this.setState({ currentStep: nextStep });
|
||||
|
||||
}
|
||||
|
||||
handleStepChange(currentStep: number) {
|
||||
this.setState({
|
||||
currentStep
|
||||
})
|
||||
}
|
||||
|
||||
getSlide(slide: Attachment) {
|
||||
const {classes} = this.props;
|
||||
switch (slide.type) {
|
||||
case 'image':
|
||||
return <img src={slide.url} alt={slide.description? slide.description: ''} className={classes.mediaObject}/>
|
||||
case 'video':
|
||||
return <video controls autoPlay={false} src={slide.url} className={classes.mediaObject}/>
|
||||
case 'gifv':
|
||||
return <img src={slide.url} alt={slide.description? slide.description: ''} className={classes.mediaObject}/>
|
||||
case 'unknown':
|
||||
return <object data={slide.url} className={classes.mediaObject}/>
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
const step = this.state.currentStep;
|
||||
const mediaItem = this.state.attachments[step];
|
||||
return (
|
||||
<div className={classes.mediaContainer}>
|
||||
<Typography variant="button">{mediaItem.description? mediaItem.description: "No description provided."}</Typography>
|
||||
<SwipeableViews
|
||||
index={this.state.currentStep}
|
||||
>
|
||||
{
|
||||
this.state.attachments.map((slide: Attachment) => {
|
||||
return (<div key={slide.id} className={classes.mediaSlide}>
|
||||
{this.getSlide(slide)}
|
||||
</div>);
|
||||
})
|
||||
}
|
||||
</SwipeableViews>
|
||||
<MobileStepper
|
||||
steps={this.state.totalSteps}
|
||||
position="static"
|
||||
activeStep={this.state.currentStep}
|
||||
className={classes.mobileStepper}
|
||||
nextButton={
|
||||
<Button size="small" onClick={() => this.moveForward()} disabled={this.state.currentStep === this.state.totalSteps - 1}>
|
||||
Next
|
||||
</Button>
|
||||
}
|
||||
backButton={
|
||||
<Button size="small" onClick={() => this.moveBack()} disabled={this.state.currentStep === 0}>
|
||||
Back
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(AttachmentComponent);
|
|
@ -0,0 +1,5 @@
|
|||
import {withStyles} from '@material-ui/core';
|
||||
import AttachmentComponent from './Attachment';
|
||||
import {styles} from './Attachment.styles';
|
||||
|
||||
export default withStyles(styles)(AttachmentComponent);
|
|
@ -0,0 +1,58 @@
|
|||
import { Theme, createStyles } from "@material-ui/core";
|
||||
|
||||
export const styles = (theme: Theme) => createStyles({
|
||||
post: {
|
||||
marginTop: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit
|
||||
},
|
||||
postReblogChip: {
|
||||
color: theme.palette.common.white,
|
||||
'&:hover': {
|
||||
backgroundColor: theme.palette.secondary.light
|
||||
},
|
||||
marginBottom: theme.spacing.unit
|
||||
},
|
||||
postContent: {
|
||||
paddingTop: 0,
|
||||
paddingBottom: 0,
|
||||
'& a': {
|
||||
textDecoration: 'none',
|
||||
color: theme.palette.secondary.light,
|
||||
'&:hover': {
|
||||
textDecoration: 'underline'
|
||||
},
|
||||
}
|
||||
},
|
||||
postCard: {
|
||||
'& a:hover': {
|
||||
textDecoration: 'none'
|
||||
}
|
||||
},
|
||||
postEmoji: {
|
||||
width: theme.typography.body2.fontSize
|
||||
},
|
||||
postMedia: {
|
||||
height: 0,
|
||||
paddingTop: '56.25%', // 16:9
|
||||
},
|
||||
postActionsReply: {
|
||||
marginLeft: theme.spacing.unit,
|
||||
marginRight: theme.spacing.unit
|
||||
},
|
||||
postFlexGrow: {
|
||||
flexGrow: 1
|
||||
},
|
||||
postTypeIconDiv: {
|
||||
marginRight: theme.spacing.unit * 2
|
||||
},
|
||||
postTypeIcon: {
|
||||
color: theme.palette.grey[500]
|
||||
},
|
||||
postWarningIcon: {
|
||||
marginRight: theme.spacing.unit,
|
||||
color: theme.palette.grey[700]
|
||||
},
|
||||
postDidAction: {
|
||||
color: theme.palette.secondary.main
|
||||
}
|
||||
});
|
|
@ -0,0 +1,341 @@
|
|||
import React from 'react';
|
||||
import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip } 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 {styles} from './Post.styles';
|
||||
import { Status } from '../../types/Status';
|
||||
import { Mention } from '../../types/Mention';
|
||||
import { Visibility } from '../../types/Visibility';
|
||||
import moment from 'moment';
|
||||
import { MastodonEmoji } from '../../types/Emojis';
|
||||
import AttachmentComponent from '../Attachment';
|
||||
import Mastodon from 'megalodon';
|
||||
import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interfaces/overrides';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ShareMenu from './PostShareMenu';
|
||||
|
||||
interface IPostProps {
|
||||
post: Status;
|
||||
classes: any;
|
||||
client: Mastodon;
|
||||
}
|
||||
|
||||
interface IPostState {
|
||||
post: Status;
|
||||
media_slides?: number;
|
||||
menuIsOpen: boolean;
|
||||
}
|
||||
|
||||
export class Post extends React.Component<any, IPostState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
post: this.props.post,
|
||||
media_slides: this.props.post.media_attachments.length > 0? this.props.post.media_attachments.length: 0,
|
||||
menuIsOpen: false
|
||||
}
|
||||
|
||||
this.client = this.props.client;
|
||||
|
||||
}
|
||||
|
||||
togglePostMenu() {
|
||||
this.setState({ menuIsOpen: !this.state.menuIsOpen })
|
||||
}
|
||||
|
||||
materializeContent(status: Status) {
|
||||
const { classes } = this.props;
|
||||
const oldContent = document.createElement('div');
|
||||
oldContent.innerHTML = status.content;
|
||||
if (status.emojis !== undefined && status.emojis.length > 0) {
|
||||
status.emojis.forEach((emoji: MastodonEmoji) => {
|
||||
let regexp = new RegExp(':' + emoji.shortcode + ':', 'g');
|
||||
oldContent.innerHTML = oldContent.innerHTML.replace(regexp, `<img src="${emoji.static_url}" class="${classes.postEmoji}"/>`)
|
||||
})
|
||||
}
|
||||
return (
|
||||
<div className={classes.mediaContainer}>
|
||||
<Typography paragraph dangerouslySetInnerHTML={{__html: oldContent.innerHTML}}/>
|
||||
{
|
||||
status.card?
|
||||
<div className={classes.postCard}>
|
||||
<Divider/>
|
||||
<CardActionArea href={status.card.url} target="_blank" rel="noreferrer">
|
||||
<CardContent>
|
||||
<Typography gutterBottom variant="h6" component="h2">{status.card.title}</Typography>
|
||||
<Typography>{status.card.description}</Typography>
|
||||
</CardContent>
|
||||
{
|
||||
status.card.image?
|
||||
<CardMedia className={classes.postMedia} image={status.card.image}/>: <span/>
|
||||
}
|
||||
<CardContent>
|
||||
<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/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
getSensitiveContent(spoiler_text: string, content: Status) {
|
||||
const { classes } = this.props;
|
||||
const warningText = spoiler_text || "Unmarked content";
|
||||
let icon;
|
||||
if (spoiler_text.includes("NSFW") || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") {
|
||||
icon = <WarningIcon className={classes.postWarningIcon}/>;
|
||||
}
|
||||
return (
|
||||
<ExpansionPanel>
|
||||
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>}>
|
||||
{icon}<Typography className={classes.heading}>{warningText}</Typography>
|
||||
</ExpansionPanelSummary>
|
||||
<ExpansionPanelDetails className={classes.postContent}>
|
||||
{this.materializeContent(content)}
|
||||
</ExpansionPanelDetails>
|
||||
</ExpansionPanel>
|
||||
)
|
||||
}
|
||||
|
||||
getReblogOfPost(of: Status | null) {
|
||||
const { classes } = this.props;
|
||||
if (of !== null) {
|
||||
return (
|
||||
<CardContent className={classes.postContent}>
|
||||
<LinkableChip
|
||||
avatar={
|
||||
<Avatar alt={of.account.acct} src={of.account.avatar_static}/>
|
||||
}
|
||||
className={classes.postReblogChip}
|
||||
label={`@${of.account.username} posted:`}
|
||||
key={of.id + "_reblog_chip_" + of.account.id}
|
||||
to={`/profile/${of.account.id}`}
|
||||
replace={true}
|
||||
clickable
|
||||
/>
|
||||
{of.sensitive? this.getSensitiveContent(of.spoiler_text, of): this.materializeContent(of)}
|
||||
</CardContent>
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getMentions(mention: [Mention]) {
|
||||
if (mention.length > 0) {
|
||||
return (
|
||||
<CardContent>
|
||||
<Typography variant="caption">Mentions</Typography>
|
||||
{
|
||||
this.state.post.mentions.map((person: Mention) => {
|
||||
return <LinkableChip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<AlternateEmailIcon/>
|
||||
</Avatar>
|
||||
}
|
||||
label={person.username}
|
||||
key={this.state.post.id + "_mention_" + person.id}
|
||||
to={`/profile/${person.id}`}
|
||||
clickable
|
||||
/>
|
||||
})
|
||||
}
|
||||
</CardContent>
|
||||
)
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
showVisibilityIcon(visibility: Visibility) {
|
||||
const { classes } = this.props;
|
||||
switch(visibility) {
|
||||
case "public":
|
||||
return <PublicIcon className={classes.postTypeIcon}/>;
|
||||
case "private":
|
||||
return <GroupIcon className={classes.postTypeIcon}/>;
|
||||
case "unlisted":
|
||||
return <VisibilityOffIcon className={classes.postTypeIcon}/>
|
||||
}
|
||||
}
|
||||
|
||||
getMastodonUrl(post: Status) {
|
||||
let url = "";
|
||||
if (post.reblog) {
|
||||
url = post.reblog.uri
|
||||
} else {
|
||||
url = post.uri
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
toggleFavorited(post: Status) {
|
||||
let _this = this;
|
||||
if (post.favourited) {
|
||||
this.client.post(`/statuses/${post.id}/unfavourite`).then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(`Couldn't unfavorite post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
console.log(err.message);
|
||||
})
|
||||
} else {
|
||||
this.client.post(`/statuses/${post.id}/favourite`).then((resp: any) => {
|
||||
let post: Status = resp.data;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
_this.props.enqueueSnackbar(`Couldn't favorite post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
console.log(err.message);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
toggleReblogged(post: Status) {
|
||||
if (post.reblogged) {
|
||||
this.client.post(`/statuses/${post.id}/unreblog`).then((resp: any) => {
|
||||
let post = this.state.post;
|
||||
post.reblogged = false;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(`Couldn't unboost post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
console.log(err.message);
|
||||
})
|
||||
} else {
|
||||
this.client.post(`/statuses/${post.id}/reblog`).then((resp: any) => {
|
||||
let post = this.state.post;
|
||||
post.reblogged = true;
|
||||
this.setState({ post });
|
||||
}).catch((err: Error) => {
|
||||
this.props.enqueueSnackbar(`Couldn't boost post: ${err.name}`, {
|
||||
variant: 'error'
|
||||
})
|
||||
console.log(err.message);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
return (
|
||||
<Zoom in={true}>
|
||||
<Card className={classes.post}>
|
||||
<CardHeader avatar={<Avatar src={this.state.post.account.avatar_static} />} action={
|
||||
<IconButton key={`${this.state.post.id}_submenu`} id={`${this.state.post.id}_submenu`} onClick={() => this.togglePostMenu()}>
|
||||
<MoreVertIcon />
|
||||
</IconButton>}
|
||||
title={
|
||||
`${this.state.post.account.display_name || this.state.post.account.username} (@${this.state.post.account.acct})`
|
||||
} subheader={moment(this.state.post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
|
||||
{
|
||||
this.state.post.reblog? this.getReblogOfPost(this.state.post.reblog): null
|
||||
}
|
||||
{
|
||||
this.state.post.sensitive? this.getSensitiveContent(this.state.post.spoiler_text, this.state.post):
|
||||
<CardContent className={classes.postContent}>
|
||||
{this.state.post.reblog? null: this.materializeContent(this.state.post)}
|
||||
</CardContent>
|
||||
}
|
||||
{
|
||||
this.getMentions(this.state.post.mentions)
|
||||
}
|
||||
{
|
||||
this.state.post.reblog && this.state.post.reblog.mentions.length > 0? this.getMentions(this.state.post.reblog.mentions): <span/>
|
||||
}
|
||||
<CardActions>
|
||||
<Tooltip title="Reply">
|
||||
<LinkableIconButton to={`/compose?reply=${this.state.post.id}`}>
|
||||
<ReplyIcon/>
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.replies_count}</Typography>
|
||||
<Tooltip title="Favorite">
|
||||
<IconButton onClick={() => this.toggleFavorited(this.state.post)}>
|
||||
<FavoriteIcon className={this.state.post.favourited? classes.postDidAction: ''}/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.favourites_count}</Typography>
|
||||
<Tooltip title="Boost">
|
||||
<IconButton onClick={() => this.toggleReblogged(this.state.post)}>
|
||||
<AutorenewIcon className={this.state.post.reblogged? classes.postDidAction: ''}/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.reblogs_count}</Typography>
|
||||
<Tooltip title="View thread">
|
||||
<LinkableIconButton to={`/conversation/${this.state.post.id}`}>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open in Web">
|
||||
<IconButton href={this.getMastodonUrl(this.state.post)} rel="noreferrer" target="_blank">
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={classes.postFlexGrow} />
|
||||
<div className={classes.postTypeIconDiv}>
|
||||
{this.showVisibilityIcon(this.state.post.visibility)}
|
||||
</div>
|
||||
</CardActions>
|
||||
<Menu
|
||||
id="postmenu"
|
||||
anchorEl={document.getElementById(`${this.state.post.id}_submenu`)}
|
||||
open={this.state.menuIsOpen}
|
||||
onClose={() => this.togglePostMenu()}
|
||||
>
|
||||
<ShareMenu config={{
|
||||
params: {
|
||||
title: `@${this.state.post.account.username} posted on Mastodon: `,
|
||||
text: this.state.post.content,
|
||||
url: this.getMastodonUrl(this.state.post),
|
||||
},
|
||||
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'})
|
||||
},
|
||||
}}/>
|
||||
<LinkableMenuItem to={`/profile/${this.state.post.account.id}`}>View author profile</LinkableMenuItem>
|
||||
{
|
||||
this.state.post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
|
||||
<div>
|
||||
<Divider/>
|
||||
<MenuItem>Delete</MenuItem>
|
||||
</div>:
|
||||
null
|
||||
}
|
||||
</Menu>
|
||||
</Card>
|
||||
</Zoom>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(withSnackbar(Post));
|
|
@ -0,0 +1,15 @@
|
|||
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
|
||||
|
||||
export default webShare<OwnProps>()(ShareMenu);
|
|
@ -0,0 +1,6 @@
|
|||
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));
|
|
@ -5,16 +5,25 @@ import { HashRouter } from 'react-router-dom';
|
|||
import * as serviceWorker from './serviceWorker';
|
||||
import {createUserDefaults} from './utilities/settings';
|
||||
import {refreshUserAccountData} from './utilities/accounts';
|
||||
import {SnackbarProvider} from 'notistack';
|
||||
|
||||
createUserDefaults();
|
||||
refreshUserAccountData();
|
||||
|
||||
ReactDOM.render(
|
||||
<HashRouter>
|
||||
<App />
|
||||
<SnackbarProvider
|
||||
anchorOrigin={{
|
||||
vertical: 'bottom',
|
||||
horizontal: 'right',
|
||||
}}
|
||||
>
|
||||
<App />
|
||||
</SnackbarProvider>
|
||||
</HashRouter>,
|
||||
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.
|
||||
// Learn more about service workers: https://bit.ly/CRA-PWA
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
import React 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 } from "react-router-dom";
|
||||
import { Link, Route } 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';
|
||||
|
||||
export interface ILinkableListItemProps extends ListItemProps {
|
||||
to: string;
|
||||
|
@ -13,10 +16,34 @@ export interface ILinkableIconButtonProps extends IconButtonProps {
|
|||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkableChipProps extends ChipProps {
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkableMenuItemProps extends MenuItemProps {
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||
<ListItem {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
|
||||
<IconButton {...props} component={Link as any}/>
|
||||
)
|
||||
)
|
||||
|
||||
export const LinkableChip = (props: ILinkableChipProps) => (
|
||||
<Chip {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
|
||||
<MenuItem {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const ProfileRoute = (rest: any, component: Component) => (
|
||||
<Route {...rest} render={props => (
|
||||
<Component {...props}/>
|
||||
)}/>
|
||||
)
|
|
@ -33,7 +33,7 @@ class AboutPage extends Component<any, IAboutPageState> {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
let client = new Mastodon(localStorage.getItem('account_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
let client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
client.get('/instance').then((resp: any) => {
|
||||
this.setState({
|
||||
instance: resp.data
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper, Button} from '@material-ui/core';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import Post from '../components/Post';
|
||||
import { Status } from '../types/Status';
|
||||
import Mastodon from 'megalodon';
|
||||
import {withSnackbar} from 'notistack';
|
||||
|
||||
interface IHomePageState {
|
||||
posts?: [Status];
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class HomePage extends Component<any, IHomePageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.streamListener = this.client.stream('/streaming/home');
|
||||
|
||||
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) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
let posts = this.state.posts;
|
||||
if (posts) {
|
||||
posts.unshift(status)
|
||||
}
|
||||
this.setState({ posts });
|
||||
})
|
||||
|
||||
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.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
if (this.state.posts) {
|
||||
this.client.get('/timelines/home', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
||||
let newPosts: [Status] = resp.data;
|
||||
let posts = this.state.posts as [Status];
|
||||
newPosts.forEach((post: Status) => {
|
||||
posts.push(post);
|
||||
});
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{ this.state.posts?
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>
|
||||
})}
|
||||
<br/>
|
||||
{
|
||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
||||
}
|
||||
</div>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
this.state.viewDidError?
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">Something went wrong when loading this timeline.</Typography>
|
||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
||||
</Paper>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
this.state.viewIsLoading?
|
||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
||||
<span/>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(withSnackbar(HomePage));
|
|
@ -17,6 +17,26 @@ export const styles = (theme: Theme) => createStyles({
|
|||
paddingRight: theme.spacing.unit * 24
|
||||
},
|
||||
},
|
||||
pageLayoutMaxConstraints: {
|
||||
marginTop: 72,
|
||||
flexGrow: 1,
|
||||
paddingTop: theme.spacing.unit * 2,
|
||||
padding: theme.spacing.unit,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
paddingLeft: theme.spacing.unit * 16,
|
||||
paddingRight: theme.spacing.unit * 16,
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
paddingLeft: theme.spacing.unit * 40,
|
||||
paddingRight: theme.spacing.unit * 40,
|
||||
},
|
||||
},
|
||||
pageLayoutMinimalConstraints: {
|
||||
flexGrow: 1,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
|
@ -25,7 +45,7 @@ export const styles = (theme: Theme) => createStyles({
|
|||
},
|
||||
pageHeroBackground: {
|
||||
position: 'relative',
|
||||
height: '100%',
|
||||
height: 'intrinsic',
|
||||
backgroundColor: theme.palette.primary.dark,
|
||||
width: '100%',
|
||||
color: theme.palette.common.white,
|
||||
|
@ -90,9 +110,14 @@ export const styles = (theme: Theme) => createStyles({
|
|||
paddingLeft: theme.spacing.unit * 4,
|
||||
paddingRight: theme.spacing.unit * 4,
|
||||
paddingTop: theme.spacing.unit * 4,
|
||||
paddingBottom: theme.spacing.unit * 2,
|
||||
[theme.breakpoints.up('md')]: {
|
||||
paddingLeft: theme.spacing.unit * 24,
|
||||
paddingRight: theme.spacing.unit * 24
|
||||
paddingLeft: theme.spacing.unit * 32,
|
||||
paddingRight: theme.spacing.unit * 32
|
||||
},
|
||||
},
|
||||
errorCard: {
|
||||
padding: theme.spacing.unit * 4,
|
||||
backgroundColor: theme.palette.error.main,
|
||||
}
|
||||
});
|
|
@ -1,60 +1,117 @@
|
|||
import React, {Component} from 'react';
|
||||
import {withStyles, Typography, Avatar, Divider, Button} from '@material-ui/core';
|
||||
import {withStyles, Typography, Avatar, Divider, Button, CircularProgress, Paper} from '@material-ui/core';
|
||||
import {styles} from './PageLayout.styles';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Account } from '../types/Account';
|
||||
import { Status} from '../types/Status';
|
||||
import Post from '../components/Post';
|
||||
import {withSnackbar} from 'notistack';
|
||||
|
||||
interface IProfilePageState {
|
||||
id: string;
|
||||
display_name: string;
|
||||
acct: string;
|
||||
followers_count: number;
|
||||
following_count: number;
|
||||
status_count: number;
|
||||
statuses: [];
|
||||
avatar: string;
|
||||
header: string;
|
||||
note: string;
|
||||
account?: Account;
|
||||
posts?: [Status];
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: string;
|
||||
}
|
||||
|
||||
class ProfilePage extends Component<any, IProfilePageState> {
|
||||
|
||||
client: Mastodon;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
const { match: { params }} = this.props;
|
||||
|
||||
let client = new Mastodon(localStorage.getItem('account_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
client.get(`/accounts/${params.profileId}`).then((resp: any) => {
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true
|
||||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
this.client.get(`/accounts/${props.match.params.profileId}`).then((resp: any) => {
|
||||
let profile: Account = resp.data;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = profile.note;
|
||||
const note = div.textContent || div.innerText || "";
|
||||
profile.note = div.textContent || div.innerText || "";
|
||||
|
||||
this.setState({
|
||||
id: profile.id,
|
||||
display_name: profile.display_name,
|
||||
acct: '@' + profile.acct,
|
||||
followers_count: profile.followers_count,
|
||||
following_count: profile.following_count,
|
||||
status_count: profile.statuses_count,
|
||||
avatar: profile.avatar_static,
|
||||
header: profile.header_static,
|
||||
note: note
|
||||
account: profile
|
||||
})
|
||||
}).catch((error: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
})
|
||||
|
||||
this.client.get(`/accounts/${props.match.params.profileId}/statuses`).then((resp: any) => {
|
||||
this.setState({
|
||||
posts: resp.data,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
})
|
||||
}).catch( (err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { match: { params }} = this.props;
|
||||
this.client.get(`/accounts/${params.profileId}`).then((resp: any) => {
|
||||
let profile: Account = resp.data;
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = profile.note;
|
||||
profile.note = div.textContent || div.innerText || "";
|
||||
|
||||
this.setState({
|
||||
account: profile
|
||||
})
|
||||
}).catch((error: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
})
|
||||
|
||||
this.client.get(`/accounts/${params.profileId}/statuses`).then((resp: any) => {
|
||||
this.setState({
|
||||
posts: resp.data,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
})
|
||||
}).catch( (err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
statElement(classes: any, stat: 'following' | 'followers' | 'posts') {
|
||||
let number = 0;
|
||||
if (this.state) {
|
||||
if (this.state.account) {
|
||||
if (stat == 'following') {
|
||||
number = this.state.following_count;
|
||||
number = this.state.account.following_count;
|
||||
} else if (stat == 'followers') {
|
||||
number = this.state.followers_count;
|
||||
number = this.state.account.followers_count;
|
||||
} else if (stat == 'posts') {
|
||||
number = this.state.status_count;
|
||||
number = this.state.account.statuses_count;
|
||||
}
|
||||
}
|
||||
return <div className={classes.pageProfileStat}>
|
||||
|
@ -63,18 +120,52 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
</div>;
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
const { match: {params}} = this.props;
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
if (this.state.posts) {
|
||||
this.client.get(`/accounts/${params.profileId}/statuses`, { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
||||
let newPosts: [Status] = resp.data;
|
||||
let posts = this.state.posts as [Status];
|
||||
if (newPosts.length <= 0) {
|
||||
this.props.enqueueSnackbar("Reached end of posts", {
|
||||
variant: 'error'
|
||||
});
|
||||
} else {
|
||||
newPosts.forEach((post: Status) => {
|
||||
posts.push(post);
|
||||
});
|
||||
}
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return(
|
||||
<div className={classes.pageLayoutMinimalConstraints}>
|
||||
<div className={classes.pageHeroBackground}>
|
||||
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state? `url("${this.state.header}")`: `url("")`}}/>
|
||||
<div className={classes.pageHeroBackgroundImage} style={{ backgroundImage: this.state.account? `url("${this.state.account.header}")`: `url("")`}}/>
|
||||
<div className={classes.pageHeroContent}>
|
||||
<Avatar className={classes.pageProfileAvatar} src={this.state ? this.state.avatar: ""}/>
|
||||
<Typography variant="h4" color="inherit">{this.state? this.state.display_name: ""}</Typography>
|
||||
<Typography variant="caption" color="inherit">{this.state? this.state.acct: ""}</Typography>
|
||||
<Typography paragraph color="inherit">{this.state? this.state.note: ""}</Typography>
|
||||
<Avatar className={classes.pageProfileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
|
||||
<Typography variant="h4" color="inherit">{this.state.account ? this.state.account.display_name: ""}</Typography>
|
||||
<Typography variant="caption" color="inherit">{this.state.account ? this.state.account.acct: ""}</Typography>
|
||||
<Typography paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
|
||||
<Divider/>
|
||||
<div className={classes.pageProfileStatsDiv}>
|
||||
{this.statElement(classes, 'followers')}
|
||||
|
@ -88,12 +179,38 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
</div>
|
||||
</div>
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
<Typography variant="h6">Looks like no one's posted here yet.</Typography>
|
||||
<Typography>Why not give a nidge to start the conversation?</Typography>
|
||||
{
|
||||
this.state.viewDidError?
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">Something went wrong when loading this profile.</Typography>
|
||||
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
|
||||
</Paper>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
this.state.posts?
|
||||
<div>
|
||||
{
|
||||
this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>;
|
||||
})
|
||||
}
|
||||
<br/>
|
||||
{
|
||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
||||
}
|
||||
</div>: <span/>
|
||||
}
|
||||
{
|
||||
this.state.viewIsLoading?
|
||||
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
|
||||
<span/>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default withStyles(styles)(ProfilePage)
|
||||
export default withStyles(styles)(withSnackbar(ProfilePage));
|
Loading…
Reference in New Issue