Merge pull request #85 from hyperspacedev/small-refinements-b

Small refinements and touch-ups
This commit is contained in:
Marquis Kurt 2019-09-23 13:34:49 -04:00 committed by GitHub
commit 1a4d4120aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
61 changed files with 6585 additions and 6097 deletions

94
package-lock.json generated
View File

@ -1297,6 +1297,12 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/events": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.0.tgz",
"integrity": "sha512-EaObqwIvayI5a8dCzhFrjKzVwKLxjoG9T6Ppd5CEo07LRKfQ8Yokw54r5+Wq7FaBQ+yXRvQAYPrHwya1/UFt9g==",
"dev": true
},
"@types/history": { "@types/history": {
"version": "4.7.2", "version": "4.7.2",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz", "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz",
@ -1491,12 +1497,13 @@
"@types/unist": "*" "@types/unist": "*"
} }
}, },
"@types/ws": { "@types/websocket": {
"version": "6.0.3", "version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.3.tgz", "resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.40.tgz",
"integrity": "sha512-yBTM0P05Tx9iXGq00BbJPo37ox68R5vaGTXivs6RGh/BQ6QP5zqZDGWdAO6JbRE/iR1l80xeGAwCQS2nMV9S/w==", "integrity": "sha512-ldteZwWIgl9cOy7FyvYn+39Ah4+PfpVE72eYKw75iy2L0zTbhbcwvzeJ5IOu6DQP93bjfXq0NGHY6FYtmYoqFQ==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/events": "*",
"@types/node": "*" "@types/node": "*"
} }
}, },
@ -12191,19 +12198,19 @@
"dev": true "dev": true
}, },
"megalodon": { "megalodon": {
"version": "1.0.3", "version": "0.6.4",
"resolved": "https://registry.npmjs.org/megalodon/-/megalodon-1.0.3.tgz", "resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.6.4.tgz",
"integrity": "sha512-RcJT3HRWCXQcE5ZQUpLEjJ+HgWvwoTpXr4XLUf0tWrtmxrDnIW43pOASDb3G7MOBKYonzM1pMfAmR2Yfh3Qw/g==", "integrity": "sha512-WGYhcSxGYlBwZSm5VebxLqnbpPemum9/6lJUi1HBsVzF5jXc9fdumhXH0vqGhWdovdqRT86iXBDJl5SwUrbr2A==",
"dev": true, "dev": true,
"requires": { "requires": {
"@types/oauth": "^0.9.0", "@types/oauth": "^0.9.0",
"@types/request": "^2.47.0", "@types/request": "^2.47.0",
"@types/ws": "^6.0.1", "@types/websocket": "0.0.40",
"axios": "^0.18.1", "axios": "^0.18.0",
"oauth": "^0.9.15", "oauth": "^0.9.15",
"request": "^2.87.0", "request": "^2.87.0",
"typescript": "^3.4.5", "typescript": "^2.9.1",
"ws": "^7.0.1" "websocket": "^1.0.28"
}, },
"dependencies": { "dependencies": {
"axios": { "axios": {
@ -12247,19 +12254,10 @@
"dev": true "dev": true
}, },
"typescript": { "typescript": {
"version": "3.6.3", "version": "2.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==", "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
"dev": true "dev": true
},
"ws": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-7.1.2.tgz",
"integrity": "sha512-gftXq3XI81cJCgkUiAVixA0raD9IVmXqsylCrjRygw4+UOOGzPoxnQ6r/CnVL9i+mDncJo94tSkyrtuuQVBmrg==",
"dev": true,
"requires": {
"async-limiter": "^1.0.0"
}
} }
} }
}, },
@ -19851,6 +19849,15 @@
"integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=",
"dev": true "dev": true
}, },
"typedarray-to-buffer": {
"version": "3.1.5",
"resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz",
"integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==",
"dev": true,
"requires": {
"is-typedarray": "^1.0.0"
}
},
"typescript": { "typescript": {
"version": "3.4.1", "version": "3.4.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz",
@ -21010,6 +21017,41 @@
} }
} }
}, },
"websocket": {
"version": "1.0.30",
"resolved": "https://registry.npmjs.org/websocket/-/websocket-1.0.30.tgz",
"integrity": "sha512-aO6klgaTdSMkhfl5VVJzD5fm+Srhh5jLYbS15+OiI1sN6h/RU/XW6WN9J1uVIpUKNmsTvT3Hs35XAFjn9NMfOw==",
"dev": true,
"requires": {
"debug": "^2.2.0",
"nan": "^2.14.0",
"typedarray-to-buffer": "^3.1.5",
"yaeti": "^0.0.6"
},
"dependencies": {
"debug": {
"version": "2.6.9",
"resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz",
"integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==",
"dev": true,
"requires": {
"ms": "2.0.0"
}
},
"ms": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz",
"integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=",
"dev": true
},
"nan": {
"version": "2.14.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.14.0.tgz",
"integrity": "sha512-INOFj37C7k3AfaNTtX8RhsTw7qRy7eLET14cROi9+5HAVbbHuIWUHEauBv5qT4Av2tWasiTY1Jw6puUNqRJXQg==",
"dev": true
}
}
},
"websocket-driver": { "websocket-driver": {
"version": "0.7.0", "version": "0.7.0",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
@ -21368,6 +21410,12 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=", "integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"dev": true "dev": true
}, },
"yaeti": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/yaeti/-/yaeti-0.0.6.tgz",
"integrity": "sha1-8m9ITXJoTPQr7ft2lwqhYI+/lXc=",
"dev": true
},
"yallist": { "yallist": {
"version": "2.1.2", "version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",

View File

@ -25,10 +25,10 @@
"file-dialog": "^0.0.7", "file-dialog": "^0.0.7",
"material-ui-pickers": "^2.2.4", "material-ui-pickers": "^2.2.4",
"mdi-material-ui": "^5.13.0", "mdi-material-ui": "^5.13.0",
"megalodon": "^1.0.3", "megalodon": "^0.6.4",
"moment": "^2.24.0", "moment": "^2.24.0",
"notistack": "^0.5.1", "notistack": "^0.5.1",
"prettier": "^1.18.2", "prettier": "1.18.2",
"query-string": "^6.8.2", "query-string": "^6.8.2",
"react": "^16.8.6", "react": "^16.8.6",
"react-dom": "^16.8.6", "react-dom": "^16.8.6",

Binary file not shown.

Before

Width:  |  Height:  |  Size: 276 KiB

After

Width:  |  Height:  |  Size: 3.3 MiB

View File

@ -2,23 +2,23 @@ import { Theme, createStyles } from "@material-ui/core";
import { isDarwinApp } from "./utilities/desktop"; import { isDarwinApp } from "./utilities/desktop";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
width: "100%", width: "100%",
display: "flex", display: "flex",
height: "100%", height: "100%",
minHeight: "100vh", minHeight: "100vh",
backgroundColor: isDarwinApp() backgroundColor: isDarwinApp()
? "transparent" ? "transparent"
: theme.palette.background.default : theme.palette.background.default
}, },
content: { content: {
marginTop: 72, marginTop: 72,
flexGrow: 1, flexGrow: 1,
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: 250, marginLeft: 250,
marginTop: 88 marginTop: 88
} }
} }
}); });

View File

@ -20,7 +20,6 @@ import MessagesPage from "./pages/Messages";
import RecommendationsPage from "./pages/Recommendations"; import RecommendationsPage from "./pages/Recommendations";
import Missingno from "./pages/Missingno"; import Missingno from "./pages/Missingno";
import You from "./pages/You"; import You from "./pages/You";
import Blocked from "./pages/Blocked";
import { withSnackbar } from "notistack"; import { withSnackbar } from "notistack";
import { PrivateRoute } from "./interfaces/overrides"; import { PrivateRoute } from "./interfaces/overrides";
import { userLoggedIn } from "./utilities/accounts"; import { userLoggedIn } from "./utilities/accounts";
@ -57,8 +56,6 @@ class App extends Component<any, any> {
removeBodyBackground() { removeBodyBackground() {
if (isDarwinApp()) { if (isDarwinApp()) {
document.body.style.backgroundColor = "transparent"; document.body.style.backgroundColor = "transparent";
console.log("Changed!");
console.log(`New color: ${document.body.style.backgroundColor}`);
} }
} }
@ -91,9 +88,7 @@ class App extends Component<any, any> {
component={Conversation} component={Conversation}
/> />
<PrivateRoute path="/search" component={SearchPage} /> <PrivateRoute path="/search" component={SearchPage} />
<PrivateRoute path="/blocked" component={Blocked} />
<PrivateRoute path="/settings" component={Settings} /> <PrivateRoute path="/settings" component={Settings} />
<PrivateRoute path="/you" component={You} /> <PrivateRoute path="/you" component={You} />
<PrivateRoute path="/about" component={AboutPage} /> <PrivateRoute path="/about" component={AboutPage} />
<PrivateRoute path="/compose" component={Composer} /> <PrivateRoute path="/compose" component={Composer} />

View File

@ -5,167 +5,167 @@ import { isDarwinApp } from "../../utilities/desktop";
import { fade } from "@material-ui/core/styles/colorManipulator"; import { fade } from "@material-ui/core/styles/colorManipulator";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
width: "100%", width: "100%",
display: "flex" display: "flex"
}, },
stickyArea: { stickyArea: {
position: "fixed", position: "fixed",
width: "100%", width: "100%",
top: 0, top: 0,
left: 0, left: 0,
zIndex: 1000 zIndex: 1000
}, },
titleBarRoot: { titleBarRoot: {
top: 0, top: 0,
left: 0, left: 0,
height: 24, height: 24,
width: "100%", width: "100%",
backgroundColor: isDarwinApp() backgroundColor: isDarwinApp()
? theme.palette.primary.main ? theme.palette.primary.main
: theme.palette.primary.dark, : theme.palette.primary.dark,
textAlign: "center", textAlign: "center",
zIndex: 1000, zIndex: 1000,
verticalAlign: "middle", verticalAlign: "middle",
WebkitUserSelect: "none", WebkitUserSelect: "none",
WebkitAppRegion: "drag" WebkitAppRegion: "drag"
}, },
titleBarText: { titleBarText: {
color: theme.palette.common.white, color: theme.palette.common.white,
fontSize: 12, fontSize: 12,
paddingTop: 2, paddingTop: 2,
paddingBottom: 1 paddingBottom: 1
}, },
appBar: { appBar: {
zIndex: 1000, zIndex: 1000,
backgroundImage: isDarwinApp() backgroundImage: isDarwinApp()
? `linear-gradient(${theme.palette.primary.main}, ${theme.palette.primary.dark})` ? `linear-gradient(${theme.palette.primary.main}, ${theme.palette.primary.dark})`
: undefined, : undefined,
backgroundColor: theme.palette.primary.main, backgroundColor: theme.palette.primary.main,
borderBottomColor: darken(theme.palette.primary.dark, 0.2), borderBottomColor: darken(theme.palette.primary.dark, 0.2),
borderBottomWidth: 1, borderBottomWidth: 1,
borderBottomStyle: isDarwinApp() ? "solid" : "none", borderBottomStyle: isDarwinApp() ? "solid" : "none",
boxShadow: isDarwinApp() ? "none" : "inherit" boxShadow: isDarwinApp() ? "none" : theme.shadows["4"]
}, },
appBarMenuButton: { appBarMenuButton: {
marginLeft: -12, marginLeft: -12,
marginRight: 20, marginRight: 20,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
display: "none" display: "none"
} }
}, },
appBarTitle: { appBarTitle: {
display: "none", display: "none",
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
display: "block" display: "block"
} }
}, },
appBarSearch: { appBarSearch: {
position: "relative", position: "relative",
borderRadius: theme.shape.borderRadius, borderRadius: theme.shape.borderRadius,
backgroundColor: fade(theme.palette.common.white, 0.15), backgroundColor: fade(theme.palette.common.white, 0.15),
"&:hover": { "&:hover": {
backgroundColor: fade(theme.palette.common.white, 0.25) backgroundColor: fade(theme.palette.common.white, 0.25)
}, },
width: "100%", width: "100%",
marginLeft: 0, marginLeft: 0,
marginRight: theme.spacing.unit, marginRight: theme.spacing.unit,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: theme.spacing.unit * 6, marginLeft: theme.spacing.unit * 6,
width: "50%" width: "50%"
} }
}, },
appBarSearchIcon: { appBarSearchIcon: {
width: theme.spacing.unit * 9, width: theme.spacing.unit * 9,
height: "100%", height: "100%",
position: "absolute", position: "absolute",
pointerEvents: "none", pointerEvents: "none",
display: "flex", display: "flex",
alignItems: "center", alignItems: "center",
justifyContent: "center" justifyContent: "center"
}, },
appBarSearchInputRoot: { appBarSearchInputRoot: {
color: "inherit", color: "inherit",
width: "100%" width: "100%"
}, },
appBarSearchInputInput: { appBarSearchInputInput: {
paddingTop: theme.spacing.unit, paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit, paddingBottom: theme.spacing.unit,
paddingLeft: theme.spacing.unit * 10, paddingLeft: theme.spacing.unit * 10,
paddingRight: theme.spacing.unit, paddingRight: theme.spacing.unit,
transition: theme.transitions.create("width"), transition: theme.transitions.create("width"),
width: "100%" width: "100%"
}, },
appBarFlexGrow: { appBarFlexGrow: {
flexGrow: 1 flexGrow: 1
}, },
appBarActionButtons: { appBarActionButtons: {
display: "none", display: "none",
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
display: "flex" display: "flex"
} }
}, },
appBarAcctMenuIcon: { appBarAcctMenuIcon: {
backgroundColor: theme.palette.primary.dark backgroundColor: theme.palette.primary.dark
}, },
acctMenu: {}, acctMenu: {},
drawer: { drawer: {
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
width: 250, width: 250,
flexShrink: 0 flexShrink: 0
}, },
zIndex: 1 zIndex: 1
}, },
drawerPaper: { drawerPaper: {
width: 250, width: 250,
zIndex: 1 zIndex: 1
}, },
drawerPaperWithAppBar: { drawerPaperWithAppBar: {
width: 250, width: 250,
zIndex: -1, zIndex: -1,
marginTop: 64, marginTop: 64,
backgroundColor: isDarwinApp() backgroundColor: isDarwinApp()
? "transparent" ? "transparent"
: theme.palette.background.paper : theme.palette.background.paper
}, },
drawerPaperWithTitleAndAppBar: { drawerPaperWithTitleAndAppBar: {
width: 250, width: 250,
zIndex: -1, zIndex: -1,
marginTop: 88, marginTop: 88,
backgroundColor: isDarwinApp() backgroundColor: isDarwinApp()
? "transparent" ? "transparent"
: theme.palette.background.paper : theme.palette.background.paper
}, },
drawerDisplayMobile: { drawerDisplayMobile: {
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
display: "none" display: "none"
} }
}, },
toolbar: theme.mixins.toolbar, toolbar: theme.mixins.toolbar,
sectionDesktop: { sectionDesktop: {
display: "none", display: "none",
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
display: "flex" display: "flex"
} }
}, },
sectionMobile: { sectionMobile: {
display: "flex", display: "flex",
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
display: "none" display: "none"
} }
}, },
content: { content: {
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: 250 marginLeft: 250
}, },
overflowY: "auto" overflowY: "auto"
}, },
composeButton: { composeButton: {
position: "fixed", position: "fixed",
bottom: theme.spacing.unit * 2, bottom: theme.spacing.unit * 2,
right: theme.spacing.unit * 2, right: theme.spacing.unit * 2,
zIndex: 50 zIndex: 50
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,17 +1,17 @@
import { Theme, createStyles } from "@material-ui/core"; import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
mediaContainer: { mediaContainer: {
padding: theme.spacing.unit * 2 padding: theme.spacing.unit * 2
}, },
mediaObject: { mediaObject: {
width: "100%", width: "100%",
height: "100%" height: "100%"
}, },
mediaSlide: { mediaSlide: {
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
width: "100%", width: "100%",
height: "auto" height: "auto"
} }
}); });

View File

@ -1,141 +1,146 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
Typography, Typography,
MobileStepper, MobileStepper,
Button Button
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./Attachment.styles"; import { styles } from "./Attachment.styles";
import { Attachment } from "../../types/Attachment"; import { Attachment } from "../../types/Attachment";
import SwipeableViews from "react-swipeable-views"; import SwipeableViews from "react-swipeable-views";
interface IAttachmentProps { interface IAttachmentProps {
media: [Attachment]; media: [Attachment];
classes?: any; classes?: any;
} }
interface IAttachmentState { interface IAttachmentState {
totalSteps: number; totalSteps: number;
currentStep: number; currentStep: number;
attachments: [Attachment]; attachments: [Attachment];
} }
class AttachmentComponent extends Component< class AttachmentComponent extends Component<
IAttachmentProps, IAttachmentProps,
IAttachmentState IAttachmentState
> { > {
constructor(props: IAttachmentProps) { constructor(props: IAttachmentProps) {
super(props); super(props);
this.state = { this.state = {
attachments: this.props.media, attachments: this.props.media,
totalSteps: this.props.media.length, totalSteps: this.props.media.length,
currentStep: 0 currentStep: 0
}; };
}
moveBack() {
let nextStep = this.state.currentStep - 1;
if (nextStep < 0) {
nextStep = 0;
} }
this.setState({ currentStep: nextStep });
}
moveForward() { moveBack() {
let nextStep = this.state.currentStep + 1; let nextStep = this.state.currentStep - 1;
if (nextStep > this.state.totalSteps) { if (nextStep < 0) {
nextStep = this.state.totalSteps; nextStep = 0;
}
this.setState({ currentStep: nextStep });
} }
this.setState({ currentStep: nextStep });
}
handleStepChange(currentStep: number) { moveForward() {
this.setState({ let nextStep = this.state.currentStep + 1;
currentStep if (nextStep > this.state.totalSteps) {
}); nextStep = this.state.totalSteps;
} }
this.setState({ currentStep: nextStep });
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() { handleStepChange(currentStep: number) {
const { classes } = this.props; this.setState({
const step = this.state.currentStep; currentStep
const mediaItem = this.state.attachments[step]; });
return ( }
<div className={classes.mediaContainer}>
<SwipeableViews index={this.state.currentStep}> getSlide(slide: Attachment) {
{this.state.attachments.map((slide: Attachment) => { const { classes } = this.props;
return ( switch (slide.type) {
<div key={slide.id} className={classes.mediaSlide}> case "image":
{this.getSlide(slide)} return (
</div> <img
); src={slide.url}
})} alt={slide.description ? slide.description : ""}
</SwipeableViews> className={classes.mediaObject}
<MobileStepper />
steps={this.state.totalSteps} );
position="static" case "video":
activeStep={this.state.currentStep} return (
className={classes.mobileStepper} <video
nextButton={ controls
<Button autoPlay={false}
size="small" src={slide.url}
onClick={() => this.moveForward()} className={classes.mediaObject}
disabled={this.state.currentStep === this.state.totalSteps - 1} />
> );
Next case "gifv":
</Button> return (
} <img
backButton={ src={slide.url}
<Button alt={slide.description ? slide.description : ""}
size="small" className={classes.mediaObject}
onClick={() => this.moveBack()} />
disabled={this.state.currentStep === 0} );
> case "unknown":
Back return (
</Button> <object data={slide.url} className={classes.mediaObject} />
} );
/> }
<Typography variant="caption"> }
{mediaItem.description
? mediaItem.description render() {
: "No description provided."} const { classes } = this.props;
</Typography> const step = this.state.currentStep;
</div> const mediaItem = this.state.attachments[step];
); return (
} <div className={classes.mediaContainer}>
<SwipeableViews index={this.state.currentStep}>
{this.state.attachments.map((slide: Attachment) => {
return (
<div key={slide.id} className={classes.mediaSlide}>
{this.getSlide(slide)}
</div>
);
})}
</SwipeableViews>
<MobileStepper
steps={this.state.totalSteps}
position="static"
activeStep={this.state.currentStep}
className={classes.mobileStepper}
nextButton={
<Button
size="small"
onClick={() => this.moveForward()}
disabled={
this.state.currentStep ===
this.state.totalSteps - 1
}
>
Next
</Button>
}
backButton={
<Button
size="small"
onClick={() => this.moveBack()}
disabled={this.state.currentStep === 0}
>
Back
</Button>
}
/>
<Typography variant="caption">
{mediaItem.description
? mediaItem.description
: "No description provided."}
</Typography>
</div>
);
}
} }
export default withStyles(styles)(AttachmentComponent); export default withStyles(styles)(AttachmentComponent);

View File

@ -1,18 +1,18 @@
import { Theme, createStyles } from "@material-ui/core"; import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
attachmentArea: { attachmentArea: {
height: 175, height: 175,
width: 268, width: 268,
backgroundColor: theme.palette.primary.light, backgroundColor: theme.palette.primary.light,
color: theme.palette.common.white color: theme.palette.common.white
}, },
attachmentBar: { attachmentBar: {
marginLeft: 0 marginLeft: 0
}, },
attachmentText: { attachmentText: {
backgroundColor: theme.palette.background.paper, backgroundColor: theme.palette.background.paper,
opacity: 0.5 opacity: 0.5
} }
}); });

View File

@ -1,10 +1,10 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
GridListTile, GridListTile,
GridListTileBar, GridListTileBar,
TextField, TextField,
withStyles, withStyles,
IconButton IconButton
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./ComposeMediaAttachment.styles"; import { styles } from "./ComposeMediaAttachment.styles";
import { withSnackbar, withSnackbarProps } from "notistack"; import { withSnackbar, withSnackbarProps } from "notistack";
@ -13,82 +13,92 @@ import { Attachment } from "../../types/Attachment";
import DeleteIcon from "@material-ui/icons/Delete"; import DeleteIcon from "@material-ui/icons/Delete";
interface IComposeMediaAttachmentProps extends withSnackbarProps { interface IComposeMediaAttachmentProps extends withSnackbarProps {
classes: any; classes: any;
client: Mastodon; client: Mastodon;
attachment: Attachment; attachment: Attachment;
onDeleteCallback: any; onDeleteCallback: any;
onAttachmentUpdate: any; onAttachmentUpdate: any;
} }
interface IComposeMediaAttachmentState { interface IComposeMediaAttachmentState {
attachment: Attachment; attachment: Attachment;
} }
class ComposeMediaAttachment extends Component< class ComposeMediaAttachment extends Component<
IComposeMediaAttachmentProps, IComposeMediaAttachmentProps,
IComposeMediaAttachmentState IComposeMediaAttachmentState
> { > {
client: Mastodon; client: Mastodon;
constructor(props: IComposeMediaAttachmentProps) { constructor(props: IComposeMediaAttachmentProps) {
super(props); super(props);
this.client = this.props.client; this.client = this.props.client;
this.state = { this.state = {
attachment: this.props.attachment attachment: this.props.attachment
}; };
} }
updateAttachmentText(text: string) { updateAttachmentText(text: string) {
this.client this.client
.put(`/media/${this.state.attachment.id}`, { description: text }) .put(`/media/${this.state.attachment.id}`, { description: text })
.then((resp: any) => { .then((resp: any) => {
this.props.onAttachmentUpdate(resp.data); this.props.onAttachmentUpdate(resp.data);
this.props.enqueueSnackbar("Description updated."); this.props.enqueueSnackbar("Description updated.");
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update description: " + err.name); this.props.enqueueSnackbar(
}); "Couldn't update description: " + err.name
} );
});
}
render() { render() {
const { classes, attachment } = this.props; const { classes, attachment } = this.props;
return ( return (
<GridListTile className={classes.attachmentArea}> <GridListTile className={classes.attachmentArea}>
{attachment.type === "image" || attachment.type === "gifv" ? ( {attachment.type === "image" || attachment.type === "gifv" ? (
<img <img
src={attachment.url} src={attachment.url}
alt={attachment.description ? attachment.description : ""} alt={
/> attachment.description ? attachment.description : ""
) : attachment.type === "video" ? ( }
<video autoPlay={false} src={attachment.url} /> />
) : ( ) : attachment.type === "video" ? (
<object data={attachment.url} /> <video autoPlay={false} src={attachment.url} />
)} ) : (
<GridListTileBar <object data={attachment.url} />
classes={{ title: classes.attachmentBar }} )}
title={ <GridListTileBar
<TextField classes={{ title: classes.attachmentBar }}
variant="filled" title={
label="Description" <TextField
margin="dense" variant="filled"
className={classes.attachmentText} label="Description"
onBlur={event => this.updateAttachmentText(event.target.value)} margin="dense"
></TextField> className={classes.attachmentText}
} onBlur={event =>
actionIcon={ this.updateAttachmentText(event.target.value)
<IconButton }
color="inherit" ></TextField>
onClick={() => this.props.onDeleteCallback(this.state.attachment)} }
> actionIcon={
<DeleteIcon /> <IconButton
</IconButton> color="inherit"
} onClick={() =>
/> this.props.onDeleteCallback(
</GridListTile> this.state.attachment
); )
} }
>
<DeleteIcon />
</IconButton>
}
/>
</GridListTile>
);
}
} }
export default withStyles(styles)(withSnackbar(ComposeMediaAttachment)); export default withStyles(styles)(withSnackbar(ComposeMediaAttachment));

