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/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": {
"version": "4.7.2",
"resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.2.tgz",
@ -1491,12 +1497,13 @@
"@types/unist": "*"
}
},
"@types/ws": {
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-6.0.3.tgz",
"integrity": "sha512-yBTM0P05Tx9iXGq00BbJPo37ox68R5vaGTXivs6RGh/BQ6QP5zqZDGWdAO6JbRE/iR1l80xeGAwCQS2nMV9S/w==",
"@types/websocket": {
"version": "0.0.40",
"resolved": "https://registry.npmjs.org/@types/websocket/-/websocket-0.0.40.tgz",
"integrity": "sha512-ldteZwWIgl9cOy7FyvYn+39Ah4+PfpVE72eYKw75iy2L0zTbhbcwvzeJ5IOu6DQP93bjfXq0NGHY6FYtmYoqFQ==",
"dev": true,
"requires": {
"@types/events": "*",
"@types/node": "*"
}
},
@ -12191,19 +12198,19 @@
"dev": true
},
"megalodon": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/megalodon/-/megalodon-1.0.3.tgz",
"integrity": "sha512-RcJT3HRWCXQcE5ZQUpLEjJ+HgWvwoTpXr4XLUf0tWrtmxrDnIW43pOASDb3G7MOBKYonzM1pMfAmR2Yfh3Qw/g==",
"version": "0.6.4",
"resolved": "https://registry.npmjs.org/megalodon/-/megalodon-0.6.4.tgz",
"integrity": "sha512-WGYhcSxGYlBwZSm5VebxLqnbpPemum9/6lJUi1HBsVzF5jXc9fdumhXH0vqGhWdovdqRT86iXBDJl5SwUrbr2A==",
"dev": true,
"requires": {
"@types/oauth": "^0.9.0",
"@types/request": "^2.47.0",
"@types/ws": "^6.0.1",
"axios": "^0.18.1",
"@types/websocket": "0.0.40",
"axios": "^0.18.0",
"oauth": "^0.9.15",
"request": "^2.87.0",
"typescript": "^3.4.5",
"ws": "^7.0.1"
"typescript": "^2.9.1",
"websocket": "^1.0.28"
},
"dependencies": {
"axios": {
@ -12247,19 +12254,10 @@
"dev": true
},
"typescript": {
"version": "3.6.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.6.3.tgz",
"integrity": "sha512-N7bceJL1CtRQ2RiG0AQME13ksR7DiuQh/QehubYcghzv20tnh+MQnQIuJddTmsbqYj+dztchykemz0zFzlvdQw==",
"version": "2.9.2",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz",
"integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==",
"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=",
"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": {
"version": "3.4.1",
"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": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.0.tgz",
@ -21368,6 +21410,12 @@
"integrity": "sha1-bRX7qITAhnnA136I53WegR4H+kE=",
"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": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz",

View File

@ -25,10 +25,10 @@
"file-dialog": "^0.0.7",
"material-ui-pickers": "^2.2.4",
"mdi-material-ui": "^5.13.0",
"megalodon": "^1.0.3",
"megalodon": "^0.6.4",
"moment": "^2.24.0",
"notistack": "^0.5.1",
"prettier": "^1.18.2",
"prettier": "1.18.2",
"query-string": "^6.8.2",
"react": "^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";
export const styles = (theme: Theme) =>
createStyles({
root: {
width: "100%",
display: "flex",
height: "100%",
minHeight: "100vh",
backgroundColor: isDarwinApp()
? "transparent"
: theme.palette.background.default
},
content: {
marginTop: 72,
flexGrow: 1,
padding: theme.spacing.unit * 3,
[theme.breakpoints.up("md")]: {
marginLeft: 250,
marginTop: 88
}
}
});
createStyles({
root: {
width: "100%",
display: "flex",
height: "100%",
minHeight: "100vh",
backgroundColor: isDarwinApp()
? "transparent"
: theme.palette.background.default
},
content: {
marginTop: 72,
flexGrow: 1,
padding: theme.spacing.unit * 3,
[theme.breakpoints.up("md")]: {
marginLeft: 250,
marginTop: 88
}
}
});

View File

@ -20,7 +20,6 @@ import MessagesPage from "./pages/Messages";
import RecommendationsPage from "./pages/Recommendations";
import Missingno from "./pages/Missingno";
import You from "./pages/You";
import Blocked from "./pages/Blocked";
import { withSnackbar } from "notistack";
import { PrivateRoute } from "./interfaces/overrides";
import { userLoggedIn } from "./utilities/accounts";
@ -57,8 +56,6 @@ class App extends Component<any, any> {
removeBodyBackground() {
if (isDarwinApp()) {
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}
/>
<PrivateRoute path="/search" component={SearchPage} />
<PrivateRoute path="/blocked" component={Blocked} />
<PrivateRoute path="/settings" component={Settings} />
<PrivateRoute path="/you" component={You} />
<PrivateRoute path="/about" component={AboutPage} />
<PrivateRoute path="/compose" component={Composer} />

View File

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

View File

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

View File

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

View File

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

View File

@ -1,99 +1,99 @@
import { Theme, createStyles } from "@material-ui/core";
export const styles = (theme: Theme) =>
createStyles({
post: {
marginTop: theme.spacing.unit,
marginBottom: theme.spacing.unit
},
postReblogChip: {
color: theme.palette.common.white,
"&:hover": {
backgroundColor: theme.palette.secondary.light
},
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"
createStyles({
post: {
marginTop: theme.spacing.unit,
marginBottom: theme.spacing.unit
},
"&.u-url.mention": {
textDecoration: "none",
color: "inherit",
fontWeight: "bold"
postReblogChip: {
color: theme.palette.common.white,
"&:hover": {
backgroundColor: theme.palette.secondary.light
},
backgroundColor: theme.palette.secondary.main,
marginBottom: theme.spacing.unit
},
"&.mention.hashtag": {
textDecoration: "none",
color: "inherit",
fontWeight: "bold"
postContent: {
paddingTop: 0,
paddingBottom: 0,
"& 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";
export interface OwnProps {
style: object;
style: object;
}
const ShareMenu: React.FunctionComponent<WebShareInterface & OwnProps> = ({
share,
isSupported,
style
share,
isSupported,
style
}) =>
isSupported ? (
<MenuItem onClick={share} style={style}>
Share
</MenuItem>
) : null;
isSupported ? (
<MenuItem onClick={share} style={style}>
Share
</MenuItem>
) : null;
export default webShare<OwnProps>()(ShareMenu);

View File

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

View File

@ -9,31 +9,31 @@ import { SnackbarProvider } from "notistack";
import { userLoggedIn, refreshUserAccountData } from "./utilities/accounts";
getConfig()
.then((config: any) => {
document.title = config.branding.name || "Hyperspace";
})
.catch((err: Error) => {
console.error(err);
});
.then((config: any) => {
document.title = config.branding.name || "Hyperspace";
})
.catch((err: Error) => {
console.error(err);
});
createUserDefaults();
if (userLoggedIn()) {
collectEmojisFromServer();
refreshUserAccountData();
collectEmojisFromServer();
refreshUserAccountData();
}
ReactDOM.render(
<HashRouter>
<SnackbarProvider
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
>
<App />
</SnackbarProvider>
</HashRouter>,
document.getElementById("root")
<HashRouter>
<SnackbarProvider
anchorOrigin={{
vertical: "bottom",
horizontal: "left"
}}
>
<App />
</SnackbarProvider>
</HashRouter>,
document.getElementById("root")
);
// 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";
export interface ILinkableListItemProps extends ListItemProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableIconButtonProps extends IconButtonProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableChipProps extends ChipProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableMenuItemProps extends MenuItemProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableButtonProps extends ButtonProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableFabProps extends FabProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export interface ILinkableAvatarProps extends AvatarProps {
to: string;
replace?: boolean;
to: string;
replace?: boolean;
}
export const LinkableListItem = (props: ILinkableListItemProps) => (
<ListItem {...props} component={Link as any} />
<ListItem {...props} component={Link as any} />
);
export const LinkableIconButton = (props: ILinkableIconButtonProps) => (
<IconButton {...props} component={Link as any} />
<IconButton {...props} component={Link as any} />
);
export const LinkableChip = (props: ILinkableChipProps) => (
<Chip {...props} component={Link as any} />
<Chip {...props} component={Link as any} />
);
export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
<MenuItem {...props} component={Link as any} />
<MenuItem {...props} component={Link as any} />
);
export const LinkableButton = (props: ILinkableButtonProps) => (
<Button {...props} component={Link as any} />
<Button {...props} component={Link as any} />
);
export const LinkableFab = (props: ILinkableFabProps) => (
<Fab {...props} component={Link as any} />
<Fab {...props} component={Link as any} />
);
export const LinkableAvatar = (props: ILinkableAvatarProps) => (
<Avatar {...props} component={Link as any} />
<Avatar {...props} component={Link as any} />
);
export const ProfileRoute = (rest: any, component: Component) => (
<Route {...rest} render={props => <Component {...props} />} />
<Route {...rest} render={props => <Component {...props} />} />
);
export const PrivateRoute = (props: IPrivateRouteProps) => {
const { component, render, ...rest } = props;
return (
<Route
{...rest}
render={(compProps: any) =>
userLoggedIn() ? (
React.createElement(component, compProps)
) : (
<Redirect to="/welcome" />
)
}
/>
);
const { component, render, ...rest } = props;
return (
<Route
{...rest}
render={(compProps: any) =>
userLoggedIn() ? (
React.createElement(component, compProps)
) : (
<Redirect to="/welcome" />
)
}
/>
);
};
interface IPrivateRouteProps extends RouteProps {
component: any;
component: any;
}

View File

@ -110,6 +110,135 @@ class AboutPage extends Component<any, IAboutPageState> {
return (
<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>
<div
className={classes.instanceHeaderPaper}
@ -130,13 +259,10 @@ class AboutPage extends Component<any, IAboutPageState> {
>
<OpenInNewIcon />
</IconButton>
<Typography
className={classes.instanceHeaderText}
variant="h4"
component="p"
>
{this.state.instance ? this.state.instance.uri : "Loading..."}
</Typography>
<div className={classes.instanceHeaderText}>
<Typography variant="h4" component="p">{this.state.instance ? this.state.instance.uri: "Loading..."}</Typography>
<Typography>Server version {this.state.instance? this.state.instance.version: "x.x.x"}</Typography>
</div>
</div>
<List className={classes.pageListConstraints}>
{localStorage["isPleroma"] == "false" && (
@ -238,168 +364,9 @@ class AboutPage extends Component<any, IAboutPageState> {
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<Avatar>
<MastodonIcon />
</Avatar>
</ListItemAvatar>
<ListItemText
primary="Mastodon version"
secondary={
this.state.instance ? this.state.instance.version : "x.x.x"
}
/>
</ListItem>
</List>
</Paper>
<br />
<Paper>
<div
className={classes.instanceHeaderPaper}
style={{
backgroundImage: `url("${
this.state.brandBg ? this.state.brandBg : ""
}")`
}}
>
<div className={classes.instanceToolbar}>
{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 />
<ListSubheader>Federation status</ListSubheader>
<Paper>

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@ -1,13 +1,13 @@
import React, { Component } from "react";
import {
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
} from "@material-ui/core";
import { styles } from "./PageLayout.styles";
import Post from "../components/Post";
@ -17,209 +17,224 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface IHomePageState {
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
}
class HomePage extends Component<any, IHomePageState> {
client: Mastodon;
streamListener: StreamListener;
client: Mastodon;
streamListener: StreamListener;
constructor(props: any) {
super(props);
constructor(props: any) {
super(props);
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/user");
}
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 });
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/user");
}
}
loadMoreTimelinePieces() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/home", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
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", () => {});
}
}
render() {
const { classes } = this.props;
componentWillUnmount() {
this.streamListener.stop();
}
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>
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() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/home", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
});
}
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutMaxConstraints}>
{this.state.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>
) : 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));

View File

@ -1,13 +1,13 @@
import React, { Component } from "react";
import {
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
} from "@material-ui/core";
import { styles } from "./PageLayout.styles";
import Post from "../components/Post";
@ -17,210 +17,225 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface ILocalPageState {
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
}
class LocalPage extends Component<any, ILocalPageState> {
client: Mastodon;
streamListener: StreamListener;
client: Mastodon;
streamListener: StreamListener;
constructor(props: any) {
super(props);
constructor(props: any) {
super(props);
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/public/local");
}
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 });
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/public/local");
}
}
loadMoreTimelinePieces() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/public", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20,
local: true
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
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", () => {});
}
}
render() {
const { classes } = this.props;
componentWillUnmount() {
this.streamListener.stop();
}
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>
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() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/public", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20,
local: true
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
});
}
}
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>
) : 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));

