hyperspace-desktop-client-w.../src/pages/Compose.tsx

348 lines
14 KiB
TypeScript
Raw Normal View History

2019-04-04 02:01:54 +02:00
import React, {Component} from 'react';
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';
import ComposeMediaAttachment from '../components/ComposeMediaAttachment';
2019-04-05 22:29:11 +02:00
import EmojiPicker from '../components/EmojiPicker';
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;
2019-04-05 22:29:11 +02:00
showEmojis: boolean;
2019-04-04 02:01:54 +02:00
}
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: '',
2019-04-05 22:29:11 +02:00
remainingChars: 500,
showEmojis: false
2019-04-04 02:01:54 +02:00
}
}
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,
2019-04-05 22:29:11 +02:00
text: state.acct? `@${state.acct}: `: '',
remainingChars: 500 - (state.acct? `@${state.acct}: `: '').length
2019-04-05 19:22:30 +02:00
})
}
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;
}
2019-04-05 22:29:11 +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.");
}
}
insertEmoji(e: any) {
if (e.custom) {
let text = this.state.text + e.colons
this.setState({
text,
remainingChars: 500 - text.length
});
} else {
let text = this.state.text + e.native
this.setState({
text,
remainingChars: 500 - text.length
});
}
}
2019-04-04 02:01:54 +02:00
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 22:29:11 +02:00
toggleEmojis() {
this.setState({ showEmojis: !this.state.showEmojis });
}
2019-04-04 02:01:54 +02:00
render() {
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 22:29:11 +02:00
value={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>
{
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);
})
}
</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">
2019-04-05 22:29:11 +02:00
<IconButton id="compose-emoji" onClick={() => this.toggleEmojis()}>
2019-04-04 02:01:54 +02:00
<TagFacesIcon/>
</IconButton>
</Tooltip>
2019-04-05 22:29:11 +02:00
<Menu
open={this.state.showEmojis}
anchorEl={document.getElementById('compose-emoji')}
onClose={() => this.toggleEmojis()}
>
<EmojiPicker onGetEmoji={(emoji: any) => this.insertEmoji(emoji)}/>
</Menu>
2019-04-04 02:01:54 +02:00
<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));