View File

@ -3,30 +3,30 @@ import { Picker, PickerProps, CustomEmoji } from "emoji-mart";
import "emoji-mart/css/emoji-mart.css"; import "emoji-mart/css/emoji-mart.css";
interface IEmojiPickerProps extends PickerProps { interface IEmojiPickerProps extends PickerProps {
onGetEmoji: any; onGetEmoji: any;
} }
export class EmojiPicker extends Component<IEmojiPickerProps, any> { export class EmojiPicker extends Component<IEmojiPickerProps, any> {
retrieveFromLocal() { retrieveFromLocal() {
return JSON.parse(localStorage.getItem("emojis") as string); return JSON.parse(localStorage.getItem("emojis") as string);
} }
render() { render() {
return ( return (
<Picker <Picker
custom={this.retrieveFromLocal()} custom={this.retrieveFromLocal()}
emoji="" emoji=""
title="" title=""
onClick={this.props.onGetEmoji} onClick={this.props.onGetEmoji}
style={{ style={{
borderColor: "transparent" borderColor: "transparent"
}} }}
perLine={10} perLine={10}
emojiSize={20} emojiSize={20}
set={"google"} set={"google"}
/> />
); );
} }
} }
export default EmojiPicker; export default EmojiPicker;

View File

@ -1,99 +1,99 @@
import { Theme, createStyles } from "@material-ui/core"; import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
post: { post: {
marginTop: theme.spacing.unit, marginTop: theme.spacing.unit,
marginBottom: theme.spacing.unit marginBottom: theme.spacing.unit
},
postReblogChip: {
color: theme.palette.common.white,
"&:hover": {
backgroundColor: theme.palette.secondary.light
},
backgroundColor: theme.palette.secondary.main,
marginBottom: theme.spacing.unit
},
postContent: {
paddingTop: 0,
paddingBottom: 0,
"& a": {
textDecoration: "none",
color: theme.palette.secondary.light,
"&:hover": {
textDecoration: "underline"
}, },
"&.u-url.mention": { postReblogChip: {
textDecoration: "none", color: theme.palette.common.white,
color: "inherit", "&:hover": {
fontWeight: "bold" backgroundColor: theme.palette.secondary.light
},
backgroundColor: theme.palette.secondary.main,
marginBottom: theme.spacing.unit
}, },
"&.mention.hashtag": { postContent: {
textDecoration: "none", paddingTop: 0,
color: "inherit", paddingBottom: 0,
fontWeight: "bold" "& a": {
textDecoration: "none",
color: theme.palette.secondary.light,
"&:hover": {
textDecoration: "underline"
},
"&.u-url.mention": {
textDecoration: "none",
color: "inherit",
fontWeight: "bold"
},
"&.mention.hashtag": {
textDecoration: "none",
color: "inherit",
fontWeight: "bold"
}
}
},
postCard: {
"& a:hover": {
textDecoration: "none"
}
},
postEmoji: {
height: theme.typography.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: "inherit"
},
postDidAction: {
color: theme.palette.secondary.main
},
postMention: {
marginRight: theme.spacing.unit,
marginBottom: theme.spacing.unit
},
nsfwCard: {
backgroundColor: theme.palette.error.main
},
postTags: {
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit
},
postAuthorEmoji: {
height: theme.typography.fontSize,
verticalAlign: "middle"
},
heading: {
color: "inherit"
},
mobileOnly: {
[theme.breakpoints.up("sm")]: {
display: "none"
}
},
desktopOnly: {
display: "none",
[theme.breakpoints.up("sm")]: {
display: "block"
}
} }
} });
},
postCard: {
"& a:hover": {
textDecoration: "none"
}
},
postEmoji: {
height: theme.typography.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: "inherit"
},
postDidAction: {
color: theme.palette.secondary.main
},
postMention: {
marginRight: theme.spacing.unit,
marginBottom: theme.spacing.unit
},
nsfwCard: {
backgroundColor: theme.palette.error.main
},
postTags: {
paddingTop: theme.spacing.unit,
paddingBottom: theme.spacing.unit
},
postAuthorEmoji: {
height: theme.typography.fontSize,
verticalAlign: "middle"
},
heading: {
color: "inherit"
},
mobileOnly: {
[theme.breakpoints.up("sm")]: {
display: "none"
}
},
desktopOnly: {
display: "none",
[theme.breakpoints.up("sm")]: {
display: "block"
}
}
});

File diff suppressed because it is too large Load Diff

View File

@ -3,18 +3,18 @@ import webShare, { WebShareInterface } from "react-web-share-api";
import { MenuItem } from "@material-ui/core"; import { MenuItem } from "@material-ui/core";
export interface OwnProps { export interface OwnProps {
style: object; style: object;
} }
const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({ const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({
share, share,
isSupported, isSupported,
style style
}) => }) =>
isSupported ? ( isSupported ? (
<MenuItem onClick={share} style={style}> <MenuItem onClick={share} style={style}>
Share Share
</MenuItem> </MenuItem>
) : null; ) : null;
export default webShare<OwnProps>()(ShareMenu); export default webShare<OwnProps>()(ShareMenu);

View File

@ -1,88 +1,93 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
MuiThemeProvider, MuiThemeProvider,
Theme, Theme,
AppBar, AppBar,
Typography, Typography,
CssBaseline, CssBaseline,
Toolbar, Toolbar,
Fab, Fab,
Paper Paper
} from "@material-ui/core"; } from "@material-ui/core";
import EditIcon from "@material-ui/icons/Edit"; import EditIcon from "@material-ui/icons/Edit";
import MenuIcon from "@material-ui/icons/Menu"; import MenuIcon from "@material-ui/icons/Menu";
interface IThemePreviewProps { interface IThemePreviewProps {
theme: Theme; theme: Theme;
} }
interface IThemePreviewState { interface IThemePreviewState {
theme: Theme; theme: Theme;
} }
class ThemePreview extends Component<IThemePreviewProps, IThemePreviewState> { class ThemePreview extends Component<IThemePreviewProps, IThemePreviewState> {
constructor(props: IThemePreviewProps) { constructor(props: IThemePreviewProps) {
super(props); super(props);
this.state = { this.state = {
theme: this.props.theme theme: this.props.theme
}; };
} }
render() { render() {
return ( return (
<div style={{ position: "relative" }}> <div style={{ position: "relative" }}>
<MuiThemeProvider theme={this.props.theme}> <MuiThemeProvider theme={this.props.theme}>
<CssBaseline /> <CssBaseline />
<Paper> <Paper>
<AppBar color="primary" position="static"> <AppBar color="primary" position="static">
<Toolbar> <Toolbar>
<MenuIcon style={{ marginRight: 20, marginLeft: -4 }} /> <MenuIcon
<Typography variant="h6" color="inherit"> style={{ marginRight: 20, marginLeft: -4 }}
Hyperspace />
</Typography> <Typography variant="h6" color="inherit">
</Toolbar> Hyperspace
</AppBar> </Typography>
<div </Toolbar>
style={{ </AppBar>
paddingLeft: 16, <div
paddingTop: 16, style={{
paddingRight: 16, paddingLeft: 16,
paddingBottom: 16, paddingTop: 16,
flexGrow: 1 paddingRight: 16,
}} paddingBottom: 16,
> flexGrow: 1
<Typography variant="h4" component="p"> }}
This is your theme. >
</Typography> <Typography variant="h4" component="p">
<br /> This is your theme.
<Typography paragraph> </Typography>
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nunc <br />
vestibulum congue sem ac ornare. In nec imperdiet neque. In <Typography paragraph>
eleifend laoreet efficitur. Vestibulum vel odio mattis, Lorem ipsum dolor sit amet, consectetur
scelerisque nibh a, ornare lectus. Phasellus sollicitudin erat adipiscing elit. Nunc vestibulum congue sem ac
et turpis pellentesque consequat. In maximus luctus purus, eu ornare. In nec imperdiet neque. In eleifend
molestie elit euismod eu. Pellentesque quam lectus, sagittis laoreet efficitur. Vestibulum vel odio mattis,
eget accumsan in, consequat ut sapien. Morbi aliquet ligula scelerisque nibh a, ornare lectus. Phasellus
erat, id dapibus nunc laoreet at. Integer sodales lacinia sollicitudin erat et turpis pellentesque
finibus. Aliquam augue nibh, eleifend quis consectetur et, consequat. In maximus luctus purus, eu molestie
rhoncus ut odio. Lorem ipsum dolor sit amet, consectetur elit euismod eu. Pellentesque quam lectus,
adipiscing elit. sagittis eget accumsan in, consequat ut sapien.
</Typography> Morbi aliquet ligula erat, id dapibus nunc
laoreet at. Integer sodales lacinia finibus.
Aliquam augue nibh, eleifend quis consectetur
et, rhoncus ut odio. Lorem ipsum dolor sit amet,
consectetur adipiscing elit.
</Typography>
</div>
<div style={{ textAlign: "right" }}>
<Fab
color="secondary"
style={{ marginRight: 8, marginBottom: 8 }}
>
<EditIcon />
</Fab>
</div>
</Paper>
</MuiThemeProvider>
</div> </div>
<div style={{ textAlign: "right" }}> );
<Fab }
color="secondary"
style={{ marginRight: 8, marginBottom: 8 }}
>
<EditIcon />
</Fab>
</div>
</Paper>
</MuiThemeProvider>
</div>
);
}
} }
export default ThemePreview; export default ThemePreview;

View File

@ -9,31 +9,31 @@ import { SnackbarProvider } from "notistack";
import { userLoggedIn, refreshUserAccountData } from "./utilities/accounts"; import { userLoggedIn, refreshUserAccountData } from "./utilities/accounts";
getConfig() getConfig()
.then((config: any) => { .then((config: any) => {
document.title = config.branding.name || "Hyperspace"; document.title = config.branding.name || "Hyperspace";
}) })
.catch((err: Error) => { .catch((err: Error) => {
console.error(err); console.error(err);
}); });
createUserDefaults(); createUserDefaults();
if (userLoggedIn()) { if (userLoggedIn()) {
collectEmojisFromServer(); collectEmojisFromServer();
refreshUserAccountData(); refreshUserAccountData();
} }
ReactDOM.render( ReactDOM.render(
<HashRouter> <HashRouter>
<SnackbarProvider <SnackbarProvider
anchorOrigin={{ anchorOrigin={{
vertical: "bottom", vertical: "bottom",
horizontal: "left" horizontal: "left"
}} }}
> >
<App /> <App />
</SnackbarProvider> </SnackbarProvider>
</HashRouter>, </HashRouter>,
document.getElementById("root") document.getElementById("root")
); );
// If you want your app to work offline and load faster, you can change // If you want your app to work offline and load faster, you can change

View File

