Add documentation to the Compose page (HD-24)
This commit is contained in:
parent
b4496720a3
commit
6c8035465a
|
@ -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;
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue