Fix streaming, add poll support to composer

This commit is contained in:
Marquis Kurt 2019-04-06 13:41:43 -04:00
parent 27b6c92c22
commit 4978af6b6e
10 changed files with 359 additions and 20 deletions

53
package-lock.json generated
View File

@ -944,6 +944,21 @@
"integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==", "integrity": "sha512-5a6wqoJV/xEdbRNKVo6I4hO3VjyDq//8q2f9I6PBAvMesJHFauXDorcNCsr9RzvsZnaWi5NYCcfyqP1QeFHFbw==",
"dev": true "dev": true
}, },
"@date-io/core": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@date-io/core/-/core-1.1.0.tgz",
"integrity": "sha512-PyjhyR2fbp7Q8xpB5zoOyT3dqr8Bn4kXfREf1w6AnQalwdftNxChB2/p88fI1qsx8KNmHDJY12eCgLoZaPegTw==",
"dev": true
},
"@date-io/moment": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@date-io/moment/-/moment-1.1.0.tgz",
"integrity": "sha512-U7Vrhk7nlrKMtlLuNs7DAWmMsMBTgWFAvgpKg6Z5Opo/U7ab5DvI8nLks5dqJbeEFQDSlO+KCmJWfsL9hVI3Jw==",
"dev": true,
"requires": {
"@date-io/core": "^1.1.0"
}
},
"@material-ui/core": { "@material-ui/core": {
"version": "3.9.3", "version": "3.9.3",
"resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.3.tgz", "resolved": "https://registry.npmjs.org/@material-ui/core/-/core-3.9.3.tgz",
@ -1290,6 +1305,15 @@
"@types/react": "*" "@types/react": "*"
} }
}, },
"@types/react-text-mask": {
"version": "5.4.4",
"resolved": "https://registry.npmjs.org/@types/react-text-mask/-/react-text-mask-5.4.4.tgz",
"integrity": "sha512-mnwyDgUwFtJVAZ8f+tzPGmYjpH7TLXxHSGty338abca6aAjdjRLCOC4h+CxlvB8xrVAIU5pkNllpHhfJCA3hXQ==",
"dev": true,
"requires": {
"@types/react": "*"
}
},
"@types/react-transition-group": { "@types/react-transition-group": {
"version": "2.0.16", "version": "2.0.16",
"resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.0.16.tgz", "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-2.0.16.tgz",
@ -4152,6 +4176,12 @@
"shallow-clone": "^0.1.2" "shallow-clone": "^0.1.2"
} }
}, },
"clsx": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-1.0.3.tgz",
"integrity": "sha512-xLoSw6DMp7YvbEeLrQJBcWWRRerdHrU1WHoL1hYJOKUeDpVMRq7pv7NI2JHQbCRAe5ptINNzhdYmtfN6MsdCUw==",
"dev": true
},
"co": { "co": {
"version": "4.6.0", "version": "4.6.0",
"resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz",
@ -10806,6 +10836,20 @@
"object-visit": "^1.0.0" "object-visit": "^1.0.0"
} }
}, },
"material-ui-pickers": {
"version": "2.2.4",
"resolved": "https://registry.npmjs.org/material-ui-pickers/-/material-ui-pickers-2.2.4.tgz",
"integrity": "sha512-QCQh08Ylmnt+o4laW+rPs92QRAcESv3sPXl50YadLm++rAZAXAOh3K8lreGdynCMYFgZfdyu81Oz9xzTlAZNfw==",
"dev": true,
"requires": {
"@types/react-text-mask": "^5.4.3",
"clsx": "^1.0.2",
"react-event-listener": "^0.6.6",
"react-text-mask": "^5.4.3",
"react-transition-group": "^2.5.3",
"tslib": "^1.9.3"
}
},
"math-random": { "math-random": {
"version": "1.0.4", "version": "1.0.4",
"resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz", "resolved": "https://registry.npmjs.org/math-random/-/math-random-1.0.4.tgz",
@ -15315,6 +15359,15 @@
} }
} }
}, },
"react-text-mask": {
"version": "5.4.3",
"resolved": "https://registry.npmjs.org/react-text-mask/-/react-text-mask-5.4.3.tgz",
"integrity": "sha1-mR77QpnjDC5sLEbRP2FxaUY+DS0=",
"dev": true,
"requires": {
"prop-types": "^15.5.6"
}
},
"react-transition-group": { "react-transition-group": {
"version": "2.7.1", "version": "2.7.1",
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.1.tgz", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-2.7.1.tgz",

View File

@ -24,7 +24,9 @@
"react-web-share-api": "^0.0.2", "react-web-share-api": "^0.0.2",
"query-string": "^6.4.2", "query-string": "^6.4.2",
"file-dialog": "^0.0.7", "file-dialog": "^0.0.7",
"emoji-mart": "^2.8.2" "emoji-mart": "^2.8.2",
"material-ui-pickers": "^2.2.4",
"@date-io/moment": "^1.1.0"
}, },
"scripts": { "scripts": {
"start": "BROWSER='Safari Technology Preview' react-scripts start", "start": "BROWSER='Safari Technology Preview' react-scripts start",

View File

@ -254,7 +254,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<ListItemText primary={this.state.currentUser.display_name || this.state.currentUser.acct} secondary={'@' + this.state.currentUser.acct}/> <ListItemText primary={this.state.currentUser.display_name || this.state.currentUser.acct} secondary={'@' + this.state.currentUser.acct}/>
</LinkableListItem> </LinkableListItem>
<Divider/> <Divider/>
<MenuItem>Switch account</MenuItem> {/* <MenuItem>Switch account</MenuItem> */}
<MenuItem>Log out</MenuItem> <MenuItem>Log out</MenuItem>
</div> </div>
</ClickAwayListener> </ClickAwayListener>

View File

@ -80,5 +80,8 @@ export const styles = (theme: Theme) => createStyles({
postAuthorEmoji: { postAuthorEmoji: {
height: theme.typography.fontSize, height: theme.typography.fontSize,
verticalAlign: "middle" verticalAlign: "middle"
},
heading: {
color: "inherit"
} }
}); });

