From 480b627ff516627c308090b43b4219d8070984a7 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 | 87 ++++++++++++++++++++++------- ui/src/interfaces.ts | 4 ++ ui/src/main.css | 10 ++++ ui/src/services/WebSocketService.ts | 1 + ui/src/utils.ts | 15 ++++- 6 files changed, 124 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 976d2e55..f3f111d0 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 1cd61ea3..feb815e5 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,30 +132,52 @@ export class Post extends Component {
); } - - // 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; + + handleCommentSortChange(i: Post, event) { + i.state.commentSort = Number(event.target.value); + i.setState(i.state); } - private createCommentsTree(comments: Array): Array { - let hashTable = {}; - for (let comment of comments) { + private buildCommentsTree(): Array { + let map = new Map(); + for (let comment of this.state.comments) { let node: CommentNodeI = { - comment: comment + comment: comment, + children: [] }; - hashTable[comment.id] = { ...node, children : [] }; + map.set(comment.id, { ...node }); } 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 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); + return tree; } + 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)); + } + + for (let node of tree) { + this.sortTree(node.children); + } + + } + commentsTree() { let nodes = this.buildCommentsTree(); return ( @@ -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 c89a254e..95ea5e09 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 d0cb8baf..089a53b8 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 b35d97a1..1ea207f4 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 02c1afbf..1d490a30 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; +} -- 2.44.1