@ -11,87 +11,87 @@ import Avatar, { AvatarProps } from "@material-ui/core/Avatar";
import { userLoggedIn } from "../utilities/accounts"; import { userLoggedIn } from "../utilities/accounts";
export interface ILinkableListItemProps extends ListItemProps { export interface ILinkableListItemProps extends ListItemProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableIconButtonProps extends IconButtonProps { export interface ILinkableIconButtonProps extends IconButtonProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableChipProps extends ChipProps { export interface ILinkableChipProps extends ChipProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableMenuItemProps extends MenuItemProps { export interface ILinkableMenuItemProps extends MenuItemProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableButtonProps extends ButtonProps { export interface ILinkableButtonProps extends ButtonProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableFabProps extends FabProps { export interface ILinkableFabProps extends FabProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export interface ILinkableAvatarProps extends AvatarProps { export interface ILinkableAvatarProps extends AvatarProps {
to: string; to: string;
replace?: boolean; replace?: boolean;
} }
export const LinkableListItem = (props: ILinkableListItemProps) => ( export const LinkableListItem = (props: ILinkableListItemProps) => (
<ListItem {...props} component={Link as any} /> <ListItem {...props} component={Link as any} />
); );
export const LinkableIconButton = (props: ILinkableIconButtonProps) => ( export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
<IconButton {...props} component={Link as any} /> <IconButton {...props} component={Link as any} />
); );
export const LinkableChip = (props: ILinkableChipProps) => ( export const LinkableChip = (props: ILinkableChipProps) => (
<Chip {...props} component={Link as any} /> <Chip {...props} component={Link as any} />
); );
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => ( export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
<MenuItem {...props} component={Link as any} /> <MenuItem {...props} component={Link as any} />
); );
export const LinkableButton = (props: ILinkableButtonProps) => ( export const LinkableButton = (props: ILinkableButtonProps) => (
<Button {...props} component={Link as any} /> <Button {...props} component={Link as any} />
); );
export const LinkableFab = (props: ILinkableFabProps) => ( export const LinkableFab = (props: ILinkableFabProps) => (
<Fab {...props} component={Link as any} /> <Fab {...props} component={Link as any} />
); );
export const LinkableAvatar = (props: ILinkableAvatarProps) => ( export const LinkableAvatar = (props: ILinkableAvatarProps) => (
<Avatar {...props} component={Link as any} /> <Avatar {...props} component={Link as any} />
); );
export const ProfileRoute = (rest: any, component: Component) => ( export const ProfileRoute = (rest: any, component: Component) => (
<Route {...rest} render={props => <Component {...props} />} /> <Route {...rest} render={props => <Component {...props} />} />
); );
export const PrivateRoute = (props: IPrivateRouteProps) => { export const PrivateRoute = (props: IPrivateRouteProps) => {
const { component, render, ...rest } = props; const { component, render, ...rest } = props;
return ( return (
<Route <Route
{...rest} {...rest}
render={(compProps: any) => render={(compProps: any) =>
userLoggedIn() ? ( userLoggedIn() ? (
React.createElement(component, compProps) React.createElement(component, compProps)
) : ( ) : (
<Redirect to="/welcome" /> <Redirect to="/welcome" />
) )
} }
/> />
); );
}; };
interface IPrivateRouteProps extends RouteProps { interface IPrivateRouteProps extends RouteProps {
component: any; component: any;
} }

View File

@ -110,6 +110,135 @@ class AboutPage extends Component<any, IAboutPageState> {
return ( return (
<div className={classes.pageLayoutConstraints}> <div className={classes.pageLayoutConstraints}>
<Paper>
<div
className={classes.instanceHeaderPaper}
style={{
backgroundImage: `url("${
this.state.brandBg ? this.state.brandBg : ""
}")`
}}
>
<div className={classes.instanceToolbar}>
{this.state.repository ? (
<Tooltip title="View source code">
<IconButton
href={this.state.repository}
target="_blank"
rel="noreferrer"
color="inherit"
>
<CodeIcon />
</IconButton>
</Tooltip>
) : null}
</div>
<div className={classes.instanceHeaderText}>
<Typography variant="h4" component="p">
{this.state.brandName? this.state.brandName: "Hyperspace"}
</Typography>
<Typography>Version {`${this.state? this.state.versionNumber: "1.0.x"} ${this.state && this.state.brandName !== "Hyperspace"? "(Hyperspace-like)": ""}`}</Typography>
</div>
</div>
<List className={classes.pageListConstraints}>
<ListItem>
<ListItemAvatar>
<LinkableAvatar
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
src={
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.avatar_static
: ""
}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary="App provider"
secondary={
this.state.hyperspaceAdmin && this.state.hyperspaceAdminName
? this.state.hyperspaceAdminName ||
this.state.hyperspaceAdmin.display_name ||
"@" + this.state.hyperspaceAdmin.acct
: "No provider set in config"
}
/>
<ListItemSecondaryAction>
<Tooltip title="Send a post or message">
<LinkableIconButton
to={`/compose?visibility=${
this.state.federated ? "public" : "private"
}&acct=${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.acct
: ""
}`}
>
<ChatIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<NotesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="License"
secondary={this.state.license.name}
/>
<ListItemSecondaryAction>
<Tooltip title="View license">
<IconButton
href={this.state.license.url}
target="_blank"
rel="noreferrer"
>
<OpenInNewIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<UpdateIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Release channel"
secondary={
this.state
? this.state.developer
? "Developer"
: "Release"
: "Loading..."
}
/>
</ListItem>
</List>
</Paper>
<br />
<Paper> <Paper>
<div <div
className={classes.instanceHeaderPaper} className={classes.instanceHeaderPaper}
@ -130,13 +259,10 @@ class AboutPage extends Component<any, IAboutPageState> {
> >
<OpenInNewIcon /> <OpenInNewIcon />
</IconButton> </IconButton>
<Typography <div className={classes.instanceHeaderText}>
className={classes.instanceHeaderText} <Typography variant="h4" component="p">{this.state.instance ? this.state.instance.uri: "Loading..."}</Typography>
variant="h4" <Typography>Server version {this.state.instance? this.state.instance.version: "x.x.x"}</Typography>
component="p" </div>
>
{this.state.instance ? this.state.instance.uri : "Loading..."}
</Typography>
</div> </div>
<List className={classes.pageListConstraints}> <List className={classes.pageListConstraints}>
{localStorage["isPleroma"] == "false" && ( {localStorage["isPleroma"] == "false" && (
@ -238,168 +364,9 @@ class AboutPage extends Component<any, IAboutPageState> {
</Tooltip> </Tooltip>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<MastodonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Mastodon version"
secondary={
this.state.instance ? this.state.instance.version : "x.x.x"
}
/>
</ListItem>
</List> </List>
</Paper> </Paper>
<br />
<Paper>
<div
className={classes.instanceHeaderPaper}
style={{
backgroundImage: `url("${
this.state.brandBg ? this.state.brandBg : ""
}")`
}}
>
<div className={classes.instanceToolbar}>
{this.state.repository ? (
<Tooltip title="View source code">
<IconButton
href={this.state.repository}
target="_blank"
rel="noreferrer"
color="inherit"
>
<CodeIcon />
</IconButton>
</Tooltip>
) : null}
</div>
<Typography
className={classes.instanceHeaderText}
variant="h4"
component="p"
>
{this.state.brandName ? this.state.brandName : "Hyperspace"}
</Typography>
</div>
<List className={classes.pageListConstraints}>
<ListItem>
<ListItemAvatar>
<LinkableAvatar
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
src={
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.avatar_static
: ""
}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary="App provider"
secondary={
this.state.hyperspaceAdmin && this.state.hyperspaceAdminName
? this.state.hyperspaceAdminName ||
this.state.hyperspaceAdmin.display_name ||
"@" + this.state.hyperspaceAdmin.acct
: "No provider set in config"
}
/>
<ListItemSecondaryAction>
<Tooltip title="Send a post or message">
<LinkableIconButton
to={`/compose?visibility=${
this.state.federated ? "public" : "private"
}&acct=${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.acct
: ""
}`}
>
<ChatIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${
this.state.hyperspaceAdmin
? this.state.hyperspaceAdmin.id
: 0
}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<NotesIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="License"
secondary={this.state.license.name}
/>
<ListItemSecondaryAction>
<Tooltip title="View license">
<IconButton
href={this.state.license.url}
target="_blank"
rel="noreferrer"
>
<OpenInNewIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<UpdateIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Release channel"
secondary={
this.state
? this.state.developer
? "Developer"
: "Release"
: "Loading..."
}
/>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<InfoIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="App version"
secondary={`${
this.state ? this.state.brandName : "Hyperspace"
} v${this.state ? this.state.versionNumber : "1.0.x"} ${
this.state && this.state.brandName !== "Hyperspace"
? "(Hyperspace-like)"
: ""
}`}
/>
</ListItem>
</List>
</Paper>
<br /> <br />
<ListSubheader>Federation status</ListSubheader> <ListSubheader>Federation status</ListSubheader>
<Paper> <Paper>

View File

@ -1,49 +1,49 @@
import { Theme, createStyles } from "@material-ui/core"; import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
dialog: { dialog: {
minHeight: 400 minHeight: 400
}, },
dialogContent: { dialogContent: {
paddingBottom: 0 paddingBottom: 0
}, },
dialogActions: { dialogActions: {
paddingLeft: theme.spacing.unit * 1.25 paddingLeft: theme.spacing.unit * 1.25
}, },
charsReachingLimit: { charsReachingLimit: {
color: theme.palette.error.main color: theme.palette.error.main
}, },
warningCaption: { warningCaption: {
height: 16, height: 16,
verticalAlign: "text-bottom" verticalAlign: "text-bottom"
}, },
composeAttachmentArea: { composeAttachmentArea: {
display: "flex", display: "flex",
flexWrap: "wrap", flexWrap: "wrap",
justifyContent: "space-around", justifyContent: "space-around",
overflow: "hidden" overflow: "hidden"
}, },
composeAttachmentAreaGridList: { composeAttachmentAreaGridList: {
height: 250, height: 250,
width: "100%" width: "100%"
}, },
composeEmoji: { composeEmoji: {
marginTop: theme.spacing.unit * 8 marginTop: theme.spacing.unit * 8
}, },
desktopOnly: { desktopOnly: {
display: "none", display: "none",
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
display: "block" display: "block"
} }
}, },
pollWizardOptionIcon: { pollWizardOptionIcon: {
marginRight: theme.spacing.unit * 2, marginRight: theme.spacing.unit * 2,
marginTop: 4, marginTop: 4,
marginBottom: 4, marginBottom: 4,
color: theme.palette.grey[700] color: theme.palette.grey[700]
}, },
pollWizardFlexGrow: { pollWizardFlexGrow: {
flexGrow: 1 flexGrow: 1
} }
}); });

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,9 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
CircularProgress, CircularProgress,
Typography, Typography,
Paper Paper
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Post from "../components/Post"; import Post from "../components/Post";
@ -13,139 +13,152 @@ import Mastodon from "megalodon";
import { withSnackbar } from "notistack"; import { withSnackbar } from "notistack";
interface IConversationPageState { interface IConversationPageState {
posts?: [Status]; posts?: [Status];
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: any; viewDidErrorCode?: any;
conversationId: string; conversationId: string;
} }
class Conversation extends Component<any, IConversationPageState> { class Conversation extends Component<any, IConversationPageState> {
client: Mastodon; client: Mastodon;
streamListener: any; streamListener: any;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
viewIsLoading: true, viewIsLoading: true,
conversationId: props.match.params.conversationId conversationId: props.match.params.conversationId
}; };
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
} }
getContext() { getContext() {
this.client this.client
.get(`/statuses/${this.state.conversationId}`) .get(`/statuses/${this.state.conversationId}`)
.then((resp: any) => { .then((resp: any) => {
let result: Status = resp.data; let result: Status = resp.data;
this.setState({ posts: [result] }); this.setState({ posts: [result] });
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.setState({ this.setState({
viewIsLoading: false, viewIsLoading: false,
viewDidError: true, viewDidError: true,
viewDidErrorCode: err.message viewDidErrorCode: err.message
}); });
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, { this.props.enqueueSnackbar(
variant: "error" "Couldn't get conversation: " + err.name,
}); { variant: "error" }
}); );
this.client });
.get(`/statuses/${this.state.conversationId}/context`) this.client
.then((resp: any) => { .get(`/statuses/${this.state.conversationId}/context`)
let context: Context = resp.data; .then((resp: any) => {
let posts = this.state.posts; let context: Context = resp.data;
let array: any[] = []; let posts = this.state.posts;
if (posts) { let array: any[] = [];
array = array if (posts) {
.concat(context.ancestors) array = array
.concat(posts) .concat(context.ancestors)
.concat(context.descendants); .concat(posts)
.concat(context.descendants);
}
this.setState({
posts: array as [Status],
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar(
"Couldn't get conversation: " + err.name,
{ variant: "error" }
);
});
}
componentWillReceiveProps(props: any) {
if (props.match.params.conversationId !== this.state.conversationId) {
this.getContext();
} }
this.setState({
posts: array as [Status],
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, {
variant: "error"
});
});
}
componentWillReceiveProps(props: any) {
if (props.match.params.conversationId !== this.state.conversationId) {
this.getContext();
} }
}
componentWillMount() { componentWillMount() {
this.getContext(); this.getContext();
}
componentDidUpdate() {
const where: HTMLElement | null = document.getElementById(
`post_${this.state.conversationId}`
);
if (
where &&
this.state.posts &&
this.state.posts[0].id !== this.state.conversationId
) {
window.scrollTo(0, where.getBoundingClientRect().top);
} }
}
render() { componentDidUpdate() {
const { classes } = this.props; const where: HTMLElement | null = document.getElementById(
return ( `post_${this.state.conversationId}`
<div className={classes.pageLayoutMaxConstraints}> );
{this.state.posts ? ( if (
<div> where &&
{this.state.posts.map((post: Status) => { this.state.posts &&
return <Post key={post.id} post={post} client={this.client} />; this.state.posts[0].id !== this.state.conversationId
})} ) {
</div> window.scrollTo(0, where.getBoundingClientRect().top);
) : ( }
<span /> }
)}
{this.state.viewDidError ? ( render() {
<Paper className={classes.errorCard}> const { classes } = this.props;
<Typography variant="h4">Bummer.</Typography> return (
<Typography variant="h6"> <div className={classes.pageLayoutMaxConstraints}>
Something went wrong when loading this conversation. {this.state.posts ? (
</Typography> <div>
<Typography> {this.state.posts.map((post: Status) => {
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""} return (
</Typography> <Post
</Paper> key={post.id}
) : ( post={post}
<span /> client={this.client}
)} />
{this.state.viewIsLoading ? ( );
<div style={{ textAlign: "center" }}> })}
<CircularProgress className={classes.progress} color="primary" /> </div>
</div> ) : (
) : ( <span />
<span /> )}
)} {this.state.viewDidError ? (
</div> <Paper className={classes.errorCard}>
); <Typography variant="h4">Bummer.</Typography>
} <Typography variant="h6">
Something went wrong when loading this conversation.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(Conversation)); export default withStyles(styles)(withSnackbar(Conversation));

View File

@ -1,13 +1,13 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
CircularProgress, CircularProgress,
Typography, Typography,
Paper, Paper,
Button, Button,
Chip, Chip,
Avatar, Avatar,
Slide Slide
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Post from "../components/Post"; import Post from "../components/Post";
@ -17,209 +17,224 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward"; import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface IHomePageState { interface IHomePageState {
posts?: [Status]; posts?: [Status];
backlogPosts?: [Status] | null; backlogPosts?: [Status] | null;
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: any; viewDidErrorCode?: any;
} }
class HomePage extends Component<any, IHomePageState> { class HomePage extends Component<any, IHomePageState> {
client: Mastodon; client: Mastodon;
streamListener: StreamListener; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
viewIsLoading: true, viewIsLoading: true,
backlogPosts: null backlogPosts: null
}; };
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
this.streamListener = this.client.stream("/streaming/user"); this.streamListener = this.client.stream("/streaming/user");
}
componentWillMount() {
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 queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
}
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() {
window.scrollTo(0, 0);
let posts = this.state.posts;
let backlog = this.state.backlogPosts;
if (posts && backlog && backlog.length > 0) {
let push = backlog.concat(posts);
this.setState({ posts: push as [Status], backlogPosts: null });
} }
}
loadMoreTimelinePieces() { componentWillMount() {
this.setState({ viewDidLoad: false, viewIsLoading: true }); this.streamListener.on("connect", () => {
if (this.state.posts) { this.client
this.client .get("/timelines/home", { limit: 40 })
.get("/timelines/home", { .then((resp: any) => {
max_id: this.state.posts[this.state.posts.length - 1].id, let statuses: [Status] = resp.data;
limit: 20 this.setState({
}) posts: statuses,
.then((resp: any) => { viewIsLoading: false,
let newPosts: [Status] = resp.data; viewDidLoad: true,
let posts = this.state.posts as [Status]; viewDidError: false
newPosts.forEach((post: Status) => { });
posts.push(post); })
}); .catch((resp: any) => {
this.setState({ this.setState({
viewIsLoading: false, viewIsLoading: false,
viewDidLoad: true, viewDidLoad: true,
posts viewDidError: true,
}); viewDidErrorCode: String(resp)
}) });
.catch((err: Error) => { this.props.enqueueSnackbar("Failed to get posts.", {
this.setState({ variant: "error"
viewIsLoading: false, });
viewDidError: true, });
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
}); });
this.streamListener.on("update", (status: Status) => {
let queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
} }
}
render() { componentWillUnmount() {
const { classes } = this.props; this.streamListener.stop();
}
return ( insertBacklog() {
<div className={classes.pageLayoutMaxConstraints}> window.scrollTo(0, 0);
{this.state.backlogPosts ? ( let posts = this.state.posts;
<div className={classes.pageTopChipContainer}> let backlog = this.state.backlogPosts;
<div className={classes.pageTopChips}> if (posts && backlog && backlog.length > 0) {
<Slide direction="down" in={true}> let push = backlog.concat(posts);
<Chip this.setState({ posts: push as [Status], backlogPosts: null });
avatar={ }
<Avatar> }
<ArrowUpwardIcon />
</Avatar> loadMoreTimelinePieces() {
} this.setState({ viewDidLoad: false, viewIsLoading: true });
label={`View ${this.state.backlogPosts.length} new post${ if (this.state.posts) {
this.state.backlogPosts.length > 1 ? "s" : "" this.client
}`} .get("/timelines/home", {
color="primary" max_id: this.state.posts[this.state.posts.length - 1].id,
className={classes.pageTopChip} limit: 20
onClick={() => this.insertBacklog()} })
clickable .then((resp: any) => {
/> let newPosts: [Status] = resp.data;
</Slide> 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.backlogPosts ? (
<div className={classes.pageTopChipContainer}>
<div className={classes.pageTopChips}>
<Slide direction="down" in={true}>
<Chip
avatar={
<Avatar>
<ArrowUpwardIcon />
</Avatar>
}
label={`View ${
this.state.backlogPosts.length
} new post${
this.state.backlogPosts.length > 1
? "s"
: ""
}`}
color="primary"
className={classes.pageTopChip}
onClick={() => this.insertBacklog()}
clickable
/>
</Slide>
</div>
</div>
) : null}
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div> </div>
</div> );
) : null} }
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return <Post key={post.id} post={post} client={this.client} />;
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress className={classes.progress} color="primary" />
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(HomePage)); export default withStyles(styles)(withSnackbar(HomePage));

View File

@ -1,13 +1,13 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
CircularProgress, CircularProgress,
Typography, Typography,
Paper, Paper,
Button, Button,
Chip, Chip,
Avatar, Avatar,
Slide Slide
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Post from "../components/Post"; import Post from "../components/Post";
@ -17,210 +17,225 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward"; import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface ILocalPageState { interface ILocalPageState {
posts?: [Status]; posts?: [Status];
backlogPosts?: [Status] | null; backlogPosts?: [Status] | null;
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: any; viewDidErrorCode?: any;
} }
class LocalPage extends Component<any, ILocalPageState> { class LocalPage extends Component<any, ILocalPageState> {
client: Mastodon; client: Mastodon;
streamListener: StreamListener; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
viewIsLoading: true, viewIsLoading: true,
backlogPosts: null backlogPosts: null
}; };
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
this.streamListener = this.client.stream("/streaming/public/local"); this.streamListener = this.client.stream("/streaming/public/local");
}
componentWillMount() {
this.streamListener.on("connect", () => {
this.client
.get("/timelines/public", { limit: 40, local: true })
.then((resp: any) => {
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 queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
}
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() {
window.scrollTo(0, 0);
let posts = this.state.posts;
let backlog = this.state.backlogPosts;
if (posts && backlog && backlog.length > 0) {
let push = backlog.concat(posts);
this.setState({ posts: push as [Status], backlogPosts: null });
} }
}
loadMoreTimelinePieces() { componentWillMount() {
this.setState({ viewDidLoad: false, viewIsLoading: true }); this.streamListener.on("connect", () => {
if (this.state.posts) { this.client
this.client .get("/timelines/public", { limit: 40, local: true })
.get("/timelines/public", { .then((resp: any) => {
max_id: this.state.posts[this.state.posts.length - 1].id, let statuses: [Status] = resp.data;
limit: 20, this.setState({
local: true posts: statuses,
}) viewIsLoading: false,
.then((resp: any) => { viewDidLoad: true,
let newPosts: [Status] = resp.data; viewDidError: false
let posts = this.state.posts as [Status]; });
newPosts.forEach((post: Status) => { })
posts.push(post); .catch((resp: any) => {
}); this.setState({
this.setState({ viewIsLoading: false,
viewIsLoading: false, viewDidLoad: true,
viewDidLoad: true, viewDidError: true,
posts viewDidErrorCode: String(resp)
}); });
}) this.props.enqueueSnackbar("Failed to get posts.", {
.catch((err: Error) => { variant: "error"
this.setState({ });
viewIsLoading: false, });
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
}); });
this.streamListener.on("update", (status: Status) => {
let queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
} }
}
render() { componentWillUnmount() {
const { classes } = this.props; this.streamListener.stop();
}
return ( insertBacklog() {
<div className={classes.pageLayoutMaxConstraints}> window.scrollTo(0, 0);
{this.state.backlogPosts ? ( let posts = this.state.posts;
<div className={classes.pageTopChipContainer}> let backlog = this.state.backlogPosts;
<div className={classes.pageTopChips}> if (posts && backlog && backlog.length > 0) {
<Slide direction="down" in={true}> let push = backlog.concat(posts);
<Chip this.setState({ posts: push as [Status], backlogPosts: null });
avatar={ }
<Avatar> }
<ArrowUpwardIcon />
</Avatar> loadMoreTimelinePieces() {
} this.setState({ viewDidLoad: false, viewIsLoading: true });
label={`View ${this.state.backlogPosts.length} new post${ if (this.state.posts) {
this.state.backlogPosts.length > 1 ? "s" : "" this.client
}`} .get("/timelines/public", {
color="primary" max_id: this.state.posts[this.state.posts.length - 1].id,
className={classes.pageTopChip} limit: 20,
onClick={() => this.insertBacklog()} local: true
clickable })
/> .then((resp: any) => {
</Slide> 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.backlogPosts ? (
<div className={classes.pageTopChipContainer}>
<div className={classes.pageTopChips}>
<Slide direction="down" in={true}>
<Chip
avatar={
<Avatar>
<ArrowUpwardIcon />
</Avatar>
}
label={`View ${
this.state.backlogPosts.length
} new post${
this.state.backlogPosts.length > 1
? "s"
: ""
}`}
color="primary"
className={classes.pageTopChip}
onClick={() => this.insertBacklog()}
clickable
/>
</Slide>
</div>
</div>
) : null}
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div> </div>
</div> );
) : null} }
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return <Post key={post.id} post={post} client={this.client} />;
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress className={classes.progress} color="primary" />
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(LocalPage)); export default withStyles(styles)(withSnackbar(LocalPage));

View File

@ -1,16 +1,16 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
ListSubheader, ListSubheader,
Paper, Paper,
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
CircularProgress, CircularProgress,
ListItemAvatar, ListItemAvatar,
Avatar, Avatar,
ListItemSecondaryAction, ListItemSecondaryAction,
Tooltip Tooltip
} from "@material-ui/core"; } from "@material-ui/core";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
import ForumIcon from "@material-ui/icons/Forum"; import ForumIcon from "@material-ui/icons/Forum";
@ -20,111 +20,130 @@ import { Status } from "../types/Status";
import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides"; import { LinkableIconButton, LinkableAvatar } from "../interfaces/overrides";
interface IMessagesState { interface IMessagesState {
posts?: [Status]; posts?: [Status];
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: any; viewDidErrorCode?: any;
} }
class MessagesPage extends Component<any, IMessagesState> { class MessagesPage extends Component<any, IMessagesState> {
client: Mastodon; client: Mastodon;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
this.state = { this.state = {
viewIsLoading: true viewIsLoading: true
}; };
} }
componentWillMount() { componentWillMount() {
this.client.get("/conversations").then(resp => { this.client.get("/conversations").then(resp => {
let data: any = resp.data; let data: any = resp.data;
let messages: any = []; let messages: any = [];
data.forEach((message: any) => { data.forEach((message: any) => {
if (message.last_status !== null) { if (message.last_status !== null) {
messages.push(message.last_status); messages.push(message.last_status);
} }
}); });
this.setState({ this.setState({
posts: messages, posts: messages,
viewIsLoading: false, viewIsLoading: false,
viewDidLoad: true viewDidLoad: true
}); });
}); });
} }
removeHTMLContent(text: string) { removeHTMLContent(text: string) {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = text; div.innerHTML = text;
let innerContent = div.textContent || div.innerText || ""; let innerContent = div.textContent || div.innerText || "";
innerContent = innerContent.slice(0, 100) + "..."; innerContent = innerContent.slice(0, 100) + "...";
return innerContent; return innerContent;
} }
render() { render() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.pageLayoutConstraints}> <div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? ( {this.state.viewDidLoad ? (
<div className={classes.pageListContsraints}> <div className={classes.pageListContsraints}>
<ListSubheader>Recent messages</ListSubheader> <ListSubheader>Recent messages</ListSubheader>
<Paper className={classes.pageListConstraints}> <Paper className={classes.pageListConstraints}>
<List> <List>
{this.state.posts {this.state.posts
? this.state.posts.map((message: Status) => { ? this.state.posts.map(
return ( (message: Status) => {
<ListItem> return (
<ListItemAvatar> <ListItem>
<LinkableAvatar <ListItemAvatar>
to={`/profile/${message.account.id}`} <LinkableAvatar
alt={message.account.username} to={`/profile/${message.account.id}`}
src={message.account.avatar_static} alt={
> message
<PersonIcon /> .account
</LinkableAvatar> .username
</ListItemAvatar> }
<ListItemText src={
primary={ message
message.account.display_name || .account
"@" + message.account.acct .avatar_static
} }
secondary={this.removeHTMLContent(message.content)} >
/> <PersonIcon />
<ListItemSecondaryAction> </LinkableAvatar>
<Tooltip title="View conversation"> </ListItemAvatar>
<LinkableIconButton <ListItemText
to={`/conversation/${message.id}`} primary={
> message.account
<ForumIcon /> .display_name ||
</LinkableIconButton> "@" +
</Tooltip> message
</ListItemSecondaryAction> .account
</ListItem> .acct
); }
}) secondary={this.removeHTMLContent(
: null} message.content
</List> )}
</Paper> />
<br /> <ListItemSecondaryAction>
</div> <Tooltip title="View conversation">
) : null} <LinkableIconButton
{this.state.viewIsLoading ? ( to={`/conversation/${message.id}`}
<div style={{ textAlign: "center" }}> >
<CircularProgress className={classes.progress} color="primary" /> <ForumIcon />
</div> </LinkableIconButton>
) : null} </Tooltip>
</div> </ListItemSecondaryAction>
); </ListItem>
} );
}
)
: null}
</List>
</Paper>
<br />
</div>
) : null}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : null}
</div>
);
}
} }
export default withStyles(styles)(MessagesPage); export default withStyles(styles)(MessagesPage);

View File

@ -4,25 +4,29 @@ import { styles } from "./PageLayout.styles";
import { LinkableButton } from "../interfaces/overrides"; import { LinkableButton } from "../interfaces/overrides";
class Missingno extends Component<any, any> { class Missingno extends Component<any, any> {
render() { render() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.pageLayoutConstraints}> <div className={classes.pageLayoutConstraints}>
<div> <div>
<Typography variant="h4" component="h1"> <Typography variant="h4" component="h1">
<b>Uh oh!</b> <b>Uh oh!</b>
</Typography> </Typography>
<Typography variant="h6" component="p"> <Typography variant="h6" component="p">
The part of Hyperspace you're looking for isn't here. The part of Hyperspace you're looking for isn't here.
</Typography> </Typography>
<br /> <br />
<LinkableButton to="/home" color="primary" variant="contained"> <LinkableButton
Go back to home timeline to="/home"
</LinkableButton> color="primary"
</div> variant="contained"
</div> >
); Go back to home timeline
} </LinkableButton>
</div>
</div>
);
}
} }
export default withStyles(styles)(Missingno); export default withStyles(styles)(Missingno);

View File

@ -1,23 +1,23 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
ListSubheader, ListSubheader,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemAvatar, ListItemAvatar,
Paper, Paper,
IconButton, IconButton,
withStyles, withStyles,
Typography, Typography,
CircularProgress, CircularProgress,
Button, Button,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogActions, DialogActions,
Tooltip Tooltip
} from "@material-ui/core"; } from "@material-ui/core";
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd"; import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
@ -33,344 +33,380 @@ import { Account } from "../types/Account";
import { withSnackbar } from "notistack"; import { withSnackbar } from "notistack";
interface INotificationsPageState { interface INotificationsPageState {
notifications?: [Notification]; notifications?: [Notification];
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: string; viewDidErrorCode?: string;
deleteDialogOpen: boolean; deleteDialogOpen: boolean;
} }
class NotificationsPage extends Component<any, INotificationsPageState> { class NotificationsPage extends Component<any, INotificationsPageState> {
client: Mastodon; client: Mastodon;
streamListener: any; streamListener: any;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1" localStorage.getItem("baseurl") + "/api/v1"
);
this.state = {
viewIsLoading: true,
deleteDialogOpen: false
};
}
componentWillMount() {
this.client
.get("/notifications")
.then((resp: any) => {
let notifications: [Notification] = resp.data;
this.setState({
notifications,
viewIsLoading: false,
viewDidLoad: true
});
})
.catch((err: Error) => {
this.setState({
viewDidLoad: true,
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
});
}
componentDidMount() {
this.streamNotifications();
}
streamNotifications() {
this.streamListener = this.client.stream("/streaming/user");
this.streamListener.on("notification", (notif: Notification) => {
let notifications = this.state.notifications;
if (notifications) {
notifications.unshift(notif);
this.setState({ notifications });
}
});
}
toggleDeleteDialog() {
this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
}
removeHTMLContent(text: string) {
const div = document.createElement("div");
div.innerHTML = text;
let innerContent = div.textContent || div.innerText || "";
if (innerContent.length > 65)
innerContent = innerContent.slice(0, 65) + "...";
return innerContent;
}
removeNotification(id: string) {
this.client
.post("/notifications/dismiss", { id: id })
.then((resp: any) => {
let notifications = this.state.notifications;
if (notifications !== undefined && notifications.length > 0) {
notifications.forEach((notification: Notification) => {
if (notifications !== undefined && notification.id === id) {
notifications.splice(notifications.indexOf(notification), 1);
}
});
}
this.setState({ notifications });
this.props.enqueueSnackbar("Notification deleted.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't delete notification: " + err.name,
{
variant: "error"
}
); );
});
}
removeAllNotifications() { this.state = {
this.client viewIsLoading: true,
.post("/notifications/clear") deleteDialogOpen: false
.then((resp: any) => { };
this.setState({ notifications: undefined });
this.props.enqueueSnackbar("All notifications deleted.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't delete notifications: " + err.name,
{
variant: "error"
}
);
});
}
createNotification(notif: Notification) {
const { classes } = this.props;
let primary = "";
let secondary = "";
switch (notif.type) {
case "follow":
primary = `${notif.account.display_name ||
notif.account.username} is now following you!`;
break;
case "mention":
primary = `${notif.account.display_name ||
notif.account.username} mentioned you in a post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
case "reblog":
primary = `${notif.account.display_name ||
notif.account.username} reblogged your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
case "favourite":
primary = `${notif.account.display_name ||
notif.account.username} favorited your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
default:
if (notif.status && notif.status.poll) {
primary = "A poll you voted in or created has ended.";
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
} else {
primary = "A magical thing happened!";
}
break;
} }
return (
<ListItem key={notif.id}>
<ListItemAvatar>
<LinkableAvatar
alt={notif.account.username}
src={notif.account.avatar_static}
to={`/profile/${notif.account.id}`}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary={primary}
secondary={
<span>
<Typography color="textSecondary" className={classes.mobileOnly}>
{secondary.slice(0, 35) + "..."}
</Typography>
<Typography color="textSecondary" className={classes.desktopOnly}>
{secondary}
</Typography>
</span>
}
/>
<ListItemSecondaryAction>
{notif.type === "follow" ? (
<span>
<Tooltip title="View profile">
<LinkableIconButton to={`/profile/${notif.account.id}`}>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Follow account">
<IconButton onClick={() => this.followMember(notif.account)}>
<PersonAddIcon />
</IconButton>
</Tooltip>
</span>
) : notif.status ? (
<span>
<Tooltip title="View conversation">
<LinkableIconButton to={`/conversation/${notif.status.id}`}>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
{notif.type === "mention" ? (
<Tooltip title="Reply">
<LinkableIconButton
to={`/compose?reply=${
notif.status.reblog
? notif.status.reblog.id
: notif.status.id
}&visibility=${notif.status.visibility}&acct=${
notif.status.reblog
? notif.status.reblog.account.acct
: notif.status.account.acct
}`}
>
<ReplyIcon />
</LinkableIconButton>
</Tooltip>
) : null}
</span>
) : null}
<Tooltip title="Remove notification">
<IconButton onClick={() => this.removeNotification(notif.id)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
followMember(acct: Account) { componentWillMount() {
this.client this.client
.post(`/accounts/${acct.id}/follow`) .get("/notifications")
.then((resp: any) => { .then((resp: any) => {
this.props.enqueueSnackbar("You are now following this account."); let notifications: [Notification] = resp.data;
}) this.setState({
.catch((err: Error) => { notifications,
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { viewIsLoading: false,
variant: "error" viewDidLoad: true
});
})
.catch((err: Error) => {
this.setState({
viewDidLoad: true,
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
});
}
componentDidMount() {
this.streamNotifications();
}
streamNotifications() {
this.streamListener = this.client.stream("/streaming/user");
this.streamListener.on("notification", (notif: Notification) => {
let notifications = this.state.notifications;
if (notifications) {
notifications.unshift(notif);
this.setState({ notifications });
}
}); });
console.error(err.message); }
});
}
render() { toggleDeleteDialog() {
const { classes } = this.props; this.setState({ deleteDialogOpen: !this.state.deleteDialogOpen });
return ( }
<div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? ( removeHTMLContent(text: string) {
this.state.notifications && this.state.notifications.length > 0 ? ( const div = document.createElement("div");
<div> div.innerHTML = text;
<ListSubheader>Recent notifications</ListSubheader> let innerContent = div.textContent || div.innerText || "";
<Button if (innerContent.length > 65)
className={classes.clearAllButton} innerContent = innerContent.slice(0, 65) + "...";
variant="text" return innerContent;
onClick={() => this.toggleDeleteDialog()} }
>
{" "} removeNotification(id: string) {
Clear All this.client
</Button> .post("/notifications/dismiss", { id: id })
<Paper className={classes.pageListConstraints}> .then((resp: any) => {
<List> let notifications = this.state.notifications;
{this.state.notifications.map( if (notifications !== undefined && notifications.length > 0) {
(notification: Notification) => { notifications.forEach((notification: Notification) => {
return this.createNotification(notification); if (
notifications !== undefined &&
notification.id === id
) {
notifications.splice(
notifications.indexOf(notification),
1
);
}
});
}
this.setState({ notifications });
this.props.enqueueSnackbar("Notification deleted.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't delete notification: " + err.name,
{
variant: "error"
} }
)} );
</List> });
</Paper> }
</div>
) : (
<div className={classes.pageLayoutEmptyTextConstraints}>
<Typography variant="h4">All clear!</Typography>
<Typography paragraph>
It looks like you have no notifications. Why not get the
conversation going with a new post?
</Typography>
</div>
)
) : null}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress className={classes.progress} color="primary" />
</div>
) : (
<span />
)}
<Dialog removeAllNotifications() {
open={this.state.deleteDialogOpen} this.client
onClose={() => this.toggleDeleteDialog()} .post("/notifications/clear")
> .then((resp: any) => {
<DialogTitle id="alert-dialog-title"> this.setState({ notifications: undefined });
Delete all notifications? this.props.enqueueSnackbar("All notifications deleted.");
</DialogTitle> })
<DialogContent> .catch((err: Error) => {
<DialogContentText id="alert-dialog-description"> this.props.enqueueSnackbar(
Are you sure you want to delete all notifications? This action "Couldn't delete notifications: " + err.name,
cannot be undone. {
</DialogContentText> variant: "error"
</DialogContent> }
<DialogActions> );
<Button });
onClick={() => this.toggleDeleteDialog()} }
color="primary"
autoFocus createNotification(notif: Notification) {
> const { classes } = this.props;
Cancel let primary = "";
</Button> let secondary = "";
<Button switch (notif.type) {
onClick={() => { case "follow":
this.removeAllNotifications(); primary = `${notif.account.display_name ||
this.toggleDeleteDialog(); notif.account.username} is now following you!`;
}} break;
color="primary" case "mention":
> primary = `${notif.account.display_name ||
Delete notif.account.username} mentioned you in a post.`;
</Button> secondary = this.removeHTMLContent(
</DialogActions> notif.status ? notif.status.content : ""
</Dialog> );
</div> break;
); case "reblog":
} primary = `${notif.account.display_name ||
notif.account.username} reblogged your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
case "favourite":
primary = `${notif.account.display_name ||
notif.account.username} favorited your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
default:
if (notif.status && notif.status.poll) {
primary = "A poll you voted in or created has ended.";
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
} else {
primary = "A magical thing happened!";
}
break;
}
return (
<ListItem key={notif.id}>
<ListItemAvatar>
<LinkableAvatar
alt={notif.account.username}
src={notif.account.avatar_static}
to={`/profile/${notif.account.id}`}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary={primary}
secondary={
<span>
<Typography
color="textSecondary"
className={classes.mobileOnly}
>
{secondary.slice(0, 35) + "..."}
</Typography>
<Typography
color="textSecondary"
className={classes.desktopOnly}
>
{secondary}
</Typography>
</span>
}
/>
<ListItemSecondaryAction>
{notif.type === "follow" ? (
<span>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${notif.account.id}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Follow account">
<IconButton
onClick={() =>
this.followMember(notif.account)
}
>
<PersonAddIcon />
</IconButton>
</Tooltip>
</span>
) : notif.status ? (
<span>
<Tooltip title="View conversation">
<LinkableIconButton
to={`/conversation/${notif.status.id}`}
>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
{notif.type === "mention" ? (
<Tooltip title="Reply">
<LinkableIconButton
to={`/compose?reply=${
notif.status.reblog
? notif.status.reblog.id
: notif.status.id
}&visibility=${
notif.status.visibility
}&acct=${
notif.status.reblog
? notif.status.reblog.account
.acct
: notif.status.account.acct
}`}
>
<ReplyIcon />
</LinkableIconButton>
</Tooltip>
) : null}
</span>
) : null}
<Tooltip title="Remove notification">
<IconButton
onClick={() => this.removeNotification(notif.id)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
followMember(acct: Account) {
this.client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar(
"You are now following this account."
);
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't follow account: " + err.name,
{ variant: "error" }
);
console.error(err.message);
});
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? (
this.state.notifications &&
this.state.notifications.length > 0 ? (
<div>
<ListSubheader>Recent notifications</ListSubheader>
<Button
className={classes.clearAllButton}
variant="text"
onClick={() => this.toggleDeleteDialog()}
>
{" "}
Clear All
</Button>
<Paper className={classes.pageListConstraints}>
<List>
{this.state.notifications.map(
(notification: Notification) => {
return this.createNotification(
notification
);
}
)}
</List>
</Paper>
</div>
) : (
<div className={classes.pageLayoutEmptyTextConstraints}>
<Typography variant="h4">All clear!</Typography>
<Typography paragraph>
It looks like you have no notifications. Why not
get the conversation going with a new post?
</Typography>
</div>
)
) : null}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
<Dialog
open={this.state.deleteDialogOpen}
onClose={() => this.toggleDeleteDialog()}
>
<DialogTitle id="alert-dialog-title">
Delete all notifications?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to delete all notifications?
This action cannot be undone.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.toggleDeleteDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.removeAllNotifications();
this.toggleDeleteDialog();
}}
color="primary"
>
Delete
</Button>
</DialogActions>
</Dialog>
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(NotificationsPage)); export default withStyles(styles)(withSnackbar(NotificationsPage));

View File

@ -3,281 +3,282 @@ import { isDarwinApp } from "../utilities/desktop";
import { isAppbarExpanded } from "../utilities/appbar"; import { isAppbarExpanded } from "../utilities/appbar";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
width: "100%", width: "100%",
display: "flex", display: "flex",
height: "100%" height: "100%"
}, },
pageLayoutConstraints: { pageLayoutConstraints: {
marginTop: 72, marginTop: 72,
flexGrow: 1, flexGrow: 1,
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit, paddingLeft: theme.spacing.unit,
paddingRight: theme.spacing.unit, paddingRight: theme.spacing.unit,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: 250, marginLeft: 250,
marginTop: 88, marginTop: 88,
paddingLeft: theme.spacing.unit * 24, paddingLeft: theme.spacing.unit * 24,
paddingRight: theme.spacing.unit * 24 paddingRight: theme.spacing.unit * 24
}, },
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
minHeight: isDarwinApp() ? "100vh" : "auto" minHeight: isDarwinApp() ? "100vh" : "auto"
}, },
pageLayoutMaxConstraints: { pageLayoutMaxConstraints: {
marginTop: 72, marginTop: 72,
flexGrow: 1, flexGrow: 1,
paddingTop: theme.spacing.unit * 2, paddingTop: theme.spacing.unit * 2,
padding: theme.spacing.unit, padding: theme.spacing.unit,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: 250, marginLeft: 250,
marginTop: 88, marginTop: 88,
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit * 16, paddingLeft: theme.spacing.unit * 16,
paddingRight: theme.spacing.unit * 16 paddingRight: theme.spacing.unit * 16
}, },
[theme.breakpoints.up("lg")]: { [theme.breakpoints.up("lg")]: {
marginLeft: 250, marginLeft: 250,
marginTop: 88, marginTop: 88,
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit * 32, paddingLeft: theme.spacing.unit * 32,
paddingRight: theme.spacing.unit * 32 paddingRight: theme.spacing.unit * 32
}, },
[theme.breakpoints.up("xl")]: { [theme.breakpoints.up("xl")]: {
marginLeft: 250, marginLeft: 250,
marginTop: 88, marginTop: 88,
padding: theme.spacing.unit * 3, padding: theme.spacing.unit * 3,
paddingLeft: theme.spacing.unit * 40, paddingLeft: theme.spacing.unit * 40,
paddingRight: theme.spacing.unit * 40 paddingRight: theme.spacing.unit * 40
}, },
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
minHeight: isDarwinApp() ? "100vh" : "auto" minHeight: isDarwinApp() ? "100vh" : "auto"
}, },
pageLayoutMinimalConstraints: { pageLayoutMinimalConstraints: {
flexGrow: 1, flexGrow: 1,
[theme.breakpoints.up("md")]: { [theme.breakpoints.up("md")]: {
marginLeft: 250 marginLeft: 250
}, },
backgroundColor: theme.palette.background.default, backgroundColor: theme.palette.background.default,
minHeight: isDarwinApp() ? "100vh" : "auto" minHeight: isDarwinApp() ? "100vh" : "auto"
}, },
pageLayoutEmptyTextConstraints: { pageLayoutEmptyTextConstraints: {
paddingLeft: theme.spacing.unit * 2, paddingLeft: theme.spacing.unit * 2,
paddingRight: theme.spacing.unit * 2 paddingRight: theme.spacing.unit * 2
}, },
pageHeroBackground: { pageHeroBackground: {
position: "relative", position: "relative",
height: "intrinsic", height: "intrinsic",
backgroundColor: theme.palette.primary.dark, backgroundColor: theme.palette.primary.dark,
width: "100%", width: "100%",
color: theme.palette.common.white, color: theme.palette.common.white,
zIndex: 1, zIndex: 1,
top: isAppbarExpanded() ? 80 : 64 top: isAppbarExpanded() ? 80 : 64
}, },
pageHeroBackgroundImage: { pageHeroBackgroundImage: {
position: "absolute", position: "absolute",
bottom: 0, bottom: 0,
left: 0, left: 0,
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: "cover", backgroundSize: "cover",
height: "100%", height: "100%",
width: "100%", width: "100%",
opacity: 0.35, opacity: 0.35,
zIndex: -1, zIndex: -1,
filter: "blur(2px)" filter: "blur(2px)"
}, },
pageHeroContent: { pageHeroContent: {
padding: 16, padding: 16,
paddingTop: 8, paddingTop: 8,
textAlign: "center", width: "100%",
width: "100%", height: "100%",
height: "100%", [theme.breakpoints.up("md")]: {
[theme.breakpoints.up("md")]: { paddingLeft: "5%",
paddingLeft: "5%", paddingRight: "5%"
paddingRight: "5%" },
}, position: "relative",
position: "relative", zIndex: 1
zIndex: 1 },
}, pageHeroToolbar: {
pageHeroToolbar: { position: "absolute",
position: "absolute", right: theme.spacing.unit * 2,
right: theme.spacing.unit * 2, marginTop: -16
marginTop: -16 },
}, pageListConstraints: {
pageListConstraints: { paddingLeft: theme.spacing.unit,
paddingLeft: theme.spacing.unit, paddingRight: theme.spacing.unit,
paddingRight: theme.spacing.unit, [theme.breakpoints.up("sm")]: {
[theme.breakpoints.up("sm")]: { paddingLeft: theme.spacing.unit * 2,
paddingLeft: theme.spacing.unit * 2, paddingRight: theme.spacing.unit * 2
paddingRight: theme.spacing.unit * 2 }
} //backgroundColor: theme.palette.background.default
//backgroundColor: theme.palette.background.default },
}, profileToolbar: {
profileToolbar: { zIndex: 2,
zIndex: 2, paddingTop: 8
paddingTop: 8 },
}, profileContent: {
profileContent: { padding: 16,
padding: 16, [theme.breakpoints.up("md")]: {
[theme.breakpoints.up("md")]: { paddingLeft: "5%",
paddingLeft: "5%", paddingRight: "5%",
paddingRight: "5%", paddingBottom: 48,
paddingBottom: 48, paddingTop: 24
paddingTop: 24 },
}, width: "100%",
width: "100%", height: "100%",
height: "100%", position: "relative",
position: "relative", zIndex: 1,
zIndex: 1, display: "flex",
display: "flex", paddingBottom: 24,
paddingBottom: 24, paddingTop: 24
paddingTop: 24 },
}, profileAvatar: {
profileAvatar: { width: 64,
width: 64, height: 64,
height: 64, [theme.breakpoints.up("md")]: {
[theme.breakpoints.up("md")]: { width: 128,
width: 128, height: 128
height: 128 },
}, backgroundColor: theme.palette.primary.main
backgroundColor: theme.palette.primary.main },
}, profileUserBox: {
profileUserBox: { paddingLeft: theme.spacing.unit * 2
paddingLeft: theme.spacing.unit * 2 },
}, pageProfileAvatar: {
pageProfileAvatar: { width: 128,
width: 128, height: 128,
height: 128, marginLeft: "auto",
marginLeft: "auto", marginRight: "auto",
marginRight: "auto", marginBottom: theme.spacing.unit,
marginBottom: theme.spacing.unit, backgroundColor: theme.palette.primary.main
backgroundColor: theme.palette.primary.main },
}, pageProfileNameEmoji: {
pageProfileNameEmoji: { height: theme.typography.h4.fontSize,
height: theme.typography.h4.fontSize, fontWeight: theme.typography.fontWeightMedium
fontWeight: theme.typography.fontWeightMedium },
}, pageProfileStatsDiv: {
pageProfileStatsDiv: { display: "inline-flex",
display: "inline-flex", marginTop: theme.spacing.unit * 2,
marginTop: theme.spacing.unit * 2, marginBottom: theme.spacing.unit * 2
marginBottom: theme.spacing.unit * 2 },
}, pageProfileStat: {
pageProfileStat: { marginLeft: theme.spacing.unit,
marginLeft: theme.spacing.unit, marginRight: theme.spacing.unit
marginRight: theme.spacing.unit },
}, pageProfileFollowButton: {
pageProfileFollowButton: { marginTop: theme.spacing.unit,
marginTop: theme.spacing.unit, marginLeft: theme.spacing.unit,
marginLeft: theme.spacing.unit, marginRight: theme.spacing.unit,
marginRight: theme.spacing.unit, zIndex: 3
zIndex: 3 },
}, pageContentLayoutConstraints: {
pageContentLayoutConstraints: { paddingLeft: theme.spacing.unit,
paddingLeft: theme.spacing.unit, paddingRight: theme.spacing.unit,
paddingRight: theme.spacing.unit, paddingTop: theme.spacing.unit * 12,
paddingTop: theme.spacing.unit * 12, paddingBottom: theme.spacing.unit * 2,
paddingBottom: theme.spacing.unit * 2, [theme.breakpoints.up("lg")]: {
[theme.breakpoints.up("lg")]: { paddingLeft: theme.spacing.unit * 32,
paddingLeft: theme.spacing.unit * 32, paddingRight: theme.spacing.unit * 32
paddingRight: theme.spacing.unit * 32 }
} //backgroundColor: theme.palette.background.default,
//backgroundColor: theme.palette.background.default, },
}, errorCard: {
errorCard: { padding: theme.spacing.unit * 4,
padding: theme.spacing.unit * 4, backgroundColor: theme.palette.error.main
backgroundColor: theme.palette.error.main },
}, pageTopChipContainer: {
pageTopChipContainer: { zIndex: 24,
zIndex: 24, position: "fixed",
position: "fixed", width: "100%"
width: "100%" },
}, pageTopChips: {
pageTopChips: { textAlign: "center",
textAlign: "center", [theme.breakpoints.up("md")]: {
[theme.breakpoints.up("md")]: { marginRight: "55%"
marginRight: "55%" },
}, [theme.breakpoints.up("xl")]: {
[theme.breakpoints.up("xl")]: { marginRight: "50%"
marginRight: "50%" }
} },
}, pageTopChip: {
pageTopChip: { boxShadow: theme.shadows[10]
boxShadow: theme.shadows[10] },
}, clearAllButton: {
clearAllButton: { zIndex: 3,
zIndex: 3, position: "absolute",
position: "absolute", right: 24,
right: 24, top: 100,
top: 100, [theme.breakpoints.up("md")]: {
[theme.breakpoints.up("md")]: { top: 116,
top: 116, right: theme.spacing.unit * 24
right: theme.spacing.unit * 24 }
} },
}, mobileOnly: {
mobileOnly: { [theme.breakpoints.up("sm")]: {
[theme.breakpoints.up("sm")]: { display: "none"
display: "none" }
} },
}, desktopOnly: {
desktopOnly: { display: "none",
display: "none", [theme.breakpoints.up("sm")]: {
[theme.breakpoints.up("sm")]: { display: "block"
display: "block" }
} },
}, pageLayoutFooter: {
pageLayoutFooter: { "& a": {
"& a": { color: theme.palette.primary.light
color: theme.palette.primary.light }
} },
}, youHeadingAvatar: {
youHeadingAvatar: { height: 88,
height: 88, width: 88
width: 88 },
}, youPaper: {
youPaper: { padding: theme.spacing.unit * 2
padding: theme.spacing.unit * 2 },
}, youGrid: {
youGrid: { textAlign: "center",
textAlign: "center", "& *": {
"& *": { marginLeft: "auto",
marginLeft: "auto", marginRight: "auto"
marginRight: "auto" }
} },
}, youGridAvatar: {
youGridAvatar: { height: 128,
height: 128, width: 128
width: 128 },
}, youGridImage: {
youGridImage: { width: "auto",
width: "auto", height: 128
height: 128 },
}, instanceHeaderPaper: {
instanceHeaderPaper: { height: 150,
height: 200, backgroundPosition: "center",
backgroundPosition: "center", backgroundRepeat: "no-repeat",
backgroundRepeat: "no-repeat", backgroundSize: "cover",
backgroundSize: "cover", position: "relative",
position: "relative", backgroundColor: theme.palette.primary.dark,
backgroundColor: theme.palette.primary.dark, borderTopLeftRadius: theme.shape.borderRadius,
borderTopLeftRadius: theme.shape.borderRadius, borderTopRightRadius: theme.shape.borderRadius
borderTopRightRadius: theme.shape.borderRadius },
}, instanceHeaderText: {
instanceHeaderText: { position: "absolute",
position: "absolute", bottom: theme.spacing.unit,
bottom: theme.spacing.unit, left: theme.spacing.unit * 2,
left: theme.spacing.unit * 2, "& *": {
color: theme.palette.common.white, color: theme.palette.common.white,
textShadow: `0 0 4px ${theme.palette.grey[700]}`, textShadow: `0 0 4px ${theme.palette.grey[700]}`,
fontWeight: 600 fontWeight: 600
}, }
instanceToolbar: { },
position: "absolute", instanceToolbar: {
top: theme.spacing.unit, position: "absolute",
right: theme.spacing.unit, top: theme.spacing.unit,
color: theme.palette.common.white right: theme.spacing.unit,
}, color: theme.palette.common.white
pageGrow: { },
flexGrow: 1 pageGrow: {
} flexGrow: 1
}); }
});

View File

@ -1,20 +1,20 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
Typography, Typography,
Avatar, Avatar,
Divider, Divider,
Button, Button,
CircularProgress, CircularProgress,
Paper, Paper,
Tooltip, Tooltip,
Dialog, Dialog,
DialogTitle, DialogTitle,
DialogContent, DialogContent,
DialogContentText, DialogContentText,
DialogActions, DialogActions,
Toolbar, Toolbar,
IconButton IconButton
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Mastodon from "megalodon"; import Mastodon from "megalodon";
@ -36,469 +36,518 @@ import AccountHeartIcon from "mdi-material-ui/AccountHeart";
import OpenInNewIcon from "@material-ui/icons/OpenInNew"; import OpenInNewIcon from "@material-ui/icons/OpenInNew";
interface IProfilePageState { interface IProfilePageState {
account?: Account; account?: Account;
relationship?: Relationship; relationship?: Relationship;
posts?: [Status]; posts?: [Status];
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: string; viewDidErrorCode?: string;
blockDialogOpen: boolean; blockDialogOpen: boolean;
} }
class ProfilePage extends Component<any, IProfilePageState> { class ProfilePage extends Component<any, IProfilePageState> {
client: Mastodon; client: Mastodon;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1" localStorage.getItem("baseurl") + "/api/v1"
); );
this.state = { this.state = {
viewIsLoading: true, viewIsLoading: true,
blockDialogOpen: false blockDialogOpen: false
}; };
}
toggleBlockDialog() {
if (this.state.relationship && !this.state.relationship.blocking)
this.setState({ blockDialogOpen: !this.state.blockDialogOpen });
else this.toggleBlock();
}
getAccountData(id: string) {
this.client
.get(`/accounts/${id}`)
.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.getRelationships();
this.client
.get(`/accounts/${id}/statuses`)
.then((resp: any) => {
this.setState({
posts: resp.data,
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
});
}
componentWillReceiveProps(props: any) {
this.getAccountData(props.match.params.profileId);
window.scrollTo(0, 0);
}
componentWillMount() {
const {
match: { params }
} = this.props;
this.getAccountData(params.profileId);
}
isItMe(): boolean {
if (this.state.account) {
return (
this.state.account.id ===
JSON.parse(localStorage.getItem("account") as string).id
);
} else {
return false;
} }
}
getRelationships() { toggleBlockDialog() {
this.client if (this.state.relationship && !this.state.relationship.blocking)
.get("/accounts/relationships", { id: this.props.match.params.profileId }) this.setState({ blockDialogOpen: !this.state.blockDialogOpen });
.then((resp: any) => { else this.toggleBlock();
let relationship: Relationship = resp.data[0]; }
this.setState({ relationship });
})
.catch((error: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: error.message
});
});
}
loadMoreTimelinePieces() { getAccountData(id: string) {
const { this.client
match: { params } .get(`/accounts/${id}`)
} = this.props; .then((resp: any) => {
this.setState({ viewDidLoad: false, viewIsLoading: true }); let profile: Account = resp.data;
if (this.state.posts && this.state.posts.length > 0) {
this.client const div = document.createElement("div");
.get(`/accounts/${params.profileId}/statuses`, { div.innerHTML = profile.note;
max_id: this.state.posts[this.state.posts.length - 1].id, profile.note = div.textContent || div.innerText || "";
limit: 20
}) this.setState({
.then((resp: any) => { account: profile
let newPosts: [Status] = resp.data; });
let posts = this.state.posts as [Status]; })
if (newPosts.length <= 0) { .catch((error: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: error.message
});
});
this.getRelationships();
this.client
.get(`/accounts/${id}/statuses`)
.then((resp: any) => {
this.setState({
posts: resp.data,
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
});
}
componentWillReceiveProps(props: any) {
this.getAccountData(props.match.params.profileId);
window.scrollTo(0, 0);
}
componentWillMount() {
const {
match: { params }
} = this.props;
this.getAccountData(params.profileId);
}
isItMe(): boolean {
if (this.state.account) {
return (
this.state.account.id ===
JSON.parse(localStorage.getItem("account") as string).id
);
} else {
return false;
}
}
getRelationships() {
this.client
.get("/accounts/relationships", {
id: this.props.match.params.profileId
})
.then((resp: any) => {
let relationship: Relationship = resp.data[0];
this.setState({ relationship });
})
.catch((error: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: error.message
});
});
}
loadMoreTimelinePieces() {
const {
match: { params }
} = this.props;
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts && this.state.posts.length > 0) {
this.client
.get(`/accounts/${params.profileId}/statuses`, {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
if (newPosts.length <= 0) {
this.props.enqueueSnackbar("Reached end of posts", {
variant: "error"
});
} else {
newPosts.forEach((post: Status) => {
posts.push(post);
});
}
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
});
} else {
this.props.enqueueSnackbar("Reached end of posts", { this.props.enqueueSnackbar("Reached end of posts", {
variant: "error" variant: "error"
}); });
} else { this.setState({
newPosts.forEach((post: Status) => { viewIsLoading: false,
posts.push(post); viewDidLoad: true
}); });
} }
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
});
} else {
this.props.enqueueSnackbar("Reached end of posts", { variant: "error" });
this.setState({
viewIsLoading: false,
viewDidLoad: true
});
} }
}
toggleFollow() { toggleFollow() {
if (this.state.relationship) { if (this.state.relationship) {
if (this.state.relationship.following) { if (this.state.relationship.following) {
this.client this.client
.post( .post(
`/accounts/${ `/accounts/${
this.state.account this.state.account
? this.state.account.id ? this.state.account.id
: this.props.match.params.profileId : this.props.match.params.profileId
}/unfollow` }/unfollow`
) )
.then((resp: any) => { .then((resp: any) => {
let relationship: Relationship = resp.data; let relationship: Relationship = resp.data;
this.setState({ relationship }); this.setState({ relationship });
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"You are no longer following this account." "You are no longer following this account."
); );
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"Couldn't unfollow account: " + err.name, "Couldn't unfollow account: " + err.name,
{ variant: "error" } { variant: "error" }
); );
console.error(err.message); console.error(err.message);
}); });
} else { } else {
this.client this.client
.post( .post(
`/accounts/${ `/accounts/${
this.state.account this.state.account
? this.state.account.id ? this.state.account.id
: this.props.match.params.profileId : this.props.match.params.profileId
}/follow` }/follow`
) )
.then((resp: any) => { .then((resp: any) => {
let relationship: Relationship = resp.data; let relationship: Relationship = resp.data;
this.setState({ relationship }); this.setState({ relationship });
this.props.enqueueSnackbar("You are now following this account."); this.props.enqueueSnackbar(
}) "You are now following this account."
.catch((err: Error) => { );
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { })
variant: "error" .catch((err: Error) => {
}); this.props.enqueueSnackbar(
console.error(err.message); "Couldn't follow account: " + err.name,
}); { variant: "error" }
} );
console.error(err.message);
});
}
}
} }
}
toggleBlock() { toggleBlock() {
if (this.state.relationship) { if (this.state.relationship) {
if (this.state.relationship.blocking) { if (this.state.relationship.blocking) {
this.client this.client
.post( .post(
`/accounts/${ `/accounts/${
this.state.account this.state.account
? this.state.account.id ? this.state.account.id
: this.props.match.params.profileId : this.props.match.params.profileId
}/unblock` }/unblock`
) )
.then((resp: any) => { .then((resp: any) => {
let relationship: Relationship = resp.data; let relationship: Relationship = resp.data;
this.setState({ relationship }); this.setState({ relationship });
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"You are no longer blocking this account." "You are no longer blocking this account."
); );
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"Couldn't unblock account: " + err.name, "Couldn't unblock account: " + err.name,
{ variant: "error" } { variant: "error" }
); );
console.error(err.message); console.error(err.message);
}); });
} else { } else {
this.client this.client
.post( .post(
`/accounts/${ `/accounts/${
this.state.account this.state.account
? this.state.account.id ? this.state.account.id
: this.props.match.params.profileId : this.props.match.params.profileId
}/block` }/block`
) )
.then((resp: any) => { .then((resp: any) => {
let relationship: Relationship = resp.data; let relationship: Relationship = resp.data;
this.setState({ relationship }); this.setState({ relationship });
this.props.enqueueSnackbar("You are now blocking this account."); this.props.enqueueSnackbar(
}) "You are now blocking this account."
.catch((err: Error) => { );
this.props.enqueueSnackbar("Couldn't block account: " + err.name, { })
variant: "error" .catch((err: Error) => {
}); this.props.enqueueSnackbar(
console.error(err.message); "Couldn't block account: " + err.name,
}); { variant: "error" }
} );
console.error(err.message);
});
}
}
} }
}
render() { render() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.pageLayoutMinimalConstraints}> <div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}> <div className={classes.pageHeroBackground}>
<div <div
className={classes.pageHeroBackgroundImage} className={classes.pageHeroBackgroundImage}
style={{ style={{
backgroundImage: this.state.account backgroundImage: this.state.account
? `url("${this.state.account.header}")` ? `url("${this.state.account.header}")`
: `url("")` : `url("")`
}} }}
/> />
<Toolbar className={classes.profileToolbar}> <Toolbar className={classes.profileToolbar}>
<div className={classes.pageGrow} /> <div className={classes.pageGrow} />
<Tooltip <Tooltip
title={ title={
this.isItMe() this.isItMe()
? "You can't follow yourself." ? "You can't follow yourself."
: this.state.relationship && this.state.relationship.following : this.state.relationship &&
? "Unfollow" this.state.relationship.following
: "Follow" ? "Unfollow"
} : "Follow"
> }
<IconButton >
color={"inherit"} <IconButton
disabled={this.isItMe()} color={"inherit"}
onClick={() => this.toggleFollow()} disabled={this.isItMe()}
> onClick={() => this.toggleFollow()}
{this.isItMe() ? ( >
<PersonAddDisabledIcon /> {this.isItMe() ? (
) : this.state.relationship && <PersonAddDisabledIcon />
this.state.relationship.following ? ( ) : this.state.relationship &&
<AccountMinusIcon /> this.state.relationship.following ? (
) : ( <AccountMinusIcon />
<PersonAddIcon /> ) : (
)} <PersonAddIcon />
</IconButton> )}
</Tooltip> </IconButton>
<Tooltip title={"Send a message or post"}> </Tooltip>
<LinkableIconButton <Tooltip title={"Send a message or post"}>
to={`/compose?acct=${ <LinkableIconButton
this.state.account ? this.state.account.acct : "" to={`/compose?acct=${
}`} this.state.account
color={"inherit"} ? this.state.account.acct
> : ""
<ChatIcon /> }`}
</LinkableIconButton> color={"inherit"}
</Tooltip> >
<Tooltip <ChatIcon />
title={ </LinkableIconButton>
this.state.relationship && this.state.relationship.blocking </Tooltip>
? "Unblock this account" <Tooltip
: "Block this account" title={
} this.state.relationship &&
> this.state.relationship.blocking
<IconButton ? "Unblock this account"
color={"inherit"} : "Block this account"
disabled={this.isItMe()} }
onClick={() => this.toggleBlockDialog()} >
> <IconButton
{this.state.relationship && this.state.relationship.blocking ? ( color={"inherit"}
<AccountHeartIcon /> disabled={this.isItMe()}
) : ( onClick={() => this.toggleBlockDialog()}
<AccountRemoveIcon /> >
)} {this.state.relationship &&
</IconButton> this.state.relationship.blocking ? (
</Tooltip> <AccountHeartIcon />
<Tooltip title="Open in web"> ) : (
<IconButton <AccountRemoveIcon />
href={this.state.account ? this.state.account.url : ""} )}
target="_blank" </IconButton>
rel={"nofollower noreferrer noopener"} </Tooltip>
color={"inherit"} <Tooltip title="Open in web">
> <IconButton
<OpenInNewIcon /> href={
</IconButton> this.state.account
</Tooltip> ? this.state.account.url
{this.isItMe() ? ( : ""
<Tooltip title="Edit profile"> }
<LinkableIconButton to="/you" color="inherit"> target="_blank"
<AccountEditIcon /> rel={"nofollower noreferrer noopener"}
</LinkableIconButton> color={"inherit"}
</Tooltip> >
) : null} <OpenInNewIcon />
</Toolbar> </IconButton>
<div className={classes.profileContent}> </Tooltip>
<Avatar {this.isItMe() ? (
className={classes.profileAvatar} <Tooltip title="Edit profile">
src={this.state.account ? this.state.account.avatar : ""} <LinkableIconButton to="/you" color="inherit">
/> <AccountEditIcon />
<div className={classes.profileUserBox}> </LinkableIconButton>
<Typography </Tooltip>
variant="h4" ) : null}
color="inherit" </Toolbar>
dangerouslySetInnerHTML={{ <div className={classes.profileContent}>
__html: this.state.account <Avatar
? this.state.account.display_name className={classes.profileAvatar}
? emojifyString( src={
this.state.account.display_name, this.state.account
this.state.account.emojis, ? this.state.account.avatar
classes.pageProfileNameEmoji : ""
) }
: this.state.account.username />
: "" <div className={classes.profileUserBox}>
}} <Typography
className={classes.pageProfileNameEmoji} variant="h4"
/> color="inherit"
<Typography variant="caption" color="inherit"> dangerouslySetInnerHTML={{
{this.state.account ? "@" + this.state.account.acct : ""} __html: this.state.account
</Typography> ? this.state.account.display_name
<Typography paragraph color="inherit"> ? emojifyString(
{this.state.account this.state.account
? this.state.account.note .display_name,
? this.state.account.note this.state.account.emojis,
: "No bio provided by user." classes.pageProfileNameEmoji
: "No bio available."} )
</Typography> : this.state.account.username
<Typography color={"inherit"}> : ""
{this.state.account ? this.state.account.followers_count : 0}{" "} }}
followers |{" "} className={classes.pageProfileNameEmoji}
{this.state.account ? this.state.account.following_count : 0}{" "} />
following |{" "} <Typography variant="caption" color="inherit">
{this.state.account ? this.state.account.statuses_count : 0}{" "} {this.state.account
posts ? "@" + this.state.account.acct
</Typography> : ""}
</div> </Typography>
</div> <Typography paragraph color="inherit">
</div> {this.state.account
<div className={classes.pageContentLayoutConstraints}> ? this.state.account.note
{this.state.viewDidError ? ( ? this.state.account.note
<Paper className={classes.errorCard}> : "No bio provided by user."
<Typography variant="h4">Bummer.</Typography> : "No bio available."}
<Typography variant="h6"> </Typography>
Something went wrong when loading this profile. <Typography color={"inherit"}>
</Typography> {this.state.account
<Typography> ? this.state.account.followers_count
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""} : 0}{" "}
</Typography> followers |{" "}
</Paper> {this.state.account
) : ( ? this.state.account.following_count
<span /> : 0}{" "}
)} following |{" "}
{this.state.posts ? ( {this.state.account
<div> ? this.state.account.statuses_count
{this.state.posts.map((post: Status) => { : 0}{" "}
return <Post key={post.id} post={post} client={this.client} />; posts
})} </Typography>
<br /> </div>
{this.state.viewDidLoad && !this.state.viewDidError ? ( </div>
<div </div>
style={{ textAlign: "center" }} <div className={classes.pageContentLayoutConstraints}>
onClick={() => this.loadMoreTimelinePieces()} {this.state.viewDidError ? (
> <Paper className={classes.errorCard}>
<Button variant="contained">Load more</Button> <Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this profile.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})}
<br />
{this.state.viewDidLoad &&
!this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() =>
this.loadMoreTimelinePieces()
}
>
<Button variant="contained">
Load more
</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
<Dialog
open={this.state.blockDialogOpen}
onClose={() => this.toggleBlockDialog()}
>
<DialogTitle id="alert-dialog-title">
Block this person?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to block this person? You
won't see their posts on your home feed, local
timeline, or public timeline.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.toggleBlockDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.toggleBlock();
this.toggleBlockDialog();
}}
color="primary"
>
Block
</Button>
</DialogActions>
</Dialog>
</div> </div>
) : null}
</div> </div>
) : ( );
<span /> }
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress className={classes.progress} color="primary" />
</div>
) : (
<span />
)}
<Dialog
open={this.state.blockDialogOpen}
onClose={() => this.toggleBlockDialog()}
>
<DialogTitle id="alert-dialog-title">
Block this person?
</DialogTitle>
<DialogContent>
<DialogContentText id="alert-dialog-description">
Are you sure you want to block this person? You won't see their
posts on your home feed, local timeline, or public timeline.
</DialogContentText>
</DialogContent>
<DialogActions>
<Button
onClick={() => this.toggleBlockDialog()}
color="primary"
autoFocus
>
Cancel
</Button>
<Button
onClick={() => {
this.toggleBlock();
this.toggleBlockDialog();
}}
color="primary"
>
Block
</Button>
</DialogActions>
</Dialog>
</div>
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(ProfilePage)); export default withStyles(styles)(withSnackbar(ProfilePage));

View File

@ -1,13 +1,13 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
CircularProgress, CircularProgress,
Typography, Typography,
Paper, Paper,
Button, Button,
Chip, Chip,
Avatar, Avatar,
Slide Slide
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Post from "../components/Post"; import Post from "../components/Post";
@ -17,209 +17,224 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward"; import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface IPublicPageState { interface IPublicPageState {
posts?: [Status]; posts?: [Status];
backlogPosts?: [Status] | null; backlogPosts?: [Status] | null;
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: any; viewDidErrorCode?: any;
} }
class PublicPage extends Component<any, IPublicPageState> { class PublicPage extends Component<any, IPublicPageState> {
client: Mastodon; client: Mastodon;
streamListener: StreamListener; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.state = { this.state = {
viewIsLoading: true, viewIsLoading: true,
backlogPosts: null backlogPosts: null
}; };
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
this.streamListener = this.client.stream("/streaming/public"); this.streamListener = this.client.stream("/streaming/public");
}
componentWillMount() {
this.streamListener.on("connect", () => {
this.client
.get("/timelines/public", { limit: 40 })
.then((resp: any) => {
let statuses: [Status] = resp.data;
this.setState({
posts: statuses,
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
})
.catch((resp: any) => {
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 queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
}
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() {
window.scrollTo(0, 0);
let posts = this.state.posts;
let backlog = this.state.backlogPosts;
if (posts && backlog && backlog.length > 0) {
let push = backlog.concat(posts);
this.setState({ posts: push as [Status], backlogPosts: null });
} }
}
loadMoreTimelinePieces() { componentWillMount() {
this.setState({ viewDidLoad: false, viewIsLoading: true }); this.streamListener.on("connect", () => {
if (this.state.posts) { this.client
this.client .get("/timelines/public", { limit: 40 })
.get("/timelines/public", { .then((resp: any) => {
max_id: this.state.posts[this.state.posts.length - 1].id, let statuses: [Status] = resp.data;
limit: 20 this.setState({
}) posts: statuses,
.then((resp: any) => { viewIsLoading: false,
let newPosts: [Status] = resp.data; viewDidLoad: true,
let posts = this.state.posts as [Status]; viewDidError: false
newPosts.forEach((post: Status) => { });
posts.push(post); })
}); .catch((resp: any) => {
this.setState({ this.setState({
viewIsLoading: false, viewIsLoading: false,
viewDidLoad: true, viewDidLoad: true,
posts viewDidError: true,
}); viewDidErrorCode: String(resp)
}) });
.catch((err: Error) => { this.props.enqueueSnackbar("Failed to get posts.", {
this.setState({ variant: "error"
viewIsLoading: false, });
viewDidError: true, });
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
}); });
this.streamListener.on("update", (status: Status) => {
let queue = this.state.backlogPosts;
if (queue !== null && queue !== undefined) {
queue.unshift(status);
} else {
queue = [status];
}
this.setState({ backlogPosts: queue });
});
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", () => {});
} }
}
render() { componentWillUnmount() {
const { classes } = this.props; this.streamListener.stop();
}
return ( insertBacklog() {
<div className={classes.pageLayoutMaxConstraints}> window.scrollTo(0, 0);
{this.state.backlogPosts ? ( let posts = this.state.posts;
<div className={classes.pageTopChipContainer}> let backlog = this.state.backlogPosts;
<div className={classes.pageTopChips}> if (posts && backlog && backlog.length > 0) {
<Slide direction="down" in={true}> let push = backlog.concat(posts);
<Chip this.setState({ posts: push as [Status], backlogPosts: null });
avatar={ }
<Avatar> }
<ArrowUpwardIcon />
</Avatar> loadMoreTimelinePieces() {
} this.setState({ viewDidLoad: false, viewIsLoading: true });
label={`View ${this.state.backlogPosts.length} new post${ if (this.state.posts) {
this.state.backlogPosts.length > 1 ? "s" : "" this.client
}`} .get("/timelines/public", {
color="primary" max_id: this.state.posts[this.state.posts.length - 1].id,
className={classes.pageTopChip} limit: 20
onClick={() => this.insertBacklog()} })
clickable .then((resp: any) => {
/> let newPosts: [Status] = resp.data;
</Slide> 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.backlogPosts ? (
<div className={classes.pageTopChipContainer}>
<div className={classes.pageTopChips}>
<Slide direction="down" in={true}>
<Chip
avatar={
<Avatar>
<ArrowUpwardIcon />
</Avatar>
}
label={`View ${
this.state.backlogPosts.length
} new post${
this.state.backlogPosts.length > 1
? "s"
: ""
}`}
color="primary"
className={classes.pageTopChip}
onClick={() => this.insertBacklog()}
clickable
/>
</Slide>
</div>
</div>
) : null}
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div> </div>
</div> );
) : null} }
{this.state.posts ? (
<div>
{this.state.posts.map((post: Status) => {
return <Post key={post.id} post={post} client={this.client} />;
})}
<br />
{this.state.viewDidLoad && !this.state.viewDidError ? (
<div
style={{ textAlign: "center" }}
onClick={() => this.loadMoreTimelinePieces()}
>
<Button variant="contained">Load more</Button>
</div>
) : null}
</div>
) : (
<span />
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress className={classes.progress} color="primary" />
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(PublicPage)); export default withStyles(styles)(withSnackbar(PublicPage));

View File

@ -1,19 +1,19 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
Typography, Typography,
List, List,
ListItem, ListItem,
Paper, Paper,
ListItemText, ListItemText,
Avatar, Avatar,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemAvatar, ListItemAvatar,
ListSubheader, ListSubheader,
CircularProgress, CircularProgress,
IconButton, IconButton,
Divider, Divider,
Tooltip Tooltip
} from "@material-ui/core"; } from "@material-ui/core";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
import Mastodon from "megalodon"; import Mastodon from "megalodon";
@ -27,286 +27,339 @@ import CloseIcon from "@material-ui/icons/Close";
import { withSnackbar, withSnackbarProps } from "notistack"; import { withSnackbar, withSnackbarProps } from "notistack";
interface IRecommendationsPageProps extends withSnackbarProps { interface IRecommendationsPageProps extends withSnackbarProps {
classes: any; classes: any;
} }
interface IRecommendationsPageState { interface IRecommendationsPageState {
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: Boolean; viewDidError?: Boolean;
viewDidErrorCode?: string; viewDidErrorCode?: string;
requestedFollows?: [Account]; requestedFollows?: [Account];
followSuggestions?: [Account]; followSuggestions?: [Account];
} }
class RecommendationsPage extends Component< class RecommendationsPage extends Component<
IRecommendationsPageProps, IRecommendationsPageProps,
IRecommendationsPageState IRecommendationsPageState
> { > {
client: Mastodon; client: Mastodon;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1" localStorage.getItem("baseurl") + "/api/v1"
);
this.state = {
viewIsLoading: true
};
}
componentDidMount() {
this.client
.get("/follow_requests")
.then((resp: any) => {
let requestedFollows: [Account] = resp.data;
this.setState({ requestedFollows });
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.name
});
console.error(err.message);
});
this.client
.get("/suggestions")
.then((resp: any) => {
let followSuggestions: [Account] = resp.data;
this.setState({
viewIsLoading: false,
viewDidLoad: true,
followSuggestions
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.name
});
console.error(err.message);
});
}
followMember(acct: Account) {
this.client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar("You are now following this account.");
this.client.del(`/suggestions/${acct.id}`).then((resp: any) => {
let followSuggestions = this.state.followSuggestions;
if (followSuggestions) {
followSuggestions.forEach((suggestion: Account, index: number) => {
if (followSuggestions && suggestion.id === acct.id) {
followSuggestions.splice(index, 1);
}
});
this.setState({ followSuggestions });
}
});
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
variant: "error"
});
console.error(err.message);
});
}
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
this.client
.post(`/follow_requests/${acct.id}/${type}`)
.then((resp: any) => {
let requestedFollows = this.state.requestedFollows;
if (requestedFollows) {
requestedFollows.forEach((request: Account, index: number) => {
if (requestedFollows && request.id === acct.id) {
requestedFollows.splice(index, 1);
}
});
}
this.setState({ requestedFollows });
let verb: string = type;
verb === "authorize" ? (verb = "authorized") : (verb = "rejected");
this.props.enqueueSnackbar(`You have ${verb} this request.`);
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
`Couldn't ${type} this request: ${err.name}`,
{ variant: "error" }
); );
console.error(err.message); this.state = {
}); viewIsLoading: true
} };
}
showFollowRequests() { componentDidMount() {
const { classes } = this.props; this.client
return ( .get("/follow_requests")
<div> .then((resp: any) => {
<ListSubheader>Follow requests</ListSubheader> let requestedFollows: [Account] = resp.data;
<Paper className={classes.pageListConstraints}> this.setState({ requestedFollows });
<List> })
{this.state.requestedFollows .catch((err: Error) => {
? this.state.requestedFollows.map((request: Account) => { this.setState({
return ( viewIsLoading: false,
<ListItem key={request.id}> viewDidError: true,
<ListItemAvatar> viewDidErrorCode: err.name
<LinkableAvatar });
to={`/profile/${request.id}`} console.error(err.message);
alt={request.username} });
src={request.avatar_static}
/> this.client
</ListItemAvatar> .get("/suggestions")
<ListItemText .then((resp: any) => {
primary={request.display_name || request.acct} let followSuggestions: [Account] = resp.data;
secondary={request.acct} this.setState({
/> viewIsLoading: false,
<ListItemSecondaryAction> viewDidLoad: true,
<Tooltip title="Accept request"> followSuggestions
<IconButton });
onClick={() => })
this.handleFollowRequest(request, "authorize") .catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.name
});
console.error(err.message);
});
}
followMember(acct: Account) {
this.client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar(
"You are now following this account."
);
this.client.del(`/suggestions/${acct.id}`).then((resp: any) => {
let followSuggestions = this.state.followSuggestions;
if (followSuggestions) {
followSuggestions.forEach(
(suggestion: Account, index: number) => {
if (
followSuggestions &&
suggestion.id === acct.id
) {
followSuggestions.splice(index, 1);
}
} }
> );
<CheckIcon /> this.setState({ followSuggestions });
</IconButton> }
</Tooltip> });
<Tooltip title="Reject request"> })
<IconButton .catch((err: Error) => {
onClick={() => this.props.enqueueSnackbar(
this.handleFollowRequest(request, "reject") "Couldn't follow account: " + err.name,
{ variant: "error" }
);
console.error(err.message);
});
}
handleFollowRequest(acct: Account, type: "authorize" | "reject") {
this.client
.post(`/follow_requests/${acct.id}/${type}`)
.then((resp: any) => {
let requestedFollows = this.state.requestedFollows;
if (requestedFollows) {
requestedFollows.forEach(
(request: Account, index: number) => {
if (requestedFollows && request.id === acct.id) {
requestedFollows.splice(index, 1);
} }
> }
<CloseIcon /> );
</IconButton> }
</Tooltip> this.setState({ requestedFollows });
<Tooltip title="View profile">
<LinkableIconButton to={`/profile/${request.id}`}>
<AccountCircleIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
})
: null}
</List>
</Paper>
<br />
</div>
);
}
showFollowSuggestions() { let verb: string = type;
const { classes } = this.props; verb === "authorize"
return ( ? (verb = "authorized")
<div> : (verb = "rejected");
<ListSubheader>Suggested accounts</ListSubheader> this.props.enqueueSnackbar(`You have ${verb} this request.`);
<Paper className={classes.pageListConstraints}> })
<List> .catch((err: Error) => {
{this.state.followSuggestions this.props.enqueueSnackbar(
? this.state.followSuggestions.map((suggestion: Account) => { `Couldn't ${type} this request: ${err.name}`,
return ( { variant: "error" }
<ListItem key={suggestion.id}> );
<ListItemAvatar> console.error(err.message);
<LinkableAvatar });
to={`/profile/${suggestion.id}`} }
alt={suggestion.username}
src={suggestion.avatar_static}
/>
</ListItemAvatar>
<ListItemText
primary={suggestion.display_name || suggestion.acct}
secondary={suggestion.acct}
/>
<ListItemSecondaryAction>
<Tooltip title="View profile">
<LinkableIconButton to={`/profile/${suggestion.id}`}>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Follow">
<IconButton
onClick={() => this.followMember(suggestion)}
>
<PersonAddIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
})
: null}
</List>
</Paper>
<br />
</div>
);
}
render() { showFollowRequests() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.pageLayoutConstraints}> <div>
{this.state.viewDidLoad ? ( <ListSubheader>Follow requests</ListSubheader>
<div> <Paper className={classes.pageListConstraints}>
{this.state.requestedFollows && <List>
this.state.requestedFollows.length > 0 ? ( {this.state.requestedFollows
this.showFollowRequests() ? this.state.requestedFollows.map(
) : ( (request: Account) => {
<div className={classes.pageLayoutEmptyTextConstraints}> return (
<Typography variant="h6"> <ListItem key={request.id}>
You don't have any follow requests. <ListItemAvatar>
</Typography> <LinkableAvatar
to={`/profile/${request.id}`}
alt={request.username}
src={
request.avatar_static
}
/>
</ListItemAvatar>
<ListItemText
primary={
request.display_name ||
request.acct
}
secondary={request.acct}
/>
<ListItemSecondaryAction>
<Tooltip title="Accept request">
<IconButton
onClick={() =>
this.handleFollowRequest(
request,
"authorize"
)
}
>
<CheckIcon />
</IconButton>
</Tooltip>
<Tooltip title="Reject request">
<IconButton
onClick={() =>
this.handleFollowRequest(
request,
"reject"
)
}
>
<CloseIcon />
</IconButton>
</Tooltip>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${request.id}`}
>
<AccountCircleIcon />
</LinkableIconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
)
: null}
</List>
</Paper>
<br /> <br />
</div> </div>
)} );
<Divider /> }
<br />
{this.state.followSuggestions && showFollowSuggestions() {
this.state.followSuggestions.length > 0 ? ( const { classes } = this.props;
this.showFollowSuggestions() return (
) : ( <div>
<div className={classes.pageLayoutEmptyTextConstraints}> <ListSubheader>Suggested accounts</ListSubheader>
<Typography variant="h5"> <Paper className={classes.pageListConstraints}>
We don't have any suggestions for you. <List>
</Typography> {this.state.followSuggestions
<Typography paragraph> ? this.state.followSuggestions.map(
Why not interact with the fediverse a bit by creating a new (suggestion: Account) => {
post? return (
</Typography> <ListItem key={suggestion.id}>
</div> <ListItemAvatar>
)} <LinkableAvatar
</div> to={`/profile/${suggestion.id}`}
) : null} alt={suggestion.username}
{this.state.viewDidError ? ( src={
<Paper className={classes.errorCard}> suggestion.avatar_static
<Typography variant="h4">Bummer.</Typography> }
<Typography variant="h6"> />
Something went wrong when loading this timeline. </ListItemAvatar>
</Typography> <ListItemText
<Typography> primary={
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""} suggestion.display_name ||
</Typography> suggestion.acct
</Paper> }
) : ( secondary={suggestion.acct}
<span /> />
)} <ListItemSecondaryAction>
{this.state.viewIsLoading ? ( <Tooltip title="View profile">
<div style={{ textAlign: "center" }}> <LinkableIconButton
<CircularProgress className={classes.progress} color="primary" /> to={`/profile/${suggestion.id}`}
</div> >
) : ( <AssignmentIndIcon />
<span /> </LinkableIconButton>
)} </Tooltip>
</div> <Tooltip title="Follow">
); <IconButton
} onClick={() =>
this.followMember(
suggestion
)
}
>
<PersonAddIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
)
: null}
</List>
</Paper>
<br />
</div>
);
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? (
<div>
{this.state.requestedFollows &&
this.state.requestedFollows.length > 0 ? (
this.showFollowRequests()
) : (
<div
className={
classes.pageLayoutEmptyTextConstraints
}
>
<Typography variant="h6">
You don't have any follow requests.
</Typography>
<br />
</div>
)}
<Divider />
<br />
{this.state.followSuggestions &&
this.state.followSuggestions.length > 0 ? (
this.showFollowSuggestions()
) : (
<div
className={
classes.pageLayoutEmptyTextConstraints
}
>
<Typography variant="h5">
We don't have any suggestions for you.
</Typography>
<Typography paragraph>
Why not interact with the fediverse a bit by
creating a new post?
</Typography>
</div>
)}
</div>
) : null}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(RecommendationsPage)); export default withStyles(styles)(withSnackbar(RecommendationsPage));

View File

@ -1,18 +1,18 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
List, List,
ListItem, ListItem,
ListItemText, ListItemText,
ListSubheader, ListSubheader,
ListItemSecondaryAction, ListItemSecondaryAction,
ListItemAvatar, ListItemAvatar,
Avatar, Avatar,
Paper, Paper,
withStyles, withStyles,
Typography, Typography,
CircularProgress, CircularProgress,
Tooltip, Tooltip,
IconButton IconButton
} from "@material-ui/core"; } from "@material-ui/core";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd"; import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
@ -28,304 +28,336 @@ import { Status } from "../types/Status";
import { Account } from "../types/Account"; import { Account } from "../types/Account";
interface ISearchPageState { interface ISearchPageState {
query: string[] | string; query: string[] | string;
type?: string[] | string; type?: string[] | string;
results?: Results; results?: Results;
tagResults?: [Status]; tagResults?: [Status];
viewIsLoading: boolean; viewIsLoading: boolean;
viewDidLoad?: boolean; viewDidLoad?: boolean;
viewDidError?: boolean; viewDidError?: boolean;
viewDidErrorCode?: string; viewDidErrorCode?: string;
} }
class SearchPage extends Component<any, ISearchPageState> { class SearchPage extends Component<any, ISearchPageState> {
client: Mastodon; client: Mastodon;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v2" localStorage.getItem("baseurl") + "/api/v2"
);
let searchParams = this.getQueryAndType(props);
this.state = {
viewIsLoading: true,
query: searchParams.query,
type: searchParams.type
};
if (searchParams.type === "tag") {
this.searchForPostsWithTags(searchParams.query);
} else {
this.searchQuery(searchParams.query);
}
}
componentWillReceiveProps(props: any) {
this.setState({
viewDidLoad: false,
viewIsLoading: true,
viewDidError: false,
viewDidErrorCode: "",
results: undefined
});
let searchParams = this.getQueryAndType(props);
this.setState({ query: searchParams.query, type: searchParams.type });
if (searchParams.type === "tag") {
this.searchForPostsWithTags(searchParams.query);
} else {
this.searchQuery(searchParams.query);
}
}
runQueryCheck(newLocation?: string): ParsedQuery {
let searchParams = "";
if (newLocation !== undefined && typeof newLocation === "string") {
searchParams = newLocation.replace("#/search", "");
} else {
searchParams = location.hash.replace("#/search", "");
}
return parseParams(searchParams);
}
getQueryAndType(props: any) {
let newSearch = this.runQueryCheck(props.location);
let query: string | string[];
let type;
if (newSearch.query) {
if (newSearch.query.toString().startsWith("tag:")) {
type = "tag";
query = newSearch.query.toString().replace("tag:", "");
} else {
query = newSearch.query;
}
} else {
query = "";
}
if (newSearch.type && newSearch.type !== undefined) {
type = newSearch.type;
}
return {
query: query,
type: type
};
}
searchQuery(query: string | string[]) {
this.client
.get("/search", { q: query })
.then((resp: any) => {
let results: Results = resp.data;
this.setState({
results,
viewDidLoad: true,
viewIsLoading: false
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar(
`Couldn't search for ${this.state.query}: ${err.name}`,
{ variant: "error" }
); );
});
}
searchForPostsWithTags(query: string | string[]) { let searchParams = this.getQueryAndType(props);
let client = new Mastodon(
localStorage.getItem("access_token") as string, this.state = {
localStorage.getItem("baseurl") + "/api/v1" viewIsLoading: true,
); query: searchParams.query,
client type: searchParams.type
.get(`/timelines/tag/${query}`) };
.then((resp: any) => {
let tagResults: [Status] = resp.data; if (searchParams.type === "tag") {
this.searchForPostsWithTags(searchParams.query);
} else {
this.searchQuery(searchParams.query);
}
}
componentWillReceiveProps(props: any) {
this.setState({ this.setState({
tagResults, viewDidLoad: false,
viewDidLoad: true, viewIsLoading: true,
viewIsLoading: false viewDidError: false,
}); viewDidErrorCode: "",
console.log(this.state.tagResults); results: undefined
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
}); });
let searchParams = this.getQueryAndType(props);
this.setState({ query: searchParams.query, type: searchParams.type });
if (searchParams.type === "tag") {
this.searchForPostsWithTags(searchParams.query);
} else {
this.searchQuery(searchParams.query);
}
}
this.props.enqueueSnackbar( runQueryCheck(newLocation?: string): ParsedQuery {
`Couldn't search for posts with tag ${this.state.query}: ${err.name}`, let searchParams = "";
{ variant: "error" } if (newLocation !== undefined && typeof newLocation === "string") {
); searchParams = newLocation.replace("#/search", "");
}); } else {
} searchParams = location.hash.replace("#/search", "");
}
return parseParams(searchParams);
}
followMemberFromQuery(acct: Account) { getQueryAndType(props: any) {
let client = new Mastodon( let newSearch = this.runQueryCheck(props.location);
localStorage.getItem("access_token") as string, let query: string | string[];
localStorage.getItem("baseurl") + "/api/v1" let type;
);
client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar("You are now following this account.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, {
variant: "error"
});
console.error(err.message);
});
}
showAllAccountsFromQuery() { if (newSearch.query) {
const { classes } = this.props; if (newSearch.query.toString().startsWith("tag:")) {
return ( type = "tag";
<div> query = newSearch.query.toString().replace("tag:", "");
<ListSubheader>Accounts</ListSubheader> } else {
query = newSearch.query;
}
} else {
query = "";
}
{this.state.results && this.state.results.accounts.length > 0 ? ( if (newSearch.type && newSearch.type !== undefined) {
<Paper className={classes.pageListConstraints}> type = newSearch.type;
<List> }
{this.state.results.accounts.map((acct: Account) => { return {
return ( query: query,
<ListItem key={acct.id}> type: type
<ListItemAvatar> };
<LinkableAvatar }
to={`/profile/${acct.id}`}
alt={acct.username} searchQuery(query: string | string[]) {
src={acct.avatar_static} this.client
/> .get("/search", { q: query })
</ListItemAvatar> .then((resp: any) => {
<ListItemText let results: Results = resp.data;
primary={acct.display_name || acct.acct} this.setState({
secondary={acct.acct} results,
/> viewDidLoad: true,
<ListItemSecondaryAction> viewIsLoading: false
<Tooltip title="View profile"> });
<LinkableIconButton to={`/profile/${acct.id}`}> })
<AssignmentIndIcon /> .catch((err: Error) => {
</LinkableIconButton> this.setState({
</Tooltip> viewIsLoading: false,
<Tooltip title="Follow"> viewDidError: true,
<IconButton viewDidErrorCode: err.message
onClick={() => this.followMemberFromQuery(acct)} });
>
<PersonAddIcon /> this.props.enqueueSnackbar(
</IconButton> `Couldn't search for ${this.state.query}: ${err.name}`,
</Tooltip> { variant: "error" }
</ListItemSecondaryAction>
</ListItem>
); );
})} });
</List> }
</Paper>
) : (
<Typography
variant="caption"
className={classes.pageLayoutEmptyTextConstraints}
>
No results found
</Typography>
)}
<br /> searchForPostsWithTags(query: string | string[]) {
</div> let client = new Mastodon(
); localStorage.getItem("access_token") as string,
} localStorage.getItem("baseurl") + "/api/v1"
);
showAllPostsFromQuery() { client
const { classes } = this.props; .get(`/timelines/tag/${query}`)
return ( .then((resp: any) => {
<div> let tagResults: [Status] = resp.data;
<ListSubheader>Posts</ListSubheader> this.setState({
{this.state.results ? ( tagResults,
this.state.results.statuses.length > 0 ? ( viewDidLoad: true,
this.state.results.statuses.map((post: Status) => { viewIsLoading: false
return <Post key={post.id} post={post} client={this.client} />; });
console.log(this.state.tagResults);
}) })
) : ( .catch((err: Error) => {
<Typography this.setState({
variant="caption" viewIsLoading: false,
className={classes.pageLayoutEmptyTextConstraints} viewDidError: true,
> viewDidErrorCode: err.message
No results found. });
</Typography>
)
) : null}
</div>
);
}
showAllPostsWithTag() { this.props.enqueueSnackbar(
const { classes } = this.props; `Couldn't search for posts with tag ${this.state.query}: ${err.name}`,
return ( { variant: "error" }
<div> );
<ListSubheader>Tagged posts</ListSubheader> });
{this.state.tagResults ? ( }
this.state.tagResults.length > 0 ? (
this.state.tagResults.map((post: Status) => { followMemberFromQuery(acct: Account) {
return <Post key={post.id} post={post} client={this.client} />; let client = new Mastodon(
localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1"
);
client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar(
"You are now following this account."
);
}) })
) : ( .catch((err: Error) => {
<Typography this.props.enqueueSnackbar(
variant="caption" "Couldn't follow account: " + err.name,
className={classes.pageLayoutEmptyTextConstraints} { variant: "error" }
> );
No results found. console.error(err.message);
</Typography> });
) }
) : null}
</div>
);
}
render() { showAllAccountsFromQuery() {
const { classes } = this.props; const { classes } = this.props;
return ( return (
<div className={classes.pageLayoutConstraints}> <div>
{this.state.type && this.state.type === "tag" ? ( <ListSubheader>Accounts</ListSubheader>
this.showAllPostsWithTag()
) : ( {this.state.results &&
<div> this.state.results.accounts.length > 0 ? (
{this.showAllAccountsFromQuery()} <Paper className={classes.pageListConstraints}>
{this.showAllPostsFromQuery()} <List>
</div> {this.state.results.accounts.map(
)} (acct: Account) => {
{this.state.viewDidError ? ( return (
<Paper className={classes.errorCard}> <ListItem key={acct.id}>
<Typography variant="h4">Bummer.</Typography> <ListItemAvatar>
<Typography variant="h6"> <LinkableAvatar
Something went wrong when loading this timeline. to={`/profile/${acct.id}`}
</Typography> alt={acct.username}
<Typography> src={acct.avatar_static}
{this.state.viewDidErrorCode ? this.state.viewDidErrorCode : ""} />
</Typography> </ListItemAvatar>
</Paper> <ListItemText
) : ( primary={
<span /> acct.display_name ||
)} acct.acct
{this.state.viewIsLoading ? ( }
<div style={{ textAlign: "center" }}> secondary={acct.acct}
<CircularProgress className={classes.progress} color="primary" /> />
</div> <ListItemSecondaryAction>
) : ( <Tooltip title="View profile">
<span /> <LinkableIconButton
)} to={`/profile/${acct.id}`}
</div> >
); <AssignmentIndIcon />
} </LinkableIconButton>
</Tooltip>
<Tooltip title="Follow">
<IconButton
onClick={() =>
this.followMemberFromQuery(
acct
)
}
>
<PersonAddIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
)}
</List>
</Paper>
) : (
<Typography
variant="caption"
className={classes.pageLayoutEmptyTextConstraints}
>
No results found
</Typography>
)}
<br />
</div>
);
}
showAllPostsFromQuery() {
const { classes } = this.props;
return (
<div>
<ListSubheader>Posts</ListSubheader>
{this.state.results ? (
this.state.results.statuses.length > 0 ? (
this.state.results.statuses.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})
) : (
<Typography
variant="caption"
className={classes.pageLayoutEmptyTextConstraints}
>
No results found.
</Typography>
)
) : null}
</div>
);
}
showAllPostsWithTag() {
const { classes } = this.props;
return (
<div>
<ListSubheader>Tagged posts</ListSubheader>
{this.state.tagResults ? (
this.state.tagResults.length > 0 ? (
this.state.tagResults.map((post: Status) => {
return (
<Post
key={post.id}
post={post}
client={this.client}
/>
);
})
) : (
<Typography
variant="caption"
className={classes.pageLayoutEmptyTextConstraints}
>
No results found.
</Typography>
)
) : null}
</div>
);
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
{this.state.type && this.state.type === "tag" ? (
this.showAllPostsWithTag()
) : (
<div>
{this.showAllAccountsFromQuery()}
{this.showAllPostsFromQuery()}
</div>
)}
{this.state.viewDidError ? (
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">
Something went wrong when loading this timeline.
</Typography>
<Typography>
{this.state.viewDidErrorCode
? this.state.viewDidErrorCode
: ""}
</Typography>
</Paper>
) : (
<span />
)}
{this.state.viewIsLoading ? (
<div style={{ textAlign: "center" }}>
<CircularProgress
className={classes.progress}
color="primary"
/>
</div>
) : (
<span />
)}
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(SearchPage)); export default withStyles(styles)(withSnackbar(SearchPage));

