Add Conversation UI
This commit is contained in:
parent
6dc2b2dc0d
commit
35df20ce8a
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
|
@ -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 (
|
||||
|
|
|
@ -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}>
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
import { Status } from './Status';
|
||||
|
||||
export type Context = {
|
||||
ancestors: [Status];
|
||||
descendants: [Status];
|
||||
}
|
Loading…
Reference in New Issue