Add tags, remove some anchors, and put new posts behind 'View more'
This commit is contained in:
parent
b8f9035be1
commit
51ed21c39c
|
@ -9,9 +9,9 @@ import Settings from './pages/Settings';
|
|||
import { getUserDefaultBool, getUserDefaultTheme } from './utilities/settings';
|
||||
import ProfilePage from './pages/ProfilePage';
|
||||
import HomePage from './pages/Home';
|
||||
import LocalPage from './pages/Local';
|
||||
import PublicPage from './pages/Public';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import {ProfileRoute} from './interfaces/overrides';
|
||||
|
||||
let theme = setHyperspaceTheme(getUserDefaultTheme());
|
||||
|
||||
class App extends Component<any, any> {
|
||||
|
@ -40,8 +40,8 @@ class App extends Component<any, any> {
|
|||
<AppLayout/>
|
||||
<Route exact path="/" component={HomePage}/>
|
||||
<Route path="/home" component={HomePage}/>
|
||||
<Route path="/local"/>
|
||||
<Route path="/public"/>
|
||||
<Route path="/local" component={LocalPage}/>
|
||||
<Route path="/public" component={PublicPage}/>
|
||||
<Route path="/messages"/>
|
||||
<Route path="/notifications"/>
|
||||
<Route path="/profile/:profileId" render={props => <ProfilePage {...props}></ProfilePage>}/>
|
||||
|
|
|
@ -10,6 +10,7 @@ export const styles = (theme: Theme) => createStyles({
|
|||
'&:hover': {
|
||||
backgroundColor: theme.palette.secondary.light
|
||||
},
|
||||
backgroundColor: theme.palette.secondary.main,
|
||||
marginBottom: theme.spacing.unit
|
||||
},
|
||||
postContent: {
|
||||
|
@ -21,6 +22,16 @@ export const styles = (theme: Theme) => createStyles({
|
|||
'&:hover': {
|
||||
textDecoration: 'underline'
|
||||
},
|
||||
'&.u-url.mention': {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
fontWeight: 'bold'
|
||||
},
|
||||
'&.mention.hashtag': {
|
||||
textDecoration: 'none',
|
||||
color: 'inherit',
|
||||
fontWeight: 'bold'
|
||||
}
|
||||
}
|
||||
},
|
||||
postCard: {
|
||||
|
@ -54,5 +65,9 @@ export const styles = (theme: Theme) => createStyles({
|
|||
},
|
||||
postDidAction: {
|
||||
color: theme.palette.secondary.main
|
||||
},
|
||||
postMention: {
|
||||
marginRight: theme.spacing.unit,
|
||||
marginBottom: theme.spacing.unit
|
||||
}
|
||||
});
|
|
@ -12,8 +12,10 @@ 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 LocalOfferIcon from '@material-ui/icons/LocalOffer';
|
||||
import {styles} from './Post.styles';
|
||||
import { Status } from '../../types/Status';
|
||||
import { Tag } from '../../types/Tag';
|
||||
import { Mention } from '../../types/Mention';
|
||||
import { Visibility } from '../../types/Visibility';
|
||||
import moment from 'moment';
|
||||
|
@ -61,6 +63,15 @@ export class Post extends React.Component<any, IPostState> {
|
|||
const { classes } = this.props;
|
||||
const oldContent = document.createElement('div');
|
||||
oldContent.innerHTML = status.content;
|
||||
|
||||
let anchors = oldContent.getElementsByTagName("a");
|
||||
|
||||
Array.prototype.forEach.call(anchors, (link: HTMLAnchorElement) => {
|
||||
if (link.className.includes("mention") || link.className.includes("hashtag")) {
|
||||
link.removeAttribute("href");
|
||||
}
|
||||
});
|
||||
|
||||
if (status.emojis !== undefined && status.emojis.length > 0) {
|
||||
status.emojis.forEach((emoji: MastodonEmoji) => {
|
||||
let regexp = new RegExp(':' + emoji.shortcode + ':', 'g');
|
||||
|
@ -77,7 +88,7 @@ export class Post extends React.Component<any, IPostState> {
|
|||
<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>
|
||||
<Typography>{status.card.description || "No description provided. Click with caution."}</Typography>
|
||||
</CardContent>
|
||||
{
|
||||
status.card.image?
|
||||
|
@ -124,17 +135,6 @@ export class Post extends React.Component<any, IPostState> {
|
|||
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>
|
||||
);
|
||||
|
@ -143,13 +143,25 @@ export class Post extends React.Component<any, IPostState> {
|
|||
}
|
||||
}
|
||||
|
||||
getReblogAuthors(post: Status) {
|
||||
if (post.reblog) {
|
||||
let author = post.reblog.account;
|
||||
return `${author.display_name || author.username} (@${author.acct}) 🔄 ${post.account.display_name || post.account.username}`
|
||||
} else {
|
||||
let author = post.account;
|
||||
return `${author.display_name || author.username} (@${author.acct})`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
getMentions(mention: [Mention]) {
|
||||
const { classes } = this.props;
|
||||
if (mention.length > 0) {
|
||||
return (
|
||||
<CardContent>
|
||||
<Typography variant="caption">Mentions</Typography>
|
||||
{
|
||||
this.state.post.mentions.map((person: Mention) => {
|
||||
mention.map((person: Mention) => {
|
||||
return <LinkableChip
|
||||
avatar={
|
||||
<Avatar>
|
||||
|
@ -159,6 +171,36 @@ export class Post extends React.Component<any, IPostState> {
|
|||
label={person.username}
|
||||
key={this.state.post.id + "_mention_" + person.id}
|
||||
to={`/profile/${person.id}`}
|
||||
className={classes.postMention}
|
||||
clickable
|
||||
/>
|
||||
})
|
||||
}
|
||||
</CardContent>
|
||||
)
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
getTags(tags: [Tag]) {
|
||||
const { classes } = this.props;
|
||||
if (tags.length > 0) {
|
||||
return (
|
||||
<CardContent>
|
||||
<Typography variant="caption">Tags</Typography>
|
||||
{
|
||||
tags.map((tag: Tag) => {
|
||||
return <LinkableChip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<LocalOfferIcon/>
|
||||
</Avatar>
|
||||
}
|
||||
label={tag.name}
|
||||
key={this.state.post.id + "_tag_" + tag.name}
|
||||
to={`/search?tag=${tag.name}`}
|
||||
className={classes.postMention}
|
||||
clickable
|
||||
/>
|
||||
})
|
||||
|
@ -245,76 +287,79 @@ export class Post extends React.Component<any, IPostState> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
const post = this.state.post;
|
||||
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()}>
|
||||
<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.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")} />
|
||||
title={this.getReblogAuthors(post)} subheader={moment(post.created_at).format("MMMM Do YYYY [at] h:mm A")} />
|
||||
{
|
||||
this.state.post.reblog? this.getReblogOfPost(this.state.post.reblog): null
|
||||
post.reblog? this.getReblogOfPost(post.reblog): null
|
||||
}
|
||||
{
|
||||
this.state.post.sensitive? this.getSensitiveContent(this.state.post.spoiler_text, this.state.post):
|
||||
post.sensitive? this.getSensitiveContent(post.spoiler_text, post):
|
||||
<CardContent className={classes.postContent}>
|
||||
{this.state.post.reblog? null: this.materializeContent(this.state.post)}
|
||||
{post.reblog? null: this.materializeContent(post)}
|
||||
</CardContent>
|
||||
}
|
||||
{
|
||||
this.getMentions(this.state.post.mentions)
|
||||
post.reblog && post.reblog.mentions.length > 0? this.getMentions(post.reblog.mentions): this.getMentions(post.mentions)
|
||||
}
|
||||
{
|
||||
this.state.post.reblog && this.state.post.reblog.mentions.length > 0? this.getMentions(this.state.post.reblog.mentions): <span/>
|
||||
post.reblog && post.reblog.tags.length > 0? this.getTags(post.reblog.tags): this.getTags(post.tags)
|
||||
}
|
||||
<CardActions>
|
||||
<Tooltip title="Reply">
|
||||
<LinkableIconButton to={`/compose?reply=${this.state.post.id}`}>
|
||||
<LinkableIconButton to={`/compose?reply=${post.id}`}>
|
||||
<ReplyIcon/>
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.replies_count}</Typography>
|
||||
<Typography>{post.replies_count}</Typography>
|
||||
<Tooltip title="Favorite">
|
||||
<IconButton onClick={() => this.toggleFavorited(this.state.post)}>
|
||||
<FavoriteIcon className={this.state.post.favourited? classes.postDidAction: ''}/>
|
||||
<IconButton onClick={() => this.toggleFavorited(post)}>
|
||||
<FavoriteIcon className={post.favourited? classes.postDidAction: ''}/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.favourites_count}</Typography>
|
||||
<Typography>{post.favourites_count}</Typography>
|
||||
<Tooltip title="Boost">
|
||||
<IconButton onClick={() => this.toggleReblogged(this.state.post)}>
|
||||
<AutorenewIcon className={this.state.post.reblogged? classes.postDidAction: ''}/>
|
||||
<IconButton onClick={() => this.toggleReblogged(post)}>
|
||||
<AutorenewIcon className={post.reblogged? classes.postDidAction: ''}/>
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<Typography>{this.state.post.reblogs_count}</Typography>
|
||||
<Typography>{post.reblogs_count}</Typography>
|
||||
<Tooltip title="View thread">
|
||||
<LinkableIconButton to={`/conversation/${this.state.post.id}`}>
|
||||
<LinkableIconButton to={`/conversation/${post.id}`}>
|
||||
<ForumIcon />
|
||||
</LinkableIconButton>
|
||||
</Tooltip>
|
||||
<Tooltip title="Open in Web">
|
||||
<IconButton href={this.getMastodonUrl(this.state.post)} rel="noreferrer" target="_blank">
|
||||
<IconButton href={this.getMastodonUrl(post)} rel="noreferrer" target="_blank">
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</Tooltip>
|
||||
<div className={classes.postFlexGrow} />
|
||||
<div className={classes.postTypeIconDiv}>
|
||||
{this.showVisibilityIcon(this.state.post.visibility)}
|
||||
{this.showVisibilityIcon(post.visibility)}
|
||||
</div>
|
||||
</CardActions>
|
||||
<Menu
|
||||
id="postmenu"
|
||||
anchorEl={document.getElementById(`${this.state.post.id}_submenu`)}
|
||||
anchorEl={document.getElementById(`${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),
|
||||
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) => {
|
||||
|
@ -322,9 +367,13 @@ export class Post extends React.Component<any, IPostState> {
|
|||
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?
|
||||
post.reblog?
|
||||
<LinkableMenuItem to={`/profile/${post.reblog.account.id}`}>View author profile</LinkableMenuItem>: null
|
||||
}
|
||||
<LinkableMenuItem to={`/profile/${post.account.id}`}>View reblogger profile</LinkableMenuItem>
|
||||
{
|
||||
post.account.id == JSON.parse(localStorage.getItem('account') as string).id?
|
||||
<div>
|
||||
<Divider/>
|
||||
<MenuItem>Delete</MenuItem>
|
||||
|
|
|
@ -5,6 +5,7 @@ import { Link, Route } from "react-router-dom";
|
|||
import Chip, { ChipProps } from '@material-ui/core/Chip';
|
||||
import { MenuItemProps } from '@material-ui/core/MenuItem';
|
||||
import { MenuItem } from '@material-ui/core';
|
||||
import Button, { ButtonProps } from '@material-ui/core/Button';
|
||||
|
||||
export interface ILinkableListItemProps extends ListItemProps {
|
||||
to: string;
|
||||
|
@ -26,6 +27,11 @@ export interface ILinkableMenuItemProps extends MenuItemProps {
|
|||
replace?: boolean;
|
||||
}
|
||||
|
||||
export interface ILinkableButtonProps extends ButtonProps {
|
||||
to: string;
|
||||
replace?: boolean;
|
||||
}
|
||||
|
||||
export const LinkableListItem = (props: ILinkableListItemProps) => (
|
||||
<ListItem {...props} component={Link as any}/>
|
||||
)
|
||||
|
@ -42,8 +48,13 @@ export const LinkableMenuItem = (props: ILinkableMenuItemProps) => (
|
|||
<MenuItem {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const LinkableButton = (props: ILinkableButtonProps) => (
|
||||
<Button {...props} component={Link as any}/>
|
||||
)
|
||||
|
||||
export const ProfileRoute = (rest: any, component: Component) => (
|
||||
<Route {...rest} render={props => (
|
||||
<Component {...props}/>
|
||||
)}/>
|
||||
)
|
||||
)
|
||||
|
|
@ -1,13 +1,15 @@
|
|||
import React, { Component } from 'react';
|
||||
import { withStyles, CircularProgress, Typography, Paper, Button} from '@material-ui/core';
|
||||
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 Mastodon from 'megalodon';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
||||
|
||||
interface IHomePageState {
|
||||
posts?: [Status];
|
||||
backlogPosts?: [Status] | null;
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
|
@ -24,7 +26,8 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
super(props);
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
||||
|
@ -32,7 +35,7 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.streamListener = this.client.stream('/streaming/home');
|
||||
this.streamListener = this.client.stream('/streaming/user');
|
||||
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/home', {limit: 40}).then((resp: any) => {
|
||||
|
@ -57,11 +60,9 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
let posts = this.state.posts;
|
||||
if (posts) {
|
||||
posts.unshift(status)
|
||||
}
|
||||
this.setState({ posts });
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.push(status); } else { queue = [status] }
|
||||
this.setState({ backlogPosts: queue });
|
||||
})
|
||||
|
||||
this.streamListener.on('delete', (id: number) => {
|
||||
|
@ -91,6 +92,16 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
})
|
||||
}
|
||||
|
||||
insertBacklog() {
|
||||
window.scrollTo(0, 0);
|
||||
let posts = this.state.posts;
|
||||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
if (this.state.posts) {
|
||||
|
@ -123,6 +134,27 @@ class HomePage extends Component<any, IHomePageState> {
|
|||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new posts`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
clickable
|
||||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
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 Mastodon from 'megalodon';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
||||
|
||||
interface ILocalPageState {
|
||||
posts?: [Status];
|
||||
backlogPosts?: [Status] | null;
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class LocalPage extends Component<any, ILocalPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.streamListener = this.client.stream('/streaming/local');
|
||||
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/public', {limit: 40, local: true}).then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
})
|
||||
}).catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.push(status); } else { queue = [status] }
|
||||
this.setState({ backlogPosts: queue });
|
||||
})
|
||||
|
||||
this.streamListener.on('delete', (id: number) => {
|
||||
let posts = this.state.posts;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
})
|
||||
this.setState({ posts });
|
||||
}
|
||||
})
|
||||
|
||||
this.streamListener.on('error', (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
insertBacklog() {
|
||||
window.scrollTo(0, 0);
|
||||
let posts = this.state.posts;
|
||||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
if (this.state.posts) {
|
||||
this.client.get('/timelines/home', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
||||
let newPosts: [Status] = resp.data;
|
||||
let posts = this.state.posts as [Status];
|
||||
newPosts.forEach((post: Status) => {
|
||||
posts.push(post);
|
||||
});
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new posts`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
clickable
|
||||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>
|
||||
})}
|
||||
<br/>
|
||||
{
|
||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
||||
}
|
||||
</div>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
this.state.viewDidError?
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">Something went wrong when loading this timeline.</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(LocalPage));
|
|
@ -30,6 +30,13 @@ export const styles = (theme: Theme) => createStyles({
|
|||
paddingRight: theme.spacing.unit * 16,
|
||||
},
|
||||
[theme.breakpoints.up('lg')]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
paddingLeft: theme.spacing.unit * 32,
|
||||
paddingRight: theme.spacing.unit * 32,
|
||||
},
|
||||
[theme.breakpoints.up('xl')]: {
|
||||
marginLeft: 250,
|
||||
marginTop: 88,
|
||||
padding: theme.spacing.unit * 3,
|
||||
|
@ -119,5 +126,19 @@ export const styles = (theme: Theme) => createStyles({
|
|||
errorCard: {
|
||||
padding: theme.spacing.unit * 4,
|
||||
backgroundColor: theme.palette.error.main,
|
||||
},
|
||||
pageTopChipContainer: {
|
||||
zIndex: 24,
|
||||
position: "fixed",
|
||||
width: '100%'
|
||||
},
|
||||
pageTopChips: {
|
||||
textAlign: 'center',
|
||||
[theme.breakpoints.up('md')]: {
|
||||
marginRight: '55%'
|
||||
}
|
||||
},
|
||||
pageTopChip: {
|
||||
boxShadow: theme.shadows[10]
|
||||
}
|
||||
});
|
|
@ -3,12 +3,15 @@ import {withStyles, Typography, Avatar, Divider, Button, CircularProgress, Paper
|
|||
import {styles} from './PageLayout.styles';
|
||||
import Mastodon from 'megalodon';
|
||||
import { Account } from '../types/Account';
|
||||
import { Status} from '../types/Status';
|
||||
import { Status } from '../types/Status';
|
||||
import { Relationship } from '../types/Relationship';
|
||||
import Post from '../components/Post';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import { LinkableButton } from '../interfaces/overrides';
|
||||
|
||||
interface IProfilePageState {
|
||||
account?: Account;
|
||||
relationship?: Relationship;
|
||||
posts?: [Status];
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
|
@ -31,8 +34,8 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
}
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
this.client.get(`/accounts/${props.match.params.profileId}`).then((resp: any) => {
|
||||
getAccountData(id: string) {
|
||||
this.client.get(`/accounts/${id}`).then((resp: any) => {
|
||||
let profile: Account = resp.data;
|
||||
|
||||
const div = document.createElement('div');
|
||||
|
@ -48,9 +51,8 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
})
|
||||
|
||||
this.client.get(`/accounts/${props.match.params.profileId}/statuses`).then((resp: any) => {
|
||||
});
|
||||
this.client.get(`/accounts/${id}/statuses`).then((resp: any) => {
|
||||
this.setState({
|
||||
posts: resp.data,
|
||||
viewIsLoading: false,
|
||||
|
@ -63,44 +65,30 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
})
|
||||
window.scrollTo(0, 0)
|
||||
});
|
||||
}
|
||||
|
||||
componentWillReceiveProps(props: any) {
|
||||
this.getAccountData(props.match.params.profileId);
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
const { match: { params }} = this.props;
|
||||
this.client.get(`/accounts/${params.profileId}`).then((resp: any) => {
|
||||
let profile: Account = resp.data;
|
||||
this.getAccountData(params.profileId);
|
||||
}
|
||||
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = profile.note;
|
||||
profile.note = div.textContent || div.innerText || "";
|
||||
|
||||
this.setState({
|
||||
account: profile
|
||||
})
|
||||
componentDidMount() {
|
||||
this.client.get("/accounts/relationships", {id: [this.props.match.params.profileId]}).then((resp: any) => {
|
||||
let relationship: Relationship = resp.data;
|
||||
this.setState({ relationship });
|
||||
}).catch((error: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: error.message
|
||||
})
|
||||
})
|
||||
|
||||
this.client.get(`/accounts/${params.profileId}/statuses`).then((resp: any) => {
|
||||
this.setState({
|
||||
posts: resp.data,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
})
|
||||
}).catch( (err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
statElement(classes: any, stat: 'following' | 'followers' | 'posts') {
|
||||
|
@ -156,7 +144,6 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
|
||||
render() {
|
||||
const { classes } = this.props;
|
||||
|
||||
return(
|
||||
<div className={classes.pageLayoutMinimalConstraints}>
|
||||
<div className={classes.pageHeroBackground}>
|
||||
|
@ -173,9 +160,14 @@ class ProfilePage extends Component<any, IProfilePageState> {
|
|||
{this.statElement(classes, 'posts')}
|
||||
</div>
|
||||
<Divider/>
|
||||
<Button variant="contained" color="primary" className={classes.pageProfileFollowButton}>Follow</Button>
|
||||
<Button variant="contained" className={classes.pageProfileFollowButton}>Mention</Button>
|
||||
<Button variant="contained" className={classes.pageProfileFollowButton}>Block</Button>
|
||||
{
|
||||
this.state.account && this.state.relationship?
|
||||
<div>
|
||||
<Button variant="contained" color="primary" className={classes.pageProfileFollowButton}>{this.state.relationship? "Unfollow": "Follow"}</Button>
|
||||
<LinkableButton to={`/compose?mention=${this.state.account.acct}`} variant="contained" className={classes.pageProfileFollowButton}>Mention</LinkableButton>
|
||||
<Button variant="contained" className={classes.pageProfileFollowButton}>{this.state.relationship.blocking? "Unblock": "Block"}</Button>
|
||||
</div>: null
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className={classes.pageContentLayoutConstraints}>
|
||||
|
|
|
@ -0,0 +1,189 @@
|
|||
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 Mastodon from 'megalodon';
|
||||
import {withSnackbar} from 'notistack';
|
||||
import ArrowUpwardIcon from '@material-ui/icons/ArrowUpward';
|
||||
|
||||
interface IPublicPageState {
|
||||
posts?: [Status];
|
||||
backlogPosts?: [Status] | null;
|
||||
viewIsLoading: boolean;
|
||||
viewDidLoad?: boolean;
|
||||
viewDidError?: boolean;
|
||||
viewDidErrorCode?: any;
|
||||
}
|
||||
|
||||
|
||||
class PublicPage extends Component<any, IPublicPageState> {
|
||||
|
||||
client: Mastodon;
|
||||
streamListener: any;
|
||||
|
||||
constructor(props: any) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
viewIsLoading: true,
|
||||
backlogPosts: null
|
||||
}
|
||||
|
||||
this.client = new Mastodon(localStorage.getItem('access_token') as string, localStorage.getItem('baseurl') as string + "/api/v1");
|
||||
|
||||
}
|
||||
|
||||
componentWillMount() {
|
||||
this.streamListener = this.client.stream('/streaming/public');
|
||||
|
||||
this.streamListener.on('connect', () => {
|
||||
this.client.get('/timelines/public', {limit: 40}).then((resp: any) => {
|
||||
let statuses: [Status] = resp.data;
|
||||
this.setState({
|
||||
posts: statuses,
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: false
|
||||
})
|
||||
}).catch((resp: any) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: String(resp)
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
this.streamListener.on('update', (status: Status) => {
|
||||
let queue = this.state.backlogPosts;
|
||||
if (queue !== null && queue !== undefined) { queue.push(status); } else { queue = [status] }
|
||||
this.setState({ backlogPosts: queue });
|
||||
})
|
||||
|
||||
this.streamListener.on('delete', (id: number) => {
|
||||
let posts = this.state.posts;
|
||||
if (posts) {
|
||||
posts.forEach((post: Status) => {
|
||||
if (posts && parseInt(post.id) === id) {
|
||||
posts.splice(posts.indexOf(post), 1);
|
||||
}
|
||||
})
|
||||
this.setState({ posts });
|
||||
}
|
||||
})
|
||||
|
||||
this.streamListener.on('error', (err: Error) => {
|
||||
this.setState({
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("An error occured.", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
|
||||
this.streamListener.on('heartbeat', () => {
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
insertBacklog() {
|
||||
window.scrollTo(0, 0);
|
||||
let posts = this.state.posts;
|
||||
let backlog = this.state.backlogPosts;
|
||||
if (posts && backlog && backlog.length > 0) {
|
||||
let push = backlog.concat(posts);
|
||||
this.setState({ posts: push as [Status], backlogPosts: null })
|
||||
}
|
||||
}
|
||||
|
||||
loadMoreTimelinePieces() {
|
||||
this.setState({ viewDidLoad: false, viewIsLoading: true})
|
||||
if (this.state.posts) {
|
||||
this.client.get('/timelines/home', { max_id: this.state.posts[this.state.posts.length - 1].id, limit: 20 }).then((resp: any) => {
|
||||
let newPosts: [Status] = resp.data;
|
||||
let posts = this.state.posts as [Status];
|
||||
newPosts.forEach((post: Status) => {
|
||||
posts.push(post);
|
||||
});
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidLoad: true,
|
||||
posts
|
||||
})
|
||||
}).catch((err: Error) => {
|
||||
this.setState({
|
||||
viewIsLoading: false,
|
||||
viewDidError: true,
|
||||
viewDidErrorCode: err.message
|
||||
})
|
||||
this.props.enqueueSnackbar("Failed to get posts", {
|
||||
variant: 'error',
|
||||
});
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const {classes} = this.props;
|
||||
|
||||
return (
|
||||
<div className={classes.pageLayoutMaxConstraints}>
|
||||
{
|
||||
this.state.backlogPosts?
|
||||
<div className={classes.pageTopChipContainer}>
|
||||
<div className={classes.pageTopChips}>
|
||||
<Slide direction="down" in={true}>
|
||||
<Chip
|
||||
avatar={
|
||||
<Avatar>
|
||||
<ArrowUpwardIcon/>
|
||||
</Avatar>
|
||||
}
|
||||
label={`View ${this.state.backlogPosts.length} new posts`}
|
||||
color="primary"
|
||||
className={classes.pageTopChip}
|
||||
onClick={() => this.insertBacklog()}
|
||||
clickable
|
||||
/>
|
||||
</Slide>
|
||||
</div>
|
||||
</div>: null
|
||||
}
|
||||
{ this.state.posts?
|
||||
<div>
|
||||
{this.state.posts.map((post: Status) => {
|
||||
return <Post key={post.id} post={post} client={this.client}/>
|
||||
})}
|
||||
<br/>
|
||||
{
|
||||
this.state.viewDidLoad && !this.state.viewDidError? <div style={{textAlign: "center"}} onClick={() => this.loadMoreTimelinePieces()}><Button variant="contained">Load more</Button></div>: null
|
||||
}
|
||||
</div>:
|
||||
<span/>
|
||||
}
|
||||
{
|
||||
this.state.viewDidError?
|
||||
<Paper className={classes.errorCard}>
|
||||
<Typography variant="h4">Bummer.</Typography>
|
||||
<Typography variant="h6">Something went wrong when loading this timeline.</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(PublicPage));
|
|
@ -0,0 +1,12 @@
|
|||
export type Relationship = {
|
||||
id: string;
|
||||
following: boolean;
|
||||
followed_by: boolean;
|
||||
blocking: boolean;
|
||||
muting: boolean;
|
||||
muting_notifications: boolean;
|
||||
requested: boolean;
|
||||
domain_blocking: boolean;
|
||||
showing_reblogs: boolean;
|
||||
endorsed: boolean;
|
||||
}
|
|
@ -5,6 +5,7 @@ import { Attachment } from './Attachment';
|
|||
import { Mention } from './Mention';
|
||||
import { Poll } from './Poll';
|
||||
import { Card } from './Card';
|
||||
import { Tag } from './Tag';
|
||||
|
||||
/**
|
||||
* Basic type for a status on Mastodon
|
||||
|
@ -31,7 +32,7 @@ export type Status = {
|
|||
visibility: Visibility;
|
||||
media_attachments: [Attachment];
|
||||
mentions: [Mention];
|
||||
tags: any;
|
||||
tags: [Tag];
|
||||
card: Card | null;
|
||||
poll: Poll | null;
|
||||
application: any;
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
export type Tag = {
|
||||
name: string;
|
||||
url: string;
|
||||
}
|
Loading…
Reference in New Issue