View File

@ -46,7 +46,6 @@ import {
} from "../utilities/themes"; } from "../utilities/themes";
import { Visibility } from "../types/Visibility"; import { Visibility } from "../types/Visibility";
import { LinkableButton } from "../interfaces/overrides"; import { LinkableButton } from "../interfaces/overrides";
import { Config } from "../types/Config";
import OpenInNewIcon from "@material-ui/icons/OpenInNew"; import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import DevicesIcon from "@material-ui/icons/Devices"; import DevicesIcon from "@material-ui/icons/Devices";
@ -59,7 +58,7 @@ import NotificationsIcon from "@material-ui/icons/Notifications";
import BellAlertIcon from "mdi-material-ui/BellAlert"; import BellAlertIcon from "mdi-material-ui/BellAlert";
import RefreshIcon from "@material-ui/icons/Refresh"; import RefreshIcon from "@material-ui/icons/Refresh";
import UndoIcon from "@material-ui/icons/Undo"; import UndoIcon from "@material-ui/icons/Undo";
import DomainDisablbedIcon from "@material-ui/icons/DomainDisabled"; import { Config } from "../types/Config";
interface ISettingsState { interface ISettingsState {
darkModeEnabled: boolean; darkModeEnabled: boolean;
@ -500,20 +499,6 @@ class SettingsPage extends Component<any, ISettingsState> {
<LinkableButton to="/you">Edit</LinkableButton> <LinkableButton to="/you">Edit</LinkableButton>
</ListItemSecondaryAction> </ListItemSecondaryAction>
</ListItem> </ListItem>
<ListItem>
<ListItemAvatar>
<DomainDisablbedIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Manage blocked servers"
secondary="View and manage servers that you've blocked"
/>
<ListItemSecondaryAction>
<LinkableButton to="/blocked">
Manage
</LinkableButton>
</ListItemSecondaryAction>
</ListItem>
<ListItem> <ListItem>
<ListItemAvatar> <ListItemAvatar>
<MastodonIcon color="action" /> <MastodonIcon color="action" />

View File

@ -17,11 +17,7 @@ import {
import { styles } from "./WelcomePage.styles"; import { styles } from "./WelcomePage.styles";
import Mastodon from "megalodon"; import Mastodon from "megalodon";
import { SaveClientSession } from "../types/SessionData"; import { SaveClientSession } from "../types/SessionData";
import { import { createHyperspaceApp, getRedirectAddress } from "../utilities/login";
createHyperspaceApp,
getRedirectAddress,
inDisallowedDomains
} from "../utilities/login";
import { parseUrl } from "query-string"; import { parseUrl } from "query-string";
import { getConfig } from "../utilities/settings"; import { getConfig } from "../utilities/settings";
import { isDarwinApp } from "../utilities/desktop"; import { isDarwinApp } from "../utilities/desktop";
@ -83,17 +79,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
let config: Config = result; let config: Config = result;
if (result.location === "dynamic") { if (result.location === "dynamic") {
console.warn( console.warn(
"Redirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!" "Recirect URI is set to dynamic, which may affect how sign-in works for some users. Careful!"
); );
} }
if (
inDisallowedDomains(result.registration.defaultInstance)
) {
console.warn(
`The default instance field in config.json contains an unsupported domain (${result.registration.defaultInstance}), so it's been reset to mastodon.social.`
);
result.registration.defaultInstance = "mastodon.social";
}
this.setState({ this.setState({
logoUrl: config.branding logoUrl: config.branding
? result.branding.logo ? result.branding.logo
@ -322,30 +310,18 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
if (this.state.user.includes("@")) { if (this.state.user.includes("@")) {
if (this.state.federates && this.state.federates === true) { if (this.state.federates && this.state.federates === true) {
let baseUrl = this.state.user.split("@")[1]; let baseUrl = this.state.user.split("@")[1];
if (inDisallowedDomains(baseUrl)) { axios
this.setState({ .get("https://" + baseUrl + "/api/v1/timelines/public")
userInputError: true, .catch((err: Error) => {
userInputErrorMessage: `Signing in with an account from ${baseUrl} isn't supported.` let userInputError = true;
}); let userInputErrorMessage =
return true; "Instance name is invalid.";
} else { this.setState({
axios userInputError,
.get( userInputErrorMessage
"https://" +
baseUrl +
"/api/v1/timelines/public"
)
.catch((err: Error) => {
let userInputError = true;
let userInputErrorMessage =
"Instance name is invalid.";
this.setState({
userInputError,
userInputErrorMessage
});
return true;
}); });
} return true;
});
} else if ( } else if (
this.state.user.includes( this.state.user.includes(
this.state.registerBase this.state.registerBase
@ -726,8 +702,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
{this.state.brandName {this.state.brandName
? this.state.brandName ? this.state.brandName
: "Hypersapce"}{" "} : "Hypersapce"}{" "}
v. v.{this.state.version}{" "}
{this.state.version}{" "}
{this.state.brandName && {this.state.brandName &&
this.state.brandName !== "Hyperspace" this.state.brandName !== "Hyperspace"
? "(Hyperspace-like)" ? "(Hyperspace-like)"

View File

@ -1,73 +1,73 @@
import { Theme, createStyles } from "@material-ui/core"; import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) => export const styles = (theme: Theme) =>
createStyles({ createStyles({
root: { root: {
width: "100%", width: "100%",
height: "100%", height: "100%",
backgroundPosition: "center", backgroundPosition: "center",
backgroundRepeat: "no-repeat", backgroundRepeat: "no-repeat",
backgroundSize: "cover", backgroundSize: "cover",
top: 0, top: 0,
left: 0, left: 0,
position: "absolute", position: "absolute",
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
paddingTop: theme.spacing.unit * 4, paddingTop: theme.spacing.unit * 4,
paddingLeft: "25%", paddingLeft: "25%",
paddingRight: "25%" paddingRight: "25%"
}, },
[theme.breakpoints.up("lg")]: { [theme.breakpoints.up("lg")]: {
paddingTop: theme.spacing.unit * 12, paddingTop: theme.spacing.unit * 12,
paddingLeft: "35%", paddingLeft: "35%",
paddingRight: "35%" paddingRight: "35%"
} }
}, },
titleBarRoot: { titleBarRoot: {
top: 0, top: 0,
left: 0, left: 0,
height: 24, height: 24,
width: "100%", width: "100%",
backgroundColor: "rgba(0, 0, 0, 0.2)", backgroundColor: "rgba(0, 0, 0, 0.2)",
textAlign: "center", textAlign: "center",
zIndex: 1000, zIndex: 1000,
verticalAlign: "middle", verticalAlign: "middle",
WebkitUserSelect: "none", WebkitUserSelect: "none",
WebkitAppRegion: "drag", WebkitAppRegion: "drag",
position: "absolute" position: "absolute"
}, },
titleBarText: { titleBarText: {
color: theme.palette.common.white, color: theme.palette.common.white,
fontSize: 12, fontSize: 12,
paddingTop: 2, paddingTop: 2,
paddingBottom: 1 paddingBottom: 1
}, },
paper: { paper: {
height: "100%", height: "100%",
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
height: "auto", height: "auto",
paddingLeft: theme.spacing.unit * 8, paddingLeft: theme.spacing.unit * 8,
paddingRight: theme.spacing.unit * 8, paddingRight: theme.spacing.unit * 8,
paddingTop: theme.spacing.unit * 6 paddingTop: theme.spacing.unit * 6
}, },
paddingTop: theme.spacing.unit * 12, paddingTop: theme.spacing.unit * 12,
paddingLeft: theme.spacing.unit * 4, paddingLeft: theme.spacing.unit * 4,
paddingRight: theme.spacing.unit * 4, paddingRight: theme.spacing.unit * 4,
paddingBottom: theme.spacing.unit * 6, paddingBottom: theme.spacing.unit * 6,
textAlign: "center" textAlign: "center"
}, },
welcomeLink: { welcomeLink: {
color: theme.palette.primary.light color: theme.palette.primary.light
}, },
flexGrow: { flexGrow: {
flexGrow: 1 flexGrow: 1
}, },
middlePadding: { middlePadding: {
height: theme.spacing.unit * 6 height: theme.spacing.unit * 6
}, },
logo: { logo: {
[theme.breakpoints.up("sm")]: { [theme.breakpoints.up("sm")]: {
height: 64, height: 64,
width: "auto" width: "auto"
} }
} }
}); });

View File

@ -1,16 +1,16 @@
import React, { Component } from "react"; import React, { Component } from "react";
import { import {
withStyles, withStyles,
Typography, Typography,
Paper, Paper,
Avatar, Avatar,
Button, Button,
TextField, TextField,
ListItem, ListItem,
ListItemText, ListItemText,
ListItemAvatar, ListItemAvatar,
List, List,
Grid Grid
} from "@material-ui/core"; } from "@material-ui/core";
import { withSnackbar, withSnackbarProps } from "notistack"; import { withSnackbar, withSnackbarProps } from "notistack";
import { styles } from "./PageLayout.styles"; import { styles } from "./PageLayout.styles";
@ -21,271 +21,303 @@ import filedialog from "file-dialog";
import PersonIcon from "@material-ui/icons/Person"; import PersonIcon from "@material-ui/icons/Person";
interface IYouProps extends withSnackbarProps { interface IYouProps extends withSnackbarProps {
classes: any; classes: any;
} }
interface IYouState { interface IYouState {
currentAccount: Account; currentAccount: Account;
newDisplayName?: string; newDisplayName?: string;
newBio?: string; newBio?: string;
} }
class You extends Component<IYouProps, IYouState> { class You extends Component<IYouProps, IYouState> {
client: Mastodon; client: Mastodon;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
); );
this.state = { this.state = {
currentAccount: this.getAccount() currentAccount: this.getAccount()
}; };
}
getAccount() {
let acct = localStorage.getItem("account");
if (acct) {
return JSON.parse(acct);
} }
}
updateAvatar() { getAccount() {
filedialog({ let acct = localStorage.getItem("account");
multiple: false, if (acct) {
accept: "image/*" return JSON.parse(acct);
}) }
.then((images: FileList) => { }
if (images.length > 0) {
this.props.enqueueSnackbar("Updating avatar...", { updateAvatar() {
persist: true, filedialog({
key: "persistAvatar" multiple: false,
}); accept: "image/*"
let upload = new FormData(); })
upload.append("avatar", images[0]); .then((images: FileList) => {
this.client if (images.length > 0) {
.patch("/accounts/update_credentials", upload) this.props.enqueueSnackbar("Updating avatar...", {
.then((acct: any) => { persist: true,
let currentAccount: Account = acct.data; key: "persistAvatar"
this.setState({ currentAccount }); });
localStorage.setItem("account", JSON.stringify(currentAccount)); let upload = new FormData();
this.props.closeSnackbar("persistAvatar"); upload.append("avatar", images[0]);
this.props.enqueueSnackbar("Avatar updated successfully."); this.client
.patch("/accounts/update_credentials", upload)
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem(
"account",
JSON.stringify(currentAccount)
);
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar(
"Avatar updated successfully."
);
})
.catch((err: Error) => {
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar(
"Couldn't update avatar: " + err.name,
{ variant: "error" }
);
});
}
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.props.closeSnackbar("persistAvatar"); this.props.enqueueSnackbar(
this.props.enqueueSnackbar( "Couldn't update avatar: " + err.name
"Couldn't update avatar: " + err.name, );
{ variant: "error" }
);
}); });
} }
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
});
}
updateHeader() { updateHeader() {
filedialog({ filedialog({
multiple: false, multiple: false,
accept: "image/*" accept: "image/*"
}) })
.then((images: FileList) => { .then((images: FileList) => {
if (images.length > 0) { if (images.length > 0) {
this.props.enqueueSnackbar("Updating header...", { this.props.enqueueSnackbar("Updating header...", {
persist: true, persist: true,
key: "persistHeader" key: "persistHeader"
}); });
let upload = new FormData(); let upload = new FormData();
upload.append("header", images[0]); upload.append("header", images[0]);
this.client this.client
.patch("/accounts/update_credentials", upload) .patch("/accounts/update_credentials", upload)
.then((acct: any) => { .then((acct: any) => {
let currentAccount: Account = acct.data; let currentAccount: Account = acct.data;
this.setState({ currentAccount }); this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount)); localStorage.setItem(
this.props.closeSnackbar("persistHeader"); "account",
this.props.enqueueSnackbar("Header updated successfully."); JSON.stringify(currentAccount)
);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Header updated successfully."
);
})
.catch((err: Error) => {
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update header: " + err.name,
{ variant: "error" }
);
});
}
}) })
.catch((err: Error) => { .catch((err: Error) => {
this.props.closeSnackbar("persistHeader"); this.props.enqueueSnackbar(
this.props.enqueueSnackbar( "Couldn't update header: " + err.name
"Couldn't update header: " + err.name, );
{ variant: "error" }
);
}); });
} }
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
});
}
removeHTMLContent(text: string) { removeHTMLContent(text: string) {
const div = document.createElement("div"); const div = document.createElement("div");
div.innerHTML = text; div.innerHTML = text;
let innerContent = div.textContent || div.innerText || ""; let innerContent = div.textContent || div.innerText || "";
return innerContent; return innerContent;
} }
changeDisplayName() { changeDisplayName() {
this.client this.client
.patch("/accounts/update_credentials", { .patch("/accounts/update_credentials", {
display_name: this.state.newDisplayName display_name: this.state.newDisplayName
? this.state.newDisplayName ? this.state.newDisplayName
: this.state.currentAccount.display_name : this.state.currentAccount.display_name
}) })
.then((acct: any) => { .then((acct: any) => {
let currentAccount: Account = acct.data; let currentAccount: Account = acct.data;
this.setState({ currentAccount }); this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount)); localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader"); this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
"Display name updated to " + this.state.newDisplayName "Display name updated to " + this.state.newDisplayName
);
})
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update display name: " + err.name,
{ variant: "error" }
);
});
}
updateDisplayName(name: string) {
this.setState({ newDisplayName: name });
}
changeBio() {
this.client
.patch("/accounts/update_credentials", {
note: this.state.newBio
? this.state.newBio
: this.state.currentAccount.note
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Bio updated successfully.");
})
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Couldn't update bio: " + err.name, {
variant: "error"
});
});
}
updateBio(bio: string) {
this.setState({ newBio: bio });
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentAccount.header_static}")`
}}
/>
<div className={classes.profileContent}>
<br />
<Avatar
className={classes.profileAvatar}
src={this.state.currentAccount.avatar_static}
/>
<div
className={classes.profileUserBox}
style={{ paddingTop: 8, paddingBottom: 8 }}
>
<Typography
variant="h4"
color="inherit"
component="h1"
>
Edit your profile
</Typography>
<Typography color="inherit">
Change information such as your display name,
bio, and images used here.
</Typography>
<div>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateAvatar()}
>
Change Avatar
</Button>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateHeader()}
>
Change Header
</Button>
</div>
</div>
</div>
</div>
<div className={classes.pageContentLayoutConstraints}>
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
Display Name
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.display_name
}
rowsMax="1"
variant="outlined"
fullWidth
onChange={(event: any) =>
this.updateDisplayName(event.target.value)
}
/>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeDisplayName()}
>
Update display Name
</Button>
</div>
</Paper>
<br />
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
About you
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.note
? this.removeHTMLContent(
this.state.currentAccount.note
)
: "Tell a little bit about yourself"
}
multiline
variant="outlined"
rows="2"
rowsMax="5"
fullWidth
onChange={(event: any) =>
this.updateBio(event.target.value)
}
/>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeBio()}
>
Update biography
</Button>
</div>
</Paper>
</div>
</div>
); );
}) }
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update display name: " + err.name,
{ variant: "error" }
);
});
}
updateDisplayname(name: string) {
this.setState({ newDisplayName: name });
}
changeBio() {
this.client
.patch("/accounts/update_credentials", {
note: this.state.newBio
? this.state.newBio
: this.state.currentAccount.note
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Bio updated successfully.");
})
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Couldn't update bio: " + err.name, {
variant: "error"
});
});
}
updateBio(bio: string) {
this.setState({ newBio: bio });
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentAccount.header_static}")`
}}
/>
<div className={classes.pageHeroContent}>
<Avatar
className={classes.pageProfileAvatar}
src={this.state.currentAccount.avatar_static}
/>
<Typography variant="h4" color="inherit" component="h1">
Edit your profile
</Typography>
<br />
<div>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateAvatar()}
>
Change Avatar
</Button>
<Button
className={classes.pageProfileFollowButton}
variant="contained"
onClick={() => this.updateHeader()}
>
Change Header
</Button>
</div>
<br />
</div>
</div>
<div className={classes.pageContentLayoutConstraints}>
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
Display Name
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={this.state.currentAccount.display_name}
rowsMax="1"
variant="outlined"
fullWidth
onChange={(event: any) =>
this.updateDisplayname(event.target.value)
}
></TextField>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeDisplayName()}
>
Update display Name
</Button>
</div>
</Paper>
<br />
<Paper className={classes.youPaper}>
<Typography variant="h5" component="h2">
About you
</Typography>
<br />
<TextField
className={classes.TextField}
defaultValue={
this.state.currentAccount.note
? this.removeHTMLContent(this.state.currentAccount.note)
: "Tell a little bit about yourself"
}
multiline
variant="outlined"
rows="2"
rowsMax="5"
fullWidth
onChange={(event: any) => this.updateBio(event.target.value)}
></TextField>
<div style={{ textAlign: "right" }}>
<Button
className={classes.pageProfileFollowButton}
color="primary"
onClick={() => this.changeBio()}
>
Update biography
</Button>
</div>
</Paper>
</div>
</div>
);
}
} }
export default withStyles(styles)(withSnackbar(You)); export default withStyles(styles)(withSnackbar(You));