View File

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

View File

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

View File

@ -1,23 +1,23 @@
import React, { Component } from "react";
import {
List,
ListItem,
ListItemText,
ListSubheader,
ListItemSecondaryAction,
ListItemAvatar,
Paper,
IconButton,
withStyles,
Typography,
CircularProgress,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Tooltip
List,
ListItem,
ListItemText,
ListSubheader,
ListItemSecondaryAction,
ListItemAvatar,
Paper,
IconButton,
withStyles,
Typography,
CircularProgress,
Button,
Dialog,
DialogTitle,
DialogContent,
DialogContentText,
DialogActions,
Tooltip
} from "@material-ui/core";
import AssignmentIndIcon from "@material-ui/icons/AssignmentInd";
import PersonIcon from "@material-ui/icons/Person";
@ -33,344 +33,380 @@ import { Account } from "../types/Account";
import { withSnackbar } from "notistack";
interface INotificationsPageState {
notifications?: [Notification];
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: string;
deleteDialogOpen: boolean;
notifications?: [Notification];
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: string;
deleteDialogOpen: boolean;
}
class NotificationsPage extends Component<any, INotificationsPageState> {
client: Mastodon;
streamListener: any;
client: Mastodon;
streamListener: any;
constructor(props: any) {
super(props);
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1"
);
this.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"
}
constructor(props: any) {
super(props);
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1"
);
});
}
removeAllNotifications() {
this.client
.post("/notifications/clear")
.then((resp: any) => {
this.setState({ notifications: undefined });
this.props.enqueueSnackbar("All notifications deleted.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't delete notifications: " + err.name,
{
variant: "error"
}
);
});
}
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;
this.state = {
viewIsLoading: true,
deleteDialogOpen: false
};
}
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"
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 });
}
});
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);
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"
}
)}
</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>
);
}
removeAllNotifications() {
this.client
.post("/notifications/clear")
.then((resp: any) => {
this.setState({ notifications: undefined });
this.props.enqueueSnackbar("All notifications deleted.");
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't delete notifications: " + err.name,
{
variant: "error"
}
);
});
}
createNotification(notif: Notification) {
const { classes } = this.props;
let primary = "";
let secondary = "";
switch (notif.type) {
case "follow":
primary = `${notif.account.display_name ||
notif.account.username} is now following you!`;
break;
case "mention":
primary = `${notif.account.display_name ||
notif.account.username} mentioned you in a post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
case "reblog":
primary = `${notif.account.display_name ||
notif.account.username} reblogged your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
case "favourite":
primary = `${notif.account.display_name ||
notif.account.username} favorited your post.`;
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
break;
default:
if (notif.status && notif.status.poll) {
primary = "A poll you voted in or created has ended.";
secondary = this.removeHTMLContent(
notif.status ? notif.status.content : ""
);
} else {
primary = "A magical thing happened!";
}
break;
}
return (
<ListItem key={notif.id}>
<ListItemAvatar>
<LinkableAvatar
alt={notif.account.username}
src={notif.account.avatar_static}
to={`/profile/${notif.account.id}`}
>
<PersonIcon />
</LinkableAvatar>
</ListItemAvatar>
<ListItemText
primary={primary}
secondary={
<span>
<Typography
color="textSecondary"
className={classes.mobileOnly}
>
{secondary.slice(0, 35) + "..."}
</Typography>
<Typography
color="textSecondary"
className={classes.desktopOnly}
>
{secondary}
</Typography>
</span>
}
/>
<ListItemSecondaryAction>
{notif.type === "follow" ? (
<span>
<Tooltip title="View profile">
<LinkableIconButton
to={`/profile/${notif.account.id}`}
>
<AssignmentIndIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Follow account">
<IconButton
onClick={() =>
this.followMember(notif.account)
}
>
<PersonAddIcon />
</IconButton>
</Tooltip>
</span>
) : notif.status ? (
<span>
<Tooltip title="View conversation">
<LinkableIconButton
to={`/conversation/${notif.status.id}`}
>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
{notif.type === "mention" ? (
<Tooltip title="Reply">
<LinkableIconButton
to={`/compose?reply=${
notif.status.reblog
? notif.status.reblog.id
: notif.status.id
}&visibility=${
notif.status.visibility
}&acct=${
notif.status.reblog
? notif.status.reblog.account
.acct
: notif.status.account.acct
}`}
>
<ReplyIcon />
</LinkableIconButton>
</Tooltip>
) : null}
</span>
) : null}
<Tooltip title="Remove notification">
<IconButton
onClick={() => this.removeNotification(notif.id)}
>
<DeleteIcon />
</IconButton>
</Tooltip>
</ListItemSecondaryAction>
</ListItem>
);
}
followMember(acct: Account) {
this.client
.post(`/accounts/${acct.id}/follow`)
.then((resp: any) => {
this.props.enqueueSnackbar(
"You are now following this account."
);
})
.catch((err: Error) => {
this.props.enqueueSnackbar(
"Couldn't follow account: " + err.name,
{ variant: "error" }
);
console.error(err.message);
});
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutConstraints}>
{this.state.viewDidLoad ? (
this.state.notifications &&
this.state.notifications.length > 0 ? (
<div>
<ListSubheader>Recent notifications</ListSubheader>
<Button
className={classes.clearAllButton}
variant="text"
onClick={() => this.toggleDeleteDialog()}
>
{" "}
Clear All
</Button>
<Paper className={classes.pageListConstraints}>
<List>
{this.state.notifications.map(
(notification: Notification) => {
return this.createNotification(
notification
);
}
)}
</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));

View File

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

View File

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

View File

@ -1,13 +1,13 @@
import React, { Component } from "react";
import {
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
withStyles,
CircularProgress,
Typography,
Paper,
Button,
Chip,
Avatar,
Slide
} from "@material-ui/core";
import { styles } from "./PageLayout.styles";
import Post from "../components/Post";
@ -17,209 +17,224 @@ import { withSnackbar } from "notistack";
import ArrowUpwardIcon from "@material-ui/icons/ArrowUpward";
interface IPublicPageState {
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
posts?: [Status];
backlogPosts?: [Status] | null;
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
}
class PublicPage extends Component<any, IPublicPageState> {
client: Mastodon;
streamListener: StreamListener;
client: Mastodon;
streamListener: StreamListener;
constructor(props: any) {
super(props);
constructor(props: any) {
super(props);
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.state = {
viewIsLoading: true,
backlogPosts: null
};
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/public");
}
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 });
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.streamListener = this.client.stream("/streaming/public");
}
}
loadMoreTimelinePieces() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/public", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
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", () => {});
}
}
render() {
const { classes } = this.props;
componentWillUnmount() {
this.streamListener.stop();
}
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>
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() {
this.setState({ viewDidLoad: false, viewIsLoading: true });
if (this.state.posts) {
this.client
.get("/timelines/public", {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
})
.then((resp: any) => {
let newPosts: [Status] = resp.data;
let posts = this.state.posts as [Status];
newPosts.forEach((post: Status) => {
posts.push(post);
});
this.setState({
viewIsLoading: false,
viewDidLoad: true,
posts
});
})
.catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
});
this.props.enqueueSnackbar("Failed to get posts", {
variant: "error"
});
});
}
}
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>
) : 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));

