From b61ca7885f0ac41e9ab27da3d4cae4327b6b69cf Mon Sep 17 00:00:00 2001 From: Dessalines Date: Fri, 29 Mar 2019 23:08:02 -0700 Subject: [PATCH] Adding comment sorting - Fixes #15 --- README.md | 28 +++++++++ ui/src/components/post.tsx | 91 ++++++++++++++++++++++------- ui/src/interfaces.ts | 4 ++ ui/src/main.css | 10 ++++ ui/src/services/WebSocketService.ts | 1 + ui/src/utils.ts | 15 ++++- 6 files changed, 126 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 976d2e55c..f3f111d05 100644 --- a/README.md +++ b/README.md @@ -38,10 +38,38 @@ We have a twitter alternative (mastodon), a facebook alternative (friendica), so - [RXJS websocket](https://stackoverflow.com/questions/44060315/reconnecting-a-websocket-in-angular-and-rxjs/44067972#44067972) - [Rust JWT](https://github.com/Keats/jsonwebtoken) - [Hierarchical tree building javascript](https://stackoverflow.com/a/40732240/1655478) +- [Hot sorting discussion](https://meta.stackexchange.com/questions/11602/what-formula-should-be-used-to-determine-hot-questions) [2](https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9) ## TODOs - Endpoints - DB - Followers / following +# Trending / Hot / Best Sorting algorithm +## Goals +- During the day, new posts and comments should be near the top, so they can be voted on. +- After a day or so, the time factor should go away. +- Use a log scale, since votes tend to snowball, and so the first 10 votes are just as important as the next hundred. +## Reddit Sorting +[Reddit's comment sorting algorithm](https://medium.com/hacking-and-gonzo/how-reddit-ranking-algorithms-work-ef111e33d0d9), the wilson confidence sort, is inadequate, because it completely ignores time. What ends up happening, especially in smaller subreddits, is that the early comments end up getting upvoted, and newer comments stay at the bottom, never to be seen. + +## Hacker News Sorting +The [Hacker New's ranking algorithm](https://medium.com/hacking-and-gonzo/how-hacker-news-ranking-algorithm-works-1d9b0cf2c08d) is great, but it doesn't use a log scale for the scores. + +## My Algorithm +``` +Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity + +Score = Upvotes - Downvotes +Time = time since submission (in hours) +Gravity = Decay gravity, 1.8 is default +``` + +- Add 1 to the score, so that the standard new comment score of +1 will be affected by time decay. Otherwise all new comments would stay at zero, near the bottom. +- The sign and abs of the score are necessary for dealing with the log of negative scores. +- A scale factor of 10k gets the rank in integer form. + +A plot of rank over 24 hours, of scores of 1, 5, 10, 100, 1000, with a scale factor of 10k. + +![](https://i.imgur.com/w8oBLlL.png) diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 1cd61ea31..feb815e57 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -1,9 +1,9 @@ import { Component, linkEvent } from 'inferno'; import { Subscription } from "rxjs"; import { retryWhen, delay, take } from 'rxjs/operators'; -import { UserOperation, Community, Post as PostI, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CreateCommentLikeResponse } from '../interfaces'; +import { UserOperation, Community, Post as PostI, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CreateCommentLikeResponse, CommentSortType } from '../interfaces'; import { WebSocketService, UserService } from '../services'; -import { msgOp } from '../utils'; +import { msgOp, hotRank } from '../utils'; import { MomentTime } from './moment-time'; interface CommentNodeI { @@ -14,6 +14,7 @@ interface CommentNodeI { interface State { post: PostI; comments: Array; + commentSort: CommentSortType; } export class Post extends Component { @@ -27,7 +28,8 @@ export class Post extends Component { id: null, published: null, }, - comments: [] + comments: [], + commentSort: CommentSortType.Hot } constructor(props, context) { @@ -59,6 +61,7 @@ export class Post extends Component {
{this.postHeader()} + {this.sortRadios()} {this.commentsTree()}
@@ -88,6 +91,28 @@ export class Post extends Component { ) } + sortRadios() { + return ( +
+ + + +
+ ) + } + newComments() { return (
@@ -107,28 +132,50 @@ export class Post extends Component {
); } + + handleCommentSortChange(i: Post, event) { + i.state.commentSort = Number(event.target.value); + i.setState(i.state); + } + + private buildCommentsTree(): Array { + let map = new Map(); + for (let comment of this.state.comments) { + let node: CommentNodeI = { + comment: comment, + children: [] + }; + map.set(comment.id, { ...node }); + } + let tree: Array = []; + for (let comment of this.state.comments) { + if( comment.parent_id ) { + map.get(comment.parent_id).children.push(map.get(comment.id)); + } + else { + tree.push(map.get(comment.id)); + } + } + + this.sortTree(tree); - // buildCommentsTree(): Array { - buildCommentsTree(): any { - let tree: Array = this.createCommentsTree(this.state.comments); - console.log(tree); // TODO this is redoing every time and it shouldn't return tree; } - private createCommentsTree(comments: Array): Array { - let hashTable = {}; - for (let comment of comments) { - let node: CommentNodeI = { - comment: comment - }; - hashTable[comment.id] = { ...node, children : [] }; + sortTree(tree: Array) { + + if (this.state.commentSort == CommentSortType.Top) { + tree.sort((a, b) => b.comment.score - a.comment.score); + } else if (this.state.commentSort == CommentSortType.New) { + tree.sort((a, b) => b.comment.published.localeCompare(a.comment.published)); + } else if (this.state.commentSort == CommentSortType.Hot) { + tree.sort((a, b) => hotRank(b.comment) - hotRank(a.comment)); } - let tree: Array = []; - for (let comment of comments) { - if( comment.parent_id ) hashTable[comment.parent_id].children.push(hashTable[comment.id]); - else tree.push(hashTable[comment.id]); + + for (let node of tree) { + this.sortTree(node.children); } - return tree; + } commentsTree() { @@ -280,7 +327,7 @@ export class CommentNode extends Component { }
{this.state.showReply && } - {this.props.node.children && } + {this.props.node.children && } ) } @@ -389,8 +436,8 @@ export class CommentForm extends Component {
- - {this.props.node && } + + {this.props.node && }
diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index c89a254ec..95ea5e093 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -112,4 +112,8 @@ export interface LoginResponse { jwt: string; } +export enum CommentSortType { + Hot, Top, New +} + diff --git a/ui/src/main.css b/ui/src/main.css index d0cb8baf8..089a53b89 100644 --- a/ui/src/main.css +++ b/ui/src/main.css @@ -13,3 +13,13 @@ body { .downvote:hover { color: var(--danger); } + +.form-control, .form-control:focus { + background-color: var(--secondary); + color: #fff; +} + +.custom-select { + color: #fff; + background-color: var(--secondary); +} diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index b35d97a1f..1ea207f4e 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -88,5 +88,6 @@ export class WebSocketService { window.onbeforeunload = (e => { WebSocketService.Instance.subject.unsubscribe(); + WebSocketService.Instance.subject = null; }); diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 02c1afbff..1d490a303 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -1,4 +1,4 @@ -import { UserOperation } from './interfaces'; +import { UserOperation, Comment } from './interfaces'; export let repoUrl = 'https://github.com/dessalines/rust-reddit-fediverse'; export let wsUri = (window.location.protocol=='https:'&&'wss://'||'ws://')+window.location.host + '/service/ws/'; @@ -8,3 +8,16 @@ export function msgOp(msg: any): UserOperation { return UserOperation[opStr]; } +export function hotRank(comment: Comment): number { + // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity + + let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date + let now: Date = new Date(); + let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5; + + let rank = (10000 * Math.sign(comment.score) * Math.log10(1 + Math.abs(comment.score))) / Math.pow(hoursElapsed + 2, 1.8); + + // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`); + + return rank; +}