Add documentation to the Compose page (HD-24)

This commit is contained in:
Marquis Kurt 2019-12-03 16:19:38 -05:00
parent b4496720a3
commit 6c8035465a
No known key found for this signature in database
GPG Key ID: 725636D259F5402D
1 changed files with 189 additions and 0 deletions

View File

@ -45,34 +45,104 @@ import {
getUserDefaultBool getUserDefaultBool
} from "../utilities/settings"; } from "../utilities/settings";
/**
* The state for the Composer page.
*/
interface IComposerState { interface IComposerState {
/**
* The current user as an Account.
*/
account: UAccount; account: UAccount;
/**
* The visibility of the post.
*/
visibility: Visibility; visibility: Visibility;
/**
* Whether there should be a content warning.
*/
sensitive: boolean; sensitive: boolean;
/**
* The content warning message.
*/
sensitiveText?: string; sensitiveText?: string;
/**
* Whether the visibility drop-down should be visible.
*/
visibilityMenu: boolean; visibilityMenu: boolean;
/**
* The text contents of the post.
*/
text: string; text: string;
/**
* The remaining amount of characters.
*/
remainingChars: number; remainingChars: number;
/**
* An optional reply ID.
*/
reply?: string; reply?: string;
/**
* The account to reply to, if it exists.
*/
acct?: string; acct?: string;
/**
* An optional list of media attachments.
*/
attachments?: [Attachment]; attachments?: [Attachment];
/**
* An optional poll for the post.
*/
poll?: PollWizard; poll?: PollWizard;
/**
* The expiration date of a poll, if it exists.
*/
pollExpiresDate?: any; pollExpiresDate?: any;
/**
* Whether the emoji picker should be visible.
*/
showEmojis: boolean; showEmojis: boolean;
/**
* Whether or not the account's instance is federated.
*/
federated: boolean; federated: boolean;
} }
/**
* The Compose page contains all of the information to create a UI for post creation.
*/
class Composer extends Component<any, IComposerState> { class Composer extends Component<any, IComposerState> {
/**
* The Mastodon client to work with.
*/
client: Mastodon; client: Mastodon;
/**
* Construct the Compose page by generating the Mastodon client and setting default values.
* @param props The properties passed into the Compose component, usually the page queries.
*/
constructor(props: any) { constructor(props: any) {
super(props); super(props);
// Generate the Mastodon client
this.client = new Mastodon( this.client = new Mastodon(
localStorage.getItem("access_token") as string, localStorage.getItem("access_token") as string,
localStorage.getItem("baseurl") + "/api/v1" localStorage.getItem("baseurl") + "/api/v1"
); );
// Set the initial state
this.state = { this.state = {
account: JSON.parse(localStorage.getItem("account") as string), account: JSON.parse(localStorage.getItem("account") as string),
visibility: getUserDefaultVisibility(), visibility: getUserDefaultVisibility(),
@ -87,13 +157,21 @@ class Composer extends Component<any, IComposerState> {
}; };
} }
/**
* Run any additional state checks and setup once the page has mounted. This includes
* parsing the query parameters and loading the configuration, as well as defining the
* clipboard listener.
*/
componentDidMount() { componentDidMount() {
// Parse the parameters and get the account information if available.
let state = this.getComposerParams(this.props); let state = this.getComposerParams(this.props);
let text = state.acct ? `@${state.acct}: ` : ""; let text = state.acct ? `@${state.acct}: ` : "";
this.client.get("/accounts/verify_credentials").then((resp: any) => { this.client.get("/accounts/verify_credentials").then((resp: any) => {
let account: UAccount = resp.data; let account: UAccount = resp.data;
this.setState({ account }); this.setState({ account });
}); });
// Get the configuration and load the config values.
getConfig().then((config: any) => { getConfig().then((config: any) => {
this.setState({ this.setState({
federated: config.federation.allowPublicPosts, federated: config.federation.allowPublicPosts,
@ -107,6 +185,8 @@ class Composer extends Component<any, IComposerState> {
}); });
}); });
// Attach the paste listener to listen for the clipboard and upload media
// if possible.
window.addEventListener("paste", (evt: Event) => { window.addEventListener("paste", (evt: Event) => {
let thePasteEvent = evt as ClipboardEvent; let thePasteEvent = evt as ClipboardEvent;
let fileList: File[] = []; let fileList: File[] = [];
@ -130,6 +210,12 @@ class Composer extends Component<any, IComposerState> {
}); });
} }
/**
* Reload the properties and set the state to those new properties. This usually
* occurs when the page is either reloaded or changes but React doesn't see the
* properties change.
* @param props The properties passed into the Compose component, usually the page queries.
*/
componentWillReceiveProps(props: any) { componentWillReceiveProps(props: any) {
let state = this.getComposerParams(props); let state = this.getComposerParams(props);
let text = state.acct ? `@${state.acct}: ` : ""; let text = state.acct ? `@${state.acct}: ` : "";
@ -144,6 +230,11 @@ class Composer extends Component<any, IComposerState> {
}); });
} }
/**
* Check the location string and attempt to parse it into a parsed query.
* @param location The location string from React Router.
* @returns The ParsedQuery object containing all of the parameters.
*/
checkComposerParams(location?: string): ParsedQuery { checkComposerParams(location?: string): ParsedQuery {
let params = ""; let params = "";
if (location !== undefined && typeof location === "string") { if (location !== undefined && typeof location === "string") {
@ -154,6 +245,11 @@ class Composer extends Component<any, IComposerState> {
return parseParams(params); return parseParams(params);
} }
/**
* Check the property's location string, parse it, and return it.
* @param props The properties passed into the Compose component, usually the page queries.
* @returns An object containing the reply ID, reply account, and visibility.
*/
getComposerParams(props: any) { getComposerParams(props: any) {
let params = this.checkComposerParams(props.location); let params = this.checkComposerParams(props.location);
let reply: string = ""; let reply: string = "";
@ -176,6 +272,10 @@ class Composer extends Component<any, IComposerState> {
}; };
} }
/**
* Update the text in the state and calculate the remaining character length.
* @param text The text to update the state to
*/
updateTextFromField(text: string) { updateTextFromField(text: string) {
this.setState({ this.setState({
text, text,
@ -185,14 +285,25 @@ class Composer extends Component<any, IComposerState> {
}); });
} }
/**
* Update the content warning text in the state
* @param sensitiveText The text to update the state to
*/
updateWarningFromField(sensitiveText: string) { updateWarningFromField(sensitiveText: string) {
this.setState({ sensitiveText }); this.setState({ sensitiveText });
} }
/**
* Update the visibility in the state
* @param visibility The visibility to update the state to
*/
changeVisibility(visibility: Visibility) { changeVisibility(visibility: Visibility) {
this.setState({ visibility }); this.setState({ visibility });
} }
/**
* Open a file dialog to let the user choose files to upload to the server and then upload them.
*/
promptMediaDialog() { promptMediaDialog() {
filedialog({ filedialog({
multiple: false, multiple: false,
@ -207,15 +318,27 @@ class Composer extends Component<any, IComposerState> {
}); });
} }
/**
* Upload a list of files to Mastodon as attachments. Reads the first item in the list.
* This also updates the attachments state after a successful upload.
* @param media The list of files (`FileList` or `File[]`) to send to Mastodon.
*/
uploadMedia(media: FileList | File[]) { uploadMedia(media: FileList | File[]) {
// Create a new FormData for Mastodon
let mediaForm = new FormData(); let mediaForm = new FormData();
mediaForm.append("file", media[0]); mediaForm.append("file", media[0]);
// Let the user know we're uploading the file
this.props.enqueueSnackbar("Uploading media...", { this.props.enqueueSnackbar("Uploading media...", {
persist: true, persist: true,
key: "media-upload" key: "media-upload"
}); });
// Try to upload the media to the server.
this.client this.client
.post("/media", mediaForm) .post("/media", mediaForm)
// If we succeed, get the attachments and update the state.
.then((resp: any) => { .then((resp: any) => {
let attachment: Attachment = resp.data; let attachment: Attachment = resp.data;
let attachments = this.state.attachments; let attachments = this.state.attachments;
@ -228,6 +351,8 @@ class Composer extends Component<any, IComposerState> {
this.props.closeSnackbar("media-upload"); this.props.closeSnackbar("media-upload");
this.props.enqueueSnackbar("Media uploaded."); this.props.enqueueSnackbar("Media uploaded.");
}) })
// If we fail, display an error.
.catch((err: Error) => { .catch((err: Error) => {
this.props.closeSnackbar("media-upload"); this.props.closeSnackbar("media-upload");
this.props.enqueueSnackbar( this.props.enqueueSnackbar(
@ -237,6 +362,10 @@ class Composer extends Component<any, IComposerState> {
}); });
} }
/**
* Iterate through the attachments and grab the attachments' IDs.
* @returns A list of IDs as `string[]`
*/
getOnlyMediaIds() { getOnlyMediaIds() {
let ids: string[] = []; let ids: string[] = [];
if (this.state.attachments) { if (this.state.attachments) {
@ -247,6 +376,10 @@ class Composer extends Component<any, IComposerState> {
return ids; return ids;
} }
/**
* Update the list of attachments by inserting an attachment.
* @param attachment The attachment to insert into the attachments list.
*/
fetchAttachmentAfterUpdate(attachment: Attachment) { fetchAttachmentAfterUpdate(attachment: Attachment) {
let attachments = this.state.attachments; let attachments = this.state.attachments;
if (attachments) { if (attachments) {
@ -259,6 +392,10 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Remove an attachment from the list of attachments and update the state.
* @param attachment The attachment to remove from the list
*/
deleteMediaAttachment(attachment: Attachment) { deleteMediaAttachment(attachment: Attachment) {
let attachments = this.state.attachments; let attachments = this.state.attachments;
if (attachments) { if (attachments) {
@ -272,6 +409,10 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Insert an emoji at the end of text string and update the state
* @param e The emoji to insert into the text
*/
insertEmoji(e: any) { insertEmoji(e: any) {
if (e.custom) { if (e.custom) {
let text = this.state.text + e.colons; let text = this.state.text + e.colons;
@ -288,6 +429,9 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Create an empty poll.
*/
createPoll() { createPoll() {
if (this.state.poll === undefined) { if (this.state.poll === undefined) {
let expiration = new Date(); let expiration = new Date();
@ -307,6 +451,9 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Insert a new poll item into the poll.
*/
addPollItem() { addPollItem() {
if ( if (
this.state.poll !== undefined && this.state.poll !== undefined &&
@ -329,6 +476,11 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Edit an existing poll item with new text
* @param position The position of the poll item in the list
* @param newTitle The new text to update
*/
editPollItem(position: number, newTitle: any) { editPollItem(position: number, newTitle: any) {
if (this.state.poll !== undefined) { if (this.state.poll !== undefined) {
let poll = this.state.poll; let poll = this.state.poll;
@ -346,6 +498,10 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Removes a poll item from the poll
* @param item The item to remove
*/
removePollItem(item: string) { removePollItem(item: string) {
if ( if (
this.state.poll !== undefined && this.state.poll !== undefined &&
@ -372,6 +528,10 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Set the expiration date of the poll.
* @param date The new expiration date
*/
setPollExpires(date: string) { setPollExpires(date: string) {
let currentDate = new Date(); let currentDate = new Date();
let newDate = new Date(date); let newDate = new Date(date);
@ -391,25 +551,38 @@ class Composer extends Component<any, IComposerState> {
} }
} }
/**
* Remove the poll from the post.
*/
removePoll() { removePoll() {
this.setState({ this.setState({
poll: undefined poll: undefined
}); });
} }
/**
* Check if the user presses the Ctrl/Cmd+Enter key and post to the server if possible.
* @param event The keyboard event
*/
postViaKeyboard(event: any) { postViaKeyboard(event: any) {
if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) { if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) {
this.post(); this.post();
} }
} }
/**
* Send the post to Mastodon and return to the previous page, if possible.
*/
post() { post() {
// First, finalize the poll.
let pollOptions: string[] = []; let pollOptions: string[] = [];
if (this.state.poll) { if (this.state.poll) {
this.state.poll.options.forEach((option: PollWizardOption) => { this.state.poll.options.forEach((option: PollWizardOption) => {
pollOptions.push(option.title); pollOptions.push(option.title);
}); });
} }
// Send a post request to Mastodon.
this.client this.client
.post("/statuses", { .post("/statuses", {
status: this.state.text, status: this.state.text,
@ -426,28 +599,44 @@ class Composer extends Component<any, IComposerState> {
} }
: null : null
}) })
// If we succeed, send a success message and go back.
.then(() => { .then(() => {
this.props.enqueueSnackbar("Posted!"); this.props.enqueueSnackbar("Posted!");
window.history.back(); window.history.back();
}) })
// Otherwise, show an error message and don't do anything.
.catch((err: Error) => { .catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't post: " + err.name); this.props.enqueueSnackbar("Couldn't post: " + err.name);
console.error(err.message); console.error(err.message);
}); });
} }
/**
* Toggle the content warning section.
*/
toggleSensitive() { toggleSensitive() {
this.setState({ sensitive: !this.state.sensitive }); this.setState({ sensitive: !this.state.sensitive });
} }
/**
* Toggle the visibility drop down menu.
*/
toggleVisibilityMenu() { toggleVisibilityMenu() {
this.setState({ visibilityMenu: !this.state.visibilityMenu }); this.setState({ visibilityMenu: !this.state.visibilityMenu });
} }
/**
* Toggle the emoji picker.
*/
toggleEmojis() { toggleEmojis() {
this.setState({ showEmojis: !this.state.showEmojis }); this.setState({ showEmojis: !this.state.showEmojis });
} }
/**
* Render all of the components on the page given a set of classes.
*/
render() { render() {
const { classes } = this.props; const { classes } = this.props;