View File

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

View File

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

View File

@ -46,7 +46,6 @@ import {
} from "../utilities/themes";
import { Visibility } from "../types/Visibility";
import { LinkableButton } from "../interfaces/overrides";
import { Config } from "../types/Config";
import OpenInNewIcon from "@material-ui/icons/OpenInNew";
import DevicesIcon from "@material-ui/icons/Devices";
@ -59,7 +58,7 @@ import NotificationsIcon from "@material-ui/icons/Notifications";
import BellAlertIcon from "mdi-material-ui/BellAlert";
import RefreshIcon from "@material-ui/icons/Refresh";
import UndoIcon from "@material-ui/icons/Undo";
import DomainDisablbedIcon from "@material-ui/icons/DomainDisabled";
import { Config } from "../types/Config";
interface ISettingsState {
darkModeEnabled: boolean;
@ -500,20 +499,6 @@ class SettingsPage extends Component<any, ISettingsState> {
<LinkableButton to="/you">Edit</LinkableButton>
</ListItemSecondaryAction>
</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>
<ListItemAvatar>
<MastodonIcon color="action" />

View File

@ -17,11 +17,7 @@ import {
import { styles } from "./WelcomePage.styles";
import Mastodon from "megalodon";
import { SaveClientSession } from "../types/SessionData";
import {
createHyperspaceApp,
getRedirectAddress,
inDisallowedDomains
} from "../utilities/login";
import { createHyperspaceApp, getRedirectAddress } from "../utilities/login";
import { parseUrl } from "query-string";
import { getConfig } from "../utilities/settings";
import { isDarwinApp } from "../utilities/desktop";
@ -83,17 +79,9 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
let config: Config = result;
if (result.location === "dynamic") {
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({
logoUrl: config.branding
? result.branding.logo
@ -322,30 +310,18 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
if (this.state.user.includes("@")) {
if (this.state.federates && this.state.federates === true) {
let baseUrl = this.state.user.split("@")[1];
if (inDisallowedDomains(baseUrl)) {
this.setState({
userInputError: true,
userInputErrorMessage: `Signing in with an account from ${baseUrl} isn't supported.`
});
return true;
} else {
axios
.get(
"https://" +
baseUrl +
"/api/v1/timelines/public"
)
.catch((err: Error) => {
let userInputError = true;
let userInputErrorMessage =
"Instance name is invalid.";
this.setState({
userInputError,
userInputErrorMessage
});
return true;
axios
.get("https://" + baseUrl + "/api/v1/timelines/public")
.catch((err: Error) => {
let userInputError = true;
let userInputErrorMessage =
"Instance name is invalid.";
this.setState({
userInputError,
userInputErrorMessage
});
}
return true;
});
} else if (
this.state.user.includes(
this.state.registerBase
@ -726,8 +702,7 @@ class WelcomePage extends Component<IWelcomeProps, IWelcomeState> {
{this.state.brandName
? this.state.brandName
: "Hypersapce"}{" "}
v.
{this.state.version}{" "}
v.{this.state.version}{" "}
{this.state.brandName &&
this.state.brandName !== "Hyperspace"
? "(Hyperspace-like)"

View File

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

View File

@ -1,16 +1,16 @@
import React, { Component } from "react";
import {
withStyles,
Typography,
Paper,
Avatar,
Button,
TextField,
ListItem,
ListItemText,
ListItemAvatar,
List,
Grid
withStyles,
Typography,
Paper,
Avatar,
Button,
TextField,
ListItem,
ListItemText,
ListItemAvatar,
List,
Grid
} from "@material-ui/core";
import { withSnackbar, withSnackbarProps } from "notistack";
import { styles } from "./PageLayout.styles";
@ -21,271 +21,303 @@ import filedialog from "file-dialog";
import PersonIcon from "@material-ui/icons/Person";
interface IYouProps extends withSnackbarProps {
classes: any;
classes: any;
}
interface IYouState {
currentAccount: Account;
newDisplayName?: string;
newBio?: string;
currentAccount: Account;
newDisplayName?: string;
newBio?: string;
}
class You extends Component<IYouProps, IYouState> {
client: Mastodon;
client: Mastodon;
constructor(props: any) {
super(props);
constructor(props: any) {
super(props);
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.client = new Mastodon(
localStorage.getItem("access_token") as string,
(localStorage.getItem("baseurl") as string) + "/api/v1"
);
this.state = {
currentAccount: this.getAccount()
};
}
getAccount() {
let acct = localStorage.getItem("account");
if (acct) {
return JSON.parse(acct);
this.state = {
currentAccount: this.getAccount()
};
}
}
updateAvatar() {
filedialog({
multiple: false,
accept: "image/*"
})
.then((images: FileList) => {
if (images.length > 0) {
this.props.enqueueSnackbar("Updating avatar...", {
persist: true,
key: "persistAvatar"
});
let upload = new FormData();
upload.append("avatar", images[0]);
this.client
.patch("/accounts/update_credentials", upload)
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar("Avatar updated successfully.");
getAccount() {
let acct = localStorage.getItem("account");
if (acct) {
return JSON.parse(acct);
}
}
updateAvatar() {
filedialog({
multiple: false,
accept: "image/*"
})
.then((images: FileList) => {
if (images.length > 0) {
this.props.enqueueSnackbar("Updating avatar...", {
persist: true,
key: "persistAvatar"
});
let upload = new FormData();
upload.append("avatar", images[0]);
this.client
.patch("/accounts/update_credentials", upload)
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem(
"account",
JSON.stringify(currentAccount)
);
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar(
"Avatar updated successfully."
);
})
.catch((err: Error) => {
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar(
"Couldn't update avatar: " + err.name,
{ variant: "error" }
);
});
}
})
.catch((err: Error) => {
this.props.closeSnackbar("persistAvatar");
this.props.enqueueSnackbar(
"Couldn't update avatar: " + err.name,
{ variant: "error" }
);
this.props.enqueueSnackbar(
"Couldn't update avatar: " + err.name
);
});
}
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update avatar: " + err.name);
});
}
}
updateHeader() {
filedialog({
multiple: false,
accept: "image/*"
})
.then((images: FileList) => {
if (images.length > 0) {
this.props.enqueueSnackbar("Updating header...", {
persist: true,
key: "persistHeader"
});
let upload = new FormData();
upload.append("header", images[0]);
this.client
.patch("/accounts/update_credentials", upload)
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Header updated successfully.");
updateHeader() {
filedialog({
multiple: false,
accept: "image/*"
})
.then((images: FileList) => {
if (images.length > 0) {
this.props.enqueueSnackbar("Updating header...", {
persist: true,
key: "persistHeader"
});
let upload = new FormData();
upload.append("header", images[0]);
this.client
.patch("/accounts/update_credentials", upload)
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem(
"account",
JSON.stringify(currentAccount)
);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Header updated successfully."
);
})
.catch((err: Error) => {
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update header: " + err.name,
{ variant: "error" }
);
});
}
})
.catch((err: Error) => {
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update header: " + err.name,
{ variant: "error" }
);
this.props.enqueueSnackbar(
"Couldn't update header: " + err.name
);
});
}
})
.catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update header: " + err.name);
});
}
}
removeHTMLContent(text: string) {
const div = document.createElement("div");
div.innerHTML = text;
let innerContent = div.textContent || div.innerText || "";
return innerContent;
}
changeDisplayName() {
this.client
.patch("/accounts/update_credentials", {
display_name: this.state.newDisplayName
? this.state.newDisplayName
: this.state.currentAccount.display_name
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Display name updated to " + this.state.newDisplayName
removeHTMLContent(text: string) {
const div = document.createElement("div");
div.innerHTML = text;
let innerContent = div.textContent || div.innerText || "";
return innerContent;
}
changeDisplayName() {
this.client
.patch("/accounts/update_credentials", {
display_name: this.state.newDisplayName
? this.state.newDisplayName
: this.state.currentAccount.display_name
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Display name updated to " + this.state.newDisplayName
);
})
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar(
"Couldn't update display name: " + err.name,
{ variant: "error" }
);
});
}
updateDisplayName(name: string) {
this.setState({ newDisplayName: name });
}
changeBio() {
this.client
.patch("/accounts/update_credentials", {
note: this.state.newBio
? this.state.newBio
: this.state.currentAccount.note
})
.then((acct: any) => {
let currentAccount: Account = acct.data;
this.setState({ currentAccount });
localStorage.setItem("account", JSON.stringify(currentAccount));
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Bio updated successfully.");
})
.catch((err: Error) => {
console.error(err.name);
this.props.closeSnackbar("persistHeader");
this.props.enqueueSnackbar("Couldn't update bio: " + err.name, {
variant: "error"
});
});
}
updateBio(bio: string) {
this.setState({ newBio: bio });
}
render() {
const { classes } = this.props;
return (
<div className={classes.pageLayoutMinimalConstraints}>
<div className={classes.pageHeroBackground}>
<div
className={classes.pageHeroBackgroundImage}
style={{
backgroundImage: `url("${this.state.currentAccount.header_static}")`
}}
/>
<div className={classes.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));

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,5 +7,5 @@ import { isDarwinApp } from "./desktop";
* @returns Boolean dictating if the title bar is visible
*/
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
*/
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
*/
export function isDesktopApp(): boolean {
return navigator.userAgent.includes("Hyperspace" || "Electron");
return navigator.userAgent.includes("Hyperspace" || "Electron");
}
/**
* Determines whether the app is the macOS application
*/
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)
*/
export function isDarkMode() {
// Lift window to an ElectronWindow and add use require()
const eWin = window as ElectronWindow;
const { remote } = eWin.require("electron");
return remote.systemPreferences.isDarkMode();
// Lift window to an ElectronWindow and add use require()
const eWin = window as ElectronWindow;
const { remote } = eWin.require("electron");
return remote.systemPreferences.isDarkMode();
}

View File

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

View File

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

View File

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