Live post and comment resorting. Fixes #522

- Moving sorting to utils.
This commit is contained in:
Dessalines 2020-02-09 11:44:24 -05:00
parent 56cd103209
commit fd8814677e
7 changed files with 164 additions and 53 deletions

View file

@ -15,6 +15,8 @@ import {
TransferCommunityForm, TransferCommunityForm,
TransferSiteForm, TransferSiteForm,
BanType, BanType,
CommentSortType,
SortType,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -61,6 +63,8 @@ interface CommentNodeProps {
// TODO is this necessary, can't I get it from the node itself? // TODO is this necessary, can't I get it from the node itself?
postCreatorId?: number; postCreatorId?: number;
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType;
sortType?: SortType;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -630,6 +634,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
moderators={this.props.moderators} moderators={this.props.moderators}
admins={this.props.admins} admins={this.props.admins}
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
sort={this.props.sort}
sortType={this.props.sortType}
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}

View file

@ -3,7 +3,10 @@ import {
CommentNode as CommentNodeI, CommentNode as CommentNodeI,
CommunityUser, CommunityUser,
UserView, UserView,
CommentSortType,
SortType,
} from '../interfaces'; } from '../interfaces';
import { commentSort, commentSortSortType } from '../utils';
import { CommentNode } from './comment-node'; import { CommentNode } from './comment-node';
interface CommentNodesState {} interface CommentNodesState {}
@ -18,6 +21,8 @@ interface CommentNodesProps {
locked?: boolean; locked?: boolean;
markable?: boolean; markable?: boolean;
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType;
sortType?: SortType;
} }
export class CommentNodes extends Component< export class CommentNodes extends Component<
@ -31,7 +36,7 @@ export class CommentNodes extends Component<
render() { render() {
return ( return (
<div className="comments"> <div className="comments">
{this.props.nodes.map(node => ( {this.sorter().map(node => (
<CommentNode <CommentNode
node={node} node={node}
noIndent={this.props.noIndent} noIndent={this.props.noIndent}
@ -42,9 +47,21 @@ export class CommentNodes extends Component<
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
markable={this.props.markable} markable={this.props.markable}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
sort={this.props.sort}
sortType={this.props.sortType}
/> />
))} ))}
</div> </div>
); );
} }
sorter(): Array<CommentNodeI> {
if (this.props.sort !== undefined) {
commentSort(this.props.nodes, this.props.sort);
} else if (this.props.sortType !== undefined) {
commentSortSortType(this.props.nodes, this.props.sortType);
}
return this.props.nodes;
}
} }

View file

