]> Untitled Git - lemmy.git/commitdiff
Adding comment sorting
authorDessalines <tyhou13@gmx.com>
Sat, 30 Mar 2019 06:08:02 +0000 (23:08 -0700)
committerDessalines <tyhou13@gmx.com>
Sat, 30 Mar 2019 06:08:02 +0000 (23:08 -0700)
- Fixes #15

README.md
ui/src/components/post.tsx
ui/src/interfaces.ts
ui/src/main.css
ui/src/services/WebSocketService.ts
ui/src/utils.ts

index 976d2e55cac1248a938b6e1d2a378daed7fa05fd..f3f111d05dd7cec61b8e44375be81e5ef4f69d0e 100644 (file)
--- 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)
index 1cd61ea31020aed9c6ac5ad2a55945db5b0cffcd..feb815e5729b5a042964cdee51c1931462adba10 100644 (file)
@@ -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<Comment>;
+  commentSort: CommentSortType;
 }
 
 export class Post extends Component<any, State> {
@@ -27,7 +28,8 @@ export class Post extends Component<any, State> {
       id: null,
       published: null,
     },
-    comments: []
+    comments: [],
+    commentSort: CommentSortType.Hot
   }
 
   constructor(props, context) {
@@ -59,6 +61,7 @@ export class Post extends Component<any, State> {
           <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">
@@ -88,6 +91,28 @@ export class Post extends Component<any, State> {
     )
   }
 
+  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">
@@ -107,30 +132,52 @@ export class Post extends Component<any, State> {
       </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 (
@@ -280,7 +327,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
           }
         </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>
     )
   }
@@ -389,8 +436,8 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
           </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>
index c89a254ec3f8a8d92b987476d2364d2583891436..95ea5e0931b98af50099218840896d7af2a4c175 100644 (file)
@@ -112,4 +112,8 @@ export interface LoginResponse {
   jwt: string;
 }
 
+export enum CommentSortType {
+  Hot, Top, New
+}
+
 
index d0cb8baf843b81efd6a1fc606b91b2ed967c4d24..089a53b890d19c829c199d8db16876952da7d5b0 100644 (file)
@@ -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);
+}
index b35d97a1fd4cf05da147be284271c62f70e5f8a9..1ea207f4eff59015a331432190fabd6b72a70284 100644 (file)
@@ -88,5 +88,6 @@ export class WebSocketService {
 
 window.onbeforeunload = (e => {
   WebSocketService.Instance.subject.unsubscribe();
+  WebSocketService.Instance.subject = null;
 });
 
index 02c1afbff3c32597f496a30e1b26442c2abfaf82..1d490a303f9fdaf1e3f636625e6a71fea0d6029d 100644 (file)
@@ -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;
+}