mirror of
https://github.com/hyperspacedev/hyperspace
synced 2025-01-30 17:14:57 +01:00
Update composer with media attachment support
This commit is contained in:
parent
57a2a0856d
commit
c64cf59165
@ -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
6
package-lock.json
generated
@ -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": {
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
}
|
||||
});
|
@ -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));
|
5
src/components/ComposeMediaAttachment/index.tsx
Normal file
5
src/components/ComposeMediaAttachment/index.tsx
Normal 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);
|
@ -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%'
|
||||
}
|
||||
});
|
@ -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">
|
||||
|
Loading…
x
Reference in New Issue
Block a user