mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-26 06:11:26 +00:00
Add new comments views to main and community pages. Fixes #480
This commit is contained in:
parent
779a72581c
commit
ecd10482a6
21 changed files with 1048 additions and 154 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
|
lang | done | missing
|
||||||
---- | ---- | -------
|
---- | ---- | -------
|
||||||
ca | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,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,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,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,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
|
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 | 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
|
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,post_title_too_long
|
es | 99% | cross_posted_to,couldnt_get_comments,post_title_too_long
|
||||||
fi | 97% | cross_posted_to,old,support_on_liberapay,post_title_too_long,time,action
|
fi | 97% | cross_posted_to,old,support_on_liberapay,couldnt_get_comments,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
|
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,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,post_title_too_long,time,action
|
nl | 98% | cross_posted_to,couldnt_get_comments,post_title_too_long,time,action
|
||||||
pt-br | 100% | post_title_too_long
|
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,post_title_too_long,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message,time,action
|
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,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,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 -->
|
<!-- translationsstop -->
|
||||||
|
|
||||||
If you'd like to update this report, run:
|
If you'd like to update this report, 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
|
||||||
|
;
|
||||||
|
|
|
@ -2,6 +2,7 @@ use super::*;
|
||||||
use crate::send_email;
|
use crate::send_email;
|
||||||
use crate::settings::Settings;
|
use crate::settings::Settings;
|
||||||
use diesel::PgConnection;
|
use diesel::PgConnection;
|
||||||
|
use std::str::FromStr;
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub struct CreateComment {
|
pub struct CreateComment {
|
||||||
|
@ -47,6 +48,21 @@ pub struct CreateCommentLike {
|
||||||
auth: String,
|
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> {
|
impl Perform<CommentResponse> for Oper<CreateComment> {
|
||||||
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
fn perform(&self, conn: &PgConnection) -> Result<CommentResponse, Error> {
|
||||||
let data: &CreateComment = &self.data;
|
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>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -22,8 +23,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -41,6 +44,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -48,8 +52,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -70,6 +76,7 @@ pub struct CommentView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -77,15 +84,19 @@ pub struct CommentView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
pub subscribed: Option<bool>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CommentQueryBuilder<'a> {
|
pub struct CommentQueryBuilder<'a> {
|
||||||
conn: &'a PgConnection,
|
conn: &'a PgConnection,
|
||||||
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
query: super::comment_view::comment_mview::BoxedQuery<'a, Pg>,
|
||||||
|
listing_type: ListingType,
|
||||||
sort: &'a SortType,
|
sort: &'a SortType,
|
||||||
|
for_community_id: Option<i32>,
|
||||||
for_post_id: Option<i32>,
|
for_post_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
search_term: Option<String>,
|
search_term: Option<String>,
|
||||||
|
@ -104,7 +115,9 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
CommentQueryBuilder {
|
CommentQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
|
listing_type: ListingType::All,
|
||||||
sort: &SortType::New,
|
sort: &SortType::New,
|
||||||
|
for_community_id: None,
|
||||||
for_post_id: None,
|
for_post_id: None,
|
||||||
for_creator_id: None,
|
for_creator_id: None,
|
||||||
search_term: 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 {
|
pub fn sort(mut self, sort: &'a SortType) -> Self {
|
||||||
self.sort = sort;
|
self.sort = sort;
|
||||||
self
|
self
|
||||||
|
@ -130,6 +148,11 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
||||||
|
self.for_community_id = for_community_id.get_optional();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||||
self.search_term = search_term.get_optional();
|
self.search_term = search_term.get_optional();
|
||||||
self
|
self
|
||||||
|
@ -171,6 +194,10 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
query = query.filter(creator_id.eq(for_creator_id));
|
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 {
|
if let Some(for_post_id) = self.for_post_id {
|
||||||
query = query.filter(post_id.eq(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)));
|
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 {
|
if self.saved_only {
|
||||||
query = query.filter(saved.eq(true));
|
query = query.filter(saved.eq(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
query = match self.sort {
|
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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -199,7 +232,7 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
SortType::TopDay => query
|
SortType::TopDay => query
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.order_by(score.desc()),
|
.order_by(score.desc()),
|
||||||
_ => query.order_by(published.desc()),
|
// _ => query.order_by(published.desc()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
@ -251,6 +284,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -258,8 +292,10 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
subscribed -> Nullable<Bool>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
recipient_id -> Int4,
|
recipient_id -> Int4,
|
||||||
}
|
}
|
||||||
|
@ -281,6 +317,7 @@ pub struct ReplyView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -288,8 +325,10 @@ pub struct ReplyView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
pub subscribed: Option<bool>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
pub recipient_id: i32,
|
pub recipient_id: i32,
|
||||||
}
|
}
|
||||||
|
@ -474,6 +513,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
community_name: inserted_community.name.to_owned(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -486,9 +526,11 @@ mod tests {
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
|
hot_rank: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
my_vote: None,
|
my_vote: None,
|
||||||
|
subscribed: None,
|
||||||
saved: None,
|
saved: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -498,6 +540,7 @@ mod tests {
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
post_id: inserted_post.id,
|
post_id: inserted_post.id,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
|
community_name: inserted_community.name.to_owned(),
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -510,21 +553,26 @@ mod tests {
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
|
hot_rank: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: Some(inserted_user.id),
|
user_id: Some(inserted_user.id),
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
|
subscribed: None,
|
||||||
saved: 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)
|
.for_post_id(inserted_post.id)
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.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)
|
.for_post_id(inserted_post.id)
|
||||||
.my_user_id(inserted_user.id)
|
.my_user_id(inserted_user.id)
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
read_comment_views_with_user[0].hot_rank = 0;
|
||||||
|
|
||||||
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
||||||
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||||
|
|
|
@ -121,6 +121,9 @@ pub struct PostQueryBuilder<'a> {
|
||||||
sort: &'a SortType,
|
sort: &'a SortType,
|
||||||
my_user_id: Option<i32>,
|
my_user_id: Option<i32>,
|
||||||
for_creator_id: Option<i32>,
|
for_creator_id: Option<i32>,
|
||||||
|
for_community_id: Option<i32>,
|
||||||
|
search_term: Option<String>,
|
||||||
|
url_search: Option<String>,
|
||||||
show_nsfw: bool,
|
show_nsfw: bool,
|
||||||
saved_only: bool,
|
saved_only: bool,
|
||||||
unread_only: bool,
|
unread_only: bool,
|
||||||
|
@ -137,10 +140,13 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
PostQueryBuilder {
|
PostQueryBuilder {
|
||||||
conn,
|
conn,
|
||||||
query,
|
query,
|
||||||
my_user_id: None,
|
|
||||||
for_creator_id: None,
|
|
||||||
listing_type: ListingType::All,
|
listing_type: ListingType::All,
|
||||||
sort: &SortType::Hot,
|
sort: &SortType::Hot,
|
||||||
|
my_user_id: None,
|
||||||
|
for_creator_id: None,
|
||||||
|
for_community_id: None,
|
||||||
|
search_term: None,
|
||||||
|
url_search: None,
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
saved_only: false,
|
saved_only: false,
|
||||||
unread_only: false,
|
unread_only: false,
|
||||||
|
@ -160,38 +166,22 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
pub fn for_community_id<T: MaybeOptional<i32>>(mut self, for_community_id: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.for_community_id = for_community_id.get_optional();
|
||||||
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn for_creator_id<T: MaybeOptional<i32>>(mut self, for_creator_id: T) -> 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 = for_creator_id.get_optional();
|
||||||
self.for_creator_id = Some(for_creator_id);
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
pub fn search_term<T: MaybeOptional<String>>(mut self, search_term: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.search_term = search_term.get_optional();
|
||||||
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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
|
pub fn url_search<T: MaybeOptional<String>>(mut self, url_search: T) -> Self {
|
||||||
use super::post_view::post_mview::dsl::*;
|
self.url_search = url_search.get_optional();
|
||||||
if let Some(url_search) = url_search.get_optional() {
|
|
||||||
self.query = self.query.filter(url.eq(url_search));
|
|
||||||
}
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -234,6 +224,22 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
query = query.filter(subscribed.eq(true));
|
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 {
|
query = match self.sort {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.then_order_by(hot_rank.desc())
|
.then_order_by(hot_rank.desc())
|
||||||
|
|
|
@ -16,6 +16,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -23,6 +24,7 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -44,6 +46,7 @@ table! {
|
||||||
updated -> Nullable<Timestamp>,
|
updated -> Nullable<Timestamp>,
|
||||||
deleted -> Bool,
|
deleted -> Bool,
|
||||||
community_id -> Int4,
|
community_id -> Int4,
|
||||||
|
community_name -> Varchar,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
@ -51,6 +54,7 @@ table! {
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
|
hot_rank -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -75,6 +79,7 @@ pub struct UserMentionView {
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub deleted: bool,
|
pub deleted: bool,
|
||||||
pub community_id: i32,
|
pub community_id: i32,
|
||||||
|
pub community_name: String,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
@ -82,6 +87,7 @@ pub struct UserMentionView {
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
|
pub hot_rank: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
|
@ -149,7 +155,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
.filter(recipient_id.eq(self.for_user_id));
|
.filter(recipient_id.eq(self.for_user_id));
|
||||||
|
|
||||||
query = match self.sort {
|
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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -164,7 +172,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
SortType::TopDay => query
|
SortType::TopDay => query
|
||||||
.filter(published.gt(now - 1.days()))
|
.filter(published.gt(now - 1.days()))
|
||||||
.order_by(score.desc()),
|
.order_by(score.desc()),
|
||||||
_ => query.order_by(published.desc()),
|
// _ => query.order_by(published.desc()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
let (limit, offset) = limit_and_offset(self.page, self.limit);
|
||||||
|
|
|
@ -6,7 +6,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg
|
cfg
|
||||||
.route("/", web::get().to(index))
|
.route("/", web::get().to(index))
|
||||||
.route(
|
.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),
|
web::get().to(index),
|
||||||
)
|
)
|
||||||
.route("/login", 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("/communities", web::get().to(index))
|
||||||
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
.route("/post/{id}/comment/{id2}", web::get().to(index))
|
||||||
.route("/post/{id}", 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("/c/{name}", web::get().to(index))
|
||||||
.route("/community/{id}", web::get().to(index))
|
.route("/community/{id}", web::get().to(index))
|
||||||
.route(
|
.route(
|
||||||
|
|
|
@ -45,4 +45,5 @@ pub enum UserOperation {
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
|
GetComments,
|
||||||
}
|
}
|
||||||
|
|
|
@ -121,6 +121,12 @@ impl ChatServer {
|
||||||
sessions.remove(&id);
|
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 the room doesn't exist yet
|
||||||
if self.community_rooms.get_mut(&community_id).is_none() {
|
if self.community_rooms.get_mut(&community_id).is_none() {
|
||||||
self.community_rooms.insert(community_id, HashSet::new());
|
self.community_rooms.insert(community_id, HashSet::new());
|
||||||
|
@ -139,6 +145,12 @@ impl ChatServer {
|
||||||
sessions.remove(&id);
|
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 the room doesn't exist yet
|
||||||
if self.post_rooms.get_mut(&post_id).is_none() {
|
if self.post_rooms.get_mut(&post_id).is_none() {
|
||||||
self.post_rooms.insert(post_id, HashSet::new());
|
self.post_rooms.insert(post_id, HashSet::new());
|
||||||
|
@ -243,6 +255,10 @@ impl ChatServer {
|
||||||
self.send_user_room_message(recipient_id, &comment_reply_sent_str, id);
|
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)
|
Ok(comment_user_sent_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -613,6 +629,15 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
|
||||||
let res = Oper::new(get_posts).perform(&conn)?;
|
let res = Oper::new(get_posts).perform(&conn)?;
|
||||||
to_json_string(&user_operation, &res)
|
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 => {
|
UserOperation::CreatePost => {
|
||||||
chat.check_rate_limit_post(msg.id, true)?;
|
chat.check_rate_limit_post(msg.id, true)?;
|
||||||
let create_post: CreatePost = serde_json::from_str(data)?;
|
let create_post: CreatePost = serde_json::from_str(data)?;
|
||||||
|
|
10
ui/src/components/comment-node.tsx
vendored
10
ui/src/components/comment-node.tsx
vendored
|
@ -58,7 +58,9 @@ interface CommentNodeProps {
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
moderators: Array<CommunityUser>;
|
moderators: Array<CommunityUser>;
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
|
// TODO is this necessary, can't I get it from the node itself?
|
||||||
postCreatorId?: number;
|
postCreatorId?: number;
|
||||||
|
showCommunity?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
|
@ -205,6 +207,14 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
<span>) </span>
|
<span>) </span>
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</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">
|
<li className="list-inline-item">
|
||||||
<span>
|
<span>
|
||||||
<MomentTime data={node.comment} />
|
<MomentTime data={node.comment} />
|
||||||
|
|
2
ui/src/components/comment-nodes.tsx
vendored
2
ui/src/components/comment-nodes.tsx
vendored
|
@ -17,6 +17,7 @@ interface CommentNodesProps {
|
||||||
viewOnly?: boolean;
|
viewOnly?: boolean;
|
||||||
locked?: boolean;
|
locked?: boolean;
|
||||||
markable?: boolean;
|
markable?: boolean;
|
||||||
|
showCommunity?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommentNodes extends Component<
|
export class CommentNodes extends Component<
|
||||||
|
@ -40,6 +41,7 @@ export class CommentNodes extends Component<
|
||||||
admins={this.props.admins}
|
admins={this.props.admins}
|
||||||
postCreatorId={this.props.postCreatorId}
|
postCreatorId={this.props.postCreatorId}
|
||||||
markable={this.props.markable}
|
markable={this.props.markable}
|
||||||
|
showCommunity={this.props.showCommunity}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
|
165
ui/src/components/community.tsx
vendored
165
ui/src/components/community.tsx
vendored
|
@ -13,17 +13,31 @@ import {
|
||||||
GetPostsForm,
|
GetPostsForm,
|
||||||
GetCommunityForm,
|
GetCommunityForm,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
GetPostsResponse,
|
GetPostsResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
AddModToCommunityResponse,
|
AddModToCommunityResponse,
|
||||||
BanFromCommunityResponse,
|
BanFromCommunityResponse,
|
||||||
|
Comment,
|
||||||
|
GetCommentsForm,
|
||||||
|
GetCommentsResponse,
|
||||||
|
CommentResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
import { DataTypeSelect } from './data-type-select';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
import { wsJsonToRes, routeSortTypeToEnum, fetchLimit, toast } from '../utils';
|
import {
|
||||||
|
wsJsonToRes,
|
||||||
|
fetchLimit,
|
||||||
|
toast,
|
||||||
|
getPageFromProps,
|
||||||
|
getSortTypeFromProps,
|
||||||
|
getDataTypeFromProps,
|
||||||
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface State {
|
interface State {
|
||||||
|
@ -35,6 +49,8 @@ interface State {
|
||||||
online: number;
|
online: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
|
comments: Array<Comment>;
|
||||||
|
dataType: DataType;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
@ -65,27 +81,18 @@ export class Community extends Component<any, State> {
|
||||||
online: null,
|
online: null,
|
||||||
loading: true,
|
loading: true,
|
||||||
posts: [],
|
posts: [],
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
comments: [],
|
||||||
page: this.getPageFromProps(this.props),
|
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) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.handleSortChange = this.handleSortChange.bind(this);
|
this.handleSortChange = this.handleSortChange.bind(this);
|
||||||
|
this.handleDataTypeChange = this.handleDataTypeChange.bind(this);
|
||||||
|
|
||||||
this.subscription = WebSocketService.Instance.subject
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
|
@ -112,10 +119,11 @@ export class Community extends Component<any, State> {
|
||||||
nextProps.history.action == 'POP' ||
|
nextProps.history.action == 'POP' ||
|
||||||
nextProps.history.action == 'PUSH'
|
nextProps.history.action == 'PUSH'
|
||||||
) {
|
) {
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.sort = getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = getPageFromProps(nextProps);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -145,7 +153,7 @@ export class Community extends Component<any, State> {
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
<PostListings posts={this.state.posts} removeDuplicates />
|
{this.listings()}
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
|
@ -162,10 +170,31 @@ export class Community extends Component<any, State> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listings() {
|
||||||
|
return this.state.dataType == DataType.Post ? (
|
||||||
|
<PostListings posts={this.state.posts} removeDuplicates />
|
||||||
|
) : (
|
||||||
|
this.state.comments.map(comment => (
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<CommentNodes nodes={[{ comment: comment }]} noIndent />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div class="mb-2">
|
<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
|
<a
|
||||||
href={`/feeds/c/${this.state.communityName}.xml?sort=${
|
href={`/feeds/c/${this.state.communityName}.xml?sort=${
|
||||||
SortType[this.state.sort]
|
SortType[this.state.sort]
|
||||||
|
@ -207,7 +236,7 @@ export class Community extends Component<any, State> {
|
||||||
i.state.page++;
|
i.state.page++;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,7 +244,7 @@ export class Community extends Component<any, State> {
|
||||||
i.state.page--;
|
i.state.page--;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -225,26 +254,48 @@ export class Community extends Component<any, State> {
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
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);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
updateUrl() {
|
||||||
|
let dataTypeStr = DataType[this.state.dataType].toLowerCase();
|
||||||
let sortStr = SortType[this.state.sort].toLowerCase();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(
|
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() {
|
fetchData() {
|
||||||
let getPostsForm: GetPostsForm = {
|
if (this.state.dataType == DataType.Post) {
|
||||||
page: this.state.page,
|
let getPostsForm: GetPostsForm = {
|
||||||
limit: fetchLimit,
|
page: this.state.page,
|
||||||
sort: SortType[this.state.sort],
|
limit: fetchLimit,
|
||||||
type_: ListingType[ListingType.Community],
|
sort: SortType[this.state.sort],
|
||||||
community_id: this.state.community.id,
|
type_: ListingType[ListingType.Community],
|
||||||
};
|
community_id: this.state.community.id,
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
};
|
||||||
|
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) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
|
@ -255,7 +306,7 @@ export class Community extends Component<any, State> {
|
||||||
this.context.router.history.push('/');
|
this.context.router.history.push('/');
|
||||||
return;
|
return;
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.GetCommunity) {
|
} else if (res.op == UserOperation.GetCommunity) {
|
||||||
let data = res.data as GetCommunityResponse;
|
let data = res.data as GetCommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
|
@ -264,7 +315,7 @@ export class Community extends Component<any, State> {
|
||||||
this.state.online = data.online;
|
this.state.online = data.online;
|
||||||
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
|
document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.EditCommunity) {
|
} else if (res.op == UserOperation.EditCommunity) {
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.community = data.community;
|
this.state.community = data.community;
|
||||||
|
@ -319,6 +370,48 @@ export class Community extends Component<any, State> {
|
||||||
.forEach(p => (p.banned = data.banned));
|
.forEach(p => (p.banned = data.banned));
|
||||||
|
|
||||||
this.setState(this.state);
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
let found = this.state.comments.find(c => c.id == data.comment.id);
|
||||||
|
found.saved = data.comment.saved;
|
||||||
|
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;
|
||||||
|
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_);
|
||||||
|
}
|
||||||
|
}
|
209
ui/src/components/main.tsx
vendored
209
ui/src/components/main.tsx
vendored
|
@ -12,30 +12,39 @@ import {
|
||||||
SortType,
|
SortType,
|
||||||
GetSiteResponse,
|
GetSiteResponse,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
SiteResponse,
|
SiteResponse,
|
||||||
GetPostsResponse,
|
GetPostsResponse,
|
||||||
PostResponse,
|
PostResponse,
|
||||||
Post,
|
Post,
|
||||||
GetPostsForm,
|
GetPostsForm,
|
||||||
|
Comment,
|
||||||
|
GetCommentsForm,
|
||||||
|
GetCommentsResponse,
|
||||||
|
CommentResponse,
|
||||||
AddAdminResponse,
|
AddAdminResponse,
|
||||||
BanUserResponse,
|
BanUserResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { PostListings } from './post-listings';
|
import { PostListings } from './post-listings';
|
||||||
|
import { CommentNodes } from './comment-nodes';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
import { ListingTypeSelect } from './listing-type-select';
|
import { ListingTypeSelect } from './listing-type-select';
|
||||||
|
import { DataTypeSelect } from './data-type-select';
|
||||||
import { SiteForm } from './site-form';
|
import { SiteForm } from './site-form';
|
||||||
import {
|
import {
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
repoUrl,
|
repoUrl,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
routeSortTypeToEnum,
|
|
||||||
routeListingTypeToEnum,
|
|
||||||
pictshareAvatarThumbnail,
|
pictshareAvatarThumbnail,
|
||||||
showAvatars,
|
showAvatars,
|
||||||
toast,
|
toast,
|
||||||
|
getListingTypeFromProps,
|
||||||
|
getPageFromProps,
|
||||||
|
getSortTypeFromProps,
|
||||||
|
getDataTypeFromProps,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -47,7 +56,9 @@ interface MainState {
|
||||||
showEditSite: boolean;
|
showEditSite: boolean;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
posts: Array<Post>;
|
posts: Array<Post>;
|
||||||
type_: ListingType;
|
comments: Array<Comment>;
|
||||||
|
listingType: ListingType;
|
||||||
|
dataType: DataType;
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
}
|
}
|
||||||
|
@ -79,38 +90,21 @@ export class Main extends Component<any, MainState> {
|
||||||
showEditSite: false,
|
showEditSite: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
posts: [],
|
posts: [],
|
||||||
type_: this.getListingTypeFromProps(this.props),
|
comments: [],
|
||||||
sort: this.getSortTypeFromProps(this.props),
|
listingType: getListingTypeFromProps(this.props),
|
||||||
page: this.getPageFromProps(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) {
|
constructor(props: any, context: any) {
|
||||||
super(props, context);
|
super(props, context);
|
||||||
|
|
||||||
this.state = this.emptyState;
|
this.state = this.emptyState;
|
||||||
this.handleEditCancel = this.handleEditCancel.bind(this);
|
this.handleEditCancel = this.handleEditCancel.bind(this);
|
||||||
this.handleSortChange = this.handleSortChange.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
|
this.subscription = WebSocketService.Instance.subject
|
||||||
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
|
||||||
|
@ -133,7 +127,7 @@ export class Main extends Component<any, MainState> {
|
||||||
|
|
||||||
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
WebSocketService.Instance.listCommunities(listCommunitiesForm);
|
||||||
|
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -146,11 +140,12 @@ export class Main extends Component<any, MainState> {
|
||||||
nextProps.history.action == 'POP' ||
|
nextProps.history.action == 'POP' ||
|
||||||
nextProps.history.action == 'PUSH'
|
nextProps.history.action == 'PUSH'
|
||||||
) {
|
) {
|
||||||
this.state.type_ = this.getListingTypeFromProps(nextProps);
|
this.state.listingType = getListingTypeFromProps(nextProps);
|
||||||
this.state.sort = this.getSortTypeFromProps(nextProps);
|
this.state.dataType = getDataTypeFromProps(nextProps);
|
||||||
this.state.page = this.getPageFromProps(nextProps);
|
this.state.sort = getSortTypeFromProps(nextProps);
|
||||||
|
this.state.page = getPageFromProps(nextProps);
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -251,10 +246,11 @@ export class Main extends Component<any, MainState> {
|
||||||
}
|
}
|
||||||
|
|
||||||
updateUrl() {
|
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();
|
let sortStr = SortType[this.state.sort].toLowerCase();
|
||||||
this.props.history.push(
|
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 +388,7 @@ export class Main extends Component<any, MainState> {
|
||||||
) : (
|
) : (
|
||||||
<div>
|
<div>
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
<PostListings
|
{this.listings()}
|
||||||
posts={this.state.posts}
|
|
||||||
showCommunity
|
|
||||||
removeDuplicates
|
|
||||||
/>
|
|
||||||
{this.paginator()}
|
{this.paginator()}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
@ -404,17 +396,41 @@ export class Main extends Component<any, MainState> {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
listings() {
|
||||||
|
return this.state.dataType == DataType.Post ? (
|
||||||
|
<PostListings posts={this.state.posts} showCommunity removeDuplicates />
|
||||||
|
) : (
|
||||||
|
this.state.comments.map(comment => (
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<CommentNodes
|
||||||
|
nodes={[{ comment: comment }]}
|
||||||
|
noIndent
|
||||||
|
showCommunity
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
selects() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div className="mb-3">
|
<div className="mb-3">
|
||||||
<ListingTypeSelect
|
<DataTypeSelect
|
||||||
type_={this.state.type_}
|
type_={this.state.dataType}
|
||||||
onChange={this.handleTypeChange}
|
onChange={this.handleDataTypeChange}
|
||||||
/>
|
/>
|
||||||
<span class="mx-2">
|
<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} />
|
<SortSelect sort={this.state.sort} onChange={this.handleSortChange} />
|
||||||
</span>
|
</span>
|
||||||
{this.state.type_ == ListingType.All && (
|
{this.state.listingType == ListingType.All && (
|
||||||
<a
|
<a
|
||||||
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
|
href={`/feeds/all.xml?sort=${SortType[this.state.sort]}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
@ -425,7 +441,7 @@ export class Main extends Component<any, MainState> {
|
||||||
</a>
|
</a>
|
||||||
)}
|
)}
|
||||||
{UserService.Instance.user &&
|
{UserService.Instance.user &&
|
||||||
this.state.type_ == ListingType.Subscribed && (
|
this.state.listingType == ListingType.Subscribed && (
|
||||||
<a
|
<a
|
||||||
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
|
href={`/feeds/front/${UserService.Instance.auth}.xml?sort=${
|
||||||
SortType[this.state.sort]
|
SortType[this.state.sort]
|
||||||
|
@ -488,7 +504,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -497,7 +513,7 @@ export class Main extends Component<any, MainState> {
|
||||||
i.state.loading = true;
|
i.state.loading = true;
|
||||||
i.setState(i.state);
|
i.setState(i.state);
|
||||||
i.updateUrl();
|
i.updateUrl();
|
||||||
i.fetchPosts();
|
i.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -507,28 +523,48 @@ export class Main extends Component<any, MainState> {
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
handleTypeChange(val: ListingType) {
|
handleListingTypeChange(val: ListingType) {
|
||||||
this.state.type_ = val;
|
this.state.listingType = val;
|
||||||
this.state.page = 1;
|
this.state.page = 1;
|
||||||
this.state.loading = true;
|
this.state.loading = true;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
this.updateUrl();
|
this.updateUrl();
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
window.scrollTo(0, 0);
|
window.scrollTo(0, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
fetchPosts() {
|
handleDataTypeChange(val: DataType) {
|
||||||
let getPostsForm: GetPostsForm = {
|
this.state.dataType = val;
|
||||||
page: this.state.page,
|
this.state.page = 1;
|
||||||
limit: fetchLimit,
|
this.state.loading = true;
|
||||||
sort: SortType[this.state.sort],
|
this.setState(this.state);
|
||||||
type_: ListingType[this.state.type_],
|
this.updateUrl();
|
||||||
};
|
this.fetchData();
|
||||||
WebSocketService.Instance.getPosts(getPostsForm);
|
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) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
|
@ -538,7 +574,7 @@ export class Main extends Component<any, MainState> {
|
||||||
toast(i18n.t(msg.error), 'danger');
|
toast(i18n.t(msg.error), 'danger');
|
||||||
return;
|
return;
|
||||||
} else if (msg.reconnect) {
|
} else if (msg.reconnect) {
|
||||||
this.fetchPosts();
|
this.fetchData();
|
||||||
} else if (res.op == UserOperation.GetFollowedCommunities) {
|
} else if (res.op == UserOperation.GetFollowedCommunities) {
|
||||||
let data = res.data as GetFollowedCommunitiesResponse;
|
let data = res.data as GetFollowedCommunitiesResponse;
|
||||||
this.state.subscribedCommunities = data.communities;
|
this.state.subscribedCommunities = data.communities;
|
||||||
|
@ -574,7 +610,7 @@ export class Main extends Component<any, MainState> {
|
||||||
let data = res.data as PostResponse;
|
let data = res.data as PostResponse;
|
||||||
|
|
||||||
// If you're on subscribed, only push it if you're subscribed.
|
// 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 (
|
if (
|
||||||
this.state.subscribedCommunities
|
this.state.subscribedCommunities
|
||||||
.map(c => c.community_id)
|
.map(c => c.community_id)
|
||||||
|
@ -633,6 +669,59 @@ export class Main extends Component<any, MainState> {
|
||||||
.forEach(p => (p.banned = data.banned));
|
.forEach(p => (p.banned = data.banned));
|
||||||
|
|
||||||
this.setState(this.state);
|
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;
|
||||||
|
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
let found = this.state.comments.find(c => c.id == data.comment.id);
|
||||||
|
found.saved = data.comment.saved;
|
||||||
|
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;
|
||||||
|
this.setState(this.state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
30
ui/src/components/user.tsx
vendored
30
ui/src/components/user.tsx
vendored
|
@ -1034,20 +1034,24 @@ export class User extends Component<any, UserState> {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
|
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
let found = this.state.comments.find(c => c.id == data.comment.id);
|
||||||
found.content = data.comment.content;
|
if (found) {
|
||||||
found.updated = data.comment.updated;
|
found.content = data.comment.content;
|
||||||
found.removed = data.comment.removed;
|
found.updated = data.comment.updated;
|
||||||
found.deleted = data.comment.deleted;
|
found.removed = data.comment.removed;
|
||||||
found.upvotes = data.comment.upvotes;
|
found.deleted = data.comment.deleted;
|
||||||
found.downvotes = data.comment.downvotes;
|
found.upvotes = data.comment.upvotes;
|
||||||
found.score = data.comment.score;
|
found.downvotes = data.comment.downvotes;
|
||||||
|
found.score = data.comment.score;
|
||||||
this.setState(this.state);
|
this.setState(this.state);
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.CreateComment) {
|
} else if (res.op == UserOperation.CreateComment) {
|
||||||
// let res: CommentResponse = msg;
|
let data = res.data as CommentResponse;
|
||||||
toast(i18n.t('reply_sent'));
|
if (
|
||||||
// this.state.comments.unshift(res.comment); // TODO do this right
|
UserService.Instance.user &&
|
||||||
// this.setState(this.state);
|
data.comment.creator_id == UserService.Instance.user.id
|
||||||
|
) {
|
||||||
|
toast(i18n.t('reply_sent'));
|
||||||
|
}
|
||||||
} else if (res.op == UserOperation.SaveComment) {
|
} else if (res.op == UserOperation.SaveComment) {
|
||||||
let data = res.data as CommentResponse;
|
let data = res.data as CommentResponse;
|
||||||
let found = this.state.comments.find(c => c.id == data.comment.id);
|
let found = this.state.comments.find(c => c.id == data.comment.id);
|
||||||
|
|
4
ui/src/index.tsx
vendored
4
ui/src/index.tsx
vendored
|
@ -41,7 +41,7 @@ class Index extends Component<any, any> {
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route exact path={`/`} component={Main} />
|
<Route exact path={`/`} component={Main} />
|
||||||
<Route
|
<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}
|
component={Main}
|
||||||
/>
|
/>
|
||||||
<Route path={`/login`} component={Login} />
|
<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/comment/:comment_id`} component={Post} />
|
||||||
<Route path={`/post/:id`} component={Post} />
|
<Route path={`/post/:id`} component={Post} />
|
||||||
<Route
|
<Route
|
||||||
path={`/c/:name/sort/:sort/page/:page`}
|
path={`/c/:name/data_type/:data_type/sort/:sort/page/:page`}
|
||||||
component={Community}
|
component={Community}
|
||||||
/>
|
/>
|
||||||
<Route path={`/community/:id`} component={Community} />
|
<Route path={`/community/:id`} component={Community} />
|
||||||
|
|
22
ui/src/interfaces.ts
vendored
22
ui/src/interfaces.ts
vendored
|
@ -42,6 +42,7 @@ export enum UserOperation {
|
||||||
EditPrivateMessage,
|
EditPrivateMessage,
|
||||||
GetPrivateMessages,
|
GetPrivateMessages,
|
||||||
UserJoin,
|
UserJoin,
|
||||||
|
GetComments,
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum CommentSortType {
|
export enum CommentSortType {
|
||||||
|
@ -57,6 +58,11 @@ export enum ListingType {
|
||||||
Community,
|
Community,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum DataType {
|
||||||
|
Post,
|
||||||
|
Comment,
|
||||||
|
}
|
||||||
|
|
||||||
export enum SortType {
|
export enum SortType {
|
||||||
Hot,
|
Hot,
|
||||||
New,
|
New,
|
||||||
|
@ -187,6 +193,7 @@ export interface Comment {
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
community_id: number;
|
community_id: number;
|
||||||
|
community_name: string;
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
banned_from_community: boolean;
|
banned_from_community: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
@ -194,8 +201,10 @@ export interface Comment {
|
||||||
score: number;
|
score: number;
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
|
hot_rank: number;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
|
subscribed?: number;
|
||||||
saved?: boolean;
|
saved?: boolean;
|
||||||
user_mention_id?: number; // For mention type
|
user_mention_id?: number; // For mention type
|
||||||
recipient_id?: number;
|
recipient_id?: number;
|
||||||
|
@ -659,6 +668,19 @@ export interface GetPostsResponse {
|
||||||
posts: Array<Post>;
|
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 {
|
export interface CreatePostLikeForm {
|
||||||
post_id: number;
|
post_id: number;
|
||||||
score: number;
|
score: number;
|
||||||
|
|
6
ui/src/services/WebSocketService.ts
vendored
6
ui/src/services/WebSocketService.ts
vendored
|
@ -38,6 +38,7 @@ import {
|
||||||
PrivateMessageForm,
|
PrivateMessageForm,
|
||||||
EditPrivateMessageForm,
|
EditPrivateMessageForm,
|
||||||
GetPrivateMessagesForm,
|
GetPrivateMessagesForm,
|
||||||
|
GetCommentsForm,
|
||||||
UserJoinForm,
|
UserJoinForm,
|
||||||
MessageType,
|
MessageType,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
@ -172,6 +173,11 @@ export class WebSocketService {
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.GetPosts, form));
|
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) {
|
public likePost(form: CreatePostLikeForm) {
|
||||||
this.setAuth(form);
|
this.setAuth(form);
|
||||||
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, 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_like_comment: "Couldn't like comment.",
|
||||||
couldnt_update_comment: "Couldn't update comment.",
|
couldnt_update_comment: "Couldn't update comment.",
|
||||||
couldnt_save_comment: "Couldn't save 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_comment_edit_allowed: 'Not allowed to edit comment.',
|
||||||
no_post_edit_allowed: 'Not allowed to edit post.',
|
no_post_edit_allowed: 'Not allowed to edit post.',
|
||||||
no_community_edit_allowed: 'Not allowed to edit community.',
|
no_community_edit_allowed: 'Not allowed to edit community.',
|
||||||
|
|
32
ui/src/utils.ts
vendored
32
ui/src/utils.ts
vendored
|
@ -19,6 +19,7 @@ import {
|
||||||
User,
|
User,
|
||||||
SortType,
|
SortType,
|
||||||
ListingType,
|
ListingType,
|
||||||
|
DataType,
|
||||||
SearchType,
|
SearchType,
|
||||||
WebSocketResponse,
|
WebSocketResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
|
@ -198,6 +199,10 @@ export function routeListingTypeToEnum(type: string): ListingType {
|
||||||
return ListingType[capitalizeFirstLetter(type)];
|
return ListingType[capitalizeFirstLetter(type)];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function routeDataTypeToEnum(type: string): DataType {
|
||||||
|
return DataType[capitalizeFirstLetter(type)];
|
||||||
|
}
|
||||||
|
|
||||||
export function routeSearchTypeToEnum(type: string): SearchType {
|
export function routeSearchTypeToEnum(type: string): SearchType {
|
||||||
return SearchType[capitalizeFirstLetter(type)];
|
return SearchType[capitalizeFirstLetter(type)];
|
||||||
}
|
}
|
||||||
|
@ -519,3 +524,30 @@ function communitySearch(text: string, cb: any) {
|
||||||
cb([]);
|
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;
|
||||||
|
}
|
||||||
|
|
Loading…
Reference in a new issue