hyperspace-desktop-client-w.../src/components/Post/Post.tsx

342 lines
14 KiB
TypeScript

import React from 'react';
import { Typography, IconButton, Card, CardHeader, Avatar, CardContent, CardActions, withStyles, Menu, MenuItem, Chip, Divider, CardMedia, CardActionArea, ExpansionPanel, ExpansionPanelSummary, ExpansionPanelDetails, Zoom, Tooltip } from '@material-ui/core';
import MoreVertIcon from '@material-ui/icons/MoreVert';
import ReplyIcon from '@material-ui/icons/Reply';
import FavoriteIcon from '@material-ui/icons/Favorite';
import AutorenewIcon from '@material-ui/icons/Autorenew';
import OpenInNewIcon from '@material-ui/icons/OpenInNew';
import PublicIcon from '@material-ui/icons/Public';
import VisibilityOffIcon from '@material-ui/icons/VisibilityOff';
import WarningIcon from '@material-ui/icons/Warning';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import GroupIcon from '@material-ui/icons/Group';
import ForumIcon from '@material-ui/icons/Forum';
import AlternateEmailIcon from '@material-ui/icons/AlternateEmail';
import {styles} from './Post.styles';
import { Status } from '../../types/Status';
import { Mention } from '../../types/Mention';
import { Visibility } from '../../types/Visibility';
import moment from 'moment';
import { MastodonEmoji } from '../../types/Emojis';
import AttachmentComponent from '../Attachment';
import Mastodon from 'megalodon';
import { LinkableChip, LinkableMenuItem, LinkableIconButton } from '../../interfaces/overrides';
import {withSnackbar} from 'notistack';
import ShareMenu from './PostShareMenu';
interface IPostProps {
post: Status;
classes: any;
client: Mastodon;
}
interface IPostState {
post: Status;
media_slides?: number;
menuIsOpen: boolean;
}
export class Post extends React.Component<any, IPostState> {
client: Mastodon;
constructor(props: any) {
super(props);
this.state = {
post: this.props.post,
media_slides: this.props.post.media_attachments.length > 0? this.props.post.media_attachments.length: 0,
menuIsOpen: false
}
this.client = this.props.client;
}
togglePostMenu() {
this.setState({ menuIsOpen: !this.state.menuIsOpen })
}
materializeContent(status: Status) {
const { classes } = this.props;
const oldContent = document.createElement('div');
oldContent.innerHTML = status.content;
if (status.emojis !== undefined && status.emojis.length > 0) {
status.emojis.forEach((emoji: MastodonEmoji) => {
let regexp = new RegExp(':' + emoji.shortcode + ':', 'g');
oldContent.innerHTML = oldContent.innerHTML.replace(regexp, `<img src="${emoji.static_url}" class="${classes.postEmoji}"/>`)
})
}
return (
<div className={classes.mediaContainer}>
<Typography paragraph dangerouslySetInnerHTML={{__html: oldContent.innerHTML}}/>
{
status.card?
<div className={classes.postCard}>
<Divider/>
<CardActionArea href={status.card.url} target="_blank" rel="noreferrer">
<CardContent>
<Typography gutterBottom variant="h6" component="h2">{status.card.title}</Typography>
<Typography>{status.card.description}</Typography>
</CardContent>
{
status.card.image?
<CardMedia className={classes.postMedia} image={status.card.image}/>: <span/>
}
<CardContent>
<Typography>{status.card.provider_url|| status.card.author_url || status.card.author_url}</Typography>
</CardContent>
</CardActionArea>
<Divider/>
</div>:
<span/>
}
{
status.media_attachments.length > 0?
<AttachmentComponent media={status.media_attachments}/>:
<span/>
}
</div>
);
}
getSensitiveContent(spoiler_text: string, content: Status) {
const { classes } = this.props;
const warningText = spoiler_text || "Unmarked content";
let icon;
if (spoiler_text.includes("NSFW") || spoiler_text.includes("Spoiler") || warningText === "Unmarked content") {
icon = <WarningIcon className={classes.postWarningIcon}/>;
}
return (
<ExpansionPanel>
<ExpansionPanelSummary expandIcon={<ExpandMoreIcon/>}>
{icon}<Typography className={classes.heading}>{warningText}</Typography>
</ExpansionPanelSummary>
<ExpansionPanelDetails className={classes.postContent}>
{this.materializeContent(content)}
</ExpansionPanelDetails>
</ExpansionPanel>
)
}
getReblogOfPost(of: Status | null) {
const { classes } = this.props;
if (of !== null) {
return (
<CardContent className={classes.postContent}>
<LinkableChip
avatar={
<Avatar alt={of.account.acct} src={of.account.avatar_static}/>
}
className={classes.postReblogChip}
label={`@${of.account.username} posted:`}
key={of.id + "_reblog_chip_" + of.account.id}
to={`/profile/${of.account.id}`}
replace={true}
clickable
/>
{of.sensitive? this.getSensitiveContent(of.spoiler_text, of): this.materializeContent(of)}
</CardContent>
);
} else {
return null;
}
}
getMentions(mention: [Mention]) {
if (mention.length > 0) {
return (
<CardContent>
<Typography variant="caption">Mentions</Typography>
{
this.state.post.mentions.map((person: Mention) => {
return <LinkableChip
avatar={
<Avatar>
<AlternateEmailIcon/>
</Avatar>
}
label={person.username}
key={this.state.post.id + "_mention_" + person.id}
to={`/profile/${person.id}`}
clickable
/>
})
}
</CardContent>
)
} else {
return null;
}
}
showVisibilityIcon(visibility: Visibility) {
const { classes } = this.props;
switch(visibility) {
case "public":
return <PublicIcon className={classes.postTypeIcon}/>;
case "private":
return <GroupIcon className={classes.postTypeIcon}/>;
case "unlisted":
return <VisibilityOffIcon className={classes.postTypeIcon}/>
}
}
getMastodonUrl(post: Status) {
let url = "";
if (post.reblog) {
url = post.reblog.uri
} else {
url = post.uri
}
return url;
}
toggleFavorited(post: Status) {
let _this = this;
if (post.favourited) {
this.client.post(`/statuses/${post.id}/unfavourite`).then((resp: any) => {
let post: Status = resp.data;
this.setState({ post });
}).catch((err: Error) => {
_this.props.enqueueSnackbar(`Couldn't unfavorite post: ${err.name}`, {
variant: 'error'
})
console.log(err.message);
})
} else {
this.client.post(`/statuses/${post.id}/favourite`).then((resp: any) => {
let post: Status = resp.data;
this.setState({ post });
}).catch((err: Error) => {
_this.props.enqueueSnackbar(`Couldn't favorite post: ${err.name}`, {
variant: 'error'
})
console.log(err.message);
})
}
}
toggleReblogged(post: Status) {
if (post.reblogged) {
this.client.post(`/statuses/${post.id}/unreblog`).then((resp: any) => {
let post = this.state.post;
post.reblogged = false;
this.setState({ post });
}).catch((err: Error) => {
this.props.enqueueSnackbar(`Couldn't unboost post: ${err.name}`, {
variant: 'error'
})
console.log(err.message);
})
} else {
this.client.post(`/statuses/${post.id}/reblog`).then((resp: any) => {
let post = this.state.post;
post.reblogged = true;
this.setState({ post });
}).catch((err: Error) => {
this.props.enqueueSnackbar(`Couldn't boost post: ${err.name}`, {
variant: 'error'
})
console.log(err.message);
})
}
}
render() {
const { classes } = this.props;
return (
<Zoom in={true}>
<Card className={classes.post}>
<CardHeader avatar={<Avatar src={this.state.post.account.avatar_static} />} action={
<IconButton key={`${this.state.post.id}_submenu`} id={`${this.state.post.id}_submenu`} onClick={() => this.togglePostMenu()}>
<MoreVertIcon />
</IconButton>}
title={
`${this.state.post.account.display_name || this.state.post.account.username} (@${this.state.post.account.acct})`
} subheader={moment(this.state.post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
{
this.state.post.reblog? this.getReblogOfPost(this.state.post.reblog): null
}
{
this.state.post.sensitive? this.getSensitiveContent(this.state.post.spoiler_text, this.state.post):
<CardContent className={classes.postContent}>
{this.state.post.reblog? null: this.materializeContent(this.state.post)}
</CardContent>
}
{
this.getMentions(this.state.post.mentions)
}
{
this.state.post.reblog && this.state.post.reblog.mentions.length > 0? this.getMentions(this.state.post.reblog.mentions): <span/>
}
<CardActions>
<Tooltip title="Reply">
<LinkableIconButton to={`/compose?reply=${this.state.post.id}`}>
<ReplyIcon/>
</LinkableIconButton>
</Tooltip>
<Typography>{this.state.post.replies_count}</Typography>
<Tooltip title="Favorite">
<IconButton onClick={() => this.toggleFavorited(this.state.post)}>
<FavoriteIcon className={this.state.post.favourited? classes.postDidAction: ''}/>
</IconButton>
</Tooltip>
<Typography>{this.state.post.favourites_count}</Typography>
<Tooltip title="Boost">
<IconButton onClick={() => this.toggleReblogged(this.state.post)}>
<AutorenewIcon className={this.state.post.reblogged? classes.postDidAction: ''}/>
</IconButton>
</Tooltip>
<Typography>{this.state.post.reblogs_count}</Typography>
<Tooltip title="View thread">
<LinkableIconButton to={`/conversation/${this.state.post.id}`}>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Open in Web">
<IconButton href={this.getMastodonUrl(this.state.post)} rel="noreferrer" target="_blank">
<OpenInNewIcon />
</IconButton>
</Tooltip>
<div className={classes.postFlexGrow} />
<div className={classes.postTypeIconDiv}>
{this.showVisibilityIcon(this.state.post.visibility)}
</div>
</CardActions>
<Menu
id="postmenu"
anchorEl={document.getElementById(`${this.state.post.id}_submenu`)}
open={this.state.menuIsOpen}
onClose={() => this.togglePostMenu()}
>
<ShareMenu config={{
params: {
title: `@${this.state.post.account.username} posted on Mastodon: `,
text: this.state.post.content,
url: this.getMastodonUrl(this.state.post),
},
onShareSuccess: () => this.props.enqueueSnackbar("Post shared!", {variant: 'success'}),
onShareError: (error: Error) => {
if (error.name != "AbortError")
this.props.enqueueSnackbar(`Couldn't share post: ${error.name}`, {variant: 'error'})
},
}}/>
<LinkableMenuItem to={`/profile/${this.state.post.account.id}`}>View author profile</LinkableMenuItem>
{
this.state.post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
<div>
<Divider/>
<MenuItem>Delete</MenuItem>
</div>:
null
}
</Menu>
</Card>
</Zoom>
);
}
}
export default withStyles(styles)(withSnackbar(Post));