@ -178,9 +178,17 @@ export class Community extends Component<any, State> {
listings() { listings() {
return this.state.dataType == DataType.Post ? ( return this.state.dataType == DataType.Post ? (
<PostListings posts={this.state.posts} removeDuplicates /> <PostListings
posts={this.state.posts}
removeDuplicates
sort={this.state.sort}
/>
) : ( ) : (
<CommentNodes nodes={commentsToFlatNodes(this.state.comments)} noIndent /> <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
noIndent
sortType={this.state.sort}
/>
); );
} }

View file

@ -51,6 +51,7 @@ import {
createPostLikeFindRes, createPostLikeFindRes,
editPostFindRes, editPostFindRes,
commentsToFlatNodes, commentsToFlatNodes,
commentSortSortType,
} from '../utils'; } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -404,12 +405,18 @@ export class Main extends Component<any, MainState> {
listings() { listings() {
return this.state.dataType == DataType.Post ? ( return this.state.dataType == DataType.Post ? (
<PostListings posts={this.state.posts} showCommunity removeDuplicates /> <PostListings
posts={this.state.posts}
showCommunity
removeDuplicates
sort={this.state.sort}
/>
) : ( ) : (
<CommentNodes <CommentNodes
nodes={commentsToFlatNodes(this.state.comments)} nodes={commentsToFlatNodes(this.state.comments)}
noIndent noIndent
showCommunity showCommunity
sortType={this.state.sort}
/> />
); );
} }

View file

@ -1,6 +1,7 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Link } from 'inferno-router'; import { Link } from 'inferno-router';
import { Post } from '../interfaces'; import { Post, SortType } from '../interfaces';
import { postSort } from '../utils';
import { PostListing } from './post-listing'; import { PostListing } from './post-listing';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
@ -9,6 +10,7 @@ interface PostListingsProps {
posts: Array<Post>; posts: Array<Post>;
showCommunity?: boolean; showCommunity?: boolean;
removeDuplicates?: boolean; removeDuplicates?: boolean;
sort?: SortType;
} }
export class PostListings extends Component<PostListingsProps, any> { export class PostListings extends Component<PostListingsProps, any> {
@ -20,10 +22,7 @@ export class PostListings extends Component<PostListingsProps, any> {
return ( return (
<div> <div>
{this.props.posts.length > 0 ? ( {this.props.posts.length > 0 ? (
(this.props.removeDuplicates this.outer().map(post => (
? this.removeDuplicates(this.props.posts)
: this.props.posts
).map(post => (
<> <>
<PostListing <PostListing
post={post} post={post}
@ -47,6 +46,19 @@ export class PostListings extends Component<PostListingsProps, any> {
); );
} }
outer(): Array<Post> {
let out = this.props.posts;
if (this.props.removeDuplicates) {
out = this.removeDuplicates(out);
}
if (this.props.sort !== undefined) {
postSort(out, this.props.sort);
}
return out;
}
removeDuplicates(posts: Array<Post>): Array<Post> { removeDuplicates(posts: Array<Post>): Array<Post> {
// A map from post url to list of posts (dupes) // A map from post url to list of posts (dupes)
let urlMap = new Map<string, Array<Post>>(); let urlMap = new Map<string, Array<Post>>();

View file

@ -31,7 +31,6 @@ import {
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
wsJsonToRes, wsJsonToRes,
hotRank,
toast, toast,
editCommentRes, editCommentRes,
saveCommentRes, saveCommentRes,
@ -314,48 +313,9 @@ export class Post extends Component<any, PostState> {
} }
} }
this.sortTree(tree);
return tree; return tree;
} }
sortTree(tree: Array<CommentNodeI>) {
// First, put removed and deleted comments at the bottom, then do your other sorts
if (this.state.commentSort == CommentSortType.Top) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.score - a.comment.score
);
} else if (this.state.commentSort == CommentSortType.New) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.published.localeCompare(a.comment.published)
);
} else if (this.state.commentSort == CommentSortType.Old) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
a.comment.published.localeCompare(b.comment.published)
);
} else if (this.state.commentSort == CommentSortType.Hot) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
hotRank(b.comment) - hotRank(a.comment)
);
}
for (let node of tree) {
this.sortTree(node.children);
}
}
commentsTree() { commentsTree() {
let nodes = this.buildCommentsTree(); let nodes = this.buildCommentsTree();
return ( return (
@ -366,6 +326,7 @@ export class Post extends Component<any, PostState> {
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.admins}
postCreatorId={this.state.post.creator_id} postCreatorId={this.state.post.creator_id}
sort={this.state.commentSort}
/> />
</div> </div>
); );

108
ui/src/utils.ts vendored
View file

@ -20,6 +20,7 @@ import {
PrivateMessage, PrivateMessage,
User, User,
SortType, SortType,
CommentSortType,
ListingType, ListingType,
DataType, DataType,
SearchType, SearchType,
@ -93,15 +94,22 @@ md.renderer.rules.emoji = function(token, idx) {
return twemoji.parse(token[idx].content); return twemoji.parse(token[idx].content);
}; };
export function hotRank(comment: Comment): number { export function hotRankComment(comment: Comment): number {
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity return hotRank(comment.score, comment.published);
}
let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date export function hotRankPost(post: Post): number {
return hotRank(post.score, post.newest_activity_time);
}
export function hotRank(score: number, timeStr: string): number {
// Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date
let now: Date = new Date(); let now: Date = new Date();
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5; let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
let rank = let rank =
(10000 * Math.log10(Math.max(1, 3 + comment.score))) / (10000 * Math.log10(Math.max(1, 3 + score))) /
Math.pow(hoursElapsed + 2, 1.8); Math.pow(hoursElapsed + 2, 1.8);
// console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`); // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
@ -639,3 +647,95 @@ export function commentsToFlatNodes(
} }
return nodes; return nodes;
} }
export function commentSort(tree: Array<CommentNode>, sort: CommentSortType) {
// First, put removed and deleted comments at the bottom, then do your other sorts
if (sort == CommentSortType.Top) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.score - a.comment.score
);
} else if (sort == CommentSortType.New) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
b.comment.published.localeCompare(a.comment.published)
);
} else if (sort == CommentSortType.Old) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
a.comment.published.localeCompare(b.comment.published)
);
} else if (sort == CommentSortType.Hot) {
tree.sort(
(a, b) =>
+a.comment.removed - +b.comment.removed ||
+a.comment.deleted - +b.comment.deleted ||
hotRankComment(b.comment) - hotRankComment(a.comment)
);
}
// Go through the children recursively
for (let node of tree) {
if (node.children) {
commentSort(node.children, sort);
}
}
}
export function commentSortSortType(tree: Array<CommentNode>, sort: SortType) {
commentSort(tree, convertCommentSortType(sort));
}
function convertCommentSortType(sort: SortType): CommentSortType {
if (
sort == SortType.TopAll ||
sort == SortType.TopDay ||
sort == SortType.TopWeek ||
sort == SortType.TopMonth ||
sort == SortType.TopYear
) {
return CommentSortType.Top;
} else if (sort == SortType.New) {
return CommentSortType.New;
} else if (sort == SortType.Hot) {
return CommentSortType.Hot;
} else {
return CommentSortType.Hot;
}
}
export function postSort(posts: Array<Post>, sort: SortType) {
// First, put removed and deleted comments at the bottom, then do your other sorts
if (
sort == SortType.TopAll ||
sort == SortType.TopDay ||
sort == SortType.TopWeek ||
sort == SortType.TopMonth ||
sort == SortType.TopYear
) {
posts.sort(
(a, b) =>
+a.removed - +b.removed || +a.deleted - +b.deleted || b.score - a.score
);
} else if (sort == SortType.New) {
posts.sort(
(a, b) =>
+a.removed - +b.removed ||
+a.deleted - +b.deleted ||
b.published.localeCompare(a.published)
);
} else if (sort == SortType.Hot) {
posts.sort(
(a, b) =>
+a.removed - +b.removed ||
+a.deleted - +b.deleted ||
hotRankPost(b) - hotRankPost(a)
);
}
}