View File

@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip } from '@material-ui/core'; import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip, RadioGroup, Radio, FormControlLabel, Button } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert'; import MoreVertIcon from '@material-ui/icons/MoreVert';
import ReplyIcon from '@material-ui/icons/Reply'; import ReplyIcon from '@material-ui/icons/Reply';
import FavoriteIcon from '@material-ui/icons/Favorite'; import FavoriteIcon from '@material-ui/icons/Favorite';
@ -25,6 +25,7 @@ import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interf
import {withSnackbar} from 'notistack'; import {withSnackbar} from 'notistack';
import ShareMenu from './PostShareMenu'; import ShareMenu from './PostShareMenu';
import {emojifyString} from '../../utilities/emojis'; import {emojifyString} from '../../utilities/emojis';
import { PollOption, Poll } from '../../types/Poll';
interface IPostProps { interface IPostProps {
post: Status; post: Status;
@ -36,6 +37,7 @@ interface IPostState {
post: Status; post: Status;
media_slides?: number; media_slides?: number;
menuIsOpen: boolean; menuIsOpen: boolean;
myVote?: [number];
} }
export class Post extends React.Component<any, IPostState> { export class Post extends React.Component<any, IPostState> {
@ -59,6 +61,54 @@ export class Post extends React.Component<any, IPostState> {
this.setState({ menuIsOpen: !this.state.menuIsOpen }) this.setState({ menuIsOpen: !this.state.menuIsOpen })
} }
findBiggestVote() {
let poll = this.state.post.poll;
let votes: number[] = [];
let bv = "";
if (poll) {
poll.options.forEach((option: PollOption) => {
votes.push(option.votes_count? option.votes_count: 0);
});
let biggestVote = Math.max.apply(null, votes);
poll.options.forEach((option: PollOption) => {
if (option.votes_count === biggestVote) {
bv = option.title;
};
});
return bv;
} else {
return "No poll option was the best.";
}
}
captureVote(option: any) {
let poll = this.state.post.poll;
let pollIndex: number = 0;
if (poll) {
poll.options.forEach((pollOption: PollOption, index: number) => {
if (pollOption.title === option) {
pollIndex = index;
}
})
}
this.setState({ myVote: [pollIndex] });
}
submitVote() {
let poll = this.state.post.poll;
if (poll) {
this.client.post(`/polls/${poll.id}/votes`, {choices: this.state.myVote}).then((resp: any) => {
let post = this.state.post;
post.poll = resp.data;
this.setState({ post });
this.props.enqueueSnackbar("Vote submitted.");
}).catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't vote: " + err.name);
console.error(err.message);
})
}
}
materializeContent(status: Status) { materializeContent(status: Status) {
const { classes } = this.props; const { classes } = this.props;
@ -105,22 +155,81 @@ export class Post extends React.Component<any, IPostState> {
<AttachmentComponent media={status.media_attachments}/>: <AttachmentComponent media={status.media_attachments}/>:
<span/> <span/>
} }
{
status.poll?
status.poll.voted || status.poll.expired?
<div>
<Typography variant="caption">You can't vote on this poll. Below are the results of the poll.</Typography>
<RadioGroup
value={this.findBiggestVote()}
>
{
status.poll.options.map((pollOption: PollOption) => {
let x = <FormControlLabel
disabled
value={pollOption.title}
control={<Radio />}
label={`${pollOption.title} (${pollOption.votes_count} votes)`}
key={pollOption.title+pollOption.votes_count}
/>;
return (x);
})
}
</RadioGroup>
{
status.poll && status.poll.expired?
<Typography variant="caption">This poll has expired.</Typography>:
<Typography variant="caption">This poll will expire on {moment(status.poll.expires_at? status.poll.expires_at: "").format('MMMM Do YYYY, [at] h:mm A')}.</Typography>
}
</div>:
<div>
<RadioGroup
onChange={(event: any, option: any) => this.captureVote(option)}
>
{
status.poll.options.map((pollOption: PollOption) => {
let x = <FormControlLabel
value={pollOption.title}
control={<Radio />}
label={pollOption.title}
key={pollOption.title+pollOption.votes_count}
/>;
return (x);
})
}
</RadioGroup>
<Button color="primary" onClick={(event: any) => this.submitVote()}>Vote</Button>
</div>: null
}
</div> </div>
</CardContent> </CardContent>
); );
} }
spoilerContainsFlags(text: string): boolean {
let unsafeFlags = ["NSFW", "nsfw", "lewd", "sex"];
let result: boolean = false;
unsafeFlags.forEach((flag: string) => {
if (text.includes(flag)) {
result = true;
}
})
return result;
}
getSensitiveContent(spoiler_text: string, content: Status) { getSensitiveContent(spoiler_text: string, content: Status) {
const { classes } = this.props; const { classes } = this.props;
const warningText = spoiler_text || "Unmarked content"; const warningText = spoiler_text || "Unmarked content";
let icon; let icon;
if (spoiler_text.includes("NSFW") || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") { if (this.spoilerContainsFlags(spoiler_text) || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") {
icon = <WarningIcon className={classes.postWarningIcon}/>; icon = <WarningIcon className={classes.postWarningIcon}/>;
} }
return ( return (
<ExpansionPanel className={spoiler_text.includes("NSFW")? classes.nsfwCard: classes.spoilerCard} color="inherit"> <ExpansionPanel className={this.spoilerContainsFlags(spoiler_text)? classes.nsfwCard: classes.spoilerCard} color="inherit">
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} color="inherit"> <ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>} color="inherit">
{icon}<Typography className={classes.heading} color="inherit">{warningText}</Typography> {icon}<Typography>{warningText}</Typography>
</ExpansionPanelSummary> </ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.postContent} color="inherit"> <ExpansionPanelDetails className={classes.postContent} color="inherit">
{this.materializeContent(content)} {this.materializeContent(content)}

