diff --git a/README.md b/README.md index fcb07e72d..47290953c 100644 --- 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 If you'd like to update this report, run: diff --git a/ansible/VERSION b/ansible/VERSION index 1adfd94d3..c32d86a50 100644 --- a/ansible/VERSION +++ b/ansible/VERSION @@ -1 +1 @@ -v0.6.13 +v0.6.17 diff --git a/docker/prod/docker-compose.yml b/docker/prod/docker-compose.yml index a5c2918ab..4baceb3ed 100644 --- a/docker/prod/docker-compose.yml +++ b/docker/prod/docker-compose.yml @@ -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 diff --git a/install.sh b/install.sh index 168a1f6b0..b368891cf 100755 --- a/install.sh +++ b/install.sh @@ -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 index 000000000..b6120d151 --- /dev/null +++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/down.sql @@ -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 index 000000000..8836a571a --- /dev/null +++ b/server/migrations/2020-02-07-210055_add_comment_subscribed/up.sql @@ -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 index 000000000..8b912fa36 --- /dev/null +++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/down.sql @@ -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 index 000000000..e15412771 --- /dev/null +++ b/server/migrations/2020-02-08-145624_add_post_newest_activity_time/up.sql @@ -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 +; + diff --git a/server/src/api/comment.rs b/server/src/api/comment.rs index 775085e93..5c6149666 100644 --- a/server/src/api/comment.rs +++ b/server/src/api/comment.rs @@ -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, + limit: Option, + pub community_id: Option, + auth: Option, +} + +#[derive(Serialize, Deserialize)] +pub struct GetCommentsResponse { + comments: Vec, +} + impl Perform for Oper { fn perform(&self, conn: &PgConnection) -> Result { let data: &CreateComment = &self.data; @@ -456,3 +472,40 @@ impl Perform for Oper { }) } } + +impl Perform for Oper { + fn perform(&self, conn: &PgConnection) -> Result { + let data: &GetComments = &self.data; + + let user_claims: Option = 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 }) + } +} diff --git a/server/src/db/comment_view.rs b/server/src/db/comment_view.rs index febf18b78..ff915d5e5 100644 --- a/server/src/db/comment_view.rs +++ b/server/src/db/comment_view.rs @@ -15,6 +15,7 @@ table! { updated -> Nullable, 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, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, } } @@ -41,6 +44,7 @@ table! { updated -> Nullable, 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, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, } } @@ -70,6 +76,7 @@ pub struct CommentView { pub updated: Option, 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, pub my_vote: Option, + pub subscribed: Option, pub saved: Option, } 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, for_post_id: Option, for_creator_id: Option, search_term: Option, @@ -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>(mut self, for_community_id: T) -> Self { + self.for_community_id = for_community_id.get_optional(); + self + } + pub fn search_term>(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, ) -> Result { - 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, 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, my_vote -> Nullable, + subscribed -> Nullable, saved -> Nullable, recipient_id -> Int4, } @@ -281,6 +316,7 @@ pub struct ReplyView { pub updated: Option, 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, pub my_vote: Option, + pub subscribed: Option, pub saved: Option, 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(); diff --git a/server/src/db/community_view.rs b/server/src/db/community_view.rs index 95e00c65c..18ff67a8a 100644 --- a/server/src/db/community_view.rs +++ b/server/src/db/community_view.rs @@ -227,9 +227,9 @@ impl CommunityView { from_community_id: i32, from_user_id: Option, ) -> Result { - 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)); diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index 4d09308d8..3f385077f 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -31,6 +31,7 @@ table! { upvotes -> BigInt, downvotes -> BigInt, hot_rank -> Int4, + newest_activity_time -> Timestamp, user_id -> Nullable, my_vote -> Nullable, subscribed -> Nullable, @@ -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, pub my_vote: Option, pub subscribed: Option, @@ -106,6 +108,7 @@ table! { upvotes -> BigInt, downvotes -> BigInt, hot_rank -> Int4, + newest_activity_time -> Timestamp, user_id -> Nullable, my_vote -> Nullable, subscribed -> Nullable, @@ -121,6 +124,9 @@ pub struct PostQueryBuilder<'a> { sort: &'a SortType, my_user_id: Option, for_creator_id: Option, + for_community_id: Option, + search_term: Option, + url_search: Option, 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>(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>(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>(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>(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, ) -> Result { - 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, diff --git a/server/src/db/user_mention_view.rs b/server/src/db/user_mention_view.rs index 1cf43984a..8046747e6 100644 --- a/server/src/db/user_mention_view.rs +++ b/server/src/db/user_mention_view.rs @@ -16,6 +16,7 @@ table! { updated -> Nullable, 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, my_vote -> Nullable, saved -> Nullable, @@ -44,6 +46,7 @@ table! { updated -> Nullable, 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, my_vote -> Nullable, saved -> Nullable, @@ -75,6 +79,7 @@ pub struct UserMentionView { pub updated: Option, 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, pub my_vote: Option, pub saved: Option, @@ -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); diff --git a/server/src/db/user_view.rs b/server/src/db/user_view.rs index 3ea506e7f..2274ecbdf 100644 --- a/server/src/db/user_view.rs +++ b/server/src/db/user_view.rs @@ -144,9 +144,8 @@ impl<'a> UserQueryBuilder<'a> { impl UserView { pub fn read(conn: &PgConnection, from_user_id: i32) -> Result { - use super::user_view::user_view::dsl::*; - - user_view.find(from_user_id).first::(conn) + use super::user_view::user_mview::dsl::*; + user_mview.find(from_user_id).first::(conn) } pub fn admins(conn: &PgConnection) -> Result, Error> { diff --git a/server/src/routes/index.rs b/server/src/routes/index.rs index b044833ef..c1c363c98 100644 --- a/server/src/routes/index.rs +++ b/server/src/routes/index.rs @@ -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( diff --git a/server/src/version.rs b/server/src/version.rs index 2d9ff73cd..92eb4e7f3 100644 --- a/server/src/version.rs +++ b/server/src/version.rs @@ -1 +1 @@ -pub const VERSION: &str = "v0.6.13"; +pub const VERSION: &str = "v0.6.17"; diff --git a/server/src/websocket/mod.rs b/server/src/websocket/mod.rs index c9a41a1fc..a1feede25 100644 --- a/server/src/websocket/mod.rs +++ b/server/src/websocket/mod.rs @@ -45,4 +45,5 @@ pub enum UserOperation { EditPrivateMessage, GetPrivateMessages, UserJoin, + GetComments, } diff --git a/server/src/websocket/server.rs b/server/src/websocket/server.rs index 7ba79e6c4..76a55540f 100644 --- a/server/src/websocket/server.rs +++ b/server/src/websocket/server.rs @@ -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 { + 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)?; diff --git a/ui/assets/css/main.css b/ui/assets/css/main.css index b1ad884a5..df75ec319 100644 --- a/ui/assets/css/main.css +++ b/ui/assets/css/main.css @@ -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); +} diff --git a/ui/package.json b/ui/package.json index 31d91bb4c..9cf349108 100644 --- a/ui/package.json +++ b/ui/package.json @@ -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", diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 3296a5c8e..1d0b12cad 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -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; admins: Array; + // 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 { @@ -77,8 +85,10 @@ export class CommentNode extends Component { 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 { } 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 { .viewOnly && 'no-click'}`} > -
- {node.comment.score} -
+
{this.state.score}
{WebSocketService.Instance.site.enable_downvotes && ( )} @@ -199,12 +191,20 @@ export class CommentNode extends Component { )}
  • - (+{node.comment.upvotes} + (+{this.state.upvotes} | - -{node.comment.downvotes} + -{this.state.downvotes} )
  • + {this.props.showCommunity && ( +
  • + {i18n.t('to')} + + {node.comment.community_name} + +
  • + )}
  • @@ -620,6 +620,8 @@ export class CommentNode extends Component { 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 { } 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) { diff --git a/ui/src/components/comment-nodes.tsx b/ui/src/components/comment-nodes.tsx index 18faf1ac4..b15da5208 100644 --- a/ui/src/components/comment-nodes.tsx +++ b/ui/src/components/comment-nodes.tsx @@ -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 (
    - {this.props.nodes.map(node => ( + {this.sorter().map(node => ( ))}
    ); } + + sorter(): Array { + 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; + } } diff --git a/ui/src/components/community.tsx b/ui/src/components/community.tsx index 069f9158d..e28c99bc7 100644 --- a/ui/src/components/community.tsx +++ b/ui/src/components/community.tsx @@ -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; + comments: Array; + dataType: DataType; sort: SortType; page: number; } @@ -65,27 +87,18 @@ export class Community extends Component { 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 { 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 { )} {this.selects()} - + {this.listings()} {this.paginator()}
    @@ -162,10 +176,33 @@ export class Community extends Component { ); } + listings() { + return this.state.dataType == DataType.Post ? ( + + ) : ( + + ); + } + selects() { return (
    - + + + + + { 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 { 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 { 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 { 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 { 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 { 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 { .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 index 000000000..f2539c810 --- /dev/null +++ b/ui/src/components/data-type-select.tsx @@ -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 ( +
    + + +
    + ); + } + + handleTypeChange(i: DataTypeSelect, event: any) { + i.state.type_ = Number(event.target.value); + i.setState(i.state); + i.props.onChange(i.state.type_); + } +} diff --git a/ui/src/components/inbox.tsx b/ui/src/components/inbox.tsx index 6849b37d2..027a1db04 100644 --- a/ui/src/components/inbox.tsx +++ b/ui/src/components/inbox.tsx @@ -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 { replies() { return (
    - {this.state.replies.map(reply => ( - - ))} +
    ); } @@ -362,15 +373,7 @@ export class Inbox extends Component { 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 { 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); } } diff --git a/ui/src/components/main.tsx b/ui/src/components/main.tsx index 161f5df45..c8e132f7a 100644 --- a/ui/src/components/main.tsx +++ b/ui/src/components/main.tsx @@ -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; - type_: ListingType; + comments: Array; + listingType: ListingType; + dataType: DataType; sort: SortType; page: number; } @@ -79,38 +97,21 @@ export class Main extends Component { 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 { WebSocketService.Instance.listCommunities(listCommunitiesForm); - this.fetchPosts(); + this.fetchData(); } componentWillUnmount() { @@ -146,11 +147,12 @@ export class Main extends Component { 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 { } 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 { ) : (
    {this.selects()} - + {this.listings()} {this.paginator()}
    )} @@ -404,17 +403,41 @@ export class Main extends Component { ); } + listings() { + return this.state.dataType == DataType.Post ? ( + + ) : ( + + ); + } + selects() { return (
    - + + + - {this.state.type_ == ListingType.All && ( + {this.state.listingType == ListingType.All && ( { )} {UserService.Instance.user && - this.state.type_ == ListingType.Subscribed && ( + this.state.listingType == ListingType.Subscribed && ( { 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 { 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 { 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 { 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 { 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 { 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 { .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); } } } diff --git a/ui/src/components/post-listing.tsx b/ui/src/components/post-listing.tsx index f11d9e144..1e7289013 100644 --- a/ui/src/components/post-listing.tsx +++ b/ui/src/components/post-listing.tsx @@ -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 { 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 { } 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 {
    -
    {post.score}
    +
    {this.state.score}
    {WebSocketService.Instance.site.enable_downvotes && ( )}
    @@ -315,9 +303,9 @@ export class PostListing extends Component {
  • - (+{post.upvotes} + (+{this.state.upvotes} | - -{post.downvotes} + -{this.state.downvotes} )
  • @@ -747,28 +735,55 @@ export class PostListing extends Component { } 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) { diff --git a/ui/src/components/post-listings.tsx b/ui/src/components/post-listings.tsx index 005c4fe03..d61f624d4 100644 --- a/ui/src/components/post-listings.tsx +++ b/ui/src/components/post-listings.tsx @@ -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; showCommunity?: boolean; removeDuplicates?: boolean; + sort?: SortType; } export class PostListings extends Component { @@ -20,10 +22,7 @@ export class PostListings extends Component { return (
    {this.props.posts.length > 0 ? ( - (this.props.removeDuplicates - ? this.removeDuplicates(this.props.posts) - : this.props.posts - ).map(post => ( + this.outer().map(post => ( <> { ); } + outer(): Array { + 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): Array { // A map from post url to list of posts (dupes) let urlMap = new Map>(); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 922fc01ea..b5b1fce36 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -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 {
    {i18n.t('recent_comments')}
    - {this.state.comments.map(comment => ( - - ))} +
    ); @@ -307,48 +313,9 @@ export class Post extends Component { } } - this.sortTree(tree); - return tree; } - sortTree(tree: Array) { - // 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 { moderators={this.state.moderators} admins={this.state.admins} postCreatorId={this.state.post.creator_id} + sort={this.state.commentSort} />
    ); @@ -408,53 +376,19 @@ export class Post extends Component { } } 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 { 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) { diff --git a/ui/src/components/search.tsx b/ui/src/components/search.tsx index 3acb71672..3fd2f4677 100644 --- a/ui/src/components/search.tsx +++ b/ui/src/components/search.tsx @@ -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 { comments() { return ( - <> - {this.state.searchResponse.comments.map(comment => ( -
    -
    - -
    -
    - ))} - + ); } @@ -474,27 +473,11 @@ export class Search extends Component { 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); } } diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index da6aa8cee..e2df15e14 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -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 { comments() { return (
    - {this.state.comments.map(comment => ( - - ))} +
    ); } @@ -1032,44 +1035,27 @@ export class User extends Component { 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; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index 8a9aa3c38..c56f6c4ea 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -41,7 +41,7 @@ class Index extends Component { @@ -56,7 +56,7 @@ class Index extends Component { diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index 02c108aa5..23551b595 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -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; } @@ -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; } +export interface GetCommentsForm { + type_: string; + sort: string; + page?: number; + limit: number; + community_id?: number; + auth?: string; +} + +export interface GetCommentsResponse { + comments: Array; +} + export interface CreatePostLikeForm { post_id: number; score: number; diff --git a/ui/src/services/WebSocketService.ts b/ui/src/services/WebSocketService.ts index 6d951618a..3df69457a 100644 --- a/ui/src/services/WebSocketService.ts +++ b/ui/src/services/WebSocketService.ts @@ -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)); diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index f71c203bc..788bce798 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -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.', diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 9ad0920f4..384d5c1d5 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -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); +} - let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date +export function hotRankPost(post: Post): number { + return hotRank(post.score, post.newest_activity_time); +} + +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 +) { + 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 +) { + let found = comments.find(c => c.id == data.comment.id); + if (found) { + found.saved = data.comment.saved; + } +} + +export function createCommentLikeRes( + data: CommentResponse, + comments: Array +) { + 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) { + 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) { + 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 +): Array { + let nodes: Array = []; + for (let comment of comments) { + nodes.push({ comment: comment }); + } + return nodes; +} + +export function commentSort(tree: Array, 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, 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, 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) + ); + } +} diff --git a/ui/src/version.ts b/ui/src/version.ts index 845bbef1e..8eab99577 100644 --- a/ui/src/version.ts +++ b/ui/src/version.ts @@ -1 +1 @@ -export const version: string = 'v0.6.13'; +export const version: string = 'v0.6.17'; diff --git a/ui/yarn.lock b/ui/yarn.lock index 64493e39f..5c9ad637a 100644 --- a/ui/yarn.lock +++ b/ui/yarn.lock @@ -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"