- [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)
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 {
interface State {
post: PostI;
comments: Array<Comment>;
+ commentSort: CommentSortType;
}
export class Post extends Component<any, State> {
id: null,
published: null,
},
- comments: []
+ comments: [],
+ commentSort: CommentSortType.Hot
}
constructor(props, context) {
<div class="col-12 col-sm-8 col-lg-7 mb-3">
{this.postHeader()}
<CommentForm postId={this.state.post.id} />
+ {this.sortRadios()}
{this.commentsTree()}
</div>
<div class="col-12 col-sm-4 col-lg-3 mb-3">
)
}
+ sortRadios() {
+ return (
+ <div class="btn-group btn-group-toggle mb-3">
+ <label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.Hot && 'active'}`}>Hot
+ <input type="radio" value={CommentSortType.Hot}
+ checked={this.state.commentSort === CommentSortType.Hot}
+ onChange={linkEvent(this, this.handleCommentSortChange)} />
+ </label>
+ <label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.Top && 'active'}`}>Top
+ <input type="radio" value={CommentSortType.Top}
+ checked={this.state.commentSort === CommentSortType.Top}
+ onChange={linkEvent(this, this.handleCommentSortChange)} />
+ </label>
+ <label className={`btn btn-sm btn-secondary ${this.state.commentSort === CommentSortType.New && 'active'}`}>New
+ <input type="radio" value={CommentSortType.New}
+ checked={this.state.commentSort === CommentSortType.New}
+ onChange={linkEvent(this, this.handleCommentSortChange)} />
+ </label>
+ </div>
+ )
+ }
+
newComments() {
return (
<div class="sticky-top">
</div>
);
}
-
- // buildCommentsTree(): Array<CommentNodeI> {
- buildCommentsTree(): any {
- let tree: Array<CommentNodeI> = 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<Comment>): Array<CommentNodeI> {
- let hashTable = {};
- for (let comment of comments) {
+ private buildCommentsTree(): Array<CommentNodeI> {
+ let map = new Map<number, CommentNodeI>();
+ 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<CommentNodeI> = [];
- 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<CommentNodeI>) {
+
+ 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 (
}
</div>
{this.state.showReply && <CommentForm node={node} onReplyCancel={this.handleReplyCancel} />}
- {this.props.node.children && <CommentNodes nodes={this.props.node.children}/>}
+ {this.props.node.children && <CommentNodes nodes={this.props.node.children} />}
</div>
)
}
</div>
<div class="row">
<div class="col-sm-12">
- <button type="submit" class="btn btn-secondary mr-2">{this.state.buttonTitle}</button>
- {this.props.node && <button type="button" class="btn btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
+ <button type="submit" class="btn btn-sm btn-secondary mr-2">{this.state.buttonTitle}</button>
+ {this.props.node && <button type="button" class="btn btn-sm btn-secondary" onClick={linkEvent(this, this.handleReplyCancel)}>Cancel</button>}
</div>
</div>
</form>