View File

@ -35,5 +35,14 @@ export const styles = (theme: Theme) => createStyles({
[theme.breakpoints.up('sm')]: { [theme.breakpoints.up('sm')]: {
display: "block" display: "block"
} }
},
pollWizardOptionIcon: {
marginRight: theme.spacing.unit * 2,
marginTop: 4,
marginBottom: 4,
color: theme.palette.grey[700]
},
pollWizardFlexGrow: {
flexGrow: 1
} }
}); });

View File

@ -9,14 +9,17 @@ import TagFacesIcon from '@material-ui/icons/TagFaces';
import HowToVoteIcon from '@material-ui/icons/HowToVote'; import HowToVoteIcon from '@material-ui/icons/HowToVote';
import VisibilityIcon from '@material-ui/icons/Visibility'; import VisibilityIcon from '@material-ui/icons/Visibility';
import WarningIcon from '@material-ui/icons/Warning'; import WarningIcon from '@material-ui/icons/Warning';
import DeleteIcon from '@material-ui/icons/Delete';
import RadioButtonCheckedIcon from '@material-ui/icons/RadioButtonChecked';
import Mastodon from 'megalodon'; import Mastodon from 'megalodon';
import {withSnackbar} from 'notistack'; import {withSnackbar} from 'notistack';
import { Attachment } from '../types/Attachment'; import { Attachment } from '../types/Attachment';
import { PollWizard } from '../types/Poll'; import { PollWizard, PollWizardOption } from '../types/Poll';
import filedialog from 'file-dialog'; import filedialog from 'file-dialog';
import ComposeMediaAttachment from '../components/ComposeMediaAttachment'; import ComposeMediaAttachment from '../components/ComposeMediaAttachment';
import EmojiPicker from '../components/EmojiPicker'; import EmojiPicker from '../components/EmojiPicker';
import { DateTimePicker, MuiPickersUtilsProvider } from 'material-ui-pickers';
import MomentUtils from '@date-io/moment';
interface IComposerState { interface IComposerState {
account: UAccount; account: UAccount;
@ -30,6 +33,7 @@ interface IComposerState {
acct?: string; acct?: string;
attachments?: [Attachment]; attachments?: [Attachment];
poll?: PollWizard; poll?: PollWizard;
pollExpiresDate?: any;
showEmojis: boolean; showEmojis: boolean;
} }
@ -200,14 +204,120 @@ class Composer extends Component<any, IComposerState> {
} }
} }
createPoll() {
if (this.state.poll === undefined) {
let expiration = new Date();
let current = new Date();
expiration.setMinutes(expiration.getMinutes() + 30);
let expiryDifference = (expiration.getTime() - current.getTime() / 1000);
let temporaryPoll: PollWizard = {
expires_at: expiryDifference.toString(),
multiple: false,
options: [{title: 'Option 1'}, {title: 'Option 2'}]
}
this.setState({
poll: temporaryPoll,
pollExpiresDate: expiration
});
}
}
addPollItem() {
if (this.state.poll !== undefined && this.state.poll.options.length < 4) {
let newOption = {title: 'New option'}
let options = this.state.poll.options;
let poll = this.state.poll;
options.push(newOption);
poll.options = options;
poll.multiple = true;
this.setState({
poll: poll
})
} else if (this.state.poll && this.state.poll.options.length == 4) {
this.props.enqueueSnackbar("You've reached the options limit in your poll.", { variant: 'error' })
}
}
editPollItem(position: number, newTitle: any) {
if (this.state.poll !== undefined) {
let poll = this.state.poll;
let options = this.state.poll.options;
options.forEach((option: PollWizardOption) => {
if (position === options.indexOf(option)) {
option.title = newTitle.target.value;
}
});
poll.options = options;
this.setState({
poll: poll
});
this.props.enqueueSnackbar('Option edited.');
}
}
removePollItem(item: string) {
if (this.state.poll !== undefined && this.state.poll.options.length > 2) {
let options = this.state.poll.options;
let poll = this.state.poll;
options.forEach((option: PollWizardOption) => {
if (item === option.title) {
options.splice(options.indexOf(option), 1);
}
});
poll.options = options;
if (options.length === 2) {
poll.multiple = false;
}
this.setState({
poll: poll
})
} else if (this.state.poll && this.state.poll.options.length <= 2) {
this.props.enqueueSnackbar('Polls must have at least two items.', { variant: 'error'} );
}
}
setPollExpires(date: string) {
let currentDate = new Date();
let newDate = new Date(date);
let poll = this.state.poll;
if (poll) {
let expiry = ((newDate.getTime() - currentDate.getTime()) / 1000);
console.log(expiry);
if (expiry >= 1800) {
poll.expires_at = expiry.toString();
this.setState({ poll, pollExpiresDate: date });
this.props.enqueueSnackbar("Expiration updated.")
} else {
this.props.enqueueSnackbar("Expiration is too small (min. 30 minutes).", { variant: 'error' });
}
}
}
removePoll() {
this.setState({
poll: undefined
});
}
post() { post() {
let pollOptions: string[] = [];
if (this.state.poll) {
this.state.poll.options.forEach((option: PollWizardOption) => {
pollOptions.push(option.title);
})
}
this.client.post('/statuses', { this.client.post('/statuses', {
status: this.state.text, status: this.state.text,
media_ids: this.getOnlyMediaIds(), media_ids: this.getOnlyMediaIds(),
visibility: this.state.visibility, visibility: this.state.visibility,
sensitive: this.state.sensitive, sensitive: this.state.sensitive,
spoiler_text: this.state.sensitiveText, spoiler_text: this.state.sensitiveText,
in_reply_to_id: this.state.reply in_reply_to_id: this.state.reply,
poll: this.state.poll? {
options: pollOptions,
expires_in: this.state.poll.expires_at,
multiple: this.state.poll.multiple
}: null
}).then(() => { }).then(() => {
this.props.enqueueSnackbar('Posted!'); this.props.enqueueSnackbar('Posted!');
window.history.back(); window.history.back();
@ -299,6 +409,44 @@ class Composer extends Component<any, IComposerState> {
</GridList> </GridList>
</div>: null </div>: null
} }
{
this.state.poll?
<div style={{ marginTop: 4}}>
{
this.state.poll?
this.state.poll.options.map((option: PollWizardOption, index: number) => {
let c = <div style={{ display: "flex" }} key={"compose_option_" + index.toString()}>
<RadioButtonCheckedIcon className={classes.pollWizardOptionIcon}/>
<TextField
onBlur={(event: any) => this.editPollItem(index, event)}
defaultValue={option.title}/>
<div className={classes.pollWizardFlexGrow}/>
<Tooltip title="Remove poll option">
<IconButton onClick={() => this.removePollItem(option.title)}>
<DeleteIcon/>
</IconButton>
</Tooltip>
</div>
return c;
}): null
}
<div style={{ display: "flex"}}>
<MuiPickersUtilsProvider utils={MomentUtils}>
<DateTimePicker
value={this.state.pollExpiresDate? this.state.pollExpiresDate: new Date()}
onChange={(date: any) => {
this.setPollExpires(date.toISOString());
}}
label="Poll exipres on"
disablePast
/>
</MuiPickersUtilsProvider>
<div className={classes.pollWizardFlexGrow}/>
<Button onClick={() => this.addPollItem()}>Add Option</Button>
</div>
</div>: null
}
</DialogContent> </DialogContent>
<Toolbar className={classes.dialogActions}> <Toolbar className={classes.dialogActions}>
<Tooltip title="Add photos or videos"> <Tooltip title="Add photos or videos">
@ -320,7 +468,11 @@ class Composer extends Component<any, IComposerState> {
<EmojiPicker onGetEmoji={(emoji: any) => this.insertEmoji(emoji)}/> <EmojiPicker onGetEmoji={(emoji: any) => this.insertEmoji(emoji)}/>
</Menu> </Menu>
<Tooltip title="Add a poll"> <Tooltip title="Add a poll">
<IconButton disabled={this.state.attachments && this.state.attachments.length > 0} id="compose-poll"> <IconButton disabled={this.state.attachments && this.state.attachments.length > 0} id="compose-poll" onClick={() => {
this.state.poll?
this.removePoll():
this.createPoll()
}}>
<HowToVoteIcon/> <HowToVoteIcon/>
</IconButton> </IconButton>
</Tooltip> </Tooltip>

View File

@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar,
import {styles} from './PageLayout.styles'; import {styles} from './PageLayout.styles';
import Post from '../components/Post'; import Post from '../components/Post';
import { Status } from '../types/Status'; import { Status } from '../types/Status';
import Mastodon from 'megalodon'; import Mastodon, { StreamListener } from 'megalodon';
import {withSnackbar} from 'notistack'; import {withSnackbar} from 'notistack';
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
@ -20,7 +20,7 @@ interface IHomePageState {
class HomePage extends Component<any, IHomePageState> { class HomePage extends Component<any, IHomePageState> {
client: Mastodon; client: Mastodon;
streamListener: any; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -31,11 +31,11 @@ class HomePage extends Component<any, IHomePageState> {
} }
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.streamListener = this.client.stream('/streaming/user');
} }
componentWillMount() { componentWillMount() {
this.streamListener = this.client.stream('/streaming/user');
this.streamListener.on('connect', () => { this.streamListener.on('connect', () => {
this.client.get('/timelines/home', {limit: 40}).then((resp: any) => { this.client.get('/timelines/home', {limit: 40}).then((resp: any) => {
@ -92,6 +92,10 @@ class HomePage extends Component<any, IHomePageState> {
}) })
} }
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() { insertBacklog() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
let posts = this.state.posts; let posts = this.state.posts;

View File

@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar,
import {styles} from './PageLayout.styles'; import {styles} from './PageLayout.styles';
import Post from '../components/Post'; import Post from '../components/Post';
import { Status } from '../types/Status'; import { Status } from '../types/Status';
import Mastodon from 'megalodon'; import Mastodon, { StreamListener } from 'megalodon';
import {withSnackbar} from 'notistack'; import {withSnackbar} from 'notistack';
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
@ -20,7 +20,7 @@ interface ILocalPageState {
class LocalPage extends Component<any, ILocalPageState> { class LocalPage extends Component<any, ILocalPageState> {
client: Mastodon; client: Mastodon;
streamListener: any; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -31,11 +31,11 @@ class LocalPage extends Component<any, ILocalPageState> {
} }
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.streamListener = this.client.stream('/streaming/public/local');
} }
componentWillMount() { componentWillMount() {
this.streamListener = this.client.stream('/streaming/public/local');
this.streamListener.on('connect', () => { this.streamListener.on('connect', () => {
this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => { this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => {
@ -92,6 +92,10 @@ class LocalPage extends Component<any, ILocalPageState> {
}) })
} }
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() { insertBacklog() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
let posts = this.state.posts; let posts = this.state.posts;

View File

@ -3,7 +3,7 @@ import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar,
import {styles} from './PageLayout.styles'; import {styles} from './PageLayout.styles';
import Post from '../components/Post'; import Post from '../components/Post';
import { Status } from '../types/Status'; import { Status } from '../types/Status';
import Mastodon from 'megalodon'; import Mastodon, { StreamListener } from 'megalodon';
import {withSnackbar} from 'notistack'; import {withSnackbar} from 'notistack';
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward'; import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
@ -20,7 +20,7 @@ interface IPublicPageState {
class PublicPage extends Component<any, IPublicPageState> { class PublicPage extends Component<any, IPublicPageState> {
client: Mastodon; client: Mastodon;
streamListener: any; streamListener: StreamListener;
constructor(props: any) { constructor(props: any) {
super(props); super(props);
@ -31,12 +31,11 @@ class PublicPage extends Component<any, IPublicPageState> {
} }
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.streamListener = this.client.stream('/streaming/public');
} }
componentWillMount() { componentWillMount() {
this.streamListener = this.client.stream('/streaming/public');
this.streamListener.on('connect', () => { this.streamListener.on('connect', () => {
this.client.get('/timelines/public', {limit: 40}).then((resp: any) => { this.client.get('/timelines/public', {limit: 40}).then((resp: any) => {
let statuses: [Status] = resp.data; let statuses: [Status] = resp.data;
@ -92,6 +91,10 @@ class PublicPage extends Component<any, IPublicPageState> {
}) })
} }
componentWillUnmount() {
this.streamListener.stop();
}
insertBacklog() { insertBacklog() {
window.scrollTo(0, 0); window.scrollTo(0, 0);
let posts = this.state.posts; let posts = this.state.posts;