Update composer with media attachment support

This commit is contained in:
Marquis Kurt 2019-04-05 12:44:03 -04:00
parent 57a2a0856d
commit c64cf59165
8 changed files with 170 additions and 6 deletions

View File

@ -16,4 +16,6 @@ The 1.0 redesign of Hyperspace acts differently from the current classic version
- **Pages over panels**. Hyperspace 1.0 uses `react-router-dom` to link components of the app via URLs instead of individual components. This means that one could visit the corresponding URL instead of needing to open a set of panels or buttons to do so.
- **Material design**. Hyperspace 1.0 uses Material Design to create a UI that scales across device types. The library used for the UI, `material-ui`, natively supports a dark mode and themes, making Hyperspace 1.0 more customizable in nature.
- **Less intrusive by nature.** Hyperspace 1.0 pushes notifications when the window isn't in focus versus all the time, and the snackbar (toast) notifications are displayed more often when something is happening or when an error occurs. Timelines also no longer keep pushing posts during streaming, letting anyone read the timeline and still be able to get updates with a non-intrusive `View (x) new posts` chip at the top.
- **Less intrusive by nature.** Hyperspace 1.0 pushes notifications when the window isn't in focus versus all the time, and the snackbar (toast) notifications are displayed more often when something is happening or when an error occurs. Timelines also no longer keep pushing posts during streaming, letting anyone read the timeline and still be able to get updates with a non-intrusive `View (x) new posts` chip at the top.
This is a growing list and new things will be added over time.

6
package-lock.json generated
View File

@ -17943,9 +17943,9 @@
}
},
"typescript": {
"version": "3.3.4000",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.3.4000.tgz",
"integrity": "sha512-jjOcCZvpkl2+z7JFn0yBOoLQyLoIkNZAs/fYJkUG6VKy6zLPHJGfQJYFHzibB6GJaF/8QrcECtlQ5cpvRHSMEA==",
"version": "3.4.1",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-3.4.1.tgz",
"integrity": "sha512-3NSMb2VzDQm8oBTLH6Nj55VVtUEpe/rgkIzMir0qVoLyjDZlnMBva0U6vDiV3IH+sl/Yu6oP5QwsAQtHPmDd2Q==",
"dev": true
},
"ua-parser-js": {

View File

@ -19,7 +19,7 @@
"react-router-dom": "^5.0.0",
"react-scripts": "2.1.8",
"react-swipeable-views": "^0.13.1",
"typescript": "3.3.4000",
"typescript": "3.4.1",
"notistack": "^0.5.1",
"react-web-share-api": "^0.0.2",
"query-string": "^6.4.2",

View File

@ -0,0 +1,17 @@
import {Theme, createStyles} from '@material-ui/core';
export const styles = (theme: Theme) => createStyles({
attachmentArea: {
height: 175,
width: 268,
backgroundColor: theme.palette.primary.light,
color: theme.palette.common.white
},
attachmentBar: {
marginLeft: 0
},
attachmentText: {
backgroundColor: theme.palette.background.paper,
opacity: 0.5
}
});

View File

@ -0,0 +1,81 @@
import React, { Component } from 'react';
import {GridListTile, GridListTileBar, TextField, withStyles, IconButton} from '@material-ui/core';
import {styles} from './ComposeMediaAttachment.styles';
import {withSnackbar, withSnackbarProps} from 'notistack';
import Mastodon from 'megalodon';
import { Attachment } from '../../types/Attachment';
import DeleteIcon from '@material-ui/icons/Delete';
interface IComposeMediaAttachmentProps extends withSnackbarProps {
classes: any;
client: Mastodon;
attachment: Attachment;
onDeleteCallback: any;
onAttachmentUpdate: any;
}
interface IComposeMediaAttachmentState {
attachment: Attachment;
}
class ComposeMediaAttachment extends Component<IComposeMediaAttachmentProps, IComposeMediaAttachmentState> {
client: Mastodon;
constructor(props: IComposeMediaAttachmentProps) {
super(props);
this.client = this.props.client;
this.state = {
attachment: this.props.attachment
}
}
updateAttachmentText(text: string) {
this.client.put(`/media/${this.state.attachment.id}`, { description: text }).then((resp: any) => {
this.props.onAttachmentUpdate(resp.data);
this.props.enqueueSnackbar("Description updated.")
}).catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't update description: " + err.name);
})
}
render() {
const { classes, attachment } = this.props;
return (
<GridListTile className={classes.attachmentArea}>
{
attachment.type === "image" || attachment.type === "gifv"?
<img src={attachment.url} alt={attachment.description? attachment.description: ""}/>:
attachment.type === "video"?
<video autoPlay={false} src={attachment.url}/>:
<object data={attachment.url}/>
}
<GridListTileBar
classes={{ title: classes.attachmentBar }}
title={
<TextField
variant="filled"
label="Description"
margin="dense"
className={classes.attachmentText}
onBlur={(event) => this.updateAttachmentText(event.target.value)}
></TextField>
}
actionIcon={
<IconButton color="inherit"
onClick={
() => this.props.onDeleteCallback(this.state.attachment)
}
>
<DeleteIcon/>
</IconButton>
}
/>
</GridListTile>
)
}
}
export default withStyles(styles)(withSnackbar(ComposeMediaAttachment));