View File

@ -5,30 +5,30 @@ import { Field } from "./Field";
* Basic type for an account on Mastodon * Basic type for an account on Mastodon
*/ */
export type Account = { export type Account = {
id: string; id: string;
username: string; username: string;
acct: string; acct: string;
display_name: string; display_name: string;
locked: boolean; locked: boolean;
created_at: string; created_at: string;
followers_count: number; followers_count: number;
following_count: number; following_count: number;
statuses_count: number; statuses_count: number;
note: string; note: string;
url: string; url: string;
avatar: string; avatar: string;
avatar_static: string; avatar_static: string;
header: string; header: string;
header_static: string; header_static: string;
emojis: [MastodonEmoji]; emojis: [MastodonEmoji];
moved: Account | null; moved: Account | null;
fields: [Field]; fields: [Field];
bot: boolean | null; bot: boolean | null;
}; };
export type UAccount = { export type UAccount = {
id: string; id: string;
acct: string; acct: string;
display_name: string; display_name: string;
avatar_static: string; avatar_static: string;
}; };

View File

@ -2,12 +2,12 @@
* Basic type for an attachment, usually on Statuses * Basic type for an attachment, usually on Statuses
*/ */
export type Attachment = { export type Attachment = {
id: string; id: string;
type: "unknown" | "image" | "gifv" | "video"; type: "unknown" | "image" | "gifv" | "video";
url: string; url: string;
remote_url: string | null; remote_url: string | null;
preview_url: string; preview_url: string;
text_url: string | null; text_url: string | null;
meta: any | null; meta: any | null;
description: string | null; description: string | null;
}; };

View File

@ -2,16 +2,16 @@
* Basic type for Cards, usually in Statuses * Basic type for Cards, usually in Statuses
*/ */
export type Card = { export type Card = {
url: string; url: string;
title: string; title: string;
description: string; description: string;
image: string | null; image: string | null;
type: "link" | "photo" | "video" | "rich"; type: "link" | "photo" | "video" | "rich";
author_name: string | null; author_name: string | null;
author_url: string | null; author_url: string | null;
provider_name: string | null; provider_name: string | null;
provider_url: string | null; provider_url: string | null;
html: string | null; html: string | null;
width: number | null; width: number | null;
height: number | null; height: number | null;
}; };

View File

@ -1,31 +1,31 @@
export type Config = { export type Config = {
version: string; version: string;
location: string; location: string;
branding?: { branding?: {
name?: string; name?: string;
logo?: string; logo?: string;
background?: string; background?: string;
}; };
developer?: boolean; developer?: boolean;
federation: Federation; federation: Federation;
registration?: { registration?: {
defaultInstance?: string; defaultInstance?: string;
}; };
admin?: { admin?: {
name?: string; name?: string;
account?: string; account?: string;
}; };
license: License; license: License;
repository?: string; repository?: string;
}; };
export type License = { export type License = {
name: string; name: string;
url: string; url: string;
}; };
export type Federation = { export type Federation = {
universalLogin: boolean; universalLogin: boolean;
allowPublicPosts: boolean; allowPublicPosts: boolean;
enablePublicTimeline: boolean; enablePublicTimeline: boolean;
}; };

