Work on adding basic posts

This commit is contained in:
Marquis Kurt 2019-03-30 17:13:49 -04:00
parent 911a25506d
commit b8f9035be1
18 changed files with 1146 additions and 85 deletions

208
package-lock.json generated
View File

@ -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",

View File

@ -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"

View File

@ -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));

View File

@ -6,7 +6,6 @@ export const styles = (theme: Theme) => createStyles({
root: {
width: '100%',
display: 'flex',
height: '100%'
},
stickyArea: {
position: 'fixed',

View File

@ -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>

View File

@ -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'
}
});

View File

@ -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);

View File

@ -0,0 +1,5 @@
import {withStyles} from '@material-ui/core';
import AttachmentComponent from './Attachment';
import {styles} from './Attachment.styles';
export default withStyles(styles)(AttachmentComponent);

View File

@ -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
}
});

View File

@ -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));

View File

@ -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);

View File

@ -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));

View File

@ -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

View File

@ -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}/>
)}/>
)

View File

@ -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

157
src/pages/Home.tsx Normal file
View File

@ -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));

View File

@ -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,
}
});

View File

@ -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));