Add Conversation UI

This commit is contained in:
Marquis Kurt 2019-03-31 21:56:41 -04:00
parent 6dc2b2dc0d
commit 35df20ce8a
7 changed files with 251 additions and 113 deletions

View File

@ -11,6 +11,7 @@ import ProfilePage from './pages/ProfilePage';
import HomePage from './pages/Home';
import LocalPage from './pages/Local';
import PublicPage from './pages/Public';
import Conversation from './pages/Conversation';
import NotificationsPage from './pages/Notifications';
import {withSnackbar} from 'notistack';
let theme = setHyperspaceTheme(getUserDefaultTheme());
@ -46,7 +47,7 @@ class App extends Component<any, any> {
<Route path="/messages"/>
<Route path="/notifications" component={NotificationsPage}/>
<Route path="/profile/:profileId" render={props => <ProfilePage {...props}></ProfilePage>}/>
<Route path="/conversation/:conversationId"/>
<Route path="/conversation/:conversationId" component={Conversation}/>
<Route path="/settings" component={Settings}/>
<Route path="/about" component={AboutPage}/>
</MuiThemeProvider>

View File

@ -182,7 +182,7 @@ export class AppLayout extends Component<any, IAppLayoutState> {
<ListItemAvatar>
<Avatar alt="You" src={this.state.currentUser.avatar_static}/>
</ListItemAvatar>
<ListItemText primary={this.state.currentUser.display_name} secondary={this.state.currentUser.acct}/>
<ListItemText primary={this.state.currentUser.display_name || this.state.currentUser.acct} secondary={'@' + this.state.currentUser.acct}/>
</LinkableListItem>
<Divider/>
<MenuItem>Switch account</MenuItem>

View File

@ -290,116 +290,116 @@ export class Post extends React.Component<any, IPostState> {
const post = this.state.post;
return (
<Zoom in={true}>
<Card className={classes.post}>
<CardHeader avatar={
<Avatar src={
post.reblog? post.reblog.account.avatar_static: post.account.avatar_static
} />
} action={
<IconButton key={`${post.id}_submenu`} id={`${post.id}_submenu`} onClick={() => this.togglePostMenu()}>
<MoreVertIcon />
</IconButton>}
title={this.getReblogAuthors(post)} subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
{
post.reblog? this.getReblogOfPost(post.reblog): null
}
{
post.sensitive? this.getSensitiveContent(post.spoiler_text, post):
post.reblog? null: this.materializeContent(post)
}
{
post.reblog && post.reblog.mentions.length > 0? this.getMentions(post.reblog.mentions): this.getMentions(post.mentions)
}
{
post.reblog && post.reblog.tags.length > 0? this.getTags(post.reblog.tags): this.getTags(post.tags)
}
<CardActions>
<Tooltip title="Reply">
<LinkableIconButton to={`/compose?reply=${post.id}`}>
<ReplyIcon/>
</LinkableIconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.replies_count: post.replies_count}</Typography>
<Tooltip title="Favorite">
<IconButton onClick={() => this.toggleFavorited(post)}>
<FavoriteIcon className={
post.reblog?
post.reblog.favourited?
classes.postDidAction:
'':
post.favourited?
classes.postDidAction:
''
}/>
</IconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.favourites_count: post.favourites_count}</Typography>
<Tooltip title="Boost">
<IconButton onClick={() => this.toggleReblogged(post)}>
<AutorenewIcon className={
post.reblog?
post.reblog.reblogged?
classes.postDidAction:
'':
post.reblogged?
classes.postDidAction:
''
}/>
</IconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.reblogs_count: post.reblogs_count}</Typography>
<Tooltip title="View thread">
<LinkableIconButton to={`/conversation/${post.id}`}>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Open in Web">
<IconButton href={this.getMastodonUrl(post)} rel="noreferrer" target="_blank">
<OpenInNewIcon />
</IconButton>
</Tooltip>
<div className={classes.postFlexGrow} />
<div className={classes.postTypeIconDiv}>
{this.showVisibilityIcon(post.visibility)}
</div>
</CardActions>
<Menu
id="postmenu"
anchorEl={document.getElementById(`${post.id}_submenu`)}
open={this.state.menuIsOpen}
onClose={() => this.togglePostMenu()}
>
<ShareMenu config={{
params: {
title: `@${post.account.username} posted on Mastodon: `,
text: post.content,
url: this.getMastodonUrl(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'})
},
}}/>
{
post.reblog?
<div>
<LinkableMenuItem to={`/profile/${post.reblog.account.id}`}>View author profile</LinkableMenuItem>
<LinkableMenuItem to={`/profile/${post.account.id}`}>View reblogger profile</LinkableMenuItem>
<Card className={classes.post} id={`post_${post.id}`}>
<CardHeader avatar={
<Avatar src={
post.reblog? post.reblog.account.avatar_static: post.account.avatar_static
} />
} action={
<IconButton key={`${post.id}_submenu`} id={`${post.id}_submenu`} onClick={() => this.togglePostMenu()}>
<MoreVertIcon />
</IconButton>}
title={this.getReblogAuthors(post)} subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
{
post.reblog? this.getReblogOfPost(post.reblog): null
}
{
post.sensitive? this.getSensitiveContent(post.spoiler_text, post):
post.reblog? null: this.materializeContent(post)
}
{
post.reblog && post.reblog.mentions.length > 0? this.getMentions(post.reblog.mentions): this.getMentions(post.mentions)
}
{
post.reblog && post.reblog.tags.length > 0? this.getTags(post.reblog.tags): this.getTags(post.tags)
}
<CardActions>
<Tooltip title="Reply">
<LinkableIconButton to={`/compose?reply=${post.id}`}>
<ReplyIcon/>
</LinkableIconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.replies_count: post.replies_count}</Typography>
<Tooltip title="Favorite">
<IconButton onClick={() => this.toggleFavorited(post)}>
<FavoriteIcon className={
post.reblog?
post.reblog.favourited?
classes.postDidAction:
'':
post.favourited?
classes.postDidAction:
''
}/>
</IconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.favourites_count: post.favourites_count}</Typography>
<Tooltip title="Boost">
<IconButton onClick={() => this.toggleReblogged(post)}>
<AutorenewIcon className={
post.reblog?
post.reblog.reblogged?
classes.postDidAction:
'':
post.reblogged?
classes.postDidAction:
''
}/>
</IconButton>
</Tooltip>
<Typography>{post.reblog? post.reblog.reblogs_count: post.reblogs_count}</Typography>
<Tooltip title="View thread">
<LinkableIconButton to={`/conversation/${post.id}`}>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip title="Open in Web">
<IconButton href={this.getMastodonUrl(post)} rel="noreferrer" target="_blank">
<OpenInNewIcon />
</IconButton>
</Tooltip>
<div className={classes.postFlexGrow} />
<div className={classes.postTypeIconDiv}>
{this.showVisibilityIcon(post.visibility)}
</div>
</CardActions>
<Menu
id="postmenu"
anchorEl={document.getElementById(`${post.id}_submenu`)}
open={this.state.menuIsOpen}
onClose={() => this.togglePostMenu()}
>
<ShareMenu config={{
params: {
title: `@${post.account.username} posted on Mastodon: `,
text: post.content,
url: this.getMastodonUrl(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'})
},
}}/>
{
post.reblog?
<div>
<LinkableMenuItem to={`/profile/${post.reblog.account.id}`}>View author profile</LinkableMenuItem>
<LinkableMenuItem to={`/profile/${post.account.id}`}>View reblogger profile</LinkableMenuItem>
</div>: <LinkableMenuItem to={`/profile/${post.account.id}`}>View profile</LinkableMenuItem>
}
{
post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
<div>
<Divider/>
<MenuItem>Delete</MenuItem>
</div>:
null
}
</Menu>
</Card>
</Zoom>
</div>: <LinkableMenuItem to={`/profile/${post.account.id}`}>View profile</LinkableMenuItem>
}
{
post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
<div>
<Divider/>
<MenuItem>Delete</MenuItem>
</div>:
null
}
</Menu>
</Card>
</Zoom>
);
}
}