View File

@ -1,6 +1,6 @@
import { Status } from "./Status"; import { Status } from "./Status";
export type Context = { export type Context = {
ancestors: [Status]; ancestors: [Status];
descendants: [Status]; descendants: [Status];
}; };

View File

@ -2,16 +2,16 @@
* Basic type for Emojis on Mastodon. * Basic type for Emojis on Mastodon.
*/ */
export type MastodonEmoji = { export type MastodonEmoji = {
shortcode: string; shortcode: string;
static_url: string; static_url: string;
url: string; url: string;
visible_in_picker: boolean; visible_in_picker: boolean;
}; };
/** /**
* Trimmed type of Emoji from emoji-mart * Trimmed type of Emoji from emoji-mart
*/ */
export type Emoji = { export type Emoji = {
name: string; name: string;
imageUrl: string; imageUrl: string;
}; };

View File

@ -2,7 +2,7 @@
* Basic type for a table entry, usually in Account * Basic type for a table entry, usually in Account
*/ */
export type Field = { export type Field = {
name: string; name: string;
value: string; value: string;
verified_at: string | null; verified_at: string | null;
}; };

View File

@ -1,19 +1,19 @@
import { Color } from "@material-ui/core"; import { Color } from "@material-ui/core";
import { import {
deepPurple, deepPurple,
red, red,
lightGreen, lightGreen,
yellow, yellow,
purple, purple,
deepOrange, deepOrange,
indigo, indigo,
lightBlue, lightBlue,
orange, orange,
blue, blue,
amber, amber,
pink, pink,
brown, brown,
blueGrey blueGrey
} from "@material-ui/core/colors"; } from "@material-ui/core/colors";
import { isDarwinApp } from "../utilities/desktop"; import { isDarwinApp } from "../utilities/desktop";
@ -21,141 +21,141 @@ import { isDarwinApp } from "../utilities/desktop";
* Basic theme colors for Hyperspace. * Basic theme colors for Hyperspace.
*/ */
export type HyperspaceTheme = { export type HyperspaceTheme = {
key: string; key: string;
name: string; name: string;
palette: { palette: {
primary: primary:
| { | {
main: string; main: string;
} }
| Color; | Color;
secondary: secondary:
| { | {
main: string; main: string;
} }
| Color; | Color;
}; };
}; };
export const defaultTheme: HyperspaceTheme = { export const defaultTheme: HyperspaceTheme = {
key: "defaultTheme", key: "defaultTheme",
name: "Royal (Default)", name: "Royal (Default)",
palette: { palette: {
primary: deepPurple, primary: deepPurple,
secondary: red secondary: red
} }
}; };
export const gardenerTheme: HyperspaceTheme = { export const gardenerTheme: HyperspaceTheme = {
key: "gardnerTheme", key: "gardnerTheme",
name: "Botanical", name: "Botanical",
palette: { palette: {
primary: lightGreen, primary: lightGreen,
secondary: yellow secondary: yellow
} }
}; };
export const teacherTheme: HyperspaceTheme = { export const teacherTheme: HyperspaceTheme = {
key: "teacherTheme", key: "teacherTheme",
name: "Compassionate", name: "Compassionate",
palette: { palette: {
primary: purple, primary: purple,
secondary: deepOrange secondary: deepOrange
} }
}; };
export const jokerTheme: HyperspaceTheme = { export const jokerTheme: HyperspaceTheme = {
key: "jokerTheme", key: "jokerTheme",
name: "Joker", name: "Joker",
palette: { palette: {
primary: indigo, primary: indigo,
secondary: lightBlue secondary: lightBlue
} }
}; };
export const guardTheme: HyperspaceTheme = { export const guardTheme: HyperspaceTheme = {
key: "guardTheme", key: "guardTheme",
name: "Enthusiastic", name: "Enthusiastic",
palette: { palette: {
primary: blue, primary: blue,
secondary: deepOrange secondary: deepOrange
} }
}; };
export const entertainerTheme: HyperspaceTheme = { export const entertainerTheme: HyperspaceTheme = {
key: "entertainerTheme", key: "entertainerTheme",
name: "Animated", name: "Animated",
palette: { palette: {
primary: pink, primary: pink,
secondary: purple secondary: purple
} }
}; };
export const classicTheme: HyperspaceTheme = { export const classicTheme: HyperspaceTheme = {
key: "classicTheme", key: "classicTheme",
name: "Classic", name: "Classic",
palette: { palette: {
primary: { primary: {
main: "#555555" main: "#555555"
}, },
secondary: { secondary: {
main: "#5c2d91" main: "#5c2d91"
}
} }
}
}; };
export const dragonTheme: HyperspaceTheme = { export const dragonTheme: HyperspaceTheme = {
key: "dragonTheme", key: "dragonTheme",
name: "Adventurous", name: "Adventurous",
palette: { palette: {
primary: purple, primary: purple,
secondary: purple secondary: purple
} }
}; };
export const memoriumTheme: HyperspaceTheme = { export const memoriumTheme: HyperspaceTheme = {
key: "memoriumTheme", key: "memoriumTheme",
name: "Memorial", name: "Memorial",
palette: { palette: {
primary: red, primary: red,
secondary: red secondary: red
} }
}; };
export const blissTheme: HyperspaceTheme = { export const blissTheme: HyperspaceTheme = {
key: "blissTheme", key: "blissTheme",
name: "Bliss", name: "Bliss",
palette: { palette: {
primary: { primary: {
main: "#3e2723" main: "#3e2723"
}, },
secondary: lightBlue secondary: lightBlue
} }
}; };
export const attractTheme: HyperspaceTheme = { export const attractTheme: HyperspaceTheme = {
key: "attractTheme", key: "attractTheme",
name: "Attract", name: "Attract",
palette: { palette: {
primary: { primary: {
main: "#E57373" main: "#E57373"
}, },
secondary: { secondary: {
main: "#78909C" main: "#78909C"
}
} }
}
}; };
export const themes = [ export const themes = [
defaultTheme, defaultTheme,
gardenerTheme, gardenerTheme,
teacherTheme, teacherTheme,
jokerTheme, jokerTheme,
guardTheme, guardTheme,
entertainerTheme, entertainerTheme,
classicTheme, classicTheme,
dragonTheme, dragonTheme,
memoriumTheme, memoriumTheme,
blissTheme, blissTheme,
attractTheme attractTheme
]; ];

View File

@ -2,14 +2,14 @@ import { Field } from "./Field";
import { Account } from "./Account"; import { Account } from "./Account";
export type Instance = { export type Instance = {
uri: string; uri: string;
title: string; title: string;
description: string; description: string;
email: string; email: string;
version: string; version: string;
thumbnail: string | null; thumbnail: string | null;
urls: Field; urls: Field;
stats: Field; stats: Field;
languages: [string]; languages: [string];
contact_account: Account; contact_account: Account;
}; };

View File

@ -2,8 +2,8 @@
* Basic type for a person mentioned in a Status * Basic type for a person mentioned in a Status
*/ */
export type Mention = { export type Mention = {
url: string; url: string;
username: string; username: string;
acct: string; acct: string;
id: string; id: string;
}; };

