Merge branch 'master' into migrate-apub-lib

This commit is contained in:
Dessalines 2020-07-08 12:13:17 -04:00
commit d720993141
50 changed files with 2490 additions and 506 deletions

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.7.11 v0.7.13

View file

@ -12,7 +12,7 @@ services:
restart: always restart: always
lemmy: lemmy:
image: dessalines/lemmy:v0.7.11 image: dessalines/lemmy:v0.7.13
ports: ports:
- "127.0.0.1:8536:8536" - "127.0.0.1:8536:8536"
restart: always restart: always

View file

@ -7,7 +7,7 @@ can copy the options you want to change into your local `config.hjson` file.
Additionally, you can override any config files with environment variables. These have the same Additionally, you can override any config files with environment variables. These have the same
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the name as the config options, and are prefixed with `LEMMY_`. For example, you can override the
`database.password` with `LEMMY__DATABASE__POOL_SIZE=10`. `database.password` with `LEMMY_DATABASE__POOL_SIZE=10`.
An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL An additional option `LEMMY_DATABASE_URL` is available, which can be used with a PostgreSQL
connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection connection string like `postgres://lemmy:password@lemmy_db:5432/lemmy`, passing all connection

View file

@ -3,30 +3,52 @@
- A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts. - A group of lemmy developers and users that use a well-defined democratic process to steer the project in a positive direction, keep it aligned to community goals, and resolve conflicts.
- Council members are also added as administrators to any official Lemmy instances. - Council members are also added as administrators to any official Lemmy instances.
## Voting / Decision-Making ## 1. What gets voted on
### Process This section describes all the aspects of Lemmy where the council has decision making power, namely:
- Anything is open for discussion
- Voting done through matrix chat reacts (thumbs up/thumbs down)
- Require a simple majority for votes. (Maybe 2/3rds for more debated decisions).
- Once a decision is reached democratically, the dicision is binding and all group members have to follow it
- All members of the Lemmy council have equal voting power.
- Voting must stay open for at least 2 days.
### What gets voted on
- Membership (joining, removing)
- Coding direction - Coding direction
- Priorities / Emphasis - Priorities / Emphasis
- Controversial features (For example, an unpopular feature should be removed) - Controversial features (For example, an unpopular feature should be removed)
- Communication mediums - Moderation and conflict resolution on:
- Conflict resolution - [dev.lemmy.ml](https://dev.lemmy.ml/)
- dev.lemmy.ml (domain and server) - [github.com/LemmyNet/lemmy](https://github.com/LemmyNet/lemmy)
- lemmy.ml and subdomains (excluding communism.lemmy.ml) - [yerbamate.dev/LemmyNet/lemmy](https://yerbamate.dev/LemmyNet/lemmy)
- git repo including mirrors (on github, gitea, etc) - [weblate.yerbamate.dev/projects/lemmy/](https://weblate.yerbamate.dev/projects/lemmy/)
- Any official accounts of the Lemmy project, for example the Mastodon account or the Liberapay account - Technical administration of dev.lemmy.ml
- Official Lemmy accounts
- [Mastodon](https://mastodon.social/@LemmyDev)
- [Liberapay](https://liberapay.com/Lemmy/)
- [Patreon](https://www.patreon.com/dessalines)
- Council membership changes
- Changes to these rules - Changes to these rules
## Joining ## 2. Feedback and Activity Reports
Every week, the council should make a thread on Lemmy that details its activity during the past week, be it development, moderation, or anything else mentioned in 1.
At the same time, users can give feedback and suggestions in this thread. This should be taken into account by the council. Council members can call for a vote on any controversial issues, if they can't be resolved by discussion.
## 2. Voting Process
Most of the time, we keep each other up to date through the Matrix chat, and take informal decisions on uncontroversial issues. For example, a user clearly violating the site rules could be banned by a single person, or ideally after discussing it with at least one other member.
If an issue can not be resolved in this way, then any council member can call for a vote, which works in the following way:
- Any council member can call for a vote, on any topic mentioned in 1.
- This should be used if there is any controversy in the community, or between council members.
- Before taking any decision, there needs to be a discussion where every council member can
explain their position.
- Discussion should be taken with the goal of reaching a compromise that is acceptable for
everyone.
- After the discussion, voting is done through Matrix emojis (👍: yes, 👎: no, X: abstain) and must
stay open for at least two days.
- All members of the Lemmy council have equal voting power.
- Decisions should be reached unanimously, or nearly so. If this is not possible, at least
2/3 of votes must be in favour for the motion to pass.
- Once a decision is reached in this way, every member needs to abide by it.
## 4. Joining
- We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council. - We use the following process: anyone who is active around Lemmy can recommend any other active person to join the council. This has to be approved by a majority of the council.
- Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole. - Active users are defined as those who contribute to Lemmy in some way for at least an hour per week on average, doing things like reporting bugs, discussing rules and features, translating, promoting, developing, or doing other things that aim to improve Lemmy as a whole.
-> people should have joined at least a month ago. -> people should have joined at least a month ago.
@ -34,23 +56,24 @@
- Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote. - Note: we would like to have a process where community members can elect candidates for the council, but this is not realistic because a single user could easily create multiple accounts and cheat the vote.
- Limit growth to one new member per month at most. - Limit growth to one new member per month at most.
## Removing members ## 5. Removing members
- Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this. - Inactive members should be removed from the council after a few months of inactivity, and after receiving a notification about this.
- Members that dont follow binding council decisions should be removed. - Members that dont follow binding council decisions should be removed.
- Any member can be removed in a vote. - Any member can be removed in a vote.
## Goals ## 6. Goals
- We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups. - We encourage the membership of groups such as LGBT, religious or ethnic minorities, abuse victims, etc etc, and strive to create a safe space for them to express their opinions. We also support measures to increase participation by the previously mentioned groups.
- The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc, - The following are banned, and will always be harshly punished: fascism, abuse, racism, sexism, etc etc,
## Communication ## 7. Communication
- A private Matrix chat for all council members. - A private Matrix chat for all council members.
- (Once private communities are done) A private community on dev.lemmy.ml for issues. - (Once private communities are done) A private community on dev.lemmy.ml for issues.
## Member List / Contact Info ## 8. Member List / Contact Info
General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev) General Contact [@LemmyDev Mastodon](https://mastodon.social/@LemmyDev)
- [Dessalines](https://dev.lemmy.ml/u/dessalines) - [Dessalines](https://dev.lemmy.ml/u/dessalines)
- [Nutomic](https://dev.lemmy.ml/u/nutomic) - [Nutomic](https://dev.lemmy.ml/u/nutomic)
- [AgreeableLandscape](https://dev.lemmy.ml/u/AgreeableLandscape) - [AgreeableLandscape](https://dev.lemmy.ml/u/AgreeableLandscape)
- [fruechtchen](https://dev.lemmy.ml/u/fruechtchen) - [fruechtchen](https://dev.lemmy.ml/u/fruechtchen)
- [kixiQu](https://dev.lemmy.ml/u/kixiQu)

View file

@ -0,0 +1,535 @@
-- Dropping all the fast tables
drop table user_fast;
drop view post_fast_view;
drop table post_aggregates_fast;
drop view community_fast_view;
drop table community_aggregates_fast;
drop view reply_fast_view;
drop view user_mention_fast_view;
drop view comment_fast_view;
drop table comment_aggregates_fast;
-- Re-adding all the triggers, functions, and mviews
-- private message
create materialized view private_message_mview as select * from private_message_view;
create unique index idx_private_message_mview_id on private_message_mview (id);
-- Create the triggers
create or replace function refresh_private_message()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently private_message_mview;
return null;
end $$;
create trigger refresh_private_message
after insert or update or delete or truncate
on private_message
for each statement
execute procedure refresh_private_message();
-- user
create or replace function refresh_user()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently user_mview;
refresh materialized view concurrently comment_aggregates_mview; -- cause of bans
refresh materialized view concurrently post_aggregates_mview;
return null;
end $$;
drop trigger refresh_user on user_;
create trigger refresh_user
after insert or update or delete or truncate
on user_
for each statement
execute procedure refresh_user();
drop view user_view cascade;
create view user_view as
select
u.id,
u.actor_id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.bio,
u.local,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
from user_ u;
create materialized view user_mview as select * from user_view;
create unique index idx_user_mview_id on user_mview (id);
-- community
drop trigger refresh_community on community;
create trigger refresh_community
after insert or update or delete or truncate
on community
for each statement
execute procedure refresh_community();
create or replace function refresh_community()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently post_aggregates_mview;
refresh materialized view concurrently community_aggregates_mview;
refresh materialized view concurrently user_mview;
return null;
end $$;
drop view community_aggregates_view cascade;
create view community_aggregates_view as
-- Now that there's public and private keys, you have to be explicit here
select c.id,
c.name,
c.title,
c.description,
c.category_id,
c.creator_id,
c.removed,
c.published,
c.updated,
c.deleted,
c.nsfw,
c.actor_id,
c.local,
c.last_refreshed_at,
(select actor_id from user_ u where c.creator_id = u.id) as creator_actor_id,
(select local from user_ u where c.creator_id = u.id) as creator_local,
(select name from user_ u where c.creator_id = u.id) as creator_name,
(select avatar from user_ u where c.creator_id = u.id) as creator_avatar,
(select name from category ct where c.category_id = ct.id) as category_name,
(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
(select count(*) from post p where p.community_id = c.id) as number_of_posts,
(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments,
hot_rank((select count(*) from community_follower cf where cf.community_id = c.id), c.published) as hot_rank
from community c;
create materialized view community_aggregates_mview as select * from community_aggregates_view;
create unique index idx_community_aggregates_mview_id on community_aggregates_mview (id);
create view community_view as
with all_community as
(
select
ca.*
from community_aggregates_view ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
create view community_mview as
with all_community as
(
select
ca.*
from community_aggregates_mview ca
)
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join all_community ac
union all
select
ac.*,
null as user_id,
null as subscribed
from all_community ac
;
-- Post
drop view post_view;
drop view post_aggregates_view;
-- regen post view
create view post_aggregates_view as
select
p.*,
(select u.banned from user_ u where p.creator_id = u.id) as banned,
(select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community,
(select actor_id from user_ where p.creator_id = user_.id) as creator_actor_id,
(select local from user_ where p.creator_id = user_.id) as creator_local,
(select name from user_ where p.creator_id = user_.id) as creator_name,
(select avatar from user_ where p.creator_id = user_.id) as creator_avatar,
(select actor_id from community where p.community_id = community.id) as community_actor_id,
(select local from community where p.community_id = community.id) as community_local,
(select name from community where p.community_id = community.id) as community_name,
(select removed from community c where p.community_id = c.id) as community_removed,
(select deleted from community c where p.community_id = c.id) as community_deleted,
(select nsfw from community c where p.community_id = c.id) as community_nsfw,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
count (case when pl.score = 1 then 1 else null end) as upvotes,
count (case when pl.score = -1 then 1 else null end) as downvotes,
hot_rank(coalesce(sum(pl.score) , 0),
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case when (p.published < ('now'::timestamp - '1 month'::interval)) then p.published -- Prevents necro-bumps
else greatest(c.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join post_like pl on p.id = pl.post_id
left join (
select post_id,
max(published) as recent_comment_time
from comment
group by 1
) c on p.id = c.post_id
group by p.id, c.recent_comment_time;
create materialized view post_aggregates_mview as select * from post_aggregates_view;
create unique index idx_post_aggregates_mview_id on post_aggregates_mview (id);
create view post_view as
with all_post as (
select
pa.*
from post_aggregates_view pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
create view post_mview as
with all_post as (
select
pa.*
from post_aggregates_mview pa
)
select
ap.*,
u.id as user_id,
coalesce(pl.score, 0) as my_vote,
(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed,
(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read,
(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
union all
select
ap.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from all_post ap
;
drop trigger refresh_post on post;
create trigger refresh_post
after insert or update or delete or truncate
on post
for each statement
execute procedure refresh_post();
create or replace function refresh_post()
returns trigger language plpgsql
as $$
begin
refresh materialized view concurrently post_aggregates_mview;
refresh materialized view concurrently user_mview;
return null;
end $$;
-- User mention, comment, reply
drop view user_mention_view;
drop view comment_view;
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.actor_id from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_actor_id,
(select co.local from post p, community co where p.id = c.post_id and p.community_id = co.id) as community_local,
(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 actor_id from user_ where c.creator_id = user_.id) as creator_actor_id,
(select local from user_ where c.creator_id = user_.id) as creator_local,
(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.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
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.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
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.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from all_comment ac
left join user_mention um on um.comment_id = ac.id
;

View file

@ -0,0 +1,939 @@
-- Drop the mviews
drop view post_mview;
drop materialized view user_mview;
drop view community_mview;
drop materialized view private_message_mview;
drop view user_mention_mview;
drop view reply_view;
drop view comment_mview;
drop materialized view post_aggregates_mview;
drop materialized view community_aggregates_mview;
drop materialized view comment_aggregates_mview;
drop trigger refresh_private_message on private_message;
-- User
drop view user_view;
create view user_view as
select
u.id,
u.actor_id,
u.name,
u.avatar,
u.email,
u.matrix_user_id,
u.bio,
u.local,
u.admin,
u.banned,
u.show_avatars,
u.send_notifications_to_email,
u.published,
coalesce(pd.posts, 0) as number_of_posts,
coalesce(pd.score, 0) as post_score,
coalesce(cd.comments, 0) as number_of_comments,
coalesce(cd.score, 0) as comment_score
from user_ u
left join (
select
p.creator_id as creator_id,
count(distinct p.id) as posts,
sum(pl.score) as score
from post p
join post_like pl on p.id = pl.post_id
group by p.creator_id
) pd on u.id = pd.creator_id
left join (
select
c.creator_id,
count(distinct c.id) as comments,
sum(cl.score) as score
from comment c
join comment_like cl on c.id = cl.comment_id
group by c.creator_id
) cd on u.id = cd.creator_id;
create table user_fast as select * from user_view;
alter table user_fast add primary key (id);
drop trigger refresh_user on user_;
create trigger refresh_user
after insert or update or delete
on user_
for each row
execute procedure refresh_user();
-- Sample insert
-- insert into user_(name, password_encrypted) values ('test_name', 'bleh');
-- Sample delete
-- delete from user_ where name like 'test_name';
-- Sample update
-- update user_ set avatar = 'hai' where name like 'test_name';
create or replace function refresh_user()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from user_fast where id = OLD.id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from user_fast where id = OLD.id;
insert into user_fast select * from user_view where id = NEW.id;
-- Refresh post_fast, cause of user info changes
delete from post_aggregates_fast where creator_id = NEW.id;
insert into post_aggregates_fast select * from post_aggregates_view where creator_id = NEW.id;
delete from comment_aggregates_fast where creator_id = NEW.id;
insert into comment_aggregates_fast select * from comment_aggregates_view where creator_id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into user_fast select * from user_view where id = NEW.id;
END IF;
return null;
end $$;
-- Post
-- Redoing the views : Credit eiknat
drop view post_view;
drop view post_aggregates_view;
create view post_aggregates_view as
select
p.*,
-- creator details
u.actor_id as creator_actor_id,
u."local" as creator_local,
u."name" as creator_name,
u.avatar as creator_avatar,
u.banned as banned,
cb.id::bool as banned_from_community,
-- community details
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
c.removed as community_removed,
c.deleted as community_deleted,
c.nsfw as community_nsfw,
-- post score data/comment count
coalesce(ct.comments, 0) as number_of_comments,
coalesce(pl.score, 0) as score,
coalesce(pl.upvotes, 0) as upvotes,
coalesce(pl.downvotes, 0) as downvotes,
hot_rank(
coalesce(pl.score , 0), (
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
)
) as hot_rank,
(
case
when (p.published < ('now'::timestamp - '1 month'::interval))
then p.published
else greatest(ct.recent_comment_time, p.published)
end
) as newest_activity_time
from post p
left join user_ u on p.creator_id = u.id
left join community_user_ban cb on p.creator_id = cb.user_id and p.community_id = cb.community_id
left join community c on p.community_id = c.id
left join (
select
post_id,
count(*) as comments,
max(published) as recent_comment_time
from comment
group by post_id
) ct on ct.post_id = p.id
left join (
select
post_id,
sum(score) as score,
sum(score) filter (where score = 1) as upvotes,
-sum(score) filter (where score = -1) as downvotes
from post_like
group by post_id
) pl on pl.post_id = p.id
order by p.id;
create view post_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_view pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_view pav;
-- The post fast table
create table post_aggregates_fast as select * from post_aggregates_view;
alter table post_aggregates_fast add primary key (id);
-- For the hot rank resorting
create index idx_post_aggregates_fast_hot_rank_published on post_aggregates_fast (hot_rank desc, published desc);
create view post_fast_view as
select
pav.*,
us.id as user_id,
us.user_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_read::bool as read,
us.is_saved::bool as saved
from post_aggregates_fast pav
cross join lateral (
select
u.id,
coalesce(cf.community_id, 0) as is_subbed,
coalesce(pr.post_id, 0) as is_read,
coalesce(ps.post_id, 0) as is_saved,
coalesce(pl.score, 0) as user_vote
from user_ u
left join community_user_ban cb on u.id = cb.user_id and cb.community_id = pav.community_id
left join community_follower cf on u.id = cf.user_id and cf.community_id = pav.community_id
left join post_read pr on u.id = pr.user_id and pr.post_id = pav.id
left join post_saved ps on u.id = ps.user_id and ps.post_id = pav.id
left join post_like pl on u.id = pl.user_id and pav.id = pl.post_id
) as us
union all
select
pav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as read,
null as saved
from post_aggregates_fast pav;
drop trigger refresh_post on post;
create trigger refresh_post
after insert or update or delete
on post
for each row
execute procedure refresh_post();
-- Sample select
-- select id, name from post_fast_view where name like 'test_post' and user_id is null;
-- Sample insert
-- insert into post(name, creator_id, community_id) values ('test_post', 2, 2);
-- Sample delete
-- delete from post where name like 'test_post';
-- Sample update
-- update post set community_id = 4 where name like 'test_post';
create or replace function refresh_post()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from post_aggregates_fast where id = OLD.id;
-- Update community number of posts
update community_aggregates_fast set number_of_posts = number_of_posts - 1 where id = OLD.community_id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from post_aggregates_fast where id = OLD.id;
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.id;
-- Update that users number of posts, post score
delete from user_fast where id = NEW.creator_id;
insert into user_fast select * from user_view where id = NEW.creator_id;
-- Update community number of posts
update community_aggregates_fast set number_of_posts = number_of_posts + 1 where id = NEW.community_id;
-- Update the hot rank on the post table
-- TODO this might not correctly update it, using a 1 week interval
update post_aggregates_fast as paf
set hot_rank = pav.hot_rank
from post_aggregates_view as pav
where paf.id = pav.id and (pav.published > ('now'::timestamp - '1 week'::interval));
END IF;
return null;
end $$;
-- Community
-- Redoing the views : Credit eiknat
drop view community_moderator_view;
drop view community_follower_view;
drop view community_user_ban_view;
drop view community_view;
drop view community_aggregates_view;
create view community_aggregates_view as
select
c.id,
c.name,
c.title,
c.description,
c.category_id,
c.creator_id,
c.removed,
c.published,
c.updated,
c.deleted,
c.nsfw,
c.actor_id,
c.local,
c.last_refreshed_at,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
cat.name as category_name,
coalesce(cf.subs, 0) as number_of_subscribers,
coalesce(cd.posts, 0) as number_of_posts,
coalesce(cd.comments, 0) as number_of_comments,
hot_rank(cf.subs, c.published) as hot_rank
from community c
left join user_ u on c.creator_id = u.id
left join category cat on c.category_id = cat.id
left join (
select
p.community_id,
count(distinct p.id) as posts,
count(distinct ct.id) as comments
from post p
join comment ct on p.id = ct.post_id
group by p.community_id
) cd on cd.community_id = c.id
left join (
select
community_id,
count(*) as subs
from community_follower
group by community_id
) cf on cf.community_id = c.id;
create view community_view as
select
cv.*,
us.user as user_id,
us.is_subbed::bool as subscribed
from community_aggregates_view cv
cross join lateral (
select
u.id as user,
coalesce(cf.community_id, 0) as is_subbed
from user_ u
left join community_follower cf on u.id = cf.user_id and cf.community_id = cv.id
) as us
union all
select
cv.*,
null as user_id,
null as subscribed
from community_aggregates_view cv;
create view community_moderator_view as
select
cm.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_moderator cm
left join user_ u on cm.user_id = u.id
left join community c on cm.community_id = c.id;
create view community_follower_view as
select
cf.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_follower cf
left join user_ u on cf.user_id = u.id
left join community c on cf.community_id = c.id;
create view community_user_ban_view as
select
cb.*,
u.actor_id as user_actor_id,
u.local as user_local,
u.name as user_name,
u.avatar as avatar,
c.actor_id as community_actor_id,
c.local as community_local,
c.name as community_name
from community_user_ban cb
left join user_ u on cb.user_id = u.id
left join community c on cb.community_id = c.id;
-- The community fast table
create table community_aggregates_fast as select * from community_aggregates_view;
alter table community_aggregates_fast add primary key (id);
create view community_fast_view as
select
ac.*,
u.id as user_id,
(select cf.id::boolean from community_follower cf where u.id = cf.user_id and ac.id = cf.community_id) as subscribed
from user_ u
cross join (
select
ca.*
from community_aggregates_fast ca
) ac
union all
select
caf.*,
null as user_id,
null as subscribed
from community_aggregates_fast caf;
drop trigger refresh_community on community;
create trigger refresh_community
after insert or update or delete
on community
for each row
execute procedure refresh_community();
-- Sample select
-- select * from community_fast_view where name like 'test_community_name' and user_id is null;
-- Sample insert
-- insert into community(name, title, category_id, creator_id) values ('test_community_name', 'test_community_title', 1, 2);
-- Sample delete
-- delete from community where name like 'test_community_name';
-- Sample update
-- update community set title = 'test_community_title_2' where name like 'test_community_name';
create or replace function refresh_community()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from community_aggregates_fast where id = OLD.id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from community_aggregates_fast where id = OLD.id;
insert into community_aggregates_fast select * from community_aggregates_view where id = NEW.id;
-- Update user view due to owner changes
delete from user_fast where id = NEW.creator_id;
insert into user_fast select * from user_view where id = NEW.creator_id;
-- Update post view due to community changes
delete from post_aggregates_fast where community_id = NEW.id;
insert into post_aggregates_fast select * from post_aggregates_view where community_id = NEW.id;
-- TODO make sure this shows up in the users page ?
ELSIF (TG_OP = 'INSERT') THEN
insert into community_aggregates_fast select * from community_aggregates_view where id = NEW.id;
END IF;
return null;
end $$;
-- Comment
drop view user_mention_view;
drop view comment_view;
drop view comment_aggregates_view;
create view comment_aggregates_view as
select
ct.*,
-- community details
p.community_id,
c.actor_id as community_actor_id,
c."local" as community_local,
c."name" as community_name,
-- creator details
u.banned as banned,
coalesce(cb.id, 0)::bool as banned_from_community,
u.actor_id as creator_actor_id,
u.local as creator_local,
u.name as creator_name,
u.avatar as creator_avatar,
-- score details
coalesce(cl.total, 0) as score,
coalesce(cl.up, 0) as upvotes,
coalesce(cl.down, 0) as downvotes,
hot_rank(coalesce(cl.total, 0), ct.published) as hot_rank
from comment ct
left join post p on ct.post_id = p.id
left join community c on p.community_id = c.id
left join user_ u on ct.creator_id = u.id
left join community_user_ban cb on ct.creator_id = cb.user_id and p.id = ct.post_id and p.community_id = cb.community_id
left join (
select
l.comment_id as id,
sum(l.score) as total,
count(case when l.score = 1 then 1 else null end) as up,
count(case when l.score = -1 then 1 else null end) as down
from comment_like l
group by comment_id
) as cl on cl.id = ct.id;
create or replace view comment_view as (
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_view cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_view cav
);
-- The fast view
create table comment_aggregates_fast as select * from comment_aggregates_view;
alter table comment_aggregates_fast add primary key (id);
create view comment_fast_view as
select
cav.*,
us.user_id as user_id,
us.my_vote as my_vote,
us.is_subbed::bool as subscribed,
us.is_saved::bool as saved
from comment_aggregates_fast cav
cross join lateral (
select
u.id as user_id,
coalesce(cl.score, 0) as my_vote,
coalesce(cf.id, 0) as is_subbed,
coalesce(cs.id, 0) as is_saved
from user_ u
left join comment_like cl on u.id = cl.user_id and cav.id = cl.comment_id
left join comment_saved cs on u.id = cs.user_id and cs.comment_id = cav.id
left join community_follower cf on u.id = cf.user_id and cav.community_id = cf.community_id
) as us
union all
select
cav.*,
null as user_id,
null as my_vote,
null as subscribed,
null as saved
from comment_aggregates_fast cav;
-- Do the reply_view referencing the comment_fast_view
create view reply_fast_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_fast_view 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.creator_actor_id,
c.creator_local,
c.post_id,
c.parent_id,
c.content,
c.removed,
um.read,
c.published,
c.updated,
c.deleted,
c.community_id,
c.community_actor_id,
c.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_mention um, comment_view c
where um.comment_id = c.id;
create view user_mention_fast_view as
select
ac.id,
um.id as user_mention_id,
ac.creator_id,
ac.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from user_ u
cross join (
select
ca.*
from comment_aggregates_fast ca
) 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.creator_actor_id,
ac.creator_local,
ac.post_id,
ac.parent_id,
ac.content,
ac.removed,
um.read,
ac.published,
ac.updated,
ac.deleted,
ac.community_id,
ac.community_actor_id,
ac.community_local,
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,
(select actor_id from user_ u where u.id = um.recipient_id) as recipient_actor_id,
(select local from user_ u where u.id = um.recipient_id) as recipient_local
from comment_aggregates_fast ac
left join user_mention um on um.comment_id = ac.id
;
drop trigger refresh_comment on comment;
create trigger refresh_comment
after insert or update or delete
on comment
for each row
execute procedure refresh_comment();
-- Sample select
-- select * from comment_fast_view where content = 'test_comment' and user_id is null;
-- Sample insert
-- insert into comment(creator_id, post_id, content) values (2, 2, 'test_comment');
-- Sample delete
-- delete from comment where content like 'test_comment';
-- Sample update
-- update comment set removed = true where content like 'test_comment';
create or replace function refresh_comment()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
delete from comment_aggregates_fast where id = OLD.id;
-- Update community number of comments
update community_aggregates_fast as caf
set number_of_comments = number_of_comments - 1
from post as p
where caf.id = p.community_id and p.id = OLD.post_id;
ELSIF (TG_OP = 'UPDATE') THEN
delete from comment_aggregates_fast where id = OLD.id;
insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
ELSIF (TG_OP = 'INSERT') THEN
insert into comment_aggregates_fast select * from comment_aggregates_view where id = NEW.id;
-- Update user view due to comment count
update user_fast
set number_of_comments = number_of_comments + 1
where id = NEW.creator_id;
-- Update post view due to comment count, new comment activity time, but only on new posts
-- TODO this could be done more efficiently
delete from post_aggregates_fast where id = NEW.post_id;
insert into post_aggregates_fast select * from post_aggregates_view where id = NEW.post_id;
-- Force the hot rank as zero on week-older posts
update post_aggregates_fast as paf
set hot_rank = 0
where paf.id = NEW.post_id and (paf.published < ('now'::timestamp - '1 week'::interval));
-- Update community number of comments
update community_aggregates_fast as caf
set number_of_comments = number_of_comments + 1
from post as p
where caf.id = p.community_id and p.id = NEW.post_id;
END IF;
return null;
end $$;
-- post_like
-- select id, score, my_vote from post_fast_view where id = 29 and user_id = 4;
-- Sample insert
-- insert into post_like(user_id, post_id, score) values (4, 29, 1);
-- Sample delete
-- delete from post_like where user_id = 4 and post_id = 29;
-- Sample update
-- update post_like set score = -1 where user_id = 4 and post_id = 29;
-- TODO test this a LOT
create or replace function refresh_post_like()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
update post_aggregates_fast
set score = case
when (OLD.score = 1) then score - 1
else score + 1 end,
upvotes = case
when (OLD.score = 1) then upvotes - 1
else upvotes end,
downvotes = case
when (OLD.score = -1) then downvotes - 1
else downvotes end
where id = OLD.post_id;
ELSIF (TG_OP = 'INSERT') THEN
update post_aggregates_fast
set score = case
when (NEW.score = 1) then score + 1
else score - 1 end,
upvotes = case
when (NEW.score = 1) then upvotes + 1
else upvotes end,
downvotes = case
when (NEW.score = -1) then downvotes + 1
else downvotes end
where id = NEW.post_id;
END IF;
return null;
end $$;
drop trigger refresh_post_like on post_like;
create trigger refresh_post_like
after insert or delete
on post_like
for each row
execute procedure refresh_post_like();
-- comment_like
-- select id, score, my_vote from comment_fast_view where id = 29 and user_id = 4;
-- Sample insert
-- insert into comment_like(user_id, comment_id, post_id, score) values (4, 29, 51, 1);
-- Sample delete
-- delete from comment_like where user_id = 4 and comment_id = 29;
-- Sample update
-- update comment_like set score = -1 where user_id = 4 and comment_id = 29;
create or replace function refresh_comment_like()
returns trigger language plpgsql
as $$
begin
-- TODO possibly select from comment_fast to get previous scores, instead of re-fetching the views?
IF (TG_OP = 'DELETE') THEN
update comment_aggregates_fast
set score = case
when (OLD.score = 1) then score - 1
else score + 1 end,
upvotes = case
when (OLD.score = 1) then upvotes - 1
else upvotes end,
downvotes = case
when (OLD.score = -1) then downvotes - 1
else downvotes end
where id = OLD.comment_id;
ELSIF (TG_OP = 'INSERT') THEN
update comment_aggregates_fast
set score = case
when (NEW.score = 1) then score + 1
else score - 1 end,
upvotes = case
when (NEW.score = 1) then upvotes + 1
else upvotes end,
downvotes = case
when (NEW.score = -1) then downvotes + 1
else downvotes end
where id = NEW.comment_id;
END IF;
return null;
end $$;
drop trigger refresh_comment_like on comment_like;
create trigger refresh_comment_like
after insert or delete
on comment_like
for each row
execute procedure refresh_comment_like();
-- Community user ban
drop trigger refresh_community_user_ban on community_user_ban;
create trigger refresh_community_user_ban
after insert or delete -- Note this is missing after update
on community_user_ban
for each row
execute procedure refresh_community_user_ban();
-- select creator_name, banned_from_community from comment_fast_view where user_id = 4 and content = 'test_before_ban';
-- select creator_name, banned_from_community, community_id from comment_aggregates_fast where content = 'test_before_ban';
-- Sample insert
-- insert into comment(creator_id, post_id, content) values (1198, 341, 'test_before_ban');
-- insert into community_user_ban(community_id, user_id) values (2, 1198);
-- Sample delete
-- delete from community_user_ban where user_id = 1198 and community_id = 2;
-- delete from comment where content = 'test_before_ban';
-- update comment_aggregates_fast set banned_from_community = false where creator_id = 1198 and community_id = 2;
create or replace function refresh_community_user_ban()
returns trigger language plpgsql
as $$
begin
-- TODO possibly select from comment_fast to get previous scores, instead of re-fetching the views?
IF (TG_OP = 'DELETE') THEN
update comment_aggregates_fast set banned_from_community = false where creator_id = OLD.user_id and community_id = OLD.community_id;
update post_aggregates_fast set banned_from_community = false where creator_id = OLD.user_id and community_id = OLD.community_id;
ELSIF (TG_OP = 'INSERT') THEN
update comment_aggregates_fast set banned_from_community = true where creator_id = NEW.user_id and community_id = NEW.community_id;
update post_aggregates_fast set banned_from_community = true where creator_id = NEW.user_id and community_id = NEW.community_id;
END IF;
return null;
end $$;
-- Community follower
drop trigger refresh_community_follower on community_follower;
create trigger refresh_community_follower
after insert or delete -- Note this is missing after update
on community_follower
for each row
execute procedure refresh_community_follower();
create or replace function refresh_community_follower()
returns trigger language plpgsql
as $$
begin
IF (TG_OP = 'DELETE') THEN
update community_aggregates_fast set number_of_subscribers = number_of_subscribers - 1 where id = OLD.community_id;
ELSIF (TG_OP = 'INSERT') THEN
update community_aggregates_fast set number_of_subscribers = number_of_subscribers + 1 where id = NEW.community_id;
END IF;
return null;
end $$;

View file

@ -1,31 +1,42 @@
#!/bin/bash #!/bin/bash
set -e set -e
# You can import these to http://tatiyants.com/pev/#/plans/new
# Do the views first # Do the views first
echo "explain (analyze, format json) select * from user_mview" > explain.sql echo "explain (analyze, format json) select * from user_fast" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_view.json psql -qAt -U lemmy -f explain.sql > user_fast.json
echo "explain (analyze, format json) select * from post_mview where user_id is null order by hot_rank desc, published desc" > explain.sql echo "explain (analyze, format json) select * from post_view where user_id is null order by hot_rank desc, published desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > post_view.json psql -qAt -U lemmy -f explain.sql > post_view.json
echo "explain (analyze, format json) select * from comment_mview where user_id is null" > explain.sql echo "explain (analyze, format json) select * from post_fast_view where user_id is null order by hot_rank desc, published desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > post_fast_view.json
echo "explain (analyze, format json) select * from comment_view where user_id is null" > explain.sql
psql -qAt -U lemmy -f explain.sql > comment_view.json psql -qAt -U lemmy -f explain.sql > comment_view.json
echo "explain (analyze, format json) select * from community_mview where user_id is null order by hot_rank desc" > explain.sql echo "explain (analyze, format json) select * from comment_fast_view where user_id is null" > explain.sql
psql -qAt -U lemmy -f explain.sql > comment_fast_view.json
echo "explain (analyze, format json) select * from community_view where user_id is null order by hot_rank desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > community_view.json psql -qAt -U lemmy -f explain.sql > community_view.json
echo "explain (analyze, format json) select * from community_fast_view where user_id is null order by hot_rank desc" > explain.sql
psql -qAt -U lemmy -f explain.sql > community_fast_view.json
echo "explain (analyze, format json) select * from site_view limit 1" > explain.sql echo "explain (analyze, format json) select * from site_view limit 1" > explain.sql
psql -qAt -U lemmy -f explain.sql > site_view.json psql -qAt -U lemmy -f explain.sql > site_view.json
echo "explain (analyze, format json) select * from reply_view where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from reply_fast_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > reply_view.json psql -qAt -U lemmy -f explain.sql > reply_fast_view.json
echo "explain (analyze, format json) select * from user_mention_view where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from user_mention_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_mention_view.json psql -qAt -U lemmy -f explain.sql > user_mention_view.json
echo "explain (analyze, format json) select * from user_mention_mview where user_id = 34 and recipient_id = 34" > explain.sql echo "explain (analyze, format json) select * from user_mention_fast_view where user_id = 34 and recipient_id = 34" > explain.sql
psql -qAt -U lemmy -f explain.sql > user_mention_mview.json psql -qAt -U lemmy -f explain.sql > user_mention_fast_view.json
grep "Execution Time" *.json grep "Execution Time" *.json

View file

@ -678,7 +678,8 @@ impl Perform for Oper<AddAdmin> {
} }
let added = data.added; let added = data.added;
let add_admin = move |conn: &'_ _| User_::add_admin(conn, user_id, added); let added_user_id = data.user_id;
let add_admin = move |conn: &'_ _| User_::add_admin(conn, added_user_id, added);
if blocking(pool, add_admin).await?.is_err() { if blocking(pool, add_admin).await?.is_err() {
return Err(APIError::err("couldnt_update_user").into()); return Err(APIError::err("couldnt_update_user").into());
} }

View file

@ -179,14 +179,10 @@ fn private_message_updates_2020_05_05(conn: &PgConnection) -> Result<(), LemmyEr
.filter(local.eq(true)) .filter(local.eq(true))
.load::<PrivateMessage>(conn)?; .load::<PrivateMessage>(conn)?;
sql_query("alter table private_message disable trigger refresh_private_message").execute(conn)?;
for cpm in &incorrect_pms { for cpm in &incorrect_pms {
PrivateMessage::update_ap_id(&conn, cpm.id)?; PrivateMessage::update_ap_id(&conn, cpm.id)?;
} }
sql_query("alter table private_message enable trigger refresh_private_message").execute(conn)?;
info!("{} private message rows updated.", incorrect_pms.len()); info!("{} private message rows updated.", incorrect_pms.len());
Ok(()) Ok(())

View file

@ -1,3 +1,4 @@
// TODO, remove the cross join here, just join to user directly
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -39,7 +40,7 @@ table! {
} }
table! { table! {
comment_mview (id) { comment_fast_view (id) {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
@ -76,7 +77,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "comment_view"] #[table_name = "comment_fast_view"]
pub struct CommentView { pub struct CommentView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
@ -112,7 +113,7 @@ pub struct CommentView {
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_fast_view::BoxedQuery<'a, Pg>,
listing_type: ListingType, listing_type: ListingType,
sort: &'a SortType, sort: &'a SortType,
for_community_id: Option<i32>, for_community_id: Option<i32>,
@ -127,9 +128,9 @@ pub struct CommentQueryBuilder<'a> {
impl<'a> CommentQueryBuilder<'a> { impl<'a> CommentQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let query = comment_mview.into_boxed(); let query = comment_fast_view.into_boxed();
CommentQueryBuilder { CommentQueryBuilder {
conn, conn,
@ -198,7 +199,7 @@ impl<'a> CommentQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<CommentView>, Error> { pub fn list(self) -> Result<Vec<CommentView>, Error> {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -270,8 +271,8 @@ impl CommentView {
from_comment_id: i32, from_comment_id: i32,
my_user_id: Option<i32>, my_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::comment_view::comment_mview::dsl::*; use super::comment_view::comment_fast_view::dsl::*;
let mut query = comment_mview.into_boxed(); let mut query = comment_fast_view.into_boxed();
// The view lets you pass a null user_id, if you're not logged in // The view lets you pass a null user_id, if you're not logged in
if let Some(my_user_id) = my_user_id { if let Some(my_user_id) = my_user_id {
@ -290,7 +291,7 @@ impl CommentView {
// The faked schema since diesel doesn't do views // The faked schema since diesel doesn't do views
table! { table! {
reply_view (id) { reply_fast_view (id) {
id -> Int4, id -> Int4,
creator_id -> Int4, creator_id -> Int4,
post_id -> Int4, post_id -> Int4,
@ -328,7 +329,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "reply_view"] #[table_name = "reply_fast_view"]
pub struct ReplyView { pub struct ReplyView {
pub id: i32, pub id: i32,
pub creator_id: i32, pub creator_id: i32,
@ -365,7 +366,7 @@ pub struct ReplyView {
pub struct ReplyQueryBuilder<'a> { pub struct ReplyQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::comment_view::reply_view::BoxedQuery<'a, Pg>, query: super::comment_view::reply_fast_view::BoxedQuery<'a, Pg>,
for_user_id: i32, for_user_id: i32,
sort: &'a SortType, sort: &'a SortType,
unread_only: bool, unread_only: bool,
@ -375,9 +376,9 @@ pub struct ReplyQueryBuilder<'a> {
impl<'a> ReplyQueryBuilder<'a> { impl<'a> ReplyQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self {
use super::comment_view::reply_view::dsl::*; use super::comment_view::reply_fast_view::dsl::*;
let query = reply_view.into_boxed(); let query = reply_fast_view.into_boxed();
ReplyQueryBuilder { ReplyQueryBuilder {
conn, conn,
@ -411,7 +412,7 @@ impl<'a> ReplyQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<ReplyView>, Error> { pub fn list(self) -> Result<Vec<ReplyView>, Error> {
use super::comment_view::reply_view::dsl::*; use super::comment_view::reply_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -615,8 +616,8 @@ mod tests {
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, subscribed: Some(false),
saved: None, saved: Some(false),
ap_id: "http://fake.com".to_string(), ap_id: "http://fake.com".to_string(),
local: true, local: true,
community_actor_id: inserted_community.actor_id.to_owned(), community_actor_id: inserted_community.actor_id.to_owned(),

View file

@ -1,4 +1,4 @@
use super::community_view::community_mview::BoxedQuery; use super::community_view::community_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{pg::Pg, result::Error, *}; use diesel::{pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -34,7 +34,7 @@ table! {
} }
table! { table! {
community_mview (id) { community_fast_view (id) {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
title -> Varchar, title -> Varchar,
@ -114,7 +114,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "community_view"] #[table_name = "community_fast_view"]
pub struct CommunityView { pub struct CommunityView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -156,9 +156,9 @@ pub struct CommunityQueryBuilder<'a> {
impl<'a> CommunityQueryBuilder<'a> { impl<'a> CommunityQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let query = community_mview.into_boxed(); let query = community_fast_view.into_boxed();
CommunityQueryBuilder { CommunityQueryBuilder {
conn, conn,
@ -203,7 +203,7 @@ impl<'a> CommunityQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<CommunityView>, Error> { pub fn list(self) -> Result<Vec<CommunityView>, Error> {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -259,9 +259,9 @@ impl CommunityView {
from_community_id: i32, from_community_id: i32,
from_user_id: Option<i32>, from_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::community_view::community_mview::dsl::*; use super::community_view::community_fast_view::dsl::*;
let mut query = community_mview.into_boxed(); let mut query = community_fast_view.into_boxed();
query = query.filter(id.eq(from_community_id)); query = query.filter(id.eq(from_community_id));

View file

@ -1,4 +1,4 @@
use super::post_view::post_mview::BoxedQuery; use super::post_view::post_fast_view::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType}; use crate::db::{fuzzy_search, limit_and_offset, ListingType, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -25,12 +25,12 @@ table! {
thumbnail_url -> Nullable<Text>, thumbnail_url -> Nullable<Text>,
ap_id -> Text, ap_id -> Text,
local -> Bool, local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
community_actor_id -> Text, community_actor_id -> Text,
community_local -> Bool, community_local -> Bool,
community_name -> Varchar, community_name -> Varchar,
@ -52,7 +52,7 @@ table! {
} }
table! { table! {
post_mview (id) { post_fast_view (id) {
id -> Int4, id -> Int4,
name -> Varchar, name -> Varchar,
url -> Nullable<Text>, url -> Nullable<Text>,
@ -72,12 +72,12 @@ table! {
thumbnail_url -> Nullable<Text>, thumbnail_url -> Nullable<Text>,
ap_id -> Text, ap_id -> Text,
local -> Bool, local -> Bool,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text, creator_actor_id -> Text,
creator_local -> Bool, creator_local -> Bool,
creator_name -> Varchar, creator_name -> Varchar,
creator_avatar -> Nullable<Text>, creator_avatar -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
community_actor_id -> Text, community_actor_id -> Text,
community_local -> Bool, community_local -> Bool,
community_name -> Varchar, community_name -> Varchar,
@ -101,7 +101,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "post_view"] #[table_name = "post_fast_view"]
pub struct PostView { pub struct PostView {
pub id: i32, pub id: i32,
pub name: String, pub name: String,
@ -122,12 +122,12 @@ pub struct PostView {
pub thumbnail_url: Option<String>, pub thumbnail_url: Option<String>,
pub ap_id: String, pub ap_id: String,
pub local: bool, pub local: bool,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String, pub creator_actor_id: String,
pub creator_local: bool, pub creator_local: bool,
pub creator_name: String, pub creator_name: String,
pub creator_avatar: Option<String>, pub creator_avatar: Option<String>,
pub banned: bool,
pub banned_from_community: bool,
pub community_actor_id: String, pub community_actor_id: String,
pub community_local: bool, pub community_local: bool,
pub community_name: String, pub community_name: String,
@ -166,9 +166,9 @@ pub struct PostQueryBuilder<'a> {
impl<'a> PostQueryBuilder<'a> { impl<'a> PostQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
let query = post_mview.into_boxed(); let query = post_fast_view.into_boxed();
PostQueryBuilder { PostQueryBuilder {
conn, conn,
@ -249,7 +249,7 @@ impl<'a> PostQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<PostView>, Error> { pub fn list(self) -> Result<Vec<PostView>, Error> {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -345,10 +345,10 @@ impl PostView {
from_post_id: i32, from_post_id: i32,
my_user_id: Option<i32>, my_user_id: Option<i32>,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::post_view::post_mview::dsl::*; use super::post_view::post_fast_view::dsl::*;
use diesel::prelude::*; use diesel::prelude::*;
let mut query = post_mview.into_boxed(); let mut query = post_fast_view.into_boxed();
query = query.filter(id.eq(from_post_id)); query = query.filter(id.eq(from_post_id));
@ -470,6 +470,25 @@ mod tests {
score: 1, score: 1,
}; };
let read_post_listings_with_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.my_user_id(inserted_user.id)
.list()
.unwrap();
let read_post_listings_no_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.list()
.unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_user =
PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
// the non user version // the non user version
let expected_post_listing_no_user = PostView { let expected_post_listing_no_user = PostView {
user_id: None, user_id: None,
@ -496,7 +515,7 @@ mod tests {
score: 1, score: 1,
upvotes: 1, upvotes: 1,
downvotes: 0, downvotes: 0,
hot_rank: 1728, hot_rank: read_post_listing_no_user.hot_rank,
published: inserted_post.published, published: inserted_post.published,
newest_activity_time: inserted_post.published, newest_activity_time: inserted_post.published,
updated: None, updated: None,
@ -541,13 +560,13 @@ mod tests {
score: 1, score: 1,
upvotes: 1, upvotes: 1,
downvotes: 0, downvotes: 0,
hot_rank: 1728, hot_rank: read_post_listing_with_user.hot_rank,
published: inserted_post.published, published: inserted_post.published,
newest_activity_time: inserted_post.published, newest_activity_time: inserted_post.published,
updated: None, updated: None,
subscribed: None, subscribed: Some(false),
read: None, read: Some(false),
saved: None, saved: Some(false),
nsfw: false, nsfw: false,
embed_title: None, embed_title: None,
embed_description: None, embed_description: None,
@ -561,25 +580,6 @@ mod tests {
community_local: true, community_local: true,
}; };
let read_post_listings_with_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.my_user_id(inserted_user.id)
.list()
.unwrap();
let read_post_listings_no_user = PostQueryBuilder::create(&conn)
.listing_type(ListingType::Community)
.sort(&SortType::New)
.for_community_id(inserted_community.id)
.list()
.unwrap();
let read_post_listing_no_user = PostView::read(&conn, inserted_post.id, None).unwrap();
let read_post_listing_with_user =
PostView::read(&conn, inserted_post.id, Some(inserted_user.id)).unwrap();
let like_removed = PostLike::remove(&conn, &post_like_form).unwrap(); let like_removed = PostLike::remove(&conn, &post_like_form).unwrap();
let num_deleted = Post::delete(&conn, inserted_post.id).unwrap(); let num_deleted = Post::delete(&conn, inserted_post.id).unwrap();
Community::delete(&conn, inserted_community.id).unwrap(); Community::delete(&conn, inserted_community.id).unwrap();

View file

@ -26,29 +26,6 @@ table! {
} }
} }
table! {
private_message_mview (id) {
id -> Int4,
creator_id -> Int4,
recipient_id -> Int4,
content -> Text,
deleted -> Bool,
read -> Bool,
published -> Timestamp,
updated -> Nullable<Timestamp>,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
}
}
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
@ -76,7 +53,7 @@ pub struct PrivateMessageView {
pub struct PrivateMessageQueryBuilder<'a> { pub struct PrivateMessageQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>, query: super::private_message_view::private_message_view::BoxedQuery<'a, Pg>,
for_recipient_id: i32, for_recipient_id: i32,
unread_only: bool, unread_only: bool,
page: Option<i64>, page: Option<i64>,
@ -85,9 +62,9 @@ pub struct PrivateMessageQueryBuilder<'a> {
impl<'a> PrivateMessageQueryBuilder<'a> { impl<'a> PrivateMessageQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
use super::private_message_view::private_message_mview::dsl::*; use super::private_message_view::private_message_view::dsl::*;
let query = private_message_mview.into_boxed(); let query = private_message_view.into_boxed();
PrivateMessageQueryBuilder { PrivateMessageQueryBuilder {
conn, conn,
@ -115,7 +92,7 @@ impl<'a> PrivateMessageQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> { pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
use super::private_message_view::private_message_mview::dsl::*; use super::private_message_view::private_message_view::dsl::*;
let mut query = self.query.filter(deleted.eq(false)); let mut query = self.query.filter(deleted.eq(false));

View file

@ -40,7 +40,7 @@ table! {
} }
table! { table! {
user_mention_mview (id) { user_mention_fast_view (id) {
id -> Int4, id -> Int4,
user_mention_id -> Int4, user_mention_id -> Int4,
creator_id -> Int4, creator_id -> Int4,
@ -78,7 +78,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "user_mention_view"] #[table_name = "user_mention_fast_view"]
pub struct UserMentionView { pub struct UserMentionView {
pub id: i32, pub id: i32,
pub user_mention_id: i32, pub user_mention_id: i32,
@ -115,7 +115,7 @@ pub struct UserMentionView {
pub struct UserMentionQueryBuilder<'a> { pub struct UserMentionQueryBuilder<'a> {
conn: &'a PgConnection, conn: &'a PgConnection,
query: super::user_mention_view::user_mention_mview::BoxedQuery<'a, Pg>, query: super::user_mention_view::user_mention_fast_view::BoxedQuery<'a, Pg>,
for_user_id: i32, for_user_id: i32,
sort: &'a SortType, sort: &'a SortType,
unread_only: bool, unread_only: bool,
@ -125,9 +125,9 @@ pub struct UserMentionQueryBuilder<'a> {
impl<'a> UserMentionQueryBuilder<'a> { impl<'a> UserMentionQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self { pub fn create(conn: &'a PgConnection, for_user_id: i32) -> Self {
use super::user_mention_view::user_mention_mview::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
let query = user_mention_mview.into_boxed(); let query = user_mention_fast_view.into_boxed();
UserMentionQueryBuilder { UserMentionQueryBuilder {
conn, conn,
@ -161,7 +161,7 @@ impl<'a> UserMentionQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<UserMentionView>, Error> { pub fn list(self) -> Result<Vec<UserMentionView>, Error> {
use super::user_mention_view::user_mention_mview::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -208,9 +208,9 @@ impl UserMentionView {
from_user_mention_id: i32, from_user_mention_id: i32,
from_recipient_id: i32, from_recipient_id: i32,
) -> Result<Self, Error> { ) -> Result<Self, Error> {
use super::user_mention_view::user_mention_view::dsl::*; use super::user_mention_view::user_mention_fast_view::dsl::*;
user_mention_view user_mention_fast_view
.filter(user_mention_id.eq(from_user_mention_id)) .filter(user_mention_id.eq(from_user_mention_id))
.filter(user_id.eq(from_recipient_id)) .filter(user_id.eq(from_recipient_id))
.first::<Self>(conn) .first::<Self>(conn)

View file

@ -1,4 +1,4 @@
use super::user_view::user_mview::BoxedQuery; use super::user_view::user_fast::BoxedQuery;
use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType}; use crate::db::{fuzzy_search, limit_and_offset, MaybeOptional, SortType};
use diesel::{dsl::*, pg::Pg, result::Error, *}; use diesel::{dsl::*, pg::Pg, result::Error, *};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -26,7 +26,7 @@ table! {
} }
table! { table! {
user_mview (id) { user_fast (id) {
id -> Int4, id -> Int4,
actor_id -> Text, actor_id -> Text,
name -> Varchar, name -> Varchar,
@ -50,7 +50,7 @@ table! {
#[derive( #[derive(
Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone, Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
)] )]
#[table_name = "user_view"] #[table_name = "user_fast"]
pub struct UserView { pub struct UserView {
pub id: i32, pub id: i32,
pub actor_id: String, pub actor_id: String,
@ -81,9 +81,9 @@ pub struct UserQueryBuilder<'a> {
impl<'a> UserQueryBuilder<'a> { impl<'a> UserQueryBuilder<'a> {
pub fn create(conn: &'a PgConnection) -> Self { pub fn create(conn: &'a PgConnection) -> Self {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
let query = user_mview.into_boxed(); let query = user_fast.into_boxed();
UserQueryBuilder { UserQueryBuilder {
conn, conn,
@ -100,7 +100,7 @@ impl<'a> UserQueryBuilder<'a> {
} }
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::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
if let Some(search_term) = search_term.get_optional() { if let Some(search_term) = search_term.get_optional() {
self.query = self.query.filter(name.ilike(fuzzy_search(&search_term))); self.query = self.query.filter(name.ilike(fuzzy_search(&search_term)));
} }
@ -118,7 +118,7 @@ impl<'a> UserQueryBuilder<'a> {
} }
pub fn list(self) -> Result<Vec<UserView>, Error> { pub fn list(self) -> Result<Vec<UserView>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
let mut query = self.query; let mut query = self.query;
@ -151,17 +151,17 @@ impl<'a> UserQueryBuilder<'a> {
impl UserView { impl UserView {
pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> { pub fn read(conn: &PgConnection, from_user_id: i32) -> Result<Self, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.find(from_user_id).first::<Self>(conn) user_fast.find(from_user_id).first::<Self>(conn)
} }
pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn admins(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.filter(admin.eq(true)).load::<Self>(conn) user_fast.filter(admin.eq(true)).load::<Self>(conn)
} }
pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> { pub fn banned(conn: &PgConnection) -> Result<Vec<Self>, Error> {
use super::user_view::user_mview::dsl::*; use super::user_view::user_fast::dsl::*;
user_mview.filter(banned.eq(true)).load::<Self>(conn) user_fast.filter(banned.eq(true)).load::<Self>(conn)
} }
} }

View file

@ -325,7 +325,7 @@ pub fn markdown_to_html(text: &str) -> String {
pub fn get_ip(conn_info: &ConnectionInfo) -> String { pub fn get_ip(conn_info: &ConnectionInfo) -> String {
conn_info conn_info
.remote_addr() .realip_remote_addr()
.unwrap_or("127.0.0.1:12345") .unwrap_or("127.0.0.1:12345")
.split(':') .split(':')
.next() .next()

View file

@ -33,6 +33,37 @@ table! {
} }
} }
table! {
comment_aggregates_fast (id) {
id -> Int4,
creator_id -> Nullable<Int4>,
post_id -> Nullable<Int4>,
parent_id -> Nullable<Int4>,
content -> Nullable<Text>,
removed -> Nullable<Bool>,
read -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
ap_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
community_id -> Nullable<Int4>,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
score -> Nullable<Int8>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
}
}
table! { table! {
comment_like (id) { comment_like (id) {
id -> Int4, id -> Int4,
@ -74,6 +105,34 @@ table! {
} }
} }
table! {
community_aggregates_fast (id) {
id -> Int4,
name -> Nullable<Varchar>,
title -> Nullable<Varchar>,
description -> Nullable<Text>,
category_id -> Nullable<Int4>,
creator_id -> Nullable<Int4>,
removed -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
nsfw -> Nullable<Bool>,
actor_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
last_refreshed_at -> Nullable<Timestamp>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
category_name -> Nullable<Varchar>,
number_of_subscribers -> Nullable<Int8>,
number_of_posts -> Nullable<Int8>,
number_of_comments -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
}
}
table! { table! {
community_follower (id) { community_follower (id) {
id -> Int4, id -> Int4,
@ -234,6 +293,48 @@ table! {
} }
} }
table! {
post_aggregates_fast (id) {
id -> Int4,
name -> Nullable<Varchar>,
url -> Nullable<Text>,
body -> Nullable<Text>,
creator_id -> Nullable<Int4>,
community_id -> Nullable<Int4>,
removed -> Nullable<Bool>,
locked -> Nullable<Bool>,
published -> Nullable<Timestamp>,
updated -> Nullable<Timestamp>,
deleted -> Nullable<Bool>,
nsfw -> Nullable<Bool>,
stickied -> Nullable<Bool>,
embed_title -> Nullable<Text>,
embed_description -> Nullable<Text>,
embed_html -> Nullable<Text>,
thumbnail_url -> Nullable<Text>,
ap_id -> Nullable<Varchar>,
local -> Nullable<Bool>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
community_removed -> Nullable<Bool>,
community_deleted -> Nullable<Bool>,
community_nsfw -> Nullable<Bool>,
number_of_comments -> Nullable<Int8>,
score -> Nullable<Int8>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
newest_activity_time -> Nullable<Timestamp>,
}
}
table! { table! {
post_like (id) { post_like (id) {
id -> Int4, id -> Int4,
@ -328,6 +429,28 @@ table! {
} }
} }
table! {
user_fast (id) {
id -> Int4,
actor_id -> Nullable<Varchar>,
name -> Nullable<Varchar>,
avatar -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
bio -> Nullable<Text>,
local -> Nullable<Bool>,
admin -> Nullable<Bool>,
banned -> Nullable<Bool>,
show_avatars -> Nullable<Bool>,
send_notifications_to_email -> Nullable<Bool>,
published -> Nullable<Timestamp>,
number_of_posts -> Nullable<Int8>,
post_score -> Nullable<Int8>,
number_of_comments -> Nullable<Int8>,
comment_score -> Nullable<Int8>,
}
}
table! { table! {
user_mention (id) { user_mention (id) {
id -> Int4, id -> Int4,
@ -384,9 +507,11 @@ allow_tables_to_appear_in_same_query!(
activity, activity,
category, category,
comment, comment,
comment_aggregates_fast,
comment_like, comment_like,
comment_saved, comment_saved,
community, community,
community_aggregates_fast,
community_follower, community_follower,
community_moderator, community_moderator,
community_user_ban, community_user_ban,
@ -401,6 +526,7 @@ allow_tables_to_appear_in_same_query!(
mod_sticky_post, mod_sticky_post,
password_reset_request, password_reset_request,
post, post,
post_aggregates_fast,
post_like, post_like,
post_read, post_read,
post_saved, post_saved,
@ -408,5 +534,6 @@ allow_tables_to_appear_in_same_query!(
site, site,
user_, user_,
user_ban, user_ban,
user_fast,
user_mention, user_mention,
); );

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.11"; pub const VERSION: &str = "v0.7.13";

View file

@ -73,6 +73,7 @@ interface CommentNodeProps {
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType; sort?: CommentSortType;
sortType?: SortType; sortType?: SortType;
enableDownvotes: boolean;
} }
export class CommentNode extends Component<CommentNodeProps, CommentNodeState> { export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
@ -279,7 +280,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
<span class="ml-1">{this.state.upvotes}</span> <span class="ml-1">{this.state.upvotes}</span>
)} )}
</button> </button>
{WebSocketService.Instance.site.enable_downvotes && ( {this.props.enableDownvotes && (
<button <button
className={`btn btn-link btn-animate ${ className={`btn btn-link btn-animate ${
this.state.my_vote == -1 this.state.my_vote == -1
@ -703,6 +704,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
postCreatorId={this.props.postCreatorId} postCreatorId={this.props.postCreatorId}
sort={this.props.sort} sort={this.props.sort}
sortType={this.props.sortType} sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/> />
)} )}
{/* A collapsed clearfix */} {/* A collapsed clearfix */}

View file

@ -24,6 +24,7 @@ interface CommentNodesProps {
showCommunity?: boolean; showCommunity?: boolean;
sort?: CommentSortType; sort?: CommentSortType;
sortType?: SortType; sortType?: SortType;
enableDownvotes: boolean;
} }
export class CommentNodes extends Component< export class CommentNodes extends Component<
@ -52,6 +53,7 @@ export class CommentNodes extends Component<
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
sort={this.props.sort} sort={this.props.sort}
sortType={this.props.sortType} sortType={this.props.sortType}
enableDownvotes={this.props.enableDownvotes}
/> />
))} ))}
</div> </div>

View file

@ -1,5 +1,4 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {
@ -11,6 +10,7 @@ import {
ListCommunitiesForm, ListCommunitiesForm,
SortType, SortType,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { wsJsonToRes, toast } from '../utils'; import { wsJsonToRes, toast } from '../utils';
@ -47,6 +47,7 @@ export class Communities extends Component<any, CommunitiesState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
getPageFromProps(props: any): number { getPageFromProps(props: any): number {
@ -57,12 +58,6 @@ export class Communities extends Component<any, CommunitiesState> {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `${i18n.t('communities')} - ${
WebSocketService.Instance.site.name
}`;
}
// Necessary for back button for some reason // Necessary for back button for some reason
componentWillReceiveProps(nextProps: any) { componentWillReceiveProps(nextProps: any) {
if (nextProps.history.action == 'POP') { if (nextProps.history.action == 'POP') {
@ -244,6 +239,9 @@ export class Communities extends Component<any, CommunitiesState> {
found.subscribed = data.community.subscribed; found.subscribed = data.community.subscribed;
found.number_of_subscribers = data.community.number_of_subscribers; found.number_of_subscribers = data.community.number_of_subscribers;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('communities')} - ${data.site.name}`;
} }
} }
} }

View file

@ -8,7 +8,6 @@ import {
Category, Category,
ListCategoriesResponse, ListCategoriesResponse,
CommunityResponse, CommunityResponse,
GetSiteResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
@ -30,13 +29,13 @@ interface CommunityFormProps {
onCancel?(): any; onCancel?(): any;
onCreate?(community: Community): any; onCreate?(community: Community): any;
onEdit?(community: Community): any; onEdit?(community: Community): any;
enableNsfw: boolean;
} }
interface CommunityFormState { interface CommunityFormState {
communityForm: CommunityFormI; communityForm: CommunityFormI;
categories: Array<Category>; categories: Array<Category>;
loading: boolean; loading: boolean;
enable_nsfw: boolean;
} }
export class CommunityForm extends Component< export class CommunityForm extends Component<
@ -56,7 +55,6 @@ export class CommunityForm extends Component<
}, },
categories: [], categories: [],
loading: false, loading: false,
enable_nsfw: null,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -86,7 +84,6 @@ export class CommunityForm extends Component<
); );
WebSocketService.Instance.listCategories(); WebSocketService.Instance.listCategories();
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentDidMount() {
@ -187,7 +184,7 @@ export class CommunityForm extends Component<
</div> </div>
</div> </div>
{this.state.enable_nsfw && ( {this.props.enableNsfw && (
<div class="form-group row"> <div class="form-group row">
<div class="col-12"> <div class="col-12">
<div class="form-check"> <div class="form-check">
@ -303,10 +300,6 @@ export class CommunityForm extends Component<
let data = res.data as CommunityResponse; let data = res.data as CommunityResponse;
this.state.loading = false; this.state.loading = false;
this.props.onEdit(data.community); this.props.onEdit(data.community);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
} }
} }
} }

View file

@ -23,6 +23,8 @@ import {
GetCommentsResponse, GetCommentsResponse,
CommentResponse, CommentResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PostListings } from './post-listings'; import { PostListings } from './post-listings';
@ -60,6 +62,7 @@ interface State {
dataType: DataType; dataType: DataType;
sort: SortType; sort: SortType;
page: number; page: number;
site: Site;
} }
export class Community extends Component<any, State> { export class Community extends Component<any, State> {
@ -97,6 +100,20 @@ export class Community extends Component<any, State> {
dataType: getDataTypeFromProps(this.props), dataType: getDataTypeFromProps(this.props),
sort: getSortTypeFromProps(this.props), sort: getSortTypeFromProps(this.props),
page: getPageFromProps(this.props), page: getPageFromProps(this.props),
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -119,6 +136,7 @@ export class Community extends Component<any, State> {
name: this.state.communityName ? this.state.communityName : null, name: this.state.communityName ? this.state.communityName : null,
}; };
WebSocketService.Instance.getCommunity(form); WebSocketService.Instance.getCommunity(form);
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
@ -174,6 +192,7 @@ export class Community extends Component<any, State> {
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.admins}
online={this.state.online} online={this.state.online}
enableNsfw={this.state.site.enable_nsfw}
/> />
</div> </div>
</div> </div>
@ -188,6 +207,8 @@ export class Community extends Component<any, State> {
posts={this.state.posts} posts={this.state.posts}
removeDuplicates removeDuplicates
sort={this.state.sort} sort={this.state.sort}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
@ -195,6 +216,7 @@ export class Community extends Component<any, State> {
noIndent noIndent
sortType={this.state.sort} sortType={this.state.sort}
showContext showContext
enableDownvotes={this.state.site.enable_downvotes}
/> />
); );
} }
@ -331,7 +353,7 @@ export class Community extends Component<any, State> {
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins; this.state.admins = data.admins;
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} - ${this.state.site.name}`;
this.setState(this.state); this.setState(this.state);
this.fetchData(); this.fetchData();
} else if (res.op == UserOperation.EditCommunity) { } else if (res.op == UserOperation.EditCommunity) {
@ -399,6 +421,10 @@ export class Community extends Component<any, State> {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.comments); createCommentLikeRes(data, this.state.comments);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
} }
} }
} }

View file

@ -1,19 +1,44 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { CommunityForm } from './community-form'; import { CommunityForm } from './community-form';
import { Community } from '../interfaces'; import {
Community,
UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreateCommunity extends Component<any, any> { interface CreateCommunityState {
enableNsfw: boolean;
}
export class CreateCommunity extends Component<any, CreateCommunityState> {
private subscription: Subscription;
private emptyState: CreateCommunityState = {
enableNsfw: null,
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handleCommunityCreate = this.handleCommunityCreate.bind(this); this.handleCommunityCreate = this.handleCommunityCreate.bind(this);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_community')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -22,7 +47,10 @@ export class CreateCommunity extends Component<any, any> {
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_community')}</h5> <h5>{i18n.t('create_community')}</h5>
<CommunityForm onCreate={this.handleCommunityCreate} /> <CommunityForm
onCreate={this.handleCommunityCreate}
enableNsfw={this.state.enableNsfw}
/>
</div> </div>
</div> </div>
</div> </div>
@ -32,4 +60,18 @@ export class CreateCommunity extends Component<any, any> {
handleCommunityCreate(community: Community) { handleCommunityCreate(community: Community) {
this.props.history.push(`/c/${community.name}`); this.props.history.push(`/c/${community.name}`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableNsfw = data.site.enable_nsfw;
this.setState(this.state);
document.title = `${i18n.t('create_community')} - ${data.site.name}`;
}
}
} }

View file

@ -1,19 +1,59 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PostForm } from './post-form'; import { PostForm } from './post-form';
import { toast, wsJsonToRes } from '../utils';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PostFormParams } from '../interfaces'; import {
UserOperation,
PostFormParams,
WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreatePost extends Component<any, any> { interface CreatePostState {
site: Site;
}
export class CreatePost extends Component<any, CreatePostState> {
private subscription: Subscription;
private emptyState: CreatePostState = {
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
};
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePostCreate = this.handlePostCreate.bind(this); this.handlePostCreate = this.handlePostCreate.bind(this);
this.state = this.emptyState;
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_post')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -22,7 +62,12 @@ export class CreatePost extends Component<any, any> {
<div class="row"> <div class="row">
<div class="col-12 col-lg-6 offset-lg-3 mb-4"> <div class="col-12 col-lg-6 offset-lg-3 mb-4">
<h5>{i18n.t('create_post')}</h5> <h5>{i18n.t('create_post')}</h5>
<PostForm onCreate={this.handlePostCreate} params={this.params} /> <PostForm
onCreate={this.handlePostCreate}
params={this.params}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
</div> </div>
</div> </div>
</div> </div>
@ -56,4 +101,18 @@ export class CreatePost extends Component<any, any> {
handlePostCreate(id: number) { handlePostCreate(id: number) {
this.props.history.push(`/post/${id}`); this.props.history.push(`/post/${id}`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('create_post')} - ${data.site.name}`;
}
}
} }

View file

@ -1,22 +1,38 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { PrivateMessageForm } from './private-message-form'; import { PrivateMessageForm } from './private-message-form';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { PrivateMessageFormParams } from '../interfaces'; import {
import { toast } from '../utils'; UserOperation,
WebSocketJsonResponse,
GetSiteResponse,
PrivateMessageFormParams,
} from '../interfaces';
import { toast, wsJsonToRes } from '../utils';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
export class CreatePrivateMessage extends Component<any, any> { export class CreatePrivateMessage extends Component<any, any> {
private subscription: Subscription;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind( this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
this this
); );
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentWillUnmount() {
document.title = `${i18n.t('create_private_message')} - ${ this.subscription.unsubscribe();
WebSocketService.Instance.site.name
}`;
} }
render() { render() {
@ -50,4 +66,18 @@ export class CreatePrivateMessage extends Component<any, any> {
// Navigate to the front // Navigate to the front
this.props.history.push(`/`); this.props.history.push(`/`);
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('create_private_message')} - ${
data.site.name
}`;
}
}
} }

View file

@ -16,6 +16,7 @@ import {
GetPrivateMessagesForm, GetPrivateMessagesForm,
PrivateMessagesResponse, PrivateMessagesResponse,
PrivateMessageResponse, PrivateMessageResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -56,6 +57,7 @@ interface InboxState {
messages: Array<PrivateMessageI>; messages: Array<PrivateMessageI>;
sort: SortType; sort: SortType;
page: number; page: number;
enableDownvotes: boolean;
} }
export class Inbox extends Component<any, InboxState> { export class Inbox extends Component<any, InboxState> {
@ -68,6 +70,7 @@ export class Inbox extends Component<any, InboxState> {
messages: [], messages: [],
sort: SortType.New, sort: SortType.New,
page: 1, page: 1,
enableDownvotes: undefined,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -85,18 +88,13 @@ export class Inbox extends Component<any, InboxState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${WebSocketService.Instance.site.name}`;
}
render() { render() {
return ( return (
<div class="container"> <div class="container">
@ -270,6 +268,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
) : ( ) : (
<PrivateMessage privateMessage={i} /> <PrivateMessage privateMessage={i} />
@ -287,6 +286,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
</div> </div>
); );
@ -301,6 +301,7 @@ export class Inbox extends Component<any, InboxState> {
noIndent noIndent
markable markable
showContext showContext
enableDownvotes={this.state.enableDownvotes}
/> />
))} ))}
</div> </div>
@ -522,6 +523,13 @@ export class Inbox extends Component<any, InboxState> {
let data = res.data as CommentResponse; let data = res.data as CommentResponse;
createCommentLikeRes(data, this.state.replies); createCommentLikeRes(data, this.state.replies);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enableDownvotes = data.site.enable_downvotes;
this.setState(this.state);
document.title = `/u/${UserService.Instance.user.username} ${i18n.t(
'inbox'
)} - ${data.site.name}`;
} }
} }

View file

@ -385,9 +385,7 @@ export class Login extends Component<any, State> {
let data = res.data as GetSiteResponse; let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw; this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state); this.setState(this.state);
document.title = `${i18n.t('login')} - ${ document.title = `${i18n.t('login')} - ${data.site.name}`;
WebSocketService.Instance.site.name
}`;
} }
} }
} }

View file

@ -418,6 +418,8 @@ export class Main extends Component<any, MainState> {
showCommunity showCommunity
removeDuplicates removeDuplicates
sort={this.state.sort} sort={this.state.sort}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
@ -426,6 +428,7 @@ export class Main extends Component<any, MainState> {
showCommunity showCommunity
sortType={this.state.sort} sortType={this.state.sort}
showContext showContext
enableDownvotes={this.state.siteRes.site.enable_downvotes}
/> />
); );
} }
@ -617,7 +620,7 @@ export class Main extends Component<any, MainState> {
this.state.siteRes.banned = data.banned; this.state.siteRes.banned = data.banned;
this.state.siteRes.online = data.online; this.state.siteRes.online = data.online;
this.setState(this.state); this.setState(this.state);
document.title = `${WebSocketService.Instance.site.name}`; document.title = `${this.state.siteRes.site.name}`;
} else if (res.op == UserOperation.EditSite) { } else if (res.op == UserOperation.EditSite) {
let data = res.data as SiteResponse; let data = res.data as SiteResponse;
this.state.siteRes.site = data.site; this.state.siteRes.site = data.site;

View file

@ -16,6 +16,7 @@ import {
ModAddCommunity, ModAddCommunity,
ModAdd, ModAdd,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils'; import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
@ -64,16 +65,13 @@ export class Modlog extends Component<any, ModlogState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `Modlog - ${WebSocketService.Instance.site.name}`;
}
setCombined(res: GetModlogResponse) { setCombined(res: GetModlogResponse) {
let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts'); let removed_posts = addTypeInfo(res.removed_posts, 'removed_posts');
let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts'); let locked_posts = addTypeInfo(res.locked_posts, 'locked_posts');
@ -434,6 +432,9 @@ export class Modlog extends Component<any, ModlogState> {
this.state.loading = false; this.state.loading = false;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setCombined(data); this.setCombined(data);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `Modlog - ${data.site.name}`;
} }
} }
} }

View file

@ -396,9 +396,6 @@ export class Navbar extends Component<any, NavbarState> {
if (data.site && !this.state.siteName) { if (data.site && !this.state.siteName) {
this.state.siteName = data.site.name; this.state.siteName = data.site.name;
this.state.admins = data.admins; this.state.admins = data.admins;
WebSocketService.Instance.site = data.site;
WebSocketService.Instance.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -6,6 +6,7 @@ import {
LoginResponse, LoginResponse,
PasswordChangeForm, PasswordChangeForm,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils'; import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
@ -40,18 +41,13 @@ export class PasswordChange extends Component<any, State> {
err => console.error(err), err => console.error(err),
() => console.log('complete') () => console.log('complete')
); );
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
this.subscription.unsubscribe(); this.subscription.unsubscribe();
} }
componentDidMount() {
document.title = `${i18n.t('password_change')} - ${
WebSocketService.Instance.site.name
}`;
}
render() { render() {
return ( return (
<div class="container"> <div class="container">
@ -138,14 +134,15 @@ export class PasswordChange extends Component<any, State> {
this.state.loading = false; this.state.loading = false;
this.setState(this.state); this.setState(this.state);
return; return;
} else { } else if (res.op == UserOperation.PasswordChange) {
if (res.op == UserOperation.PasswordChange) {
let data = res.data as LoginResponse; let data = res.data as LoginResponse;
this.state = this.emptyState; this.state = this.emptyState;
this.setState(this.state); this.setState(this.state);
UserService.Instance.login(data); UserService.Instance.login(data);
this.props.history.push('/'); this.props.history.push('/');
} } else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('password_change')} - ${data.site.name}`;
} }
} }
} }

View file

@ -16,7 +16,6 @@ import {
SearchForm, SearchForm,
SearchType, SearchType,
SearchResponse, SearchResponse,
GetSiteResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
@ -52,6 +51,8 @@ interface PostFormProps {
onCancel?(): any; onCancel?(): any;
onCreate?(id: number): any; onCreate?(id: number): any;
onEdit?(post: Post): any; onEdit?(post: Post): any;
enableNsfw: boolean;
enableDownvotes: boolean;
} }
interface PostFormState { interface PostFormState {
@ -63,7 +64,6 @@ interface PostFormState {
suggestedTitle: string; suggestedTitle: string;
suggestedPosts: Array<Post>; suggestedPosts: Array<Post>;
crossPosts: Array<Post>; crossPosts: Array<Post>;
enable_nsfw: boolean;
} }
export class PostForm extends Component<PostFormProps, PostFormState> { export class PostForm extends Component<PostFormProps, PostFormState> {
@ -87,7 +87,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
suggestedTitle: undefined, suggestedTitle: undefined,
suggestedPosts: [], suggestedPosts: [],
crossPosts: [], crossPosts: [],
enable_nsfw: undefined,
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -138,7 +137,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
}; };
WebSocketService.Instance.listCommunities(listCommunitiesForm); WebSocketService.Instance.listCommunities(listCommunitiesForm);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentDidMount() {
@ -240,7 +238,12 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div class="my-1 text-muted small font-weight-bold"> <div class="my-1 text-muted small font-weight-bold">
{i18n.t('cross_posts')} {i18n.t('cross_posts')}
</div> </div>
<PostListings showCommunity posts={this.state.crossPosts} /> <PostListings
showCommunity
posts={this.state.crossPosts}
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
</> </>
)} )}
</div> </div>
@ -265,7 +268,11 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
<div class="my-1 text-muted small font-weight-bold"> <div class="my-1 text-muted small font-weight-bold">
{i18n.t('related_posts')} {i18n.t('related_posts')}
</div> </div>
<PostListings posts={this.state.suggestedPosts} /> <PostListings
posts={this.state.suggestedPosts}
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
</> </>
)} )}
</div> </div>
@ -346,7 +353,7 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
</div> </div>
</div> </div>
)} )}
{this.state.enable_nsfw && ( {this.props.enableNsfw && (
<div class="form-group row"> <div class="form-group row">
<div class="col-sm-10"> <div class="col-sm-10">
<div class="form-check"> <div class="form-check">
@ -631,10 +638,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
this.state.crossPosts = data.posts; this.state.crossPosts = data.posts;
} }
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.enable_nsfw = data.site.enable_nsfw;
this.setState(this.state);
} }
} }
} }

View file

@ -61,6 +61,8 @@ interface PostListingProps {
showBody?: boolean; showBody?: boolean;
moderators?: Array<CommunityUser>; moderators?: Array<CommunityUser>;
admins?: Array<UserView>; admins?: Array<UserView>;
enableDownvotes: boolean;
enableNsfw: boolean;
} }
export class PostListing extends Component<PostListingProps, PostListingState> { export class PostListing extends Component<PostListingProps, PostListingState> {
@ -115,6 +117,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
post={this.props.post} post={this.props.post}
onEdit={this.handleEditPost} onEdit={this.handleEditPost}
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw}
enableDownvotes={this.props.enableDownvotes}
/> />
</div> </div>
)} )}
@ -273,7 +277,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
> >
{this.state.score} {this.state.score}
</div> </div>
{WebSocketService.Instance.site.enable_downvotes && ( {this.props.enableDownvotes && (
<button <button
className={`btn-animate btn btn-link p-0 ${ className={`btn-animate btn btn-link p-0 ${
this.state.my_vote == -1 ? 'text-danger' : 'text-muted' this.state.my_vote == -1 ? 'text-danger' : 'text-muted'

View file

@ -11,6 +11,8 @@ interface PostListingsProps {
showCommunity?: boolean; showCommunity?: boolean;
removeDuplicates?: boolean; removeDuplicates?: boolean;
sort?: SortType; sort?: SortType;
enableDownvotes: boolean;
enableNsfw: boolean;
} }
export class PostListings extends Component<PostListingsProps, any> { export class PostListings extends Component<PostListingsProps, any> {
@ -27,6 +29,8 @@ export class PostListings extends Component<PostListingsProps, any> {
<PostListing <PostListing
post={post} post={post}
showCommunity={this.props.showCommunity} showCommunity={this.props.showCommunity}
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/> />
<hr class="my-2" /> <hr class="my-2" />
</> </>

View file

@ -18,7 +18,6 @@ import {
BanUserResponse, BanUserResponse,
AddModToCommunityResponse, AddModToCommunityResponse,
AddAdminResponse, AddAdminResponse,
UserView,
SearchType, SearchType,
SortType, SortType,
SearchForm, SearchForm,
@ -52,12 +51,12 @@ interface PostState {
commentSort: CommentSortType; commentSort: CommentSortType;
community: Community; community: Community;
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number; online: number;
scrolled?: boolean; scrolled?: boolean;
scrolled_comment_id?: number; scrolled_comment_id?: number;
loading: boolean; loading: boolean;
crossPosts: Array<PostI>; crossPosts: Array<PostI>;
siteRes: GetSiteResponse;
} }
export class Post extends Component<any, PostState> { export class Post extends Component<any, PostState> {
@ -68,11 +67,29 @@ export class Post extends Component<any, PostState> {
commentSort: CommentSortType.Hot, commentSort: CommentSortType.Hot,
community: null, community: null,
moderators: [], moderators: [],
admins: [],
online: null, online: null,
scrolled: false, scrolled: false,
loading: true, loading: true,
crossPosts: [], crossPosts: [],
siteRes: {
admins: [],
banned: [],
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
online: null,
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -97,6 +114,7 @@ export class Post extends Component<any, PostState> {
id: postId, id: postId,
}; };
WebSocketService.Instance.getPost(form); WebSocketService.Instance.getPost(form);
WebSocketService.Instance.getSite();
} }
componentWillUnmount() { componentWillUnmount() {
@ -180,7 +198,9 @@ export class Post extends Component<any, PostState> {
showBody showBody
showCommunity showCommunity
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.siteRes.admins}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
/> />
<div className="mb-2" /> <div className="mb-2" />
<CommentForm <CommentForm
@ -269,9 +289,10 @@ export class Post extends Component<any, PostState> {
noIndent noIndent
locked={this.state.post.locked} locked={this.state.post.locked}
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.siteRes.admins}
postCreatorId={this.state.post.creator_id} postCreatorId={this.state.post.creator_id}
showContext showContext
enableDownvotes={this.state.siteRes.site.enable_downvotes}
/> />
</div> </div>
</div> </div>
@ -284,8 +305,9 @@ export class Post extends Component<any, PostState> {
<Sidebar <Sidebar
community={this.state.community} community={this.state.community}
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.siteRes.admins}
online={this.state.online} online={this.state.online}
enableNsfw={this.state.siteRes.site.enable_nsfw}
/> />
</div> </div>
); );
@ -336,9 +358,10 @@ export class Post extends Component<any, PostState> {
nodes={nodes} nodes={nodes}
locked={this.state.post.locked} locked={this.state.post.locked}
moderators={this.state.moderators} moderators={this.state.moderators}
admins={this.state.admins} admins={this.state.siteRes.admins}
postCreatorId={this.state.post.creator_id} postCreatorId={this.state.post.creator_id}
sort={this.state.commentSort} sort={this.state.commentSort}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
/> />
</div> </div>
); );
@ -360,10 +383,10 @@ export class Post extends Component<any, PostState> {
this.state.comments = data.comments; this.state.comments = data.comments;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins; this.state.siteRes.admins = data.admins;
this.state.online = data.online; this.state.online = data.online;
this.state.loading = false; this.state.loading = false;
document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`; document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
// Get cross-posts // Get cross-posts
if (this.state.post.url) { if (this.state.post.url) {
@ -450,7 +473,7 @@ export class Post extends Component<any, PostState> {
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) { } else if (res.op == UserOperation.AddAdmin) {
let data = res.data as AddAdminResponse; let data = res.data as AddAdminResponse;
this.state.admins = data.admins; this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.Search) { } else if (res.op == UserOperation.Search) {
let data = res.data as SearchResponse; let data = res.data as SearchResponse;
@ -461,15 +484,18 @@ export class Post extends Component<any, PostState> {
this.state.post.duplicates = this.state.crossPosts; this.state.post.duplicates = this.state.crossPosts;
} }
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.TransferSite) { } else if (
res.op == UserOperation.TransferSite ||
res.op == UserOperation.GetSite
) {
let data = res.data as GetSiteResponse; let data = res.data as GetSiteResponse;
this.state.admins = data.admins; this.state.siteRes = data;
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.TransferCommunity) { } else if (res.op == UserOperation.TransferCommunity) {
let data = res.data as GetCommunityResponse; let data = res.data as GetCommunityResponse;
this.state.community = data.community; this.state.community = data.community;
this.state.moderators = data.moderators; this.state.moderators = data.moderators;
this.state.admins = data.admins; this.state.siteRes.admins = data.admins;
this.setState(this.state); this.setState(this.state);
} }
} }

View file

@ -1,6 +1,5 @@
import { Component, linkEvent } from 'inferno'; import { Component, linkEvent } from 'inferno';
import { Prompt } from 'inferno-router'; import { Prompt } from 'inferno-router';
import { Link } from 'inferno-router';
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators'; import { retryWhen, delay, take } from 'rxjs/operators';
import { import {

View file

@ -15,6 +15,8 @@ import {
PostResponse, PostResponse,
CommentResponse, CommentResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import { import {
@ -41,6 +43,7 @@ interface SearchState {
page: number; page: number;
searchResponse: SearchResponse; searchResponse: SearchResponse;
loading: boolean; loading: boolean;
site: Site;
} }
export class Search extends Component<any, SearchState> { export class Search extends Component<any, SearchState> {
@ -58,6 +61,20 @@ export class Search extends Component<any, SearchState> {
users: [], users: [],
}, },
loading: false, loading: false,
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
}; };
getSearchQueryFromProps(props: any): string { getSearchQueryFromProps(props: any): string {
@ -94,6 +111,8 @@ export class Search extends Component<any, SearchState> {
() => console.log('complete') () => console.log('complete')
); );
WebSocketService.Instance.getSite();
if (this.state.q) { if (this.state.q) {
this.search(); this.search();
} }
@ -118,12 +137,6 @@ export class Search extends Component<any, SearchState> {
} }
} }
componentDidMount() {
document.title = `${i18n.t('search')} - ${
WebSocketService.Instance.site.name
}`;
}
render() { render() {
return ( return (
<div class="container"> <div class="container">
@ -241,13 +254,19 @@ export class Search extends Component<any, SearchState> {
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
{i.type_ == 'posts' && ( {i.type_ == 'posts' && (
<PostListing post={i.data as Post} showCommunity /> <PostListing
post={i.data as Post}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
)} )}
{i.type_ == 'comments' && ( {i.type_ == 'comments' && (
<CommentNodes <CommentNodes
nodes={[{ comment: i.data as Comment }]} nodes={[{ comment: i.data as Comment }]}
locked locked
noIndent noIndent
enableDownvotes={this.state.site.enable_downvotes}
/> />
)} )}
{i.type_ == 'communities' && ( {i.type_ == 'communities' && (
@ -281,6 +300,7 @@ export class Search extends Component<any, SearchState> {
nodes={commentsToFlatNodes(this.state.searchResponse.comments)} nodes={commentsToFlatNodes(this.state.searchResponse.comments)}
locked locked
noIndent noIndent
enableDownvotes={this.state.site.enable_downvotes}
/> />
); );
} }
@ -291,7 +311,12 @@ export class Search extends Component<any, SearchState> {
{this.state.searchResponse.posts.map(post => ( {this.state.searchResponse.posts.map(post => (
<div class="row"> <div class="row">
<div class="col-12"> <div class="col-12">
<PostListing post={post} showCommunity /> <PostListing
post={post}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
</div> </div>
</div> </div>
))} ))}
@ -455,7 +480,7 @@ export class Search extends Component<any, SearchState> {
this.state.searchResponse = data; this.state.searchResponse = data;
this.state.loading = false; this.state.loading = false;
document.title = `${i18n.t('search')} - ${this.state.q} - ${ document.title = `${i18n.t('search')} - ${this.state.q} - ${
WebSocketService.Instance.site.name this.state.site.name
}`; }`;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
@ -467,6 +492,11 @@ export class Search extends Component<any, SearchState> {
let data = res.data as PostResponse; let data = res.data as PostResponse;
createPostLikeFindRes(data, this.state.searchResponse.posts); createPostLikeFindRes(data, this.state.searchResponse.posts);
this.setState(this.state); this.setState(this.state);
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
document.title = `${i18n.t('search')} - ${data.site.name}`;
} }
} }
} }

View file

@ -19,6 +19,7 @@ interface SidebarProps {
moderators: Array<CommunityUser>; moderators: Array<CommunityUser>;
admins: Array<UserView>; admins: Array<UserView>;
online: number; online: number;
enableNsfw: boolean;
} }
interface SidebarState { interface SidebarState {
@ -53,6 +54,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
community={this.props.community} community={this.props.community}
onEdit={this.handleEditCommunity} onEdit={this.handleEditCommunity}
onCancel={this.handleEditCancel} onCancel={this.handleEditCancel}
enableNsfw={this.props.enableNsfw}
/> />
)} )}
</div> </div>

View file

@ -1,8 +1,15 @@
import { Component } from 'inferno'; import { Component } from 'inferno';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take } from 'rxjs/operators';
import { WebSocketService } from '../services'; import { WebSocketService } from '../services';
import {
GetSiteResponse,
WebSocketJsonResponse,
UserOperation,
} from '../interfaces';
import { i18n } from '../i18next'; import { i18n } from '../i18next';
import { T } from 'inferno-i18next'; import { T } from 'inferno-i18next';
import { repoUrl } from '../utils'; import { repoUrl, wsJsonToRes, toast } from '../utils';
interface SilverUser { interface SilverUser {
name: string; name: string;
@ -33,17 +40,28 @@ let silver: Array<SilverUser> = [
// let latinum = []; // let latinum = [];
export class Sponsors extends Component<any, any> { export class Sponsors extends Component<any, any> {
private subscription: Subscription;
constructor(props: any, context: any) { constructor(props: any, context: any) {
super(props, context); super(props, context);
this.subscription = WebSocketService.Instance.subject
.pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
.subscribe(
msg => this.parseMessage(msg),
err => console.error(err),
() => console.log('complete')
);
WebSocketService.Instance.getSite();
} }
componentDidMount() { componentDidMount() {
document.title = `${i18n.t('sponsors')} - ${
WebSocketService.Instance.site.name
}`;
window.scrollTo(0, 0); window.scrollTo(0, 0);
} }
componentWillUnmount() {
this.subscription.unsubscribe();
}
render() { render() {
return ( return (
<div class="container text-center"> <div class="container text-center">
@ -153,4 +171,16 @@ export class Sponsors extends Component<any, any> {
</div> </div>
); );
} }
parseMessage(msg: WebSocketJsonResponse) {
console.log(msg);
let res = wsJsonToRes(msg);
if (msg.error) {
toast(i18n.t(msg.error), 'danger');
return;
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
document.title = `${i18n.t('sponsors')} - ${data.site.name}`;
}
}
} }

View file

@ -20,6 +20,8 @@ import {
DeleteAccountForm, DeleteAccountForm,
PostResponse, PostResponse,
WebSocketJsonResponse, WebSocketJsonResponse,
GetSiteResponse,
Site,
} from '../interfaces'; } from '../interfaces';
import { WebSocketService, UserService } from '../services'; import { WebSocketService, UserService } from '../services';
import { import {
@ -74,6 +76,7 @@ interface UserState {
deleteAccountLoading: boolean; deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean; deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm; deleteAccountForm: DeleteAccountForm;
site: Site;
} }
export class User extends Component<any, UserState> { export class User extends Component<any, UserState> {
@ -122,6 +125,20 @@ export class User extends Component<any, UserState> {
deleteAccountForm: { deleteAccountForm: {
password: null, password: null,
}, },
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
}; };
constructor(props: any, context: any) { constructor(props: any, context: any) {
@ -148,6 +165,7 @@ export class User extends Component<any, UserState> {
); );
this.refetch(); this.refetch();
WebSocketService.Instance.getSite();
} }
get isCurrentUser() { get isCurrentUser() {
@ -356,6 +374,8 @@ export class User extends Component<any, UserState> {
post={i.data as Post} post={i.data as Post}
admins={this.state.admins} admins={this.state.admins}
showCommunity showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/> />
) : ( ) : (
<CommentNodes <CommentNodes
@ -363,6 +383,7 @@ export class User extends Component<any, UserState> {
admins={this.state.admins} admins={this.state.admins}
noIndent noIndent
showContext showContext
enableDownvotes={this.state.site.enable_downvotes}
/> />
)} )}
</div> </div>
@ -379,6 +400,7 @@ export class User extends Component<any, UserState> {
admins={this.state.admins} admins={this.state.admins}
noIndent noIndent
showContext showContext
enableDownvotes={this.state.site.enable_downvotes}
/> />
</div> </div>
); );
@ -388,7 +410,13 @@ export class User extends Component<any, UserState> {
return ( return (
<div> <div>
{this.state.posts.map(post => ( {this.state.posts.map(post => (
<PostListing post={post} admins={this.state.admins} showCommunity /> <PostListing
post={post}
admins={this.state.admins}
showCommunity
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
/>
))} ))}
</div> </div>
); );
@ -670,7 +698,7 @@ export class User extends Component<any, UserState> {
/> />
</div> </div>
</div> </div>
{WebSocketService.Instance.site.enable_nsfw && ( {this.state.site.enable_nsfw && (
<div class="form-group"> <div class="form-group">
<div class="form-check"> <div class="form-check">
<input <input
@ -1107,7 +1135,7 @@ export class User extends Component<any, UserState> {
UserService.Instance.user.show_avatars; UserService.Instance.user.show_avatars;
this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id; this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
} }
document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`; document.title = `/u/${this.state.user.name} - ${this.state.site.name}`;
window.scrollTo(0, 0); window.scrollTo(0, 0);
this.setState(this.state); this.setState(this.state);
setupTippy(); setupTippy();
@ -1159,6 +1187,10 @@ export class User extends Component<any, UserState> {
this.state.deleteAccountShowConfirm = false; this.state.deleteAccountShowConfirm = false;
this.setState(this.state); this.setState(this.state);
this.context.router.history.push('/'); this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.setState(this.state);
} }
} }
} }

View file

@ -25,7 +25,6 @@ import {
TransferSiteForm, TransferSiteForm,
BanUserForm, BanUserForm,
SiteForm, SiteForm,
Site,
UserView, UserView,
GetRepliesForm, GetRepliesForm,
GetUserMentionsForm, GetUserMentionsForm,
@ -57,7 +56,6 @@ export class WebSocketService {
public ws: ReconnectingWebSocket; public ws: ReconnectingWebSocket;
public subject: Observable<any>; public subject: Observable<any>;
public site: Site;
public admins: Array<UserView>; public admins: Array<UserView>;
public banned: Array<UserView>; public banned: Array<UserView>;

2
ui/src/utils.ts vendored
View file

@ -923,7 +923,7 @@ export function postSort(
+a.removed - +b.removed || +a.removed - +b.removed ||
+a.deleted - +b.deleted || +a.deleted - +b.deleted ||
(communityType && +b.stickied - +a.stickied) || (communityType && +b.stickied - +a.stickied) ||
hotRankPost(b) - hotRankPost(a) b.hot_rank - a.hot_rank
); );
} }
} }

2
ui/src/version.ts vendored
View file

@ -1 +1 @@
export const version: string = 'v0.7.11'; export const version: string = 'v0.7.13';

View file

@ -1,99 +1,105 @@
{ {
"post": "Publicar", "post": "Publicar",
"remove_post": "Eliminar publicació", "remove_post": "Suprimir publicació",
"no_posts": "Sense publicacions.", "no_posts": "No hi han publicacions.",
"create_a_post": "Crear una publicació", "create_a_post": "Crear una publicació",
"create_post": "Crear Publicació", "create_post": "Crear publicació",
"number_of_posts": "{{count}} Publicacions", "number_of_posts": "{{count}} Publicació",
"number_of_posts_plural": "{{count}} Publicacions",
"posts": "Publicacions", "posts": "Publicacions",
"related_posts": "Aquestes publicacions podrien estar relacionades", "related_posts": "Aquestes publicacions podrien estar relacionades",
"cross_posts": "Aquest link també ha sigut publicat en:", "cross_posts": "Aquest enllaç també sha publicat en:",
"cross_post": "cross-post", "cross_post": "cross-post",
"comments": "Comentaris", "comments": "Comentaris",
"number_of_comments": "{{count}} Comentaris", "number_of_comments": "{{count}} comentari",
"remove_comment": "Eliminar Comentaris", "number_of_comments_plural": "{{count}} comentaris",
"remove_comment": "Suprimeix el comentari",
"communities": "Comunitats", "communities": "Comunitats",
"users": "Usuaris", "users": "Usuaris",
"create_a_community": "Crear una comunitat", "create_a_community": "Crea una comunitat",
"create_community": "Crear Comunitat", "create_community": "Crea una comunitat",
"remove_community": "Eliminar Comunitat", "remove_community": "Suprimeix la comunitat",
"subscribed_to_communities": "Subscrit a <1>comunitats</1>", "subscribed_to_communities": "Subscrit a <1>comunitats</1>",
"trending_communities": "<1>Comunitats</1> en tendència", "trending_communities": "<1>Comunitats</1> en tendència",
"list_of_communities": "Llista de comunitats", "list_of_communities": "Llista de comunitats",
"number_of_communities": "{{count}} Comunitats", "number_of_communities": "{{count}} comunitat",
"number_of_communities_plural": "{{count}} comunitats",
"community_reqs": "minúscules, guió baix, i sense espais.", "community_reqs": "minúscules, guió baix, i sense espais.",
"create_private_message": "Crear Missatge Privat", "create_private_message": "Crea un missatge privat",
"send_secure_message": "Enviar Missatge Segur", "send_secure_message": "Envia un missatge segur",
"send_message": "Enviar Missatge", "send_message": "Envia el missatge",
"message": "Missatge", "message": "Missatge",
"edit": "editar", "edit": "edita",
"reply": "respondre", "reply": "respon",
"cancel": "Cancelar", "cancel": "Cancel·la",
"preview": "Previsualitzar", "preview": "Previsualitza",
"upload_image": "pujar imatge", "upload_image": "puja una imatge",
"avatar": "Avatar", "avatar": "Avatar",
"upload_avatar": "Pujar Avatar", "upload_avatar": "Puja un avatar",
"show_avatars": "Veure Avatares", "show_avatars": "Mostra els avatars",
"formatting_help": "Ajuda de format", "formatting_help": "ajuda amb la formatació",
"view_source": "veure font", "view_source": "mostran el codi font",
"unlock": "desbloquejar", "unlock": "desbloca",
"lock": "bloquejar", "lock": "bloca",
"sticky": "fijat", "sticky": "fijat",
"unsticky": "no fijat", "unsticky": "no fijat",
"link": "link", "link": "enllaç",
"archive_link": "arxivar link", "archive_link": "enllaç de larxiu",
"mod": "moderador", "mod": "moderador",
"mods": "moderadores", "mods": "moderadores",
"moderates": "Modera", "moderates": "Modera",
"settings": "Configuració", "settings": "Configuració",
"remove_as_mod": "eliminar com moderador", "remove_as_mod": "suprimeix com a moderador",
"appoint_as_mod": "designar com moderador", "appoint_as_mod": "designa com a moderador",
"modlog": "Historial de moderació", "modlog": "Historial de moderació",
"admin": "administrador", "admin": "administrador",
"admins": "administradors", "admins": "administradors",
"remove_as_admin": "eliminar com administrador", "remove_as_admin": "suprimeix com a administrador",
"appoint_as_admin": "designar com administrador", "appoint_as_admin": "designa com a administrador",
"remove": "eliminar", "remove": "suprimeix",
"removed": "eliminat", "removed": "suprimit pel moderador",
"locked": "bloquejat", "locked": "blocat",
"stickied": "fijat", "stickied": "fijat",
"reason": "Raó", "reason": "Raó",
"mark_as_read": "marcar com llegit", "mark_as_read": "marca com a llegit",
"mark_as_unread": "marcar com no llegit", "mark_as_unread": "marca com a no llegit",
"delete": "eliminar", "delete": "suprimeix",
"deleted": "eliminat", "deleted": "suprimit pel creador",
"delete_account": "Eliminar Compte", "delete_account": "Suprimeix el compte",
"delete_account_confirm": "delete_account_confirm": "Atenció: aquesta acció suprimirà permanentment la vostra informació. Introduïu la vostra contrasenya per a confirmar.",
"Avís: aquesta acció eliminarà permanentment la teva informació. Introdueix la teva contrasenya per a continuar", "restore": "restaura",
"restore": "restaurar", "ban": "expulsa",
"ban": "expulsar", "ban_from_site": "expulsa del lloc",
"ban_from_site": "expulsar del lloc", "unban": "readmet",
"unban": "admetre", "unban_from_site": "readmet al lloc",
"unban_from_site": "admetre al lloc",
"banned": "expulsat", "banned": "expulsat",
"save": "guardar", "save": "desa",
"unsave": "descartar", "unsave": "descarta",
"create": "crear", "create": "crea",
"creator": "creador", "creator": "creador",
"username": "Nom d'Usuari", "username": "Nom dusuari",
"email_or_username": "Correu o Usuari", "email_or_username": "Adreça electrònica o usuari",
"number_of_users": "{{count}} Usuaris", "number_of_users": "{{count}} usuari",
"number_of_subscribers": "{{count}} Subscriptors", "number_of_users_plural": "{{count}} usuaris",
"number_of_points": "{{count}} Punts", "number_of_subscribers": "{{count}} subscriptor",
"number_online": "{{count}} Usauris En Línia", "number_of_subscribers_plural": "{{count}} subscriptors",
"number_of_points": "{{count}} punt",
"number_of_points_plural": "{{count}} punts",
"number_online": "{{count}} usuari en línia",
"number_online_plural": "{{count}} usuaris en línia",
"name": "Nom", "name": "Nom",
"title": "Titol", "title": "Títol",
"category": "Categoria", "category": "Categoria",
"subscribers": "Suscriptors", "subscribers": "Subscriptors",
"both": "Ambdos", "both": "Tots dos",
"saved": "Guardat", "saved": "Desat",
"unsubscribe": "Desubscriure's", "unsubscribe": "Dónat de baixa",
"subscribe": "Subscriure's", "subscribe": "Subscriu-thi",
"subscribed": "Subscrit", "subscribed": "Subscrit",
"prev": "Anterior", "prev": "Anterior",
"next": "Següent", "next": "Següent",
"sidebar": "Descripció de la comunitat", "sidebar": "Barra lateral",
"sort_type": "Tipus d'orden", "sort_type": "Tipus dordenació",
"hot": "Popular", "hot": "Popular",
"new": "Nou", "new": "Nou",
"top_day": "El millor del dia", "top_day": "El millor del dia",
@ -103,75 +109,71 @@
"all": "Tot", "all": "Tot",
"top": "Millor", "top": "Millor",
"api": "API", "api": "API",
"docs": "Docs", "docs": "Documentació",
"inbox": "Bústia d'entrada", "inbox": "Bústia dentrada",
"inbox_for": "Bústia d'entrada per a <1>{{user}}</1>", "inbox_for": "Bústia dentrada per a <1>{{user}}</1>",
"mark_all_as_read": "marcar tot com llegit", "mark_all_as_read": "marca-ho tot com a llegit",
"type": "Tipus", "type": "Tipus",
"unread": "No llegit", "unread": "No llegit",
"replies": "Respostes", "replies": "Respostes",
"mentions": "Menciones", "mentions": "Mencions",
"reply_sent": "Resposta enviada", "reply_sent": "La resposta ha estat enviada",
"message_sent": "Missatge enviado", "message_sent": "El missatge ha estat enviat",
"search": "Buscar", "search": "Cerca",
"overview": "Resum", "overview": "Visió de conjunt",
"view": "Vista", "view": "Vista",
"logout": "Tancar sessió", "logout": "Finalitza la sessió",
"login_sign_up": "Iniciar sessió / Crear compte", "login_sign_up": "Inicia una sessió / crea un compte",
"login": "Iniciar sessió", "login": "Inicia una sessió",
"sign_up": "Crear compte", "sign_up": "Crea un compte",
"notifications_error": "notifications_error": "Les notificacions descriptori no estan disponibles al vostre navegador. Proveu amb el Firefox o el Chrome.",
"Notificacions d'escriptori no disponibles al teu navegador. Prova amb Firefox o Chrome.",
"unread_messages": "Missatges no llegits", "unread_messages": "Missatges no llegits",
"messages": "Missatges", "messages": "Missatges",
"password": "Contrasenya", "password": "Contrasenya",
"verify_password": "Verificar Contrasenya", "verify_password": "Verifica la contrasenya",
"old_password": "Antiga Contrasenya", "old_password": "Contrasenya antiga",
"forgot_password": "oblidí la meva contrasenya", "forgot_password": "vaig oblidar la meva contrasenya",
"reset_password_mail_sent": "Enviar correu per a restablir la contrasenya.", "reset_password_mail_sent": "Hem enviat un missatge per correu per a restablir la contrasenya.",
"password_change": "Canvi de Contrasenya", "password_change": "Canvi de contrasenya",
"new_password": "Nueva Contrasenya", "new_password": "Contrasenya nova",
"no_email_setup": "Aquest servidor no ha activat correctament el correu.", "no_email_setup": "Aquest servidor no ha configurat correctament el correu.",
"email": "Correu electrònic", "email": "Correu electrònic",
"matrix_user_id": "Usuari Matricial", "matrix_user_id": "Usuari del Matrix",
"private_message_disclaimer": "private_message_disclaimer": "Atenció: els missatges privats al Lemmy no són segurs. Creeu-vos un compte a <1>Riot.im</1> per a una missatgeria segura.",
"Avís: Els missatges privats en Lemmy no són segurs. Sisplau creu un compte en <1>Riot.im</1> per a mensajeria segura.", "send_notifications_to_email": "Envia notificacions al correu",
"send_notifications_to_email": "Enviar notificacions al correu",
"optional": "Opcional", "optional": "Opcional",
"expires": "Expira", "expires": "Expira",
"language": "Llenguatge", "language": "Llengua",
"browser_default": "Per defecte del navegador", "browser_default": "Per defecte del navegador",
"downvotes_disabled": "Vots negatius deshabilitats", "downvotes_disabled": "Vots negatius inhabilitats",
"enable_downvotes": "Habilitar vots negatius", "enable_downvotes": "Habilita els vots negatius",
"open_registration": "Obrir registre", "open_registration": "Obre els registres",
"registration_closed": "Registre tancat", "registration_closed": "Shan tancat els registres",
"enable_nsfw": "Habilitar NSFW", "enable_nsfw": "Habilita el contingut per a adults",
"url": "URL", "url": "URL",
"body": "Descripció", "body": "Corps",
"copy_suggested_title": "Copiar el títol sugerido: {{title}}", "copy_suggested_title": "copia el títol suggerit: {{title}}",
"community": "Comunitat", "community": "Comunitat",
"expand_here": "Expandir ací", "expand_here": "Expandeix ací",
"subscribe_to_communities": "Subscriure's a algunes <1>comunitats</1>.", "subscribe_to_communities": "Subscriviu-vos a algunes <1>comunitats</1>.",
"chat": "Chat", "chat": "Xat",
"recent_comments": "Comentaris recients", "recent_comments": "Comentaris recients",
"no_results": "Sense resultats.", "no_results": "Sense resultats.",
"setup": "Configurar", "setup": "Configurar",
"lemmy_instance_setup": "Configuració d'instancia de Lemmy", "lemmy_instance_setup": "Configuració dinstància del Lemmy",
"setup_admin": "Configurar administrador del Lloc", "setup_admin": "Configura administrador del lloc",
"your_site": "el teu lloc", "your_site": "el vostre lloc",
"modified": "modificat", "modified": "modificat",
"nsfw": "NSFW", "nsfw": "Per a adults",
"show_nsfw": "Mostrar contingut NSFW", "show_nsfw": "Mostra el contingut per a adults",
"theme": "Tema", "theme": "Tema",
"sponsors": "Patrocinadors", "sponsors": "Patrocinadors",
"sponsors_of_lemmy": "Patrocinadors de Lemmy", "sponsors_of_lemmy": "Patrocinadors del Lemmy",
"sponsor_message": "sponsor_message": "El Lemmy és programari lliure i de <1>codi obert</1>, la qual cosa significa que no tindrà publicitats, monetització, ni capitals emprenedors, mai. Les vostres donacions donen suport directament al desenvolupament a temps complet del projecte. Moltes gràcies a les següents persones:",
"Lemmy és programari lliure i de <1>codi obert</1>, la qual cosa significa que no tindrà publicitats, monetització, ni capitals emprenedors, mai. Les teves donacions secunden directament el desenvolupament a temps complet del projecte. Moltes gràcies a les següents persones:", "support_on_patreon": "Suport al Patreon",
"support_on_patreon": "Suport a Patreon",
"donate_to_lemmy": "Donar a Lemmy", "donate_to_lemmy": "Donar a Lemmy",
"donate": "Donar", "donate": "Dona",
"general_sponsors": "general_sponsors": "Els patrocinadors generals són aquells que es van comprometre a donar entre 10 y 39 $ al Lemmy.",
"Los Patrocinadores Generales son aquellos que señaron entre $10 y $39 a Lemmy.",
"crypto": "Crypto", "crypto": "Crypto",
"bitcoin": "Bitcoin", "bitcoin": "Bitcoin",
"ethereum": "Ethereum", "ethereum": "Ethereum",
@ -181,57 +183,78 @@
"by": "per", "by": "per",
"to": "a", "to": "a",
"from": "des de", "from": "des de",
"transfer_community": "transferir comunitat", "transfer_community": "transfereix la comunitat",
"transfer_site": "transferir lloc", "transfer_site": "transfereix el lloc",
"are_you_sure": "Ets segur?", "are_you_sure": "nesteu segur?",
"yes": "sí", "yes": "sí",
"no": "no", "no": "no",
"powered_by": "Impulsat per", "powered_by": "Funciona amb",
"landing_0": "landing_0": "Lemmy és un <1>agregador de links</1> / alternativa a reddit, amb la intenció de funcionar al <2>fedivers</2>.<3></3>És allotjable per un mateix (sense necessitat de grans companyies), té actualització en directe de cadenes de comentaris, i és petit (<4>~80kB</4>). Federar amb el sistema de xarxes ActivityPub forma part dels objectius del projecte. <5></5>Aquesta és una <6>versió beta molt prematura</6>, i actualment moltes de les característiques són trencades o falten. <7></7>Suggereix noves característiques o reporta errors <8>aquí</8>.<9></9>Fet amb <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
"Lemmy és un <1>agregador de links</1> / alternativa a reddit, amb la intenció de funcionar al <2>fedivers</2>.<3></3>És allotjable per un mateix (sense necessitat de grans companyies), té actualització en directe de cadenes de comentaris, i és petit (<4>~80kB</4>). Federar amb el sistema de xarxes ActivityPub forma part dels objectius del projecte. <5></5>Aquesta és una <6>versió beta molt prematura</6>, i actualment moltes de les característiques són trencades o falten. <7></7>Suggereix noves característiques o reporta errors <8>aquí</8>.<9></9>Fet amb <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.", "not_logged_in": "No heu iniciat una sessió.",
"not_logged_in": "No has iniciat sessió.", "logged_in": "Heu iniciat una sessió.",
"logged_in": "Has iniciat sessió.", "community_ban": "Us han expulsat daquesta comunitat.",
"community_ban": "Has sigut expulsat d'aquesta comunitat.", "site_ban": "Us han expulsat del lloc",
"site_ban": "Has sigut expulsat d'aquest lloc.", "couldnt_create_comment": "No sha pogut crear el comentari.",
"couldnt_create_comment": "No s'ha pogut crear el comentari.", "couldnt_like_comment": "No sha pogut donar «magrada» al comentari.",
"couldnt_like_comment": "No s'ha pogut donar m'agrada al comentari.", "couldnt_update_comment": "No sha pogut actualitzar el comentari.",
"couldnt_update_comment": "No s'ha pogut actualitzar el comentari.", "couldnt_save_comment": "No sha pogut desar el comentari.",
"couldnt_save_comment": "No s'ha pogut guardar el comentari.", "no_comment_edit_allowed": "No teniu permisos per a editar el comentari.",
"no_comment_edit_allowed": "No tens permisos per a editar el comentari.", "no_post_edit_allowed": "No teniu permisos per a editar lapunt.",
"no_post_edit_allowed": "No tens permisos per a editar la publicació.", "no_community_edit_allowed": "No teniu permisos per a editar la comunitat.",
"no_community_edit_allowed": "No tens permisos per a editar la comunitat.", "couldnt_find_community": "No sha pogut trobar la comunitat.",
"couldnt_find_community": "No s'ha pogut trobar la comunitat.", "couldnt_update_community": "No sha pogut actualitzar la comunitat.",
"couldnt_update_community": "No s'ha pogut actualitzar la comunitat.",
"community_already_exists": "Aquesta comunitat ja existeix.", "community_already_exists": "Aquesta comunitat ja existeix.",
"community_moderator_already_exists": "community_moderator_already_exists": "Aquest moderador de la comunitat ja existeix.",
"Aquest moderador de la comunitat ja existeix.", "community_follower_already_exists": "Aquest seguidor de la comunitat ja existeix.",
"community_follower_already_exists": "community_user_already_banned": "Aquest usuari de la comunitat ja fou expulsat.",
"Aquest seguidor de la comunitat ja existeix.", "couldnt_create_post": "No sha pogut crear lapunt.",
"community_user_already_banned": "couldnt_like_post": "No sha pogut donar «magrada» a lapunt.",
"Aquest usuari de la comunitat ja fou expulsat.", "couldnt_find_post": "No sha pogut trobar lapunt.",
"couldnt_create_post": "No s'ha pogut crear la publicació.", "couldnt_get_posts": "No shan pogut recuperar els apunts",
"couldnt_like_post": "No s'ha pogut donar m'agrada a la publicació.", "couldnt_update_post": "No sha pogut actualitzar lapunt",
"couldnt_find_post": "No s'ha pogut trobar la publicació.", "couldnt_save_post": "No sha pogut desar lapunt.",
"couldnt_get_posts": "No s'han pogut obtindre les publicacions.",
"couldnt_update_post": "No s'ha pogut actualitzar la publicació.",
"couldnt_save_post": "No s'ha pogut guardar la publicació.",
"no_slurs": "Prohibit insultar.", "no_slurs": "Prohibit insultar.",
"not_an_admin": "No és un administrador.", "not_an_admin": "No és un administrador.",
"site_already_exists": "El lloc ja existeix.", "site_already_exists": "El lloc ja existeix.",
"couldnt_update_site": "No s'ha pogut actualitzar el lloc.", "couldnt_update_site": "No sha pogut actualitzar el lloc.",
"couldnt_find_that_username_or_email": "couldnt_find_that_username_or_email": "No sha pogut trobar aquest nom de usuari o adreça electrònica.",
"No s'ha pogut trobar aquest nom de usuari o correu electrònic.",
"password_incorrect": "Contrasenya incorrecta.", "password_incorrect": "Contrasenya incorrecta.",
"passwords_dont_match": "Les contrasenyes no coincideixen.", "passwords_dont_match": "Les contrasenyes no coincideixen.",
"admin_already_created": "Ho sentim, ja hi ha un adminisitrador.", "admin_already_created": "Disculpeu; ja hi ha un adminisitrador.",
"user_already_exists": "L'usuari ja existeix.", "user_already_exists": "Lusuari ja existeix.",
"email_already_exists": "El correu ja és en ús.", "email_already_exists": "Ladreça ja és en ús.",
"couldnt_update_user": "No s'ha pogut actualitzar l'usuari.", "couldnt_update_user": "No sha pogut actualitzar lusuari.",
"system_err_login": "system_err_login": "Error del sistema. Intenti tancar sessió i ingressar de nou.",
"Error del sistema. Intenti tancar sessió i ingressar de nou.", "couldnt_create_private_message": "No sha pogut crear el missatge privat.",
"couldnt_create_private_message": "No s'ha pogut crear el missatge privat.", "no_private_message_edit_allowed": "No teniu permisos per a editar el missatge privat.",
"no_private_message_edit_allowed": "couldnt_update_private_message": "No sha pogut actualitzar el missatge privat.",
"Sense permisos per a editar el missatge privat.", "cross_posted_to": "publicat també a: ",
"couldnt_update_private_message": "invalid_username": "El nom dusuari no és vàlid.",
"No s'ha pogut actualitzar el missatge privat." "click_to_delete_picture": "Feu clic per a suprimir la imatge.",
"picture_deleted": "Sha suprimit la imatge.",
"invalid_community_name": "El nom no és vàlid.",
"sorting_help": "ajuda amb lordenament",
"old": "Antic",
"more": "més",
"show_context": "Mostra el context",
"upvote": "Dona un vot positiu",
"number_of_upvotes": "{{count}} vot positiu",
"number_of_upvotes_plural": "{{count}} vots positius",
"downvote": "Dona un vot negatiu",
"number_of_downvotes": "{{count}} vot negatiu",
"number_of_downvotes_plural": "{{count}} vots negatius",
"admin_settings": "Configuració administrativa",
"site_config": "Configuració del lloc",
"banned_users": "Usuaris expulsats",
"support_on_liberapay": "Suport al Liberapay",
"support_on_open_collective": "Suport a lOpenCollective",
"silver_sponsors": "Els patrocinadors «silver» són els que van comprometres a donar 40 $ al Lemmy.",
"site_saved": "Sha desat el lloc.",
"couldnt_get_comments": "No shan pogut recuperar els comentaris.",
"post_title_too_long": "El títol de lapunt és massa llarg.",
"time": "Hora",
"action": "Acció",
"emoji_picker": "Selector demojis",
"block_leaving": "Segur que voleu sortir?",
"select_a_community": "Seleccioneu una comunitat"
} }

View file

@ -25,26 +25,26 @@
"number_of_communities": "{{count}} Comunidad", "number_of_communities": "{{count}} Comunidad",
"number_of_communities_plural": "{{count}} Comunidades", "number_of_communities_plural": "{{count}} Comunidades",
"community_reqs": "minúsculas, guión bajo, y sin espacios.", "community_reqs": "minúsculas, guión bajo, y sin espacios.",
"create_private_message": "Crear Mensaje Privado", "create_private_message": "Crear mensaje privado",
"send_secure_message": "Enviar Mensaje Seguro", "send_secure_message": "Enviar mensaje seguro",
"send_message": "Enviar Mensaje", "send_message": "Enviar mensaje",
"message": "Mensaje", "message": "Mensaje",
"edit": "editar", "edit": "editar",
"reply": "responder", "reply": "responder",
"cancel": "Cancelar", "cancel": "Cancelar",
"preview": "Previsualizar", "preview": "Previsualizar",
"upload_image": "subir imagen", "upload_image": "cargar imagen",
"avatar": "Avatar", "avatar": "Avatar",
"upload_avatar": "Subir Avatar", "upload_avatar": "Cargar avatar",
"show_avatars": "Ver Avatares", "show_avatars": "Mostrar avatares",
"formatting_help": "Ayuda de formato", "formatting_help": "ayuda de formato",
"view_source": "ver fuente", "view_source": "ver fuente",
"unlock": "desbloquear", "unlock": "desbloquear",
"lock": "bloquear", "lock": "bloquear",
"sticky": "fijado", "sticky": "fijado",
"unsticky": "no fijado", "unsticky": "no fijado",
"link": "link", "link": "enlace",
"archive_link": "archivar link", "archive_link": "enlace de archivo",
"mod": "moderador", "mod": "moderador",
"mods": "moderadores", "mods": "moderadores",
"moderates": "Modera", "moderates": "Modera",
@ -65,8 +65,8 @@
"mark_as_unread": "marcar como no leído", "mark_as_unread": "marcar como no leído",
"delete": "eliminar", "delete": "eliminar",
"deleted": "eliminado por creador", "deleted": "eliminado por creador",
"delete_account": "Eliminar Cuenta", "delete_account": "Eliminar cuenta",
"delete_account_confirm": "Advertencia: esta acción eliminará permanentemente toda tu información. Introduce tu contraseña para confirmar.", "delete_account_confirm": "Advertencia: esta acción eliminará permanentemente toda su información. Introduzca su contraseña para confirmar.",
"restore": "restaurar", "restore": "restaurar",
"ban": "expulsar", "ban": "expulsar",
"ban_from_site": "expulsar del sitio", "ban_from_site": "expulsar del sitio",
@ -77,16 +77,16 @@
"unsave": "descartar", "unsave": "descartar",
"create": "crear", "create": "crear",
"creator": "creador", "creator": "creador",
"username": "Nombre de Usuario", "username": "Nombre de usuario",
"email_or_username": "Correo o Usuario", "email_or_username": "Correo o usuario",
"number_of_users": "{{count}} Usuario", "number_of_users": "{{count}} usuario",
"number_of_users_plural": "{{count}} Usuarios", "number_of_users_plural": "{{count}} usuarios",
"number_of_subscribers": "{{count}} Suscriptor", "number_of_subscribers": "{{count}} suscriptor",
"number_of_subscribers_plural": "{{count}} Suscriptores", "number_of_subscribers_plural": "{{count}} suscriptores",
"number_of_points": "{{count}} Punto", "number_of_points": "{{count}} punto",
"number_of_points_plural": "{{count}} Puntos", "number_of_points_plural": "{{count}} puntos",
"number_online": "{{count}} Usuario En Línea", "number_online": "{{count}} usuario en línea",
"number_online_plural": "{{count}} Usuarios En Línea", "number_online_plural": "{{count}} usuarios en línea",
"name": "Nombre", "name": "Nombre",
"title": "Titulo", "title": "Titulo",
"category": "Categoría", "category": "Categoría",
@ -98,7 +98,7 @@
"subscribed": "Suscrito", "subscribed": "Suscrito",
"prev": "Anterior", "prev": "Anterior",
"next": "Siguiente", "next": "Siguiente",
"sidebar": "Descripción de la comunidad", "sidebar": "Barra lateral",
"sort_type": "Tipo de orden", "sort_type": "Tipo de orden",
"hot": "Popular", "hot": "Popular",
"new": "Nuevo", "new": "Nuevo",
@ -109,7 +109,7 @@
"all": "Todo", "all": "Todo",
"top": "Mejor", "top": "Mejor",
"api": "API", "api": "API",
"docs": "Docs", "docs": "Documentación",
"inbox": "Buzón de entrada", "inbox": "Buzón de entrada",
"inbox_for": "Buzón de entrada para <1>{{user}}</1>", "inbox_for": "Buzón de entrada para <1>{{user}}</1>",
"mark_all_as_read": "marcar todo como leído", "mark_all_as_read": "marcar todo como leído",
@ -161,11 +161,11 @@
"no_results": "Sin resultados.", "no_results": "Sin resultados.",
"setup": "Configurar", "setup": "Configurar",
"lemmy_instance_setup": "Configuración de instancia de Lemmy", "lemmy_instance_setup": "Configuración de instancia de Lemmy",
"setup_admin": "Configurar administrador del Sitio", "setup_admin": "Configurar administrador del sitio",
"your_site": "tu sitio", "your_site": "su sitio",
"modified": "modificado", "modified": "modificado",
"nsfw": "NSFW", "nsfw": "Para adultos",
"show_nsfw": "Mostrar contenido NSFW", "show_nsfw": "Mostrar contenido para adultos",
"theme": "Tema", "theme": "Tema",
"sponsors": "Patrocinadores", "sponsors": "Patrocinadores",
"sponsors_of_lemmy": "Patrocinadores de Lemmy", "sponsors_of_lemmy": "Patrocinadores de Lemmy",
@ -186,7 +186,7 @@
"from": "desde", "from": "desde",
"transfer_community": "transferir comunidad", "transfer_community": "transferir comunidad",
"transfer_site": "transferir sitio", "transfer_site": "transferir sitio",
"are_you_sure": "¿Estás seguro?", "are_you_sure": "¿está seguro?",
"yes": "sí", "yes": "sí",
"no": "no", "no": "no",
"powered_by": "Impulsado por", "powered_by": "Impulsado por",
@ -234,7 +234,7 @@
"action": "Acción", "action": "Acción",
"more": "más", "more": "más",
"cross_posted_to": "publicado también en: ", "cross_posted_to": "publicado también en: ",
"sorting_help": "ayuda del orden", "sorting_help": "ayuda de ordenación",
"upvote": "Voto Positivo", "upvote": "Voto Positivo",
"number_of_upvotes": "{{count}} Voto Positivo", "number_of_upvotes": "{{count}} Voto Positivo",
"number_of_upvotes_plural": "{{count}} Votos Positivos", "number_of_upvotes_plural": "{{count}} Votos Positivos",
@ -246,15 +246,15 @@
"block_leaving": "¿Está seguro de que desea salir?", "block_leaving": "¿Está seguro de que desea salir?",
"show_context": "Mostrar contexto", "show_context": "Mostrar contexto",
"silver_sponsors": "Sponsors Plata son los que han dado $40 a Lemmy.", "silver_sponsors": "Sponsors Plata son los que han dado $40 a Lemmy.",
"site_config": "Configuración del Sitio", "site_config": "Configuración del sitio",
"banned_users": "Usuarios Baneados", "banned_users": "Usuarios expulsados",
"support_on_open_collective": "Dona en OpenCollective", "support_on_open_collective": "Apoyo en OpenCollective",
"site_saved": "Sitio Guardado.", "site_saved": "Sitio Guardado.",
"emoji_picker": "Lista de emojis", "emoji_picker": "Selector de emoyis",
"admin_settings": "Panel de Administración", "admin_settings": "Configuración administrativa",
"select_a_community": "Selecciona una comunidad", "select_a_community": "Selecciona una comunidad",
"invalid_username": "Nombre de usuario inválido.", "invalid_username": "Nombre de usuario inválido.",
"invalid_community_name": "Nombre inválido.", "invalid_community_name": "Nombre no válido.",
"click_to_delete_picture": "Haz click para eliminar la imagen.", "click_to_delete_picture": "Pulse para eliminar la imagen.",
"picture_deleted": "Foto eliminada." "picture_deleted": "Imagen eliminada."
} }

View file

@ -156,7 +156,7 @@
"body": "Texte", "body": "Texte",
"copy_suggested_title": "copier le titre suggéré : {{title}}", "copy_suggested_title": "copier le titre suggéré : {{title}}",
"community": "Communauté", "community": "Communauté",
"expand_here": "Développer ici", "expand_here": "Ouvrir ici",
"subscribe_to_communities": "Sabonner à quelques <1>communautés</1>.", "subscribe_to_communities": "Sabonner à quelques <1>communautés</1>.",
"chat": "Chat", "chat": "Chat",
"recent_comments": "Commentaires récents", "recent_comments": "Commentaires récents",
@ -244,7 +244,7 @@
"sorting_help": "aide au tri", "sorting_help": "aide au tri",
"upvote": "Voter pour", "upvote": "Voter pour",
"show_context": "Afficher le contexte", "show_context": "Afficher le contexte",
"block_leaving": "Etes-vous sûr de vouloir partir ?", "block_leaving": "Êtes-vous sûr de vouloir partir ?",
"number_of_upvotes": "{{count}} Votes pour", "number_of_upvotes": "{{count}} Votes pour",
"number_of_upvotes_plural": "{{count}} Votes contre", "number_of_upvotes_plural": "{{count}} Votes contre",
"number_of_downvotes": "{{count}} Vote contre", "number_of_downvotes": "{{count}} Vote contre",

View file

@ -1,11 +1,11 @@
{ {
"remove_post": "Hiqe Postimin", "remove_post": "Hiqe Postimin",
"no_posts": "Nuk ka Postime.", "no_posts": "Nuk ka Postime.",
"create_a_post": "Krijo Postim", "create_a_post": "Krijo një Postim",
"create_post": "Krijo Postim", "create_post": "Krijo Postim",
"posts": "Postime", "posts": "Postime",
"related_posts": "Këto postime mund të jenë të lidhura", "related_posts": "Këto postime mund të jenë të lidhura",
"cross_posts": "Ky link është postuar edhe te:", "cross_posts": "Ky link është postuar gjithashtu në:",
"cross_post": "shumë-postim", "cross_post": "shumë-postim",
"cross_posted_to": "shumë-postuar në: ", "cross_posted_to": "shumë-postuar në: ",
"comments": "Komentet", "comments": "Komentet",

View file

@ -4,13 +4,15 @@
"no_posts": "Inga inlägg.", "no_posts": "Inga inlägg.",
"create_a_post": "Skriv ett inlägg", "create_a_post": "Skriv ett inlägg",
"create_post": "Skapa inlägg", "create_post": "Skapa inlägg",
"number_of_posts": "{{count}} inlägg", "number_of_posts": "{{count}} Inlägg",
"number_of_posts_plural": "{{count}} Inlägg",
"posts": "Inlägg", "posts": "Inlägg",
"related_posts": "Dessa inlägg kan vara relaterade", "related_posts": "Dessa inlägg kan vara relaterade",
"cross_posts": "Den här länken har även publicerats i:", "cross_posts": "Den här länken har även publicerats i:",
"cross_post": "tvärinlägg", "cross_post": "tvärposta",
"comments": "Kommentarer", "comments": "Kommentarer",
"number_of_comments": "{{count}} kommentarer", "number_of_comments": "{{count}} Kommentar",
"number_of_comments_plural": "{{count}} Kommentarer",
"remove_comment": "Radera kommentar", "remove_comment": "Radera kommentar",
"communities": "Gemenskaper", "communities": "Gemenskaper",
"users": "Användare", "users": "Användare",
@ -20,7 +22,8 @@
"subscribed_to_communities": "Prenumererar på <1>gemenskaper</1>", "subscribed_to_communities": "Prenumererar på <1>gemenskaper</1>",
"trending_communities": "Populära <1>gemenskaper</1>", "trending_communities": "Populära <1>gemenskaper</1>",
"list_of_communities": "Lista över gemenskaper", "list_of_communities": "Lista över gemenskaper",
"number_of_communities": "{{count}} gemenskaper", "number_of_communities": "{{count}} Gemenskap",
"number_of_communities_plural": "{{count}} Gemenskaper",
"community_reqs": "gemener, understreck och inga blanksteg.", "community_reqs": "gemener, understreck och inga blanksteg.",
"edit": "redigera", "edit": "redigera",
"reply": "svara", "reply": "svara",
@ -46,33 +49,36 @@
"remove_as_admin": "tag bort som administratör", "remove_as_admin": "tag bort som administratör",
"appoint_as_admin": "lägg till som administratör", "appoint_as_admin": "lägg till som administratör",
"remove": "ta bort", "remove": "ta bort",
"removed": "borttagen", "removed": "borttagen av moderator",
"locked": "låst", "locked": "låst",
"stickied": "fastnålad", "stickied": "fastnålad",
"reason": "Anledning", "reason": "Anledning",
"mark_as_read": "markera som läst", "mark_as_read": "markera som läst",
"mark_as_unread": "markera som oläst", "mark_as_unread": "markera som oläst",
"delete": "radera", "delete": "radera",
"deleted": "raderad", "deleted": "raderad av skapare",
"delete_account": "Ta bort konto", "delete_account": "Ta bort konto",
"delete_account_confirm": "delete_account_confirm": "Varning: den här åtgärden kommer radera alla dina data permanent. Skriv in ditt lösenord för att bekräfta.",
"Varning: den här åtgärden kommer radera alla dina data permanent. Är du säker?",
"restore": "återställ", "restore": "återställ",
"ban": "blockera", "ban": "blockera",
"ban_from_site": "blockera från webbplats", "ban_from_site": "blockera från webbplats",
"unban": "ta bort blockering", "unban": "ta bort blockering",
"unban_from_site": "ta bort blockering från webbplats", "unban_from_site": "ta bort blockering från webbplats",
"banned": "blocerad", "banned": "blockerad",
"save": "spara", "save": "spara",
"unsave": "förkasta", "unsave": "förkasta",
"create": "skapa", "create": "skapa",
"creator": "skapare", "creator": "skapare",
"username": "Användarnamn", "username": "Användarnamn",
"email_or_username": "E-postadress eller användarnamn", "email_or_username": "E-postadress eller användarnamn",
"number_of_users": "{{count}} användare", "number_of_users": "{{count}} Användare",
"number_of_subscribers": "{{count}} prenumeranter", "number_of_users_plural": "{{count}} Användare",
"number_of_points": "{{count}} poäng", "number_of_subscribers": "{{count}} Prenumerant",
"number_online": "{{count}} användare inloggade", "number_of_subscribers_plural": "{{count}} Prenumeranter",
"number_of_points": "{{count}} Poäng",
"number_of_points_plural": "{{count}} Poäng",
"number_online": "{{count}} Användare inloggad",
"number_online_plural": "{{count}} Användare inloggade",
"name": "Namn", "name": "Namn",
"title": "Titel", "title": "Titel",
"category": "Kategori", "category": "Kategori",
@ -108,8 +114,7 @@
"login_sign_up": "Logga in eller skapa konto", "login_sign_up": "Logga in eller skapa konto",
"login": "Logga in", "login": "Logga in",
"sign_up": "Skapa konto", "sign_up": "Skapa konto",
"notifications_error": "notifications_error": "Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.",
"Din webbläsare har inte stöd för skrivbordsaviseringar. Testa Firefox eller Chrome.",
"unread_messages": "Olästa meddelanden", "unread_messages": "Olästa meddelanden",
"password": "Lösenord", "password": "Lösenord",
"verify_password": "Bekräfta lösenord", "verify_password": "Bekräfta lösenord",
@ -135,11 +140,9 @@
"theme": "Utseende", "theme": "Utseende",
"sponsors": "Sponsorer", "sponsors": "Sponsorer",
"sponsors_of_lemmy": "Lemmys sponsorer", "sponsors_of_lemmy": "Lemmys sponsorer",
"sponsor_message": "sponsor_message": "Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:",
"Lemmy är fri mjukvara med <1>öppen källkod</1>, vilket innebär att ingen reklam, vinstindrivning eller venturekapital förekommer, någonsin. Dina donationer går direkt till att stöda utvecklingen av projektet. Stort tack till följande personer:",
"support_on_patreon": "Stöd på Patreon", "support_on_patreon": "Stöd på Patreon",
"general_sponsors": "general_sponsors": "Allmänna sponsorer är de som donerat mellan 10 och 39 dollar till Lemmy.",
"Allmänna sponsorer är dem som givit mellan 10 och 39\u00a0dollar till Lemmy.",
"crypto": "Kryptovaluta", "crypto": "Kryptovaluta",
"bitcoin": "Bitcoin", "bitcoin": "Bitcoin",
"ethereum": "Ethereum", "ethereum": "Ethereum",
@ -154,16 +157,15 @@
"yes": "ja", "yes": "ja",
"no": "nej", "no": "nej",
"powered_by": "Drivs av", "powered_by": "Drivs av",
"landing_0": "landing_0": "Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80 kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.",
"Lemmy är en <1>länksamlare</1> och alternativ till reddit, ämnad att fungera i <2>Fediversumet</2>.<3></3>Lemmy kan drivas av vem som helst, har kommentarstrådar som updateras i realid och är mycket liten (<4>ca 80\u00a0kB</4>). Federering med ActivityPub-nätverket är planerat. <5></5>Detta är en <6>väldigt tidig betaversion</6> och många funktioner saknas därför eller är trasiga.<7></7>Föreslå nya funktioner eller anmäl buggar <8>här</8>.<9></9>Skapad i <10>Rust</10>, <11>Actix</11>, <12>Inferno</12> och <13>Typescript</13>.",
"not_logged_in": "Inte inloggad.", "not_logged_in": "Inte inloggad.",
"community_ban": "Du har blockerats från den här gemenskapen.", "community_ban": "Du har blockerats från den här gemenskapen.",
"site_ban": "Du har blockerats från webbplatsen.", "site_ban": "Du har blockerats från webbplatsen",
"couldnt_create_comment": "Kunde inte skapa kommentar.", "couldnt_create_comment": "Kunde inte skapa kommentar.",
"couldnt_like_comment": "Kunde inte gilla kommentar.", "couldnt_like_comment": "Kunde inte gilla kommentar.",
"couldnt_update_comment": "Kunde inte uppdatera kommentar.", "couldnt_update_comment": "Kunde inte uppdatera kommentar.",
"couldnt_save_comment": "Kunde inte spara kommentar.", "couldnt_save_comment": "Kunde inte spara kommentar.",
"no_comment_edit_allowed": "Har inte behörighet att redigera komentar.", "no_comment_edit_allowed": "Har inte behörighet att redigera kommentar.",
"no_post_edit_allowed": "Har inte behörighet att redigera inlägg.", "no_post_edit_allowed": "Har inte behörighet att redigera inlägg.",
"no_community_edit_allowed": "Har inte behörighet att redigera gemenskap.", "no_community_edit_allowed": "Har inte behörighet att redigera gemenskap.",
"couldnt_find_community": "Kunde inte hitta gemenskap.", "couldnt_find_community": "Kunde inte hitta gemenskap.",
@ -175,19 +177,84 @@
"couldnt_create_post": "Kunde inte skapa inlägg.", "couldnt_create_post": "Kunde inte skapa inlägg.",
"couldnt_like_post": "Kunde inte gilla inlägg.", "couldnt_like_post": "Kunde inte gilla inlägg.",
"couldnt_find_post": "Kunde inte hitta inlägg.", "couldnt_find_post": "Kunde inte hitta inlägg.",
"couldnt_get_posts": "Kunde inte hämta inlägg.", "couldnt_get_posts": "Kunde inte hämta inlägg",
"couldnt_update_post": "Kunde inte uppdatera inlägg.", "couldnt_update_post": "Kunde inte uppdatera inlägg",
"couldnt_save_post": "Kunde inte spara inlägg.", "couldnt_save_post": "Kunde inte spara inlägg.",
"no_slurs": "Inga förolämpningar.", "no_slurs": "Inga förolämpningar.",
"not_an_admin": "Inte en administratör.", "not_an_admin": "Inte en administratör.",
"site_already_exists": "Webbplatsen finns redan.", "site_already_exists": "Webbplatsen finns redan.",
"couldnt_update_site": "Kunde inte uppdatera webbplats.", "couldnt_update_site": "Kunde inte uppdatera webbplats.",
"couldnt_find_that_username_or_email": "couldnt_find_that_username_or_email": "Kunde inte hitta det användarnamnet eller e-postadressen.",
"Kunde inte hitta det användarnamnet eller e-postadressen.",
"password_incorrect": "Ogiltigt lösenord.", "password_incorrect": "Ogiltigt lösenord.",
"passwords_dont_match": "Lösenorden stämmer inte överens.", "passwords_dont_match": "Lösenorden stämmer inte överens.",
"admin_already_created": "Beklagar, men det finns redan en administratör.", "admin_already_created": "Beklagar, men det finns redan en administratör.",
"user_already_exists": "Användaren finns redan.", "user_already_exists": "Användaren finns redan.",
"couldnt_update_user": "Kunde inte uppdatera användare.", "couldnt_update_user": "Kunde inte uppdatera användare.",
"system_err_login": "Systemfel. Försök att logga ut och sedan in igen." "system_err_login": "Systemfel. Försök att logga ut och sedan in igen.",
"invalid_community_name": "Ogiltigt namn.",
"click_to_delete_picture": "Klicka för att ta bort bild.",
"picture_deleted": "Bild borttagen.",
"upload_avatar": "Ladda upp avatar",
"enable_nsfw": "Aktivera NSFW",
"sorting_help": "sorteringshjälp",
"more": "mer",
"avatar": "Avatar",
"cross_posted_to": "Tvärpostat till: ",
"send_secure_message": "Skicka säkert meddelande",
"send_message": "Skicka meddelande",
"message": "Meddelande",
"create_private_message": "Skapa Privatmeddelande",
"show_avatars": "Visa avatarer",
"archive_link": "Arkivera länk",
"admin_settings": "Administratörsinställningar",
"site_config": "Webbplats konfiguration",
"old": "Gammal",
"banned_users": "Blockerade användare",
"docs": "Dokumentation",
"post_title_too_long": "Inläggstitel är för lång.",
"replies": "Svar",
"mentions": "Nämner",
"message_sent": "Meddelande skickat",
"messages": "Meddelanden",
"old_password": "Gammalt lösenord",
"reset_password_mail_sent": "Skicka e-post för att återställa ditt lösenord.",
"forgot_password": "Glömt lösenord",
"password_change": "Lösenordsbyte",
"new_password": "Nytt lösenord",
"no_email_setup": "Denna server har inte satt upp e-post korrekt.",
"matrix_user_id": "Matrix-användare",
"show_context": "Visa innehåll",
"private_message_disclaimer": "Varning: Privata meddelanden på Lemmy är inte säkra. Vänligen skapa ett konto på <1>Riot.im</1> för att skicka säkra meddelanden.",
"send_notifications_to_email": "Skicka aviseringar till E-post",
"language": "Språk",
"browser_default": "Webbläsarestandard",
"downvotes_disabled": "Nedröstningar inaktiverat",
"enable_downvotes": "Aktivera nedröstningar",
"upvote": "Upprösta",
"number_of_upvotes": "{{count}} Uppröst",
"number_of_upvotes_plural": "{{count}} Uppröstningar",
"downvote": "Nedrösta",
"number_of_downvotes": "{{count}} Nedröst",
"number_of_downvotes_plural": "{{count}} Nedröstningar",
"open_registration": "Öppen registrering",
"registration_closed": "Registrering stängd",
"support_on_liberapay": "Stöd på Liberapay",
"support_on_open_collective": "Stöd på OpenCollective",
"donate_to_lemmy": "Donera till Lemmy",
"donate": "Donera",
"silver_sponsors": "Silversponsor är de som donerat 40 dollar till Lemmy.",
"logged_in": "Inloggad.",
"site_saved": "Webbplats sparad.",
"couldnt_get_comments": "Kunde inte hämta kommentarer.",
"action": "Åtgärd",
"email_already_exists": "E-post finns redan.",
"couldnt_create_private_message": "Kunde inte skapa privat meddelande.",
"no_private_message_edit_allowed": "Inte tillåtet att redigera privata meddelanden.",
"couldnt_update_private_message": "Kunde inte uppdatera privat meddelande.",
"time": "Tid",
"emoji_picker": "Emoji-väljare",
"block_leaving": "Är du säker på att du vill lämna?",
"select_a_community": "Välj en gemenskap",
"from": "från",
"invalid_username": "Ogiltigt användarnamn."
} }

View file

@ -27,9 +27,9 @@
"mod": "社群管理", "mod": "社群管理",
"mods": "社群管理", "mods": "社群管理",
"moderates": "监管", "moderates": "监管",
"remove_as_mod": "删除社群管理", "remove_as_mod": "删除社群管理权限",
"appoint_as_mod": "任命为社群管理", "appoint_as_mod": "任命为社群管理",
"modlog": "审核记录", "modlog": "管理记录",
"admin": "总管理员", "admin": "总管理员",
"admins": "总管理员", "admins": "总管理员",
"remove_as_admin": "移除管理员权限", "remove_as_admin": "移除管理员权限",
@ -55,8 +55,8 @@
"number_of_users": "{{count}} 名用户", "number_of_users": "{{count}} 名用户",
"number_of_subscribers": "{{count}} 名订阅者", "number_of_subscribers": "{{count}} 名订阅者",
"number_of_points": "{{count}} 积分", "number_of_points": "{{count}} 积分",
"name": "名", "name": "名",
"title": "标", "title": "标",
"category": "分类", "category": "分类",
"subscribers": "订阅", "subscribers": "订阅",
"both": "全部", "both": "全部",
@ -66,11 +66,11 @@
"subscribed": "已订阅", "subscribed": "已订阅",
"prev": "上一页", "prev": "上一页",
"next": "下一页", "next": "下一页",
"sidebar": "侧边栏", "sidebar": "介绍",
"sort_type": "排序方式", "sort_type": "排序方式",
"hot": "最热", "hot": "最热",
"new": "最新", "new": "最新",
"top_day": "推荐", "top_day": "日",
"week": "周", "week": "周",
"month": "月", "month": "月",
"year": "年", "year": "年",
@ -82,7 +82,7 @@
"mark_all_as_read": "标记所有已读", "mark_all_as_read": "标记所有已读",
"type": "类型", "type": "类型",
"unread": "未读", "unread": "未读",
"reply_sent": "回复发送", "reply_sent": "回复发送",
"search": "搜索", "search": "搜索",
"overview": "个人中心", "overview": "个人中心",
"view": "查看", "view": "查看",
@ -168,10 +168,10 @@
"yes": "是", "yes": "是",
"no": "否", "no": "否",
"logged_in": "已登录。", "logged_in": "已登录。",
"message": "息", "message": "息",
"create_private_message": "创建私信", "create_private_message": "创建私信",
"send_secure_message": "发送安全息", "send_secure_message": "发送安全息",
"send_message": "发送息", "send_message": "发送息",
"more": "更多", "more": "更多",
"preview": "预览", "preview": "预览",
"upload_image": "上传图片", "upload_image": "上传图片",
@ -184,7 +184,7 @@
"sticky": "固定", "sticky": "固定",
"unsticky": "取消固定", "unsticky": "取消固定",
"archive_link": "链接归档", "archive_link": "链接归档",
"settings": "设", "settings": "设",
"stickied": "已置顶", "stickied": "已置顶",
"delete_account": "删除账号", "delete_account": "删除账号",
"delete_account_confirm": "警告!此操作将永久删除你的数据,请输入密码进行确认。", "delete_account_confirm": "警告!此操作将永久删除你的数据,请输入密码进行确认。",
@ -201,7 +201,7 @@
"replies": "回复", "replies": "回复",
"number_online": "{{count}} 名在线用户", "number_online": "{{count}} 名在线用户",
"mentions": "提到", "mentions": "提到",
"message_sent": "已发送信息", "message_sent": "消息已发送",
"old_password": "当前密码", "old_password": "当前密码",
"forgot_password": "忘记密码", "forgot_password": "忘记密码",
"reset_password_mail_sent": "发送邮件重置密码。", "reset_password_mail_sent": "发送邮件重置密码。",
@ -223,7 +223,7 @@
"open_registration": "开放注册", "open_registration": "开放注册",
"registration_closed": "注册功能已关闭", "registration_closed": "注册功能已关闭",
"recent_comments": "最新评论", "recent_comments": "最新评论",
"by": "", "by": " ",
"transfer_community": "转让社群", "transfer_community": "转让社群",
"transfer_site": "站点转让", "transfer_site": "站点转让",
"post_title_too_long": "帖子标题过长。", "post_title_too_long": "帖子标题过长。",
@ -233,7 +233,7 @@
"no_private_message_edit_allowed": "没有编辑私信的权限。", "no_private_message_edit_allowed": "没有编辑私信的权限。",
"couldnt_update_private_message": "无法更新私信。", "couldnt_update_private_message": "无法更新私信。",
"time": "时间", "time": "时间",
"action": "动", "action": "",
"block_leaving": "确定要离开吗?", "block_leaving": "确定要离开吗?",
"show_context": "显示上下文", "show_context": "显示上下文",
"admin_settings": "管理员设置", "admin_settings": "管理员设置",