121
src/pages/Conversation.tsx Normal file
View File

@ -0,0 +1,121 @@
import React, { Component } from 'react';
import { withStyles, CircularProgress, Typography, Paper, Button, Chip, Avatar, Slide} from '@material-ui/core';
import {styles} from './PageLayout.styles';
import Post from '../components/Post';
import { Status } from '../types/Status';
import { Context } from '../types/Context';
import Mastodon from 'megalodon';
import {withSnackbar} from 'notistack';
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
interface IConversationPageState {
posts?: [Status];
viewIsLoading: boolean;
viewDidLoad?: boolean;
viewDidError?: boolean;
viewDidErrorCode?: any;
conversationId: string;
}
class Conversation extends Component<any, IConversationPageState> {
client: Mastodon;
streamListener: any;
constructor(props: any) {
super(props);
this.state = {
viewIsLoading: true,
conversationId: props.match.params.conversationId
}
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
}
getContext() {
this.client.get(`/statuses/${this.state.conversationId}`).then((resp: any) => {
let result: Status = resp.data;
this.setState({ posts: [result] });
}).catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
})
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, { variant: 'error' });
})
this.client.get(`/statuses/${this.state.conversationId}/context`).then((resp: any) => {
let context: Context = resp.data;
let posts = this.state.posts;
let array: any[] = [];
if (posts) {
array = array.concat(context.ancestors).concat(posts).concat(context.descendants);
}
this.setState({
posts: array as [Status],
viewIsLoading: false,
viewDidLoad: true,
viewDidError: false
});
}).catch((err: Error) => {
this.setState({
viewIsLoading: false,
viewDidError: true,
viewDidErrorCode: err.message
})
this.props.enqueueSnackbar("Couldn't get conversation: " + err.name, { variant: 'error' });
});
}
componentWillReceiveProps(props: any) {
if (props.match.params.conversationId !== this.state.conversationId) {
this.getContext()
}
}
componentWillMount() {
this.getContext()
}
componentDidUpdate() {
const where: HTMLElement | null = document.getElementById(`post_${this.state.conversationId}`);
if (where) {
window.scrollTo(0, where.getBoundingClientRect().top);
}
}
render() {
const {classes} = this.props;
return (
<div className={classes.pageLayoutMaxConstraints}>
{ this.state.posts?
<div>
{ this.state.posts.map((post: Status) => {
return <Post key={post.id} post={post} client={this.client}/>
}) }
</div>:
<span/>
}
{
this.state.viewDidError?
<Paper className={classes.errorCard}>
<Typography variant="h4">Bummer.</Typography>
<Typography variant="h6">Something went wrong when loading this conversation.</Typography>
<Typography>{this.state.viewDidErrorCode? this.state.viewDidErrorCode: ""}</Typography>
</Paper>:
<span/>
}
{
this.state.viewIsLoading?
<div style={{ textAlign: 'center' }}><CircularProgress className={classes.progress} color="primary" /></div>:
<span/>
}
</div>
);
}
}
export default withStyles(styles)(withSnackbar(Conversation));

