]> Untitled Git - lemmy.git/commitdiff
Merge branch 'dev' into federation
authorDessalines <tyhou13@gmx.com>
Mon, 10 Feb 2020 16:52:32 +0000 (11:52 -0500)
committerDessalines <tyhou13@gmx.com>
Mon, 10 Feb 2020 16:52:32 +0000 (11:52 -0500)
38 files changed:
README.md
ansible/VERSION
docker/prod/docker-compose.yml
install.sh
server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql [new file with mode: 0644]
server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql [new file with mode: 0644]
server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql [new file with mode: 0644]
server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql [new file with mode: 0644]
server/src/api/comment.rs
server/src/db/comment_view.rs
server/src/db/community_view.rs
server/src/db/post_view.rs
server/src/db/user_mention_view.rs
server/src/db/user_view.rs
server/src/routes/index.rs
server/src/version.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/assets/css/main.css
ui/package.json
ui/src/components/comment-node.tsx
ui/src/components/comment-nodes.tsx
ui/src/components/community.tsx
ui/src/components/data-type-select.tsx [new file with mode: 0644]
ui/src/components/inbox.tsx
ui/src/components/main.tsx
ui/src/components/post-listing.tsx
ui/src/components/post-listings.tsx
ui/src/components/post.tsx
ui/src/components/search.tsx
ui/src/components/user.tsx
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts
ui/src/translations/en.ts
ui/src/utils.ts
ui/src/version.ts
ui/yarn.lock

index fcb07e72d94ed579e53788ee7d5b680dada29fbc..47290953c4ff16d982b586c23a028312fbd57d29 100644 (file)
--- a/README.md
+++ b/README.md
@@ -130,19 +130,19 @@ If you'd like to add translations, take a look at the [English translation file]
 
 lang | done | missing
 ---- | ---- | -------
-ca | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
-de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-eo | 74% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-es | 99% | cross_posted_to,post_title_too_long
-fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
-fr | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-nl | 98% | cross_posted_to,post_title_too_long,time,action
-pt-br | 100% | post_title_too_long
-ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+ca | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,couldnt_get_comments,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+eo | 73% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
+fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+fr | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,time,action
+pt-br | 99% | couldnt_get_comments,post_title_too_long
+ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
 <!-- translationsstop -->
 
 If you'd like to update this report, run:
index 1adfd94d330ec8281ae0727f2c2e7941e34910aa..c32d86a508f6423156c185339d99fe7bc57725c2 100644 (file)
@@ -1 +1 @@
-v0.6.13
+v0.6.17
index a5c2918ab0d27744039b67b4effe14efa92ab506..4baceb3edfd9a31f62e6ddcec661736e62b74388 100644 (file)
@@ -11,7 +11,7 @@ services:
       - lemmy_db:/var/lib/postgresql/data
     restart: always
   lemmy:
-    image: dessalines/lemmy:v0.6.13
+    image: dessalines/lemmy:v0.6.17
     ports:
       - "127.0.0.1:8536:8536"
     restart: always
index 168a1f6b0c29dbe8b13d77dbecbf7d074e43d76a..b368891cf8d2be1069ac089d164f22cb5beced58 100755 (executable)
@@ -1,13 +1,41 @@
-#!/bin/sh
+#!/bin/bash
 set -e
 
+# Set the database variable to the default first.
+# Don't forget to change this string to your actual database parameters
+# if you don't plan to initialize the database in this script.
 export LEMMY_DATABASE_URL=postgres://lemmy:password@localhost:5432/lemmy
+
+# Set other environment variables
 export JWT_SECRET=changeme
 export HOSTNAME=rrr
 
+# Optionally initialize the database
+init_db_valid=0
+init_db_final=0
+while [ "$init_db_valid" == 0 ]
+do
+  read -p "Initialize database (y/n)? " init_db
+  case "${init_db,,}" in 
+    y|yes ) init_db_valid=1; init_db_final=1;;
+    n|no ) init_db_valid=1; init_db_final=0;;
+    * ) echo "Invalid input" 1>&2;;
+  esac
+  echo
+done
+if [ "$init_db_final" = 1 ]
+then
+  source ./server/db-init.sh
+  read -n 1 -s -r -p "Press ANY KEY to continue execution of this script, press CTRL+C to quit..."
+  echo
+fi
+
+# Build the web client
 cd ui
 yarn
 yarn build
+
+# Build and run the backend
 cd ../server
 cargo run
 
diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql
new file mode 100644 (file)
index 0000000..b6120d1
--- /dev/null
@@ -0,0 +1,206 @@
+
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select        
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as saved
+from all_comment ac
+;
+
+
+-- Do the reply_view referencing the comment_mview
+create view reply_view as 
+with closereply as (
+    select 
+    c2.id, 
+    c2.creator_id as sender_id, 
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+create view user_mention_view as
+select 
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+
+create view user_mention_mview as 
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select 
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    null as user_id, 
+    null as my_vote,
+    null as saved,
+    um.recipient_id
+from all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql
new file mode 100644 (file)
index 0000000..8836a57
--- /dev/null
@@ -0,0 +1,220 @@
+
+-- Adding community name, hot_rank, to comment_view, user_mention_view, and subscribed to comment_view
+
+-- Rebuild the comment view
+drop view reply_view;
+drop view user_mention_view;
+drop view user_mention_mview;
+drop view comment_view;
+drop view comment_mview;
+drop materialized view comment_aggregates_mview;
+drop view comment_aggregates_view;
+
+-- reply and comment view
+create view comment_aggregates_view as
+select        
+c.*,
+(select community_id from post p where p.id = c.post_id),
+(select co.name from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_name,
+(select u.banned from user_ u where c.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb, post p where c.creator_id = cb.user_id and p.id = c.post_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where c.creator_id = user_.id) as creator_name,
+(select avatar from user_ where c.creator_id = user_.id) as creator_avatar,
+coalesce(sum(cl.score), 0) as score,
+count (case when cl.score = 1 then 1 else null end) as upvotes,
+count (case when cl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(cl.score) , 0), c.published) as hot_rank
+from comment c
+left join comment_like cl on c.id = cl.comment_id
+group by c.id;
+
+create materialized view comment_aggregates_mview as select * from comment_aggregates_view;
+
+create unique index idx_comment_aggregates_mview_id on comment_aggregates_mview (id);
+
+create view comment_view as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_view ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+create view comment_mview as
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+ac.*,
+u.id as user_id,
+coalesce(cl.score, 0) as my_vote,
+(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.community_id = cf.community_id) as subscribed,
+(select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+
+union all
+
+select 
+    ac.*,
+    null as user_id, 
+    null as my_vote,
+    null as subscribed,
+    null as saved
+from all_comment ac
+;
+
+-- Do the reply_view referencing the comment_mview
+create view reply_view as 
+with closereply as (
+    select 
+    c2.id, 
+    c2.creator_id as sender_id, 
+    c.creator_id as recipient_id
+    from comment c
+    inner join comment c2 on c.id = c2.parent_id
+    where c2.creator_id != c.creator_id
+    -- Do union where post is null
+    union
+    select
+    c.id,
+    c.creator_id as sender_id,
+    p.creator_id as recipient_id
+    from comment c, post p
+    where c.post_id = p.id and c.parent_id is null and c.creator_id != p.creator_id
+)
+select cv.*,
+closereply.recipient_id
+from comment_mview cv, closereply
+where closereply.id = cv.id
+;
+
+-- user mention
+create view user_mention_view as
+select 
+    c.id,
+    um.id as user_mention_id,
+    c.creator_id,
+    c.post_id,
+    c.parent_id,
+    c.content,
+    c.removed,
+    um.read,
+    c.published,
+    c.updated,
+    c.deleted,
+    c.community_id,
+    c.community_name,
+    c.banned,
+    c.banned_from_community,
+    c.creator_name,
+    c.creator_avatar,
+    c.score,
+    c.upvotes,
+    c.downvotes,
+    c.hot_rank,
+    c.user_id,
+    c.my_vote,
+    c.saved,
+    um.recipient_id
+from user_mention um, comment_view c
+where um.comment_id = c.id;
+
+
+create view user_mention_mview as 
+with all_comment as
+(
+  select
+  ca.*
+  from comment_aggregates_mview ca
+)
+
+select
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    u.id as user_id,
+    coalesce(cl.score, 0) as my_vote,
+    (select cs.id::bool from comment_saved cs where u.id = cs.user_id and cs.comment_id = ac.id) as saved,
+    um.recipient_id
+from user_ u
+cross join all_comment ac
+left join comment_like cl on u.id = cl.user_id and ac.id = cl.comment_id
+left join user_mention um on um.comment_id = ac.id
+
+union all
+
+select 
+    ac.id,
+    um.id as user_mention_id,
+    ac.creator_id,
+    ac.post_id,
+    ac.parent_id,
+    ac.content,
+    ac.removed,
+    um.read,
+    ac.published,
+    ac.updated,
+    ac.deleted,
+    ac.community_id,
+    ac.community_name,
+    ac.banned,
+    ac.banned_from_community,
+    ac.creator_name,
+    ac.creator_avatar,
+    ac.score,
+    ac.upvotes,
+    ac.downvotes,
+    ac.hot_rank,
+    null as user_id, 
+    null as my_vote,
+    null as saved,
+    um.recipient_id
+from all_comment ac
+left join user_mention um on um.comment_id = ac.id
+;
+
diff --git a/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql
new file mode 100644 (file)
index 0000000..8b912fa
--- /dev/null
@@ -0,0 +1,88 @@
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select        
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(select name from community where p.community_id = community.id) as community_name,
+(select removed from community c where p.community_id = c.id) as community_removed,
+(select deleted from community c where p.community_id = c.id) as community_deleted,
+(select nsfw from community c where p.community_id = c.id) as community_nsfw,
+(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+coalesce(sum(pl.score), 0) as score,
+count (case when pl.score = 1 then 1 else null end) as upvotes,
+count (case when pl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank
+from post p
+left join post_like pl on p.id = pl.post_id
+group by p.id;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_view pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+create view post_mview as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_mview pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
diff --git a/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql
new file mode 100644 (file)
index 0000000..e154127
--- /dev/null
@@ -0,0 +1,106 @@
+-- Adds a newest_activity_time for the post_views, in order to sort by newest comment
+drop view post_view;
+drop view post_mview;
+drop materialized view post_aggregates_mview;
+drop view post_aggregates_view;
+
+-- regen post view
+create view post_aggregates_view as
+select        
+p.*,
+(select u.banned from user_ u where p.creator_id = u.id) as banned,
+(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
+(select name from user_ where p.creator_id = user_.id) as creator_name,
+(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
+(select name from community where p.community_id = community.id) as community_name,
+(select removed from community c where p.community_id = c.id) as community_removed,
+(select deleted from community c where p.community_id = c.id) as community_deleted,
+(select nsfw from community c where p.community_id = c.id) as community_nsfw,
+(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
+coalesce(sum(pl.score), 0) as score,
+count (case when pl.score = 1 then 1 else null end) as upvotes,
+count (case when pl.score = -1 then 1 else null end) as downvotes,
+hot_rank(coalesce(sum(pl.score) , 0), 
+  (
+    case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+    else greatest(c.recent_comment_time, p.published)
+    end
+  )
+) as hot_rank,
+(
+  case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
+  else greatest(c.recent_comment_time, p.published)
+  end
+) as newest_activity_time 
+from post p
+left join post_like pl on p.id = pl.post_id
+left join (
+  select post_id, 
+  max(published) as recent_comment_time
+  from comment
+  group by 1
+) c on p.id = c.post_id
+group by p.id, c.recent_comment_time;
+
+create materialized view post_aggregates_mview as select * from post_aggregates_view;
+
+create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
+
+create view post_view as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_view pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
+create view post_mview as 
+with all_post as (
+  select
+  pa.*
+  from post_aggregates_mview pa
+)
+select
+ap.*,
+u.id as user_id,
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
+(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
+(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
+from user_ u
+cross join all_post ap
+left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
+
+union all
+
+select 
+ap.*,
+null as user_id,
+null as my_vote,
+null as subscribed,
+null as read,
+null as saved
+from all_post ap
+;
+
index 775085e93cb87ec4085e3d14d8c517e5ac43405e..5c614966682dfa138ae52cde66e6a19e46eacaa2 100644 (file)
@@ -2,6 +2,7 @@ use super::*;
 use crate::send_email;
 use crate::settings::Settings;
 use diesel::PgConnection;
+use std::str::FromStr;
 
 #[derive(Serialize, Deserialize)]
 pub struct CreateComment {
@@ -47,6 +48,21 @@ pub struct CreateCommentLike {
   auth: String,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct GetComments {
+  type_: String,
+  sort: String,
+  page: Option<i64>,
+  limit: Option<i64>,
+  pub community_id: Option<i32>,
+  auth: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCommentsResponse {
+  comments: Vec<CommentView>,
+}
+
 impl Perform<CommentResponse> for Oper<CreateComment> {
   fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
     let data: &CreateComment = &self.data;
@@ -456,3 +472,40 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     })
   }
 }
+
+impl Perform<GetCommentsResponse> for Oper<GetComments> {
+  fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
+    let data: &GetComments = &self.data;
+
+    let user_claims: Option<Claims> = match &data.auth {
+      Some(auth) => match Claims::decode(&auth) {
+        Ok(claims) => Some(claims.claims),
+        Err(_e) => None,
+      },
+      None => None,
+    };
+
+    let user_id = match &user_claims {
+      Some(claims) => Some(claims.id),
+      None => None,
+    };
+
+    let type_ = ListingType::from_str(&data.type_)?;
+    let sort = SortType::from_str(&data.sort)?;
+
+    let comments = match CommentQueryBuilder::create(&conn)
+      .listing_type(type_)
+      .sort(&sort)
+      .for_community_id(data.community_id)
+      .my_user_id(user_id)
+      .page(data.page)
+      .limit(data.limit)
+      .list()
+    {
+      Ok(comments) => comments,
+      Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
+    };
+
+    Ok(GetCommentsResponse { comments })
+  }
+}
index febf18b78c5305e73533db78450369abc3c8154b..ff915d5e5cc3e1eb24119c35cc8ea35585efbec2 100644 (file)
@@ -15,6 +15,7 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
@@ -22,8 +23,10 @@ table! {
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
+    hot_rank -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
+    subscribed -> Nullable<Bool>,
     saved -> Nullable<Bool>,
   }
 }
@@ -41,6 +44,7 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
@@ -48,8 +52,10 @@ table! {
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
+    hot_rank -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
+    subscribed -> Nullable<Bool>,
     saved -> Nullable<Bool>,
   }
 }
@@ -70,6 +76,7 @@ pub struct CommentView {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub community_id: i32,
+  pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_name: String,
@@ -77,15 +84,19 @@ pub struct CommentView {
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
+  pub hot_rank: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
+  pub subscribed: Option<bool>,
   pub saved: Option<bool>,
 }
 
 pub struct CommentQueryBuilder<'a> {
   conn: &'a PgConnection,
   query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
+  listing_type: ListingType,
   sort: &'a SortType,
+  for_community_id: Option<i32>,
   for_post_id: Option<i32>,
   for_creator_id: Option<i32>,
   search_term: Option<String>,
@@ -104,7 +115,9 @@ impl<'a> CommentQueryBuilder<'a> {
     CommentQueryBuilder {
       conn,
       query,
+      listing_type: ListingType::All,
       sort: &SortType::New,
+      for_community_id: None,
       for_post_id: None,
       for_creator_id: None,
       search_term: None,
@@ -115,6 +128,11 @@ impl<'a> CommentQueryBuilder<'a> {
     }
   }
 
+  pub fn listing_type(mut self, listing_type: ListingType) -> Self {
+    self.listing_type = listing_type;
+    self
+  }
+
   pub fn sort(mut self, sort: &'a SortType) -> Self {
     self.sort = sort;
     self
@@ -130,6 +148,11 @@ impl<'a> CommentQueryBuilder<'a> {
     self
   }
 
+  pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
+    self.for_community_id = for_community_id.get_optional();
+    self
+  }
+
   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
     self.search_term = search_term.get_optional();
     self
@@ -171,6 +194,10 @@ impl<'a> CommentQueryBuilder<'a> {
       query = query.filter(creator_id.eq(for_creator_id));
     };
 
+    if let Some(for_community_id) = self.for_community_id {
+      query = query.filter(community_id.eq(for_community_id));
+    }
+
     if let Some(for_post_id) = self.for_post_id {
       query = query.filter(post_id.eq(for_post_id));
     };
@@ -179,12 +206,18 @@ impl<'a> CommentQueryBuilder<'a> {
       query = query.filter(content.ilike(fuzzy_search(&search_term)));
     };
 
+    if let ListingType::Subscribed = self.listing_type {
+      query = query.filter(subscribed.eq(true));
+    }
+
     if self.saved_only {
       query = query.filter(saved.eq(true));
     }
 
     query = match self.sort {
-      // SortType::Hot => query.order(hot_rank.desc(), published.desc()),
+      SortType::Hot => query
+        .order_by(hot_rank.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -199,7 +232,7 @@ impl<'a> CommentQueryBuilder<'a> {
       SortType::TopDay => query
         .filter(published.gt(now - 1.days()))
         .order_by(score.desc()),
-      _ => query.order_by(published.desc()),
+      // _ => query.order_by(published.desc()),
     };
 
     let (limit, offset) = limit_and_offset(self.page, self.limit);
@@ -218,9 +251,8 @@ impl CommentView {
     from_comment_id: i32,
     my_user_id: Option<i32>,
   ) -> Result<Self, Error> {
-    use super::comment_view::comment_view::dsl::*;
-
-    let mut query = comment_view.into_boxed();
+    use super::comment_view::comment_mview::dsl::*;
+    let mut query = comment_mview.into_boxed();
 
     // The view lets you pass a null user_id, if you're not logged in
     if let Some(my_user_id) = my_user_id {
@@ -251,6 +283,7 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
@@ -258,8 +291,10 @@ table! {
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
+    hot_rank -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
+    subscribed -> Nullable<Bool>,
     saved -> Nullable<Bool>,
     recipient_id -> Int4,
   }
@@ -281,6 +316,7 @@ pub struct ReplyView {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub community_id: i32,
+  pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_name: String,
@@ -288,8 +324,10 @@ pub struct ReplyView {
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
+  pub hot_rank: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
+  pub subscribed: Option<bool>,
   pub saved: Option<bool>,
   pub recipient_id: i32,
 }
@@ -474,6 +512,7 @@ mod tests {
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
       community_id: inserted_community.id,
+      community_name: inserted_community.name.to_owned(),
       parent_id: None,
       removed: false,
       deleted: false,
@@ -486,9 +525,11 @@ mod tests {
       creator_avatar: None,
       score: 1,
       downvotes: 0,
+      hot_rank: 0,
       upvotes: 1,
       user_id: None,
       my_vote: None,
+      subscribed: None,
       saved: None,
     };
 
@@ -498,6 +539,7 @@ mod tests {
       creator_id: inserted_user.id,
       post_id: inserted_post.id,
       community_id: inserted_community.id,
+      community_name: inserted_community.name.to_owned(),
       parent_id: None,
       removed: false,
       deleted: false,
@@ -510,21 +552,26 @@ mod tests {
       creator_avatar: None,
       score: 1,
       downvotes: 0,
+      hot_rank: 0,
       upvotes: 1,
       user_id: Some(inserted_user.id),
       my_vote: Some(1),
+      subscribed: None,
       saved: None,
     };
 
-    let read_comment_views_no_user = CommentQueryBuilder::create(&conn)
+    let mut read_comment_views_no_user = CommentQueryBuilder::create(&conn)
       .for_post_id(inserted_post.id)
       .list()
       .unwrap();
-    let read_comment_views_with_user = CommentQueryBuilder::create(&conn)
+    read_comment_views_no_user[0].hot_rank = 0;
+
+    let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
       .for_post_id(inserted_post.id)
       .my_user_id(inserted_user.id)
       .list()
       .unwrap();
+    read_comment_views_with_user[0].hot_rank = 0;
 
     let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
     let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
index 95e00c65c9bd97c43268a8705ce56ae7b555fd95..18ff67a8a014afd005933d88bde829c29ef5c816 100644 (file)
@@ -227,9 +227,9 @@ impl CommunityView {
     from_community_id: i32,
     from_user_id: Option<i32>,
   ) -> Result<Self, Error> {
-    use super::community_view::community_view::dsl::*;
+    use super::community_view::community_mview::dsl::*;
 
-    let mut query = community_view.into_boxed();
+    let mut query = community_mview.into_boxed();
 
     query = query.filter(id.eq(from_community_id));
 
index 4d09308d89556281f27fee8f504c3a56fcfc5b28..3f385077fabc82fe8ff8f2a575e4c1fa5e84ddab 100644 (file)
@@ -31,6 +31,7 @@ table! {
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    newest_activity_time -> Timestamp,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
@@ -70,6 +71,7 @@ pub struct PostView {
   pub upvotes: i64,
   pub downvotes: i64,
   pub hot_rank: i32,
+  pub newest_activity_time: chrono::NaiveDateTime,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub subscribed: Option<bool>,
@@ -106,6 +108,7 @@ table! {
     upvotes -> BigInt,
     downvotes -> BigInt,
     hot_rank -> Int4,
+    newest_activity_time -> Timestamp,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     subscribed -> Nullable<Bool>,
@@ -121,6 +124,9 @@ pub struct PostQueryBuilder<'a> {
   sort: &'a SortType,
   my_user_id: Option<i32>,
   for_creator_id: Option<i32>,
+  for_community_id: Option<i32>,
+  search_term: Option<String>,
+  url_search: Option<String>,
   show_nsfw: bool,
   saved_only: bool,
   unread_only: bool,
@@ -137,10 +143,13 @@ impl<'a> PostQueryBuilder<'a> {
     PostQueryBuilder {
       conn,
       query,
-      my_user_id: None,
-      for_creator_id: None,
       listing_type: ListingType::All,
       sort: &SortType::Hot,
+      my_user_id: None,
+      for_creator_id: None,
+      for_community_id: None,
+      search_term: None,
+      url_search: None,
       show_nsfw: true,
       saved_only: false,
       unread_only: false,
@@ -160,38 +169,22 @@ impl<'a> PostQueryBuilder<'a> {
   }
 
   pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
-    use super::post_view::post_mview::dsl::*;
-    if let Some(for_community_id) = for_community_id.get_optional() {
-      self.query = self.query.filter(community_id.eq(for_community_id));
-      self.query = self.query.then_order_by(stickied.desc());
-    }
+    self.for_community_id = for_community_id.get_optional();
     self
   }
 
   pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
-    if let Some(for_creator_id) = for_creator_id.get_optional() {
-      self.for_creator_id = Some(for_creator_id);
-    }
+    self.for_creator_id = for_creator_id.get_optional();
     self
   }
 
   pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
-    use super::post_view::post_mview::dsl::*;
-    if let Some(search_term) = search_term.get_optional() {
-      let searcher = fuzzy_search(&search_term);
-      self.query = self
-        .query
-        .filter(name.ilike(searcher.to_owned()))
-        .or_filter(body.ilike(searcher));
-    }
+    self.search_term = search_term.get_optional();
     self
   }
 
   pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
-    use super::post_view::post_mview::dsl::*;
-    if let Some(url_search) = url_search.get_optional() {
-      self.query = self.query.filter(url.eq(url_search));
-    }
+    self.url_search = url_search.get_optional();
     self
   }
 
@@ -234,6 +227,22 @@ impl<'a> PostQueryBuilder<'a> {
       query = query.filter(subscribed.eq(true));
     }
 
+    if let Some(for_community_id) = self.for_community_id {
+      query = query.filter(community_id.eq(for_community_id));
+      query = query.then_order_by(stickied.desc());
+    }
+
+    if let Some(url_search) = self.url_search {
+      query = query.filter(url.eq(url_search));
+    }
+
+    if let Some(search_term) = self.search_term {
+      let searcher = fuzzy_search(&search_term);
+      query = query
+        .filter(name.ilike(searcher.to_owned()))
+        .or_filter(body.ilike(searcher));
+    }
+
     query = match self.sort {
       SortType::Hot => query
         .then_order_by(hot_rank.desc())
@@ -306,10 +315,10 @@ impl PostView {
     from_post_id: i32,
     my_user_id: Option<i32>,
   ) -> Result<Self, Error> {
-    use super::post_view::post_view::dsl::*;
+    use super::post_view::post_mview::dsl::*;
     use diesel::prelude::*;
 
-    let mut query = post_view.into_boxed();
+    let mut query = post_mview.into_boxed();
 
     query = query.filter(id.eq(from_post_id));
 
@@ -439,6 +448,7 @@ mod tests {
       downvotes: 0,
       hot_rank: 1728,
       published: inserted_post.published,
+      newest_activity_time: inserted_post.published,
       updated: None,
       subscribed: None,
       read: None,
@@ -473,6 +483,7 @@ mod tests {
       downvotes: 0,
       hot_rank: 1728,
       published: inserted_post.published,
+      newest_activity_time: inserted_post.published,
       updated: None,
       subscribed: None,
       read: None,
index 1cf43984a07f01fff0d91ab56707f2ad9cfe18f6..8046747e68ea48c79788ad20db9418679261ee75 100644 (file)
@@ -16,6 +16,7 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
@@ -23,6 +24,7 @@ table! {
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
+    hot_rank -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
@@ -44,6 +46,7 @@ table! {
     updated -> Nullable<Timestamp>,
     deleted -> Bool,
     community_id -> Int4,
+    community_name -> Varchar,
     banned -> Bool,
     banned_from_community -> Bool,
     creator_name -> Varchar,
@@ -51,6 +54,7 @@ table! {
     score -> BigInt,
     upvotes -> BigInt,
     downvotes -> BigInt,
+    hot_rank -> Int4,
     user_id -> Nullable<Int4>,
     my_vote -> Nullable<Int4>,
     saved -> Nullable<Bool>,
@@ -75,6 +79,7 @@ pub struct UserMentionView {
   pub updated: Option<chrono::NaiveDateTime>,
   pub deleted: bool,
   pub community_id: i32,
+  pub community_name: String,
   pub banned: bool,
   pub banned_from_community: bool,
   pub creator_name: String,
@@ -82,6 +87,7 @@ pub struct UserMentionView {
   pub score: i64,
   pub upvotes: i64,
   pub downvotes: i64,
+  pub hot_rank: i32,
   pub user_id: Option<i32>,
   pub my_vote: Option<i32>,
   pub saved: Option<bool>,
@@ -149,7 +155,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
       .filter(recipient_id.eq(self.for_user_id));
 
     query = match self.sort {
-      // SortType::Hot => query.order_by(hot_rank.desc()),
+      SortType::Hot => query
+        .order_by(hot_rank.desc())
+        .then_order_by(published.desc()),
       SortType::New => query.order_by(published.desc()),
       SortType::TopAll => query.order_by(score.desc()),
       SortType::TopYear => query
@@ -164,7 +172,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
       SortType::TopDay => query
         .filter(published.gt(now - 1.days()))
         .order_by(score.desc()),
-      _ => query.order_by(published.desc()),
+      // _ => query.order_by(published.desc()),
     };
 
     let (limit, offset) = limit_and_offset(self.page, self.limit);
index 3ea506e7f9c1a0ed252414260b53f79aaf632fc0..2274ecbdffd2950dbdb78486c7d030811ec932b5 100644 (file)
@@ -144,9 +144,8 @@ impl<'a> UserQueryBuilder<'a> {
 
 impl UserView {
   pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
-    use super::user_view::user_view::dsl::*;
-
-    user_view.find(from_user_id).first::<Self>(conn)
+    use super::user_view::user_mview::dsl::*;
+    user_mview.find(from_user_id).first::<Self>(conn)
   }
 
   pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
index b044833efb1b88687363e97e7e3cb480a3175fa1..c1c363c982a76c26cc7599b7cc227afa7ffc3624 100644 (file)
@@ -6,7 +6,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
   cfg
     .route("/", web::get().to(index))
     .route(
-      "/home/type/{type}/sort/{sort}/page/{page}",
+      "/home/data_type/{data_type}/listing_type/{listing_type}/sort/{sort}/page/{page}",
       web::get().to(index),
     )
     .route("/login", web::get().to(index))
@@ -17,7 +17,10 @@ pub fn config(cfg: &mut web::ServiceConfig) {
     .route("/communities", web::get().to(index))
     .route("/post/{id}/comment/{id2}", web::get().to(index))
     .route("/post/{id}", web::get().to(index))
-    .route("/c/{name}/sort/{sort}/page/{page}", web::get().to(index))
+    .route(
+      "/c/{name}/data_type/{data_type}/sort/{sort}/page/{page}",
+      web::get().to(index),
+    )
     .route("/c/{name}", web::get().to(index))
     .route("/community/{id}", web::get().to(index))
     .route(
index 2d9ff73cde8df5e3efe1fea87a511de7ed3a43ef..92eb4e7f3ce1f2eff9a0587ccd5962741b69760e 100644 (file)
@@ -1 +1 @@
-pub const VERSION: &str = "v0.6.13";
+pub const VERSION: &str = "v0.6.17";
index c9a41a1fcf0ea9474419f7e62aa0731ede83cdbe..a1feede257e3fae45831f532424a64452329987b 100644 (file)
@@ -45,4 +45,5 @@ pub enum UserOperation {
   EditPrivateMessage,
   GetPrivateMessages,
   UserJoin,
+  GetComments,
 }
index 7ba79e6c49dff70ef074ca773ba318bcf341ae5f..76a55540f887ea2b5fad8b444e48dcdaeb52d0d8 100644 (file)
@@ -122,6 +122,12 @@ impl ChatServer {
       sessions.remove(&id);
     }
 
+    // Also leave all post rooms
+    // This avoids double messages
+    for sessions in self.post_rooms.values_mut() {
+      sessions.remove(&id);
+    }
+
     // If the room doesn't exist yet
     if self.community_rooms.get_mut(&community_id).is_none() {
       self.community_rooms.insert(community_id, HashSet::new());
@@ -140,6 +146,12 @@ impl ChatServer {
       sessions.remove(&id);
     }
 
+    // Also leave all communities
+    // This avoids double messages
+    for sessions in self.community_rooms.values_mut() {
+      sessions.remove(&id);
+    }
+
     // If the room doesn't exist yet
     if self.post_rooms.get_mut(&post_id).is_none() {
       self.post_rooms.insert(post_id, HashSet::new());
@@ -244,6 +256,10 @@ impl ChatServer {
       self.send_user_room_message(recipient_id, &comment_reply_sent_str, id);
     }
 
+    // Send it to the community too
+    self.send_community_room_message(0, &comment_post_sent_str, id);
+    self.send_community_room_message(comment.comment.community_id, &comment_post_sent_str, id);
+
     Ok(comment_user_sent_str)
   }
 
@@ -265,6 +281,9 @@ impl ChatServer {
     self.send_community_room_message(0, &post_sent_str, id);
     self.send_community_room_message(community_id, &post_sent_str, id);
 
+    // Send it to the post room
+    self.send_post_room_message(post_sent.post.id, &post_sent_str, id);
+
     to_json_string(&user_operation, post)
   }
 
@@ -637,6 +656,15 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
       let res = Oper::new(get_posts).perform(&conn)?;
       to_json_string(&user_operation, &res)
     }
+    UserOperation::GetComments => {
+      let get_comments: GetComments = serde_json::from_str(data)?;
+      if get_comments.community_id.is_none() {
+        // 0 is the "all" community
+        chat.join_community_room(0, msg.id);
+      }
+      let res = Oper::new(get_comments).perform(&conn)?;
+      to_json_string(&user_operation, &res)
+    }
     UserOperation::CreatePost => {
       chat.check_rate_limit_post(msg.id, true)?;
       let create_post: CreatePost = serde_json::from_str(data)?;
index b1ad884a575588b9fd925a12d390478ae2c538dc..df75ec319cfbd0414c5929e66e88aab06d0da045 100644 (file)
@@ -175,3 +175,9 @@ hr {
 .img-expanded {
   max-height: 90vh;
 }
+
+.vote-animate:active {
+  transform: scale(1.2);
+  -webkit-transform: scale(1.2);
+  -ms-transform: scale(1.2);
+}
index 31d91bb4c57a2a1945d4278d4d3b866a15922c5f..9cf349108bd475f712db87eea1980a45f5dd3d8c 100644 (file)
@@ -7,7 +7,7 @@
   "main": "index.js",
   "scripts": {
     "build": "node fuse prod",
-    "lint": "eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
+    "lint": "tsc --noEmit && eslint --report-unused-disable-directives --ext .js,.ts,.tsx src",
     "start": "node fuse dev"
   },
   "keywords": [],
@@ -22,7 +22,7 @@
     "bootswatch": "^4.3.1",
     "classcat": "^1.1.3",
     "dotenv": "^8.2.0",
-    "emoji-short-name": "^0.1.0",
+    "emoji-short-name": "^1.0.0",
     "husky": "^4.2.1",
     "i18next": "^19.0.3",
     "inferno": "^7.0.1",
@@ -35,7 +35,7 @@
     "markdown-it-emoji": "^1.4.0",
     "moment": "^2.24.0",
     "prettier": "^1.18.2",
-    "reconnecting-websocket": "^4.3.0",
+    "reconnecting-websocket": "^4.4.0",
     "rxjs": "^6.4.0",
     "terser": "^4.6.3",
     "toastify-js": "^1.6.2",
index 3296a5c8e8da2eb572802e9e4df5f903606308da..1d0b12cad6b9a5eec1e726a14d6d76c4a80a93d7 100644 (file)
@@ -15,6 +15,8 @@ import {
   TransferCommunityForm,
   TransferSiteForm,
   BanType,
+  CommentSortType,
+  SortType,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
@@ -46,8 +48,10 @@ interface CommentNodeState {
   showConfirmAppointAsAdmin: boolean;
   collapsed: boolean;
   viewSource: boolean;
-  upvoteLoading: boolean;
-  downvoteLoading: boolean;
+  my_vote: number;
+  score: number;
+  upvotes: number;
+  downvotes: number;
 }
 
 interface CommentNodeProps {
@@ -58,7 +62,11 @@ interface CommentNodeProps {
   markable?: boolean;
   moderators: Array<CommunityUser>;
   admins: Array<UserView>;
+  // TODO is this necessary, can't I get it from the node itself?
   postCreatorId?: number;
+  showCommunity?: boolean;
+  sort?: CommentSortType;
+  sortType?: SortType;
 }
 
 export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@@ -77,8 +85,10 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
     showConfirmTransferCommunity: false,
     showConfirmAppointAsMod: false,
     showConfirmAppointAsAdmin: false,
-    upvoteLoading: this.props.node.comment.upvoteLoading,
-    downvoteLoading: this.props.node.comment.downvoteLoading,
+    my_vote: this.props.node.comment.my_vote,
+    score: this.props.node.comment.score,
+    upvotes: this.props.node.comment.upvotes,
+    downvotes: this.props.node.comment.downvotes,
   };
 
   constructor(props: any, context: any) {
@@ -91,15 +101,11 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   componentWillReceiveProps(nextProps: CommentNodeProps) {
-    if (
-      nextProps.node.comment.upvoteLoading !== this.state.upvoteLoading ||
-      nextProps.node.comment.downvoteLoading !== this.state.downvoteLoading
-    ) {
-      this.setState({
-        upvoteLoading: false,
-        downvoteLoading: false,
-      });
-    }
+    this.state.my_vote = nextProps.node.comment.my_vote;
+    this.state.upvotes = nextProps.node.comment.upvotes;
+    this.state.downvotes = nextProps.node.comment.downvotes;
+    this.state.score = nextProps.node.comment.score;
+    this.setState(this.state);
   }
 
   render() {
@@ -116,40 +122,26 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
               .viewOnly && 'no-click'}`}
           >
             <button
-              className={`btn btn-link p-0 ${
-                node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
+              className={`vote-animate btn btn-link p-0 ${
+                this.state.my_vote == 1 ? 'text-info' : 'text-muted'
               }`}
               onClick={linkEvent(node, this.handleCommentUpvote)}
             >
-              {this.state.upvoteLoading ? (
-                <svg class="icon icon-spinner spin upvote">
-                  <use xlinkHref="#icon-spinner"></use>
-                </svg>
-              ) : (
-                <svg class="icon upvote">
-                  <use xlinkHref="#icon-arrow-up"></use>
-                </svg>
-              )}
+              <svg class="icon upvote">
+                <use xlinkHref="#icon-arrow-up"></use>
+              </svg>
             </button>
-            <div class={`font-weight-bold text-muted`}>
-              {node.comment.score}
-            </div>
+            <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
             {WebSocketService.Instance.site.enable_downvotes && (
               <button
-                className={`btn btn-link p-0 ${
-                  node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
+                className={`vote-animate btn btn-link p-0 ${
+                  this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
                 }`}
                 onClick={linkEvent(node, this.handleCommentDownvote)}
               >
-                {this.state.downvoteLoading ? (
-                  <svg class="icon icon-spinner spin downvote">
-                    <use xlinkHref="#icon-spinner"></use>
-                  </svg>
-                ) : (
-                  <svg class="icon downvote">
-                    <use xlinkHref="#icon-arrow-down"></use>
-                  </svg>
-                )}
+                <svg class="icon downvote">
+                  <use xlinkHref="#icon-arrow-down"></use>
+                </svg>
               </button>
             )}
           </div>
@@ -199,12 +191,20 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             )}
             <li className="list-inline-item">
               <span>
-                (<span className="text-info">+{node.comment.upvotes}</span>
+                (<span className="text-info">+{this.state.upvotes}</span>
                 <span> | </span>
-                <span className="text-danger">-{node.comment.downvotes}</span>
+                <span className="text-danger">-{this.state.downvotes}</span>
                 <span>) </span>
               </span>
             </li>
+            {this.props.showCommunity && (
+              <li className="list-inline-item">
+                <span> {i18n.t('to')} </span>
+                <Link to={`/c/${node.comment.community_name}`}>
+                  {node.comment.community_name}
+                </Link>
+              </li>
+            )}
             <li className="list-inline-item">
               <span>
                 <MomentTime data={node.comment} />
@@ -620,6 +620,8 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
             moderators={this.props.moderators}
             admins={this.props.admins}
             postCreatorId={this.props.postCreatorId}
+            sort={this.props.sort}
+            sortType={this.props.sortType}
           />
         )}
         {/* A collapsed clearfix */}
@@ -756,31 +758,57 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
   }
 
   handleCommentUpvote(i: CommentNodeI) {
-    if (UserService.Instance.user) {
-      this.setState({
-        upvoteLoading: true,
-      });
+    let new_vote = this.state.my_vote == 1 ? 0 : 1;
+
+    if (this.state.my_vote == 1) {
+      this.state.score--;
+      this.state.upvotes--;
+    } else if (this.state.my_vote == -1) {
+      this.state.downvotes--;
+      this.state.upvotes++;
+      this.state.score += 2;
+    } else {
+      this.state.upvotes++;
+      this.state.score++;
     }
+
+    this.state.my_vote = new_vote;
+
     let form: CommentLikeForm = {
       comment_id: i.comment.id,
       post_id: i.comment.post_id,
-      score: i.comment.my_vote == 1 ? 0 : 1,
+      score: this.state.my_vote,
     };
+
     WebSocketService.Instance.likeComment(form);
+    this.setState(this.state);
   }
 
   handleCommentDownvote(i: CommentNodeI) {
-    if (UserService.Instance.user) {
-      this.setState({
-        downvoteLoading: true,
-      });
+    let new_vote = this.state.my_vote == -1 ? 0 : -1;
+
+    if (this.state.my_vote == 1) {
+      this.state.score -= 2;
+      this.state.upvotes--;
+      this.state.downvotes++;
+    } else if (this.state.my_vote == -1) {
+      this.state.downvotes--;
+      this.state.score++;
+    } else {
+      this.state.downvotes++;
+      this.state.score--;
     }
+
+    this.state.my_vote = new_vote;
+
     let form: CommentLikeForm = {
       comment_id: i.comment.id,
       post_id: i.comment.post_id,
-      score: i.comment.my_vote == -1 ? 0 : -1,
+      score: this.state.my_vote,
     };
+
     WebSocketService.Instance.likeComment(form);
+    this.setState(this.state);
   }
 
   handleModRemoveShow(i: CommentNode) {
index 18faf1ac4c87ff14bad3ce1d94ca709013d56bd8..b15da5208954739d1e82c131c8f19b596024916d 100644 (file)
@@ -3,7 +3,10 @@ import {
   CommentNode as CommentNodeI,
   CommunityUser,
   UserView,
+  CommentSortType,
+  SortType,
 } from '../interfaces';
+import { commentSort, commentSortSortType } from '../utils';
 import { CommentNode } from './comment-node';
 
 interface CommentNodesState {}
@@ -17,6 +20,9 @@ interface CommentNodesProps {
   viewOnly?: boolean;
   locked?: boolean;
   markable?: boolean;
+  showCommunity?: boolean;
+  sort?: CommentSortType;
+  sortType?: SortType;
 }
 
 export class CommentNodes extends Component<
@@ -30,7 +36,7 @@ export class CommentNodes extends Component<
   render() {
     return (
       <div className="comments">
-        {this.props.nodes.map(node => (
+        {this.sorter().map(node => (
           <CommentNode
             node={node}
             noIndent={this.props.noIndent}
@@ -40,9 +46,22 @@ export class CommentNodes extends Component<
             admins={this.props.admins}
             postCreatorId={this.props.postCreatorId}
             markable={this.props.markable}
+            showCommunity={this.props.showCommunity}
+            sort={this.props.sort}
+            sortType={this.props.sortType}
           />
         ))}
       </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;
+  }
 }
index 069f9158daad250494a01ae3a8409f4c7ec60f46..e28c99bc7d7f2a82b0185d0d78155934736ec2aa 100644 (file)
@@ -13,17 +13,37 @@ import {
   GetPostsForm,
   GetCommunityForm,
   ListingType,
+  DataType,
   GetPostsResponse,
   PostResponse,
   AddModToCommunityResponse,
   BanFromCommunityResponse,
+  Comment,
+  GetCommentsForm,
+  GetCommentsResponse,
+  CommentResponse,
   WebSocketJsonResponse,
 } from '../interfaces';
-import { WebSocketService, UserService } from '../services';
+import { WebSocketService } from '../services';
 import { PostListings } from './post-listings';
+import { CommentNodes } from './comment-nodes';
 import { SortSelect } from './sort-select';
+import { DataTypeSelect } from './data-type-select';
 import { Sidebar } from './sidebar';
-import { wsJsonToRes, routeSortTypeToEnum, fetchLimit, toast } from '../utils';
+import {
+  wsJsonToRes,
+  fetchLimit,
+  toast,
+  getPageFromProps,
+  getSortTypeFromProps,
+  getDataTypeFromProps,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  editPostFindRes,
+  commentsToFlatNodes,
+} from '../utils';
 import { i18n } from '../i18next';
 
 interface State {
@@ -35,6 +55,8 @@ interface State {
   online: number;
   loading: boolean;
   posts: Array<Post>;
+  comments: Array<Comment>;
+  dataType: DataType;
   sort: SortType;
   page: number;
 }
@@ -65,27 +87,18 @@ export class Community extends Component<any, State> {
     online: null,
     loading: true,
     posts: [],
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    comments: [],
+    dataType: getDataTypeFromProps(this.props),
+    sort: getSortTypeFromProps(this.props),
+    page: getPageFromProps(this.props),
   };
 
-  getSortTypeFromProps(props: any): SortType {
-    return props.match.params.sort
-      ? routeSortTypeToEnum(props.match.params.sort)
-      : UserService.Instance.user
-      ? UserService.Instance.user.default_sort_type
-      : SortType.Hot;
-  }
-
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
     this.handleSortChange = this.handleSortChange.bind(this);
+    this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
 
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@@ -112,10 +125,11 @@ export class Community extends Component<any, State> {
       nextProps.history.action == 'POP' ||
       nextProps.history.action == 'PUSH'
     ) {
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
+      this.state.dataType = getDataTypeFromProps(nextProps);
+      this.state.sort = getSortTypeFromProps(nextProps);
+      this.state.page = getPageFromProps(nextProps);
       this.setState(this.state);
-      this.fetchPosts();
+      this.fetchData();
     }
   }
 
@@ -145,7 +159,7 @@ export class Community extends Component<any, State> {
                 )}
               </h5>
               {this.selects()}
-              <PostListings posts={this.state.posts} removeDuplicates />
+              {this.listings()}
               {this.paginator()}
             </div>
             <div class="col-12 col-md-4">
@@ -162,10 +176,33 @@ export class Community extends Component<any, State> {
     );
   }
 
+  listings() {
+    return this.state.dataType == DataType.Post ? (
+      <PostListings
+        posts={this.state.posts}
+        removeDuplicates
+        sort={this.state.sort}
+      />
+    ) : (
+      <CommentNodes
+        nodes={commentsToFlatNodes(this.state.comments)}
+        noIndent
+        sortType={this.state.sort}
+      />
+    );
+  }
+
   selects() {
     return (
       <div class="mb-2">
-        <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+        <DataTypeSelect
+          type_={this.state.dataType}
+          onChange={this.handleDataTypeChange}
+        />
+
+        <span class="mx-2">
+          <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+        </span>
         <a
           href={`/feeds/c/${this.state.communityName}.xml?sort=${
             SortType[this.state.sort]
@@ -207,7 +244,7 @@ export class Community extends Component<any, State> {
     i.state.page++;
     i.setState(i.state);
     i.updateUrl();
-    i.fetchPosts();
+    i.fetchData();
     window.scrollTo(0, 0);
   }
 
@@ -215,7 +252,7 @@ export class Community extends Component<any, State> {
     i.state.page--;
     i.setState(i.state);
     i.updateUrl();
-    i.fetchPosts();
+    i.fetchData();
     window.scrollTo(0, 0);
   }
 
@@ -225,26 +262,48 @@ export class Community extends Component<any, State> {
     this.state.loading = true;
     this.setState(this.state);
     this.updateUrl();
-    this.fetchPosts();
+    this.fetchData();
+    window.scrollTo(0, 0);
+  }
+
+  handleDataTypeChange(val: DataType) {
+    this.state.dataType = val;
+    this.state.page = 1;
+    this.state.loading = true;
+    this.setState(this.state);
+    this.updateUrl();
+    this.fetchData();
     window.scrollTo(0, 0);
   }
 
   updateUrl() {
+    let dataTypeStr = DataType[this.state.dataType].toLowerCase();
     let sortStr = SortType[this.state.sort].toLowerCase();
     this.props.history.push(
-      `/c/${this.state.community.name}/sort/${sortStr}/page/${this.state.page}`
+      `/c/${this.state.community.name}/data_type/${dataTypeStr}/sort/${sortStr}/page/${this.state.page}`
     );
   }
 
-  fetchPosts() {
-    let getPostsForm: GetPostsForm = {
-      page: this.state.page,
-      limit: fetchLimit,
-      sort: SortType[this.state.sort],
-      type_: ListingType[ListingType.Community],
-      community_id: this.state.community.id,
-    };
-    WebSocketService.Instance.getPosts(getPostsForm);
+  fetchData() {
+    if (this.state.dataType == DataType.Post) {
+      let getPostsForm: GetPostsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: SortType[this.state.sort],
+        type_: ListingType[ListingType.Community],
+        community_id: this.state.community.id,
+      };
+      WebSocketService.Instance.getPosts(getPostsForm);
+    } else {
+      let getCommentsForm: GetCommentsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: SortType[this.state.sort],
+        type_: ListingType[ListingType.Community],
+        community_id: this.state.community.id,
+      };
+      WebSocketService.Instance.getComments(getCommentsForm);
+    }
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -255,7 +314,7 @@ export class Community extends Component<any, State> {
       this.context.router.history.push('/');
       return;
     } else if (msg.reconnect) {
-      this.fetchPosts();
+      this.fetchData();
     } else if (res.op == UserOperation.GetCommunity) {
       let data = res.data as GetCommunityResponse;
       this.state.community = data.community;
@@ -264,7 +323,7 @@ export class Community extends Component<any, State> {
       this.state.online = data.online;
       document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
       this.setState(this.state);
-      this.fetchPosts();
+      this.fetchData();
     } else if (res.op == UserOperation.EditCommunity) {
       let data = res.data as CommunityResponse;
       this.state.community = data.community;
@@ -282,30 +341,15 @@ export class Community extends Component<any, State> {
       this.setState(this.state);
     } else if (res.op == UserOperation.EditPost) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-      if (found) {
-        found.url = data.post.url;
-        found.name = data.post.name;
-        found.nsfw = data.post.nsfw;
-        this.setState(this.state);
-      }
+      editPostFindRes(data, this.state.posts);
+      this.setState(this.state);
     } else if (res.op == UserOperation.CreatePost) {
       let data = res.data as PostResponse;
       this.state.posts.unshift(data.post);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-      if (found) {
-        found.score = data.post.score;
-        found.upvotes = data.post.upvotes;
-        found.downvotes = data.post.downvotes;
-        if (data.post.my_vote !== null) {
-          found.my_vote = data.post.my_vote;
-          found.upvoteLoading = false;
-          found.downvoteLoading = false;
-        }
-      }
+      createPostLikeFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.AddModToCommunity) {
       let data = res.data as AddModToCommunityResponse;
@@ -319,6 +363,31 @@ export class Community extends Component<any, State> {
         .forEach(p => (p.banned = data.banned));
 
       this.setState(this.state);
+    } else if (res.op == UserOperation.GetComments) {
+      let data = res.data as GetCommentsResponse;
+      this.state.comments = data.comments;
+      this.state.loading = false;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
+      editCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateComment) {
+      let data = res.data as CommentResponse;
+
+      // Necessary since it might be a user reply
+      if (data.recipient_ids.length == 0) {
+        this.state.comments.unshift(data.comment);
+        this.setState(this.state);
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.comments);
+      this.setState(this.state);
     }
   }
 }
diff --git a/ui/src/components/data-type-select.tsx b/ui/src/components/data-type-select.tsx
new file mode 100644 (file)
index 0000000..f2539c8
--- /dev/null
@@ -0,0 +1,65 @@
+import { Component, linkEvent } from 'inferno';
+import { DataType } from '../interfaces';
+
+import { i18n } from '../i18next';
+
+interface DataTypeSelectProps {
+  type_: DataType;
+  onChange?(val: DataType): any;
+}
+
+interface DataTypeSelectState {
+  type_: DataType;
+}
+
+export class DataTypeSelect extends Component<
+  DataTypeSelectProps,
+  DataTypeSelectState
+> {
+  private emptyState: DataTypeSelectState = {
+    type_: this.props.type_,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = this.emptyState;
+  }
+
+  render() {
+    return (
+      <div class="btn-group btn-group-toggle">
+        <label
+          className={`pointer btn btn-sm btn-secondary 
+            ${this.state.type_ == DataType.Post && 'active'}
+          `}
+        >
+          <input
+            type="radio"
+            value={DataType.Post}
+            checked={this.state.type_ == DataType.Post}
+            onChange={linkEvent(this, this.handleTypeChange)}
+          />
+          {i18n.t('posts')}
+        </label>
+        <label
+          className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
+            DataType.Comment && 'active'}`}
+        >
+          <input
+            type="radio"
+            value={DataType.Comment}
+            checked={this.state.type_ == DataType.Comment}
+            onChange={linkEvent(this, this.handleTypeChange)}
+          />
+          {i18n.t('comments')}
+        </label>
+      </div>
+    );
+  }
+
+  handleTypeChange(i: DataTypeSelect, event: any) {
+    i.state.type_ = Number(event.target.value);
+    i.setState(i.state);
+    i.props.onChange(i.state.type_);
+  }
+}
index 6849b37d25edcb4875fffc96689fa98cabd3041e..027a1db0498f95a97785d36f1caefd881440ef83 100644 (file)
@@ -19,7 +19,16 @@ import {
   PrivateMessageResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils';
+import {
+  wsJsonToRes,
+  fetchLimit,
+  isCommentType,
+  toast,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  commentsToFlatNodes,
+} from '../utils';
 import { CommentNodes } from './comment-nodes';
 import { PrivateMessage } from './private-message';
 import { SortSelect } from './sort-select';
@@ -197,9 +206,11 @@ export class Inbox extends Component<any, InboxState> {
   replies() {
     return (
       <div>
-        {this.state.replies.map(reply => (
-          <CommentNodes nodes={[{ comment: reply }]} noIndent markable />
-        ))}
+        <CommentNodes
+          nodes={commentsToFlatNodes(this.state.replies)}
+          noIndent
+          markable
+        />
       </div>
     );
   }
@@ -362,15 +373,7 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.EditComment) {
       let data = res.data as CommentResponse;
-
-      let found = this.state.replies.find(c => c.id == data.comment.id);
-      found.content = data.comment.content;
-      found.updated = data.comment.updated;
-      found.removed = data.comment.removed;
-      found.deleted = data.comment.deleted;
-      found.upvotes = data.comment.upvotes;
-      found.downvotes = data.comment.downvotes;
-      found.score = data.comment.score;
+      editCommentRes(data, this.state.replies);
 
       // If youre in the unread view, just remove it from the list
       if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
@@ -418,28 +421,17 @@ export class Inbox extends Component<any, InboxState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePrivateMessage) {
       let data = res.data as PrivateMessageResponse;
-
       if (data.message.recipient_id == UserService.Instance.user.id) {
         this.state.messages.unshift(data.message);
         this.setState(this.state);
-      } else if (data.message.creator_id == UserService.Instance.user.id) {
-        toast(i18n.t('message_sent'));
       }
-      this.setState(this.state);
     } else if (res.op == UserOperation.SaveComment) {
       let data = res.data as CommentResponse;
-      let found = this.state.replies.find(c => c.id == data.comment.id);
-      found.saved = data.comment.saved;
+      saveCommentRes(data, this.state.replies);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      let found: Comment = this.state.replies.find(
-        c => c.id === data.comment.id
-      );
-      found.score = data.comment.score;
-      found.upvotes = data.comment.upvotes;
-      found.downvotes = data.comment.downvotes;
-      if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
+      createCommentLikeRes(data, this.state.replies);
       this.setState(this.state);
     }
   }
index 161f5df45261e54accafce1761212987468b9ea6..c8e132f7a4d0c5f503dfa41a213348f5a9091aec 100644 (file)
@@ -12,30 +12,46 @@ import {
   SortType,
   GetSiteResponse,
   ListingType,
+  DataType,
   SiteResponse,
   GetPostsResponse,
   PostResponse,
   Post,
   GetPostsForm,
+  Comment,
+  GetCommentsForm,
+  GetCommentsResponse,
+  CommentResponse,
   AddAdminResponse,
   BanUserResponse,
   WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
+import { CommentNodes } from './comment-nodes';
 import { SortSelect } from './sort-select';
 import { ListingTypeSelect } from './listing-type-select';
+import { DataTypeSelect } from './data-type-select';
 import { SiteForm } from './site-form';
 import {
   wsJsonToRes,
   repoUrl,
   mdToHtml,
   fetchLimit,
-  routeSortTypeToEnum,
-  routeListingTypeToEnum,
   pictshareAvatarThumbnail,
   showAvatars,
   toast,
+  getListingTypeFromProps,
+  getPageFromProps,
+  getSortTypeFromProps,
+  getDataTypeFromProps,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  editPostFindRes,
+  commentsToFlatNodes,
+  commentSortSortType,
 } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -47,7 +63,9 @@ interface MainState {
   showEditSite: boolean;
   loading: boolean;
   posts: Array<Post>;
-  type_: ListingType;
+  comments: Array<Comment>;
+  listingType: ListingType;
+  dataType: DataType;
   sort: SortType;
   page: number;
 }
@@ -79,38 +97,21 @@ export class Main extends Component<any, MainState> {
     showEditSite: false,
     loading: true,
     posts: [],
-    type_: this.getListingTypeFromProps(this.props),
-    sort: this.getSortTypeFromProps(this.props),
-    page: this.getPageFromProps(this.props),
+    comments: [],
+    listingType: getListingTypeFromProps(this.props),
+    dataType: getDataTypeFromProps(this.props),
+    sort: getSortTypeFromProps(this.props),
+    page: getPageFromProps(this.props),
   };
 
-  getListingTypeFromProps(props: any): ListingType {
-    return props.match.params.type
-      ? routeListingTypeToEnum(props.match.params.type)
-      : UserService.Instance.user
-      ? UserService.Instance.user.default_listing_type
-      : ListingType.All;
-  }
-
-  getSortTypeFromProps(props: any): SortType {
-    return props.match.params.sort
-      ? routeSortTypeToEnum(props.match.params.sort)
-      : UserService.Instance.user
-      ? UserService.Instance.user.default_sort_type
-      : SortType.Hot;
-  }
-
-  getPageFromProps(props: any): number {
-    return props.match.params.page ? Number(props.match.params.page) : 1;
-  }
-
   constructor(props: any, context: any) {
     super(props, context);
 
     this.state = this.emptyState;
     this.handleEditCancel = this.handleEditCancel.bind(this);
     this.handleSortChange = this.handleSortChange.bind(this);
-    this.handleTypeChange = this.handleTypeChange.bind(this);
+    this.handleListingTypeChange = this.handleListingTypeChange.bind(this);
+    this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
 
     this.subscription = WebSocketService.Instance.subject
       .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
@@ -133,7 +134,7 @@ export class Main extends Component<any, MainState> {
 
     WebSocketService.Instance.listCommunities(listCommunitiesForm);
 
-    this.fetchPosts();
+    this.fetchData();
   }
 
   componentWillUnmount() {
@@ -146,11 +147,12 @@ export class Main extends Component<any, MainState> {
       nextProps.history.action == 'POP' ||
       nextProps.history.action == 'PUSH'
     ) {
-      this.state.type_ = this.getListingTypeFromProps(nextProps);
-      this.state.sort = this.getSortTypeFromProps(nextProps);
-      this.state.page = this.getPageFromProps(nextProps);
+      this.state.listingType = getListingTypeFromProps(nextProps);
+      this.state.dataType = getDataTypeFromProps(nextProps);
+      this.state.sort = getSortTypeFromProps(nextProps);
+      this.state.page = getPageFromProps(nextProps);
       this.setState(this.state);
-      this.fetchPosts();
+      this.fetchData();
     }
   }
 
@@ -251,10 +253,11 @@ export class Main extends Component<any, MainState> {
   }
 
   updateUrl() {
-    let typeStr = ListingType[this.state.type_].toLowerCase();
+    let listingTypeStr = ListingType[this.state.listingType].toLowerCase();
+    let dataTypeStr = DataType[this.state.dataType].toLowerCase();
     let sortStr = SortType[this.state.sort].toLowerCase();
     this.props.history.push(
-      `/home/type/${typeStr}/sort/${sortStr}/page/${this.state.page}`
+      `/home/data_type/${dataTypeStr}/listing_type/${listingTypeStr}/sort/${sortStr}/page/${this.state.page}`
     );
   }
 
@@ -392,11 +395,7 @@ export class Main extends Component<any, MainState> {
         ) : (
           <div>
             {this.selects()}
-            <PostListings
-              posts={this.state.posts}
-              showCommunity
-              removeDuplicates
-            />
+            {this.listings()}
             {this.paginator()}
           </div>
         )}
@@ -404,17 +403,41 @@ export class Main extends Component<any, MainState> {
     );
   }
 
+  listings() {
+    return this.state.dataType == DataType.Post ? (
+      <PostListings
+        posts={this.state.posts}
+        showCommunity
+        removeDuplicates
+        sort={this.state.sort}
+      />
+    ) : (
+      <CommentNodes
+        nodes={commentsToFlatNodes(this.state.comments)}
+        noIndent
+        showCommunity
+        sortType={this.state.sort}
+      />
+    );
+  }
+
   selects() {
     return (
       <div className="mb-3">
-        <ListingTypeSelect
-          type_={this.state.type_}
-          onChange={this.handleTypeChange}
+        <DataTypeSelect
+          type_={this.state.dataType}
+          onChange={this.handleDataTypeChange}
         />
         <span class="mx-2">
+          <ListingTypeSelect
+            type_={this.state.listingType}
+            onChange={this.handleListingTypeChange}
+          />
+        </span>
+        <span class="mr-2">
           <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
         </span>
-        {this.state.type_ == ListingType.All && (
+        {this.state.listingType == ListingType.All && (
           <a
             href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
             target="_blank"
@@ -425,7 +448,7 @@ export class Main extends Component<any, MainState> {
           </a>
         )}
         {UserService.Instance.user &&
-          this.state.type_ == ListingType.Subscribed && (
+          this.state.listingType == ListingType.Subscribed && (
             <a
               href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
                 SortType[this.state.sort]
@@ -488,7 +511,7 @@ export class Main extends Component<any, MainState> {
     i.state.loading = true;
     i.setState(i.state);
     i.updateUrl();
-    i.fetchPosts();
+    i.fetchData();
     window.scrollTo(0, 0);
   }
 
@@ -497,7 +520,7 @@ export class Main extends Component<any, MainState> {
     i.state.loading = true;
     i.setState(i.state);
     i.updateUrl();
-    i.fetchPosts();
+    i.fetchData();
     window.scrollTo(0, 0);
   }
 
@@ -507,28 +530,48 @@ export class Main extends Component<any, MainState> {
     this.state.loading = true;
     this.setState(this.state);
     this.updateUrl();
-    this.fetchPosts();
+    this.fetchData();
     window.scrollTo(0, 0);
   }
 
-  handleTypeChange(val: ListingType) {
-    this.state.type_ = val;
+  handleListingTypeChange(val: ListingType) {
+    this.state.listingType = val;
     this.state.page = 1;
     this.state.loading = true;
     this.setState(this.state);
     this.updateUrl();
-    this.fetchPosts();
+    this.fetchData();
     window.scrollTo(0, 0);
   }
 
-  fetchPosts() {
-    let getPostsForm: GetPostsForm = {
-      page: this.state.page,
-      limit: fetchLimit,
-      sort: SortType[this.state.sort],
-      type_: ListingType[this.state.type_],
-    };
-    WebSocketService.Instance.getPosts(getPostsForm);
+  handleDataTypeChange(val: DataType) {
+    this.state.dataType = val;
+    this.state.page = 1;
+    this.state.loading = true;
+    this.setState(this.state);
+    this.updateUrl();
+    this.fetchData();
+    window.scrollTo(0, 0);
+  }
+
+  fetchData() {
+    if (this.state.dataType == DataType.Post) {
+      let getPostsForm: GetPostsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: SortType[this.state.sort],
+        type_: ListingType[this.state.listingType],
+      };
+      WebSocketService.Instance.getPosts(getPostsForm);
+    } else {
+      let getCommentsForm: GetCommentsForm = {
+        page: this.state.page,
+        limit: fetchLimit,
+        sort: SortType[this.state.sort],
+        type_: ListingType[this.state.listingType],
+      };
+      WebSocketService.Instance.getComments(getCommentsForm);
+    }
   }
 
   parseMessage(msg: WebSocketJsonResponse) {
@@ -538,7 +581,7 @@ export class Main extends Component<any, MainState> {
       toast(i18n.t(msg.error), 'danger');
       return;
     } else if (msg.reconnect) {
-      this.fetchPosts();
+      this.fetchData();
     } else if (res.op == UserOperation.GetFollowedCommunities) {
       let data = res.data as GetFollowedCommunitiesResponse;
       this.state.subscribedCommunities = data.communities;
@@ -574,7 +617,7 @@ export class Main extends Component<any, MainState> {
       let data = res.data as PostResponse;
 
       // If you're on subscribed, only push it if you're subscribed.
-      if (this.state.type_ == ListingType.Subscribed) {
+      if (this.state.listingType == ListingType.Subscribed) {
         if (
           this.state.subscribedCommunities
             .map(c => c.community_id)
@@ -589,28 +632,12 @@ export class Main extends Component<any, MainState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.EditPost) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-      if (found) {
-        found.url = data.post.url;
-        found.name = data.post.name;
-        found.nsfw = data.post.nsfw;
-
-        this.setState(this.state);
-      }
+      editPostFindRes(data, this.state.posts);
+      this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-      if (found) {
-        found.score = data.post.score;
-        found.upvotes = data.post.upvotes;
-        found.downvotes = data.post.downvotes;
-        if (data.post.my_vote !== null) {
-          found.my_vote = data.post.my_vote;
-          found.upvoteLoading = false;
-          found.downvoteLoading = false;
-        }
-        this.setState(this.state);
-      }
+      createPostLikeFindRes(data, this.state.posts);
+      this.setState(this.state);
     } else if (res.op == UserOperation.AddAdmin) {
       let data = res.data as AddAdminResponse;
       this.state.siteRes.admins = data.admins;
@@ -633,6 +660,42 @@ export class Main extends Component<any, MainState> {
         .forEach(p => (p.banned = data.banned));
 
       this.setState(this.state);
+    } else if (res.op == UserOperation.GetComments) {
+      let data = res.data as GetCommentsResponse;
+      this.state.comments = data.comments;
+      this.state.loading = false;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
+      editCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateComment) {
+      let data = res.data as CommentResponse;
+
+      // Necessary since it might be a user reply
+      if (data.recipient_ids.length == 0) {
+        // If you're on subscribed, only push it if you're subscribed.
+        if (this.state.listingType == ListingType.Subscribed) {
+          if (
+            this.state.subscribedCommunities
+              .map(c => c.community_id)
+              .includes(data.comment.community_id)
+          ) {
+            this.state.comments.unshift(data.comment);
+          }
+        } else {
+          this.state.comments.unshift(data.comment);
+        }
+        this.setState(this.state);
+      }
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      saveCommentRes(data, this.state.comments);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
+      createCommentLikeRes(data, this.state.comments);
+      this.setState(this.state);
     }
   }
 }
index f11d9e1441439f3c2bcb11e7e796ff7e1ca4e5cc..1e7289013594fe8694e06e8989baef2bbb277d10 100644 (file)
@@ -43,8 +43,10 @@ interface PostListingState {
   showConfirmTransferCommunity: boolean;
   imageExpanded: boolean;
   viewSource: boolean;
-  upvoteLoading: boolean;
-  downvoteLoading: boolean;
+  my_vote: number;
+  score: number;
+  upvotes: number;
+  downvotes: number;
 }
 
 interface PostListingProps {
@@ -68,8 +70,10 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
     showConfirmTransferCommunity: false,
     imageExpanded: false,
     viewSource: false,
-    upvoteLoading: this.props.post.upvoteLoading,
-    downvoteLoading: this.props.post.downvoteLoading,
+    my_vote: this.props.post.my_vote,
+    score: this.props.post.score,
+    upvotes: this.props.post.upvotes,
+    downvotes: this.props.post.downvotes,
   };
 
   constructor(props: any, context: any) {
@@ -83,15 +87,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   componentWillReceiveProps(nextProps: PostListingProps) {
-    if (
-      nextProps.post.upvoteLoading !== this.state.upvoteLoading ||
-      nextProps.post.downvoteLoading !== this.state.downvoteLoading
-    ) {
-      this.setState({
-        upvoteLoading: false,
-        downvoteLoading: false,
-      });
-    }
+    this.state.my_vote = nextProps.post.my_vote;
+    this.state.upvotes = nextProps.post.upvotes;
+    this.state.downvotes = nextProps.post.downvotes;
+    this.state.score = nextProps.post.score;
+    this.setState(this.state);
   }
 
   render() {
@@ -118,38 +118,26 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
       <div class="listing col-12">
         <div className={`vote-bar mr-2 float-left small text-center`}>
           <button
-            className={`btn btn-link p-0 ${
-              post.my_vote == 1 ? 'text-info' : 'text-muted'
+            className={`vote-animate btn btn-link p-0 ${
+              this.state.my_vote == 1 ? 'text-info' : 'text-muted'
             }`}
             onClick={linkEvent(this, this.handlePostLike)}
           >
-            {this.state.upvoteLoading ? (
-              <svg class="icon icon-spinner spin upvote">
-                <use xlinkHref="#icon-spinner"></use>
-              </svg>
-            ) : (
-              <svg class="icon upvote">
-                <use xlinkHref="#icon-arrow-up"></use>
-              </svg>
-            )}
+            <svg class="icon upvote">
+              <use xlinkHref="#icon-arrow-up"></use>
+            </svg>
           </button>
-          <div class={`font-weight-bold text-muted`}>{post.score}</div>
+          <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
           {WebSocketService.Instance.site.enable_downvotes && (
             <button
-              className={`btn btn-link p-0 ${
-                post.my_vote == -1 ? 'text-danger' : 'text-muted'
+              className={`vote-animate btn btn-link p-0 ${
+                this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
               }`}
               onClick={linkEvent(this, this.handlePostDisLike)}
             >
-              {this.state.downvoteLoading ? (
-                <svg class="icon icon-spinner spin downvote">
-                  <use xlinkHref="#icon-spinner"></use>
-                </svg>
-              ) : (
-                <svg class="icon downvote">
-                  <use xlinkHref="#icon-arrow-down"></use>
-                </svg>
-              )}
+              <svg class="icon downvote">
+                <use xlinkHref="#icon-arrow-down"></use>
+              </svg>
             </button>
           )}
         </div>
@@ -315,9 +303,9 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
             </li>
             <li className="list-inline-item">
               <span>
-                (<span className="text-info">+{post.upvotes}</span>
+                (<span className="text-info">+{this.state.upvotes}</span>
                 <span> | </span>
-                <span className="text-danger">-{post.downvotes}</span>
+                <span className="text-danger">-{this.state.downvotes}</span>
                 <span>) </span>
               </span>
             </li>
@@ -747,28 +735,55 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
   }
 
   handlePostLike(i: PostListing) {
-    if (UserService.Instance.user) {
-      i.setState({ upvoteLoading: true });
+    let new_vote = i.state.my_vote == 1 ? 0 : 1;
+
+    if (i.state.my_vote == 1) {
+      i.state.score--;
+      i.state.upvotes--;
+    } else if (i.state.my_vote == -1) {
+      i.state.downvotes--;
+      i.state.upvotes++;
+      i.state.score += 2;
+    } else {
+      i.state.upvotes++;
+      i.state.score++;
     }
 
+    i.state.my_vote = new_vote;
+
     let form: CreatePostLikeForm = {
       post_id: i.props.post.id,
-      score: i.props.post.my_vote == 1 ? 0 : 1,
+      score: i.state.my_vote,
     };
 
     WebSocketService.Instance.likePost(form);
+    i.setState(i.state);
   }
 
   handlePostDisLike(i: PostListing) {
-    if (UserService.Instance.user) {
-      i.setState({ downvoteLoading: true });
+    let new_vote = i.state.my_vote == -1 ? 0 : -1;
+
+    if (i.state.my_vote == 1) {
+      i.state.score -= 2;
+      i.state.upvotes--;
+      i.state.downvotes++;
+    } else if (i.state.my_vote == -1) {
+      i.state.downvotes--;
+      i.state.score++;
+    } else {
+      i.state.downvotes++;
+      i.state.score--;
     }
 
+    i.state.my_vote = new_vote;
+
     let form: CreatePostLikeForm = {
       post_id: i.props.post.id,
-      score: i.props.post.my_vote == -1 ? 0 : -1,
+      score: i.state.my_vote,
     };
+
     WebSocketService.Instance.likePost(form);
+    i.setState(i.state);
   }
 
   handleEditClick(i: PostListing) {
index 005c4fe03ce669ca7c32394efcdd7b768a8c741e..d61f624d425beeb29b8b6e7a391b9bbf05d648dd 100644 (file)
@@ -1,6 +1,7 @@
 import { Component } from 'inferno';
 import { Link } from 'inferno-router';
-import { Post } from '../interfaces';
+import { Post, SortType } from '../interfaces';
+import { postSort } from '../utils';
 import { PostListing } from './post-listing';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -9,6 +10,7 @@ interface PostListingsProps {
   posts: Array<Post>;
   showCommunity?: boolean;
   removeDuplicates?: boolean;
+  sort?: SortType;
 }
 
 export class PostListings extends Component<PostListingsProps, any> {
@@ -20,10 +22,7 @@ export class PostListings extends Component<PostListingsProps, any> {
     return (
       <div>
         {this.props.posts.length > 0 ? (
-          (this.props.removeDuplicates
-            ? this.removeDuplicates(this.props.posts)
-            : this.props.posts
-          ).map(post => (
+          this.outer().map(post => (
             <>
               <PostListing
                 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> {
     // A map from post url to list of posts (dupes)
     let urlMap = new Map<string, Array<Post>>();
index 922fc01ead1c05c5730121a50bab5d1e38de2565..b5b1fce364f2198f76f3275621d779fb83bcc2b3 100644 (file)
@@ -29,7 +29,15 @@ import {
   WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { wsJsonToRes, hotRank, toast } from '../utils';
+import {
+  wsJsonToRes,
+  toast,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeRes,
+  commentsToFlatNodes,
+} from '../utils';
 import { PostListing } from './post-listing';
 import { PostListings } from './post-listings';
 import { Sidebar } from './sidebar';
@@ -256,16 +264,14 @@ export class Post extends Component<any, PostState> {
       <div class="d-none d-md-block new-comments mb-3 card border-secondary">
         <div class="card-body small">
           <h6>{i18n.t('recent_comments')}</h6>
-          {this.state.comments.map(comment => (
-            <CommentNodes
-              nodes={[{ comment: comment }]}
-              noIndent
-              locked={this.state.post.locked}
-              moderators={this.state.moderators}
-              admins={this.state.admins}
-              postCreatorId={this.state.post.creator_id}
-            />
-          ))}
+          <CommentNodes
+            nodes={commentsToFlatNodes(this.state.comments)}
+            noIndent
+            locked={this.state.post.locked}
+            moderators={this.state.moderators}
+            admins={this.state.admins}
+            postCreatorId={this.state.post.creator_id}
+          />
         </div>
       </div>
     );
@@ -307,48 +313,9 @@ export class Post extends Component<any, PostState> {
       }
     }
 
-    this.sortTree(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() {
     let nodes = this.buildCommentsTree();
     return (
@@ -359,6 +326,7 @@ export class Post extends Component<any, PostState> {
           moderators={this.state.moderators}
           admins={this.state.admins}
           postCreatorId={this.state.post.creator_id}
+          sort={this.state.commentSort}
         />
       </div>
     );
@@ -408,53 +376,19 @@ export class Post extends Component<any, PostState> {
       }
     } else if (res.op == UserOperation.EditComment) {
       let data = res.data as CommentResponse;
-      let found = this.state.comments.find(c => c.id == data.comment.id);
-      if (found) {
-        found.content = data.comment.content;
-        found.updated = data.comment.updated;
-        found.removed = data.comment.removed;
-        found.deleted = data.comment.deleted;
-        found.upvotes = data.comment.upvotes;
-        found.downvotes = data.comment.downvotes;
-        found.score = data.comment.score;
-        found.read = data.comment.read;
-
-        this.setState(this.state);
-      }
+      editCommentRes(data, this.state.comments);
+      this.setState(this.state);
     } else if (res.op == UserOperation.SaveComment) {
       let data = res.data as CommentResponse;
-      let found = this.state.comments.find(c => c.id == data.comment.id);
-      if (found) {
-        found.saved = data.comment.saved;
-        this.setState(this.state);
-      }
+      saveCommentRes(data, this.state.comments);
+      this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      let found: Comment = this.state.comments.find(
-        c => c.id === data.comment.id
-      );
-      if (found) {
-        found.score = data.comment.score;
-        found.upvotes = data.comment.upvotes;
-        found.downvotes = data.comment.downvotes;
-        if (data.comment.my_vote !== null) {
-          found.my_vote = data.comment.my_vote;
-          found.upvoteLoading = false;
-          found.downvoteLoading = false;
-        }
-      }
+      createCommentLikeRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      this.state.post.score = data.post.score;
-      this.state.post.upvotes = data.post.upvotes;
-      this.state.post.downvotes = data.post.downvotes;
-      if (data.post.my_vote !== null) {
-        this.state.post.my_vote = data.post.my_vote;
-        this.state.post.upvoteLoading = false;
-        this.state.post.downvoteLoading = false;
-      }
-
+      createPostLikeRes(data, this.state.post);
       this.setState(this.state);
     } else if (res.op == UserOperation.EditPost) {
       let data = res.data as PostResponse;
@@ -510,7 +444,6 @@ export class Post extends Component<any, PostState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.TransferSite) {
       let data = res.data as GetSiteResponse;
-
       this.state.admins = data.admins;
       this.setState(this.state);
     } else if (res.op == UserOperation.TransferCommunity) {
index 3acb716728766a69bdf7fa87965f5811d712d56d..3fd2f46773e0913bf295b12683471946b66fdc53 100644 (file)
@@ -25,6 +25,9 @@ import {
   pictshareAvatarThumbnail,
   showAvatars,
   toast,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  commentsToFlatNodes,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { SortSelect } from './sort-select';
@@ -294,15 +297,11 @@ export class Search extends Component<any, SearchState> {
 
   comments() {
     return (
-      <>
-        {this.state.searchResponse.comments.map(comment => (
-          <div class="row">
-            <div class="col-12">
-              <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
-            </div>
-          </div>
-        ))}
-      </>
+      <CommentNodes
+        nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
+        locked
+        noIndent
+      />
     );
   }
 
@@ -474,27 +473,11 @@ export class Search extends Component<any, SearchState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      let found: Comment = this.state.searchResponse.comments.find(
-        c => c.id === data.comment.id
-      );
-      found.score = data.comment.score;
-      found.upvotes = data.comment.upvotes;
-      found.downvotes = data.comment.downvotes;
-      if (data.comment.my_vote !== null) {
-        found.my_vote = data.comment.my_vote;
-        found.upvoteLoading = false;
-        found.downvoteLoading = false;
-      }
+      createCommentLikeRes(data, this.state.searchResponse.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      let found = this.state.searchResponse.posts.find(
-        c => c.id == data.post.id
-      );
-      found.my_vote = data.post.my_vote;
-      found.score = data.post.score;
-      found.upvotes = data.post.upvotes;
-      found.downvotes = data.post.downvotes;
+      createPostLikeFindRes(data, this.state.searchResponse.posts);
       this.setState(this.state);
     }
   }
index da6aa8cee27257f02272aace391b4d25f18f3e58..e2df15e1405194886b423d71e489057e684fd4a3 100644 (file)
@@ -32,6 +32,11 @@ import {
   languages,
   showAvatars,
   toast,
+  editCommentRes,
+  saveCommentRes,
+  createCommentLikeRes,
+  createPostLikeFindRes,
+  commentsToFlatNodes,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { SortSelect } from './sort-select';
@@ -316,13 +321,11 @@ export class User extends Component<any, UserState> {
   comments() {
     return (
       <div>
-        {this.state.comments.map(comment => (
-          <CommentNodes
-            nodes={[{ comment: comment }]}
-            admins={this.state.admins}
-            noIndent
-          />
-        ))}
+        <CommentNodes
+          nodes={commentsToFlatNodes(this.state.comments)}
+          admins={this.state.admins}
+          noIndent
+        />
       </div>
     );
   }
@@ -1032,44 +1035,27 @@ export class User extends Component<any, UserState> {
       this.setState(this.state);
     } else if (res.op == UserOperation.EditComment) {
       let data = res.data as CommentResponse;
-
-      let found = this.state.comments.find(c => c.id == data.comment.id);
-      found.content = data.comment.content;
-      found.updated = data.comment.updated;
-      found.removed = data.comment.removed;
-      found.deleted = data.comment.deleted;
-      found.upvotes = data.comment.upvotes;
-      found.downvotes = data.comment.downvotes;
-      found.score = data.comment.score;
-
+      editCommentRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateComment) {
-      // let res: CommentResponse = msg;
-      toast(i18n.t('reply_sent'));
-      // this.state.comments.unshift(res.comment); // TODO do this right
-      // this.setState(this.state);
+      let data = res.data as CommentResponse;
+      if (
+        UserService.Instance.user &&
+        data.comment.creator_id == UserService.Instance.user.id
+      ) {
+        toast(i18n.t('reply_sent'));
+      }
     } else if (res.op == UserOperation.SaveComment) {
       let data = res.data as CommentResponse;
-      let found = this.state.comments.find(c => c.id == data.comment.id);
-      found.saved = data.comment.saved;
+      saveCommentRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreateCommentLike) {
       let data = res.data as CommentResponse;
-      let found: Comment = this.state.comments.find(
-        c => c.id === data.comment.id
-      );
-      found.score = data.comment.score;
-      found.upvotes = data.comment.upvotes;
-      found.downvotes = data.comment.downvotes;
-      if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
+      createCommentLikeRes(data, this.state.comments);
       this.setState(this.state);
     } else if (res.op == UserOperation.CreatePostLike) {
       let data = res.data as PostResponse;
-      let found = this.state.posts.find(c => c.id == data.post.id);
-      found.my_vote = data.post.my_vote;
-      found.score = data.post.score;
-      found.upvotes = data.post.upvotes;
-      found.downvotes = data.post.downvotes;
+      createPostLikeFindRes(data, this.state.posts);
       this.setState(this.state);
     } else if (res.op == UserOperation.BanUser) {
       let data = res.data as BanUserResponse;
index 8a9aa3c387feced6c3540371775b0ec4176eaa63..c56f6c4eaba08e520293bba781769b1aceca7eaa 100644 (file)
@@ -41,7 +41,7 @@ class Index extends Component<any, any> {
             <Switch>
               <Route exact path={`/`} component={Main} />
               <Route
-                path={`/home/type/:type/sort/:sort/page/:page`}
+                path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`}
                 component={Main}
               />
               <Route path={`/login`} component={Login} />
@@ -56,7 +56,7 @@ class Index extends Component<any, any> {
               <Route path={`/post/:id/comment/:comment_id`} component={Post} />
               <Route path={`/post/:id`} component={Post} />
               <Route
-                path={`/c/:name/sort/:sort/page/:page`}
+                path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
                 component={Community}
               />
               <Route path={`/community/:id`} component={Community} />
index 02c108aa59e4ccdefafc699467238844f6d969bf..23551b595264d945ab43c54db32a71f1357f0261 100644 (file)
@@ -42,6 +42,7 @@ export enum UserOperation {
   EditPrivateMessage,
   GetPrivateMessages,
   UserJoin,
+  GetComments,
 }
 
 export enum CommentSortType {
@@ -57,6 +58,11 @@ export enum ListingType {
   Community,
 }
 
+export enum DataType {
+  Post,
+  Comment,
+}
+
 export enum SortType {
   Hot,
   New,
@@ -165,13 +171,12 @@ export interface Post {
   upvotes: number;
   downvotes: number;
   hot_rank: number;
+  newest_activity_time: string;
   user_id?: number;
   my_vote?: number;
   subscribed?: boolean;
   read?: boolean;
   saved?: boolean;
-  upvoteLoading?: boolean;
-  downvoteLoading?: boolean;
   duplicates?: Array<Post>;
 }
 
@@ -187,6 +192,7 @@ export interface Comment {
   published: string;
   updated?: string;
   community_id: number;
+  community_name: string;
   banned: boolean;
   banned_from_community: boolean;
   creator_name: string;
@@ -194,13 +200,13 @@ export interface Comment {
   score: number;
   upvotes: number;
   downvotes: number;
+  hot_rank: number;
   user_id?: number;
   my_vote?: number;
+  subscribed?: number;
   saved?: boolean;
   user_mention_id?: number; // For mention type
   recipient_id?: number;
-  upvoteLoading?: boolean;
-  downvoteLoading?: boolean;
 }
 
 export interface Category {
@@ -659,6 +665,19 @@ export interface GetPostsResponse {
   posts: Array<Post>;
 }
 
+export interface GetCommentsForm {
+  type_: string;
+  sort: string;
+  page?: number;
+  limit: number;
+  community_id?: number;
+  auth?: string;
+}
+
+export interface GetCommentsResponse {
+  comments: Array<Comment>;
+}
+
 export interface CreatePostLikeForm {
   post_id: number;
   score: number;
index 6d951618a8ad0be55ad506bf38cf6f90d4ffdc3d..3df69457a612909790876b8b3c8b623cb6cd7ab1 100644 (file)
@@ -38,6 +38,7 @@ import {
   PrivateMessageForm,
   EditPrivateMessageForm,
   GetPrivateMessagesForm,
+  GetCommentsForm,
   UserJoinForm,
   MessageType,
   WebSocketJsonResponse,
@@ -172,6 +173,11 @@ export class WebSocketService {
     this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form));
   }
 
+  public getComments(form: GetCommentsForm) {
+    this.setAuth(form, false);
+    this.ws.send(this.wsSendWrapper(UserOperation.GetComments, form));
+  }
+
   public likePost(form: CreatePostLikeForm) {
     this.setAuth(form);
     this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
index f71c203bc5f10469ff25f986bb1b0a7e30c09975..788bce798726084a150abc7b8b7efd894ebdd160 100644 (file)
@@ -201,6 +201,7 @@ export const en = {
     couldnt_like_comment: "Couldn't like comment.",
     couldnt_update_comment: "Couldn't update comment.",
     couldnt_save_comment: "Couldn't save comment.",
+    couldnt_get_comments: "Couldn't get comments.",
     no_comment_edit_allowed: 'Not allowed to edit comment.',
     no_post_edit_allowed: 'Not allowed to edit post.',
     no_community_edit_allowed: 'Not allowed to edit community.',
index 9ad0920f4f66b1313149c8e712ef69ba3001c026..384d5c1d500ab71d56ee456fc05704f8d6aba6c8 100644 (file)
@@ -15,15 +15,21 @@ import 'moment/locale/pt-br';
 import {
   UserOperation,
   Comment,
+  CommentNode,
+  Post,
   PrivateMessage,
   User,
   SortType,
+  CommentSortType,
   ListingType,
+  DataType,
   SearchType,
   WebSocketResponse,
   WebSocketJsonResponse,
   SearchForm,
   SearchResponse,
+  CommentResponse,
+  PostResponse,
 } from './interfaces';
 import { UserService, WebSocketService } from './services';
 
@@ -88,15 +94,22 @@ md.renderer.rules.emoji = function(token, idx) {
   return twemoji.parse(token[idx].content);
 };
 
-export function hotRank(comment: Comment): number {
-  // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
+export function hotRankComment(comment: Comment): number {
+  return hotRank(comment.score, comment.published);
+}
+
+export function hotRankPost(post: Post): number {
+  return hotRank(post.score, post.newest_activity_time);
+}
 
-  let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date
+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 hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
 
   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);
 
   // console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
@@ -198,6 +211,10 @@ export function routeListingTypeToEnum(type: string): ListingType {
   return ListingType[capitalizeFirstLetter(type)];
 }
 
+export function routeDataTypeToEnum(type: string): DataType {
+  return DataType[capitalizeFirstLetter(type)];
+}
+
 export function routeSearchTypeToEnum(type: string): SearchType {
   return SearchType[capitalizeFirstLetter(type)];
 }
@@ -519,3 +536,202 @@ function communitySearch(text: string, cb: any) {
     cb([]);
   }
 }
+
+export function getListingTypeFromProps(props: any): ListingType {
+  return props.match.params.listing_type
+    ? routeListingTypeToEnum(props.match.params.listing_type)
+    : UserService.Instance.user
+    ? UserService.Instance.user.default_listing_type
+    : ListingType.All;
+}
+
+// TODO might need to add a user setting for this too
+export function getDataTypeFromProps(props: any): DataType {
+  return props.match.params.data_type
+    ? routeDataTypeToEnum(props.match.params.data_type)
+    : DataType.Post;
+}
+
+export function getSortTypeFromProps(props: any): SortType {
+  return props.match.params.sort
+    ? routeSortTypeToEnum(props.match.params.sort)
+    : UserService.Instance.user
+    ? UserService.Instance.user.default_sort_type
+    : SortType.Hot;
+}
+
+export function getPageFromProps(props: any): number {
+  return props.match.params.page ? Number(props.match.params.page) : 1;
+}
+
+export function editCommentRes(
+  data: CommentResponse,
+  comments: Array<Comment>
+) {
+  let found = comments.find(c => c.id == data.comment.id);
+  if (found) {
+    found.content = data.comment.content;
+    found.updated = data.comment.updated;
+    found.removed = data.comment.removed;
+    found.deleted = data.comment.deleted;
+    found.upvotes = data.comment.upvotes;
+    found.downvotes = data.comment.downvotes;
+    found.score = data.comment.score;
+  }
+}
+
+export function saveCommentRes(
+  data: CommentResponse,
+  comments: Array<Comment>
+) {
+  let found = comments.find(c => c.id == data.comment.id);
+  if (found) {
+    found.saved = data.comment.saved;
+  }
+}
+
+export function createCommentLikeRes(
+  data: CommentResponse,
+  comments: Array<Comment>
+) {
+  let found: Comment = comments.find(c => c.id === data.comment.id);
+  if (found) {
+    found.score = data.comment.score;
+    found.upvotes = data.comment.upvotes;
+    found.downvotes = data.comment.downvotes;
+    if (data.comment.my_vote !== null) {
+      found.my_vote = data.comment.my_vote;
+    }
+  }
+}
+
+export function createPostLikeFindRes(data: PostResponse, posts: Array<Post>) {
+  let found = posts.find(c => c.id == data.post.id);
+  if (found) {
+    createPostLikeRes(data, found);
+  }
+}
+
+export function createPostLikeRes(data: PostResponse, post: Post) {
+  post.score = data.post.score;
+  post.upvotes = data.post.upvotes;
+  post.downvotes = data.post.downvotes;
+  if (data.post.my_vote !== null) {
+    post.my_vote = data.post.my_vote;
+  }
+}
+
+export function editPostFindRes(data: PostResponse, posts: Array<Post>) {
+  let found = posts.find(c => c.id == data.post.id);
+  if (found) {
+    editPostRes(data, found);
+  }
+}
+
+export function editPostRes(data: PostResponse, post: Post) {
+  post.url = data.post.url;
+  post.name = data.post.name;
+  post.nsfw = data.post.nsfw;
+}
+
+export function commentsToFlatNodes(
+  comments: Array<Comment>
+): Array<CommentNode> {
+  let nodes: Array<CommentNode> = [];
+  for (let comment of comments) {
+    nodes.push({ comment: comment });
+  }
+  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)
+    );
+  }
+}
index 845bbef1eff2536e06fa47545c753fb4b63de0cb..8eab99577e2acfb10af092d6fc12f6354815e149 100644 (file)
@@ -1 +1 @@
-export const version: string = 'v0.6.13';
+export const version: string = 'v0.6.17';
index 64493e39ffb7691b5d80a0412f1ce58fa2ae46a2..5c9ad637a80dbd2670782c9c88f8c7db62bab49a 100644 (file)
@@ -1079,10 +1079,10 @@ emoji-regex@^8.0.0:
   resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
   integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
 
-emoji-short-name@^0.1.0:
-  version "0.1.4"
-  resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-0.1.4.tgz#125a452adc22a399b089f802f9d8d46ecb6e5b08"
-  integrity sha512-VTjEKkhN1UARtHLqlK70N5K3SwxuZAkmdm5sXvSjkV677kr0jt/O7mvB5eQqM+3rKCa+w3Qb5G7wwU/fezonKQ==
+emoji-short-name@^1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-short-name/-/emoji-short-name-1.0.0.tgz#82e6f543b6c68984d69bdc80eac735104fdd4af8"
+  integrity sha512-+tiniHvgRR7XMI1jAaGveumWg5LALE/nWkFD6CcOn6M5IDM9w4PkMs8UwzLTMoZtDLdTdQmzxGvLOxHVIjPzjg==
 
 encodeurl@~1.0.2:
   version "1.0.2"
@@ -3731,10 +3731,10 @@ realm-utils@^1.0.9:
     app-root-path "^1.3.0"
     mkdirp "^0.5.1"
 
-reconnecting-websocket@^4.3.0:
-  version "4.3.0"
-  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.3.0.tgz#aaefbc7629a89450aa45324b89aec2276e728cc5"
-  integrity sha512-3eaHIEVYB9Zb0GfYy1xdEHKJLA2JaawAegByZ1AZ8Npb3AiRgUN5l89cvE2H+pHTsFcoC88t32ky9qET6DJ75Q==
+reconnecting-websocket@^4.4.0:
+  version "4.4.0"
+  resolved "https://registry.yarnpkg.com/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz#3b0e5b96ef119e78a03135865b8bb0af1b948783"
+  integrity sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==
 
 regenerate-unicode-properties@^8.1.0:
   version "8.1.0"