Merge pull request #172 from hyperspacedev/HD-44-uneven-masonry-columns

HD-44 #done
This commit is contained in:
Marquis Kurt 2020-02-17 17:27:16 -05:00 committed by GitHub
commit 5838039fef
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 289 additions and 209 deletions

View File

@ -101,6 +101,11 @@ export class Post extends React.Component<any, IPostState> {
});
}
shouldComponentUpdate(nextProps: any, nextState: any) {
if (nextState == this.state) return false;
return true;
}
togglePostMenu() {
this.setState({ menuIsOpen: !this.state.menuIsOpen });
}
@ -549,7 +554,7 @@ export class Post extends React.Component<any, IPostState> {
* Tell server a post has been un/favorited and update post state
* @param post The post to un/favorite
*/
async toggleFavorite(post: Status) {
async toggleFavorited(post: Status) {
let action: string = post.favourited ? "unfavourite" : "favourite";
try {
// favorite the original post, not the reblog
@ -576,7 +581,7 @@ export class Post extends React.Component<any, IPostState> {
* Tell server a post has been un/reblogged and update post state
* @param post The post to un/reblog
*/
async toggleReblog(post: Status) {
async toggleReblogged(post: Status) {
let action: string =
post.reblogged || post.reblog ? "unreblog" : "reblog";
try {
@ -637,222 +642,220 @@ export class Post extends React.Component<any, IPostState> {
const { classes } = this.props;
const post = this.state.post;
return (
<Zoom in={true}>
<Card
className={classes.post}
id={`post_${post.id}`}
elevation={this.props.threadHeader ? 0 : 1}
>
<CardHeader
avatar={
<LinkableAvatar
to={`/profile/${
<Card
className={classes.post}
id={`post_${post.id}`}
elevation={this.props.threadHeader ? 0 : 1}
>
<CardHeader
avatar={
<LinkableAvatar
to={`/profile/${
post.reblog
? post.reblog.account.id
: post.account.id
}`}
src={
post.reblog
? post.reblog.account.avatar_static
: post.account.avatar_static
}
/>
}
action={
<Tooltip title="More" placement="left">
<IconButton
key={`${post.id}_submenu`}
id={`${post.id}_submenu`}
onClick={() => this.togglePostMenu()}
>
<MoreVertIcon />
</IconButton>
</Tooltip>
}
title={
<Typography>{this.getReblogAuthors(post)}</Typography>
}
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.reblog ? post.reblog.id : post.id
}&visibility=${post.visibility}&acct=${
post.reblog
? post.reblog.account.acct
: post.account.acct
}`}
>
<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.account.id
: post.account.id
}`}
src={
post.reblog
? post.reblog.account.avatar_static
: post.account.avatar_static
? post.reblog.favourited
? classes.postDidAction
: ""
: post.favourited
? classes.postDidAction
: ""
}
/>
}
action={
<Tooltip title="More" placement="left">
<IconButton
key={`${post.id}_submenu`}
id={`${post.id}_submenu`}
onClick={() => this.togglePostMenu()}
>
<MoreVertIcon />
</IconButton>
</Tooltip>
}
title={
<Typography>
{this.getReblogAuthors(post)}
</Typography>
}
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.reblog ? post.reblog.id : post.id
}&visibility=${post.visibility}&acct=${
</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.account.acct
: post.account.acct
}`}
>
<ReplyIcon />
</LinkableIconButton>
</Tooltip>
<Typography>
{post.reblog
? post.reblog.replies_count
: post.replies_count}
</Typography>
<Tooltip title="Favorite">
<IconButton
onClick={() => this.toggleFavorite(post)}
>
<FavoriteIcon
className={
post.favourited
? post.reblog.reblogged
? classes.postDidAction
: ""
}
/>
</IconButton>
</Tooltip>
<Typography>{post.favourites_count}</Typography>
<Tooltip title="Boost">
<IconButton onClick={() => this.toggleReblog(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
className={classes.desktopOnly}
title="View thread"
>
<LinkableIconButton
to={`/conversation/${
post.reblog ? post.reblog.id : post.id
}`}
>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip
className={classes.desktopOnly}
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.reblogged
? classes.postDidAction
: ""
}
}}
/>
{post.reblog ? (
<div className={classes.postReblogMenu}>
<LinkableMenuItem
to={`/profile/${post.reblog.account.id}`}
>
View author profile
</LinkableMenuItem>
<LinkableMenuItem
to={`/profile/${post.account.id}`}
>
View reblogger profile
</LinkableMenuItem>
</div>
) : (
/>
</IconButton>
</Tooltip>
<Typography>
{post.reblog
? post.reblog.reblogs_count
: post.reblogs_count}
</Typography>
<Tooltip
className={classes.desktopOnly}
title="View thread"
>
<LinkableIconButton
to={`/conversation/${
post.reblog ? post.reblog.id : post.id
}`}
>
<ForumIcon />
</LinkableIconButton>
</Tooltip>
<Tooltip
className={classes.desktopOnly}
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 className={classes.postReblogMenu}>
<LinkableMenuItem
to={`/profile/${post.reblog.account.id}`}
>
View author profile
</LinkableMenuItem>
<LinkableMenuItem
to={`/profile/${post.account.id}`}
>
View profile
View reblogger profile
</LinkableMenuItem>
)}
<div className={classes.mobileOnly}>
</div>
) : (
<LinkableMenuItem to={`/profile/${post.account.id}`}>
View profile
</LinkableMenuItem>
)}
<div className={classes.mobileOnly}>
<Divider />
<LinkableMenuItem
to={`/conversation/${
post.reblog ? post.reblog.id : post.id
}`}
>
View thread
</LinkableMenuItem>
<MenuItem
component="a"
href={this.getMastodonUrl(post)}
rel="noreferrer"
target="_blank"
>
Open in Web
</MenuItem>
</div>
{this.state.myAccount &&
post.account.id === this.state.myAccount ? (
<div>
<Divider />
<LinkableMenuItem
to={`/conversation/${
post.reblog ? post.reblog.id : post.id
}`}
>
View thread
</LinkableMenuItem>
<MenuItem
component="a"
href={this.getMastodonUrl(post)}
rel="noreferrer"
target="_blank"
onClick={() => this.togglePostDeleteDialog()}
>
Open in Web
Delete
</MenuItem>
</div>
{this.state.myAccount &&
post.account.id === this.state.myAccount ? (
<div>
<Divider />
<MenuItem
onClick={() =>
this.togglePostDeleteDialog()
}
>
Delete
</MenuItem>
</div>
) : null}
{this.showDeleteDialog()}
</Menu>
</Card>
</Zoom>
) : null}
{this.showDeleteDialog()}
</Menu>
</Card>
);
}
}

View File

@ -65,6 +65,7 @@ import DomainDisabledIcon from "@material-ui/icons/DomainDisabled";
import AccountSettingsIcon from "mdi-material-ui/AccountSettings";
import AlphabeticalVariantOffIcon from "mdi-material-ui/AlphabeticalVariantOff";
import DashboardIcon from "@material-ui/icons/Dashboard";
import InfiniteIcon from "@material-ui/icons/AllInclusive";
import { Config } from "../types/Config";
import { Account } from "../types/Account";
@ -88,6 +89,7 @@ interface ISettingsState {
currentUser?: Account;
imposeCharacterLimit: boolean;
masonryLayout?: boolean;
infiniteScroll?: boolean;
}
class SettingsPage extends Component<any, ISettingsState> {
@ -120,7 +122,8 @@ class SettingsPage extends Component<any, ISettingsState> {
brandName: "Hyperspace",
federated: true,
imposeCharacterLimit: getUserDefaultBool("imposeCharacterLimit"),
masonryLayout: getUserDefaultBool("isMasonryLayout")
masonryLayout: getUserDefaultBool("isMasonryLayout"),
infiniteScroll: getUserDefaultBool("isInfiniteScroll")
};
this.toggleDarkMode = this.toggleDarkMode.bind(this);
@ -130,6 +133,7 @@ class SettingsPage extends Component<any, ISettingsState> {
this.toggleThemeDialog = this.toggleThemeDialog.bind(this);
this.toggleVisibilityDialog = this.toggleVisibilityDialog.bind(this);
this.toggleMasonryLayout = this.toggleMasonryLayout.bind(this);
this.toggleInfiniteScroll = this.toggleInfiniteScroll.bind(this);
this.changeThemeName = this.changeThemeName.bind(this);
this.changeTheme = this.changeTheme.bind(this);
this.setVisibility = this.setVisibility.bind(this);
@ -250,6 +254,11 @@ class SettingsPage extends Component<any, ISettingsState> {
setUserDefaultBool("isMasonryLayout", !this.state.masonryLayout);
}
toggleInfiniteScroll() {
this.setState({ infiniteScroll: !this.state.infiniteScroll });
setUserDefaultBool("isInfiniteScroll", !this.state.infiniteScroll);
}
changeTheme() {
setUserDefaultTheme(this.state.selectThemeName);
window.location.reload();
@ -675,6 +684,22 @@ class SettingsPage extends Component<any, ISettingsState> {
/>
</ListItemSecondaryAction>
</ListItem>
<ListItem>
<ListItemAvatar>
<InfiniteIcon color="action" />
</ListItemAvatar>
<ListItemText
primary="Enable infinite scroll"
secondary="Automatically load more posts when scrolling"
/>
<ListItemSecondaryAction>
<Switch
checked={this.state.infiniteScroll}
onChange={this.toggleInfiniteScroll}
/>
</ListItemSecondaryAction>
</ListItem>
</List>
</Paper>
<br />

View File

@ -77,6 +77,11 @@ interface ITimelinePageState {
* the user settings.
*/
isMasonryLayout?: boolean;
/**
* Whether posts should automatically load when scrolling.
*/
isInfiniteScroll?: boolean;
}
/**
@ -109,7 +114,8 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
this.state = {
viewIsLoading: true,
backlogPosts: null,
isMasonryLayout: getUserDefaultBool("isMasonryLayout")
isMasonryLayout: getUserDefaultBool("isMasonryLayout"),
isInfiniteScroll: getUserDefaultBool("isInfiniteScroll")
};
// Generate the client.
@ -120,6 +126,9 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
// Create the stream listener from the properties.
this.streamListener = this.client.stream(this.props.stream);
this.loadMoreTimelinePieces = this.loadMoreTimelinePieces.bind(this);
this.shouldLoadMorePosts = this.shouldLoadMorePosts.bind(this);
}
/**
@ -129,8 +138,7 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
this.streamListener.on("connect", () => {
// Get the latest posts from this timeline.
this.client
.get(this.props.timeline, { limit: 40 })
.get(this.props.timeline, { limit: 50 })
// If we succeeded, update the state and turn off loading.
.then((resp: any) => {
let statuses: [Status] = resp.data;
@ -198,10 +206,43 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
}
/**
* Halt the stream listener when unmounting the component.
* Insert a delay between repeated function calls
* codeburst.io/throttling-and-debouncing-in-javascript-646d076d0a44
* @param delay How long to wait before calling function (ms)
* @param fn The function to call
*/
debounced(delay: number, fn: Function) {
let lastCall = 0;
return function(...args: any) {
const now = new Date().getTime();
if (now - lastCall < delay) {
return;
}
lastCall = now;
return fn(...args);
};
}
/**
* Listen for when scroll position changes
*/
componentDidMount() {
if (this.state.isInfiniteScroll) {
window.addEventListener(
"scroll",
this.debounced(200, this.shouldLoadMorePosts)
);
}
}
/**
* Halt the stream and scroll listeners when unmounting the component.
*/
componentWillUnmount() {
this.streamListener.stop();
if (this.state.isInfiniteScroll) {
window.removeEventListener("scroll", this.shouldLoadMorePosts);
}
}
/**
@ -230,7 +271,7 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
this.client
.get(this.props.timeline, {
max_id: this.state.posts[this.state.posts.length - 1].id,
limit: 20
limit: 50
})
// If we succeeded, append them to the end of the list of posts.
@ -261,6 +302,17 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
}
}
/**
* Load more posts when scroll is near the end of the page
*/
shouldLoadMorePosts(e: Event) {
let difference =
document.body.clientHeight - window.scrollY - window.innerHeight;
if (difference < 10000 && this.state.viewIsLoading === false) {
this.loadMoreTimelinePieces();
}
}
/**
* Render the timeline page.
*/
@ -316,9 +368,9 @@ class TimelinePage extends Component<ITimelinePageProps, ITimelinePageState> {
return (
<div
className={classes.masonryGrid_item}
key={post.id}
>
<Post
key={post.id}
post={post}
client={this.client}
/>