View File

@ -27,6 +27,7 @@ import {LinkableIconButton} from '../interfaces/overrides';
import ForumIcon from '@material-ui/icons/Forum';
import Mastodon from 'megalodon';
import { Notification } from '../types/Notification';
import { Account } from '../types/Account';
import { withSnackbar } from 'notistack';
interface INotificationsPageState {
@ -146,7 +147,7 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
<ListItemSecondaryAction>
{
notif.type === "follow"?
<IconButton href={localStorage.getItem("baseurl") as string} target="_blank" rel="noreferrer">
<IconButton onClick={() => this.followMember(notif.account)}>
<PersonAddIcon/>
</IconButton>:
@ -164,6 +165,15 @@ class NotificationsPage extends Component<any, INotificationsPageState> {
);
}
followMember(acct: Account) {
this.client.post(`/accounts/${acct.id}/follow`).then((resp: any) => {
this.props.enqueueSnackbar('You are now following this account.');
}).catch((err: Error) => {
this.props.enqueueSnackbar("Couldn't follow account: " + err.name, { variant: 'error' });
console.error(err.message);
})
}
render() {
const { classes } = this.props;
return (

View File

@ -211,7 +211,7 @@ class ProfilePage extends Component<any, IProfilePageState> {
<div className={classes.pageHeroContent}>
<Avatar className={classes.pageProfileAvatar} src={this.state.account ? this.state.account.avatar: ""}/>
<Typography variant="h4" color="inherit">{this.state.account ? this.state.account.display_name: ""}</Typography>
<Typography variant="caption" color="inherit">{this.state.account ? this.state.account.acct: ""}</Typography>
<Typography variant="caption" color="inherit">{this.state.account ? '@' + this.state.account.acct: ""}</Typography>
<Typography paragraph color="inherit">{this.state.account ? this.state.account.note: ""}</Typography>
<Divider/>
<div className={classes.pageProfileStatsDiv}>

6
src/types/Context.tsx Normal file
View File

@ -0,0 +1,6 @@
import { Status } from './Status';
export type Context = {
ancestors: [Status];
descendants: [Status];
}