mirror of
https://github.com/LemmyNet/lemmy
synced 2024-11-26 22:40:21 +00:00
Merge branch 'federation-authorisation' into apub_security_checks
This commit is contained in:
commit
cabbbf0afd
88 changed files with 3734 additions and 1213 deletions
40
RELEASES.md
vendored
40
RELEASES.md
vendored
|
@ -1,3 +1,43 @@
|
|||
# Lemmy v0.7.40 Pre-Release (2020-08-05)
|
||||
|
||||
We've [added a lot](https://github.com/LemmyNet/lemmy/compare/v0.7.40...v0.7.0) in this pre-release:
|
||||
|
||||
- New post sorts `Active` (previously called hot), and `Hot`. Active shows posts with recent comments, hot shows highly ranked posts.
|
||||
- Customizeable site icon and banner, user icon and banner, and community icon and banner.
|
||||
- Added user preferred names / display names, bios, and cakedays.
|
||||
- User settings are now shared across browsers (a page refresh will pick up changes).
|
||||
- Visual / Audio captchas through the lemmy API.
|
||||
- Lots of UI prettiness.
|
||||
- Lots of bug fixes.
|
||||
- Lots of additional translations.
|
||||
- Lots of federation prepping / additions / refactors.
|
||||
|
||||
This release removes the need for you to have a pictrs nginx route (the requests are now routed through lemmy directly). Follow the upgrade instructions below to replace your nginx with the new one.
|
||||
|
||||
## Upgrading
|
||||
|
||||
**With Ansible:**
|
||||
|
||||
```
|
||||
# run these commands locally
|
||||
git pull
|
||||
cd ansible
|
||||
ansible-playbook lemmy.yml
|
||||
```
|
||||
|
||||
**With manual Docker installation:**
|
||||
```
|
||||
# run these commands on your server
|
||||
cd /lemmy
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/ansible/templates/nginx.conf
|
||||
# Replace the {{ vars }}
|
||||
sudo mv nginx.conf /etc/nginx/sites-enabled/lemmy.conf
|
||||
sudo nginx -s reload
|
||||
wget https://raw.githubusercontent.com/LemmyNet/lemmy/master/docker/prod/docker-compose.yml
|
||||
sudo docker-compose up -d
|
||||
```
|
||||
|
||||
|
||||
# Lemmy v0.7.0 Release (2020-06-23)
|
||||
|
||||
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
|
||||
|
|
2
ansible/VERSION
vendored
2
ansible/VERSION
vendored
|
@ -1 +1 @@
|
|||
v0.7.39
|
||||
v0.7.43
|
||||
|
|
12
ansible/templates/nginx.conf
vendored
12
ansible/templates/nginx.conf
vendored
|
@ -74,18 +74,6 @@ server {
|
|||
return 301 /pictrs/image/$1;
|
||||
}
|
||||
|
||||
# pict-rs images
|
||||
location /pictrs {
|
||||
location /pictrs/image {
|
||||
proxy_pass http://0.0.0.0:8537/image;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
# Block the import
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
proxy_pass http://0.0.0.0:8061/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
|
3
docker/dev/docker-compose.yml
vendored
3
docker/dev/docker-compose.yml
vendored
|
@ -21,7 +21,8 @@ services:
|
|||
postgres:
|
||||
image: postgres:12-alpine
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
# use a different port so it doesnt conflict with postgres running on the host
|
||||
- "127.0.0.1:5433:5432"
|
||||
environment:
|
||||
- POSTGRES_USER=lemmy
|
||||
- POSTGRES_PASSWORD=password
|
||||
|
|
36
docker/federation/nginx.conf
vendored
36
docker/federation/nginx.conf
vendored
|
@ -26,18 +26,6 @@ http {
|
|||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# pict-rs images
|
||||
location /pictrs {
|
||||
location /pictrs/image {
|
||||
proxy_pass http://pictrs:8080/image;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
# Block the import
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
proxy_pass http://iframely:80/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
@ -69,18 +57,6 @@ http {
|
|||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# pict-rs images
|
||||
location /pictrs {
|
||||
location /pictrs/image {
|
||||
proxy_pass http://pictrs:8080/image;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
# Block the import
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
proxy_pass http://iframely:80/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
@ -112,18 +88,6 @@ http {
|
|||
proxy_set_header Connection "upgrade";
|
||||
}
|
||||
|
||||
# pict-rs images
|
||||
location /pictrs {
|
||||
location /pictrs/image {
|
||||
proxy_pass http://pictrs:8080/image;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
}
|
||||
# Block the import
|
||||
return 403;
|
||||
}
|
||||
|
||||
location /iframely/ {
|
||||
proxy_pass http://iframely:80/;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
|
|
9
docker/lemmy.hjson
vendored
9
docker/lemmy.hjson
vendored
|
@ -2,6 +2,15 @@
|
|||
# for more info about the config, check out the documentation
|
||||
# https://dev.lemmy.ml/docs/administration_configuration.html
|
||||
|
||||
setup: {
|
||||
# username for the admin user
|
||||
admin_username: "lemmy"
|
||||
# password for the admin user
|
||||
admin_password: "lemmy"
|
||||
# name of the site (can be changed later)
|
||||
site_name: "lemmy-test"
|
||||
}
|
||||
|
||||
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||
hostname: "my_domain"
|
||||
# address where lemmy should listen for incoming requests
|
||||
|
|
2
docker/prod/docker-compose.yml
vendored
2
docker/prod/docker-compose.yml
vendored
|
@ -12,7 +12,7 @@ services:
|
|||
restart: always
|
||||
|
||||
lemmy:
|
||||
image: dessalines/lemmy:v0.7.39
|
||||
image: dessalines/lemmy:v0.7.43
|
||||
ports:
|
||||
- "127.0.0.1:8536:8536"
|
||||
restart: always
|
||||
|
|
4
docker/travis/docker_push.sh
vendored
4
docker/travis/docker_push.sh
vendored
|
@ -1,5 +1,5 @@
|
|||
#!/bin/sh
|
||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||
docker tag dessalines/lemmy:travis \
|
||||
dessalines/lemmy:v0.7.39
|
||||
docker push dessalines/lemmy:v0.7.39
|
||||
dessalines/lemmy:v0.7.43
|
||||
docker push dessalines/lemmy:v0.7.43
|
||||
|
|
4
docs/src/about_ranking.md
vendored
4
docs/src/about_ranking.md
vendored
|
@ -18,7 +18,9 @@ Score = Upvotes - Downvotes
|
|||
Time = time since submission (in hours)
|
||||
Gravity = Decay gravity, 1.8 is default
|
||||
```
|
||||
- For posts, in order to bring up active posts, it uses the latest comment time (limited to a max creation age of a month ago)
|
||||
- Lemmy uses the same `Rank` algorithm above, in two sorts: `Active`, and `Hot`.
|
||||
- `Active` uses the post votes, and latest comment time (limited to two days).
|
||||
- `Hot` uses the post votes, and the post published time.
|
||||
- Use Max(1, score) to make sure all comments are affected by time decay.
|
||||
- Add 3 to the score, so that everything that has less than 3 downvotes will seem new. Otherwise all new comments would stay at zero, near the bottom.
|
||||
- The sign and abs of the score are necessary for dealing with the log of negative scores.
|
||||
|
|
13
docs/src/contributing_federation_development.md
vendored
13
docs/src/contributing_federation_development.md
vendored
|
@ -68,3 +68,16 @@ cd /lemmy/
|
|||
sudo docker-compose pull
|
||||
sudo docker-compose up -d
|
||||
```
|
||||
|
||||
## Security Model
|
||||
|
||||
- HTTP signature verify: This ensures that activity really comes from the activity that it claims
|
||||
- check_is_apub_valid : Makes sure its in our allowed instances list
|
||||
- Lower level checks: To make sure that the user that creates/updates/removes a post is actually on the same instance as that post
|
||||
|
||||
For the last point, note that we are *not* checking whether the actor that sends the create activity for a post is
|
||||
actually identical to the post's creator, or that the user that removes a post is a mod/admin. These things are checked
|
||||
by the API code, and its the responsibility of each instance to check user permissions. This does not leave any attack
|
||||
vector, as a normal instance user cant do actions that violate the API rules. The only one who could do that is the
|
||||
admin (and the software deployed by the admin). But the admin can do anything on the instance, including send activities
|
||||
from other user accounts. So we wouldnt actually gain any security by checking mod permissions or similar.
|
25
docs/src/contributing_websocket_http_api.md
vendored
25
docs/src/contributing_websocket_http_api.md
vendored
|
@ -330,7 +330,8 @@ curl -i -H \
|
|||
|
||||
These go wherever there is a `sort` field. The available sort types are:
|
||||
|
||||
- `Hot` - the hottest posts/communities, depending on votes, views, comments and publish date
|
||||
- `Active` - the hottest posts/communities, depending on votes, and newest comment publish date.
|
||||
- `Hot` - the hottest posts/communities, depending on votes and publish date.
|
||||
- `New` - the newest posts/communities
|
||||
- `TopDay` - the most upvoted posts/communities of the current day.
|
||||
- `TopWeek` - the most upvoted posts/communities of the current week.
|
||||
|
@ -482,7 +483,19 @@ These expire after 10 minutes.
|
|||
theme: String, // Default 'darkly'
|
||||
default_sort_type: i16, // The Sort types from above, zero indexed as a number
|
||||
default_listing_type: i16, // Post listing types are `All, Subscribed, Community`
|
||||
auth: String
|
||||
lang: String,
|
||||
avatar: Option<String>,
|
||||
banner: Option<String>,
|
||||
preferred_username: Option<String>,
|
||||
email: Option<String>,
|
||||
bio: Option<String>,
|
||||
matrix_user_id: Option<String>,
|
||||
new_password: Option<String>,
|
||||
new_password_verify: Option<String>,
|
||||
old_password: Option<String>,
|
||||
show_avatars: bool,
|
||||
send_notifications_to_email: bool,
|
||||
auth: String,
|
||||
}
|
||||
}
|
||||
```
|
||||
|
@ -924,6 +937,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
data: {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
auth: String
|
||||
}
|
||||
}
|
||||
|
@ -950,6 +965,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
data: {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
auth: String
|
||||
}
|
||||
}
|
||||
|
@ -1105,6 +1122,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
|||
name: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
category_id: i32 ,
|
||||
auth: String
|
||||
}
|
||||
|
@ -1215,6 +1234,8 @@ Only mods can edit a community.
|
|||
edit_id: i32,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
category_id: i32,
|
||||
auth: String
|
||||
}
|
||||
|
|
6
server/config/defaults.hjson
vendored
6
server/config/defaults.hjson
vendored
|
@ -35,6 +35,8 @@
|
|||
jwt_secret: "changeme"
|
||||
# The location of the frontend
|
||||
front_end_dir: "../ui/dist"
|
||||
# address where pictrs is available
|
||||
pictrs_url: "http://pictrs:8080"
|
||||
# rate limits for various user actions, by user ip
|
||||
rate_limit: {
|
||||
# maximum number of messages created in interval
|
||||
|
@ -49,6 +51,10 @@
|
|||
register: 3
|
||||
# interval length for registration limit
|
||||
register_per_second: 3600
|
||||
# maximum number of image uploads in interval
|
||||
image: 6
|
||||
# interval length for image uploads
|
||||
image_per_second: 3600
|
||||
}
|
||||
# settings related to activitypub federation
|
||||
federation: {
|
||||
|
|
|
@ -107,6 +107,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
|
|
@ -255,6 +255,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -291,6 +292,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
banner: None,
|
||||
icon: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
|
|
@ -23,17 +23,20 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
|
@ -60,17 +63,20 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
|
@ -100,17 +106,20 @@ pub struct CommentView {
|
|||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub hot_rank: i32,
|
||||
pub hot_rank_active: i32,
|
||||
pub user_id: Option<i32>,
|
||||
pub my_vote: Option<i32>,
|
||||
pub subscribed: Option<bool>,
|
||||
|
@ -244,6 +253,9 @@ impl<'a> CommentQueryBuilder<'a> {
|
|||
SortType::Hot => query
|
||||
.order_by(hot_rank.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::Active => query
|
||||
.order_by(hot_rank_active.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::New => query.order_by(published.desc()),
|
||||
SortType::TopAll => query.order_by(score.desc()),
|
||||
SortType::TopYear => query
|
||||
|
@ -315,17 +327,20 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Varchar>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
creator_published -> Timestamp,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
subscribed -> Nullable<Bool>,
|
||||
|
@ -356,17 +371,20 @@ pub struct ReplyView {
|
|||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub hot_rank: i32,
|
||||
pub hot_rank_active: i32,
|
||||
pub user_id: Option<i32>,
|
||||
pub my_vote: Option<i32>,
|
||||
pub subscribed: Option<bool>,
|
||||
|
@ -437,7 +455,7 @@ impl<'a> ReplyQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
query = match self.sort {
|
||||
// SortType::Hot => query.order_by(hot_rank.desc()),
|
||||
// SortType::Hot => query.order_by(hot_rank.desc()), // TODO why is this commented
|
||||
SortType::New => query.order_by(published.desc()),
|
||||
SortType::TopAll => query.order_by(score.desc()),
|
||||
SortType::TopYear => query
|
||||
|
@ -488,6 +506,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -524,6 +543,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
@ -584,6 +605,7 @@ mod tests {
|
|||
post_name: inserted_post.name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
community_name: inserted_community.name.to_owned(),
|
||||
community_icon: None,
|
||||
parent_id: None,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
|
@ -593,11 +615,13 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_preferred_username: None,
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
hot_rank: 0,
|
||||
hot_rank_active: 0,
|
||||
upvotes: 1,
|
||||
user_id: None,
|
||||
my_vote: None,
|
||||
|
@ -619,6 +643,7 @@ mod tests {
|
|||
post_name: inserted_post.name.to_owned(),
|
||||
community_id: inserted_community.id,
|
||||
community_name: inserted_community.name.to_owned(),
|
||||
community_icon: None,
|
||||
parent_id: None,
|
||||
removed: false,
|
||||
deleted: false,
|
||||
|
@ -628,11 +653,13 @@ mod tests {
|
|||
published: inserted_comment.published,
|
||||
updated: None,
|
||||
creator_name: inserted_user.name.to_owned(),
|
||||
creator_preferred_username: None,
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
score: 1,
|
||||
downvotes: 0,
|
||||
hot_rank: 0,
|
||||
hot_rank_active: 0,
|
||||
upvotes: 1,
|
||||
user_id: Some(inserted_user.id),
|
||||
my_vote: Some(1),
|
||||
|
@ -651,6 +678,7 @@ mod tests {
|
|||
.list()
|
||||
.unwrap();
|
||||
read_comment_views_no_user[0].hot_rank = 0;
|
||||
read_comment_views_no_user[0].hot_rank_active = 0;
|
||||
|
||||
let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
|
||||
.for_post_id(inserted_post.id)
|
||||
|
@ -658,6 +686,7 @@ mod tests {
|
|||
.list()
|
||||
.unwrap();
|
||||
read_comment_views_with_user[0].hot_rank = 0;
|
||||
read_comment_views_with_user[0].hot_rank_active = 0;
|
||||
|
||||
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
||||
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||
|
|
|
@ -28,6 +28,8 @@ pub struct Community {
|
|||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||
pub icon: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
||||
|
@ -48,6 +50,8 @@ pub struct CommunityForm {
|
|||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||
pub icon: Option<Option<String>>,
|
||||
pub banner: Option<Option<String>>,
|
||||
}
|
||||
|
||||
impl Crud<CommunityForm> for Community {
|
||||
|
@ -299,6 +303,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -335,6 +340,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
@ -356,6 +363,8 @@ mod tests {
|
|||
private_key: None,
|
||||
public_key: None,
|
||||
last_refreshed_at: inserted_community.published,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
|
|
|
@ -8,6 +8,8 @@ table! {
|
|||
id -> Int4,
|
||||
name -> Varchar,
|
||||
title -> Varchar,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Int4,
|
||||
creator_id -> Int4,
|
||||
|
@ -22,6 +24,7 @@ table! {
|
|||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
|
@ -38,6 +41,8 @@ table! {
|
|||
id -> Int4,
|
||||
name -> Varchar,
|
||||
title -> Varchar,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Int4,
|
||||
creator_id -> Int4,
|
||||
|
@ -52,6 +57,7 @@ table! {
|
|||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Varchar,
|
||||
number_of_subscribers -> BigInt,
|
||||
|
@ -72,10 +78,12 @@ table! {
|
|||
user_actor_id -> Text,
|
||||
user_local -> Bool,
|
||||
user_name -> Varchar,
|
||||
user_preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,10 +96,12 @@ table! {
|
|||
user_actor_id -> Text,
|
||||
user_local -> Bool,
|
||||
user_name -> Varchar,
|
||||
user_preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,10 +114,12 @@ table! {
|
|||
user_actor_id -> Text,
|
||||
user_local -> Bool,
|
||||
user_name -> Varchar,
|
||||
user_preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -119,6 +131,8 @@ pub struct CommunityView {
|
|||
pub id: i32,
|
||||
pub name: String,
|
||||
pub title: String,
|
||||
pub icon: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub category_id: i32,
|
||||
pub creator_id: i32,
|
||||
|
@ -133,6 +147,7 @@ pub struct CommunityView {
|
|||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub category_name: String,
|
||||
pub number_of_subscribers: i64,
|
||||
|
@ -288,10 +303,12 @@ pub struct CommunityModeratorView {
|
|||
pub user_actor_id: String,
|
||||
pub user_local: bool,
|
||||
pub user_name: String,
|
||||
pub user_preferred_username: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
}
|
||||
|
||||
impl CommunityModeratorView {
|
||||
|
@ -324,10 +341,12 @@ pub struct CommunityFollowerView {
|
|||
pub user_actor_id: String,
|
||||
pub user_local: bool,
|
||||
pub user_name: String,
|
||||
pub user_preferred_username: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
}
|
||||
|
||||
impl CommunityFollowerView {
|
||||
|
@ -358,10 +377,12 @@ pub struct CommunityUserBanView {
|
|||
pub user_actor_id: String,
|
||||
pub user_local: bool,
|
||||
pub user_name: String,
|
||||
pub user_preferred_username: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
}
|
||||
|
||||
impl CommunityUserBanView {
|
||||
|
|
|
@ -134,6 +134,7 @@ pub fn get_database_url_from_env() -> Result<String, VarError> {
|
|||
|
||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||
pub enum SortType {
|
||||
Active,
|
||||
Hot,
|
||||
New,
|
||||
TopDay,
|
||||
|
@ -180,6 +181,20 @@ pub fn is_email_regex(test: &str) -> bool {
|
|||
EMAIL_REGEX.is_match(test)
|
||||
}
|
||||
|
||||
pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
|
||||
match opt {
|
||||
// An empty string is an erase
|
||||
Some(unwrapped) => {
|
||||
if !unwrapped.eq("") {
|
||||
Some(Some(unwrapped.to_owned()))
|
||||
} else {
|
||||
Some(None)
|
||||
}
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
static ref EMAIL_REGEX: Regex =
|
||||
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||
|
|
|
@ -460,6 +460,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -487,6 +488,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -523,6 +525,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
|
|
@ -95,6 +95,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
|
|
@ -316,6 +316,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -352,6 +353,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
|
|
@ -28,6 +28,7 @@ table! {
|
|||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
|
@ -35,6 +36,7 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
community_nsfw -> Bool,
|
||||
|
@ -43,6 +45,7 @@ table! {
|
|||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
newest_activity_time -> Timestamp,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
|
@ -76,6 +79,7 @@ table! {
|
|||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Timestamp,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
|
@ -83,6 +87,7 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
community_removed -> Bool,
|
||||
community_deleted -> Bool,
|
||||
community_nsfw -> Bool,
|
||||
|
@ -91,6 +96,7 @@ table! {
|
|||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
newest_activity_time -> Timestamp,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
|
@ -127,6 +133,7 @@ pub struct PostView {
|
|||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_published: chrono::NaiveDateTime,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub banned: bool,
|
||||
|
@ -134,6 +141,7 @@ pub struct PostView {
|
|||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
pub community_removed: bool,
|
||||
pub community_deleted: bool,
|
||||
pub community_nsfw: bool,
|
||||
|
@ -142,6 +150,7 @@ pub struct PostView {
|
|||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub hot_rank: i32,
|
||||
pub hot_rank_active: i32,
|
||||
pub newest_activity_time: chrono::NaiveDateTime,
|
||||
pub user_id: Option<i32>,
|
||||
pub my_vote: Option<i32>,
|
||||
|
@ -289,6 +298,9 @@ impl<'a> PostQueryBuilder<'a> {
|
|||
}
|
||||
|
||||
query = match self.sort {
|
||||
SortType::Active => query
|
||||
.then_order_by(hot_rank_active.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::Hot => query
|
||||
.then_order_by(hot_rank.desc())
|
||||
.then_order_by(published.desc()),
|
||||
|
@ -405,6 +417,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
updated: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
|
@ -441,6 +454,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
@ -519,6 +534,7 @@ mod tests {
|
|||
body: None,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name.to_owned(),
|
||||
creator_preferred_username: None,
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
|
@ -529,6 +545,7 @@ mod tests {
|
|||
locked: false,
|
||||
stickied: false,
|
||||
community_name: community_name.to_owned(),
|
||||
community_icon: None,
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
community_nsfw: false,
|
||||
|
@ -537,6 +554,7 @@ mod tests {
|
|||
upvotes: 1,
|
||||
downvotes: 0,
|
||||
hot_rank: read_post_listing_no_user.hot_rank,
|
||||
hot_rank_active: read_post_listing_no_user.hot_rank_active,
|
||||
published: inserted_post.published,
|
||||
newest_activity_time: inserted_post.published,
|
||||
updated: None,
|
||||
|
@ -569,12 +587,14 @@ mod tests {
|
|||
stickied: false,
|
||||
creator_id: inserted_user.id,
|
||||
creator_name: user_name,
|
||||
creator_preferred_username: None,
|
||||
creator_published: inserted_user.published,
|
||||
creator_avatar: None,
|
||||
banned: false,
|
||||
banned_from_community: false,
|
||||
community_id: inserted_community.id,
|
||||
community_name,
|
||||
community_icon: None,
|
||||
community_removed: false,
|
||||
community_deleted: false,
|
||||
community_nsfw: false,
|
||||
|
@ -583,6 +603,7 @@ mod tests {
|
|||
upvotes: 1,
|
||||
downvotes: 0,
|
||||
hot_rank: read_post_listing_with_user.hot_rank,
|
||||
hot_rank_active: read_post_listing_with_user.hot_rank_active,
|
||||
published: inserted_post.published,
|
||||
newest_activity_time: inserted_post.published,
|
||||
updated: None,
|
||||
|
|
|
@ -147,6 +147,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -174,6 +175,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
|
|
@ -16,10 +16,12 @@ table! {
|
|||
ap_id -> Text,
|
||||
local -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
creator_actor_id -> Text,
|
||||
creator_local -> Bool,
|
||||
recipient_name -> Varchar,
|
||||
recipient_preferred_username -> Nullable<Varchar>,
|
||||
recipient_avatar -> Nullable<Text>,
|
||||
recipient_actor_id -> Text,
|
||||
recipient_local -> Bool,
|
||||
|
@ -42,10 +44,12 @@ pub struct PrivateMessageView {
|
|||
pub ap_id: String,
|
||||
pub local: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub creator_actor_id: String,
|
||||
pub creator_local: bool,
|
||||
pub recipient_name: String,
|
||||
pub recipient_preferred_username: Option<String>,
|
||||
pub recipient_avatar: Option<String>,
|
||||
pub recipient_actor_id: String,
|
||||
pub recipient_local: bool,
|
||||
|
|
|
@ -52,17 +52,20 @@ table! {
|
|||
community_actor_id -> Nullable<Varchar>,
|
||||
community_local -> Nullable<Bool>,
|
||||
community_name -> Nullable<Varchar>,
|
||||
community_icon -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
banned_from_community -> Nullable<Bool>,
|
||||
creator_actor_id -> Nullable<Varchar>,
|
||||
creator_local -> Nullable<Bool>,
|
||||
creator_name -> Nullable<Varchar>,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Nullable<Timestamp>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> Nullable<Int8>,
|
||||
upvotes -> Nullable<Int8>,
|
||||
downvotes -> Nullable<Int8>,
|
||||
hot_rank -> Nullable<Int4>,
|
||||
hot_rank_active -> Nullable<Int4>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,6 +107,8 @@ table! {
|
|||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
last_refreshed_at -> Timestamp,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -112,6 +117,8 @@ table! {
|
|||
id -> Int4,
|
||||
name -> Nullable<Varchar>,
|
||||
title -> Nullable<Varchar>,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
description -> Nullable<Text>,
|
||||
category_id -> Nullable<Int4>,
|
||||
creator_id -> Nullable<Int4>,
|
||||
|
@ -126,6 +133,7 @@ table! {
|
|||
creator_actor_id -> Nullable<Varchar>,
|
||||
creator_local -> Nullable<Bool>,
|
||||
creator_name -> Nullable<Varchar>,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
category_name -> Nullable<Varchar>,
|
||||
number_of_subscribers -> Nullable<Int8>,
|
||||
|
@ -319,6 +327,7 @@ table! {
|
|||
creator_actor_id -> Nullable<Varchar>,
|
||||
creator_local -> Nullable<Bool>,
|
||||
creator_name -> Nullable<Varchar>,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_published -> Nullable<Timestamp>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
banned -> Nullable<Bool>,
|
||||
|
@ -326,6 +335,7 @@ table! {
|
|||
community_actor_id -> Nullable<Varchar>,
|
||||
community_local -> Nullable<Bool>,
|
||||
community_name -> Nullable<Varchar>,
|
||||
community_icon -> Nullable<Text>,
|
||||
community_removed -> Nullable<Bool>,
|
||||
community_deleted -> Nullable<Bool>,
|
||||
community_nsfw -> Nullable<Bool>,
|
||||
|
@ -334,6 +344,7 @@ table! {
|
|||
upvotes -> Nullable<Int8>,
|
||||
downvotes -> Nullable<Int8>,
|
||||
hot_rank -> Nullable<Int4>,
|
||||
hot_rank_active -> Nullable<Int4>,
|
||||
newest_activity_time -> Nullable<Timestamp>,
|
||||
}
|
||||
}
|
||||
|
@ -392,6 +403,8 @@ table! {
|
|||
enable_downvotes -> Bool,
|
||||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -421,6 +434,7 @@ table! {
|
|||
private_key -> Nullable<Text>,
|
||||
public_key -> Nullable<Text>,
|
||||
last_refreshed_at -> Timestamp,
|
||||
banner -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -437,7 +451,9 @@ table! {
|
|||
id -> Int4,
|
||||
actor_id -> Nullable<Varchar>,
|
||||
name -> Nullable<Varchar>,
|
||||
preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
bio -> Nullable<Text>,
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{schema::site, Crud};
|
||||
use crate::{naive_now, schema::site, Crud};
|
||||
use diesel::{dsl::*, result::Error, *};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -14,6 +14,8 @@ pub struct Site {
|
|||
pub enable_downvotes: bool,
|
||||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
pub icon: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
||||
|
@ -26,6 +28,9 @@ pub struct SiteForm {
|
|||
pub enable_downvotes: bool,
|
||||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
// when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column.
|
||||
pub icon: Option<Option<String>>,
|
||||
pub banner: Option<Option<String>>,
|
||||
}
|
||||
|
||||
impl Crud<SiteForm> for Site {
|
||||
|
@ -51,3 +56,12 @@ impl Crud<SiteForm> for Site {
|
|||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
||||
impl Site {
|
||||
pub fn transfer(conn: &PgConnection, new_creator_id: i32) -> Result<Self, Error> {
|
||||
use crate::schema::site::dsl::*;
|
||||
diesel::update(site.find(1))
|
||||
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
|
||||
.get_result::<Self>(conn)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,7 +12,10 @@ table! {
|
|||
enable_downvotes -> Bool,
|
||||
open_registration -> Bool,
|
||||
enable_nsfw -> Bool,
|
||||
icon -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
number_of_users -> BigInt,
|
||||
number_of_posts -> BigInt,
|
||||
|
@ -35,7 +38,10 @@ pub struct SiteView {
|
|||
pub enable_downvotes: bool,
|
||||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
pub icon: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub number_of_users: i64,
|
||||
pub number_of_posts: i64,
|
||||
|
|
|
@ -35,6 +35,7 @@ pub struct User_ {
|
|||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||
pub banner: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Insertable, AsChangeset, Clone, Debug)]
|
||||
|
@ -46,7 +47,7 @@ pub struct UserForm {
|
|||
pub admin: bool,
|
||||
pub banned: bool,
|
||||
pub email: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub avatar: Option<Option<String>>,
|
||||
pub updated: Option<chrono::NaiveDateTime>,
|
||||
pub show_nsfw: bool,
|
||||
pub theme: String,
|
||||
|
@ -62,6 +63,7 @@ pub struct UserForm {
|
|||
pub private_key: Option<String>,
|
||||
pub public_key: Option<String>,
|
||||
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||
pub banner: Option<Option<String>>,
|
||||
}
|
||||
|
||||
impl Crud<UserForm> for User_ {
|
||||
|
@ -167,6 +169,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -195,6 +198,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
published: inserted_user.published,
|
||||
|
|
|
@ -100,6 +100,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -127,6 +128,7 @@ mod tests {
|
|||
email: None,
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
admin: false,
|
||||
banned: false,
|
||||
updated: None,
|
||||
|
@ -163,6 +165,8 @@ mod tests {
|
|||
public_key: None,
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
|
||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||
|
|
|
@ -23,14 +23,17 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
saved -> Nullable<Bool>,
|
||||
|
@ -60,14 +63,17 @@ table! {
|
|||
community_actor_id -> Text,
|
||||
community_local -> Bool,
|
||||
community_name -> Varchar,
|
||||
community_icon -> Nullable<Text>,
|
||||
banned -> Bool,
|
||||
banned_from_community -> Bool,
|
||||
creator_name -> Varchar,
|
||||
creator_preferred_username -> Nullable<Varchar>,
|
||||
creator_avatar -> Nullable<Text>,
|
||||
score -> BigInt,
|
||||
upvotes -> BigInt,
|
||||
downvotes -> BigInt,
|
||||
hot_rank -> Int4,
|
||||
hot_rank_active -> Int4,
|
||||
user_id -> Nullable<Int4>,
|
||||
my_vote -> Nullable<Int4>,
|
||||
saved -> Nullable<Bool>,
|
||||
|
@ -100,14 +106,17 @@ pub struct UserMentionView {
|
|||
pub community_actor_id: String,
|
||||
pub community_local: bool,
|
||||
pub community_name: String,
|
||||
pub community_icon: Option<String>,
|
||||
pub banned: bool,
|
||||
pub banned_from_community: bool,
|
||||
pub creator_name: String,
|
||||
pub creator_preferred_username: Option<String>,
|
||||
pub creator_avatar: Option<String>,
|
||||
pub score: i64,
|
||||
pub upvotes: i64,
|
||||
pub downvotes: i64,
|
||||
pub hot_rank: i32,
|
||||
pub hot_rank_active: i32,
|
||||
pub user_id: Option<i32>,
|
||||
pub my_vote: Option<i32>,
|
||||
pub saved: Option<bool>,
|
||||
|
@ -180,6 +189,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
|||
SortType::Hot => query
|
||||
.order_by(hot_rank.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::Active => query
|
||||
.order_by(hot_rank_active.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::New => query.order_by(published.desc()),
|
||||
SortType::TopAll => query.order_by(score.desc()),
|
||||
SortType::TopYear => query
|
||||
|
|
|
@ -8,7 +8,9 @@ table! {
|
|||
id -> Int4,
|
||||
actor_id -> Text,
|
||||
name -> Varchar,
|
||||
preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
bio -> Nullable<Text>,
|
||||
|
@ -30,7 +32,9 @@ table! {
|
|||
id -> Int4,
|
||||
actor_id -> Text,
|
||||
name -> Varchar,
|
||||
preferred_username -> Nullable<Varchar>,
|
||||
avatar -> Nullable<Text>,
|
||||
banner -> Nullable<Text>,
|
||||
email -> Nullable<Text>,
|
||||
matrix_user_id -> Nullable<Text>,
|
||||
bio -> Nullable<Text>,
|
||||
|
@ -55,7 +59,9 @@ pub struct UserView {
|
|||
pub id: i32,
|
||||
pub actor_id: String,
|
||||
pub name: String,
|
||||
pub preferred_username: Option<String>,
|
||||
pub avatar: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub email: Option<String>, // TODO this shouldn't be in this view
|
||||
pub matrix_user_id: Option<String>,
|
||||
pub bio: Option<String>,
|
||||
|
@ -126,6 +132,9 @@ impl<'a> UserQueryBuilder<'a> {
|
|||
SortType::Hot => query
|
||||
.order_by(comment_score.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::Active => query
|
||||
.order_by(comment_score.desc())
|
||||
.then_order_by(published.desc()),
|
||||
SortType::New => query.order_by(published.desc()),
|
||||
SortType::TopAll => query.order_by(comment_score.desc()),
|
||||
SortType::TopYear => query
|
||||
|
@ -164,7 +173,9 @@ impl UserView {
|
|||
id,
|
||||
actor_id,
|
||||
name,
|
||||
preferred_username,
|
||||
avatar,
|
||||
banner,
|
||||
"".into_sql::<Nullable<Text>>(),
|
||||
matrix_user_id,
|
||||
bio,
|
||||
|
@ -192,7 +203,9 @@ impl UserView {
|
|||
id,
|
||||
actor_id,
|
||||
name,
|
||||
preferred_username,
|
||||
avatar,
|
||||
banner,
|
||||
"".into_sql::<Nullable<Text>>(),
|
||||
matrix_user_id,
|
||||
bio,
|
||||
|
|
|
@ -14,6 +14,7 @@ pub struct Settings {
|
|||
pub port: u16,
|
||||
pub jwt_secret: String,
|
||||
pub front_end_dir: String,
|
||||
pub pictrs_url: String,
|
||||
pub rate_limit: RateLimitConfig,
|
||||
pub email: Option<EmailConfig>,
|
||||
pub federation: Federation,
|
||||
|
@ -36,6 +37,8 @@ pub struct RateLimitConfig {
|
|||
pub post_per_second: i32,
|
||||
pub register: i32,
|
||||
pub register_per_second: i32,
|
||||
pub image: i32,
|
||||
pub image_per_second: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone)]
|
||||
|
|
704
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/down.sql
vendored
Normal file
704
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/down.sql
vendored
Normal file
|
@ -0,0 +1,704 @@
|
|||
-- Drops first
|
||||
drop view site_view;
|
||||
drop table user_fast;
|
||||
drop view user_view;
|
||||
drop view post_fast_view;
|
||||
drop table post_aggregates_fast;
|
||||
drop view post_view;
|
||||
drop view post_aggregates_view;
|
||||
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;
|
||||
drop view community_fast_view;
|
||||
drop table community_aggregates_fast;
|
||||
drop view private_message_view;
|
||||
drop view user_mention_view;
|
||||
drop view reply_fast_view;
|
||||
drop view comment_fast_view;
|
||||
drop view comment_view;
|
||||
drop view user_mention_fast_view;
|
||||
drop table comment_aggregates_fast;
|
||||
drop view comment_aggregates_view;
|
||||
|
||||
alter table site
|
||||
drop column icon,
|
||||
drop column banner;
|
||||
|
||||
alter table community
|
||||
drop column icon,
|
||||
drop column banner;
|
||||
|
||||
alter table user_ drop column banner;
|
||||
|
||||
-- Site
|
||||
create view site_view as
|
||||
select *,
|
||||
(select name from user_ u where s.creator_id = u.id) as creator_name,
|
||||
(select avatar from user_ u where s.creator_id = u.id) as creator_avatar,
|
||||
(select count(*) from user_) as number_of_users,
|
||||
(select count(*) from post) as number_of_posts,
|
||||
(select count(*) from comment) as number_of_comments,
|
||||
(select count(*) from community) as number_of_communities
|
||||
from site s;
|
||||
|
||||
-- User
|
||||
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);
|
||||
|
||||
-- Post fast
|
||||
|
||||
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.published as creator_published,
|
||||
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;
|
||||
|
||||
create table post_aggregates_fast as select * from post_aggregates_view;
|
||||
alter table post_aggregates_fast add primary key (id);
|
||||
|
||||
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;
|
||||
|
||||
-- Community
|
||||
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;
|
||||
|
||||
|
||||
-- Private message
|
||||
create view private_message_view as
|
||||
select
|
||||
pm.*,
|
||||
u.name as creator_name,
|
||||
u.avatar as creator_avatar,
|
||||
u.actor_id as creator_actor_id,
|
||||
u.local as creator_local,
|
||||
u2.name as recipient_name,
|
||||
u2.avatar as recipient_avatar,
|
||||
u2.actor_id as recipient_actor_id,
|
||||
u2.local as recipient_local
|
||||
from private_message pm
|
||||
inner join user_ u on u.id = pm.creator_id
|
||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
||||
|
||||
|
||||
-- Comments, mentions, replies
|
||||
|
||||
create view comment_aggregates_view as
|
||||
select
|
||||
ct.*,
|
||||
-- post details
|
||||
p."name" as post_name,
|
||||
p.community_id,
|
||||
-- community details
|
||||
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.published as creator_published,
|
||||
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
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
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.post_name,
|
||||
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.post_name,
|
||||
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.post_name,
|
||||
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
|
||||
;
|
||||
|
||||
-- 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
|
||||
;
|
||||
|
||||
-- redoing the triggers
|
||||
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 $$;
|
||||
|
||||
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 $$;
|
748
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/up.sql
vendored
Normal file
748
server/migrations/2020-08-03-000110_add_preferred_usernames_banners_and_icons/up.sql
vendored
Normal file
|
@ -0,0 +1,748 @@
|
|||
-- This adds the following columns, as well as updates the views:
|
||||
-- Site icon
|
||||
-- Site banner
|
||||
-- Community icon
|
||||
-- Community Banner
|
||||
-- User Banner (User avatar is already there)
|
||||
-- User preferred name (already in table, needs to be added to view)
|
||||
|
||||
-- It also adds hot_rank_active to post_view
|
||||
|
||||
alter table site
|
||||
add column icon text,
|
||||
add column banner text;
|
||||
|
||||
alter table community
|
||||
add column icon text,
|
||||
add column banner text;
|
||||
|
||||
alter table user_ add column banner text;
|
||||
|
||||
drop view site_view;
|
||||
create view site_view as
|
||||
select s.*,
|
||||
u.name as creator_name,
|
||||
u.preferred_username as creator_preferred_username,
|
||||
u.avatar as creator_avatar,
|
||||
(select count(*) from user_) as number_of_users,
|
||||
(select count(*) from post) as number_of_posts,
|
||||
(select count(*) from comment) as number_of_comments,
|
||||
(select count(*) from community) as number_of_communities
|
||||
from site s
|
||||
left join user_ u on s.creator_id = u.id;
|
||||
|
||||
-- User
|
||||
drop table user_fast;
|
||||
drop view user_view;
|
||||
create view user_view as
|
||||
select
|
||||
u.id,
|
||||
u.actor_id,
|
||||
u.name,
|
||||
u.preferred_username,
|
||||
u.avatar,
|
||||
u.banner,
|
||||
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);
|
||||
|
||||
-- private message
|
||||
drop view private_message_view;
|
||||
create view private_message_view as
|
||||
select
|
||||
pm.*,
|
||||
u.name as creator_name,
|
||||
u.preferred_username as creator_preferred_username,
|
||||
u.avatar as creator_avatar,
|
||||
u.actor_id as creator_actor_id,
|
||||
u.local as creator_local,
|
||||
u2.name as recipient_name,
|
||||
u2.preferred_username as recipient_preferred_username,
|
||||
u2.avatar as recipient_avatar,
|
||||
u2.actor_id as recipient_actor_id,
|
||||
u2.local as recipient_local
|
||||
from private_message pm
|
||||
inner join user_ u on u.id = pm.creator_id
|
||||
inner join user_ u2 on u2.id = pm.recipient_id;
|
||||
|
||||
-- Post fast
|
||||
drop view post_fast_view;
|
||||
drop table post_aggregates_fast;
|
||||
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."preferred_username" as creator_preferred_username,
|
||||
u.published as creator_published,
|
||||
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.icon as community_icon,
|
||||
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, 1), p.published) as hot_rank,
|
||||
hot_rank(coalesce(pl.score, 1), greatest(ct.recent_comment_time, p.published)) as hot_rank_active,
|
||||
greatest(ct.recent_comment_time, p.published) 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;
|
||||
|
||||
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 index idx_post_aggregates_fast_hot_rank_active_published on post_aggregates_fast (hot_rank_active 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;
|
||||
|
||||
-- Community
|
||||
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;
|
||||
drop view community_fast_view;
|
||||
drop table community_aggregates_fast;
|
||||
|
||||
create view community_aggregates_view as
|
||||
select
|
||||
c.id,
|
||||
c.name,
|
||||
c.title,
|
||||
c.icon,
|
||||
c.banner,
|
||||
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.preferred_username as creator_preferred_username,
|
||||
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.preferred_username as user_preferred_username,
|
||||
u.avatar as avatar,
|
||||
c.actor_id as community_actor_id,
|
||||
c.local as community_local,
|
||||
c.name as community_name,
|
||||
c.icon as community_icon
|
||||
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.preferred_username as user_preferred_username,
|
||||
u.avatar as avatar,
|
||||
c.actor_id as community_actor_id,
|
||||
c.local as community_local,
|
||||
c.name as community_name,
|
||||
c.icon as community_icon
|
||||
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.preferred_username as user_preferred_username,
|
||||
u.avatar as avatar,
|
||||
c.actor_id as community_actor_id,
|
||||
c.local as community_local,
|
||||
c.name as community_name,
|
||||
c.icon as community_icon
|
||||
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;
|
||||
|
||||
-- Comments, mentions, replies
|
||||
drop view user_mention_view;
|
||||
drop view reply_fast_view;
|
||||
drop view comment_fast_view;
|
||||
drop view comment_view;
|
||||
drop view user_mention_fast_view;
|
||||
drop table comment_aggregates_fast;
|
||||
drop view comment_aggregates_view;
|
||||
|
||||
create view comment_aggregates_view as
|
||||
select
|
||||
ct.*,
|
||||
-- post details
|
||||
p."name" as post_name,
|
||||
p.community_id,
|
||||
-- community details
|
||||
c.actor_id as community_actor_id,
|
||||
c."local" as community_local,
|
||||
c."name" as community_name,
|
||||
c.icon as community_icon,
|
||||
-- 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.preferred_username as creator_preferred_username,
|
||||
u.published as creator_published,
|
||||
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, 1), p.published) as hot_rank,
|
||||
hot_rank(coalesce(cl.total, 1), ct.published) as hot_rank_active
|
||||
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
|
||||
);
|
||||
|
||||
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;
|
||||
|
||||
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.post_name,
|
||||
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.community_icon,
|
||||
c.banned,
|
||||
c.banned_from_community,
|
||||
c.creator_name,
|
||||
c.creator_preferred_username,
|
||||
c.creator_avatar,
|
||||
c.score,
|
||||
c.upvotes,
|
||||
c.downvotes,
|
||||
c.hot_rank,
|
||||
c.hot_rank_active,
|
||||
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.post_name,
|
||||
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.community_icon,
|
||||
ac.banned,
|
||||
ac.banned_from_community,
|
||||
ac.creator_name,
|
||||
ac.creator_preferred_username,
|
||||
ac.creator_avatar,
|
||||
ac.score,
|
||||
ac.upvotes,
|
||||
ac.downvotes,
|
||||
ac.hot_rank,
|
||||
ac.hot_rank_active,
|
||||
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.post_name,
|
||||
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.community_icon,
|
||||
ac.banned,
|
||||
ac.banned_from_community,
|
||||
ac.creator_name,
|
||||
ac.creator_preferred_username,
|
||||
ac.creator_avatar,
|
||||
ac.score,
|
||||
ac.upvotes,
|
||||
ac.downvotes,
|
||||
ac.hot_rank,
|
||||
ac.hot_rank_active,
|
||||
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
|
||||
;
|
||||
|
||||
-- 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
|
||||
;
|
||||
|
||||
-- Adding hot rank active to the triggers
|
||||
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,
|
||||
hot_rank_active = pav.hot_rank_active
|
||||
from post_aggregates_view as pav
|
||||
where paf.id = pav.id and (pav.published > ('now'::timestamp - '1 week'::interval));
|
||||
END IF;
|
||||
|
||||
return null;
|
||||
end $$;
|
||||
|
||||
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;
|
||||
|
||||
-- Update the comment hot_ranks as of last week
|
||||
update comment_aggregates_fast as caf
|
||||
set
|
||||
hot_rank = cav.hot_rank,
|
||||
hot_rank_active = cav.hot_rank_active
|
||||
from comment_aggregates_view as cav
|
||||
where caf.id = cav.id and (cav.published > ('now'::timestamp - '1 week'::interval));
|
||||
|
||||
-- Update the post ranks
|
||||
update post_aggregates_fast as paf
|
||||
set
|
||||
hot_rank = pav.hot_rank,
|
||||
hot_rank_active = pav.hot_rank_active
|
||||
from post_aggregates_view as pav
|
||||
where paf.id = pav.id and (pav.published > ('now'::timestamp - '1 week'::interval));
|
||||
|
||||
-- Force the hot rank active as zero on 2 day-older posts (necro-bump)
|
||||
update post_aggregates_fast as paf
|
||||
set hot_rank_active = 0
|
||||
where paf.id = NEW.post_id and (paf.published < ('now'::timestamp - '2 days'::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 $$;
|
|
@ -10,7 +10,15 @@ use crate::{
|
|||
},
|
||||
DbPool,
|
||||
};
|
||||
use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
|
||||
use lemmy_db::{
|
||||
diesel_option_overwrite,
|
||||
naive_now,
|
||||
Bannable,
|
||||
Crud,
|
||||
Followable,
|
||||
Joinable,
|
||||
SortType,
|
||||
};
|
||||
use lemmy_utils::{
|
||||
generate_actor_keypair,
|
||||
is_valid_community_name,
|
||||
|
@ -40,6 +48,8 @@ pub struct CreateCommunity {
|
|||
name: String,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
category_id: i32,
|
||||
nsfw: bool,
|
||||
auth: String,
|
||||
|
@ -97,6 +107,8 @@ pub struct EditCommunity {
|
|||
pub edit_id: i32,
|
||||
title: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
category_id: i32,
|
||||
nsfw: bool,
|
||||
auth: String,
|
||||
|
@ -251,6 +263,8 @@ impl Perform for Oper<CreateCommunity> {
|
|||
name: data.name.to_owned(),
|
||||
title: data.title.to_owned(),
|
||||
description: data.description.to_owned(),
|
||||
icon: Some(data.icon.to_owned()),
|
||||
banner: Some(data.banner.to_owned()),
|
||||
category_id: data.category_id,
|
||||
creator_id: user.id,
|
||||
removed: None,
|
||||
|
@ -332,10 +346,15 @@ impl Perform for Oper<EditCommunity> {
|
|||
let edit_id = data.edit_id;
|
||||
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
||||
|
||||
let icon = diesel_option_overwrite(&data.icon);
|
||||
let banner = diesel_option_overwrite(&data.banner);
|
||||
|
||||
let community_form = CommunityForm {
|
||||
name: read_community.name,
|
||||
title: data.title.to_owned(),
|
||||
description: data.description.to_owned(),
|
||||
icon,
|
||||
banner,
|
||||
category_id: data.category_id.to_owned(),
|
||||
creator_id: read_community.creator_id,
|
||||
removed: Some(read_community.removed),
|
||||
|
|
|
@ -55,7 +55,7 @@ pub trait Perform {
|
|||
) -> Result<Self::Response, LemmyError>;
|
||||
}
|
||||
|
||||
pub async fn is_mod_or_admin(
|
||||
pub(in crate::api) async fn is_mod_or_admin(
|
||||
pool: &DbPool,
|
||||
user_id: i32,
|
||||
community_id: i32,
|
||||
|
@ -65,8 +65,7 @@ pub async fn is_mod_or_admin(
|
|||
})
|
||||
.await?;
|
||||
if !is_mod_or_admin {
|
||||
// TODO: more accurately, not_a_mod_or_admin?
|
||||
return Err(APIError::err("not_an_admin").into());
|
||||
return Err(APIError::err("not_a_mod_or_admin").into());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@ use lemmy_db::{
|
|||
category::*,
|
||||
comment_view::*,
|
||||
community_view::*,
|
||||
diesel_option_overwrite,
|
||||
moderator::*,
|
||||
moderator_views::*,
|
||||
naive_now,
|
||||
|
@ -91,6 +92,8 @@ pub struct GetModlogResponse {
|
|||
pub struct CreateSite {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub icon: Option<String>,
|
||||
pub banner: Option<String>,
|
||||
pub enable_downvotes: bool,
|
||||
pub open_registration: bool,
|
||||
pub enable_nsfw: bool,
|
||||
|
@ -101,6 +104,8 @@ pub struct CreateSite {
|
|||
pub struct EditSite {
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
icon: Option<String>,
|
||||
banner: Option<String>,
|
||||
enable_downvotes: bool,
|
||||
open_registration: bool,
|
||||
enable_nsfw: bool,
|
||||
|
@ -263,6 +268,8 @@ impl Perform for Oper<CreateSite> {
|
|||
let site_form = SiteForm {
|
||||
name: data.name.to_owned(),
|
||||
description: data.description.to_owned(),
|
||||
icon: Some(data.icon.to_owned()),
|
||||
banner: Some(data.banner.to_owned()),
|
||||
creator_id: user.id,
|
||||
enable_downvotes: data.enable_downvotes,
|
||||
open_registration: data.open_registration,
|
||||
|
@ -300,9 +307,14 @@ impl Perform for Oper<EditSite> {
|
|||
|
||||
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
|
||||
|
||||
let icon = diesel_option_overwrite(&data.icon);
|
||||
let banner = diesel_option_overwrite(&data.banner);
|
||||
|
||||
let site_form = SiteForm {
|
||||
name: data.name.to_owned(),
|
||||
description: data.description.to_owned(),
|
||||
icon,
|
||||
banner,
|
||||
creator_id: found_site.creator_id,
|
||||
updated: Some(naive_now()),
|
||||
enable_downvotes: data.enable_downvotes,
|
||||
|
@ -365,6 +377,8 @@ impl Perform for Oper<GetSite> {
|
|||
let create_site = CreateSite {
|
||||
name: setup.site_name.to_owned(),
|
||||
description: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
enable_downvotes: true,
|
||||
open_registration: true,
|
||||
enable_nsfw: true,
|
||||
|
@ -611,18 +625,9 @@ impl Perform for Oper<TransferSite> {
|
|||
return Err(APIError::err("not_an_admin").into());
|
||||
}
|
||||
|
||||
let site_form = SiteForm {
|
||||
name: read_site.name,
|
||||
description: read_site.description,
|
||||
creator_id: data.user_id,
|
||||
updated: Some(naive_now()),
|
||||
enable_downvotes: read_site.enable_downvotes,
|
||||
open_registration: read_site.open_registration,
|
||||
enable_nsfw: read_site.enable_nsfw,
|
||||
};
|
||||
|
||||
let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
|
||||
if blocking(pool, update_site).await?.is_err() {
|
||||
let new_creator_id = data.user_id;
|
||||
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
|
||||
if blocking(pool, transfer_site).await?.is_err() {
|
||||
return Err(APIError::err("couldnt_update_site").into());
|
||||
};
|
||||
|
||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_db::{
|
|||
comment_view::*,
|
||||
community::*,
|
||||
community_view::*,
|
||||
diesel_option_overwrite,
|
||||
moderator::*,
|
||||
naive_now,
|
||||
password_reset_request::*,
|
||||
|
@ -103,6 +104,8 @@ pub struct SaveUserSettings {
|
|||
default_listing_type: i16,
|
||||
lang: String,
|
||||
avatar: Option<String>,
|
||||
banner: Option<String>,
|
||||
preferred_username: Option<String>,
|
||||
email: Option<String>,
|
||||
bio: Option<String>,
|
||||
matrix_user_id: Option<String>,
|
||||
|
@ -395,6 +398,7 @@ impl Perform for Oper<Register> {
|
|||
email: data.email.to_owned(),
|
||||
matrix_user_id: None,
|
||||
avatar: None,
|
||||
banner: None,
|
||||
password_encrypted: data.password.to_owned(),
|
||||
preferred_username: None,
|
||||
updated: None,
|
||||
|
@ -402,7 +406,7 @@ impl Perform for Oper<Register> {
|
|||
banned: false,
|
||||
show_nsfw: data.show_nsfw,
|
||||
theme: "darkly".into(),
|
||||
default_sort_type: SortType::Hot as i16,
|
||||
default_sort_type: SortType::Active as i16,
|
||||
default_listing_type: ListingType::Subscribed as i16,
|
||||
lang: "browser".into(),
|
||||
show_avatars: true,
|
||||
|
@ -454,6 +458,8 @@ impl Perform for Oper<Register> {
|
|||
public_key: Some(main_community_keypair.public_key),
|
||||
last_refreshed_at: None,
|
||||
published: None,
|
||||
icon: None,
|
||||
banner: None,
|
||||
};
|
||||
blocking(pool, move |conn| Community::create(conn, &community_form)).await??
|
||||
}
|
||||
|
@ -569,9 +575,13 @@ impl Perform for Oper<SaveUserSettings> {
|
|||
None => read_user.bio,
|
||||
};
|
||||
|
||||
let avatar = match &data.avatar {
|
||||
Some(avatar) => Some(avatar.to_owned()),
|
||||
None => read_user.avatar,
|
||||
let avatar = diesel_option_overwrite(&data.avatar);
|
||||
let banner = diesel_option_overwrite(&data.banner);
|
||||
|
||||
// The DB constraint should stop too many characters
|
||||
let preferred_username = match &data.preferred_username {
|
||||
Some(preferred_username) => Some(preferred_username.to_owned()),
|
||||
None => read_user.preferred_username,
|
||||
};
|
||||
|
||||
let password_encrypted = match &data.new_password {
|
||||
|
@ -612,8 +622,9 @@ impl Perform for Oper<SaveUserSettings> {
|
|||
email,
|
||||
matrix_user_id: data.matrix_user_id.to_owned(),
|
||||
avatar,
|
||||
banner,
|
||||
password_encrypted,
|
||||
preferred_username: read_user.preferred_username,
|
||||
preferred_username,
|
||||
updated: Some(naive_now()),
|
||||
admin: read_user.admin,
|
||||
banned: read_user.banned,
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
use crate::{
|
||||
api::check_slurs,
|
||||
apub::{
|
||||
activities::{generate_activity_id, send_activity_to_community},
|
||||
check_actor_domain,
|
||||
|
@ -50,7 +49,7 @@ use lemmy_db::{
|
|||
user::User_,
|
||||
Crud,
|
||||
};
|
||||
use lemmy_utils::{convert_datetime, scrape_text_for_mentions, MentionData};
|
||||
use lemmy_utils::{convert_datetime, remove_slurs, scrape_text_for_mentions, MentionData};
|
||||
use log::debug;
|
||||
use serde::Deserialize;
|
||||
use serde_json::Error;
|
||||
|
@ -174,13 +173,13 @@ impl FromApub for CommentForm {
|
|||
.as_single_xsd_string()
|
||||
.unwrap()
|
||||
.to_string();
|
||||
check_slurs(&content)?;
|
||||
let content_slurs_removed = remove_slurs(&content);
|
||||
|
||||
Ok(CommentForm {
|
||||
creator_id: creator.id,
|
||||
post_id: post.id,
|
||||
parent_id,
|
||||
content,
|
||||
content: content_slurs_removed,
|
||||
removed: None,
|
||||
read: None,
|
||||
published: note.published().map(|u| u.to_owned().naive_local()),
|
||||
|
|
|
@ -34,7 +34,7 @@ use activitystreams::{
|
|||
base::{AnyBase, BaseExt},
|
||||
collection::{OrderedCollection, UnorderedCollection},
|
||||
context,
|
||||
object::Tombstone,
|
||||
object::{Image, Tombstone},
|
||||
prelude::*,
|
||||
public,
|
||||
};
|
||||
|
@ -361,6 +361,32 @@ impl FromApub for CommunityForm {
|
|||
check_slurs(&title)?;
|
||||
check_slurs_opt(&description)?;
|
||||
|
||||
let icon = match group.icon() {
|
||||
Some(any_image) => Some(
|
||||
Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let banner = match group.image() {
|
||||
Some(any_image) => Some(
|
||||
Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(CommunityForm {
|
||||
name,
|
||||
title,
|
||||
|
@ -377,6 +403,8 @@ impl FromApub for CommunityForm {
|
|||
private_key: None,
|
||||
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
icon,
|
||||
banner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -350,7 +350,7 @@ async fn fetch_remote_community(
|
|||
let outbox_items = outbox.items().unwrap().clone();
|
||||
for o in outbox_items.many().unwrap() {
|
||||
let page = PageExt::from_any_base(o)?.unwrap();
|
||||
let post = PostForm::from_apub(&page, client, pool, Some(apub_id.to_owned())).await?;
|
||||
let post = PostForm::from_apub(&page, client, pool, None).await?;
|
||||
let post_ap_id = post.ap_id.clone();
|
||||
// Check whether the post already exists in the local db
|
||||
let existing = blocking(pool, move |conn| Post::read_from_apub_id(conn, &post_ap_id)).await?;
|
||||
|
@ -358,6 +358,7 @@ async fn fetch_remote_community(
|
|||
Ok(e) => blocking(pool, move |conn| Post::update(conn, e.id, &post)).await??,
|
||||
Err(_) => blocking(pool, move |conn| Post::create(conn, &post)).await??,
|
||||
};
|
||||
// TODO: we need to send a websocket update here
|
||||
}
|
||||
|
||||
Ok(community)
|
||||
|
|
|
@ -1,18 +1,15 @@
|
|||
use crate::{
|
||||
apub::{
|
||||
inbox::{
|
||||
activities::{
|
||||
create::receive_create,
|
||||
delete::receive_delete,
|
||||
dislike::receive_dislike,
|
||||
like::receive_like,
|
||||
remove::receive_remove,
|
||||
undo::receive_undo,
|
||||
update::receive_update,
|
||||
},
|
||||
shared_inbox::{get_community_from_activity, receive_unhandled_activity},
|
||||
apub::inbox::{
|
||||
activities::{
|
||||
create::receive_create,
|
||||
delete::receive_delete,
|
||||
dislike::receive_dislike,
|
||||
like::receive_like,
|
||||
remove::receive_remove,
|
||||
undo::receive_undo,
|
||||
update::receive_update,
|
||||
},
|
||||
ActorType,
|
||||
shared_inbox::{get_community_id_from_activity, receive_unhandled_activity},
|
||||
},
|
||||
routes::ChatServerParam,
|
||||
DbPool,
|
||||
|
@ -34,8 +31,8 @@ pub async fn receive_announce(
|
|||
let announce = Announce::from_any_base(activity)?.unwrap();
|
||||
|
||||
// ensure that announce and community come from the same instance
|
||||
let community = get_community_from_activity(&announce, client, pool).await?;
|
||||
announce.id(community.actor_id()?.domain().unwrap())?;
|
||||
let community = get_community_id_from_activity(&announce)?;
|
||||
announce.id(community.domain().unwrap())?;
|
||||
|
||||
let kind = announce.object().as_single_kind_str();
|
||||
let object = announce.object();
|
||||
|
|
|
@ -24,7 +24,6 @@ use crate::{
|
|||
};
|
||||
use activitystreams::{activity::Create, base::AnyBase, object::Note, prelude::*};
|
||||
use actix_web::{client::Client, HttpResponse};
|
||||
use anyhow::anyhow;
|
||||
use lemmy_db::{
|
||||
comment::{Comment, CommentForm},
|
||||
comment_view::CommentView,
|
||||
|
@ -63,10 +62,6 @@ async fn receive_create_post(
|
|||
let page = PageExt::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
|
||||
|
||||
let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
|
||||
// TODO: not sure if it makes sense to check for the exact user, seeing as we already check the domain
|
||||
if post.creator_id != user.id {
|
||||
return Err(anyhow!("Actor for create activity and post creator need to be identical").into());
|
||||
}
|
||||
|
||||
let inserted_post = blocking(pool, move |conn| Post::create(conn, &post)).await??;
|
||||
|
||||
|
@ -99,11 +94,6 @@ async fn receive_create_comment(
|
|||
let note = Note::from_any_base(create.object().to_owned().one().unwrap())?.unwrap();
|
||||
|
||||
let comment = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?)).await?;
|
||||
if comment.creator_id != user.id {
|
||||
return Err(
|
||||
anyhow!("Actor for create activity and comment creator need to be identical").into(),
|
||||
);
|
||||
}
|
||||
|
||||
let inserted_comment = blocking(pool, move |conn| Comment::create(conn, &comment)).await??;
|
||||
|
||||
|
|
|
@ -194,6 +194,8 @@ async fn receive_delete_community(
|
|||
private_key: community.private_key,
|
||||
public_key: community.public_key,
|
||||
last_refreshed_at: None,
|
||||
icon: Some(community.icon.to_owned()),
|
||||
banner: Some(community.banner.to_owned()),
|
||||
};
|
||||
|
||||
let community_id = community.id;
|
||||
|
|
|
@ -1,15 +1,10 @@
|
|||
use crate::{
|
||||
api::{
|
||||
comment::CommentResponse,
|
||||
community::CommunityResponse,
|
||||
is_mod_or_admin,
|
||||
post::PostResponse,
|
||||
},
|
||||
api::{comment::CommentResponse, community::CommunityResponse, post::PostResponse},
|
||||
apub::{
|
||||
fetcher::{get_or_fetch_and_insert_comment, get_or_fetch_and_insert_post},
|
||||
inbox::shared_inbox::{
|
||||
announce_if_community_is_local,
|
||||
get_community_from_activity,
|
||||
get_community_id_from_activity,
|
||||
get_user_from_activity,
|
||||
receive_unhandled_activity,
|
||||
},
|
||||
|
@ -29,6 +24,7 @@ use crate::{
|
|||
};
|
||||
use activitystreams::{activity::Remove, base::AnyBase, object::Note, prelude::*};
|
||||
use actix_web::{client::Client, HttpResponse};
|
||||
use anyhow::anyhow;
|
||||
use lemmy_db::{
|
||||
comment::{Comment, CommentForm},
|
||||
comment_view::CommentView,
|
||||
|
@ -48,9 +44,10 @@ pub async fn receive_remove(
|
|||
) -> Result<HttpResponse, LemmyError> {
|
||||
let remove = Remove::from_any_base(activity)?.unwrap();
|
||||
let actor = get_user_from_activity(&remove, client, pool).await?;
|
||||
let community = get_community_from_activity(&remove, client, pool).await?;
|
||||
// TODO: we dont federate remote admins at all, and remote mods arent federated properly
|
||||
is_mod_or_admin(pool, actor.id, community.id).await?;
|
||||
let community = get_community_id_from_activity(&remove)?;
|
||||
if actor.actor_id()?.domain() != community.domain() {
|
||||
return Err(anyhow!("Remove activities are only allowed on local objects").into());
|
||||
}
|
||||
|
||||
match remove.object().as_single_kind_str() {
|
||||
Some("Page") => receive_remove_post(remove, client, pool, chat_server).await,
|
||||
|
@ -205,6 +202,8 @@ async fn receive_remove_community(
|
|||
private_key: community.private_key,
|
||||
public_key: community.public_key,
|
||||
last_refreshed_at: None,
|
||||
icon: Some(community.icon.to_owned()),
|
||||
banner: Some(community.banner.to_owned()),
|
||||
};
|
||||
|
||||
let community_id = community.id;
|
||||
|
|
|
@ -67,8 +67,8 @@ where
|
|||
let inner_actor = inner_activity.actor()?;
|
||||
let inner_actor_uri = inner_actor.as_single_xsd_any_uri().unwrap();
|
||||
|
||||
if outer_actor_uri != inner_actor_uri {
|
||||
Err(anyhow!("An actor can only undo its own activities").into())
|
||||
if outer_actor_uri.domain() != inner_actor_uri.domain() {
|
||||
Err(anyhow!("Cant undo activities from a different instance").into())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
|
@ -209,7 +209,7 @@ async fn receive_undo_remove_comment(
|
|||
let mod_ = get_user_from_activity(remove, client, pool).await?;
|
||||
let note = Note::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
|
||||
|
||||
let comment_ap_id = CommentForm::from_apub(¬e, client, pool, Some(mod_.actor_id()?))
|
||||
let comment_ap_id = CommentForm::from_apub(¬e, client, pool, None)
|
||||
.await?
|
||||
.get_ap_id()?;
|
||||
|
||||
|
@ -322,7 +322,7 @@ async fn receive_undo_remove_post(
|
|||
let mod_ = get_user_from_activity(remove, client, pool).await?;
|
||||
let page = PageExt::from_any_base(remove.object().to_owned().one().unwrap())?.unwrap();
|
||||
|
||||
let post_ap_id = PostForm::from_apub(&page, client, pool, Some(mod_.actor_id()?))
|
||||
let post_ap_id = PostForm::from_apub(&page, client, pool, None)
|
||||
.await?
|
||||
.get_ap_id()?;
|
||||
|
||||
|
@ -402,6 +402,8 @@ async fn receive_undo_delete_community(
|
|||
private_key: community.private_key,
|
||||
public_key: community.public_key,
|
||||
last_refreshed_at: None,
|
||||
icon: Some(community.icon.to_owned()),
|
||||
banner: Some(community.banner.to_owned()),
|
||||
};
|
||||
|
||||
let community_id = community.id;
|
||||
|
@ -466,6 +468,8 @@ async fn receive_undo_remove_community(
|
|||
private_key: community.private_key,
|
||||
public_key: community.public_key,
|
||||
last_refreshed_at: None,
|
||||
icon: Some(community.icon.to_owned()),
|
||||
banner: Some(community.banner.to_owned()),
|
||||
};
|
||||
|
||||
let community_id = community.id;
|
||||
|
|
|
@ -25,7 +25,6 @@ use crate::{
|
|||
};
|
||||
use activitystreams::{activity::Update, base::AnyBase, object::Note, prelude::*};
|
||||
use actix_web::{client::Client, HttpResponse};
|
||||
use anyhow::anyhow;
|
||||
use lemmy_db::{
|
||||
comment::{Comment, CommentForm},
|
||||
comment_view::CommentView,
|
||||
|
@ -64,16 +63,11 @@ async fn receive_update_post(
|
|||
let page = PageExt::from_any_base(update.object().to_owned().one().unwrap())?.unwrap();
|
||||
|
||||
let post = PostForm::from_apub(&page, client, pool, Some(user.actor_id()?)).await?;
|
||||
if post.creator_id != user.id {
|
||||
return Err(anyhow!("Actor for update activity and post creator need to be identical").into());
|
||||
}
|
||||
|
||||
let original_post = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool).await?;
|
||||
if post.ap_id != original_post.ap_id {
|
||||
return Err(anyhow!("Updated post ID needs to be identical to the original ID").into());
|
||||
}
|
||||
let original_post_id = get_or_fetch_and_insert_post(&post.get_ap_id()?, client, pool)
|
||||
.await?
|
||||
.id;
|
||||
|
||||
let original_post_id = original_post.id;
|
||||
blocking(pool, move |conn| {
|
||||
Post::update(conn, original_post_id, &post)
|
||||
})
|
||||
|
@ -107,17 +101,11 @@ async fn receive_update_comment(
|
|||
let user = get_user_from_activity(&update, client, pool).await?;
|
||||
|
||||
let comment = CommentForm::from_apub(¬e, client, pool, Some(user.actor_id()?)).await?;
|
||||
if comment.creator_id != user.id {
|
||||
return Err(anyhow!("Actor for update activity and post creator need to be identical").into());
|
||||
}
|
||||
|
||||
let original_comment =
|
||||
get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool).await?;
|
||||
if comment.ap_id != original_comment.ap_id {
|
||||
return Err(anyhow!("Updated post ID needs to be identical to the original ID").into());
|
||||
}
|
||||
let original_comment_id = get_or_fetch_and_insert_comment(&comment.get_ap_id()?, client, pool)
|
||||
.await?
|
||||
.id;
|
||||
|
||||
let original_comment_id = original_comment.id;
|
||||
let updated_comment = blocking(pool, move |conn| {
|
||||
Comment::update(conn, original_comment_id, &comment)
|
||||
})
|
||||
|
|
|
@ -69,14 +69,16 @@ pub async fn community_inbox(
|
|||
|
||||
verify(&request, &user)?;
|
||||
|
||||
insert_activity(user.id, activity.clone(), false, &db).await?;
|
||||
|
||||
let any_base = activity.clone().into_any_base()?;
|
||||
let kind = activity.kind().unwrap();
|
||||
match kind {
|
||||
ValidTypes::Follow => handle_follow(any_base, user, community, &client, db).await,
|
||||
ValidTypes::Undo => handle_undo_follow(any_base, user, community, db).await,
|
||||
}
|
||||
let user_id = user.id;
|
||||
let res = match kind {
|
||||
ValidTypes::Follow => handle_follow(any_base, user, community, &client, &db).await,
|
||||
ValidTypes::Undo => handle_undo_follow(any_base, user, community, &db).await,
|
||||
};
|
||||
|
||||
insert_activity(user_id, activity.clone(), false, &db).await?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Handle a follow request from a remote user, adding it to the local database and returning an
|
||||
|
@ -86,7 +88,7 @@ async fn handle_follow(
|
|||
user: User_,
|
||||
community: Community,
|
||||
client: &Client,
|
||||
db: DbPoolParam,
|
||||
db: &DbPoolParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let follow = Follow::from_any_base(activity)?.unwrap();
|
||||
let community_follower_form = CommunityFollowerForm {
|
||||
|
@ -95,12 +97,12 @@ async fn handle_follow(
|
|||
};
|
||||
|
||||
// This will fail if they're already a follower, but ignore the error.
|
||||
blocking(&db, move |conn| {
|
||||
blocking(db, move |conn| {
|
||||
CommunityFollower::follow(&conn, &community_follower_form).ok()
|
||||
})
|
||||
.await?;
|
||||
|
||||
community.send_accept_follow(follow, &client, &db).await?;
|
||||
community.send_accept_follow(follow, &client, db).await?;
|
||||
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
}
|
||||
|
@ -109,7 +111,7 @@ async fn handle_undo_follow(
|
|||
activity: AnyBase,
|
||||
user: User_,
|
||||
community: Community,
|
||||
db: DbPoolParam,
|
||||
db: &DbPoolParam,
|
||||
) -> Result<HttpResponse, LemmyError> {
|
||||
let _undo = Undo::from_any_base(activity)?.unwrap();
|
||||
|
||||
|
@ -119,7 +121,7 @@ async fn handle_undo_follow(
|
|||
};
|
||||
|
||||
// This will fail if they aren't a follower, but ignore the error.
|
||||
blocking(&db, move |conn| {
|
||||
blocking(db, move |conn| {
|
||||
CommunityFollower::unfollow(&conn, &community_follower_form).ok()
|
||||
})
|
||||
.await?;
|
||||
|
|
|
@ -31,7 +31,7 @@ use activitystreams::{
|
|||
prelude::*,
|
||||
};
|
||||
use actix_web::{client::Client, web, HttpRequest, HttpResponse};
|
||||
use lemmy_db::{community::Community, user::User_};
|
||||
use lemmy_db::user::User_;
|
||||
use log::debug;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Debug;
|
||||
|
@ -68,21 +68,17 @@ pub async fn shared_inbox(
|
|||
debug!("Shared inbox received activity: {}", json);
|
||||
|
||||
let sender = &activity.actor()?.to_owned().single_xsd_any_uri().unwrap();
|
||||
// TODO: pass this actor in instead of using get_user_from_activity()
|
||||
let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
|
||||
|
||||
// TODO: i dont think this works for Announce/Undo activities
|
||||
//let community = get_community_id_from_activity(&activity).await;
|
||||
let community = get_community_id_from_activity(&activity)?;
|
||||
|
||||
check_is_apub_id_valid(sender)?;
|
||||
verify(&request, actor.as_ref())?;
|
||||
check_is_apub_id_valid(&community)?;
|
||||
|
||||
// TODO: probably better to do this after, so we dont store activities that fail a check somewhere
|
||||
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
|
||||
let actor = get_or_fetch_and_upsert_actor(sender, &client, &pool).await?;
|
||||
verify(&request, actor.as_ref())?;
|
||||
|
||||
let any_base = activity.clone().into_any_base()?;
|
||||
let kind = activity.kind().unwrap();
|
||||
match kind {
|
||||
let res = match kind {
|
||||
ValidTypes::Announce => receive_announce(any_base, &client, &pool, chat_server).await,
|
||||
ValidTypes::Create => receive_create(any_base, &client, &pool, chat_server).await,
|
||||
ValidTypes::Update => receive_update(any_base, &client, &pool, chat_server).await,
|
||||
|
@ -91,7 +87,10 @@ pub async fn shared_inbox(
|
|||
ValidTypes::Remove => receive_remove(any_base, &client, &pool, chat_server).await,
|
||||
ValidTypes::Delete => receive_delete(any_base, &client, &pool, chat_server).await,
|
||||
ValidTypes::Undo => receive_undo(any_base, &client, &pool, chat_server).await,
|
||||
}
|
||||
};
|
||||
|
||||
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
|
||||
res
|
||||
}
|
||||
|
||||
pub(in crate::apub::inbox) fn receive_unhandled_activity<A>(
|
||||
|
@ -117,18 +116,15 @@ where
|
|||
get_or_fetch_and_upsert_user(&user_uri, client, pool).await
|
||||
}
|
||||
|
||||
pub(in crate::apub::inbox) async fn get_community_from_activity<T, A>(
|
||||
pub(in crate::apub::inbox) fn get_community_id_from_activity<T, A>(
|
||||
activity: &T,
|
||||
client: &Client,
|
||||
pool: &DbPool,
|
||||
) -> Result<Community, LemmyError>
|
||||
) -> Result<Url, LemmyError>
|
||||
where
|
||||
T: AsBase<A> + ActorAndObjectRef + AsObject<A>,
|
||||
{
|
||||
let cc = activity.cc().unwrap();
|
||||
let cc = cc.as_many().unwrap();
|
||||
let community_uri = cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned();
|
||||
get_or_fetch_and_upsert_community(&community_uri, client, pool).await
|
||||
Ok(cc.first().unwrap().as_xsd_any_uri().unwrap().to_owned())
|
||||
}
|
||||
|
||||
pub(in crate::apub::inbox) async fn announce_if_community_is_local<T, Kind>(
|
||||
|
|
|
@ -65,11 +65,9 @@ pub async fn user_inbox(
|
|||
let actor = get_or_fetch_and_upsert_actor(actor_uri, &client, &pool).await?;
|
||||
verify(&request, actor.as_ref())?;
|
||||
|
||||
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
|
||||
|
||||
let any_base = activity.clone().into_any_base()?;
|
||||
let kind = activity.kind().unwrap();
|
||||
match kind {
|
||||
let res = match kind {
|
||||
ValidTypes::Accept => receive_accept(any_base, username, &client, &pool).await,
|
||||
ValidTypes::Create => {
|
||||
receive_create_private_message(any_base, &client, &pool, chat_server).await
|
||||
|
@ -83,7 +81,10 @@ pub async fn user_inbox(
|
|||
ValidTypes::Undo => {
|
||||
receive_undo_delete_private_message(any_base, &client, &pool, chat_server).await
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
insert_activity(actor.user_id(), activity.clone(), false, &pool).await?;
|
||||
res
|
||||
}
|
||||
|
||||
/// Handle accepted follows.
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
use crate::{
|
||||
api::{check_slurs, check_slurs_opt},
|
||||
api::check_slurs,
|
||||
apub::{
|
||||
activities::{generate_activity_id, send_activity_to_community},
|
||||
check_actor_domain,
|
||||
|
@ -44,7 +44,7 @@ use lemmy_db::{
|
|||
user::User_,
|
||||
Crud,
|
||||
};
|
||||
use lemmy_utils::convert_datetime;
|
||||
use lemmy_utils::{convert_datetime, remove_slurs};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
|
@ -225,11 +225,11 @@ impl FromApub for PostForm {
|
|||
.as_ref()
|
||||
.map(|c| c.as_single_xsd_string().unwrap().to_string());
|
||||
check_slurs(&name)?;
|
||||
check_slurs_opt(&body)?;
|
||||
let body_slurs_removed = body.map(|b| remove_slurs(&b));
|
||||
Ok(PostForm {
|
||||
name,
|
||||
url,
|
||||
body,
|
||||
body: body_slurs_removed,
|
||||
creator_id: creator.id,
|
||||
community_id: community.id,
|
||||
removed: None,
|
||||
|
|
|
@ -65,6 +65,12 @@ impl ToApub for User_ {
|
|||
person.set_icon(image.into_any_base()?);
|
||||
}
|
||||
|
||||
if let Some(banner_url) = &self.banner {
|
||||
let mut image = Image::new();
|
||||
image.set_url(banner_url.to_owned());
|
||||
person.set_image(image.into_any_base()?);
|
||||
}
|
||||
|
||||
if let Some(bio) = &self.bio {
|
||||
person.set_summary(bio.to_owned());
|
||||
}
|
||||
|
@ -214,13 +220,28 @@ impl FromApub for UserForm {
|
|||
expected_domain: Option<Url>,
|
||||
) -> Result<Self, LemmyError> {
|
||||
let avatar = match person.icon() {
|
||||
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
Some(any_image) => Some(
|
||||
Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let banner = match person.image() {
|
||||
Some(any_image) => Some(
|
||||
Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
.url()
|
||||
.unwrap()
|
||||
.as_single_xsd_any_uri()
|
||||
.map(|u| u.to_string()),
|
||||
),
|
||||
None => None,
|
||||
};
|
||||
|
||||
|
@ -249,6 +270,7 @@ impl FromApub for UserForm {
|
|||
banned: false,
|
||||
email: None,
|
||||
avatar,
|
||||
banner,
|
||||
updated: person.updated().map(|u| u.to_owned().naive_local()),
|
||||
show_nsfw: false,
|
||||
theme: "".to_string(),
|
||||
|
|
|
@ -53,7 +53,8 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
|||
name: cuser.name.to_owned(),
|
||||
email: cuser.email.to_owned(),
|
||||
matrix_user_id: cuser.matrix_user_id.to_owned(),
|
||||
avatar: cuser.avatar.to_owned(),
|
||||
avatar: Some(cuser.avatar.to_owned()),
|
||||
banner: Some(cuser.banner.to_owned()),
|
||||
password_encrypted: cuser.password_encrypted.to_owned(),
|
||||
preferred_username: cuser.preferred_username.to_owned(),
|
||||
updated: None,
|
||||
|
@ -116,6 +117,8 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
|||
public_key: Some(keypair.public_key),
|
||||
last_refreshed_at: Some(naive_now()),
|
||||
published: None,
|
||||
icon: Some(ccommunity.icon.to_owned()),
|
||||
banner: Some(ccommunity.banner.to_owned()),
|
||||
};
|
||||
|
||||
Community::update(&conn, ccommunity.id, &form)?;
|
||||
|
|
|
@ -27,7 +27,7 @@ use lemmy_server::{
|
|||
blocking,
|
||||
code_migrations::run_advanced_migrations,
|
||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||
routes::{api, federation, feeds, index, nodeinfo, webfinger},
|
||||
routes::*,
|
||||
websocket::server::*,
|
||||
LemmyError,
|
||||
};
|
||||
|
@ -91,9 +91,10 @@ async fn main() -> Result<(), LemmyError> {
|
|||
.data(server.clone())
|
||||
.data(Client::default())
|
||||
// The routes
|
||||
.configure(move |cfg| api::config(cfg, &rate_limiter))
|
||||
.configure(|cfg| api::config(cfg, &rate_limiter))
|
||||
.configure(federation::config)
|
||||
.configure(feeds::config)
|
||||
.configure(|cfg| images::config(cfg, &rate_limiter))
|
||||
.configure(index::config)
|
||||
.configure(nodeinfo::config)
|
||||
.configure(webfinger::config)
|
||||
|
|
|
@ -45,6 +45,10 @@ impl RateLimit {
|
|||
self.kind(RateLimitType::Register)
|
||||
}
|
||||
|
||||
pub fn image(&self) -> RateLimited {
|
||||
self.kind(RateLimitType::Image)
|
||||
}
|
||||
|
||||
fn kind(&self, type_: RateLimitType) -> RateLimited {
|
||||
RateLimited {
|
||||
rate_limiter: self.rate_limiter.clone(),
|
||||
|
@ -101,6 +105,15 @@ impl RateLimited {
|
|||
true,
|
||||
)?;
|
||||
}
|
||||
RateLimitType::Image => {
|
||||
limiter.check_rate_limit_full(
|
||||
self.type_,
|
||||
&ip_addr,
|
||||
rate_limit.image,
|
||||
rate_limit.image_per_second,
|
||||
false,
|
||||
)?;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -15,6 +15,7 @@ pub enum RateLimitType {
|
|||
Message,
|
||||
Register,
|
||||
Post,
|
||||
Image,
|
||||
}
|
||||
|
||||
/// Rate limiting based on rate type and IP addr
|
||||
|
|
140
server/src/routes/images.rs
Normal file
140
server/src/routes/images.rs
Normal file
|
@ -0,0 +1,140 @@
|
|||
use crate::rate_limit::RateLimit;
|
||||
use actix::clock::Duration;
|
||||
use actix_web::{body::BodyStream, http::StatusCode, *};
|
||||
use awc::Client;
|
||||
use lemmy_utils::settings::Settings;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
|
||||
let client = Client::build()
|
||||
.header("User-Agent", "pict-rs-frontend, v0.1.0")
|
||||
.timeout(Duration::from_secs(30))
|
||||
.finish();
|
||||
|
||||
cfg
|
||||
.data(client)
|
||||
.service(
|
||||
web::resource("/pictrs/image")
|
||||
.wrap(rate_limit.image())
|
||||
.route(web::post().to(upload)),
|
||||
)
|
||||
.service(web::resource("/pictrs/image/{filename}").route(web::get().to(full_res)))
|
||||
.service(
|
||||
web::resource("/pictrs/image/thumbnail{size}/{filename}").route(web::get().to(thumbnail)),
|
||||
)
|
||||
.service(web::resource("/pictrs/image/delete/{token}/{filename}").route(web::get().to(delete)));
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Image {
|
||||
file: String,
|
||||
delete_token: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Images {
|
||||
msg: String,
|
||||
files: Option<Vec<Image>>,
|
||||
}
|
||||
|
||||
async fn upload(
|
||||
req: HttpRequest,
|
||||
body: web::Payload,
|
||||
client: web::Data<Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
// TODO: check auth and rate limit here
|
||||
|
||||
let mut res = client
|
||||
.request_from(format!("{}/image", Settings::get().pictrs_url), req.head())
|
||||
.if_some(req.head().peer_addr, |addr, req| {
|
||||
req.header("X-Forwarded-For", addr.to_string())
|
||||
})
|
||||
.send_stream(body)
|
||||
.await?;
|
||||
|
||||
let images = res.json::<Images>().await?;
|
||||
|
||||
Ok(HttpResponse::build(res.status()).json(images))
|
||||
}
|
||||
|
||||
async fn full_res(
|
||||
filename: web::Path<String>,
|
||||
req: HttpRequest,
|
||||
client: web::Data<Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let url = format!(
|
||||
"{}/image/{}",
|
||||
Settings::get().pictrs_url,
|
||||
&filename.into_inner()
|
||||
);
|
||||
image(url, req, client).await
|
||||
}
|
||||
|
||||
async fn thumbnail(
|
||||
parts: web::Path<(u64, String)>,
|
||||
req: HttpRequest,
|
||||
client: web::Data<Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (size, file) = parts.into_inner();
|
||||
|
||||
let url = format!(
|
||||
"{}/image/thumbnail{}/{}",
|
||||
Settings::get().pictrs_url,
|
||||
size,
|
||||
&file
|
||||
);
|
||||
|
||||
image(url, req, client).await
|
||||
}
|
||||
|
||||
async fn image(
|
||||
url: String,
|
||||
req: HttpRequest,
|
||||
client: web::Data<Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let res = client
|
||||
.request_from(url, req.head())
|
||||
.if_some(req.head().peer_addr, |addr, req| {
|
||||
req.header("X-Forwarded-For", addr.to_string())
|
||||
})
|
||||
.no_decompress()
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
if res.status() == StatusCode::NOT_FOUND {
|
||||
return Ok(HttpResponse::NotFound().finish());
|
||||
}
|
||||
|
||||
let mut client_res = HttpResponse::build(res.status());
|
||||
|
||||
for (name, value) in res.headers().iter().filter(|(h, _)| *h != "connection") {
|
||||
client_res.header(name.clone(), value.clone());
|
||||
}
|
||||
|
||||
Ok(client_res.body(BodyStream::new(res)))
|
||||
}
|
||||
|
||||
async fn delete(
|
||||
components: web::Path<(String, String)>,
|
||||
req: HttpRequest,
|
||||
client: web::Data<Client>,
|
||||
) -> Result<HttpResponse, Error> {
|
||||
let (token, file) = components.into_inner();
|
||||
|
||||
let url = format!(
|
||||
"{}/image/delete/{}/{}",
|
||||
Settings::get().pictrs_url,
|
||||
&token,
|
||||
&file
|
||||
);
|
||||
let res = client
|
||||
.request_from(url, req.head())
|
||||
.if_some(req.head().peer_addr, |addr, req| {
|
||||
req.header("X-Forwarded-For", addr.to_string())
|
||||
})
|
||||
.no_decompress()
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
Ok(HttpResponse::build(res.status()).body(BodyStream::new(res)))
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
pub mod api;
|
||||
pub mod federation;
|
||||
pub mod feeds;
|
||||
pub mod images;
|
||||
pub mod index;
|
||||
pub mod nodeinfo;
|
||||
pub mod webfinger;
|
||||
|
|
|
@ -1 +1 @@
|
|||
pub const VERSION: &str = "v0.7.39";
|
||||
pub const VERSION: &str = "v0.7.43";
|
||||
|
|
16
ui/assets/css/main.css
vendored
16
ui/assets/css/main.css
vendored
|
@ -282,3 +282,19 @@ br.big {
|
|||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.banner {
|
||||
object-fit: cover;
|
||||
width: 100%;
|
||||
max-height: 240px;
|
||||
}
|
||||
|
||||
.avatar-overlay {
|
||||
width: 20%;
|
||||
height: 20%;
|
||||
max-width: 120px;
|
||||
max-height: 120px;
|
||||
}
|
||||
|
||||
.avatar-pushup {
|
||||
margin-top: -60px;
|
||||
}
|
||||
|
|
104
ui/assets/css/themes/_variables.darkly.scss
vendored
Normal file
104
ui/assets/css/themes/_variables.darkly.scss
vendored
Normal file
|
@ -0,0 +1,104 @@
|
|||
|
||||
$white: #fff;
|
||||
$gray-100: #f8f9fa;
|
||||
$gray-200: #ebebeb;
|
||||
$gray-300: #dee2e6;
|
||||
$gray-400: #ced4da;
|
||||
$gray-500: #adb5bd;
|
||||
$gray-600: #888;
|
||||
$gray-700: #444;
|
||||
$gray-800: #303030;
|
||||
$gray-900: #222;
|
||||
$black: #000;
|
||||
$blue: #375a7f;
|
||||
$indigo: #6610f2;
|
||||
$purple: #6f42c1;
|
||||
$pink: #e83e8c;
|
||||
$red: #e74c3c;
|
||||
$orange: #fd7e14;
|
||||
$yellow: #f39c12;
|
||||
$green: #00bc8c;
|
||||
$teal: #20c997;
|
||||
$cyan: #3498db;
|
||||
$primary: $blue;
|
||||
$secondary: $gray-700;
|
||||
$success: $green;
|
||||
$info: $cyan;
|
||||
$warning: $yellow;
|
||||
$danger: $red;
|
||||
$dark: $gray-300;
|
||||
$yiq-contrasted-threshold: 175;
|
||||
$body-bg: $gray-900;
|
||||
$body-color: $gray-300;
|
||||
$link-color: $success;
|
||||
$font-family-sans-serif: "Lato", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
|
||||
$font-size-base: 0.9375rem;
|
||||
$h1-font-size: 3rem;
|
||||
$h2-font-size: 2.5rem;
|
||||
$h3-font-size: 2rem;
|
||||
$text-muted: $gray-600;
|
||||
$table-accent-bg: $gray-800;
|
||||
$table-border-color: $gray-700;
|
||||
$input-border-color: $body-bg;
|
||||
$input-group-addon-color: $gray-500;
|
||||
$input-group-addon-bg: $gray-700;
|
||||
$custom-file-color: $gray-500;
|
||||
$custom-file-border-color: $body-bg;
|
||||
$dropdown-bg: $gray-900;
|
||||
$dropdown-border-color: $gray-700;
|
||||
$dropdown-divider-bg: $gray-700;
|
||||
$dropdown-link-color: $white;
|
||||
$dropdown-link-hover-color: $white;
|
||||
$dropdown-link-hover-bg: $primary;
|
||||
$nav-link-padding-x: 2rem;
|
||||
$nav-link-disabled-color: $gray-500;
|
||||
$nav-tabs-border-color: $gray-700;
|
||||
$nav-tabs-link-hover-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
|
||||
$nav-tabs-link-active-color: $white;
|
||||
$nav-tabs-link-active-border-color: $nav-tabs-border-color $nav-tabs-border-color transparent;
|
||||
$navbar-padding-y: 1rem;
|
||||
$navbar-dark-color: rgba($white,.6);
|
||||
$navbar-dark-hover-color: $white;
|
||||
$navbar-light-color: rgba($white,.6);
|
||||
$navbar-light-hover-color: $white;
|
||||
$navbar-light-active-color: $white;
|
||||
$navbar-light-toggler-border-color: rgba($gray-900, .1);
|
||||
$pagination-color: $white;
|
||||
$pagination-bg: $success;
|
||||
$pagination-border-width: 0;
|
||||
$pagination-border-color: transparent;
|
||||
$pagination-hover-color: $white;
|
||||
$pagination-hover-bg: lighten($success, 10%);
|
||||
$pagination-hover-border-color: transparent;
|
||||
$pagination-active-bg: $pagination-hover-bg;
|
||||
$pagination-active-border-color: transparent;
|
||||
$pagination-disabled-color: $white;
|
||||
$pagination-disabled-bg: darken($success, 15%);
|
||||
$pagination-disabled-border-color: transparent;
|
||||
$jumbotron-bg: $gray-800;
|
||||
$card-cap-bg: $gray-700;
|
||||
$card-bg: $gray-800;
|
||||
$popover-bg: $gray-800;
|
||||
$popover-header-bg: $gray-700;
|
||||
$toast-background-color: $gray-700;
|
||||
$toast-header-background-color: $gray-800;
|
||||
$modal-content-bg: $gray-800;
|
||||
$modal-content-border-color: $gray-700;
|
||||
$modal-header-border-color: $gray-700;
|
||||
$progress-bg: $gray-700;
|
||||
$list-group-bg: $gray-800;
|
||||
$list-group-border-color: $gray-700;
|
||||
$list-group-hover-bg: $gray-700;
|
||||
$breadcrumb-bg: $gray-700;
|
||||
$close-color: $white;
|
||||
$close-text-shadow: none;
|
||||
$pre-color: inherit;
|
||||
$mark-bg: #333;
|
||||
$custom-select-bg: $secondary;
|
||||
$custom-select-color: $white;
|
||||
$input-bg: $secondary;
|
||||
$input-color: $white;
|
||||
$input-disabled-bg: darken($secondary, 10%);;
|
||||
$light: $gray-800;
|
||||
$navbar-light-brand-color: $navbar-dark-active-color;
|
||||
$navbar-light-brand-hover-color: $navbar-dark-active-color;
|
36
ui/assets/css/themes/darkly.min.css
vendored
36
ui/assets/css/themes/darkly.min.css
vendored
File diff suppressed because one or more lines are too long
2
ui/src/api_tests/shared.ts
vendored
2
ui/src/api_tests/shared.ts
vendored
|
@ -668,7 +668,7 @@ export async function saveUserSettingsBio(
|
|||
let form: UserSettingsForm = {
|
||||
show_nsfw: true,
|
||||
theme: 'darkly',
|
||||
default_sort_type: SortType.Hot,
|
||||
default_sort_type: SortType.Active,
|
||||
default_listing_type: ListingType.All,
|
||||
lang: 'en',
|
||||
show_avatars: true,
|
||||
|
|
2
ui/src/components/admin-settings.tsx
vendored
2
ui/src/components/admin-settings.tsx
vendored
|
@ -125,6 +125,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: admin.name,
|
||||
preferred_username: admin.preferred_username,
|
||||
avatar: admin.avatar,
|
||||
id: admin.id,
|
||||
local: admin.local,
|
||||
|
@ -148,6 +149,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: banned.name,
|
||||
preferred_username: banned.preferred_username,
|
||||
avatar: banned.avatar,
|
||||
id: banned.id,
|
||||
local: banned.local,
|
||||
|
|
30
ui/src/components/banner-icon-header.tsx
vendored
Normal file
30
ui/src/components/banner-icon-header.tsx
vendored
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { Component } from 'inferno';
|
||||
|
||||
interface BannerIconHeaderProps {
|
||||
banner?: string;
|
||||
icon?: string;
|
||||
}
|
||||
|
||||
export class BannerIconHeader extends Component<BannerIconHeaderProps, any> {
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="position-relative mb-2">
|
||||
{this.props.banner && (
|
||||
<img src={this.props.banner} class="banner img-fluid" />
|
||||
)}
|
||||
{this.props.icon && (
|
||||
<img
|
||||
src={this.props.icon}
|
||||
className={`ml-2 mb-0 ${
|
||||
this.props.banner ? 'avatar-pushup' : ''
|
||||
} rounded-circle avatar-overlay`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
2
ui/src/components/comment-node.tsx
vendored
2
ui/src/components/comment-node.tsx
vendored
|
@ -158,6 +158,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: node.comment.creator_name,
|
||||
preferred_username: node.comment.creator_preferred_username,
|
||||
avatar: node.comment.creator_avatar,
|
||||
id: node.comment.creator_id,
|
||||
local: node.comment.creator_local,
|
||||
|
@ -196,6 +197,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
|||
id: node.comment.community_id,
|
||||
local: node.comment.community_local,
|
||||
actor_id: node.comment.community_actor_id,
|
||||
icon: node.comment.community_icon,
|
||||
}}
|
||||
/>
|
||||
<span class="mx-2">•</span>
|
||||
|
|
2
ui/src/components/communities.tsx
vendored
2
ui/src/components/communities.tsx
vendored
|
@ -101,7 +101,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
<thead class="pointer">
|
||||
<tr>
|
||||
<th>{i18n.t('name')}</th>
|
||||
<th class="d-none d-lg-table-cell">{i18n.t('title')}</th>
|
||||
<th>{i18n.t('category')}</th>
|
||||
<th class="text-right">{i18n.t('subscribers')}</th>
|
||||
<th class="text-right d-none d-lg-table-cell">
|
||||
|
@ -119,7 +118,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
|||
<td>
|
||||
<CommunityLink community={community} />
|
||||
</td>
|
||||
<td class="d-none d-lg-table-cell">{community.title}</td>
|
||||
<td>{community.category_name}</td>
|
||||
<td class="text-right">
|
||||
{community.number_of_subscribers}
|
||||
|
|
54
ui/src/components/community-form.tsx
vendored
54
ui/src/components/community-form.tsx
vendored
|
@ -16,6 +16,7 @@ import { i18n } from '../i18next';
|
|||
|
||||
import { Community } from '../interfaces';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { ImageUploadForm } from './image-upload-form';
|
||||
|
||||
interface CommunityFormProps {
|
||||
community?: Community; // If a community is given, that means this is an edit
|
||||
|
@ -44,6 +45,8 @@ export class CommunityForm extends Component<
|
|||
title: null,
|
||||
category_id: null,
|
||||
nsfw: false,
|
||||
icon: null,
|
||||
banner: null,
|
||||
},
|
||||
categories: [],
|
||||
loading: false,
|
||||
|
@ -58,6 +61,12 @@ export class CommunityForm extends Component<
|
|||
this
|
||||
);
|
||||
|
||||
this.handleIconUpload = this.handleIconUpload.bind(this);
|
||||
this.handleIconRemove = this.handleIconRemove.bind(this);
|
||||
|
||||
this.handleBannerUpload = this.handleBannerUpload.bind(this);
|
||||
this.handleBannerRemove = this.handleBannerRemove.bind(this);
|
||||
|
||||
if (this.props.community) {
|
||||
this.state.communityForm = {
|
||||
name: this.props.community.name,
|
||||
|
@ -66,6 +75,8 @@ export class CommunityForm extends Component<
|
|||
description: this.props.community.description,
|
||||
edit_id: this.props.community.id,
|
||||
nsfw: this.props.community.nsfw,
|
||||
icon: this.props.community.icon,
|
||||
banner: this.props.community.banner,
|
||||
auth: null,
|
||||
};
|
||||
}
|
||||
|
@ -166,6 +177,25 @@ export class CommunityForm extends Component<
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('icon')}</label>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_icon')}
|
||||
imageSrc={this.state.communityForm.icon}
|
||||
onUpload={this.handleIconUpload}
|
||||
onRemove={this.handleIconRemove}
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('banner')}</label>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_banner')}
|
||||
imageSrc={this.state.communityForm.banner}
|
||||
onUpload={this.handleBannerUpload}
|
||||
onRemove={this.handleBannerRemove}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label" htmlFor={this.id}>
|
||||
{i18n.t('sidebar')}
|
||||
|
@ -286,6 +316,26 @@ export class CommunityForm extends Component<
|
|||
i.props.onCancel();
|
||||
}
|
||||
|
||||
handleIconUpload(url: string) {
|
||||
this.state.communityForm.icon = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleIconRemove() {
|
||||
this.state.communityForm.icon = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerUpload(url: string) {
|
||||
this.state.communityForm.banner = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerRemove() {
|
||||
this.state.communityForm.banner = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
parseMessage(msg: WebSocketJsonResponse) {
|
||||
let res = wsJsonToRes(msg);
|
||||
console.log(msg);
|
||||
|
@ -305,9 +355,7 @@ export class CommunityForm extends Component<
|
|||
let data = res.data as CommunityResponse;
|
||||
this.state.loading = false;
|
||||
this.props.onCreate(data.community);
|
||||
}
|
||||
// TODO is this necessary
|
||||
else if (res.op == UserOperation.EditCommunity) {
|
||||
} else if (res.op == UserOperation.EditCommunity) {
|
||||
let data = res.data as CommunityResponse;
|
||||
this.state.loading = false;
|
||||
this.props.onEdit(data.community);
|
||||
|
|
26
ui/src/components/community-link.tsx
vendored
26
ui/src/components/community-link.tsx
vendored
|
@ -1,11 +1,12 @@
|
|||
import { Component } from 'inferno';
|
||||
import { Link } from 'inferno-router';
|
||||
import { Community } from '../interfaces';
|
||||
import { hostname } from '../utils';
|
||||
import { hostname, pictrsAvatarThumbnail, showAvatars } from '../utils';
|
||||
|
||||
interface CommunityOther {
|
||||
name: string;
|
||||
id?: number; // Necessary if its federated
|
||||
icon?: string;
|
||||
local?: boolean;
|
||||
actor_id?: string;
|
||||
}
|
||||
|
@ -13,6 +14,9 @@ interface CommunityOther {
|
|||
interface CommunityLinkProps {
|
||||
community: Community | CommunityOther;
|
||||
realLink?: boolean;
|
||||
useApubName?: boolean;
|
||||
muted?: boolean;
|
||||
hideAvatar?: boolean;
|
||||
}
|
||||
|
||||
export class CommunityLink extends Component<CommunityLinkProps, any> {
|
||||
|
@ -33,6 +37,24 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
|
|||
? `/community/${community.id}`
|
||||
: community.actor_id;
|
||||
}
|
||||
return <Link to={link}>{name_}</Link>;
|
||||
|
||||
let apubName = `!${name_}`;
|
||||
let displayName = this.props.useApubName ? apubName : name_;
|
||||
return (
|
||||
<Link
|
||||
title={apubName}
|
||||
className={`${this.props.muted ? 'text-muted' : ''}`}
|
||||
to={link}
|
||||
>
|
||||
{!this.props.hideAvatar && community.icon && showAvatars() && (
|
||||
<img
|
||||
style="width: 2rem; height: 2rem;"
|
||||
src={pictrsAvatarThumbnail(community.icon)}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>{displayName}</span>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
44
ui/src/components/community.tsx
vendored
44
ui/src/components/community.tsx
vendored
|
@ -33,6 +33,8 @@ import { CommentNodes } from './comment-nodes';
|
|||
import { SortSelect } from './sort-select';
|
||||
import { DataTypeSelect } from './data-type-select';
|
||||
import { Sidebar } from './sidebar';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { BannerIconHeader } from './banner-icon-header';
|
||||
import {
|
||||
wsJsonToRes,
|
||||
fetchLimit,
|
||||
|
@ -47,6 +49,7 @@ import {
|
|||
editPostFindRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
favIconUrl,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
|
@ -126,6 +129,9 @@ export class Community extends Component<any, State> {
|
|||
enable_downvotes: undefined,
|
||||
open_registration: undefined,
|
||||
enable_nsfw: undefined,
|
||||
icon: undefined,
|
||||
banner: undefined,
|
||||
creator_preferred_username: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
|
@ -183,10 +189,25 @@ export class Community extends Component<any, State> {
|
|||
}
|
||||
}
|
||||
|
||||
get favIcon(): string {
|
||||
return this.state.community.icon
|
||||
? this.state.community.icon
|
||||
: this.state.site.icon
|
||||
? this.state.site.icon
|
||||
: favIconUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<Helmet title={this.documentTitle} />
|
||||
<Helmet title={this.documentTitle}>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href={this.favIcon}
|
||||
/>
|
||||
</Helmet>
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -196,6 +217,7 @@ export class Community extends Component<any, State> {
|
|||
) : (
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
{this.communityInfo()}
|
||||
{this.selects()}
|
||||
{this.listings()}
|
||||
{this.paginator()}
|
||||
|
@ -235,6 +257,26 @@ export class Community extends Component<any, State> {
|
|||
);
|
||||
}
|
||||
|
||||
communityInfo() {
|
||||
return (
|
||||
<div>
|
||||
<BannerIconHeader
|
||||
banner={this.state.community.banner}
|
||||
icon={this.state.community.icon}
|
||||
/>
|
||||
<h5 class="mb-0">{this.state.community.title}</h5>
|
||||
<CommunityLink
|
||||
community={this.state.community}
|
||||
realLink
|
||||
useApubName
|
||||
muted
|
||||
hideAvatar
|
||||
/>
|
||||
<hr />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
selects() {
|
||||
return (
|
||||
<div class="mb-3">
|
||||
|
|
114
ui/src/components/image-upload-form.tsx
vendored
Normal file
114
ui/src/components/image-upload-form.tsx
vendored
Normal file
|
@ -0,0 +1,114 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { UserService } from '../services';
|
||||
import { toast, randomStr } from '../utils';
|
||||
|
||||
interface ImageUploadFormProps {
|
||||
uploadTitle: string;
|
||||
imageSrc: string;
|
||||
onUpload(url: string): any;
|
||||
onRemove(): any;
|
||||
rounded?: boolean;
|
||||
}
|
||||
|
||||
interface ImageUploadFormState {
|
||||
loading: boolean;
|
||||
}
|
||||
|
||||
export class ImageUploadForm extends Component<
|
||||
ImageUploadFormProps,
|
||||
ImageUploadFormState
|
||||
> {
|
||||
private id = `image-upload-form-${randomStr()}`;
|
||||
private emptyState: ImageUploadFormState = {
|
||||
loading: false,
|
||||
};
|
||||
|
||||
constructor(props: any, context: any) {
|
||||
super(props, context);
|
||||
this.state = this.emptyState;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form class="d-inline">
|
||||
<label
|
||||
htmlFor={this.id}
|
||||
class="pointer ml-4 text-muted small font-weight-bold"
|
||||
>
|
||||
{!this.props.imageSrc ? (
|
||||
<span class="btn btn-secondary">{this.props.uploadTitle}</span>
|
||||
) : (
|
||||
<span class="d-inline-block position-relative">
|
||||
<img
|
||||
src={this.props.imageSrc}
|
||||
height={this.props.rounded ? 60 : ''}
|
||||
width={this.props.rounded ? 60 : ''}
|
||||
className={`img-fluid ${
|
||||
this.props.rounded ? 'rounded-circle' : ''
|
||||
}`}
|
||||
/>
|
||||
<a onClick={linkEvent(this, this.handleRemoveImage)}>
|
||||
<svg class="icon mini-overlay">
|
||||
<use xlinkHref="#icon-x"></use>
|
||||
</svg>
|
||||
</a>
|
||||
</span>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id={this.id}
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name={this.id}
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
||||
handleImageUpload(i: ImageUploadForm, event: any) {
|
||||
event.preventDefault();
|
||||
let file = event.target.files[0];
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
fetch(imageUploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
i.state.loading = false;
|
||||
i.setState(i.state);
|
||||
i.props.onUpload(url);
|
||||
} else {
|
||||
i.state.loading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.loading = false;
|
||||
i.setState(i.state);
|
||||
toast(error, 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
handleRemoveImage(i: ImageUploadForm, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.loading = true;
|
||||
i.setState(i.state);
|
||||
i.props.onRemove();
|
||||
}
|
||||
}
|
29
ui/src/components/main.tsx
vendored
29
ui/src/components/main.tsx
vendored
|
@ -36,6 +36,7 @@ import { DataTypeSelect } from './data-type-select';
|
|||
import { SiteForm } from './site-form';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { BannerIconHeader } from './banner-icon-header';
|
||||
import {
|
||||
wsJsonToRes,
|
||||
repoUrl,
|
||||
|
@ -53,6 +54,7 @@ import {
|
|||
editPostFindRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
favIconUrl,
|
||||
} from '../utils';
|
||||
import { i18n } from '../i18next';
|
||||
import { T } from 'inferno-i18next';
|
||||
|
@ -104,6 +106,9 @@ export class Main extends Component<any, MainState> {
|
|||
enable_downvotes: null,
|
||||
open_registration: null,
|
||||
enable_nsfw: null,
|
||||
icon: null,
|
||||
banner: null,
|
||||
creator_preferred_username: null,
|
||||
},
|
||||
admins: [],
|
||||
banned: [],
|
||||
|
@ -186,10 +191,23 @@ export class Main extends Component<any, MainState> {
|
|||
}
|
||||
}
|
||||
|
||||
get favIcon(): string {
|
||||
return this.state.siteRes.site.icon
|
||||
? this.state.siteRes.site.icon
|
||||
: favIconUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<Helmet title={this.documentTitle} />
|
||||
<Helmet title={this.documentTitle}>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href={this.favIcon}
|
||||
/>
|
||||
</Helmet>
|
||||
<div class="row">
|
||||
<main role="main" class="col-12 col-md-8">
|
||||
{this.posts()}
|
||||
|
@ -207,8 +225,11 @@ export class Main extends Component<any, MainState> {
|
|||
<div>
|
||||
<div class="card bg-transparent border-secondary mb-3">
|
||||
<div class="card-header bg-transparent border-secondary">
|
||||
{this.siteName()}
|
||||
{this.adminButtons()}
|
||||
<div class="mb-2">
|
||||
{this.siteName()}
|
||||
{this.adminButtons()}
|
||||
</div>
|
||||
<BannerIconHeader banner={this.state.siteRes.site.banner} />
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{this.trendingCommunities()}
|
||||
|
@ -284,6 +305,7 @@ export class Main extends Component<any, MainState> {
|
|||
id: community.community_id,
|
||||
local: community.community_local,
|
||||
actor_id: community.community_actor_id,
|
||||
icon: community.community_icon,
|
||||
}}
|
||||
/>
|
||||
</li>
|
||||
|
@ -346,6 +368,7 @@ export class Main extends Component<any, MainState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: admin.name,
|
||||
preferred_username: admin.preferred_username,
|
||||
avatar: admin.avatar,
|
||||
local: admin.local,
|
||||
actor_id: admin.actor_id,
|
||||
|
|
121
ui/src/components/navbar.tsx
vendored
121
ui/src/components/navbar.tsx
vendored
|
@ -16,7 +16,6 @@ import {
|
|||
Comment,
|
||||
CommentResponse,
|
||||
PrivateMessage,
|
||||
UserView,
|
||||
PrivateMessageResponse,
|
||||
WebSocketJsonResponse,
|
||||
} from '../interfaces';
|
||||
|
@ -41,12 +40,11 @@ interface NavbarState {
|
|||
mentions: Array<Comment>;
|
||||
messages: Array<PrivateMessage>;
|
||||
unreadCount: number;
|
||||
siteName: string;
|
||||
version: string;
|
||||
admins: Array<UserView>;
|
||||
searchParam: string;
|
||||
toggleSearch: boolean;
|
||||
siteLoading: boolean;
|
||||
siteRes: GetSiteResponse;
|
||||
onSiteBanner?(url: string): any;
|
||||
}
|
||||
|
||||
export class Navbar extends Component<any, NavbarState> {
|
||||
|
@ -61,9 +59,30 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
mentions: [],
|
||||
messages: [],
|
||||
expanded: false,
|
||||
siteName: undefined,
|
||||
version: undefined,
|
||||
admins: [],
|
||||
siteRes: {
|
||||
site: {
|
||||
id: null,
|
||||
name: null,
|
||||
creator_id: null,
|
||||
creator_name: null,
|
||||
published: null,
|
||||
number_of_users: null,
|
||||
number_of_posts: null,
|
||||
number_of_comments: null,
|
||||
number_of_communities: null,
|
||||
enable_downvotes: null,
|
||||
open_registration: null,
|
||||
enable_nsfw: null,
|
||||
icon: null,
|
||||
banner: null,
|
||||
creator_preferred_username: null,
|
||||
},
|
||||
my_user: null,
|
||||
admins: [],
|
||||
banned: [],
|
||||
online: null,
|
||||
version: null,
|
||||
},
|
||||
searchParam: '',
|
||||
toggleSearch: false,
|
||||
siteLoading: true,
|
||||
|
@ -158,12 +177,25 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
|
||||
// TODO class active corresponding to current page
|
||||
navbar() {
|
||||
let user = UserService.Instance.user;
|
||||
return (
|
||||
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
||||
<div class="container">
|
||||
{!this.state.siteLoading ? (
|
||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
||||
{this.state.siteName}
|
||||
<Link
|
||||
title={this.state.siteRes.version}
|
||||
class="d-flex align-items-center navbar-brand mr-md-3"
|
||||
to="/"
|
||||
>
|
||||
{this.state.siteRes.site.icon && showAvatars() && (
|
||||
<img
|
||||
src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
|
||||
height="32"
|
||||
width="32"
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
{this.state.siteRes.site.name}
|
||||
</Link>
|
||||
) : (
|
||||
<div class="navbar-item">
|
||||
|
@ -182,14 +214,14 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<use xlinkHref="#icon-bell"></use>
|
||||
</svg>
|
||||
{this.state.unreadCount > 0 && (
|
||||
<span class="ml-1 badge badge-light">
|
||||
<span class="mx-1 badge badge-light">
|
||||
{this.state.unreadCount}
|
||||
</span>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
<button
|
||||
class="navbar-toggler border-0"
|
||||
class="navbar-toggler border-0 p-1"
|
||||
type="button"
|
||||
aria-label="menu"
|
||||
onClick={linkEvent(this, this.expandNavbar)}
|
||||
|
@ -246,6 +278,21 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
</Link>
|
||||
</li>
|
||||
</ul>
|
||||
<ul class="navbar-nav my-2">
|
||||
{this.canAdmin && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={`/admin`}
|
||||
title={i18n.t('admin_settings')}
|
||||
>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-settings"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{!this.context.router.history.location.pathname.match(
|
||||
/^\/search/
|
||||
) && (
|
||||
|
@ -267,7 +314,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<button
|
||||
name="search-btn"
|
||||
onClick={linkEvent(this, this.handleSearchBtn)}
|
||||
class="btn btn-link"
|
||||
class="px-1 btn btn-link"
|
||||
style="color: var(--gray)"
|
||||
>
|
||||
<svg class="icon">
|
||||
|
@ -276,21 +323,6 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
</button>
|
||||
</form>
|
||||
)}
|
||||
<ul class="navbar-nav my-2">
|
||||
{this.canAdmin && (
|
||||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={`/admin`}
|
||||
title={i18n.t('admin_settings')}
|
||||
>
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-settings"></use>
|
||||
</svg>
|
||||
</Link>
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
{this.state.isLoggedIn ? (
|
||||
<>
|
||||
<ul class="navbar-nav my-2">
|
||||
|
@ -315,22 +347,21 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
<li className="nav-item">
|
||||
<Link
|
||||
class="nav-link"
|
||||
to={`/u/${UserService.Instance.user.name}`}
|
||||
to={`/u/${user.name}`}
|
||||
title={i18n.t('settings')}
|
||||
>
|
||||
<span>
|
||||
{UserService.Instance.user.avatar &&
|
||||
showAvatars() && (
|
||||
<img
|
||||
src={pictrsAvatarThumbnail(
|
||||
UserService.Instance.user.avatar
|
||||
)}
|
||||
height="32"
|
||||
width="32"
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
{UserService.Instance.user.name}
|
||||
{user.avatar && showAvatars() && (
|
||||
<img
|
||||
src={pictrsAvatarThumbnail(user.avatar)}
|
||||
height="32"
|
||||
width="32"
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
{user.preferred_username
|
||||
? user.preferred_username
|
||||
: user.name}
|
||||
</span>
|
||||
</Link>
|
||||
</li>
|
||||
|
@ -422,11 +453,7 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
} else if (res.op == UserOperation.GetSite) {
|
||||
let data = res.data as GetSiteResponse;
|
||||
|
||||
if (data.site && !this.state.siteName) {
|
||||
this.state.siteName = data.site.name;
|
||||
this.state.version = data.version;
|
||||
this.state.admins = data.admins;
|
||||
}
|
||||
this.state.siteRes = data;
|
||||
|
||||
// The login
|
||||
if (data.my_user) {
|
||||
|
@ -495,7 +522,9 @@ export class Navbar extends Component<any, NavbarState> {
|
|||
get canAdmin(): boolean {
|
||||
return (
|
||||
UserService.Instance.user &&
|
||||
this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
|
||||
this.state.siteRes.admins
|
||||
.map(a => a.id)
|
||||
.includes(UserService.Instance.user.id)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
1384
ui/src/components/post-listing.tsx
vendored
1384
ui/src/components/post-listing.tsx
vendored
File diff suppressed because it is too large
Load diff
15
ui/src/components/post.tsx
vendored
15
ui/src/components/post.tsx
vendored
|
@ -39,6 +39,7 @@ import {
|
|||
createPostLikeRes,
|
||||
commentsToFlatNodes,
|
||||
setupTippy,
|
||||
favIconUrl,
|
||||
} from '../utils';
|
||||
import { PostListing } from './post-listing';
|
||||
import { Sidebar } from './sidebar';
|
||||
|
@ -189,10 +190,21 @@ export class Post extends Component<any, PostState> {
|
|||
}
|
||||
}
|
||||
|
||||
get favIcon(): string {
|
||||
return this.state.post ? this.state.post.community_icon : favIconUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<Helmet title={this.documentTitle} />
|
||||
<Helmet title={this.documentTitle}>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href={this.favIcon}
|
||||
/>
|
||||
</Helmet>
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -332,6 +344,7 @@ export class Post extends Component<any, PostState> {
|
|||
admins={this.state.siteRes.admins}
|
||||
online={this.state.online}
|
||||
enableNsfw={this.state.siteRes.site.enable_nsfw}
|
||||
showIcon
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
|
2
ui/src/components/private-message-form.tsx
vendored
2
ui/src/components/private-message-form.tsx
vendored
|
@ -128,6 +128,8 @@ export class PrivateMessageForm extends Component<
|
|||
<UserListing
|
||||
user={{
|
||||
name: this.state.recipient.name,
|
||||
preferred_username: this.state.recipient
|
||||
.preferred_username,
|
||||
avatar: this.state.recipient.avatar,
|
||||
id: this.state.recipient.id,
|
||||
local: this.state.recipient.local,
|
||||
|
|
49
ui/src/components/private-message.tsx
vendored
49
ui/src/components/private-message.tsx
vendored
|
@ -9,6 +9,7 @@ import { WebSocketService, UserService } from '../services';
|
|||
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
||||
import { MomentTime } from './moment-time';
|
||||
import { PrivateMessageForm } from './private-message-form';
|
||||
import { UserListing, UserOther } from './user-listing';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface PrivateMessageState {
|
||||
|
@ -53,6 +54,26 @@ export class PrivateMessage extends Component<
|
|||
|
||||
render() {
|
||||
let message = this.props.privateMessage;
|
||||
let userOther: UserOther = this.mine
|
||||
? {
|
||||
name: message.recipient_name,
|
||||
preferred_username: message.recipient_preferred_username,
|
||||
id: message.id,
|
||||
avatar: message.recipient_avatar,
|
||||
local: message.recipient_local,
|
||||
actor_id: message.recipient_actor_id,
|
||||
published: message.published,
|
||||
}
|
||||
: {
|
||||
name: message.creator_name,
|
||||
preferred_username: message.creator_preferred_username,
|
||||
id: message.id,
|
||||
avatar: message.creator_avatar,
|
||||
local: message.creator_local,
|
||||
actor_id: message.creator_actor_id,
|
||||
published: message.published,
|
||||
};
|
||||
|
||||
return (
|
||||
<div class="border-top border-light">
|
||||
<div>
|
||||
|
@ -62,33 +83,7 @@ export class PrivateMessage extends Component<
|
|||
{this.mine ? i18n.t('to') : i18n.t('from')}
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<Link
|
||||
className="text-body font-weight-bold"
|
||||
to={
|
||||
this.mine
|
||||
? `/u/${message.recipient_name}`
|
||||
: `/u/${message.creator_name}`
|
||||
}
|
||||
>
|
||||
{(this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar) &&
|
||||
showAvatars() && (
|
||||
<img
|
||||
height="32"
|
||||
width="32"
|
||||
src={pictrsAvatarThumbnail(
|
||||
this.mine
|
||||
? message.recipient_avatar
|
||||
: message.creator_avatar
|
||||
)}
|
||||
class="rounded-circle mr-1"
|
||||
/>
|
||||
)}
|
||||
<span>
|
||||
{this.mine ? message.recipient_name : message.creator_name}
|
||||
</span>
|
||||
</Link>
|
||||
<UserListing user={userOther} />
|
||||
</li>
|
||||
<li className="list-inline-item">
|
||||
<span>
|
||||
|
|
2
ui/src/components/search.tsx
vendored
2
ui/src/components/search.tsx
vendored
|
@ -315,6 +315,8 @@ export class Search extends Component<any, SearchState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: (i.data as UserView).name,
|
||||
preferred_username: (i.data as UserView)
|
||||
.preferred_username,
|
||||
avatar: (i.data as UserView).avatar,
|
||||
}}
|
||||
/>
|
||||
|
|
53
ui/src/components/sidebar.tsx
vendored
53
ui/src/components/sidebar.tsx
vendored
|
@ -9,10 +9,11 @@ import {
|
|||
UserView,
|
||||
} from '../interfaces';
|
||||
import { WebSocketService, UserService } from '../services';
|
||||
import { mdToHtml, getUnixTime } from '../utils';
|
||||
import { mdToHtml, getUnixTime, pictrsAvatarThumbnail } from '../utils';
|
||||
import { CommunityForm } from './community-form';
|
||||
import { UserListing } from './user-listing';
|
||||
import { CommunityLink } from './community-link';
|
||||
import { BannerIconHeader } from './banner-icon-header';
|
||||
import { i18n } from '../i18next';
|
||||
|
||||
interface SidebarProps {
|
||||
|
@ -21,6 +22,7 @@ interface SidebarProps {
|
|||
admins: Array<UserView>;
|
||||
online: number;
|
||||
enableNsfw: boolean;
|
||||
showIcon?: boolean;
|
||||
}
|
||||
|
||||
interface SidebarState {
|
||||
|
@ -86,24 +88,36 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
communityTitle() {
|
||||
let community = this.props.community;
|
||||
return (
|
||||
<h5 className="mb-2">
|
||||
<span>{community.title}</span>
|
||||
{community.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('removed')}
|
||||
</small>
|
||||
)}
|
||||
{community.deleted && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('deleted')}
|
||||
</small>
|
||||
)}
|
||||
{community.nsfw && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('nsfw')}
|
||||
</small>
|
||||
)}
|
||||
</h5>
|
||||
<div>
|
||||
<h5 className="mb-0">
|
||||
{this.props.showIcon && (
|
||||
<BannerIconHeader icon={community.icon} banner={community.banner} />
|
||||
)}
|
||||
<span>{community.title}</span>
|
||||
{community.removed && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('removed')}
|
||||
</small>
|
||||
)}
|
||||
{community.deleted && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('deleted')}
|
||||
</small>
|
||||
)}
|
||||
{community.nsfw && (
|
||||
<small className="ml-2 text-muted font-italic">
|
||||
{i18n.t('nsfw')}
|
||||
</small>
|
||||
)}
|
||||
</h5>
|
||||
<CommunityLink
|
||||
community={community}
|
||||
realLink
|
||||
useApubName
|
||||
muted
|
||||
hideAvatar
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -160,6 +174,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
|||
<UserListing
|
||||
user={{
|
||||
name: mod.user_name,
|
||||
preferred_username: mod.user_preferred_username,
|
||||
avatar: mod.avatar,
|
||||
id: mod.user_id,
|
||||
local: mod.user_local,
|
||||
|
|
50
ui/src/components/site-form.tsx
vendored
50
ui/src/components/site-form.tsx
vendored
|
@ -1,6 +1,7 @@
|
|||
import { Component, linkEvent } from 'inferno';
|
||||
import { Prompt } from 'inferno-router';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { ImageUploadForm } from './image-upload-form';
|
||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||
import { WebSocketService } from '../services';
|
||||
import { capitalizeFirstLetter, randomStr } from '../utils';
|
||||
|
@ -24,6 +25,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
open_registration: true,
|
||||
enable_nsfw: true,
|
||||
name: null,
|
||||
icon: null,
|
||||
banner: null,
|
||||
},
|
||||
loading: false,
|
||||
};
|
||||
|
@ -36,6 +39,12 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
this
|
||||
);
|
||||
|
||||
this.handleIconUpload = this.handleIconUpload.bind(this);
|
||||
this.handleIconRemove = this.handleIconRemove.bind(this);
|
||||
|
||||
this.handleBannerUpload = this.handleBannerUpload.bind(this);
|
||||
this.handleBannerRemove = this.handleBannerRemove.bind(this);
|
||||
|
||||
if (this.props.site) {
|
||||
this.state.siteForm = {
|
||||
name: this.props.site.name,
|
||||
|
@ -43,6 +52,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
enable_downvotes: this.props.site.enable_downvotes,
|
||||
open_registration: this.props.site.open_registration,
|
||||
enable_nsfw: this.props.site.enable_nsfw,
|
||||
icon: this.props.site.icon,
|
||||
banner: this.props.site.banner,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -103,6 +114,25 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('icon')}</label>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_icon')}
|
||||
imageSrc={this.state.siteForm.icon}
|
||||
onUpload={this.handleIconUpload}
|
||||
onRemove={this.handleIconRemove}
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('banner')}</label>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_banner')}
|
||||
imageSrc={this.state.siteForm.banner}
|
||||
onUpload={this.handleBannerUpload}
|
||||
onRemove={this.handleBannerRemove}
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-12 col-form-label" htmlFor={this.id}>
|
||||
{i18n.t('sidebar')}
|
||||
|
@ -247,4 +277,24 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
|||
handleCancel(i: SiteForm) {
|
||||
i.props.onCancel();
|
||||
}
|
||||
|
||||
handleIconUpload(url: string) {
|
||||
this.state.siteForm.icon = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleIconRemove() {
|
||||
this.state.siteForm.icon = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerUpload(url: string) {
|
||||
this.state.siteForm.banner = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerRemove() {
|
||||
this.state.siteForm.banner = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
}
|
||||
|
|
5
ui/src/components/sort-select.tsx
vendored
5
ui/src/components/sort-select.tsx
vendored
|
@ -39,7 +39,10 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
|
|||
>
|
||||
<option disabled>{i18n.t('sort_type')}</option>
|
||||
{!this.props.hideHot && (
|
||||
<option value={SortType.Hot}>{i18n.t('hot')}</option>
|
||||
<>
|
||||
<option value={SortType.Active}>{i18n.t('active')}</option>
|
||||
<option value={SortType.Hot}>{i18n.t('hot')}</option>
|
||||
</>
|
||||
)}
|
||||
<option value={SortType.New}>{i18n.t('new')}</option>
|
||||
<option disabled>─────</option>
|
||||
|
|
3
ui/src/components/symbols.tsx
vendored
3
ui/src/components/symbols.tsx
vendored
|
@ -15,6 +15,9 @@ export class Symbols extends Component<any, any> {
|
|||
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||
>
|
||||
<defs>
|
||||
<symbol id="icon-x" viewBox="0 0 24 24">
|
||||
<path d="M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
|
||||
</symbol>
|
||||
<symbol id="icon-refresh-cw" viewBox="0 0 24 24">
|
||||
<path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
|
||||
</symbol>
|
||||
|
|
30
ui/src/components/user-listing.tsx
vendored
30
ui/src/components/user-listing.tsx
vendored
|
@ -9,8 +9,9 @@ import {
|
|||
} from '../utils';
|
||||
import { CakeDay } from './cake-day';
|
||||
|
||||
interface UserOther {
|
||||
export interface UserOther {
|
||||
name: string;
|
||||
preferred_username?: string;
|
||||
id?: number; // Necessary if its federated
|
||||
avatar?: string;
|
||||
local?: boolean;
|
||||
|
@ -21,6 +22,9 @@ interface UserOther {
|
|||
interface UserListingProps {
|
||||
user: UserView | UserOther;
|
||||
realLink?: boolean;
|
||||
useApubName?: boolean;
|
||||
muted?: boolean;
|
||||
hideAvatar?: boolean;
|
||||
}
|
||||
|
||||
export class UserListing extends Component<UserListingProps, any> {
|
||||
|
@ -31,30 +35,40 @@ export class UserListing extends Component<UserListingProps, any> {
|
|||
render() {
|
||||
let user = this.props.user;
|
||||
let local = user.local == null ? true : user.local;
|
||||
let name_: string, link: string;
|
||||
let apubName: string, link: string;
|
||||
|
||||
if (local) {
|
||||
name_ = user.name;
|
||||
apubName = `@${user.name}`;
|
||||
link = `/u/${user.name}`;
|
||||
} else {
|
||||
name_ = `${user.name}@${hostname(user.actor_id)}`;
|
||||
apubName = `@${user.name}@${hostname(user.actor_id)}`;
|
||||
link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
|
||||
}
|
||||
|
||||
let displayName = this.props.useApubName
|
||||
? apubName
|
||||
: user.preferred_username
|
||||
? user.preferred_username
|
||||
: apubName;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Link className="text-info" to={link}>
|
||||
{user.avatar && showAvatars() && (
|
||||
<Link
|
||||
title={apubName}
|
||||
className={this.props.muted ? 'text-muted' : 'text-info'}
|
||||
to={link}
|
||||
>
|
||||
{!this.props.hideAvatar && user.avatar && showAvatars() && (
|
||||
<img
|
||||
style="width: 2rem; height: 2rem;"
|
||||
src={pictrsAvatarThumbnail(user.avatar)}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>{name_}</span>
|
||||
<span>{displayName}</span>
|
||||
</Link>
|
||||
|
||||
{isCakeDay(user.published) && <CakeDay creatorName={name_} />}
|
||||
{isCakeDay(user.published) && <CakeDay creatorName={apubName} />}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
396
ui/src/components/user.tsx
vendored
396
ui/src/components/user.tsx
vendored
|
@ -27,11 +27,12 @@ import {
|
|||
themes,
|
||||
setTheme,
|
||||
languages,
|
||||
showAvatars,
|
||||
toast,
|
||||
setupTippy,
|
||||
getLanguage,
|
||||
mdToHtml,
|
||||
elementUrl,
|
||||
favIconUrl,
|
||||
} from '../utils';
|
||||
import { UserListing } from './user-listing';
|
||||
import { SortSelect } from './sort-select';
|
||||
|
@ -41,6 +42,8 @@ import { i18n } from '../i18next';
|
|||
import moment from 'moment';
|
||||
import { UserDetails } from './user-details';
|
||||
import { MarkdownTextArea } from './markdown-textarea';
|
||||
import { ImageUploadForm } from './image-upload-form';
|
||||
import { BannerIconHeader } from './banner-icon-header';
|
||||
|
||||
interface UserState {
|
||||
user: UserView;
|
||||
|
@ -52,7 +55,6 @@ interface UserState {
|
|||
sort: SortType;
|
||||
page: number;
|
||||
loading: boolean;
|
||||
avatarLoading: boolean;
|
||||
userSettingsForm: UserSettingsForm;
|
||||
userSettingsLoading: boolean;
|
||||
deleteAccountLoading: boolean;
|
||||
|
@ -98,7 +100,6 @@ export class User extends Component<any, UserState> {
|
|||
follows: [],
|
||||
moderates: [],
|
||||
loading: true,
|
||||
avatarLoading: false,
|
||||
view: User.getViewFromProps(this.props.match.view),
|
||||
sort: User.getSortTypeFromProps(this.props.match.sort),
|
||||
page: User.getPageFromProps(this.props.match.page),
|
||||
|
@ -112,6 +113,7 @@ export class User extends Component<any, UserState> {
|
|||
send_notifications_to_email: null,
|
||||
auth: null,
|
||||
bio: null,
|
||||
preferred_username: null,
|
||||
},
|
||||
userSettingsLoading: null,
|
||||
deleteAccountLoading: null,
|
||||
|
@ -136,6 +138,9 @@ export class User extends Component<any, UserState> {
|
|||
enable_downvotes: undefined,
|
||||
open_registration: undefined,
|
||||
enable_nsfw: undefined,
|
||||
icon: undefined,
|
||||
banner: undefined,
|
||||
creator_preferred_username: undefined,
|
||||
},
|
||||
version: undefined,
|
||||
},
|
||||
|
@ -157,6 +162,12 @@ export class User extends Component<any, UserState> {
|
|||
this
|
||||
);
|
||||
|
||||
this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
|
||||
this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
|
||||
|
||||
this.handleBannerUpload = this.handleBannerUpload.bind(this);
|
||||
this.handleBannerRemove = this.handleBannerRemove.bind(this);
|
||||
|
||||
this.state.user_id = Number(this.props.match.params.id) || null;
|
||||
this.state.username = this.props.match.params.username;
|
||||
|
||||
|
@ -226,23 +237,27 @@ export class User extends Component<any, UserState> {
|
|||
}
|
||||
}
|
||||
|
||||
get favIcon(): string {
|
||||
return this.state.user.avatar
|
||||
? this.state.user.avatar
|
||||
: this.state.siteRes.site.icon
|
||||
? this.state.siteRes.site.icon
|
||||
: favIconUrl;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div class="container">
|
||||
<Helmet title={this.documentTitle} />
|
||||
<Helmet title={this.documentTitle}>
|
||||
<link
|
||||
id="favicon"
|
||||
rel="icon"
|
||||
type="image/x-icon"
|
||||
href={this.favIcon}
|
||||
/>
|
||||
</Helmet>
|
||||
<div class="row">
|
||||
<div class="col-12 col-md-8">
|
||||
<h5>
|
||||
{this.state.user.avatar && showAvatars() && (
|
||||
<img
|
||||
height="80"
|
||||
width="80"
|
||||
src={this.state.user.avatar}
|
||||
class="rounded-circle mr-2"
|
||||
/>
|
||||
)}
|
||||
<span>@{this.state.username}</span>
|
||||
</h5>
|
||||
{this.state.loading ? (
|
||||
<h5>
|
||||
<svg class="icon icon-spinner spin">
|
||||
|
@ -250,8 +265,12 @@ export class User extends Component<any, UserState> {
|
|||
</svg>
|
||||
</h5>
|
||||
) : (
|
||||
this.selects()
|
||||
<>
|
||||
{this.userInfo()}
|
||||
<hr />
|
||||
</>
|
||||
)}
|
||||
{!this.state.loading && this.selects()}
|
||||
<UserDetails
|
||||
user_id={this.state.user_id}
|
||||
username={this.state.username}
|
||||
|
@ -268,7 +287,6 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
{!this.state.loading && (
|
||||
<div class="col-12 col-md-4">
|
||||
{this.userInfo()}
|
||||
{this.isCurrentUser && this.userSettings()}
|
||||
{this.moderates()}
|
||||
{this.follows()}
|
||||
|
@ -365,22 +383,66 @@ export class User extends Component<any, UserState> {
|
|||
|
||||
userInfo() {
|
||||
let user = this.state.user;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div class="card bg-transparent border-secondary mb-3">
|
||||
<div class="card-body">
|
||||
<h5>
|
||||
<ul class="list-inline mb-0">
|
||||
<li className="list-inline-item">
|
||||
<UserListing user={user} realLink />
|
||||
</li>
|
||||
{user.banned && (
|
||||
<li className="list-inline-item badge badge-danger">
|
||||
{i18n.t('banned')}
|
||||
</li>
|
||||
<BannerIconHeader
|
||||
banner={this.state.user.banner}
|
||||
icon={this.state.user.avatar}
|
||||
/>
|
||||
<div class="mb-3">
|
||||
<div class="">
|
||||
<div class="mb-0 d-flex flex-wrap">
|
||||
<div>
|
||||
{user.preferred_username && (
|
||||
<h5 class="mb-0">{user.preferred_username}</h5>
|
||||
)}
|
||||
</ul>
|
||||
</h5>
|
||||
<ul class="list-inline mb-2">
|
||||
<li className="list-inline-item">
|
||||
<UserListing
|
||||
user={user}
|
||||
realLink
|
||||
useApubName
|
||||
muted
|
||||
hideAvatar
|
||||
/>
|
||||
</li>
|
||||
{user.banned && (
|
||||
<li className="list-inline-item badge badge-danger">
|
||||
{i18n.t('banned')}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
||||
{this.isCurrentUser ? (
|
||||
<button
|
||||
class="d-flex align-self-start btn btn-secondary ml-2"
|
||||
onClick={linkEvent(this, this.handleLogoutClick)}
|
||||
>
|
||||
{i18n.t('logout')}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
className={`d-flex align-self-start btn btn-secondary ml-2 ${
|
||||
!this.state.user.matrix_user_id && 'invisible'
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
|
||||
>
|
||||
{i18n.t('send_secure_message')}
|
||||
</a>
|
||||
<Link
|
||||
class="d-flex align-self-start btn btn-secondary ml-2"
|
||||
to={`/create_private_message?recipient_id=${this.state.user.id}`}
|
||||
>
|
||||
{i18n.t('send_message')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{user.bio && (
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div
|
||||
|
@ -389,7 +451,22 @@ export class User extends Component<any, UserState> {
|
|||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="d-flex align-items-center mb-2">
|
||||
<div>
|
||||
<ul class="list-inline mb-2">
|
||||
<li className="list-inline-item badge badge-light">
|
||||
{i18n.t('number_of_posts', { count: user.number_of_posts })}
|
||||
</li>
|
||||
<li className="list-inline-item badge badge-light">
|
||||
{i18n.t('number_of_comments', {
|
||||
count: user.number_of_comments,
|
||||
})}
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="text-muted">
|
||||
{i18n.t('joined')} <MomentTime data={user} showAgo />
|
||||
</div>
|
||||
<div className="d-flex align-items-center text-muted mb-2">
|
||||
<svg class="icon">
|
||||
<use xlinkHref="#icon-cake"></use>
|
||||
</svg>
|
||||
|
@ -398,71 +475,6 @@ export class User extends Component<any, UserState> {
|
|||
{moment.utc(user.published).local().format('MMM DD, YYYY')}
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
{i18n.t('joined')} <MomentTime data={user} showAgo />
|
||||
</div>
|
||||
<div class="table-responsive mt-1">
|
||||
<table class="table table-bordered table-sm mt-2 mb-0">
|
||||
{/*
|
||||
<tr>
|
||||
<td class="text-center" colSpan={2}>
|
||||
{i18n.t('number_of_points', {
|
||||
count: user.post_score + user.comment_score,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
*/}
|
||||
<tr>
|
||||
{/*
|
||||
<td>
|
||||
{i18n.t('number_of_points', { count: user.post_score })}
|
||||
</td>
|
||||
*/}
|
||||
<td>
|
||||
{i18n.t('number_of_posts', { count: user.number_of_posts })}
|
||||
</td>
|
||||
{/*
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
{i18n.t('number_of_points', { count: user.comment_score })}
|
||||
</td>
|
||||
*/}
|
||||
<td>
|
||||
{i18n.t('number_of_comments', {
|
||||
count: user.number_of_comments,
|
||||
})}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
{this.isCurrentUser ? (
|
||||
<button
|
||||
class="btn btn-block btn-secondary mt-3"
|
||||
onClick={linkEvent(this, this.handleLogoutClick)}
|
||||
>
|
||||
{i18n.t('logout')}
|
||||
</button>
|
||||
) : (
|
||||
<>
|
||||
<a
|
||||
className={`btn btn-block btn-secondary mt-3 ${
|
||||
!this.state.user.matrix_user_id && 'disabled'
|
||||
}`}
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
|
||||
>
|
||||
{i18n.t('send_secure_message')}
|
||||
</a>
|
||||
<Link
|
||||
class="btn btn-block btn-secondary mt-3"
|
||||
to={`/create_private_message?recipient_id=${this.state.user.id}`}
|
||||
>
|
||||
{i18n.t('send_message')}
|
||||
</Link>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -478,47 +490,23 @@ export class User extends Component<any, UserState> {
|
|||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('avatar')}</label>
|
||||
<form class="d-inline">
|
||||
<label
|
||||
htmlFor="file-upload"
|
||||
class="pointer ml-4 text-muted small font-weight-bold"
|
||||
>
|
||||
{!this.checkSettingsAvatar ? (
|
||||
<span class="btn btn-secondary">
|
||||
{i18n.t('upload_avatar')}
|
||||
</span>
|
||||
) : (
|
||||
<img
|
||||
height="80"
|
||||
width="80"
|
||||
src={this.state.userSettingsForm.avatar}
|
||||
class="rounded-circle"
|
||||
/>
|
||||
)}
|
||||
</label>
|
||||
<input
|
||||
id="file-upload"
|
||||
type="file"
|
||||
accept="image/*,video/*"
|
||||
name="file"
|
||||
class="d-none"
|
||||
disabled={!UserService.Instance.user}
|
||||
onChange={linkEvent(this, this.handleImageUpload)}
|
||||
/>
|
||||
</form>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_avatar')}
|
||||
imageSrc={this.state.userSettingsForm.avatar}
|
||||
onUpload={this.handleAvatarUpload}
|
||||
onRemove={this.handleAvatarRemove}
|
||||
rounded
|
||||
/>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('banner')}</label>
|
||||
<ImageUploadForm
|
||||
uploadTitle={i18n.t('upload_banner')}
|
||||
imageSrc={this.state.userSettingsForm.banner}
|
||||
onUpload={this.handleBannerUpload}
|
||||
onRemove={this.handleBannerRemove}
|
||||
/>
|
||||
</div>
|
||||
{this.checkSettingsAvatar && (
|
||||
<div class="form-group">
|
||||
<button
|
||||
class="btn btn-secondary btn-block"
|
||||
onClick={linkEvent(this, this.removeAvatar)}
|
||||
>
|
||||
{`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
|
||||
'avatar'
|
||||
)}`}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<div class="form-group">
|
||||
<label>{i18n.t('language')}</label>
|
||||
<select
|
||||
|
@ -565,6 +553,38 @@ export class User extends Component<any, UserState> {
|
|||
onChange={this.handleUserSettingsSortTypeChange}
|
||||
/>
|
||||
</form>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
{i18n.t('display_name')}
|
||||
</label>
|
||||
<div class="col-lg-7">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
placeholder={i18n.t('optional')}
|
||||
value={this.state.userSettingsForm.preferred_username}
|
||||
onInput={linkEvent(
|
||||
this,
|
||||
this.handleUserSettingsPreferredUsernameChange
|
||||
)}
|
||||
minLength={3}
|
||||
maxLength={20}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label" htmlFor="user-bio">
|
||||
{i18n.t('bio')}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.userSettingsForm.bio}
|
||||
onContentChange={this.handleUserSettingsBioChange}
|
||||
maxLength={300}
|
||||
hideNavigationWarnings
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label" htmlFor="user-email">
|
||||
{i18n.t('email')}
|
||||
|
@ -584,26 +604,9 @@ export class User extends Component<any, UserState> {
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-3 col-form-label" htmlFor="user-bio">
|
||||
{i18n.t('bio')}
|
||||
</label>
|
||||
<div class="col-lg-9">
|
||||
<MarkdownTextArea
|
||||
initialContent={this.state.userSettingsForm.bio}
|
||||
onContentChange={this.handleUserSettingsBioChange}
|
||||
maxLength={300}
|
||||
hideNavigationWarnings
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group row">
|
||||
<label class="col-lg-5 col-form-label">
|
||||
<a
|
||||
href="https://about.riot.im/"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
>
|
||||
<a href={elementUrl} target="_blank" rel="noopener">
|
||||
{i18n.t('matrix_user_id')}
|
||||
</a>
|
||||
</label>
|
||||
|
@ -932,6 +935,31 @@ export class User extends Component<any, UserState> {
|
|||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleAvatarUpload(url: string) {
|
||||
this.state.userSettingsForm.avatar = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleAvatarRemove() {
|
||||
this.state.userSettingsForm.avatar = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerUpload(url: string) {
|
||||
this.state.userSettingsForm.banner = url;
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleBannerRemove() {
|
||||
this.state.userSettingsForm.banner = '';
|
||||
this.setState(this.state);
|
||||
}
|
||||
|
||||
handleUserSettingsPreferredUsernameChange(i: User, event: any) {
|
||||
i.state.userSettingsForm.preferred_username = event.target.value;
|
||||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleUserSettingsMatrixUserIdChange(i: User, event: any) {
|
||||
i.state.userSettingsForm.matrix_user_id = event.target.value;
|
||||
if (
|
||||
|
@ -967,59 +995,6 @@ export class User extends Component<any, UserState> {
|
|||
i.setState(i.state);
|
||||
}
|
||||
|
||||
handleImageUpload(i: User, event: any) {
|
||||
event.preventDefault();
|
||||
let file = event.target.files[0];
|
||||
const imageUploadUrl = `/pictrs/image`;
|
||||
const formData = new FormData();
|
||||
formData.append('images[]', file);
|
||||
|
||||
i.state.avatarLoading = true;
|
||||
i.setState(i.state);
|
||||
|
||||
fetch(imageUploadUrl, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(res => {
|
||||
console.log('pictrs upload:');
|
||||
console.log(res);
|
||||
if (res.msg == 'ok') {
|
||||
let hash = res.files[0].file;
|
||||
let url = `${window.location.origin}/pictrs/image/${hash}`;
|
||||
i.state.userSettingsForm.avatar = url;
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
} else {
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(JSON.stringify(res), 'danger');
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
i.state.avatarLoading = false;
|
||||
i.setState(i.state);
|
||||
toast(error, 'danger');
|
||||
});
|
||||
}
|
||||
|
||||
removeAvatar(i: User, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.userSettingsLoading = true;
|
||||
i.state.userSettingsForm.avatar = '';
|
||||
i.setState(i.state);
|
||||
|
||||
WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
|
||||
}
|
||||
|
||||
get checkSettingsAvatar(): boolean {
|
||||
return (
|
||||
this.state.userSettingsForm.avatar &&
|
||||
this.state.userSettingsForm.avatar != ''
|
||||
);
|
||||
}
|
||||
|
||||
handleUserSettingsSubmit(i: User, event: any) {
|
||||
event.preventDefault();
|
||||
i.state.userSettingsLoading = true;
|
||||
|
@ -1062,7 +1037,6 @@ export class User extends Component<any, UserState> {
|
|||
}
|
||||
this.setState({
|
||||
deleteAccountLoading: false,
|
||||
avatarLoading: false,
|
||||
userSettingsLoading: false,
|
||||
});
|
||||
return;
|
||||
|
@ -1088,6 +1062,9 @@ export class User extends Component<any, UserState> {
|
|||
UserService.Instance.user.default_listing_type;
|
||||
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
|
||||
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
|
||||
this.state.userSettingsForm.banner = UserService.Instance.user.banner;
|
||||
this.state.userSettingsForm.preferred_username =
|
||||
UserService.Instance.user.preferred_username;
|
||||
this.state.userSettingsForm.email = this.state.user.email;
|
||||
this.state.userSettingsForm.bio = this.state.user.bio;
|
||||
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
|
||||
|
@ -1102,6 +1079,9 @@ export class User extends Component<any, UserState> {
|
|||
const data = res.data as LoginResponse;
|
||||
UserService.Instance.login(data);
|
||||
this.state.user.bio = this.state.userSettingsForm.bio;
|
||||
this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
|
||||
this.state.user.banner = this.state.userSettingsForm.banner;
|
||||
this.state.user.avatar = this.state.userSettingsForm.avatar;
|
||||
this.state.userSettingsLoading = false;
|
||||
this.setState(this.state);
|
||||
|
||||
|
|
26
ui/src/interfaces.ts
vendored
26
ui/src/interfaces.ts
vendored
|
@ -83,6 +83,7 @@ export enum DataType {
|
|||
}
|
||||
|
||||
export enum SortType {
|
||||
Active,
|
||||
Hot,
|
||||
New,
|
||||
TopDay,
|
||||
|
@ -112,6 +113,7 @@ export interface User {
|
|||
preferred_username?: string;
|
||||
email?: string;
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
admin: boolean;
|
||||
banned: boolean;
|
||||
published: string;
|
||||
|
@ -134,7 +136,9 @@ export interface UserView {
|
|||
id: number;
|
||||
actor_id: string;
|
||||
name: string;
|
||||
preferred_username?: string;
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
email?: string;
|
||||
matrix_user_id?: string;
|
||||
bio?: string;
|
||||
|
@ -155,11 +159,13 @@ export interface CommunityUser {
|
|||
user_actor_id: string;
|
||||
user_local: boolean;
|
||||
user_name: string;
|
||||
user_preferred_username?: string;
|
||||
avatar?: string;
|
||||
community_id: number;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
community_icon?: string;
|
||||
published: string;
|
||||
}
|
||||
|
||||
|
@ -169,6 +175,8 @@ export interface Community {
|
|||
local: boolean;
|
||||
name: string;
|
||||
title: string;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
description?: string;
|
||||
category_id: number;
|
||||
creator_id: number;
|
||||
|
@ -181,6 +189,7 @@ export interface Community {
|
|||
creator_local: boolean;
|
||||
last_refreshed_at: string;
|
||||
creator_name: string;
|
||||
creator_preferred_username?: string;
|
||||
creator_avatar?: string;
|
||||
category_name: string;
|
||||
number_of_subscribers: number;
|
||||
|
@ -215,11 +224,13 @@ export interface Post {
|
|||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_preferred_username?: string;
|
||||
creator_published: string;
|
||||
creator_avatar?: string;
|
||||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
community_icon?: string;
|
||||
community_removed: boolean;
|
||||
community_deleted: boolean;
|
||||
community_nsfw: boolean;
|
||||
|
@ -228,6 +239,7 @@ export interface Post {
|
|||
upvotes: number;
|
||||
downvotes: number;
|
||||
hot_rank: number;
|
||||
hot_rank_active: number;
|
||||
newest_activity_time: string;
|
||||
user_id?: number;
|
||||
my_vote?: number;
|
||||
|
@ -255,17 +267,20 @@ export interface Comment {
|
|||
community_actor_id: string;
|
||||
community_local: boolean;
|
||||
community_name: string;
|
||||
community_icon?: string;
|
||||
banned: boolean;
|
||||
banned_from_community: boolean;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
creator_name: string;
|
||||
creator_preferred_username?: string;
|
||||
creator_avatar?: string;
|
||||
creator_published: string;
|
||||
score: number;
|
||||
upvotes: number;
|
||||
downvotes: number;
|
||||
hot_rank: number;
|
||||
hot_rank_active: number;
|
||||
user_id?: number;
|
||||
my_vote?: number;
|
||||
subscribed?: number;
|
||||
|
@ -290,6 +305,7 @@ export interface Site {
|
|||
published: string;
|
||||
updated?: string;
|
||||
creator_name: string;
|
||||
creator_preferred_username?: string;
|
||||
number_of_users: number;
|
||||
number_of_posts: number;
|
||||
number_of_comments: number;
|
||||
|
@ -297,6 +313,8 @@ export interface Site {
|
|||
enable_downvotes: boolean;
|
||||
open_registration: boolean;
|
||||
enable_nsfw: boolean;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
}
|
||||
|
||||
export interface PrivateMessage {
|
||||
|
@ -311,10 +329,12 @@ export interface PrivateMessage {
|
|||
ap_id: string;
|
||||
local: boolean;
|
||||
creator_name: string;
|
||||
creator_preferred_username?: string;
|
||||
creator_avatar?: string;
|
||||
creator_actor_id: string;
|
||||
creator_local: boolean;
|
||||
recipient_name: string;
|
||||
recipient_preferred_username?: string;
|
||||
recipient_avatar?: string;
|
||||
recipient_actor_id: string;
|
||||
recipient_local: boolean;
|
||||
|
@ -596,6 +616,8 @@ export interface UserSettingsForm {
|
|||
default_listing_type: ListingType;
|
||||
lang: string;
|
||||
avatar?: string;
|
||||
banner?: string;
|
||||
preferred_username?: string;
|
||||
email?: string;
|
||||
bio?: string;
|
||||
matrix_user_id?: string;
|
||||
|
@ -612,6 +634,8 @@ export interface CommunityForm {
|
|||
edit_id?: number;
|
||||
title: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
category_id: number;
|
||||
nsfw: boolean;
|
||||
auth?: string;
|
||||
|
@ -814,6 +838,8 @@ export interface CreatePostLikeForm {
|
|||
export interface SiteForm {
|
||||
name: string;
|
||||
description?: string;
|
||||
icon?: string;
|
||||
banner?: string;
|
||||
enable_downvotes: boolean;
|
||||
open_registration: boolean;
|
||||
enable_nsfw: boolean;
|
||||
|
|
39
ui/src/utils.ts
vendored
39
ui/src/utils.ts
vendored
|
@ -58,11 +58,13 @@ import Toastify from 'toastify-js';
|
|||
import tippy from 'tippy.js';
|
||||
import moment from 'moment';
|
||||
|
||||
export const favIconUrl = '/static/assets/favicon.svg';
|
||||
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
|
||||
export const helpGuideUrl = '/docs/about_guide.html';
|
||||
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
|
||||
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
|
||||
export const archiveUrl = 'https://archive.is';
|
||||
export const elementUrl = 'https://element.io/';
|
||||
|
||||
export const postRefetchSeconds: number = 60 * 1000;
|
||||
export const fetchLimit: number = 20;
|
||||
|
@ -273,6 +275,8 @@ export function routeSortTypeToEnum(sort: string): SortType {
|
|||
return SortType.New;
|
||||
} else if (sort == 'hot') {
|
||||
return SortType.Hot;
|
||||
} else if (sort == 'active') {
|
||||
return SortType.Active;
|
||||
} else if (sort == 'topday') {
|
||||
return SortType.TopDay;
|
||||
} else if (sort == 'topweek') {
|
||||
|
@ -754,7 +758,7 @@ export function getSortTypeFromProps(props: any): SortType {
|
|||
? routeSortTypeToEnum(props.match.params.sort)
|
||||
: UserService.Instance.user
|
||||
? UserService.Instance.user.default_sort_type
|
||||
: SortType.Hot;
|
||||
: SortType.Active;
|
||||
}
|
||||
|
||||
export function getPageFromProps(props: any): number {
|
||||
|
@ -905,7 +909,7 @@ function convertCommentSortType(sort: SortType): CommentSortType {
|
|||
return CommentSortType.Top;
|
||||
} else if (sort == SortType.New) {
|
||||
return CommentSortType.New;
|
||||
} else if (sort == SortType.Hot) {
|
||||
} else if (sort == SortType.Hot || sort == SortType.Active) {
|
||||
return CommentSortType.Hot;
|
||||
} else {
|
||||
return CommentSortType.Hot;
|
||||
|
@ -948,6 +952,14 @@ export function postSort(
|
|||
(communityType && +b.stickied - +a.stickied) ||
|
||||
b.hot_rank - a.hot_rank
|
||||
);
|
||||
} else if (sort == SortType.Active) {
|
||||
posts.sort(
|
||||
(a, b) =>
|
||||
+a.removed - +b.removed ||
|
||||
+a.deleted - +b.deleted ||
|
||||
(communityType && +b.stickied - +a.stickied) ||
|
||||
b.hot_rank_active - a.hot_rank_active
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -970,10 +982,12 @@ function randomHsl() {
|
|||
|
||||
export function previewLines(text: string, lines: number = 3): string {
|
||||
// Use lines * 2 because markdown requires 2 lines
|
||||
return text
|
||||
.split('\n')
|
||||
.slice(0, lines * 2)
|
||||
.join('\n');
|
||||
return (
|
||||
text
|
||||
.split('\n')
|
||||
.slice(0, lines * 2)
|
||||
.join('\n') + '...'
|
||||
);
|
||||
}
|
||||
|
||||
export function hostname(url: string): string {
|
||||
|
@ -1008,3 +1022,16 @@ export function validTitle(title?: string): boolean {
|
|||
|
||||
return regex.test(title);
|
||||
}
|
||||
|
||||
export function siteBannerCss(banner: string): string {
|
||||
return ` \
|
||||
background-image: linear-gradient( rgba(0, 0, 0, 0.8), rgba(0, 0, 0, 0.8) ) ,url("${banner}"); \
|
||||
background-attachment: fixed; \
|
||||
background-position: top; \
|
||||
background-repeat: no-repeat; \
|
||||
background-size: 100% cover; \
|
||||
|
||||
width: 100%; \
|
||||
max-height: 100vh; \
|
||||
`;
|
||||
}
|
||||
|
|
7
ui/translations/en.json
vendored
7
ui/translations/en.json
vendored
|
@ -40,6 +40,10 @@
|
|||
"upload_image": "upload image",
|
||||
"avatar": "Avatar",
|
||||
"upload_avatar": "Upload Avatar",
|
||||
"banner": "Banner",
|
||||
"upload_banner": "Upload Banner",
|
||||
"icon": "Icon",
|
||||
"upload_icon": "Upload Icon",
|
||||
"show_avatars": "Show Avatars",
|
||||
"show_context": "Show context",
|
||||
"formatting_help": "formatting help",
|
||||
|
@ -125,6 +129,7 @@
|
|||
"sidebar": "Sidebar",
|
||||
"sort_type": "Sort type",
|
||||
"hot": "Hot",
|
||||
"active": "Active",
|
||||
"new": "New",
|
||||
"old": "Old",
|
||||
"top_day": "Top day",
|
||||
|
@ -231,7 +236,7 @@
|
|||
"landing_0":
|
||||
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
|
||||
"not_logged_in": "Not logged in.",
|
||||
"bio_length_overflow": "User bio cannot exceed 300 characters!",
|
||||
"bio_length_overflow": "User bio cannot exceed 300 characters.",
|
||||
"logged_in": "Logged in.",
|
||||
"must_login": "You must <1>log in or register</1> to comment.",
|
||||
"site_saved": "Site Saved.",
|
||||
|
|
21
ui/translations/it.json
vendored
21
ui/translations/it.json
vendored
|
@ -84,7 +84,7 @@
|
|||
"category": "Categoria",
|
||||
"subscribers": "Iscritti",
|
||||
"both": "Entrambi",
|
||||
"saved": "Salvato",
|
||||
"saved": "Salvati",
|
||||
"unsubscribe": "Disiscriviti",
|
||||
"subscribe": "Iscriviti",
|
||||
"subscribed": "Iscritto",
|
||||
|
@ -205,7 +205,7 @@
|
|||
"old_password": "Vecchia Password",
|
||||
"forgot_password": "password dimenticata",
|
||||
"new_password": "Nuova Password",
|
||||
"private_message_disclaimer": "Attenzione: i messaggi privati su Lemmy non sono sicuri. Crea un account su <1>Riot.im</1> per una messaggistica sicura.",
|
||||
"private_message_disclaimer": "Attenzione: i messaggi privati su Lemmy non sono sicuri. Crea un account su <1>Element.io</1> per una messaggistica sicura.",
|
||||
"language": "Lingua",
|
||||
"enable_downvotes": "Abilita voti negativi",
|
||||
"enable_nsfw": "Abilita NSFW",
|
||||
|
@ -217,7 +217,7 @@
|
|||
"downvotes_disabled": "Voti negativi disabilitati",
|
||||
"post_title_too_long": "Titolo della pubblicazione troppo lungo.",
|
||||
"email_already_exists": "Indirizzo email già presente.",
|
||||
"cross_posted_to": "pubblicato pure su: ",
|
||||
"cross_posted_to": "pubblicato anche su: ",
|
||||
"support_on_open_collective": "Sostieni su OpenCollective",
|
||||
"admin_settings": "Impostazioni per Admin",
|
||||
"site_config": "Configurazione del sito",
|
||||
|
@ -260,7 +260,18 @@
|
|||
"what_is": "Cos'è",
|
||||
"must_login": "Devi <1>effettuare l'accesso o registrarti</1> per commentare.",
|
||||
"no_password_reset": "Non sarai in grado di resettare la tua password senza una email.",
|
||||
"cake_day_title": "Cake day:",
|
||||
"cake_day_title": "Torta-giorno:",
|
||||
"cake_day_info": "Oggi è il cake day di {{ creator_name }}!",
|
||||
"invalid_post_title": "Titolo della pubblicazione non valido"
|
||||
"invalid_post_title": "Titolo della pubblicazione non valido",
|
||||
"bold": "grassetto",
|
||||
"italic": "corsivo",
|
||||
"subscript": "pedice",
|
||||
"superscript": "apice",
|
||||
"header": "intestazione",
|
||||
"strikethrough": "barrato",
|
||||
"quote": "citazione",
|
||||
"spoiler": "spoiler",
|
||||
"list": "lista",
|
||||
"invalid_url": "URL non valido.",
|
||||
"not_a_moderator": "Non moderatore."
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue