mirror of
https://github.com/hyperspacedev/hyperspace
synced 2025-02-07 12:53:31 +01:00
Merge pull request #134 from hyperspacedev/HD-25-clipboard-fire-fix
HD-25 #done
This commit is contained in:
commit
68ff7e8663
@ -35,7 +35,8 @@ export const styles = (theme: Theme) =>
|
||||
titleBarText: {
|
||||
fontSize: 12,
|
||||
paddingTop: 2,
|
||||
paddingBottom: 1
|
||||
paddingBottom: 1,
|
||||
color: theme.palette.getContrastText(theme.palette.primary.main)
|
||||
},
|
||||
appBar: {
|
||||
zIndex: 1000,
|
||||
|
@ -46,34 +46,104 @@ import {
|
||||
} from "../utilities/settings";
|
||||
import { draftExists, writeDraft, loadDraft } from "../utilities/compose";
|
||||
|
||||
/**
|
||||
* The state for the Composer page.
|
||||
*/
|
||||
interface IComposerState {
|
||||
/**
|
||||
* The current user as an Account.
|
||||
*/
|
||||
account: UAccount;
|
||||
|
||||
/**
|
||||
* The visibility of the post.
|
||||
*/
|
||||
visibility: Visibility;
|
||||
|
||||
/**
|
||||
* Whether there should be a content warning.
|
||||
*/
|
||||
sensitive: boolean;
|
||||
|
||||
/**
|
||||
* The content warning message.
|
||||
*/
|
||||
sensitiveText?: string;
|
||||
|
||||
/**
|
||||
* Whether the visibility drop-down should be visible.
|
||||
*/
|
||||
visibilityMenu: boolean;
|
||||
|
||||
/**
|
||||
* The text contents of the post.
|
||||
*/
|
||||
text: string;
|
||||
|
||||
/**
|
||||
* The remaining amount of characters.
|
||||
*/
|
||||
remainingChars: number;
|
||||
|
||||
/**
|
||||
* An optional reply ID.
|
||||
*/
|
||||
reply?: string;
|
||||
|
||||
/**
|
||||
* The account to reply to, if it exists.
|
||||
*/
|
||||
acct?: string;
|
||||
|
||||
/**
|
||||
* An optional list of media attachments.
|
||||
*/
|
||||
attachments?: [Attachment];
|
||||
|
||||
/**
|
||||
* An optional poll for the post.
|
||||
*/
|
||||
poll?: PollWizard;
|
||||
|
||||
/**
|
||||
* The expiration date of a poll, if it exists.
|
||||
*/
|
||||
pollExpiresDate?: any;
|
||||
|
||||
/**
|
||||
* Whether the emoji picker should be visible.
|
||||
*/
|
||||
showEmojis: boolean;
|
||||
|
||||
/**
|
||||
* Whether or not the account's instance is federated.
|
||||
*/
|
||||
federated: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* The Compose page contains all of the information to create a UI for post creation.
|
||||
*/
|
||||
class Composer extends Component<any, IComposerState> {
|
||||
/**
|
||||
* The Mastodon client to work with.
|
||||
*/
|
||||
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) {
|
||||
super(props);
|
||||
|
||||
// Generate the Mastodon client
|
||||
this.client = new Mastodon(
|
||||
localStorage.getItem("access_token") as string,
|
||||
localStorage.getItem("baseurl") + "/api/v1"
|
||||
);
|
||||
|
||||
// Set the initial state
|
||||
this.state = {
|
||||
account: JSON.parse(localStorage.getItem("account") as string),
|
||||
visibility: getUserDefaultVisibility(),
|
||||
@ -88,13 +158,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() {
|
||||
// Parse the parameters and get the account information if available.
|
||||
let state = this.getComposerParams(this.props);
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
this.client.get("/accounts/verify_credentials").then((resp: any) => {
|
||||
let account: UAccount = resp.data;
|
||||
this.setState({ account });
|
||||
});
|
||||
|
||||
// Get the configuration and load the config values.
|
||||
getConfig().then((config: any) => {
|
||||
this.setState({
|
||||
federated: config.federation.allowPublicPosts,
|
||||
@ -108,6 +186,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) => {
|
||||
let thePasteEvent = evt as ClipboardEvent;
|
||||
let fileList: File[] = [];
|
||||
@ -122,12 +202,21 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
this.actuallyUploadMedia(fileList);
|
||||
|
||||
if (fileList.length > 0) {
|
||||
this.uploadMedia(fileList);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let state = this.getComposerParams(props);
|
||||
let text = state.acct ? `@${state.acct}: ` : "";
|
||||
@ -167,6 +256,11 @@ class Composer extends Component<any, IComposerState> {
|
||||
this.props.enqueueSnackbar("Restored draft.");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 {
|
||||
let params = "";
|
||||
if (location !== undefined && typeof location === "string") {
|
||||
@ -177,6 +271,11 @@ class Composer extends Component<any, IComposerState> {
|
||||
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) {
|
||||
let params = this.checkComposerParams(props.location);
|
||||
let reply: string = "";
|
||||
@ -199,6 +298,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) {
|
||||
this.setState({
|
||||
text,
|
||||
@ -208,20 +311,31 @@ 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) {
|
||||
this.setState({ sensitiveText });
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the visibility in the state
|
||||
* @param visibility The visibility to update the state to
|
||||
*/
|
||||
changeVisibility(visibility: Visibility) {
|
||||
this.setState({ visibility });
|
||||
}
|
||||
|
||||
uploadMedia() {
|
||||
/**
|
||||
* Open a file dialog to let the user choose files to upload to the server and then upload them.
|
||||
*/
|
||||
promptMediaDialog() {
|
||||
filedialog({
|
||||
multiple: false,
|
||||
accept: ".jpeg,.jpg,.png,.gif,.webm,.mp4,.mov,.ogg,.wav,.mp3,.flac"
|
||||
})
|
||||
.then((media: FileList) => this.actuallyUploadMedia(media))
|
||||
.then((media: FileList) => this.uploadMedia(media))
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't get media: " + err.name, {
|
||||
variant: "error"
|
||||
@ -230,15 +344,27 @@ class Composer extends Component<any, IComposerState> {
|
||||
});
|
||||
}
|
||||
|
||||
actuallyUploadMedia(media: FileList | File[]) {
|
||||
/**
|
||||
* 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[]) {
|
||||
// Create a new FormData for Mastodon
|
||||
let mediaForm = new FormData();
|
||||
mediaForm.append("file", media[0]);
|
||||
|
||||
// Let the user know we're uploading the file
|
||||
this.props.enqueueSnackbar("Uploading media...", {
|
||||
persist: true,
|
||||
key: "media-upload"
|
||||
});
|
||||
|
||||
// Try to upload the media to the server.
|
||||
this.client
|
||||
.post("/media", mediaForm)
|
||||
|
||||
// If we succeed, get the attachments and update the state.
|
||||
.then((resp: any) => {
|
||||
let attachment: Attachment = resp.data;
|
||||
let attachments = this.state.attachments;
|
||||
@ -251,6 +377,8 @@ class Composer extends Component<any, IComposerState> {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar("Media uploaded.");
|
||||
})
|
||||
|
||||
// If we fail, display an error.
|
||||
.catch((err: Error) => {
|
||||
this.props.closeSnackbar("media-upload");
|
||||
this.props.enqueueSnackbar(
|
||||
@ -260,6 +388,10 @@ class Composer extends Component<any, IComposerState> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate through the attachments and grab the attachments' IDs.
|
||||
* @returns A list of IDs as `string[]`
|
||||
*/
|
||||
getOnlyMediaIds() {
|
||||
let ids: string[] = [];
|
||||
if (this.state.attachments) {
|
||||
@ -270,6 +402,10 @@ class Composer extends Component<any, IComposerState> {
|
||||
return ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the list of attachments by inserting an attachment.
|
||||
* @param attachment The attachment to insert into the attachments list.
|
||||
*/
|
||||
fetchAttachmentAfterUpdate(attachment: Attachment) {
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
@ -282,6 +418,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) {
|
||||
let attachments = this.state.attachments;
|
||||
if (attachments) {
|
||||
@ -295,6 +435,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) {
|
||||
if (e.custom) {
|
||||
let text = this.state.text + e.colons;
|
||||
@ -311,6 +455,9 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty poll.
|
||||
*/
|
||||
createPoll() {
|
||||
if (this.state.poll === undefined) {
|
||||
let expiration = new Date();
|
||||
@ -330,6 +477,9 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Insert a new poll item into the poll.
|
||||
*/
|
||||
addPollItem() {
|
||||
if (
|
||||
this.state.poll !== undefined &&
|
||||
@ -352,6 +502,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) {
|
||||
if (this.state.poll !== undefined) {
|
||||
let poll = this.state.poll;
|
||||
@ -369,6 +524,10 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a poll item from the poll
|
||||
* @param item The item to remove
|
||||
*/
|
||||
removePollItem(item: string) {
|
||||
if (
|
||||
this.state.poll !== undefined &&
|
||||
@ -395,6 +554,10 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the expiration date of the poll.
|
||||
* @param date The new expiration date
|
||||
*/
|
||||
setPollExpires(date: string) {
|
||||
let currentDate = new Date();
|
||||
let newDate = new Date(date);
|
||||
@ -414,25 +577,38 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the poll from the post.
|
||||
*/
|
||||
removePoll() {
|
||||
this.setState({
|
||||
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) {
|
||||
if ((event.metaKey || event.ctrlKey) && event.keyCode === 13) {
|
||||
this.post();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the post to Mastodon and return to the previous page, if possible.
|
||||
*/
|
||||
post() {
|
||||
// First, finalize the poll.
|
||||
let pollOptions: string[] = [];
|
||||
if (this.state.poll) {
|
||||
this.state.poll.options.forEach((option: PollWizardOption) => {
|
||||
pollOptions.push(option.title);
|
||||
});
|
||||
}
|
||||
|
||||
// Send a post request to Mastodon.
|
||||
this.client
|
||||
.post("/statuses", {
|
||||
status: this.state.text,
|
||||
@ -449,28 +625,44 @@ class Composer extends Component<any, IComposerState> {
|
||||
}
|
||||
: null
|
||||
})
|
||||
|
||||
// If we succeed, send a success message and go back.
|
||||
.then(() => {
|
||||
this.props.enqueueSnackbar("Posted!");
|
||||
window.history.back();
|
||||
})
|
||||
|
||||
// Otherwise, show an error message and don't do anything.
|
||||
.catch((err: Error) => {
|
||||
this.props.enqueueSnackbar("Couldn't post: " + err.name);
|
||||
console.error(err.message);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the content warning section.
|
||||
*/
|
||||
toggleSensitive() {
|
||||
this.setState({ sensitive: !this.state.sensitive });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the visibility drop down menu.
|
||||
*/
|
||||
toggleVisibilityMenu() {
|
||||
this.setState({ visibilityMenu: !this.state.visibilityMenu });
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle the emoji picker.
|
||||
*/
|
||||
toggleEmojis() {
|
||||
this.setState({ showEmojis: !this.state.showEmojis });
|
||||
}
|
||||
|
||||
/**
|
||||
* Render all of the components on the page given a set of classes.
|
||||
*/
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
@ -678,7 +870,7 @@ class Composer extends Component<any, IComposerState> {
|
||||
<Tooltip title="Add photos, videos, or audio">
|
||||
<IconButton
|
||||
disabled={this.state.poll !== undefined}
|
||||
onClick={() => this.uploadMedia()}
|
||||
onClick={() => this.promptMediaDialog()}
|
||||
id="compose-media"
|
||||
>
|
||||
<AttachFileIcon />
|
||||
|
Loading…
x
Reference in New Issue
Block a user