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"; import { parse as parseParams, ParsedQuery } from "query-string"; 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 DeleteIcon from "@material-ui/icons/Delete"; import RadioButtonCheckedIcon from "@material-ui/icons/RadioButtonChecked"; import Mastodon from "megalodon"; import { withSnackbar } from "notistack"; import { Attachment } from "../types/Attachment"; import { PollWizard, PollWizardOption } from "../types/Poll"; import filedialog from "file-dialog"; import ComposeMediaAttachment from "../components/ComposeMediaAttachment"; import EmojiPicker from "../components/EmojiPicker"; import { DateTimePicker, MuiPickersUtilsProvider } from "material-ui-pickers"; import MomentUtils from "@date-io/moment"; import { getUserDefaultVisibility, getConfig } from "../utilities/settings"; interface IComposerState { account: UAccount; visibility: Visibility; sensitive: boolean; sensitiveText?: string; visibilityMenu: boolean; text: string; remainingChars: number; reply?: string; acct?: string; attachments?: [Attachment]; poll?: PollWizard; pollExpiresDate?: any; showEmojis: boolean; federated: boolean; } class Composer extends Component { 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: getUserDefaultVisibility(), sensitive: false, visibilityMenu: false, text: "", remainingChars: 500, showEmojis: false, federated: true }; } componentDidMount() { let state = this.getComposerParams(this.props); let text = state.acct ? `@${state.acct}: ` : ""; getConfig().then((config: any) => { this.setState({ federated: config.federation.allowPublicPosts, reply: state.reply, acct: state.acct, visibility: state.visibility, text, remainingChars: 500 - text.length }); }); } componentWillReceiveProps(props: any) { let state = this.getComposerParams(props); let text = state.acct ? `@${state.acct}: ` : ""; this.setState({ reply: state.reply, acct: state.acct, visibility: state.visibility, text, remainingChars: 500 - text.length }); } 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 = this.state.visibility; 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 }; } 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]); this.props.enqueueSnackbar("Uploading media...", { persist: true, key: "media-upload" }); 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("media-upload"); this.props.enqueueSnackbar("Media uploaded."); }) .catch((err: Error) => { this.props.closeSnackbar("media-upload"); this.props.enqueueSnackbar("Couldn't upload media: " + err.name, { variant: "error" }); }); }) .catch((err: Error) => { this.props.enqueueSnackbar("Couldn't get media: " + err.name, { variant: "error" }); console.error(err.message); }); } getOnlyMediaIds() { let ids: string[] = []; if (this.state.attachments) { this.state.attachments.map((attachment: Attachment) => { ids.push(attachment.id); }); } return ids; } 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 }); } } 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 }); } postViaKeyboard(event: any) { if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) { this.post(); } } post() { let pollOptions: string[] = []; if (this.state.poll) { this.state.poll.options.forEach((option: PollWizardOption) => { pollOptions.push(option.title); }); } this.client .post("/statuses", { status: this.state.text, media_ids: this.getOnlyMediaIds(), visibility: this.state.visibility, sensitive: this.state.sensitive, spoiler_text: this.state.sensitiveText, 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(() => { 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 }); } toggleEmojis() { this.setState({ showEmojis: !this.state.showEmojis }); } render() { const { classes } = this.props; console.log(this.state); return ( window.history.back()} > } title={`${this.state.account.display_name} (@${this.state.account.acct})`} subheader={ this.state.visibility.charAt(0).toUpperCase() + this.state.visibility.substr(1) } /> {this.state.sensitive ? ( this.updateWarningFromField(event.target.value) } > ) : null} {this.state.visibility === "direct" ? ( Don't forget to add the usernames of the accounts you want to message in your post. ) : null} this.updateTextFromField(event.target.value)} onKeyDown={event => this.postViaKeyboard(event)} inputProps={{ maxLength: 500 }} value={this.state.text} /> {`${this.state.remainingChars} character${ this.state.remainingChars === 1 ? "" : "s" } remaining`} {this.state.attachments && this.state.attachments.length > 0 ? (
Attachments {this.state.attachments.map((attachment: Attachment) => { let c = ( this.fetchAttachmentAfterUpdate(attachment) } onDeleteCallback={(attachment: Attachment) => this.deleteMediaAttachment(attachment) } /> ); return c; })}
) : null} {this.state.poll ? (
{this.state.poll ? this.state.poll.options.map( (option: PollWizardOption, index: number) => { let c = (
this.editPollItem(index, event) } defaultValue={option.title} />
this.removePollItem(option.title)} >
); return c; } ) : null}
{ this.setPollExpires(date.toISOString()); }} label="Poll exipres on" disablePast />
) : null} this.uploadMedia()} id="compose-media" > this.toggleEmojis()} className={classes.desktopOnly} > this.toggleEmojis()} className={classes.composeEmoji} > this.insertEmoji(emoji)} /> 0 } id="compose-poll" onClick={() => { this.state.poll ? this.removePoll() : this.createPoll(); }} > this.toggleVisibilityMenu()} > this.toggleSensitive()} id="compose-warning" > this.toggleVisibilityMenu()} > this.changeVisibility("direct")}> Direct (direct message) this.changeVisibility("private")}> Private (followers only) this.changeVisibility("unlisted")}> Unlisted {this.state.federated ? ( this.changeVisibility("public")}> Public ) : null}
); } } export default withStyles(styles)(withSnackbar(Composer));