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