View File

@ -0,0 +1,5 @@
import {styles} from './ComposeMediaAttachment.styles';
import ComposeMediaAttachment from './ComposeMediaAttachment';
import {withStyles} from '@material-ui/core';
export default withStyles(styles)(ComposeMediaAttachment);

View File

@ -16,5 +16,15 @@ export const styles = (theme: Theme) => createStyles({
warningCaption: {
height: 16,
verticalAlign: "text-bottom"
},
composeAttachmentArea: {
display: 'flex',
flexWrap: 'wrap',
justifyContent: 'space-around',
overflow: 'hidden'
},
composeAttachmentAreaGridList: {
height: 250,
width: '100%'
}
});

View File

@ -1,5 +1,5 @@
import React, {Component} from 'react';
import { Dialog, DialogContent, DialogActions, withStyles, Button, CardHeader, Avatar, TextField, Toolbar, IconButton, Fade, Typography, Tooltip, Menu, MenuItem } from '@material-ui/core';
import { Dialog, DialogContent, DialogActions, withStyles, Button, CardHeader, Avatar, TextField, Toolbar, IconButton, Fade, Typography, Tooltip, Menu, MenuItem, GridList, ListSubheader, GridListTile } from '@material-ui/core';
import {styles} from './Compose.styles';
import { UAccount } from '../types/Account';
import { Visibility } from '../types/Visibility';
@ -13,6 +13,7 @@ import {withSnackbar} from 'notistack';
import { Attachment } from '../types/Attachment';
import { PollWizard } from '../types/Poll';
import filedialog from 'file-dialog';
import ComposeMediaAttachment from '../components/ComposeMediaAttachment';
interface IComposerState {
@ -122,6 +123,31 @@ class Composer extends Component<any, IComposerState> {
this.setState({ visibilityMenu: !this.state.visibilityMenu });
}
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.");
}
}
render() {
const {classes} = this.props;
@ -170,6 +196,29 @@ class Composer extends Component<any, IComposerState> {
<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.length > 0?
this.state.attachments.map((attachment: Attachment) => {
return <ComposeMediaAttachment
key={attachment.id + "_compose_attachment"}
client={this.client}
attachment={attachment}
onAttachmentUpdate={(attachment: Attachment) => this.fetchAttachmentAfterUpdate(attachment)}
onDeleteCallback={(attachment: Attachment) => this.deleteMediaAttachment(attachment)}
/>
})
:null
}
</GridList>
</div>: null
}
</DialogContent>
<Toolbar className={classes.dialogActions}>
<Tooltip title="Add photos or videos">