View File

@ -2,9 +2,9 @@ import { Account } from "./Account";
import { Status } from "./Status"; import { Status } from "./Status";
export type Notification = { export type Notification = {
id: string; id: string;
type: "follow" | "mention" | "reblog" | "favourite"; type: "follow" | "mention" | "reblog" | "favourite";
created_at: string; created_at: string;
account: Account; account: Account;
status: Status | null; status: Status | null;
}; };

View File

@ -2,29 +2,29 @@
* Basic type for a Poll on Mastodon * Basic type for a Poll on Mastodon
*/ */
export type Poll = { export type Poll = {
id: string; id: string;
expires_at: string | null; expires_at: string | null;
expired: boolean; expired: boolean;
multiple: boolean; multiple: boolean;
votes_count: number; votes_count: number;
options: [PollOption]; options: [PollOption];
voted: boolean | null; voted: boolean | null;
}; };
/** /**
* Basic type for a Poll option in a Poll * Basic type for a Poll option in a Poll
*/ */
export type PollOption = { export type PollOption = {
title: string; title: string;
votes_count: number | null; votes_count: number | null;
}; };
export type PollWizard = { export type PollWizard = {
expires_at: string; expires_at: string;
multiple: boolean; multiple: boolean;
options: PollWizardOption[]; options: PollWizardOption[];
}; };
export type PollWizardOption = { export type PollWizardOption = {
title: string; title: string;
}; };

View File

@ -1,12 +1,12 @@
export type Relationship = { export type Relationship = {
id: string; id: string;
following: boolean; following: boolean;
followed_by: boolean; followed_by: boolean;
blocking: boolean; blocking: boolean;
muting: boolean; muting: boolean;
muting_notifications: boolean; muting_notifications: boolean;
requested: boolean; requested: boolean;
domain_blocking: boolean; domain_blocking: boolean;
showing_reblogs: boolean; showing_reblogs: boolean;
endorsed: boolean; endorsed: boolean;
}; };

View File

@ -3,7 +3,7 @@ import { Status } from "./Status";
import { Tag } from "./Tag"; import { Tag } from "./Tag";
export type Results = { export type Results = {
accounts: [Account]; accounts: [Account];
statuses: [Status]; statuses: [Status];
hashtags: [Tag]; hashtags: [Tag];
}; };

View File

@ -1,6 +1,6 @@
export type SaveClientSession = { export type SaveClientSession = {
clientId: string; clientId: string;
clientSecret: string; clientSecret: string;
authUrl: string; authUrl: string;
emergency: boolean; emergency: boolean;
}; };

View File

@ -11,30 +11,30 @@ import { Tag } from "./Tag";
* Basic type for a status on Mastodon * Basic type for a status on Mastodon
*/ */
export type Status = { export type Status = {
id: string; id: string;
uri: string; uri: string;
url: string | null; url: string | null;
account: Account; account: Account;
in_reply_to_id: string | null; in_reply_to_id: string | null;
in_reply_to_account_id: string | null; in_reply_to_account_id: string | null;
reblog: Status | null; reblog: Status | null;
content: string; content: string;
created_at: string; created_at: string;
emojis: [MastodonEmoji]; emojis: [MastodonEmoji];
replies_count: number; replies_count: number;
reblogs_count: number; reblogs_count: number;
favourites_count: number; favourites_count: number;
reblogged: boolean | null; reblogged: boolean | null;
favourited: boolean | null; favourited: boolean | null;
muted: boolean | null; muted: boolean | null;
sensitive: boolean; sensitive: boolean;
spoiler_text: string; spoiler_text: string;
visibility: Visibility; visibility: Visibility;
media_attachments: [Attachment]; media_attachments: [Attachment];
mentions: [Mention]; mentions: [Mention];
tags: [Tag]; tags: [Tag];
card: Card | null; card: Card | null;
poll: Poll | null; poll: Poll | null;
application: any; application: any;
pinned: boolean | null; pinned: boolean | null;
}; };

View File

@ -1,4 +1,4 @@
export type Tag = { export type Tag = {
name: string; name: string;
url: string; url: string;
}; };

View File

@ -1,30 +1,33 @@
import Mastodon from "megalodon"; import Mastodon from "megalodon";
export function userLoggedIn(): boolean { export function userLoggedIn(): boolean {
if (localStorage.getItem("baseurl") && localStorage.getItem("access_token")) { if (
return true; localStorage.getItem("baseurl") &&
} else { localStorage.getItem("access_token")
return false; ) {
} return true;
} else {
return false;
}
} }
export function refreshUserAccountData() { export function refreshUserAccountData() {
let client = new Mastodon( let client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1" (localStorage.getItem("baseurl") as string) + "/api/v1"
);
client
.get("/accounts/verify_credentials")
.then((resp: any) => {
localStorage.setItem("account", JSON.stringify(resp.data));
})
.catch((err: Error) => {
console.error(err.message);
});
client.get("/instance").then((resp: any) => {
localStorage.setItem(
"isPleroma",
resp.data.version.match(/Pleroma/) ? "true" : "false"
); );
}); client
.get("/accounts/verify_credentials")
.then((resp: any) => {
localStorage.setItem("account", JSON.stringify(resp.data));
})
.catch((err: Error) => {
console.error(err.message);
});
client.get("/instance").then((resp: any) => {
localStorage.setItem(
"isPleroma",
resp.data.version.match(/Pleroma/) ? "true" : "false"
);
});
} }

View File

@ -7,5 +7,5 @@ import { isDarwinApp } from "./desktop";
* @returns Boolean dictating if the title bar is visible * @returns Boolean dictating if the title bar is visible
*/ */
export function isAppbarExpanded(): boolean { export function isAppbarExpanded(): boolean {
return isDarwinApp() || process.env.NODE_ENV === "development"; return isDarwinApp() || process.env.NODE_ENV === "development";
} }

View File

@ -2,7 +2,7 @@
* A Window interface with extra properties for Electron * A Window interface with extra properties for Electron
*/ */
interface ElectronWindow extends Window { interface ElectronWindow extends Window {
require?: any; require?: any;
} }
/** /**
@ -10,22 +10,22 @@ interface ElectronWindow extends Window {
* @returns Boolean of whether it is in desktop mode or not * @returns Boolean of whether it is in desktop mode or not
*/ */
export function isDesktopApp(): boolean { export function isDesktopApp(): boolean {
return navigator.userAgent.includes("Hyperspace" || "Electron"); return navigator.userAgent.includes("Hyperspace" || "Electron");
} }
/** /**
* Determines whether the app is the macOS application * Determines whether the app is the macOS application
*/ */
export function isDarwinApp(): boolean { export function isDarwinApp(): boolean {
return isDesktopApp() && navigator.userAgent.includes("Macintosh"); return isDesktopApp() && navigator.userAgent.includes("Macintosh");
} }
/** /**
* Determine whether the system is in dark mode or not (macOS) * Determine whether the system is in dark mode or not (macOS)
*/ */
export function isDarkMode() { export function isDarkMode() {
// Lift window to an ElectronWindow and add use require() // Lift window to an ElectronWindow and add use require()
const eWin = window as ElectronWindow; const eWin = window as ElectronWindow;
const { remote } = eWin.require("electron"); const { remote } = eWin.require("electron");
return remote.systemPreferences.isDarkMode(); return remote.systemPreferences.isDarkMode();
} }

View File

@ -9,50 +9,50 @@ import Mastodon from "megalodon";
* @returns String with image tags for emojis * @returns String with image tags for emojis
*/ */
export function emojifyString( export function emojifyString(
contents: string, contents: string,
emojis: [MastodonEmoji], emojis: [MastodonEmoji],
className?: any className?: any
): string { ): string {
let newContents: string = contents; let newContents: string = contents;
emojis.forEach((emoji: MastodonEmoji) => { emojis.forEach((emoji: MastodonEmoji) => {
let filter = new RegExp(`:${emoji.shortcode}:`, "g"); let filter = new RegExp(`:${emoji.shortcode}:`, "g");
newContents = newContents.replace( newContents = newContents.replace(
filter, filter,
`<img src=${emoji.static_url} ${ `<img src=${emoji.static_url} ${
className ? `class="${className}"` : "" className ? `class="${className}"` : ""
}/>` }/>`
); );
}); });
return newContents; return newContents;
} }
export function collectEmojisFromServer() { export function collectEmojisFromServer() {
let client = new Mastodon( let client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1" localStorage.getItem("baseurl") + "/api/v1"
); );
let emojisPath = localStorage.getItem("emojis"); let emojisPath = localStorage.getItem("emojis");
let emojis: any[] = []; let emojis: any[] = [];
if (emojisPath === null) { if (emojisPath === null) {
client client
.get("/custom_emojis") .get("/custom_emojis")
.then((resp: any) => { .then((resp: any) => {
resp.data.forEach((emoji: MastodonEmoji) => { resp.data.forEach((emoji: MastodonEmoji) => {
let customEmoji = { let customEmoji = {
name: emoji.shortcode, name: emoji.shortcode,
emoticons: [""], emoticons: [""],
short_names: [emoji.shortcode], short_names: [emoji.shortcode],
imageUrl: emoji.static_url, imageUrl: emoji.static_url,
keywords: ["mastodon", "custom"] keywords: ["mastodon", "custom"]
}; };
emojis.push(customEmoji); emojis.push(customEmoji);
localStorage.setItem("emojis", JSON.stringify(emojis)); localStorage.setItem("emojis", JSON.stringify(emojis));
}); });
}) })
.catch((err: Error) => { .catch((err: Error) => {
console.error(err.message); console.error(err.message);
}); });
} }
} }

View File

@ -55,13 +55,3 @@ export function getRedirectAddress(
return type; return type;
} }
} }
/**
* Determine whether a base URL is in the 'disallowed' domains section.
* @param domain The URL to test
* @returns Boolean dictating the URL's presence in disallowed domains
*/
export function inDisallowedDomains(domain: string): boolean {
let disallowed = ["gab.com"];
return disallowed.includes(domain);
}

View File

@ -4,22 +4,22 @@ import { getUserDefaultBool, setUserDefaultBool } from "./settings";
* Get the person's permission to send notification requests. * Get the person's permission to send notification requests.
*/ */
export function getNotificationRequestPermission() { export function getNotificationRequestPermission() {
if ("Notification" in window) { if ("Notification" in window) {
Notification.requestPermission(); Notification.requestPermission();
let request = Notification.permission; let request = Notification.permission;
if (request === "granted") { if (request === "granted") {
setUserDefaultBool("enablePushNotifications", true); setUserDefaultBool("enablePushNotifications", true);
setUserDefaultBool("userDeniedNotification", false); setUserDefaultBool("userDeniedNotification", false);
} else {
setUserDefaultBool("enablePushNotifications", false);
setUserDefaultBool("userDeniedNotification", true);
}
} else { } else {
setUserDefaultBool("enablePushNotifications", false); console.warn(
setUserDefaultBool("userDeniedNotification", true); "Notifications aren't supported in this browser. The setting will be disabled."
);
setUserDefaultBool("enablePushNotifications", false);
} }
} else {
console.warn(
"Notifications aren't supported in this browser. The setting will be disabled."
);
setUserDefaultBool("enablePushNotifications", false);
}
} }
/** /**
@ -27,7 +27,7 @@ export function getNotificationRequestPermission() {
* @returns Boolean value that determines whether the browser supports the Notification API * @returns Boolean value that determines whether the browser supports the Notification API
*/ */
export function browserSupportsNotificationRequests(): boolean { export function browserSupportsNotificationRequests(): boolean {
return "Notification" in window; return "Notification" in window;
} }
/** /**
@ -35,7 +35,7 @@ export function browserSupportsNotificationRequests(): boolean {
* @returns Boolean value of `enablePushNotifications` * @returns Boolean value of `enablePushNotifications`
*/ */
export function canSendNotifications() { export function canSendNotifications() {
return getUserDefaultBool("enablePushNotifications"); return getUserDefaultBool("enablePushNotifications");
} }
/** /**
@ -44,15 +44,15 @@ export function canSendNotifications() {
* @param body The contents of the push notification * @param body The contents of the push notification
*/ */
export function sendNotificationRequest(title: string, body: string) { export function sendNotificationRequest(title: string, body: string) {
if (canSendNotifications()) { if (canSendNotifications()) {
let notif = new Notification(title, { let notif = new Notification(title, {
body: body body: body
}); });
notif.onclick = () => { notif.onclick = () => {
window.focus(); window.focus();
}; };
} else { } else {
console.warn("The person has opted to not receive push notifications."); console.warn("The person has opted to not receive push notifications.");
} }
} }

View File

@ -5,13 +5,13 @@ import { Config } from "../types/Config";
import { Visibility } from "../types/Visibility"; import { Visibility } from "../types/Visibility";
type SettingsTemplate = { type SettingsTemplate = {
[key: string]: any; [key: string]: any;
darkModeEnabled: boolean; darkModeEnabled: boolean;
systemDecidesDarkMode: boolean; systemDecidesDarkMode: boolean;
enablePushNotifications: boolean; enablePushNotifications: boolean;
clearNotificationsOnRead: boolean; clearNotificationsOnRead: boolean;
displayAllOnNotificationBadge: boolean; displayAllOnNotificationBadge: boolean;
defaultVisibility: string; defaultVisibility: string;
}; };
/** /**
@ -20,14 +20,14 @@ type SettingsTemplate = {
* @returns The boolean value associated with the key * @returns The boolean value associated with the key
*/ */
export function getUserDefaultBool(key: string): boolean { export function getUserDefaultBool(key: string): boolean {
if (localStorage.getItem(key) === null) { if (localStorage.getItem(key) === null) {
console.warn( console.warn(
"This key has not been set before, so the default value is FALSE for now." "This key has not been set before, so the default value is FALSE for now."
); );
return false; return false;
} else { } else {
return localStorage.getItem(key) === "true"; return localStorage.getItem(key) === "true";
} }
} }
/** /**
@ -36,10 +36,10 @@ export function getUserDefaultBool(key: string): boolean {
* @param value The boolean value for the key * @param value The boolean value for the key
*/ */
export function setUserDefaultBool(key: string, value: boolean) { export function setUserDefaultBool(key: string, value: boolean) {
if (localStorage.getItem(key) === null) { if (localStorage.getItem(key) === null) {
console.warn("This key has not been set before."); console.warn("This key has not been set before.");
} }
localStorage.setItem(key, value.toString()); localStorage.setItem(key, value.toString());
} }
/** /**
@ -47,14 +47,14 @@ export function setUserDefaultBool(key: string, value: boolean) {
* @returns The Visibility value associated with the key * @returns The Visibility value associated with the key
*/ */
export function getUserDefaultVisibility(): Visibility { export function getUserDefaultVisibility(): Visibility {
if (localStorage.getItem("defaultVisibility") === null) { if (localStorage.getItem("defaultVisibility") === null) {
console.warn( console.warn(
"This key has not been set before, so the default value is PUBLIC for now." "This key has not been set before, so the default value is PUBLIC for now."
); );
return "public"; return "public";
} else { } else {
return localStorage.getItem("defaultVisibility") as Visibility; return localStorage.getItem("defaultVisibility") as Visibility;
} }
} }
/** /**
@ -62,23 +62,23 @@ export function getUserDefaultVisibility(): Visibility {
* @param key The settings key in localStorage to change * @param key The settings key in localStorage to change
*/ */
export function setUserDefaultVisibility(key: string) { export function setUserDefaultVisibility(key: string) {
if (localStorage.getItem("defaultVisibility") === null) { if (localStorage.getItem("defaultVisibility") === null) {
console.warn("This key has not been set before."); console.warn("This key has not been set before.");
} }
localStorage.setItem("defaultVisibility", key.toString()); localStorage.setItem("defaultVisibility", key.toString());
} }
/** /**
* Gets the user's default theme or the default theme * Gets the user's default theme or the default theme
*/ */
export function getUserDefaultTheme() { export function getUserDefaultTheme() {
let returnTheme = defaultTheme; let returnTheme = defaultTheme;
themes.forEach(theme => { themes.forEach(theme => {
if (theme.key === localStorage.getItem("theme")) { if (theme.key === localStorage.getItem("theme")) {
returnTheme = theme; returnTheme = theme;
} }
}); });
return returnTheme; return returnTheme;
} }
/** /**
@ -86,42 +86,42 @@ export function getUserDefaultTheme() {
* @param themeName The name of the theme * @param themeName The name of the theme
*/ */
export function setUserDefaultTheme(themeName: string) { export function setUserDefaultTheme(themeName: string) {
localStorage.setItem("theme", themeName); localStorage.setItem("theme", themeName);
} }
/** /**
* Creates the user defaults if they do not exist already. * Creates the user defaults if they do not exist already.
*/ */
export function createUserDefaults() { export function createUserDefaults() {
let defaults: SettingsTemplate = { let defaults: SettingsTemplate = {
darkModeEnabled: false, darkModeEnabled: false,
systemDecidesDarkMode: true, systemDecidesDarkMode: true,
enablePushNotifications: true, enablePushNotifications: true,
clearNotificationsOnRead: false, clearNotificationsOnRead: false,
displayAllOnNotificationBadge: false, displayAllOnNotificationBadge: false,
defaultVisibility: "public" defaultVisibility: "public"
}; };
let settings = [ let settings = [
"darkModeEnabled", "darkModeEnabled",
"systemDecidesDarkMode", "systemDecidesDarkMode",
"clearNotificationsOnRead", "clearNotificationsOnRead",
"displayAllOnNotificationBadge", "displayAllOnNotificationBadge",
"defaultVisibility" "defaultVisibility"
]; ];
migrateExistingSettings(); migrateExistingSettings();
settings.forEach((setting: string) => { settings.forEach((setting: string) => {
if (localStorage.getItem(setting) === null) { if (localStorage.getItem(setting) === null) {
if (typeof defaults[setting] === "boolean") { if (typeof defaults[setting] === "boolean") {
setUserDefaultBool(setting, defaults[setting]); setUserDefaultBool(setting, defaults[setting]);
} else { } else {
localStorage.setItem(setting, defaults[setting].toString()); localStorage.setItem(setting, defaults[setting].toString());
} }
} }
}); });
getNotificationRequestPermission(); getNotificationRequestPermission();
} }
/** /**
@ -129,22 +129,23 @@ export function createUserDefaults() {
* @returns The Promise data from getting the config. * @returns The Promise data from getting the config.
*/ */
export async function getConfig(): Promise<Config | undefined> { export async function getConfig(): Promise<Config | undefined> {
try { try {
const resp = await axios.get("config.json"); const resp = await axios.get("config.json");
let config: Config = resp.data; let config: Config = resp.data;
return config; return config;
} catch (err) { } catch (err) {
console.error( console.error(
"Couldn't configure Hyperspace with the config file. Reason: " + err.name "Couldn't configure Hyperspace with the config file. Reason: " +
); err.name
} );
}
} }
export function migrateExistingSettings() { export function migrateExistingSettings() {
if (localStorage.getItem("prefers-dark-mode")) { if (localStorage.getItem("prefers-dark-mode")) {
setUserDefaultBool( setUserDefaultBool(
"darkModeEnabled", "darkModeEnabled",
localStorage.getItem("prefers-dark-mode") === "true" localStorage.getItem("prefers-dark-mode") === "true"
); );
} }
} }

View File

@ -1,8 +1,8 @@
import { createMuiTheme, Theme } from "@material-ui/core"; import { createMuiTheme, Theme } from "@material-ui/core";
import { import {
HyperspaceTheme, HyperspaceTheme,
themes, themes,
defaultTheme defaultTheme
} from "../types/HyperspaceTheme"; } from "../types/HyperspaceTheme";
import { getUserDefaultBool } from "./settings"; import { getUserDefaultBool } from "./settings";
import { isDarwinApp, isDarkMode } from "./desktop"; import { isDarwinApp, isDarkMode } from "./desktop";
@ -13,13 +13,13 @@ import { isDarwinApp, isDarkMode } from "./desktop";
* @returns Hyperspace theme with name or the default * @returns Hyperspace theme with name or the default
*/ */
export function getHyperspaceTheme(name: string): HyperspaceTheme { export function getHyperspaceTheme(name: string): HyperspaceTheme {
let theme: HyperspaceTheme = defaultTheme; let theme: HyperspaceTheme = defaultTheme;
themes.forEach((themeItem: HyperspaceTheme) => { themes.forEach((themeItem: HyperspaceTheme) => {
if (themeItem.key === name) { if (themeItem.key === name) {
theme = themeItem; theme = themeItem;
} }
}); });
return theme; return theme;
} }
/** /**
@ -28,48 +28,48 @@ export function getHyperspaceTheme(name: string): HyperspaceTheme {
* @returns A Material-UI theme with the Hyperspace theme's palette colors * @returns A Material-UI theme with the Hyperspace theme's palette colors
*/ */
export function setHyperspaceTheme(theme: HyperspaceTheme): Theme { export function setHyperspaceTheme(theme: HyperspaceTheme): Theme {
return createMuiTheme({ return createMuiTheme({
typography: { typography: {
fontFamily: [ fontFamily: [
"-apple-system", "-apple-system",
"BlinkMacSystemFont", "BlinkMacSystemFont",
'"Segoe UI"', '"Segoe UI"',
"Roboto", "Roboto",
'"Helvetica Neue"', '"Helvetica Neue"',
"Arial", "Arial",
"sans-serif", "sans-serif",
'"Apple Color Emoji"', '"Apple Color Emoji"',
'"Segoe UI Emoji"', '"Segoe UI Emoji"',
'"Segoe UI Symbol"' '"Segoe UI Symbol"'
].join(","), ].join(","),
useNextVariants: true useNextVariants: true
}, },
palette: { palette: {
primary: theme.palette.primary, primary: theme.palette.primary,
secondary: theme.palette.secondary, secondary: theme.palette.secondary,
type: getUserDefaultBool("darkModeEnabled") type: getUserDefaultBool("darkModeEnabled")
? "dark" ? "dark"
: getDarkModeFromSystem() === "dark" : getDarkModeFromSystem() === "dark"
? "dark" ? "dark"
: "light" : "light"
} }
}); });
} }
export function getDarkModeFromSystem(): string { export function getDarkModeFromSystem(): string {
if (getUserDefaultBool("systemDecidesDarkMode")) { if (getUserDefaultBool("systemDecidesDarkMode")) {
if (window.matchMedia("(prefers-color-scheme: dark)").matches) { if (window.matchMedia("(prefers-color-scheme: dark)").matches) {
return "dark"; return "dark";
} else {
if (isDarwinApp()) {
return isDarkMode() ? "dark" : "light";
} else {
return "light";
}
}
} else { } else {
if (isDarwinApp()) {
return isDarkMode() ? "dark" : "light";
} else {
return "light"; return "light";
}
} }
} else {
return "light";
}
} }
/** /**
@ -78,10 +78,10 @@ export function getDarkModeFromSystem(): string {
* @param setting Whether dark mode should be on (`true`) or off (`false`) * @param setting Whether dark mode should be on (`true`) or off (`false`)
*/ */
export function darkMode(theme: Theme, setting: boolean): Theme { export function darkMode(theme: Theme, setting: boolean): Theme {
if (setting) { if (setting) {
theme.palette.type = "dark"; theme.palette.type = "dark";
} else { } else {
theme.palette.type = "light"; theme.palette.type = "light";
} }
return theme; return theme;
} }