2019-04-04 02:01:54 +02:00
|
|
|
import React, {Component} from 'react';
|
2019-04-05 18:44:03 +02:00
|
|
|
import { Dialog, DialogContent, DialogActions, withStyles, Button, CardHeader, Avatar, TextField, Toolbar, IconButton, Fade, Typography, Tooltip, Menu, MenuItem, GridList, ListSubheader, GridListTile } from '@material-ui/core';
|
2019-04-05 19:22:30 +02:00
|
|
|
import {parse as parseParams, ParsedQuery} from 'query-string';
|
2019-04-04 02:01:54 +02:00
|
|
|
import {styles} from './Compose.styles';
|
|
|
|
import { UAccount } from '../types/Account';
|
|
|
|
import { Visibility } from '../types/Visibility';
|
|
|
|
import CameraAltIcon from '@material-ui/icons/CameraAlt';
|
|
|
|
import TagFacesIcon from '@material-ui/icons/TagFaces';
|
|
|
|
import HowToVoteIcon from '@material-ui/icons/HowToVote';
|
|
|
|
import VisibilityIcon from '@material-ui/icons/Visibility';
|
|
|
|
import WarningIcon from '@material-ui/icons/Warning';
|
|
|
|
import Mastodon from 'megalodon';
|
|
|
|
import {withSnackbar} from 'notistack';
|
|
|
|
import { Attachment } from '../types/Attachment';
|
|
|
|
import { PollWizard } from '../types/Poll';
|
|
|
|
import filedialog from 'file-dialog';
|
2019-04-05 18:44:03 +02:00
|
|
|
import ComposeMediaAttachment from '../components/ComposeMediaAttachment';
|
2019-04-04 02:01:54 +02:00
|
|
|
|
|
|
|
|
2019-04-05 19:22:30 +02:00
|
|
|
|
2019-04-04 02:01:54 +02:00
|
|
|
interface IComposerState {
|
|
|
|
account: UAccount;
|
|
|
|
visibility: Visibility;
|
|
|
|
sensitive: boolean;
|
|
|
|
sensitiveText?: string;
|
|
|
|
visibilityMenu: boolean;
|
|
|
|
text: string;
|
|
|
|
remainingChars: number;
|
|
|
|
reply?: string;
|
|
|
|
acct?: string;
|
|
|
|
attachments?: [Attachment];
|
|
|
|
poll?: PollWizard;
|
|
|
|
}
|
|
|
|
|
|
|
|
class Composer extends Component<any, IComposerState> {
|
|
|
|
|
|
|
|
client: Mastodon;
|
|
|
|
|
|
|
|
constructor(props: any) {
|
|
|
|
super(props);
|
|
|
|
|
|
|
|
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') + "/api/v1");
|
|
|
|
|
|
|
|
this.state = {
|
|
|
|
account: JSON.parse(localStorage.getItem('account') as string),
|
|
|
|
visibility: "public",
|
|
|
|
sensitive: false,
|
|
|
|
visibilityMenu: false,
|
|
|
|
text: '',
|
|
|
|
remainingChars: 500
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-05 19:22:30 +02:00
|
|
|
componentDidMount() {
|
|
|
|
let state = this.getComposerParams(this.props);
|
|
|
|
this.setState({
|
|
|
|
reply: state.reply,
|
|
|
|
acct: state.acct,
|
|
|
|
visibility: state.visibility,
|
|
|
|
text: state.acct? `@${state.acct}: `: ''
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
componentWillReceiveProps(props: any) {
|
|
|
|
let state = this.getComposerParams(props);
|
|
|
|
this.setState({
|
|
|
|
reply: state.reply,
|
|
|
|
acct: state.acct,
|
|
|
|
visibility: state.visibility,
|
|
|
|
text: state.acct? `@${state.acct}: `: ''
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
checkComposerParams(location?: string): ParsedQuery {
|
|
|
|
let params = "";
|
|
|
|
if (location !== undefined && typeof(location) === "string") {
|
|
|
|
params = location.replace("#/compose", "");
|
|
|
|
} else {
|
|
|
|
params = window.location.hash.replace("#/compose", "");
|
|
|
|
}
|
|
|
|
return parseParams(params);
|
|
|
|
}
|
|
|
|
|
|
|
|
getComposerParams(props: any) {
|
|
|
|
let params = this.checkComposerParams(props.location);
|
|
|
|
let reply: string = "";
|
|
|
|
let acct: string = "";
|
|
|
|
let visibility: Visibility = "public";
|
|
|
|
|
|
|
|
if (params.reply) {
|
|
|
|
reply = params.reply.toString();
|
|
|
|
}
|
|
|
|
if (params.acct) {
|
|
|
|
acct = params.acct.toString();
|
|
|
|
}
|
|
|
|
if (params.visibility) {
|
|
|
|
visibility = params.visibility.toString() as Visibility;
|
|
|
|
}
|
|
|
|
return {
|
|
|
|
reply,
|
|
|
|
acct,
|
|
|
|
visibility
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-04 02:01:54 +02:00
|
|
|
updateTextFromField(text: string) {
|
|
|
|
this.setState({ text, remainingChars: 500 - text.length });
|
|
|
|
}
|
|
|
|
|
|
|
|
updateWarningFromField(sensitiveText: string) {
|
|
|
|
this.setState({ sensitiveText });
|
|
|
|
}
|
|
|
|
|
|
|
|
changeVisibility(visibility: Visibility) {
|
|
|
|
this.setState({ visibility });
|
|
|
|
}
|
|
|
|
|
|
|
|
uploadMedia() {
|
|
|
|
filedialog({
|
|
|
|
multiple: false,
|
|
|
|
accept: "image/*, video/*"
|
|
|
|
}).then((media: FileList) => {
|
|
|
|
let mediaForm = new FormData();
|
|
|
|
mediaForm.append('file', media[0]);
|
|
|
|
const uploading = this.props.enqueueSnackbar("Uploading media...", { persist: true })
|
|
|
|
this.client.post('/media', mediaForm).then((resp: any) => {
|
|
|
|
let attachment: Attachment = resp.data;
|
|
|
|
let attachments = this.state.attachments;
|
|
|
|
if (attachments) {
|
|
|
|
attachments.push(attachment);
|
|
|
|
} else {
|
|
|
|
attachments = [attachment];
|
|
|
|
}
|
|
|
|
this.setState({ attachments });
|
|
|
|
this.props.closeSnackbar(uploading);
|
|
|
|
this.props.enqueueSnackbar('Media uploaded.');
|
|
|
|
}).catch((err: Error) => {
|
|
|
|
this.props.enqueueSnackbar("Couldn't upload media: " + err.name);
|
|
|
|
})
|
|
|
|
}).catch((err: Error) => {
|
|
|
|
this.props.enqueueSnackbar("Couldn't get media: " + err.name);
|
|
|
|
console.error(err.message);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
getOnlyMediaIds() {
|
|
|
|
let ids: string[] = [];
|
|
|
|
if (this.state.attachments) {
|
|
|
|
this.state.attachments.map((attachment: Attachment) => {
|
|
|
|
ids.push(attachment.id);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
return ids;
|
|
|
|
}
|
|
|
|
|
|
|
|
post() {
|
|
|
|
this.client.post('/statuses', {
|
|
|
|
status: this.state.text,
|
|
|
|
media_ids: this.getOnlyMediaIds(),
|
|
|
|
visibility: this.state.visibility,
|
|
|
|
sensitive: this.state.sensitive,
|
2019-04-05 19:22:30 +02:00
|
|
|
spoiler_text: this.state.sensitiveText,
|
|
|
|
in_reply_to_id: this.state.reply
|
2019-04-04 02:01:54 +02:00
|
|
|
}).then(() => {
|
|
|
|
this.props.enqueueSnackbar('Posted!');
|
|
|
|
window.history.back();
|
|
|
|
}).catch((err: Error) => {
|
|
|
|
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
|
|
|
console.log(err.message);
|
|
|
|
})
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleSensitive() {
|
|
|
|
this.setState({ sensitive: !this.state.sensitive });
|
|
|
|
}
|
|
|
|
|
|
|
|
toggleVisibilityMenu() {
|
|
|
|
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
|
|
|
}
|
|
|
|
|
2019-04-05 18:44:03 +02:00
|
|
|
fetchAttachmentAfterUpdate(attachment: Attachment) {
|
|
|
|
let attachments = this.state.attachments;
|
|
|
|
if (attachments) {
|
|
|
|
attachments.forEach((attach: Attachment) => {
|
|
|
|
if (attach.id === attachment.id && attachments) {
|
|
|
|
attachments[attachments.indexOf(attach)] = attachment;
|
|
|
|
}
|
|
|
|
})
|
|
|
|
this.setState({ attachments });
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
deleteMediaAttachment(attachment: Attachment) {
|
|
|
|
let attachments = this.state.attachments;
|
|
|
|
if (attachments) {
|
|
|
|
attachments.forEach((attach: Attachment) => {
|
|
|
|
if (attach.id === attachment.id && attachments) {
|
|
|
|
attachments.splice(attachments.indexOf(attach), 1);
|
|
|
|
}
|
|
|
|
this.setState({ attachments });
|
|
|
|
})
|
|
|
|
this.props.enqueueSnackbar("Attachment removed.");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-04-04 02:01:54 +02:00
|
|
|
render() {
|
2019-04-05 19:22:30 +02:00
|
|
|
console.log(this.state);
|
2019-04-04 02:01:54 +02:00
|
|
|
const {classes} = this.props;
|
|
|
|
|
|
|
|
return (
|
|
|
|
<Dialog open={true} maxWidth="sm" fullWidth={true} className={classes.dialog} onClose={() => window.history.back()}>
|
|
|
|
<CardHeader
|
|
|
|
avatar={
|
|
|
|
<Avatar src={this.state.account.avatar_static} />
|
|
|
|
}
|
|
|
|
title={`${this.state.account.display_name} (@${this.state.account.acct})`}
|
|
|
|
subheader={this.state.visibility.charAt(0).toUpperCase() + this.state.visibility.substr(1)}
|
|
|
|
/>
|
|
|
|
<DialogContent className={classes.dialogContent}>
|
|
|
|
{
|
|
|
|
this.state.sensitive?
|
|
|
|
<Fade in={this.state.sensitive}>
|
|
|
|
<TextField
|
|
|
|
variant="outlined"
|
|
|
|
fullWidth
|
|
|
|
label="Content warning"
|
|
|
|
margin="dense"
|
|
|
|
onChange={(event) => this.updateWarningFromField(event.target.value)}
|
|
|
|
></TextField>
|
|
|
|
</Fade>: null
|
|
|
|
}
|
|
|
|
{
|
|
|
|
this.state.visibility === "direct"?
|
|
|
|
<Typography variant="caption" >
|
|
|
|
<WarningIcon className={classes.warningCaption}/> Don't forget to add the usernames of the accounts you want to message in your post.
|
|
|
|
</Typography>: null
|
|
|
|
}
|
|
|
|
|
|
|
|
<TextField
|
|
|
|
variant="outlined"
|
|
|
|
multiline
|
|
|
|
fullWidth
|
|
|
|
placeholder="What's on your mind?"
|
|
|
|
margin="normal"
|
|
|
|
onChange={(event) => this.updateTextFromField(event.target.value)}
|
|
|
|
inputProps = {
|
|
|
|
{
|
|
|
|
maxLength: 500
|
|
|
|
}
|
|
|
|
}
|
2019-04-05 19:22:30 +02:00
|
|
|
defaultValue={this.state.text}
|
2019-04-04 02:01:54 +02:00
|
|
|
/>
|
|
|
|
<Typography variant="caption" className={this.state.remainingChars <= 100? classes.charsReachingLimit: null}>
|
|
|
|
{`${this.state.remainingChars} character${this.state.remainingChars === 1? '': 's'} remaining`}
|
|
|
|
</Typography>
|
2019-04-05 18:44:03 +02:00
|
|
|
{
|
|
|
|
this.state.attachments && this.state.attachments.length > 0?
|
|
|
|
<div className={classes.composeAttachmentArea}>
|
|
|
|
<GridList cellHeight={48} className={classes.composeAttachmentAreaGridList}>
|
|
|
|
<GridListTile key="Subheader-composer" cols={2} style={{ height: 'auto' }}>
|
|
|
|
<ListSubheader>Attachments</ListSubheader>
|
|
|
|
</GridListTile>
|
|
|
|
{
|
|
|
|
this.state.attachments.map((attachment: Attachment) => {
|
2019-04-05 19:22:30 +02:00
|
|
|
let c = <ComposeMediaAttachment
|
|
|
|
client={this.client}
|
|
|
|
attachment={attachment}
|
|
|
|
onAttachmentUpdate={(attachment: Attachment) => this.fetchAttachmentAfterUpdate(attachment)}
|
|
|
|
onDeleteCallback={(attachment: Attachment) => this.deleteMediaAttachment(attachment)}
|
|
|
|
/>;
|
|
|
|
return (c);
|
2019-04-05 18:44:03 +02:00
|
|
|
})
|
|
|
|
}
|
|
|
|
</GridList>
|
|
|
|
</div>: null
|
|
|
|
}
|
2019-04-04 02:01:54 +02:00
|
|
|
</DialogContent>
|
|
|
|
<Toolbar className={classes.dialogActions}>
|
|
|
|
<Tooltip title="Add photos or videos">
|
|
|
|
<IconButton disabled={this.state.poll !== undefined} onClick={() => this.uploadMedia()} id="compose-media">
|
|
|
|
<CameraAltIcon/>
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Insert emoji">
|
|
|
|
<IconButton id="compose-emoji">
|
|
|
|
<TagFacesIcon/>
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Add a poll">
|
|
|
|
<IconButton disabled={this.state.attachments && this.state.attachments.length > 0} id="compose-poll">
|
|
|
|
<HowToVoteIcon/>
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Change who sees your post">
|
|
|
|
<IconButton id="compose-visibility" onClick={() => this.toggleVisibilityMenu()}>
|
|
|
|
<VisibilityIcon/>
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
<Tooltip title="Set a content warning">
|
|
|
|
<IconButton onClick={() => this.toggleSensitive()} id="compose-warning">
|
|
|
|
<WarningIcon/>
|
|
|
|
</IconButton>
|
|
|
|
</Tooltip>
|
|
|
|
<Menu open={this.state.visibilityMenu} anchorEl={document.getElementById('compose-visibility')} onClose={() => this.toggleVisibilityMenu()}>
|
|
|
|
<MenuItem onClick={() => this.changeVisibility('direct')}>Direct (direct message)</MenuItem>
|
|
|
|
<MenuItem onClick={() => this.changeVisibility('private')}>Private (followers only)</MenuItem>
|
|
|
|
<MenuItem onClick={() => this.changeVisibility('unlisted')}>Unlisted</MenuItem>
|
|
|
|
<MenuItem onClick={() => this.changeVisibility('public')}>Public</MenuItem>
|
|
|
|
</Menu>
|
|
|
|
</Toolbar>
|
|
|
|
<DialogActions>
|
|
|
|
<Button color="secondary" onClick={() => this.post()}>Post</Button>
|
|
|
|
</DialogActions>
|
|
|
|
</Dialog>
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export default withStyles(styles)(withSnackbar(Composer));
|