lang | done | missing
---- | ---- | -------
-ca | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
-de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-eo | 74% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-es | 99% | cross_posted_to,post_title_too_long
-fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
-fr | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-nl | 98% | cross_posted_to,post_title_too_long,time,action
-pt-br | 100% | post_title_too_long
-ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
-zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+ca | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+de | 86% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,old,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+fa | 71% | cross_post,cross_posted_to,subscribed_to_communities,trending_communities,create_private_message,send_secure_message,send_message,message,mod,mods,moderates,remove_as_mod,appoint_as_mod,modlog,stickied,ban,ban_from_site,unban,unban_from_site,banned,number_of_subscribers,subscribers,both,saved,unsubscribe,subscribe,subscribed,old,api,docs,inbox,inbox_for,message_sent,notifications_error,messages,no_email_setup,matrix_user_id,private_message_disclaimer,url,body,copy_suggested_title,community,expand_here,subscribe_to_communities,theme,sponsor_message,support_on_liberapay,general_sponsors,joined,by,to,from,landing_0,logged_in,couldnt_get_comments,community_moderator_already_exists,community_follower_already_exists,community_user_already_banned,post_title_too_long,no_slurs,admin_already_created,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+eo | 73% | cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,support_on_liberapay,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
+fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,post_title_too_long,time,action
+fr | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+it | 82% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,time,action
+pt-br | 99% | couldnt_get_comments,post_title_too_long
+ru | 70% | cross_posts,cross_post,cross_posted_to,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,support_on_liberapay,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+sv | 81% | cross_posted_to,create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,support_on_liberapay,donate_to_lemmy,donate,from,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
+zh | 69% | cross_posts,cross_post,cross_posted_to,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,old,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,couldnt_get_comments,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
<!-- translationsstop -->
If you'd like to update this report, run:
- 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
-#!/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
--- /dev/null
+
+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
+;
+
--- /dev/null
+
+-- 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
+;
+
--- /dev/null
+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
+;
+
--- /dev/null
+-- 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
+;
+
use crate::send_email;
use crate::settings::Settings;
use diesel::PgConnection;
+use std::str::FromStr;
#[derive(Serialize, Deserialize)]
pub struct CreateComment {
auth: String,
}
+#[derive(Serialize, Deserialize)]
+pub struct GetComments {
+ type_: String,
+ sort: String,
+ page: Option<i64>,
+ limit: Option<i64>,
+ pub community_id: Option<i32>,
+ auth: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetCommentsResponse {
+ comments: Vec<CommentView>,
+}
+
impl Perform<CommentResponse> for Oper<CreateComment> {
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
let data: &CreateComment = &self.data;
})
}
}
+
+impl Perform<GetCommentsResponse> for Oper<GetComments> {
+ fn perform(&self, conn: &PgConnection) -> Result<GetCommentsResponse, Error> {
+ let data: &GetComments = &self.data;
+
+ let user_claims: Option<Claims> = match &data.auth {
+ Some(auth) => match Claims::decode(&auth) {
+ Ok(claims) => Some(claims.claims),
+ Err(_e) => None,
+ },
+ None => None,
+ };
+
+ let user_id = match &user_claims {
+ Some(claims) => Some(claims.id),
+ None => None,
+ };
+
+ let type_ = ListingType::from_str(&data.type_)?;
+ let sort = SortType::from_str(&data.sort)?;
+
+ let comments = match CommentQueryBuilder::create(&conn)
+ .listing_type(type_)
+ .sort(&sort)
+ .for_community_id(data.community_id)
+ .my_user_id(user_id)
+ .page(data.page)
+ .limit(data.limit)
+ .list()
+ {
+ Ok(comments) => comments,
+ Err(_e) => return Err(APIError::err("couldnt_get_comments").into()),
+ };
+
+ Ok(GetCommentsResponse { comments })
+ }
+}
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
+ subscribed -> Nullable<Bool>,
saved -> Nullable<Bool>,
}
}
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
+ subscribed -> Nullable<Bool>,
saved -> Nullable<Bool>,
}
}
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
+ pub subscribed: Option<bool>,
pub saved: Option<bool>,
}
pub struct CommentQueryBuilder<'a> {
conn: &'a PgConnection,
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
+ listing_type: ListingType,
sort: &'a SortType,
+ for_community_id: Option<i32>,
for_post_id: Option<i32>,
for_creator_id: Option<i32>,
search_term: Option<String>,
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,
}
}
+ 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
self
}
+ pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
+ self.for_community_id = for_community_id.get_optional();
+ self
+ }
+
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
self.search_term = search_term.get_optional();
self
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));
};
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
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);
from_comment_id: i32,
my_user_id: Option<i32>,
) -> Result<Self, Error> {
- use super::comment_view::comment_view::dsl::*;
-
- let mut query = comment_view.into_boxed();
+ use super::comment_view::comment_mview::dsl::*;
+ let mut query = comment_mview.into_boxed();
// The view lets you pass a null user_id, if you're not logged in
if let Some(my_user_id) = my_user_id {
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
+ subscribed -> Nullable<Bool>,
saved -> Nullable<Bool>,
recipient_id -> Int4,
}
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
+ pub subscribed: Option<bool>,
pub saved: Option<bool>,
pub recipient_id: i32,
}
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,
creator_avatar: None,
score: 1,
downvotes: 0,
+ hot_rank: 0,
upvotes: 1,
user_id: None,
my_vote: None,
+ subscribed: None,
saved: None,
};
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,
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();
from_community_id: i32,
from_user_id: Option<i32>,
) -> Result<Self, Error> {
- use super::community_view::community_view::dsl::*;
+ use super::community_view::community_mview::dsl::*;
- let mut query = community_view.into_boxed();
+ let mut query = community_mview.into_boxed();
query = query.filter(id.eq(from_community_id));
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ newest_activity_time -> Timestamp,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub newest_activity_time: chrono::NaiveDateTime,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub subscribed: Option<bool>,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ newest_activity_time -> Timestamp,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
sort: &'a SortType,
my_user_id: Option<i32>,
for_creator_id: Option<i32>,
+ for_community_id: Option<i32>,
+ search_term: Option<String>,
+ url_search: Option<String>,
show_nsfw: bool,
saved_only: bool,
unread_only: bool,
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,
}
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(for_community_id) = for_community_id.get_optional() {
- self.query = self.query.filter(community_id.eq(for_community_id));
- self.query = self.query.then_order_by(stickied.desc());
- }
+ self.for_community_id = for_community_id.get_optional();
self
}
pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> Self {
- if let Some(for_creator_id) = for_creator_id.get_optional() {
- self.for_creator_id = Some(for_creator_id);
- }
+ self.for_creator_id = for_creator_id.get_optional();
self
}
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(search_term) = search_term.get_optional() {
- let searcher = fuzzy_search(&search_term);
- self.query = self
- .query
- .filter(name.ilike(searcher.to_owned()))
- .or_filter(body.ilike(searcher));
- }
+ self.search_term = search_term.get_optional();
self
}
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
- use super::post_view::post_mview::dsl::*;
- if let Some(url_search) = url_search.get_optional() {
- self.query = self.query.filter(url.eq(url_search));
- }
+ self.url_search = url_search.get_optional();
self
}
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())
from_post_id: i32,
my_user_id: Option<i32>,
) -> Result<Self, Error> {
- use super::post_view::post_view::dsl::*;
+ use super::post_view::post_mview::dsl::*;
use diesel::prelude::*;
- let mut query = post_view.into_boxed();
+ let mut query = post_mview.into_boxed();
query = query.filter(id.eq(from_post_id));
downvotes: 0,
hot_rank: 1728,
published: inserted_post.published,
+ newest_activity_time: inserted_post.published,
updated: None,
subscribed: None,
read: None,
downvotes: 0,
hot_rank: 1728,
published: inserted_post.published,
+ newest_activity_time: inserted_post.published,
updated: None,
subscribed: None,
read: None,
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
updated -> Nullable<Timestamp>,
deleted -> Bool,
community_id -> Int4,
+ community_name -> Varchar,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
+ hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
pub updated: Option<chrono::NaiveDateTime>,
pub deleted: bool,
pub community_id: i32,
+ pub community_name: String,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
+ pub hot_rank: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub saved: Option<bool>,
.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
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);
impl UserView {
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
- use super::user_view::user_view::dsl::*;
-
- user_view.find(from_user_id).first::<Self>(conn)
+ use super::user_view::user_mview::dsl::*;
+ user_mview.find(from_user_id).first::<Self>(conn)
}
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
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))
.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(
-pub const VERSION: &str = "v0.6.13";
+pub const VERSION: &str = "v0.6.17";
EditPrivateMessage,
GetPrivateMessages,
UserJoin,
+ GetComments,
}
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());
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());
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)
}
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)
}
let res = Oper::new(get_posts).perform(&conn)?;
to_json_string(&user_operation, &res)
}
+ UserOperation::GetComments => {
+ let get_comments: GetComments = serde_json::from_str(data)?;
+ if get_comments.community_id.is_none() {
+ // 0 is the "all" community
+ chat.join_community_room(0, msg.id);
+ }
+ let res = Oper::new(get_comments).perform(&conn)?;
+ to_json_string(&user_operation, &res)
+ }
UserOperation::CreatePost => {
chat.check_rate_limit_post(msg.id, true)?;
let create_post: CreatePost = serde_json::from_str(data)?;
.img-expanded {
max-height: 90vh;
}
+
+.vote-animate:active {
+ transform: scale(1.2);
+ -webkit-transform: scale(1.2);
+ -ms-transform: scale(1.2);
+}
"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": [],
"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",
"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",
TransferCommunityForm,
TransferSiteForm,
BanType,
+ CommentSortType,
+ SortType,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
showConfirmAppointAsAdmin: boolean;
collapsed: boolean;
viewSource: boolean;
- upvoteLoading: boolean;
- downvoteLoading: boolean;
+ my_vote: number;
+ score: number;
+ upvotes: number;
+ downvotes: number;
}
interface CommentNodeProps {
markable?: boolean;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
+ // TODO is this necessary, can't I get it from the node itself?
postCreatorId?: number;
+ showCommunity?: boolean;
+ sort?: CommentSortType;
+ sortType?: SortType;
}
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
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) {
}
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() {
.viewOnly && 'no-click'}`}
>
<button
- className={`btn btn-link p-0 ${
- node.comment.my_vote == 1 ? 'text-info' : 'text-muted'
+ className={`vote-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
>
- {this.state.upvoteLoading ? (
- <svg class="icon icon-spinner spin upvote">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- <svg class="icon upvote">
- <use xlinkHref="#icon-arrow-up"></use>
- </svg>
- )}
+ <svg class="icon upvote">
+ <use xlinkHref="#icon-arrow-up"></use>
+ </svg>
</button>
- <div class={`font-weight-bold text-muted`}>
- {node.comment.score}
- </div>
+ <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
{WebSocketService.Instance.site.enable_downvotes && (
<button
- className={`btn btn-link p-0 ${
- node.comment.my_vote == -1 ? 'text-danger' : 'text-muted'
+ className={`vote-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(node, this.handleCommentDownvote)}
>
- {this.state.downvoteLoading ? (
- <svg class="icon icon-spinner spin downvote">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- <svg class="icon downvote">
- <use xlinkHref="#icon-arrow-down"></use>
- </svg>
- )}
+ <svg class="icon downvote">
+ <use xlinkHref="#icon-arrow-down"></use>
+ </svg>
</button>
)}
</div>
)}
<li className="list-inline-item">
<span>
- (<span className="text-info">+{node.comment.upvotes}</span>
+ (<span className="text-info">+{this.state.upvotes}</span>
<span> | </span>
- <span className="text-danger">-{node.comment.downvotes}</span>
+ <span className="text-danger">-{this.state.downvotes}</span>
<span>) </span>
</span>
</li>
+ {this.props.showCommunity && (
+ <li className="list-inline-item">
+ <span> {i18n.t('to')} </span>
+ <Link to={`/c/${node.comment.community_name}`}>
+ {node.comment.community_name}
+ </Link>
+ </li>
+ )}
<li className="list-inline-item">
<span>
<MomentTime data={node.comment} />
moderators={this.props.moderators}
admins={this.props.admins}
postCreatorId={this.props.postCreatorId}
+ sort={this.props.sort}
+ sortType={this.props.sortType}
/>
)}
{/* A collapsed clearfix */}
}
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) {
CommentNode as CommentNodeI,
CommunityUser,
UserView,
+ CommentSortType,
+ SortType,
} from '../interfaces';
+import { commentSort, commentSortSortType } from '../utils';
import { CommentNode } from './comment-node';
interface CommentNodesState {}
viewOnly?: boolean;
locked?: boolean;
markable?: boolean;
+ showCommunity?: boolean;
+ sort?: CommentSortType;
+ sortType?: SortType;
}
export class CommentNodes extends Component<
render() {
return (
<div className="comments">
- {this.props.nodes.map(node => (
+ {this.sorter().map(node => (
<CommentNode
node={node}
noIndent={this.props.noIndent}
admins={this.props.admins}
postCreatorId={this.props.postCreatorId}
markable={this.props.markable}
+ showCommunity={this.props.showCommunity}
+ sort={this.props.sort}
+ sortType={this.props.sortType}
/>
))}
</div>
);
}
+
+ sorter(): Array<CommentNodeI> {
+ if (this.props.sort !== undefined) {
+ commentSort(this.props.nodes, this.props.sort);
+ } else if (this.props.sortType !== undefined) {
+ commentSortSortType(this.props.nodes, this.props.sortType);
+ }
+
+ return this.props.nodes;
+ }
}
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 {
online: number;
loading: boolean;
posts: Array<Post>;
+ comments: Array<Comment>;
+ dataType: DataType;
sort: SortType;
page: number;
}
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))))
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();
}
}
)}
</h5>
{this.selects()}
- <PostListings posts={this.state.posts} removeDuplicates />
+ {this.listings()}
{this.paginator()}
</div>
<div class="col-12 col-md-4">
);
}
+ listings() {
+ return this.state.dataType == DataType.Post ? (
+ <PostListings
+ posts={this.state.posts}
+ removeDuplicates
+ sort={this.state.sort}
+ />
+ ) : (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ sortType={this.state.sort}
+ />
+ );
+ }
+
selects() {
return (
<div class="mb-2">
- <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ <DataTypeSelect
+ type_={this.state.dataType}
+ onChange={this.handleDataTypeChange}
+ />
+
+ <span class="mx-2">
+ <SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
+ </span>
<a
href={`/feeds/c/${this.state.communityName}.xml?sort=${
SortType[this.state.sort]
i.state.page++;
i.setState(i.state);
i.updateUrl();
- i.fetchPosts();
+ i.fetchData();
window.scrollTo(0, 0);
}
i.state.page--;
i.setState(i.state);
i.updateUrl();
- i.fetchPosts();
+ i.fetchData();
window.scrollTo(0, 0);
}
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) {
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;
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;
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;
.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);
}
}
}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { DataType } from '../interfaces';
+
+import { i18n } from '../i18next';
+
+interface DataTypeSelectProps {
+ type_: DataType;
+ onChange?(val: DataType): any;
+}
+
+interface DataTypeSelectState {
+ type_: DataType;
+}
+
+export class DataTypeSelect extends Component<
+ DataTypeSelectProps,
+ DataTypeSelectState
+> {
+ private emptyState: DataTypeSelectState = {
+ type_: this.props.type_,
+ };
+
+ constructor(props: any, context: any) {
+ super(props, context);
+ this.state = this.emptyState;
+ }
+
+ render() {
+ return (
+ <div class="btn-group btn-group-toggle">
+ <label
+ className={`pointer btn btn-sm btn-secondary
+ ${this.state.type_ == DataType.Post && 'active'}
+ `}
+ >
+ <input
+ type="radio"
+ value={DataType.Post}
+ checked={this.state.type_ == DataType.Post}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ />
+ {i18n.t('posts')}
+ </label>
+ <label
+ className={`pointer btn btn-sm btn-secondary ${this.state.type_ ==
+ DataType.Comment && 'active'}`}
+ >
+ <input
+ type="radio"
+ value={DataType.Comment}
+ checked={this.state.type_ == DataType.Comment}
+ onChange={linkEvent(this, this.handleTypeChange)}
+ />
+ {i18n.t('comments')}
+ </label>
+ </div>
+ );
+ }
+
+ handleTypeChange(i: DataTypeSelect, event: any) {
+ i.state.type_ = Number(event.target.value);
+ i.setState(i.state);
+ i.props.onChange(i.state.type_);
+ }
+}
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';
replies() {
return (
<div>
- {this.state.replies.map(reply => (
- <CommentNodes nodes={[{ comment: reply }]} noIndent markable />
- ))}
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.replies)}
+ noIndent
+ markable
+ />
</div>
);
}
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) {
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);
}
}
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';
showEditSite: boolean;
loading: boolean;
posts: Array<Post>;
- type_: ListingType;
+ comments: Array<Comment>;
+ listingType: ListingType;
+ dataType: DataType;
sort: SortType;
page: number;
}
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))))
WebSocketService.Instance.listCommunities(listCommunitiesForm);
- this.fetchPosts();
+ this.fetchData();
}
componentWillUnmount() {
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();
}
}
}
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}`
);
}
) : (
<div>
{this.selects()}
- <PostListings
- posts={this.state.posts}
- showCommunity
- removeDuplicates
- />
+ {this.listings()}
{this.paginator()}
</div>
)}
);
}
+ listings() {
+ return this.state.dataType == DataType.Post ? (
+ <PostListings
+ posts={this.state.posts}
+ showCommunity
+ removeDuplicates
+ sort={this.state.sort}
+ />
+ ) : (
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ showCommunity
+ sortType={this.state.sort}
+ />
+ );
+ }
+
selects() {
return (
<div className="mb-3">
- <ListingTypeSelect
- type_={this.state.type_}
- onChange={this.handleTypeChange}
+ <DataTypeSelect
+ type_={this.state.dataType}
+ onChange={this.handleDataTypeChange}
/>
<span class="mx-2">
+ <ListingTypeSelect
+ type_={this.state.listingType}
+ onChange={this.handleListingTypeChange}
+ />
+ </span>
+ <span class="mr-2">
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
</span>
- {this.state.type_ == ListingType.All && (
+ {this.state.listingType == ListingType.All && (
<a
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
target="_blank"
</a>
)}
{UserService.Instance.user &&
- this.state.type_ == ListingType.Subscribed && (
+ this.state.listingType == ListingType.Subscribed && (
<a
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
SortType[this.state.sort]
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
- i.fetchPosts();
+ i.fetchData();
window.scrollTo(0, 0);
}
i.state.loading = true;
i.setState(i.state);
i.updateUrl();
- i.fetchPosts();
+ i.fetchData();
window.scrollTo(0, 0);
}
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) {
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;
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)
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;
.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);
}
}
}
showConfirmTransferCommunity: boolean;
imageExpanded: boolean;
viewSource: boolean;
- upvoteLoading: boolean;
- downvoteLoading: boolean;
+ my_vote: number;
+ score: number;
+ upvotes: number;
+ downvotes: number;
}
interface PostListingProps {
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) {
}
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() {
<div class="listing col-12">
<div className={`vote-bar mr-2 float-left small text-center`}>
<button
- className={`btn btn-link p-0 ${
- post.my_vote == 1 ? 'text-info' : 'text-muted'
+ className={`vote-animate btn btn-link p-0 ${
+ this.state.my_vote == 1 ? 'text-info' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostLike)}
>
- {this.state.upvoteLoading ? (
- <svg class="icon icon-spinner spin upvote">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- <svg class="icon upvote">
- <use xlinkHref="#icon-arrow-up"></use>
- </svg>
- )}
+ <svg class="icon upvote">
+ <use xlinkHref="#icon-arrow-up"></use>
+ </svg>
</button>
- <div class={`font-weight-bold text-muted`}>{post.score}</div>
+ <div class={`font-weight-bold text-muted`}>{this.state.score}</div>
{WebSocketService.Instance.site.enable_downvotes && (
<button
- className={`btn btn-link p-0 ${
- post.my_vote == -1 ? 'text-danger' : 'text-muted'
+ className={`vote-animate btn btn-link p-0 ${
+ this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
}`}
onClick={linkEvent(this, this.handlePostDisLike)}
>
- {this.state.downvoteLoading ? (
- <svg class="icon icon-spinner spin downvote">
- <use xlinkHref="#icon-spinner"></use>
- </svg>
- ) : (
- <svg class="icon downvote">
- <use xlinkHref="#icon-arrow-down"></use>
- </svg>
- )}
+ <svg class="icon downvote">
+ <use xlinkHref="#icon-arrow-down"></use>
+ </svg>
</button>
)}
</div>
</li>
<li className="list-inline-item">
<span>
- (<span className="text-info">+{post.upvotes}</span>
+ (<span className="text-info">+{this.state.upvotes}</span>
<span> | </span>
- <span className="text-danger">-{post.downvotes}</span>
+ <span className="text-danger">-{this.state.downvotes}</span>
<span>) </span>
</span>
</li>
}
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) {
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';
posts: Array<Post>;
showCommunity?: boolean;
removeDuplicates?: boolean;
+ sort?: SortType;
}
export class PostListings extends Component<PostListingsProps, any> {
return (
<div>
{this.props.posts.length > 0 ? (
- (this.props.removeDuplicates
- ? this.removeDuplicates(this.props.posts)
- : this.props.posts
- ).map(post => (
+ this.outer().map(post => (
<>
<PostListing
post={post}
);
}
+ outer(): Array<Post> {
+ let out = this.props.posts;
+ if (this.props.removeDuplicates) {
+ out = this.removeDuplicates(out);
+ }
+
+ if (this.props.sort !== undefined) {
+ postSort(out, this.props.sort);
+ }
+
+ return out;
+ }
+
removeDuplicates(posts: Array<Post>): Array<Post> {
// A map from post url to list of posts (dupes)
let urlMap = new Map<string, Array<Post>>();
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';
<div class="d-none d-md-block new-comments mb-3 card border-secondary">
<div class="card-body small">
<h6>{i18n.t('recent_comments')}</h6>
- {this.state.comments.map(comment => (
- <CommentNodes
- nodes={[{ comment: comment }]}
- noIndent
- locked={this.state.post.locked}
- moderators={this.state.moderators}
- admins={this.state.admins}
- postCreatorId={this.state.post.creator_id}
- />
- ))}
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ noIndent
+ locked={this.state.post.locked}
+ moderators={this.state.moderators}
+ admins={this.state.admins}
+ postCreatorId={this.state.post.creator_id}
+ />
</div>
</div>
);
}
}
- this.sortTree(tree);
-
return tree;
}
- sortTree(tree: Array<CommentNodeI>) {
- // First, put removed and deleted comments at the bottom, then do your other sorts
- if (this.state.commentSort == CommentSortType.Top) {
- tree.sort(
- (a, b) =>
- +a.comment.removed - +b.comment.removed ||
- +a.comment.deleted - +b.comment.deleted ||
- b.comment.score - a.comment.score
- );
- } else if (this.state.commentSort == CommentSortType.New) {
- tree.sort(
- (a, b) =>
- +a.comment.removed - +b.comment.removed ||
- +a.comment.deleted - +b.comment.deleted ||
- b.comment.published.localeCompare(a.comment.published)
- );
- } else if (this.state.commentSort == CommentSortType.Old) {
- tree.sort(
- (a, b) =>
- +a.comment.removed - +b.comment.removed ||
- +a.comment.deleted - +b.comment.deleted ||
- a.comment.published.localeCompare(b.comment.published)
- );
- } else if (this.state.commentSort == CommentSortType.Hot) {
- tree.sort(
- (a, b) =>
- +a.comment.removed - +b.comment.removed ||
- +a.comment.deleted - +b.comment.deleted ||
- hotRank(b.comment) - hotRank(a.comment)
- );
- }
-
- for (let node of tree) {
- this.sortTree(node.children);
- }
- }
-
commentsTree() {
let nodes = this.buildCommentsTree();
return (
moderators={this.state.moderators}
admins={this.state.admins}
postCreatorId={this.state.post.creator_id}
+ sort={this.state.commentSort}
/>
</div>
);
}
} 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;
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) {
pictshareAvatarThumbnail,
showAvatars,
toast,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+ commentsToFlatNodes,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
comments() {
return (
- <>
- {this.state.searchResponse.comments.map(comment => (
- <div class="row">
- <div class="col-12">
- <CommentNodes nodes={[{ comment: comment }]} locked noIndent />
- </div>
- </div>
- ))}
- </>
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
+ locked
+ noIndent
+ />
);
}
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);
}
}
languages,
showAvatars,
toast,
+ editCommentRes,
+ saveCommentRes,
+ createCommentLikeRes,
+ createPostLikeFindRes,
+ commentsToFlatNodes,
} from '../utils';
import { PostListing } from './post-listing';
import { SortSelect } from './sort-select';
comments() {
return (
<div>
- {this.state.comments.map(comment => (
- <CommentNodes
- nodes={[{ comment: comment }]}
- admins={this.state.admins}
- noIndent
- />
- ))}
+ <CommentNodes
+ nodes={commentsToFlatNodes(this.state.comments)}
+ admins={this.state.admins}
+ noIndent
+ />
</div>
);
}
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;
<Switch>
<Route exact path={`/`} component={Main} />
<Route
- path={`/home/type/:type/sort/:sort/page/:page`}
+ path={`/home/data_type/:data_type/listing_type/:listing_type/sort/:sort/page/:page`}
component={Main}
/>
<Route path={`/login`} component={Login} />
<Route path={`/post/:id/comment/:comment_id`} component={Post} />
<Route path={`/post/:id`} component={Post} />
<Route
- path={`/c/:name/sort/:sort/page/:page`}
+ path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
component={Community}
/>
<Route path={`/community/:id`} component={Community} />
EditPrivateMessage,
GetPrivateMessages,
UserJoin,
+ GetComments,
}
export enum CommentSortType {
Community,
}
+export enum DataType {
+ Post,
+ Comment,
+}
+
export enum SortType {
Hot,
New,
upvotes: number;
downvotes: number;
hot_rank: number;
+ newest_activity_time: string;
user_id?: number;
my_vote?: number;
subscribed?: boolean;
read?: boolean;
saved?: boolean;
- upvoteLoading?: boolean;
- downvoteLoading?: boolean;
duplicates?: Array<Post>;
}
published: string;
updated?: string;
community_id: number;
+ community_name: string;
banned: boolean;
banned_from_community: boolean;
creator_name: string;
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 {
posts: Array<Post>;
}
+export interface GetCommentsForm {
+ type_: string;
+ sort: string;
+ page?: number;
+ limit: number;
+ community_id?: number;
+ auth?: string;
+}
+
+export interface GetCommentsResponse {
+ comments: Array<Comment>;
+}
+
export interface CreatePostLikeForm {
post_id: number;
score: number;
PrivateMessageForm,
EditPrivateMessageForm,
GetPrivateMessagesForm,
+ GetCommentsForm,
UserJoinForm,
MessageType,
WebSocketJsonResponse,
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));
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.',
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';
return twemoji.parse(token[idx].content);
};
-export function hotRank(comment: Comment): number {
- // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
+export function hotRankComment(comment: Comment): number {
+ return hotRank(comment.score, comment.published);
+}
+
+export function hotRankPost(post: Post): number {
+ return hotRank(post.score, post.newest_activity_time);
+}
- let date: Date = new Date(comment.published + 'Z'); // Add Z to convert from UTC date
+export function hotRank(score: number, timeStr: string): number {
+ // Rank = ScaleFactor * sign(Score) * log(1 + abs(Score)) / (Time + 2)^Gravity
+ let date: Date = new Date(timeStr + 'Z'); // Add Z to convert from UTC date
let now: Date = new Date();
let hoursElapsed: number = (now.getTime() - date.getTime()) / 36e5;
let rank =
- (10000 * Math.log10(Math.max(1, 3 + comment.score))) /
+ (10000 * Math.log10(Math.max(1, 3 + score))) /
Math.pow(hoursElapsed + 2, 1.8);
// console.log(`Comment: ${comment.content}\nRank: ${rank}\nScore: ${comment.score}\nHours: ${hoursElapsed}`);
return ListingType[capitalizeFirstLetter(type)];
}
+export function routeDataTypeToEnum(type: string): DataType {
+ return DataType[capitalizeFirstLetter(type)];
+}
+
export function routeSearchTypeToEnum(type: string): SearchType {
return SearchType[capitalizeFirstLetter(type)];
}
cb([]);
}
}
+
+export function getListingTypeFromProps(props: any): ListingType {
+ return props.match.params.listing_type
+ ? routeListingTypeToEnum(props.match.params.listing_type)
+ : UserService.Instance.user
+ ? UserService.Instance.user.default_listing_type
+ : ListingType.All;
+}
+
+// TODO might need to add a user setting for this too
+export function getDataTypeFromProps(props: any): DataType {
+ return props.match.params.data_type
+ ? routeDataTypeToEnum(props.match.params.data_type)
+ : DataType.Post;
+}
+
+export function getSortTypeFromProps(props: any): SortType {
+ return props.match.params.sort
+ ? routeSortTypeToEnum(props.match.params.sort)
+ : UserService.Instance.user
+ ? UserService.Instance.user.default_sort_type
+ : SortType.Hot;
+}
+
+export function getPageFromProps(props: any): number {
+ return props.match.params.page ? Number(props.match.params.page) : 1;
+}
+
+export function editCommentRes(
+ data: CommentResponse,
+ comments: Array<Comment>
+) {
+ let found = comments.find(c => c.id == data.comment.id);
+ if (found) {
+ found.content = data.comment.content;
+ found.updated = data.comment.updated;
+ found.removed = data.comment.removed;
+ found.deleted = data.comment.deleted;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ found.score = data.comment.score;
+ }
+}
+
+export function saveCommentRes(
+ data: CommentResponse,
+ comments: Array<Comment>
+) {
+ let found = comments.find(c => c.id == data.comment.id);
+ if (found) {
+ found.saved = data.comment.saved;
+ }
+}
+
+export function createCommentLikeRes(
+ data: CommentResponse,
+ comments: Array<Comment>
+) {
+ let found: Comment = comments.find(c => c.id === data.comment.id);
+ if (found) {
+ found.score = data.comment.score;
+ found.upvotes = data.comment.upvotes;
+ found.downvotes = data.comment.downvotes;
+ if (data.comment.my_vote !== null) {
+ found.my_vote = data.comment.my_vote;
+ }
+ }
+}
+
+export function createPostLikeFindRes(data: PostResponse, posts: Array<Post>) {
+ let found = posts.find(c => c.id == data.post.id);
+ if (found) {
+ createPostLikeRes(data, found);
+ }
+}
+
+export function createPostLikeRes(data: PostResponse, post: Post) {
+ post.score = data.post.score;
+ post.upvotes = data.post.upvotes;
+ post.downvotes = data.post.downvotes;
+ if (data.post.my_vote !== null) {
+ post.my_vote = data.post.my_vote;
+ }
+}
+
+export function editPostFindRes(data: PostResponse, posts: Array<Post>) {
+ let found = posts.find(c => c.id == data.post.id);
+ if (found) {
+ editPostRes(data, found);
+ }
+}
+
+export function editPostRes(data: PostResponse, post: Post) {
+ post.url = data.post.url;
+ post.name = data.post.name;
+ post.nsfw = data.post.nsfw;
+}
+
+export function commentsToFlatNodes(
+ comments: Array<Comment>
+): Array<CommentNode> {
+ let nodes: Array<CommentNode> = [];
+ for (let comment of comments) {
+ nodes.push({ comment: comment });
+ }
+ return nodes;
+}
+
+export function commentSort(tree: Array<CommentNode>, sort: CommentSortType) {
+ // First, put removed and deleted comments at the bottom, then do your other sorts
+ if (sort == CommentSortType.Top) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ b.comment.score - a.comment.score
+ );
+ } else if (sort == CommentSortType.New) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ b.comment.published.localeCompare(a.comment.published)
+ );
+ } else if (sort == CommentSortType.Old) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ a.comment.published.localeCompare(b.comment.published)
+ );
+ } else if (sort == CommentSortType.Hot) {
+ tree.sort(
+ (a, b) =>
+ +a.comment.removed - +b.comment.removed ||
+ +a.comment.deleted - +b.comment.deleted ||
+ hotRankComment(b.comment) - hotRankComment(a.comment)
+ );
+ }
+
+ // Go through the children recursively
+ for (let node of tree) {
+ if (node.children) {
+ commentSort(node.children, sort);
+ }
+ }
+}
+
+export function commentSortSortType(tree: Array<CommentNode>, sort: SortType) {
+ commentSort(tree, convertCommentSortType(sort));
+}
+
+function convertCommentSortType(sort: SortType): CommentSortType {
+ if (
+ sort == SortType.TopAll ||
+ sort == SortType.TopDay ||
+ sort == SortType.TopWeek ||
+ sort == SortType.TopMonth ||
+ sort == SortType.TopYear
+ ) {
+ return CommentSortType.Top;
+ } else if (sort == SortType.New) {
+ return CommentSortType.New;
+ } else if (sort == SortType.Hot) {
+ return CommentSortType.Hot;
+ } else {
+ return CommentSortType.Hot;
+ }
+}
+
+export function postSort(posts: Array<Post>, sort: SortType) {
+ // First, put removed and deleted comments at the bottom, then do your other sorts
+ if (
+ sort == SortType.TopAll ||
+ sort == SortType.TopDay ||
+ sort == SortType.TopWeek ||
+ sort == SortType.TopMonth ||
+ sort == SortType.TopYear
+ ) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed || +a.deleted - +b.deleted || b.score - a.score
+ );
+ } else if (sort == SortType.New) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ b.published.localeCompare(a.published)
+ );
+ } else if (sort == SortType.Hot) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ hotRankPost(b) - hotRankPost(a)
+ );
+ }
+}
-export const version: string = 'v0.6.13';
+export const version: string = 'v0.6.17';
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"
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"