mirror of
https://github.com/LemmyNet/lemmy.git
synced 2024-11-16 09:24:00 +00:00
Merge branch 'main' into federation-authorisation
This commit is contained in:
commit
0cc49e6ca9
77 changed files with 3650 additions and 1112 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)
|
# Lemmy v0.7.0 Release (2020-06-23)
|
||||||
|
|
||||||
This release replaces [pictshare](https://github.com/HaschekSolutions/pictshare)
|
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;
|
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/ {
|
location /iframely/ {
|
||||||
proxy_pass http://0.0.0.0:8061/;
|
proxy_pass http://0.0.0.0:8061/;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
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:
|
postgres:
|
||||||
image: postgres:12-alpine
|
image: postgres:12-alpine
|
||||||
ports:
|
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:
|
environment:
|
||||||
- POSTGRES_USER=lemmy
|
- POSTGRES_USER=lemmy
|
||||||
- POSTGRES_PASSWORD=password
|
- 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";
|
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/ {
|
location /iframely/ {
|
||||||
proxy_pass http://iframely:80/;
|
proxy_pass http://iframely:80/;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
@ -69,18 +57,6 @@ http {
|
||||||
proxy_set_header Connection "upgrade";
|
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/ {
|
location /iframely/ {
|
||||||
proxy_pass http://iframely:80/;
|
proxy_pass http://iframely:80/;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
|
@ -112,18 +88,6 @@ http {
|
||||||
proxy_set_header Connection "upgrade";
|
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/ {
|
location /iframely/ {
|
||||||
proxy_pass http://iframely:80/;
|
proxy_pass http://iframely:80/;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
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
|
# for more info about the config, check out the documentation
|
||||||
# https://dev.lemmy.ml/docs/administration_configuration.html
|
# 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")
|
# the domain name of your instance (eg "dev.lemmy.ml")
|
||||||
hostname: "my_domain"
|
hostname: "my_domain"
|
||||||
# address where lemmy should listen for incoming requests
|
# 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
|
restart: always
|
||||||
|
|
||||||
lemmy:
|
lemmy:
|
||||||
image: dessalines/lemmy:v0.7.39
|
image: dessalines/lemmy:v0.7.43
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:8536:8536"
|
- "127.0.0.1:8536:8536"
|
||||||
restart: always
|
restart: always
|
||||||
|
|
4
docker/travis/docker_push.sh
vendored
4
docker/travis/docker_push.sh
vendored
|
@ -1,5 +1,5 @@
|
||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
echo "$DOCKER_PASSWORD" | docker login -u "$DOCKER_USERNAME" --password-stdin
|
||||||
docker tag dessalines/lemmy:travis \
|
docker tag dessalines/lemmy:travis \
|
||||||
dessalines/lemmy:v0.7.39
|
dessalines/lemmy:v0.7.43
|
||||||
docker push dessalines/lemmy:v0.7.39
|
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)
|
Time = time since submission (in hours)
|
||||||
Gravity = Decay gravity, 1.8 is default
|
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.
|
- 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.
|
- 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.
|
- The sign and abs of the score are necessary for dealing with the log of negative scores.
|
||||||
|
|
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:
|
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
|
- `New` - the newest posts/communities
|
||||||
- `TopDay` - the most upvoted posts/communities of the current day.
|
- `TopDay` - the most upvoted posts/communities of the current day.
|
||||||
- `TopWeek` - the most upvoted posts/communities of the current week.
|
- `TopWeek` - the most upvoted posts/communities of the current week.
|
||||||
|
@ -482,7 +483,19 @@ These expire after 10 minutes.
|
||||||
theme: String, // Default 'darkly'
|
theme: String, // Default 'darkly'
|
||||||
default_sort_type: i16, // The Sort types from above, zero indexed as a number
|
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`
|
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: {
|
data: {
|
||||||
name: String,
|
name: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -950,6 +965,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
data: {
|
data: {
|
||||||
name: String,
|
name: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1105,6 +1122,8 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
|
||||||
name: String,
|
name: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
category_id: i32 ,
|
category_id: i32 ,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
|
@ -1215,6 +1234,8 @@ Only mods can edit a community.
|
||||||
edit_id: i32,
|
edit_id: i32,
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
auth: String
|
auth: String
|
||||||
}
|
}
|
||||||
|
|
6
server/config/defaults.hjson
vendored
6
server/config/defaults.hjson
vendored
|
@ -35,6 +35,8 @@
|
||||||
jwt_secret: "changeme"
|
jwt_secret: "changeme"
|
||||||
# The location of the frontend
|
# The location of the frontend
|
||||||
front_end_dir: "../ui/dist"
|
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 limits for various user actions, by user ip
|
||||||
rate_limit: {
|
rate_limit: {
|
||||||
# maximum number of messages created in interval
|
# maximum number of messages created in interval
|
||||||
|
@ -49,6 +51,10 @@
|
||||||
register: 3
|
register: 3
|
||||||
# interval length for registration limit
|
# interval length for registration limit
|
||||||
register_per_second: 3600
|
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
|
# settings related to activitypub federation
|
||||||
federation: {
|
federation: {
|
||||||
|
|
|
@ -107,6 +107,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
|
|
@ -255,6 +255,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -291,6 +292,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
banner: None,
|
||||||
|
icon: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
|
@ -23,17 +23,20 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Timestamp,
|
creator_published -> Timestamp,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
|
@ -60,17 +63,20 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Timestamp,
|
creator_published -> Timestamp,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
|
@ -100,17 +106,20 @@ pub struct CommentView {
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_actor_id: String,
|
pub creator_actor_id: String,
|
||||||
pub creator_local: bool,
|
pub creator_local: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_published: chrono::NaiveDateTime,
|
pub creator_published: chrono::NaiveDateTime,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
|
pub hot_rank_active: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub subscribed: Option<bool>,
|
pub subscribed: Option<bool>,
|
||||||
|
@ -244,6 +253,9 @@ impl<'a> CommentQueryBuilder<'a> {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.order_by(hot_rank.desc())
|
.order_by(hot_rank.desc())
|
||||||
.then_order_by(published.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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -315,17 +327,20 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Varchar>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
creator_published -> Timestamp,
|
creator_published -> Timestamp,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
subscribed -> Nullable<Bool>,
|
subscribed -> Nullable<Bool>,
|
||||||
|
@ -356,17 +371,20 @@ pub struct ReplyView {
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_actor_id: String,
|
pub creator_actor_id: String,
|
||||||
pub creator_local: bool,
|
pub creator_local: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub creator_published: chrono::NaiveDateTime,
|
pub creator_published: chrono::NaiveDateTime,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
|
pub hot_rank_active: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub subscribed: Option<bool>,
|
pub subscribed: Option<bool>,
|
||||||
|
@ -437,7 +455,7 @@ impl<'a> ReplyQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
query = match self.sort {
|
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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -488,6 +506,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -524,6 +543,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -584,6 +605,7 @@ mod tests {
|
||||||
post_name: inserted_post.name.to_owned(),
|
post_name: inserted_post.name.to_owned(),
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name: inserted_community.name.to_owned(),
|
community_name: inserted_community.name.to_owned(),
|
||||||
|
community_icon: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -593,11 +615,13 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_preferred_username: None,
|
||||||
creator_published: inserted_user.published,
|
creator_published: inserted_user.published,
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: 0,
|
hot_rank: 0,
|
||||||
|
hot_rank_active: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: None,
|
user_id: None,
|
||||||
my_vote: None,
|
my_vote: None,
|
||||||
|
@ -619,6 +643,7 @@ mod tests {
|
||||||
post_name: inserted_post.name.to_owned(),
|
post_name: inserted_post.name.to_owned(),
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name: inserted_community.name.to_owned(),
|
community_name: inserted_community.name.to_owned(),
|
||||||
|
community_icon: None,
|
||||||
parent_id: None,
|
parent_id: None,
|
||||||
removed: false,
|
removed: false,
|
||||||
deleted: false,
|
deleted: false,
|
||||||
|
@ -628,11 +653,13 @@ mod tests {
|
||||||
published: inserted_comment.published,
|
published: inserted_comment.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
creator_name: inserted_user.name.to_owned(),
|
creator_name: inserted_user.name.to_owned(),
|
||||||
|
creator_preferred_username: None,
|
||||||
creator_published: inserted_user.published,
|
creator_published: inserted_user.published,
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
score: 1,
|
score: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: 0,
|
hot_rank: 0,
|
||||||
|
hot_rank_active: 0,
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
user_id: Some(inserted_user.id),
|
user_id: Some(inserted_user.id),
|
||||||
my_vote: Some(1),
|
my_vote: Some(1),
|
||||||
|
@ -651,6 +678,7 @@ mod tests {
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
read_comment_views_no_user[0].hot_rank = 0;
|
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)
|
let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
|
||||||
.for_post_id(inserted_post.id)
|
.for_post_id(inserted_post.id)
|
||||||
|
@ -658,6 +686,7 @@ mod tests {
|
||||||
.list()
|
.list()
|
||||||
.unwrap();
|
.unwrap();
|
||||||
read_comment_views_with_user[0].hot_rank = 0;
|
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 like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
|
||||||
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
|
||||||
|
|
|
@ -28,6 +28,8 @@ pub struct Community {
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
|
||||||
|
@ -48,6 +50,8 @@ pub struct CommunityForm {
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||||
|
pub icon: Option<Option<String>>,
|
||||||
|
pub banner: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<CommunityForm> for Community {
|
impl Crud<CommunityForm> for Community {
|
||||||
|
@ -299,6 +303,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -335,6 +340,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -356,6 +363,8 @@ mod tests {
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: inserted_community.published,
|
last_refreshed_at: inserted_community.published,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_follower_form = CommunityFollowerForm {
|
let community_follower_form = CommunityFollowerForm {
|
||||||
|
|
|
@ -8,6 +8,8 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
title -> Varchar,
|
title -> Varchar,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
description -> Nullable<Text>,
|
description -> Nullable<Text>,
|
||||||
category_id -> Int4,
|
category_id -> Int4,
|
||||||
creator_id -> Int4,
|
creator_id -> Int4,
|
||||||
|
@ -22,6 +24,7 @@ table! {
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
category_name -> Varchar,
|
category_name -> Varchar,
|
||||||
number_of_subscribers -> BigInt,
|
number_of_subscribers -> BigInt,
|
||||||
|
@ -38,6 +41,8 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
title -> Varchar,
|
title -> Varchar,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
description -> Nullable<Text>,
|
description -> Nullable<Text>,
|
||||||
category_id -> Int4,
|
category_id -> Int4,
|
||||||
creator_id -> Int4,
|
creator_id -> Int4,
|
||||||
|
@ -52,6 +57,7 @@ table! {
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
category_name -> Varchar,
|
category_name -> Varchar,
|
||||||
number_of_subscribers -> BigInt,
|
number_of_subscribers -> BigInt,
|
||||||
|
@ -72,10 +78,12 @@ table! {
|
||||||
user_actor_id -> Text,
|
user_actor_id -> Text,
|
||||||
user_local -> Bool,
|
user_local -> Bool,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
user_preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -88,10 +96,12 @@ table! {
|
||||||
user_actor_id -> Text,
|
user_actor_id -> Text,
|
||||||
user_local -> Bool,
|
user_local -> Bool,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
user_preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,10 +114,12 @@ table! {
|
||||||
user_actor_id -> Text,
|
user_actor_id -> Text,
|
||||||
user_local -> Bool,
|
user_local -> Bool,
|
||||||
user_name -> Varchar,
|
user_name -> Varchar,
|
||||||
|
user_preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -119,6 +131,8 @@ pub struct CommunityView {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
pub category_id: i32,
|
pub category_id: i32,
|
||||||
pub creator_id: i32,
|
pub creator_id: i32,
|
||||||
|
@ -133,6 +147,7 @@ pub struct CommunityView {
|
||||||
pub creator_actor_id: String,
|
pub creator_actor_id: String,
|
||||||
pub creator_local: bool,
|
pub creator_local: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub category_name: String,
|
pub category_name: String,
|
||||||
pub number_of_subscribers: i64,
|
pub number_of_subscribers: i64,
|
||||||
|
@ -288,10 +303,12 @@ pub struct CommunityModeratorView {
|
||||||
pub user_actor_id: String,
|
pub user_actor_id: String,
|
||||||
pub user_local: bool,
|
pub user_local: bool,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub user_preferred_username: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunityModeratorView {
|
impl CommunityModeratorView {
|
||||||
|
@ -324,10 +341,12 @@ pub struct CommunityFollowerView {
|
||||||
pub user_actor_id: String,
|
pub user_actor_id: String,
|
||||||
pub user_local: bool,
|
pub user_local: bool,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub user_preferred_username: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunityFollowerView {
|
impl CommunityFollowerView {
|
||||||
|
@ -358,10 +377,12 @@ pub struct CommunityUserBanView {
|
||||||
pub user_actor_id: String,
|
pub user_actor_id: String,
|
||||||
pub user_local: bool,
|
pub user_local: bool,
|
||||||
pub user_name: String,
|
pub user_name: String,
|
||||||
|
pub user_preferred_username: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommunityUserBanView {
|
impl CommunityUserBanView {
|
||||||
|
|
|
@ -134,6 +134,7 @@ pub fn get_database_url_from_env() -> Result<String, VarError> {
|
||||||
|
|
||||||
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
|
||||||
pub enum SortType {
|
pub enum SortType {
|
||||||
|
Active,
|
||||||
Hot,
|
Hot,
|
||||||
New,
|
New,
|
||||||
TopDay,
|
TopDay,
|
||||||
|
@ -180,6 +181,20 @@ pub fn is_email_regex(test: &str) -> bool {
|
||||||
EMAIL_REGEX.is_match(test)
|
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! {
|
lazy_static! {
|
||||||
static ref EMAIL_REGEX: Regex =
|
static ref EMAIL_REGEX: Regex =
|
||||||
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
|
||||||
|
|
|
@ -460,6 +460,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -487,6 +488,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -523,6 +525,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
|
@ -95,6 +95,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
|
|
@ -316,6 +316,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -352,6 +353,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
|
@ -28,6 +28,7 @@ table! {
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Timestamp,
|
creator_published -> Timestamp,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
|
@ -35,6 +36,7 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
community_removed -> Bool,
|
community_removed -> Bool,
|
||||||
community_deleted -> Bool,
|
community_deleted -> Bool,
|
||||||
community_nsfw -> Bool,
|
community_nsfw -> Bool,
|
||||||
|
@ -43,6 +45,7 @@ table! {
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
newest_activity_time -> Timestamp,
|
newest_activity_time -> Timestamp,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
@ -76,6 +79,7 @@ table! {
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Timestamp,
|
creator_published -> Timestamp,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
|
@ -83,6 +87,7 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
community_removed -> Bool,
|
community_removed -> Bool,
|
||||||
community_deleted -> Bool,
|
community_deleted -> Bool,
|
||||||
community_nsfw -> Bool,
|
community_nsfw -> Bool,
|
||||||
|
@ -91,6 +96,7 @@ table! {
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
newest_activity_time -> Timestamp,
|
newest_activity_time -> Timestamp,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
|
@ -127,6 +133,7 @@ pub struct PostView {
|
||||||
pub creator_actor_id: String,
|
pub creator_actor_id: String,
|
||||||
pub creator_local: bool,
|
pub creator_local: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_published: chrono::NaiveDateTime,
|
pub creator_published: chrono::NaiveDateTime,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
|
@ -134,6 +141,7 @@ pub struct PostView {
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
pub community_removed: bool,
|
pub community_removed: bool,
|
||||||
pub community_deleted: bool,
|
pub community_deleted: bool,
|
||||||
pub community_nsfw: bool,
|
pub community_nsfw: bool,
|
||||||
|
@ -142,6 +150,7 @@ pub struct PostView {
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
|
pub hot_rank_active: i32,
|
||||||
pub newest_activity_time: chrono::NaiveDateTime,
|
pub newest_activity_time: chrono::NaiveDateTime,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
|
@ -289,6 +298,9 @@ impl<'a> PostQueryBuilder<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
query = match self.sort {
|
query = match self.sort {
|
||||||
|
SortType::Active => query
|
||||||
|
.then_order_by(hot_rank_active.desc())
|
||||||
|
.then_order_by(published.desc()),
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.then_order_by(hot_rank.desc())
|
.then_order_by(hot_rank.desc())
|
||||||
.then_order_by(published.desc()),
|
.then_order_by(published.desc()),
|
||||||
|
@ -405,6 +417,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
|
@ -441,6 +454,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
@ -519,6 +534,7 @@ mod tests {
|
||||||
body: None,
|
body: None,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name.to_owned(),
|
creator_name: user_name.to_owned(),
|
||||||
|
creator_preferred_username: None,
|
||||||
creator_published: inserted_user.published,
|
creator_published: inserted_user.published,
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
|
@ -529,6 +545,7 @@ mod tests {
|
||||||
locked: false,
|
locked: false,
|
||||||
stickied: false,
|
stickied: false,
|
||||||
community_name: community_name.to_owned(),
|
community_name: community_name.to_owned(),
|
||||||
|
community_icon: None,
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
community_deleted: false,
|
community_deleted: false,
|
||||||
community_nsfw: false,
|
community_nsfw: false,
|
||||||
|
@ -537,6 +554,7 @@ mod tests {
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: read_post_listing_no_user.hot_rank,
|
hot_rank: read_post_listing_no_user.hot_rank,
|
||||||
|
hot_rank_active: read_post_listing_no_user.hot_rank_active,
|
||||||
published: inserted_post.published,
|
published: inserted_post.published,
|
||||||
newest_activity_time: inserted_post.published,
|
newest_activity_time: inserted_post.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -569,12 +587,14 @@ mod tests {
|
||||||
stickied: false,
|
stickied: false,
|
||||||
creator_id: inserted_user.id,
|
creator_id: inserted_user.id,
|
||||||
creator_name: user_name,
|
creator_name: user_name,
|
||||||
|
creator_preferred_username: None,
|
||||||
creator_published: inserted_user.published,
|
creator_published: inserted_user.published,
|
||||||
creator_avatar: None,
|
creator_avatar: None,
|
||||||
banned: false,
|
banned: false,
|
||||||
banned_from_community: false,
|
banned_from_community: false,
|
||||||
community_id: inserted_community.id,
|
community_id: inserted_community.id,
|
||||||
community_name,
|
community_name,
|
||||||
|
community_icon: None,
|
||||||
community_removed: false,
|
community_removed: false,
|
||||||
community_deleted: false,
|
community_deleted: false,
|
||||||
community_nsfw: false,
|
community_nsfw: false,
|
||||||
|
@ -583,6 +603,7 @@ mod tests {
|
||||||
upvotes: 1,
|
upvotes: 1,
|
||||||
downvotes: 0,
|
downvotes: 0,
|
||||||
hot_rank: read_post_listing_with_user.hot_rank,
|
hot_rank: read_post_listing_with_user.hot_rank,
|
||||||
|
hot_rank_active: read_post_listing_with_user.hot_rank_active,
|
||||||
published: inserted_post.published,
|
published: inserted_post.published,
|
||||||
newest_activity_time: inserted_post.published,
|
newest_activity_time: inserted_post.published,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
|
|
@ -147,6 +147,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -174,6 +175,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
|
|
@ -16,10 +16,12 @@ table! {
|
||||||
ap_id -> Text,
|
ap_id -> Text,
|
||||||
local -> Bool,
|
local -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
creator_actor_id -> Text,
|
creator_actor_id -> Text,
|
||||||
creator_local -> Bool,
|
creator_local -> Bool,
|
||||||
recipient_name -> Varchar,
|
recipient_name -> Varchar,
|
||||||
|
recipient_preferred_username -> Nullable<Varchar>,
|
||||||
recipient_avatar -> Nullable<Text>,
|
recipient_avatar -> Nullable<Text>,
|
||||||
recipient_actor_id -> Text,
|
recipient_actor_id -> Text,
|
||||||
recipient_local -> Bool,
|
recipient_local -> Bool,
|
||||||
|
@ -42,10 +44,12 @@ pub struct PrivateMessageView {
|
||||||
pub ap_id: String,
|
pub ap_id: String,
|
||||||
pub local: bool,
|
pub local: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub creator_actor_id: String,
|
pub creator_actor_id: String,
|
||||||
pub creator_local: bool,
|
pub creator_local: bool,
|
||||||
pub recipient_name: String,
|
pub recipient_name: String,
|
||||||
|
pub recipient_preferred_username: Option<String>,
|
||||||
pub recipient_avatar: Option<String>,
|
pub recipient_avatar: Option<String>,
|
||||||
pub recipient_actor_id: String,
|
pub recipient_actor_id: String,
|
||||||
pub recipient_local: bool,
|
pub recipient_local: bool,
|
||||||
|
|
|
@ -52,17 +52,20 @@ table! {
|
||||||
community_actor_id -> Nullable<Varchar>,
|
community_actor_id -> Nullable<Varchar>,
|
||||||
community_local -> Nullable<Bool>,
|
community_local -> Nullable<Bool>,
|
||||||
community_name -> Nullable<Varchar>,
|
community_name -> Nullable<Varchar>,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
banned -> Nullable<Bool>,
|
banned -> Nullable<Bool>,
|
||||||
banned_from_community -> Nullable<Bool>,
|
banned_from_community -> Nullable<Bool>,
|
||||||
creator_actor_id -> Nullable<Varchar>,
|
creator_actor_id -> Nullable<Varchar>,
|
||||||
creator_local -> Nullable<Bool>,
|
creator_local -> Nullable<Bool>,
|
||||||
creator_name -> Nullable<Varchar>,
|
creator_name -> Nullable<Varchar>,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Nullable<Timestamp>,
|
creator_published -> Nullable<Timestamp>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> Nullable<Int8>,
|
score -> Nullable<Int8>,
|
||||||
upvotes -> Nullable<Int8>,
|
upvotes -> Nullable<Int8>,
|
||||||
downvotes -> Nullable<Int8>,
|
downvotes -> Nullable<Int8>,
|
||||||
hot_rank -> Nullable<Int4>,
|
hot_rank -> Nullable<Int4>,
|
||||||
|
hot_rank_active -> Nullable<Int4>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -104,6 +107,8 @@ table! {
|
||||||
private_key -> Nullable<Text>,
|
private_key -> Nullable<Text>,
|
||||||
public_key -> Nullable<Text>,
|
public_key -> Nullable<Text>,
|
||||||
last_refreshed_at -> Timestamp,
|
last_refreshed_at -> Timestamp,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,6 +117,8 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
name -> Nullable<Varchar>,
|
name -> Nullable<Varchar>,
|
||||||
title -> Nullable<Varchar>,
|
title -> Nullable<Varchar>,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
description -> Nullable<Text>,
|
description -> Nullable<Text>,
|
||||||
category_id -> Nullable<Int4>,
|
category_id -> Nullable<Int4>,
|
||||||
creator_id -> Nullable<Int4>,
|
creator_id -> Nullable<Int4>,
|
||||||
|
@ -126,6 +133,7 @@ table! {
|
||||||
creator_actor_id -> Nullable<Varchar>,
|
creator_actor_id -> Nullable<Varchar>,
|
||||||
creator_local -> Nullable<Bool>,
|
creator_local -> Nullable<Bool>,
|
||||||
creator_name -> Nullable<Varchar>,
|
creator_name -> Nullable<Varchar>,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
category_name -> Nullable<Varchar>,
|
category_name -> Nullable<Varchar>,
|
||||||
number_of_subscribers -> Nullable<Int8>,
|
number_of_subscribers -> Nullable<Int8>,
|
||||||
|
@ -319,6 +327,7 @@ table! {
|
||||||
creator_actor_id -> Nullable<Varchar>,
|
creator_actor_id -> Nullable<Varchar>,
|
||||||
creator_local -> Nullable<Bool>,
|
creator_local -> Nullable<Bool>,
|
||||||
creator_name -> Nullable<Varchar>,
|
creator_name -> Nullable<Varchar>,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_published -> Nullable<Timestamp>,
|
creator_published -> Nullable<Timestamp>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
banned -> Nullable<Bool>,
|
banned -> Nullable<Bool>,
|
||||||
|
@ -326,6 +335,7 @@ table! {
|
||||||
community_actor_id -> Nullable<Varchar>,
|
community_actor_id -> Nullable<Varchar>,
|
||||||
community_local -> Nullable<Bool>,
|
community_local -> Nullable<Bool>,
|
||||||
community_name -> Nullable<Varchar>,
|
community_name -> Nullable<Varchar>,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
community_removed -> Nullable<Bool>,
|
community_removed -> Nullable<Bool>,
|
||||||
community_deleted -> Nullable<Bool>,
|
community_deleted -> Nullable<Bool>,
|
||||||
community_nsfw -> Nullable<Bool>,
|
community_nsfw -> Nullable<Bool>,
|
||||||
|
@ -334,6 +344,7 @@ table! {
|
||||||
upvotes -> Nullable<Int8>,
|
upvotes -> Nullable<Int8>,
|
||||||
downvotes -> Nullable<Int8>,
|
downvotes -> Nullable<Int8>,
|
||||||
hot_rank -> Nullable<Int4>,
|
hot_rank -> Nullable<Int4>,
|
||||||
|
hot_rank_active -> Nullable<Int4>,
|
||||||
newest_activity_time -> Nullable<Timestamp>,
|
newest_activity_time -> Nullable<Timestamp>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -392,6 +403,8 @@ table! {
|
||||||
enable_downvotes -> Bool,
|
enable_downvotes -> Bool,
|
||||||
open_registration -> Bool,
|
open_registration -> Bool,
|
||||||
enable_nsfw -> Bool,
|
enable_nsfw -> Bool,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -421,6 +434,7 @@ table! {
|
||||||
private_key -> Nullable<Text>,
|
private_key -> Nullable<Text>,
|
||||||
public_key -> Nullable<Text>,
|
public_key -> Nullable<Text>,
|
||||||
last_refreshed_at -> Timestamp,
|
last_refreshed_at -> Timestamp,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -437,7 +451,9 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
actor_id -> Nullable<Varchar>,
|
actor_id -> Nullable<Varchar>,
|
||||||
name -> Nullable<Varchar>,
|
name -> Nullable<Varchar>,
|
||||||
|
preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
matrix_user_id -> Nullable<Text>,
|
matrix_user_id -> Nullable<Text>,
|
||||||
bio -> 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 diesel::{dsl::*, result::Error, *};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -14,6 +14,8 @@ pub struct Site {
|
||||||
pub enable_downvotes: bool,
|
pub enable_downvotes: bool,
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: bool,
|
pub enable_nsfw: bool,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
|
||||||
|
@ -26,6 +28,9 @@ pub struct SiteForm {
|
||||||
pub enable_downvotes: bool,
|
pub enable_downvotes: bool,
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: 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 {
|
impl Crud<SiteForm> for Site {
|
||||||
|
@ -51,3 +56,12 @@ impl Crud<SiteForm> for Site {
|
||||||
.get_result::<Self>(conn)
|
.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,
|
enable_downvotes -> Bool,
|
||||||
open_registration -> Bool,
|
open_registration -> Bool,
|
||||||
enable_nsfw -> Bool,
|
enable_nsfw -> Bool,
|
||||||
|
icon -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
number_of_users -> BigInt,
|
number_of_users -> BigInt,
|
||||||
number_of_posts -> BigInt,
|
number_of_posts -> BigInt,
|
||||||
|
@ -35,7 +38,10 @@ pub struct SiteView {
|
||||||
pub enable_downvotes: bool,
|
pub enable_downvotes: bool,
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: bool,
|
pub enable_nsfw: bool,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub number_of_users: i64,
|
pub number_of_users: i64,
|
||||||
pub number_of_posts: i64,
|
pub number_of_posts: i64,
|
||||||
|
|
|
@ -35,6 +35,7 @@ pub struct User_ {
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
pub last_refreshed_at: chrono::NaiveDateTime,
|
pub last_refreshed_at: chrono::NaiveDateTime,
|
||||||
|
pub banner: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Insertable, AsChangeset, Clone, Debug)]
|
#[derive(Insertable, AsChangeset, Clone, Debug)]
|
||||||
|
@ -46,7 +47,7 @@ pub struct UserForm {
|
||||||
pub admin: bool,
|
pub admin: bool,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub email: Option<String>,
|
pub email: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<Option<String>>,
|
||||||
pub updated: Option<chrono::NaiveDateTime>,
|
pub updated: Option<chrono::NaiveDateTime>,
|
||||||
pub show_nsfw: bool,
|
pub show_nsfw: bool,
|
||||||
pub theme: String,
|
pub theme: String,
|
||||||
|
@ -62,6 +63,7 @@ pub struct UserForm {
|
||||||
pub private_key: Option<String>,
|
pub private_key: Option<String>,
|
||||||
pub public_key: Option<String>,
|
pub public_key: Option<String>,
|
||||||
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
|
||||||
|
pub banner: Option<Option<String>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Crud<UserForm> for User_ {
|
impl Crud<UserForm> for User_ {
|
||||||
|
@ -167,6 +169,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -195,6 +198,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
published: inserted_user.published,
|
published: inserted_user.published,
|
||||||
|
|
|
@ -100,6 +100,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -127,6 +128,7 @@ mod tests {
|
||||||
email: None,
|
email: None,
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
admin: false,
|
admin: false,
|
||||||
banned: false,
|
banned: false,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -163,6 +165,8 @@ mod tests {
|
||||||
public_key: None,
|
public_key: None,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
let inserted_community = Community::create(&conn, &new_community).unwrap();
|
||||||
|
|
|
@ -23,14 +23,17 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -60,14 +63,17 @@ table! {
|
||||||
community_actor_id -> Text,
|
community_actor_id -> Text,
|
||||||
community_local -> Bool,
|
community_local -> Bool,
|
||||||
community_name -> Varchar,
|
community_name -> Varchar,
|
||||||
|
community_icon -> Nullable<Text>,
|
||||||
banned -> Bool,
|
banned -> Bool,
|
||||||
banned_from_community -> Bool,
|
banned_from_community -> Bool,
|
||||||
creator_name -> Varchar,
|
creator_name -> Varchar,
|
||||||
|
creator_preferred_username -> Nullable<Varchar>,
|
||||||
creator_avatar -> Nullable<Text>,
|
creator_avatar -> Nullable<Text>,
|
||||||
score -> BigInt,
|
score -> BigInt,
|
||||||
upvotes -> BigInt,
|
upvotes -> BigInt,
|
||||||
downvotes -> BigInt,
|
downvotes -> BigInt,
|
||||||
hot_rank -> Int4,
|
hot_rank -> Int4,
|
||||||
|
hot_rank_active -> Int4,
|
||||||
user_id -> Nullable<Int4>,
|
user_id -> Nullable<Int4>,
|
||||||
my_vote -> Nullable<Int4>,
|
my_vote -> Nullable<Int4>,
|
||||||
saved -> Nullable<Bool>,
|
saved -> Nullable<Bool>,
|
||||||
|
@ -100,14 +106,17 @@ pub struct UserMentionView {
|
||||||
pub community_actor_id: String,
|
pub community_actor_id: String,
|
||||||
pub community_local: bool,
|
pub community_local: bool,
|
||||||
pub community_name: String,
|
pub community_name: String,
|
||||||
|
pub community_icon: Option<String>,
|
||||||
pub banned: bool,
|
pub banned: bool,
|
||||||
pub banned_from_community: bool,
|
pub banned_from_community: bool,
|
||||||
pub creator_name: String,
|
pub creator_name: String,
|
||||||
|
pub creator_preferred_username: Option<String>,
|
||||||
pub creator_avatar: Option<String>,
|
pub creator_avatar: Option<String>,
|
||||||
pub score: i64,
|
pub score: i64,
|
||||||
pub upvotes: i64,
|
pub upvotes: i64,
|
||||||
pub downvotes: i64,
|
pub downvotes: i64,
|
||||||
pub hot_rank: i32,
|
pub hot_rank: i32,
|
||||||
|
pub hot_rank_active: i32,
|
||||||
pub user_id: Option<i32>,
|
pub user_id: Option<i32>,
|
||||||
pub my_vote: Option<i32>,
|
pub my_vote: Option<i32>,
|
||||||
pub saved: Option<bool>,
|
pub saved: Option<bool>,
|
||||||
|
@ -180,6 +189,9 @@ impl<'a> UserMentionQueryBuilder<'a> {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.order_by(hot_rank.desc())
|
.order_by(hot_rank.desc())
|
||||||
.then_order_by(published.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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(score.desc()),
|
SortType::TopAll => query.order_by(score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
|
|
@ -8,7 +8,9 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
actor_id -> Text,
|
actor_id -> Text,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
|
preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
matrix_user_id -> Nullable<Text>,
|
matrix_user_id -> Nullable<Text>,
|
||||||
bio -> Nullable<Text>,
|
bio -> Nullable<Text>,
|
||||||
|
@ -30,7 +32,9 @@ table! {
|
||||||
id -> Int4,
|
id -> Int4,
|
||||||
actor_id -> Text,
|
actor_id -> Text,
|
||||||
name -> Varchar,
|
name -> Varchar,
|
||||||
|
preferred_username -> Nullable<Varchar>,
|
||||||
avatar -> Nullable<Text>,
|
avatar -> Nullable<Text>,
|
||||||
|
banner -> Nullable<Text>,
|
||||||
email -> Nullable<Text>,
|
email -> Nullable<Text>,
|
||||||
matrix_user_id -> Nullable<Text>,
|
matrix_user_id -> Nullable<Text>,
|
||||||
bio -> Nullable<Text>,
|
bio -> Nullable<Text>,
|
||||||
|
@ -55,7 +59,9 @@ pub struct UserView {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub actor_id: String,
|
pub actor_id: String,
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
pub preferred_username: Option<String>,
|
||||||
pub avatar: Option<String>,
|
pub avatar: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
pub email: Option<String>, // TODO this shouldn't be in this view
|
pub email: Option<String>, // TODO this shouldn't be in this view
|
||||||
pub matrix_user_id: Option<String>,
|
pub matrix_user_id: Option<String>,
|
||||||
pub bio: Option<String>,
|
pub bio: Option<String>,
|
||||||
|
@ -126,6 +132,9 @@ impl<'a> UserQueryBuilder<'a> {
|
||||||
SortType::Hot => query
|
SortType::Hot => query
|
||||||
.order_by(comment_score.desc())
|
.order_by(comment_score.desc())
|
||||||
.then_order_by(published.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::New => query.order_by(published.desc()),
|
||||||
SortType::TopAll => query.order_by(comment_score.desc()),
|
SortType::TopAll => query.order_by(comment_score.desc()),
|
||||||
SortType::TopYear => query
|
SortType::TopYear => query
|
||||||
|
@ -164,7 +173,9 @@ impl UserView {
|
||||||
id,
|
id,
|
||||||
actor_id,
|
actor_id,
|
||||||
name,
|
name,
|
||||||
|
preferred_username,
|
||||||
avatar,
|
avatar,
|
||||||
|
banner,
|
||||||
"".into_sql::<Nullable<Text>>(),
|
"".into_sql::<Nullable<Text>>(),
|
||||||
matrix_user_id,
|
matrix_user_id,
|
||||||
bio,
|
bio,
|
||||||
|
@ -192,7 +203,9 @@ impl UserView {
|
||||||
id,
|
id,
|
||||||
actor_id,
|
actor_id,
|
||||||
name,
|
name,
|
||||||
|
preferred_username,
|
||||||
avatar,
|
avatar,
|
||||||
|
banner,
|
||||||
"".into_sql::<Nullable<Text>>(),
|
"".into_sql::<Nullable<Text>>(),
|
||||||
matrix_user_id,
|
matrix_user_id,
|
||||||
bio,
|
bio,
|
||||||
|
|
|
@ -14,6 +14,7 @@ pub struct Settings {
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
pub jwt_secret: String,
|
pub jwt_secret: String,
|
||||||
pub front_end_dir: String,
|
pub front_end_dir: String,
|
||||||
|
pub pictrs_url: String,
|
||||||
pub rate_limit: RateLimitConfig,
|
pub rate_limit: RateLimitConfig,
|
||||||
pub email: Option<EmailConfig>,
|
pub email: Option<EmailConfig>,
|
||||||
pub federation: Federation,
|
pub federation: Federation,
|
||||||
|
@ -36,6 +37,8 @@ pub struct RateLimitConfig {
|
||||||
pub post_per_second: i32,
|
pub post_per_second: i32,
|
||||||
pub register: i32,
|
pub register: i32,
|
||||||
pub register_per_second: i32,
|
pub register_per_second: i32,
|
||||||
|
pub image: i32,
|
||||||
|
pub image_per_second: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Deserialize, Clone)]
|
#[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,
|
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::{
|
use lemmy_utils::{
|
||||||
generate_actor_keypair,
|
generate_actor_keypair,
|
||||||
is_valid_community_name,
|
is_valid_community_name,
|
||||||
|
@ -40,6 +48,8 @@ pub struct CreateCommunity {
|
||||||
name: String,
|
name: String,
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
nsfw: bool,
|
nsfw: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
|
@ -97,6 +107,8 @@ pub struct EditCommunity {
|
||||||
pub edit_id: i32,
|
pub edit_id: i32,
|
||||||
title: String,
|
title: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
category_id: i32,
|
category_id: i32,
|
||||||
nsfw: bool,
|
nsfw: bool,
|
||||||
auth: String,
|
auth: String,
|
||||||
|
@ -251,6 +263,8 @@ impl Perform for Oper<CreateCommunity> {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
title: data.title.to_owned(),
|
title: data.title.to_owned(),
|
||||||
description: data.description.to_owned(),
|
description: data.description.to_owned(),
|
||||||
|
icon: Some(data.icon.to_owned()),
|
||||||
|
banner: Some(data.banner.to_owned()),
|
||||||
category_id: data.category_id,
|
category_id: data.category_id,
|
||||||
creator_id: user.id,
|
creator_id: user.id,
|
||||||
removed: None,
|
removed: None,
|
||||||
|
@ -332,10 +346,15 @@ impl Perform for Oper<EditCommunity> {
|
||||||
let edit_id = data.edit_id;
|
let edit_id = data.edit_id;
|
||||||
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
|
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 {
|
let community_form = CommunityForm {
|
||||||
name: read_community.name,
|
name: read_community.name,
|
||||||
title: data.title.to_owned(),
|
title: data.title.to_owned(),
|
||||||
description: data.description.to_owned(),
|
description: data.description.to_owned(),
|
||||||
|
icon,
|
||||||
|
banner,
|
||||||
category_id: data.category_id.to_owned(),
|
category_id: data.category_id.to_owned(),
|
||||||
creator_id: read_community.creator_id,
|
creator_id: read_community.creator_id,
|
||||||
removed: Some(read_community.removed),
|
removed: Some(read_community.removed),
|
||||||
|
|
|
@ -21,6 +21,7 @@ use lemmy_db::{
|
||||||
category::*,
|
category::*,
|
||||||
comment_view::*,
|
comment_view::*,
|
||||||
community_view::*,
|
community_view::*,
|
||||||
|
diesel_option_overwrite,
|
||||||
moderator::*,
|
moderator::*,
|
||||||
moderator_views::*,
|
moderator_views::*,
|
||||||
naive_now,
|
naive_now,
|
||||||
|
@ -91,6 +92,8 @@ pub struct GetModlogResponse {
|
||||||
pub struct CreateSite {
|
pub struct CreateSite {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub description: Option<String>,
|
pub description: Option<String>,
|
||||||
|
pub icon: Option<String>,
|
||||||
|
pub banner: Option<String>,
|
||||||
pub enable_downvotes: bool,
|
pub enable_downvotes: bool,
|
||||||
pub open_registration: bool,
|
pub open_registration: bool,
|
||||||
pub enable_nsfw: bool,
|
pub enable_nsfw: bool,
|
||||||
|
@ -101,6 +104,8 @@ pub struct CreateSite {
|
||||||
pub struct EditSite {
|
pub struct EditSite {
|
||||||
name: String,
|
name: String,
|
||||||
description: Option<String>,
|
description: Option<String>,
|
||||||
|
icon: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
enable_downvotes: bool,
|
enable_downvotes: bool,
|
||||||
open_registration: bool,
|
open_registration: bool,
|
||||||
enable_nsfw: bool,
|
enable_nsfw: bool,
|
||||||
|
@ -263,6 +268,8 @@ impl Perform for Oper<CreateSite> {
|
||||||
let site_form = SiteForm {
|
let site_form = SiteForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
description: data.description.to_owned(),
|
description: data.description.to_owned(),
|
||||||
|
icon: Some(data.icon.to_owned()),
|
||||||
|
banner: Some(data.banner.to_owned()),
|
||||||
creator_id: user.id,
|
creator_id: user.id,
|
||||||
enable_downvotes: data.enable_downvotes,
|
enable_downvotes: data.enable_downvotes,
|
||||||
open_registration: data.open_registration,
|
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 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 {
|
let site_form = SiteForm {
|
||||||
name: data.name.to_owned(),
|
name: data.name.to_owned(),
|
||||||
description: data.description.to_owned(),
|
description: data.description.to_owned(),
|
||||||
|
icon,
|
||||||
|
banner,
|
||||||
creator_id: found_site.creator_id,
|
creator_id: found_site.creator_id,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
enable_downvotes: data.enable_downvotes,
|
enable_downvotes: data.enable_downvotes,
|
||||||
|
@ -365,6 +377,8 @@ impl Perform for Oper<GetSite> {
|
||||||
let create_site = CreateSite {
|
let create_site = CreateSite {
|
||||||
name: setup.site_name.to_owned(),
|
name: setup.site_name.to_owned(),
|
||||||
description: None,
|
description: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
enable_downvotes: true,
|
enable_downvotes: true,
|
||||||
open_registration: true,
|
open_registration: true,
|
||||||
enable_nsfw: true,
|
enable_nsfw: true,
|
||||||
|
@ -611,18 +625,9 @@ impl Perform for Oper<TransferSite> {
|
||||||
return Err(APIError::err("not_an_admin").into());
|
return Err(APIError::err("not_an_admin").into());
|
||||||
}
|
}
|
||||||
|
|
||||||
let site_form = SiteForm {
|
let new_creator_id = data.user_id;
|
||||||
name: read_site.name,
|
let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
|
||||||
description: read_site.description,
|
if blocking(pool, transfer_site).await?.is_err() {
|
||||||
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() {
|
|
||||||
return Err(APIError::err("couldnt_update_site").into());
|
return Err(APIError::err("couldnt_update_site").into());
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -28,6 +28,7 @@ use lemmy_db::{
|
||||||
comment_view::*,
|
comment_view::*,
|
||||||
community::*,
|
community::*,
|
||||||
community_view::*,
|
community_view::*,
|
||||||
|
diesel_option_overwrite,
|
||||||
moderator::*,
|
moderator::*,
|
||||||
naive_now,
|
naive_now,
|
||||||
password_reset_request::*,
|
password_reset_request::*,
|
||||||
|
@ -103,6 +104,8 @@ pub struct SaveUserSettings {
|
||||||
default_listing_type: i16,
|
default_listing_type: i16,
|
||||||
lang: String,
|
lang: String,
|
||||||
avatar: Option<String>,
|
avatar: Option<String>,
|
||||||
|
banner: Option<String>,
|
||||||
|
preferred_username: Option<String>,
|
||||||
email: Option<String>,
|
email: Option<String>,
|
||||||
bio: Option<String>,
|
bio: Option<String>,
|
||||||
matrix_user_id: Option<String>,
|
matrix_user_id: Option<String>,
|
||||||
|
@ -395,6 +398,7 @@ impl Perform for Oper<Register> {
|
||||||
email: data.email.to_owned(),
|
email: data.email.to_owned(),
|
||||||
matrix_user_id: None,
|
matrix_user_id: None,
|
||||||
avatar: None,
|
avatar: None,
|
||||||
|
banner: None,
|
||||||
password_encrypted: data.password.to_owned(),
|
password_encrypted: data.password.to_owned(),
|
||||||
preferred_username: None,
|
preferred_username: None,
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -402,7 +406,7 @@ impl Perform for Oper<Register> {
|
||||||
banned: false,
|
banned: false,
|
||||||
show_nsfw: data.show_nsfw,
|
show_nsfw: data.show_nsfw,
|
||||||
theme: "darkly".into(),
|
theme: "darkly".into(),
|
||||||
default_sort_type: SortType::Hot as i16,
|
default_sort_type: SortType::Active as i16,
|
||||||
default_listing_type: ListingType::Subscribed as i16,
|
default_listing_type: ListingType::Subscribed as i16,
|
||||||
lang: "browser".into(),
|
lang: "browser".into(),
|
||||||
show_avatars: true,
|
show_avatars: true,
|
||||||
|
@ -454,6 +458,8 @@ impl Perform for Oper<Register> {
|
||||||
public_key: Some(main_community_keypair.public_key),
|
public_key: Some(main_community_keypair.public_key),
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: None,
|
||||||
|
banner: None,
|
||||||
};
|
};
|
||||||
blocking(pool, move |conn| Community::create(conn, &community_form)).await??
|
blocking(pool, move |conn| Community::create(conn, &community_form)).await??
|
||||||
}
|
}
|
||||||
|
@ -569,9 +575,13 @@ impl Perform for Oper<SaveUserSettings> {
|
||||||
None => read_user.bio,
|
None => read_user.bio,
|
||||||
};
|
};
|
||||||
|
|
||||||
let avatar = match &data.avatar {
|
let avatar = diesel_option_overwrite(&data.avatar);
|
||||||
Some(avatar) => Some(avatar.to_owned()),
|
let banner = diesel_option_overwrite(&data.banner);
|
||||||
None => read_user.avatar,
|
|
||||||
|
// 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 {
|
let password_encrypted = match &data.new_password {
|
||||||
|
@ -612,8 +622,9 @@ impl Perform for Oper<SaveUserSettings> {
|
||||||
email,
|
email,
|
||||||
matrix_user_id: data.matrix_user_id.to_owned(),
|
matrix_user_id: data.matrix_user_id.to_owned(),
|
||||||
avatar,
|
avatar,
|
||||||
|
banner,
|
||||||
password_encrypted,
|
password_encrypted,
|
||||||
preferred_username: read_user.preferred_username,
|
preferred_username,
|
||||||
updated: Some(naive_now()),
|
updated: Some(naive_now()),
|
||||||
admin: read_user.admin,
|
admin: read_user.admin,
|
||||||
banned: read_user.banned,
|
banned: read_user.banned,
|
||||||
|
|
|
@ -34,7 +34,7 @@ use activitystreams::{
|
||||||
base::{AnyBase, BaseExt},
|
base::{AnyBase, BaseExt},
|
||||||
collection::{OrderedCollection, UnorderedCollection},
|
collection::{OrderedCollection, UnorderedCollection},
|
||||||
context,
|
context,
|
||||||
object::Tombstone,
|
object::{Image, Tombstone},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
public,
|
public,
|
||||||
};
|
};
|
||||||
|
@ -361,6 +361,32 @@ impl FromApub for CommunityForm {
|
||||||
check_slurs(&title)?;
|
check_slurs(&title)?;
|
||||||
check_slurs_opt(&description)?;
|
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 {
|
Ok(CommunityForm {
|
||||||
name,
|
name,
|
||||||
title,
|
title,
|
||||||
|
@ -377,6 +403,8 @@ impl FromApub for CommunityForm {
|
||||||
private_key: None,
|
private_key: None,
|
||||||
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
|
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
|
icon,
|
||||||
|
banner,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -194,6 +194,8 @@ async fn receive_delete_community(
|
||||||
private_key: community.private_key,
|
private_key: community.private_key,
|
||||||
public_key: community.public_key,
|
public_key: community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
|
icon: Some(community.icon.to_owned()),
|
||||||
|
banner: Some(community.banner.to_owned()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let community_id = community.id;
|
||||||
|
|
|
@ -202,6 +202,8 @@ async fn receive_remove_community(
|
||||||
private_key: community.private_key,
|
private_key: community.private_key,
|
||||||
public_key: community.public_key,
|
public_key: community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
|
icon: Some(community.icon.to_owned()),
|
||||||
|
banner: Some(community.banner.to_owned()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let community_id = community.id;
|
||||||
|
|
|
@ -402,6 +402,8 @@ async fn receive_undo_delete_community(
|
||||||
private_key: community.private_key,
|
private_key: community.private_key,
|
||||||
public_key: community.public_key,
|
public_key: community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
|
icon: Some(community.icon.to_owned()),
|
||||||
|
banner: Some(community.banner.to_owned()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let community_id = community.id;
|
||||||
|
@ -466,6 +468,8 @@ async fn receive_undo_remove_community(
|
||||||
private_key: community.private_key,
|
private_key: community.private_key,
|
||||||
public_key: community.public_key,
|
public_key: community.public_key,
|
||||||
last_refreshed_at: None,
|
last_refreshed_at: None,
|
||||||
|
icon: Some(community.icon.to_owned()),
|
||||||
|
banner: Some(community.banner.to_owned()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let community_id = community.id;
|
let community_id = community.id;
|
||||||
|
|
|
@ -65,6 +65,12 @@ impl ToApub for User_ {
|
||||||
person.set_icon(image.into_any_base()?);
|
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 {
|
if let Some(bio) = &self.bio {
|
||||||
person.set_summary(bio.to_owned());
|
person.set_summary(bio.to_owned());
|
||||||
}
|
}
|
||||||
|
@ -214,13 +220,28 @@ impl FromApub for UserForm {
|
||||||
expected_domain: Option<Url>,
|
expected_domain: Option<Url>,
|
||||||
) -> Result<Self, LemmyError> {
|
) -> Result<Self, LemmyError> {
|
||||||
let avatar = match person.icon() {
|
let avatar = match person.icon() {
|
||||||
Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
|
Some(any_image) => Some(
|
||||||
|
Image::from_any_base(any_image.as_one().unwrap().clone())
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.url()
|
.url()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.as_single_xsd_any_uri()
|
.as_single_xsd_any_uri()
|
||||||
.map(|u| u.to_string()),
|
.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,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -249,6 +270,7 @@ impl FromApub for UserForm {
|
||||||
banned: false,
|
banned: false,
|
||||||
email: None,
|
email: None,
|
||||||
avatar,
|
avatar,
|
||||||
|
banner,
|
||||||
updated: person.updated().map(|u| u.to_owned().naive_local()),
|
updated: person.updated().map(|u| u.to_owned().naive_local()),
|
||||||
show_nsfw: false,
|
show_nsfw: false,
|
||||||
theme: "".to_string(),
|
theme: "".to_string(),
|
||||||
|
|
|
@ -53,7 +53,8 @@ fn user_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
||||||
name: cuser.name.to_owned(),
|
name: cuser.name.to_owned(),
|
||||||
email: cuser.email.to_owned(),
|
email: cuser.email.to_owned(),
|
||||||
matrix_user_id: cuser.matrix_user_id.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(),
|
password_encrypted: cuser.password_encrypted.to_owned(),
|
||||||
preferred_username: cuser.preferred_username.to_owned(),
|
preferred_username: cuser.preferred_username.to_owned(),
|
||||||
updated: None,
|
updated: None,
|
||||||
|
@ -116,6 +117,8 @@ fn community_updates_2020_04_02(conn: &PgConnection) -> Result<(), LemmyError> {
|
||||||
public_key: Some(keypair.public_key),
|
public_key: Some(keypair.public_key),
|
||||||
last_refreshed_at: Some(naive_now()),
|
last_refreshed_at: Some(naive_now()),
|
||||||
published: None,
|
published: None,
|
||||||
|
icon: Some(ccommunity.icon.to_owned()),
|
||||||
|
banner: Some(ccommunity.banner.to_owned()),
|
||||||
};
|
};
|
||||||
|
|
||||||
Community::update(&conn, ccommunity.id, &form)?;
|
Community::update(&conn, ccommunity.id, &form)?;
|
||||||
|
|
|
@ -27,7 +27,7 @@ use lemmy_server::{
|
||||||
blocking,
|
blocking,
|
||||||
code_migrations::run_advanced_migrations,
|
code_migrations::run_advanced_migrations,
|
||||||
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
rate_limit::{rate_limiter::RateLimiter, RateLimit},
|
||||||
routes::{api, federation, feeds, index, nodeinfo, webfinger},
|
routes::*,
|
||||||
websocket::server::*,
|
websocket::server::*,
|
||||||
LemmyError,
|
LemmyError,
|
||||||
};
|
};
|
||||||
|
@ -91,9 +91,10 @@ async fn main() -> Result<(), LemmyError> {
|
||||||
.data(server.clone())
|
.data(server.clone())
|
||||||
.data(Client::default())
|
.data(Client::default())
|
||||||
// The routes
|
// The routes
|
||||||
.configure(move |cfg| api::config(cfg, &rate_limiter))
|
.configure(|cfg| api::config(cfg, &rate_limiter))
|
||||||
.configure(federation::config)
|
.configure(federation::config)
|
||||||
.configure(feeds::config)
|
.configure(feeds::config)
|
||||||
|
.configure(|cfg| images::config(cfg, &rate_limiter))
|
||||||
.configure(index::config)
|
.configure(index::config)
|
||||||
.configure(nodeinfo::config)
|
.configure(nodeinfo::config)
|
||||||
.configure(webfinger::config)
|
.configure(webfinger::config)
|
||||||
|
|
|
@ -45,6 +45,10 @@ impl RateLimit {
|
||||||
self.kind(RateLimitType::Register)
|
self.kind(RateLimitType::Register)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn image(&self) -> RateLimited {
|
||||||
|
self.kind(RateLimitType::Image)
|
||||||
|
}
|
||||||
|
|
||||||
fn kind(&self, type_: RateLimitType) -> RateLimited {
|
fn kind(&self, type_: RateLimitType) -> RateLimited {
|
||||||
RateLimited {
|
RateLimited {
|
||||||
rate_limiter: self.rate_limiter.clone(),
|
rate_limiter: self.rate_limiter.clone(),
|
||||||
|
@ -101,6 +105,15 @@ impl RateLimited {
|
||||||
true,
|
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,
|
Message,
|
||||||
Register,
|
Register,
|
||||||
Post,
|
Post,
|
||||||
|
Image,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Rate limiting based on rate type and IP addr
|
/// 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 api;
|
||||||
pub mod federation;
|
pub mod federation;
|
||||||
pub mod feeds;
|
pub mod feeds;
|
||||||
|
pub mod images;
|
||||||
pub mod index;
|
pub mod index;
|
||||||
pub mod nodeinfo;
|
pub mod nodeinfo;
|
||||||
pub mod webfinger;
|
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;
|
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 = {
|
let form: UserSettingsForm = {
|
||||||
show_nsfw: true,
|
show_nsfw: true,
|
||||||
theme: 'darkly',
|
theme: 'darkly',
|
||||||
default_sort_type: SortType.Hot,
|
default_sort_type: SortType.Active,
|
||||||
default_listing_type: ListingType.All,
|
default_listing_type: ListingType.All,
|
||||||
lang: 'en',
|
lang: 'en',
|
||||||
show_avatars: true,
|
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
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: admin.name,
|
name: admin.name,
|
||||||
|
preferred_username: admin.preferred_username,
|
||||||
avatar: admin.avatar,
|
avatar: admin.avatar,
|
||||||
id: admin.id,
|
id: admin.id,
|
||||||
local: admin.local,
|
local: admin.local,
|
||||||
|
@ -148,6 +149,7 @@ export class AdminSettings extends Component<any, AdminSettingsState> {
|
||||||
<UserListing
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: banned.name,
|
name: banned.name,
|
||||||
|
preferred_username: banned.preferred_username,
|
||||||
avatar: banned.avatar,
|
avatar: banned.avatar,
|
||||||
id: banned.id,
|
id: banned.id,
|
||||||
local: banned.local,
|
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
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: node.comment.creator_name,
|
name: node.comment.creator_name,
|
||||||
|
preferred_username: node.comment.creator_preferred_username,
|
||||||
avatar: node.comment.creator_avatar,
|
avatar: node.comment.creator_avatar,
|
||||||
id: node.comment.creator_id,
|
id: node.comment.creator_id,
|
||||||
local: node.comment.creator_local,
|
local: node.comment.creator_local,
|
||||||
|
@ -196,6 +197,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
|
||||||
id: node.comment.community_id,
|
id: node.comment.community_id,
|
||||||
local: node.comment.community_local,
|
local: node.comment.community_local,
|
||||||
actor_id: node.comment.community_actor_id,
|
actor_id: node.comment.community_actor_id,
|
||||||
|
icon: node.comment.community_icon,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<span class="mx-2">•</span>
|
<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">
|
<thead class="pointer">
|
||||||
<tr>
|
<tr>
|
||||||
<th>{i18n.t('name')}</th>
|
<th>{i18n.t('name')}</th>
|
||||||
<th class="d-none d-lg-table-cell">{i18n.t('title')}</th>
|
|
||||||
<th>{i18n.t('category')}</th>
|
<th>{i18n.t('category')}</th>
|
||||||
<th class="text-right">{i18n.t('subscribers')}</th>
|
<th class="text-right">{i18n.t('subscribers')}</th>
|
||||||
<th class="text-right d-none d-lg-table-cell">
|
<th class="text-right d-none d-lg-table-cell">
|
||||||
|
@ -119,7 +118,6 @@ export class Communities extends Component<any, CommunitiesState> {
|
||||||
<td>
|
<td>
|
||||||
<CommunityLink community={community} />
|
<CommunityLink community={community} />
|
||||||
</td>
|
</td>
|
||||||
<td class="d-none d-lg-table-cell">{community.title}</td>
|
|
||||||
<td>{community.category_name}</td>
|
<td>{community.category_name}</td>
|
||||||
<td class="text-right">
|
<td class="text-right">
|
||||||
{community.number_of_subscribers}
|
{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 { Community } from '../interfaces';
|
||||||
import { MarkdownTextArea } from './markdown-textarea';
|
import { MarkdownTextArea } from './markdown-textarea';
|
||||||
|
import { ImageUploadForm } from './image-upload-form';
|
||||||
|
|
||||||
interface CommunityFormProps {
|
interface CommunityFormProps {
|
||||||
community?: Community; // If a community is given, that means this is an edit
|
community?: Community; // If a community is given, that means this is an edit
|
||||||
|
@ -44,6 +45,8 @@ export class CommunityForm extends Component<
|
||||||
title: null,
|
title: null,
|
||||||
category_id: null,
|
category_id: null,
|
||||||
nsfw: false,
|
nsfw: false,
|
||||||
|
icon: null,
|
||||||
|
banner: null,
|
||||||
},
|
},
|
||||||
categories: [],
|
categories: [],
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -58,6 +61,12 @@ export class CommunityForm extends Component<
|
||||||
this
|
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) {
|
if (this.props.community) {
|
||||||
this.state.communityForm = {
|
this.state.communityForm = {
|
||||||
name: this.props.community.name,
|
name: this.props.community.name,
|
||||||
|
@ -66,6 +75,8 @@ export class CommunityForm extends Component<
|
||||||
description: this.props.community.description,
|
description: this.props.community.description,
|
||||||
edit_id: this.props.community.id,
|
edit_id: this.props.community.id,
|
||||||
nsfw: this.props.community.nsfw,
|
nsfw: this.props.community.nsfw,
|
||||||
|
icon: this.props.community.icon,
|
||||||
|
banner: this.props.community.banner,
|
||||||
auth: null,
|
auth: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -166,6 +177,25 @@ export class CommunityForm extends Component<
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label" htmlFor={this.id}>
|
<label class="col-12 col-form-label" htmlFor={this.id}>
|
||||||
{i18n.t('sidebar')}
|
{i18n.t('sidebar')}
|
||||||
|
@ -286,6 +316,26 @@ export class CommunityForm extends Component<
|
||||||
i.props.onCancel();
|
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) {
|
parseMessage(msg: WebSocketJsonResponse) {
|
||||||
let res = wsJsonToRes(msg);
|
let res = wsJsonToRes(msg);
|
||||||
console.log(msg);
|
console.log(msg);
|
||||||
|
@ -305,9 +355,7 @@ export class CommunityForm extends Component<
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.props.onCreate(data.community);
|
this.props.onCreate(data.community);
|
||||||
}
|
} else if (res.op == UserOperation.EditCommunity) {
|
||||||
// TODO is this necessary
|
|
||||||
else if (res.op == UserOperation.EditCommunity) {
|
|
||||||
let data = res.data as CommunityResponse;
|
let data = res.data as CommunityResponse;
|
||||||
this.state.loading = false;
|
this.state.loading = false;
|
||||||
this.props.onEdit(data.community);
|
this.props.onEdit(data.community);
|
||||||
|
|
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 { Component } from 'inferno';
|
||||||
import { Link } from 'inferno-router';
|
import { Link } from 'inferno-router';
|
||||||
import { Community } from '../interfaces';
|
import { Community } from '../interfaces';
|
||||||
import { hostname } from '../utils';
|
import { hostname, pictrsAvatarThumbnail, showAvatars } from '../utils';
|
||||||
|
|
||||||
interface CommunityOther {
|
interface CommunityOther {
|
||||||
name: string;
|
name: string;
|
||||||
id?: number; // Necessary if its federated
|
id?: number; // Necessary if its federated
|
||||||
|
icon?: string;
|
||||||
local?: boolean;
|
local?: boolean;
|
||||||
actor_id?: string;
|
actor_id?: string;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +14,9 @@ interface CommunityOther {
|
||||||
interface CommunityLinkProps {
|
interface CommunityLinkProps {
|
||||||
community: Community | CommunityOther;
|
community: Community | CommunityOther;
|
||||||
realLink?: boolean;
|
realLink?: boolean;
|
||||||
|
useApubName?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
hideAvatar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class CommunityLink extends Component<CommunityLinkProps, any> {
|
export class CommunityLink extends Component<CommunityLinkProps, any> {
|
||||||
|
@ -33,6 +37,24 @@ export class CommunityLink extends Component<CommunityLinkProps, any> {
|
||||||
? `/community/${community.id}`
|
? `/community/${community.id}`
|
||||||
: community.actor_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 { SortSelect } from './sort-select';
|
||||||
import { DataTypeSelect } from './data-type-select';
|
import { DataTypeSelect } from './data-type-select';
|
||||||
import { Sidebar } from './sidebar';
|
import { Sidebar } from './sidebar';
|
||||||
|
import { CommunityLink } from './community-link';
|
||||||
|
import { BannerIconHeader } from './banner-icon-header';
|
||||||
import {
|
import {
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
fetchLimit,
|
fetchLimit,
|
||||||
|
@ -47,6 +49,7 @@ import {
|
||||||
editPostFindRes,
|
editPostFindRes,
|
||||||
commentsToFlatNodes,
|
commentsToFlatNodes,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
|
favIconUrl,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
|
@ -126,6 +129,9 @@ export class Community extends Component<any, State> {
|
||||||
enable_downvotes: undefined,
|
enable_downvotes: undefined,
|
||||||
open_registration: undefined,
|
open_registration: undefined,
|
||||||
enable_nsfw: 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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<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 ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -196,6 +217,7 @@ export class Community extends Component<any, State> {
|
||||||
) : (
|
) : (
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-12 col-md-8">
|
<div class="col-12 col-md-8">
|
||||||
|
{this.communityInfo()}
|
||||||
{this.selects()}
|
{this.selects()}
|
||||||
{this.listings()}
|
{this.listings()}
|
||||||
{this.paginator()}
|
{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() {
|
selects() {
|
||||||
return (
|
return (
|
||||||
<div class="mb-3">
|
<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();
|
||||||
|
}
|
||||||
|
}
|
25
ui/src/components/main.tsx
vendored
25
ui/src/components/main.tsx
vendored
|
@ -36,6 +36,7 @@ import { DataTypeSelect } from './data-type-select';
|
||||||
import { SiteForm } from './site-form';
|
import { SiteForm } from './site-form';
|
||||||
import { UserListing } from './user-listing';
|
import { UserListing } from './user-listing';
|
||||||
import { CommunityLink } from './community-link';
|
import { CommunityLink } from './community-link';
|
||||||
|
import { BannerIconHeader } from './banner-icon-header';
|
||||||
import {
|
import {
|
||||||
wsJsonToRes,
|
wsJsonToRes,
|
||||||
repoUrl,
|
repoUrl,
|
||||||
|
@ -53,6 +54,7 @@ import {
|
||||||
editPostFindRes,
|
editPostFindRes,
|
||||||
commentsToFlatNodes,
|
commentsToFlatNodes,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
|
favIconUrl,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
import { T } from 'inferno-i18next';
|
import { T } from 'inferno-i18next';
|
||||||
|
@ -104,6 +106,9 @@ export class Main extends Component<any, MainState> {
|
||||||
enable_downvotes: null,
|
enable_downvotes: null,
|
||||||
open_registration: null,
|
open_registration: null,
|
||||||
enable_nsfw: null,
|
enable_nsfw: null,
|
||||||
|
icon: null,
|
||||||
|
banner: null,
|
||||||
|
creator_preferred_username: null,
|
||||||
},
|
},
|
||||||
admins: [],
|
admins: [],
|
||||||
banned: [],
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<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="row">
|
||||||
<main role="main" class="col-12 col-md-8">
|
<main role="main" class="col-12 col-md-8">
|
||||||
{this.posts()}
|
{this.posts()}
|
||||||
|
@ -207,9 +225,12 @@ export class Main extends Component<any, MainState> {
|
||||||
<div>
|
<div>
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
<div class="card bg-transparent border-secondary mb-3">
|
||||||
<div class="card-header bg-transparent border-secondary">
|
<div class="card-header bg-transparent border-secondary">
|
||||||
|
<div class="mb-2">
|
||||||
{this.siteName()}
|
{this.siteName()}
|
||||||
{this.adminButtons()}
|
{this.adminButtons()}
|
||||||
</div>
|
</div>
|
||||||
|
<BannerIconHeader banner={this.state.siteRes.site.banner} />
|
||||||
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
{this.trendingCommunities()}
|
{this.trendingCommunities()}
|
||||||
{this.createCommunityButton()}
|
{this.createCommunityButton()}
|
||||||
|
@ -284,6 +305,7 @@ export class Main extends Component<any, MainState> {
|
||||||
id: community.community_id,
|
id: community.community_id,
|
||||||
local: community.community_local,
|
local: community.community_local,
|
||||||
actor_id: community.community_actor_id,
|
actor_id: community.community_actor_id,
|
||||||
|
icon: community.community_icon,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
|
@ -346,6 +368,7 @@ export class Main extends Component<any, MainState> {
|
||||||
<UserListing
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: admin.name,
|
name: admin.name,
|
||||||
|
preferred_username: admin.preferred_username,
|
||||||
avatar: admin.avatar,
|
avatar: admin.avatar,
|
||||||
local: admin.local,
|
local: admin.local,
|
||||||
actor_id: admin.actor_id,
|
actor_id: admin.actor_id,
|
||||||
|
|
107
ui/src/components/navbar.tsx
vendored
107
ui/src/components/navbar.tsx
vendored
|
@ -16,7 +16,6 @@ import {
|
||||||
Comment,
|
Comment,
|
||||||
CommentResponse,
|
CommentResponse,
|
||||||
PrivateMessage,
|
PrivateMessage,
|
||||||
UserView,
|
|
||||||
PrivateMessageResponse,
|
PrivateMessageResponse,
|
||||||
WebSocketJsonResponse,
|
WebSocketJsonResponse,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
|
@ -41,12 +40,11 @@ interface NavbarState {
|
||||||
mentions: Array<Comment>;
|
mentions: Array<Comment>;
|
||||||
messages: Array<PrivateMessage>;
|
messages: Array<PrivateMessage>;
|
||||||
unreadCount: number;
|
unreadCount: number;
|
||||||
siteName: string;
|
|
||||||
version: string;
|
|
||||||
admins: Array<UserView>;
|
|
||||||
searchParam: string;
|
searchParam: string;
|
||||||
toggleSearch: boolean;
|
toggleSearch: boolean;
|
||||||
siteLoading: boolean;
|
siteLoading: boolean;
|
||||||
|
siteRes: GetSiteResponse;
|
||||||
|
onSiteBanner?(url: string): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navbar extends Component<any, NavbarState> {
|
export class Navbar extends Component<any, NavbarState> {
|
||||||
|
@ -61,9 +59,30 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
mentions: [],
|
mentions: [],
|
||||||
messages: [],
|
messages: [],
|
||||||
expanded: false,
|
expanded: false,
|
||||||
siteName: undefined,
|
siteRes: {
|
||||||
version: undefined,
|
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: [],
|
admins: [],
|
||||||
|
banned: [],
|
||||||
|
online: null,
|
||||||
|
version: null,
|
||||||
|
},
|
||||||
searchParam: '',
|
searchParam: '',
|
||||||
toggleSearch: false,
|
toggleSearch: false,
|
||||||
siteLoading: true,
|
siteLoading: true,
|
||||||
|
@ -158,12 +177,25 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
|
|
||||||
// TODO class active corresponding to current page
|
// TODO class active corresponding to current page
|
||||||
navbar() {
|
navbar() {
|
||||||
|
let user = UserService.Instance.user;
|
||||||
return (
|
return (
|
||||||
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
{!this.state.siteLoading ? (
|
{!this.state.siteLoading ? (
|
||||||
<Link title={this.state.version} class="navbar-brand" to="/">
|
<Link
|
||||||
{this.state.siteName}
|
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>
|
</Link>
|
||||||
) : (
|
) : (
|
||||||
<div class="navbar-item">
|
<div class="navbar-item">
|
||||||
|
@ -182,14 +214,14 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
<use xlinkHref="#icon-bell"></use>
|
<use xlinkHref="#icon-bell"></use>
|
||||||
</svg>
|
</svg>
|
||||||
{this.state.unreadCount > 0 && (
|
{this.state.unreadCount > 0 && (
|
||||||
<span class="ml-1 badge badge-light">
|
<span class="mx-1 badge badge-light">
|
||||||
{this.state.unreadCount}
|
{this.state.unreadCount}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
<button
|
<button
|
||||||
class="navbar-toggler border-0"
|
class="navbar-toggler border-0 p-1"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="menu"
|
aria-label="menu"
|
||||||
onClick={linkEvent(this, this.expandNavbar)}
|
onClick={linkEvent(this, this.expandNavbar)}
|
||||||
|
@ -246,6 +278,21 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</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(
|
{!this.context.router.history.location.pathname.match(
|
||||||
/^\/search/
|
/^\/search/
|
||||||
) && (
|
) && (
|
||||||
|
@ -267,7 +314,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
<button
|
<button
|
||||||
name="search-btn"
|
name="search-btn"
|
||||||
onClick={linkEvent(this, this.handleSearchBtn)}
|
onClick={linkEvent(this, this.handleSearchBtn)}
|
||||||
class="btn btn-link"
|
class="px-1 btn btn-link"
|
||||||
style="color: var(--gray)"
|
style="color: var(--gray)"
|
||||||
>
|
>
|
||||||
<svg class="icon">
|
<svg class="icon">
|
||||||
|
@ -276,21 +323,6 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</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 ? (
|
{this.state.isLoggedIn ? (
|
||||||
<>
|
<>
|
||||||
<ul class="navbar-nav my-2">
|
<ul class="navbar-nav my-2">
|
||||||
|
@ -315,22 +347,21 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
<li className="nav-item">
|
<li className="nav-item">
|
||||||
<Link
|
<Link
|
||||||
class="nav-link"
|
class="nav-link"
|
||||||
to={`/u/${UserService.Instance.user.name}`}
|
to={`/u/${user.name}`}
|
||||||
title={i18n.t('settings')}
|
title={i18n.t('settings')}
|
||||||
>
|
>
|
||||||
<span>
|
<span>
|
||||||
{UserService.Instance.user.avatar &&
|
{user.avatar && showAvatars() && (
|
||||||
showAvatars() && (
|
|
||||||
<img
|
<img
|
||||||
src={pictrsAvatarThumbnail(
|
src={pictrsAvatarThumbnail(user.avatar)}
|
||||||
UserService.Instance.user.avatar
|
|
||||||
)}
|
|
||||||
height="32"
|
height="32"
|
||||||
width="32"
|
width="32"
|
||||||
class="rounded-circle mr-2"
|
class="rounded-circle mr-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{UserService.Instance.user.name}
|
{user.preferred_username
|
||||||
|
? user.preferred_username
|
||||||
|
: user.name}
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
|
@ -422,11 +453,7 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
} else if (res.op == UserOperation.GetSite) {
|
} else if (res.op == UserOperation.GetSite) {
|
||||||
let data = res.data as GetSiteResponse;
|
let data = res.data as GetSiteResponse;
|
||||||
|
|
||||||
if (data.site && !this.state.siteName) {
|
this.state.siteRes = data;
|
||||||
this.state.siteName = data.site.name;
|
|
||||||
this.state.version = data.version;
|
|
||||||
this.state.admins = data.admins;
|
|
||||||
}
|
|
||||||
|
|
||||||
// The login
|
// The login
|
||||||
if (data.my_user) {
|
if (data.my_user) {
|
||||||
|
@ -495,7 +522,9 @@ export class Navbar extends Component<any, NavbarState> {
|
||||||
get canAdmin(): boolean {
|
get canAdmin(): boolean {
|
||||||
return (
|
return (
|
||||||
UserService.Instance.user &&
|
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)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
392
ui/src/components/post-listing.tsx
vendored
392
ui/src/components/post-listing.tsx
vendored
|
@ -163,7 +163,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className={`img-fluid thumbnail rounded ${
|
className={`img-fluid thumbnail rounded ${
|
||||||
(post.nsfw || post.community_nsfw) && 'img-blur'
|
post.nsfw || post.community_nsfw ? 'img-blur' : ''
|
||||||
}`}
|
}`}
|
||||||
src={src}
|
src={src}
|
||||||
/>
|
/>
|
||||||
|
@ -190,8 +190,8 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
|
|
||||||
if (isImage(post.url)) {
|
if (isImage(post.url)) {
|
||||||
return (
|
return (
|
||||||
<span
|
<div
|
||||||
class="text-body pointer"
|
class="float-right text-body pointer d-inline-block position-relative"
|
||||||
data-tippy-content={i18n.t('expand_here')}
|
data-tippy-content={i18n.t('expand_here')}
|
||||||
onClick={linkEvent(this, this.handleImageExpandClick)}
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
>
|
>
|
||||||
|
@ -199,12 +199,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<svg class="icon mini-overlay">
|
<svg class="icon mini-overlay">
|
||||||
<use xlinkHref="#icon-image"></use>
|
<use xlinkHref="#icon-image"></use>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (post.thumbnail_url) {
|
} else if (post.thumbnail_url) {
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="text-body"
|
class="float-right text-body d-inline-block position-relative"
|
||||||
href={post.url}
|
href={post.url}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
@ -265,10 +265,93 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listing() {
|
createdLine() {
|
||||||
let post = this.props.post;
|
let post = this.props.post;
|
||||||
return (
|
return (
|
||||||
<div class="row">
|
<ul class="list-inline mb-1 text-muted small">
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<UserListing
|
||||||
|
user={{
|
||||||
|
name: post.creator_name,
|
||||||
|
preferred_username: post.creator_preferred_username,
|
||||||
|
avatar: post.creator_avatar,
|
||||||
|
id: post.creator_id,
|
||||||
|
local: post.creator_local,
|
||||||
|
actor_id: post.creator_actor_id,
|
||||||
|
published: post.creator_published,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
{this.isMod && (
|
||||||
|
<span className="mx-1 badge badge-light">{i18n.t('mod')}</span>
|
||||||
|
)}
|
||||||
|
{this.isAdmin && (
|
||||||
|
<span className="mx-1 badge badge-light">{i18n.t('admin')}</span>
|
||||||
|
)}
|
||||||
|
{(post.banned_from_community || post.banned) && (
|
||||||
|
<span className="mx-1 badge badge-danger">{i18n.t('banned')}</span>
|
||||||
|
)}
|
||||||
|
{this.props.showCommunity && (
|
||||||
|
<span>
|
||||||
|
<span class="mx-1"> {i18n.t('to')} </span>
|
||||||
|
<CommunityLink
|
||||||
|
community={{
|
||||||
|
name: post.community_name,
|
||||||
|
id: post.community_id,
|
||||||
|
local: post.community_local,
|
||||||
|
actor_id: post.community_actor_id,
|
||||||
|
icon: post.community_icon,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
|
{post.url && !(hostname(post.url) == window.location.hostname) && (
|
||||||
|
<>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<a
|
||||||
|
className="text-muted font-italic"
|
||||||
|
href={post.url}
|
||||||
|
target="_blank"
|
||||||
|
title={post.url}
|
||||||
|
rel="noopener"
|
||||||
|
>
|
||||||
|
{hostname(post.url)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<li className="list-inline-item">
|
||||||
|
<span>
|
||||||
|
<MomentTime data={post} />
|
||||||
|
</span>
|
||||||
|
</li>
|
||||||
|
{post.body && (
|
||||||
|
<>
|
||||||
|
<li className="list-inline-item">•</li>
|
||||||
|
<li className="list-inline-item">
|
||||||
|
{/* Using a link with tippy doesn't work on touch devices unfortunately */}
|
||||||
|
<Link
|
||||||
|
className="text-muted"
|
||||||
|
data-tippy-content={md.render(previewLines(post.body))}
|
||||||
|
data-tippy-allowHtml={true}
|
||||||
|
to={`/post/${post.id}`}
|
||||||
|
>
|
||||||
|
<svg class="mr-1 icon icon-inline">
|
||||||
|
<use xlinkHref="#icon-book-open"></use>
|
||||||
|
</svg>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
voteBar() {
|
||||||
|
return (
|
||||||
<div className={`vote-bar col-1 pr-0 small text-center`}>
|
<div className={`vote-bar col-1 pr-0 small text-center`}>
|
||||||
<button
|
<button
|
||||||
className={`btn-animate btn btn-link p-0 ${
|
className={`btn-animate btn btn-link p-0 ${
|
||||||
|
@ -301,18 +384,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{!this.state.imageExpanded && (
|
);
|
||||||
<div class="col-3 col-sm-2 pr-0">
|
}
|
||||||
<div class="position-relative">{this.thumbnail()}</div>
|
|
||||||
</div>
|
postTitleLine() {
|
||||||
)}
|
let post = this.props.post;
|
||||||
<div
|
return (
|
||||||
class={`${this.state.imageExpanded ? 'col-12' : 'col-8 col-sm-9'}`}
|
<div className="post-title overflow-hidden">
|
||||||
>
|
<h5>
|
||||||
<div class="row">
|
|
||||||
<div className="col-12">
|
|
||||||
<div className="post-title">
|
|
||||||
<h5 className="mb-1 d-inline-block">
|
|
||||||
{this.props.showBody && post.url ? (
|
{this.props.showBody && post.url ? (
|
||||||
<a
|
<a
|
||||||
className={!post.stickied ? 'text-body' : 'text-primary'}
|
className={!post.stickied ? 'text-body' : 'text-primary'}
|
||||||
|
@ -332,23 +411,6 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{post.name}
|
{post.name}
|
||||||
</Link>
|
</Link>
|
||||||
)}
|
)}
|
||||||
</h5>
|
|
||||||
{post.url && !(hostname(post.url) == window.location.hostname) && (
|
|
||||||
<small class="d-inline-block">
|
|
||||||
<a
|
|
||||||
className="ml-2 text-muted font-italic"
|
|
||||||
href={post.url}
|
|
||||||
target="_blank"
|
|
||||||
title={post.url}
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
{hostname(post.url)}
|
|
||||||
<svg class="ml-1 icon icon-inline">
|
|
||||||
<use xlinkHref="#icon-external-link"></use>
|
|
||||||
</svg>
|
|
||||||
</a>
|
|
||||||
</small>
|
|
||||||
)}
|
|
||||||
{(isImage(post.url) || this.props.post.thumbnail_url) && (
|
{(isImage(post.url) || this.props.post.thumbnail_url) && (
|
||||||
<>
|
<>
|
||||||
{!this.state.imageExpanded ? (
|
{!this.state.imageExpanded ? (
|
||||||
|
@ -374,10 +436,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
<div>
|
<div>
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleImageExpandClick)}
|
||||||
this,
|
|
||||||
this.handleImageExpandClick
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
<img
|
<img
|
||||||
class="img-fluid img-expanded"
|
class="img-fluid img-expanded"
|
||||||
|
@ -429,80 +488,15 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{i18n.t('nsfw')}
|
{i18n.t('nsfw')}
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
|
</h5>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
);
|
||||||
</div>
|
}
|
||||||
<div class="row">
|
|
||||||
<div className="details col-12">
|
|
||||||
<ul class="list-inline mb-1 text-muted small">
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<span>{i18n.t('by')} </span>
|
|
||||||
<UserListing
|
|
||||||
user={{
|
|
||||||
name: post.creator_name,
|
|
||||||
avatar: post.creator_avatar,
|
|
||||||
id: post.creator_id,
|
|
||||||
local: post.creator_local,
|
|
||||||
actor_id: post.creator_actor_id,
|
|
||||||
published: post.creator_published,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{this.isMod && (
|
commentsLine(showVotes: boolean = false) {
|
||||||
<span className="mx-1 badge badge-light">
|
let post = this.props.post;
|
||||||
{i18n.t('mod')}
|
return (
|
||||||
</span>
|
<ul class="d-flex align-items-center list-inline mb-1 text-muted small">
|
||||||
)}
|
|
||||||
{this.isAdmin && (
|
|
||||||
<span className="mx-1 badge badge-light">
|
|
||||||
{i18n.t('admin')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{(post.banned_from_community || post.banned) && (
|
|
||||||
<span className="mx-1 badge badge-danger">
|
|
||||||
{i18n.t('banned')}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
{this.props.showCommunity && (
|
|
||||||
<span>
|
|
||||||
<span> {i18n.t('to')} </span>
|
|
||||||
<CommunityLink
|
|
||||||
community={{
|
|
||||||
name: post.community_name,
|
|
||||||
id: post.community_id,
|
|
||||||
local: post.community_local,
|
|
||||||
actor_id: post.community_actor_id,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</li>
|
|
||||||
<li className="list-inline-item">•</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
<span>
|
|
||||||
<MomentTime data={post} />
|
|
||||||
</span>
|
|
||||||
</li>
|
|
||||||
{post.body && (
|
|
||||||
<>
|
|
||||||
<li className="list-inline-item">•</li>
|
|
||||||
<li className="list-inline-item">
|
|
||||||
{/* Using a link with tippy doesn't work on touch devices unfortunately */}
|
|
||||||
<Link
|
|
||||||
className="text-muted"
|
|
||||||
data-tippy-content={md.render(previewLines(post.body))}
|
|
||||||
data-tippy-allowHtml={true}
|
|
||||||
to={`/post/${post.id}`}
|
|
||||||
>
|
|
||||||
<svg class="mr-1 icon icon-inline">
|
|
||||||
<use xlinkHref="#icon-book-open"></use>
|
|
||||||
</svg>
|
|
||||||
</Link>
|
|
||||||
</li>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</ul>
|
|
||||||
<ul class="list-inline mb-1 text-muted small">
|
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link
|
<Link
|
||||||
className="text-muted"
|
className="text-muted"
|
||||||
|
@ -519,7 +513,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
})}
|
})}
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
{this.state.upvotes !== this.state.score && (
|
{(showVotes || this.state.upvotes !== this.state.score) && (
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item">•</li>
|
<li className="list-inline-item">•</li>
|
||||||
<span
|
<span
|
||||||
|
@ -527,26 +521,41 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
data-tippy-content={this.pointsTippy}
|
data-tippy-content={this.pointsTippy}
|
||||||
>
|
>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span className="text-muted">
|
<a
|
||||||
|
className={`btn-animate btn btn-link p-0 ${
|
||||||
|
this.state.my_vote == 1 ? 'text-info' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(this, this.handlePostLike)}
|
||||||
|
>
|
||||||
<svg class="small icon icon-inline mr-1">
|
<svg class="small icon icon-inline mr-1">
|
||||||
<use xlinkHref="#icon-arrow-up"></use>
|
<use xlinkHref="#icon-arrow-up"></use>
|
||||||
</svg>
|
</svg>
|
||||||
{this.state.upvotes}
|
{this.state.upvotes}
|
||||||
</span>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span className="text-muted">
|
<a
|
||||||
|
className={`btn-animate btn btn-link p-0 ${
|
||||||
|
this.state.my_vote == -1 ? 'text-danger' : 'text-muted'
|
||||||
|
}`}
|
||||||
|
onClick={linkEvent(this, this.handlePostDisLike)}
|
||||||
|
>
|
||||||
<svg class="small icon icon-inline mr-1">
|
<svg class="small icon icon-inline mr-1">
|
||||||
<use xlinkHref="#icon-arrow-down"></use>
|
<use xlinkHref="#icon-arrow-down"></use>
|
||||||
</svg>
|
</svg>
|
||||||
{this.state.downvotes}
|
{this.state.downvotes}
|
||||||
</span>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
</span>
|
</span>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{this.props.post.duplicates && (
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
duplicatesLine() {
|
||||||
|
return (
|
||||||
|
this.props.post.duplicates && (
|
||||||
<ul class="list-inline mb-1 small text-muted">
|
<ul class="list-inline mb-1 small text-muted">
|
||||||
<>
|
<>
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
|
@ -554,14 +563,18 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</li>
|
</li>
|
||||||
{this.props.post.duplicates.map(post => (
|
{this.props.post.duplicates.map(post => (
|
||||||
<li className="list-inline-item mr-2">
|
<li className="list-inline-item mr-2">
|
||||||
<Link to={`/post/${post.id}`}>
|
<Link to={`/post/${post.id}`}>{post.community_name}</Link>
|
||||||
{post.community_name}
|
|
||||||
</Link>
|
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</>
|
</>
|
||||||
</ul>
|
</ul>
|
||||||
)}
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
postActions() {
|
||||||
|
let post = this.props.post;
|
||||||
|
return (
|
||||||
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
<ul class="list-inline mb-1 text-muted font-weight-bold">
|
||||||
{UserService.Instance.user && (
|
{UserService.Instance.user && (
|
||||||
<>
|
<>
|
||||||
|
@ -576,9 +589,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
class={`icon icon-inline ${
|
class={`icon icon-inline ${post.saved && 'text-warning'}`}
|
||||||
post.saved && 'text-warning'
|
|
||||||
}`}
|
|
||||||
>
|
>
|
||||||
<use xlinkHref="#icon-star"></use>
|
<use xlinkHref="#icon-star"></use>
|
||||||
</svg>
|
</svg>
|
||||||
|
@ -615,9 +626,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="btn btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleDeleteClick)}
|
onClick={linkEvent(this, this.handleDeleteClick)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
!post.deleted
|
!post.deleted ? i18n.t('delete') : i18n.t('restore')
|
||||||
? i18n.t('delete')
|
|
||||||
: i18n.t('restore')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -670,9 +679,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="btn btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleModLock)}
|
onClick={linkEvent(this, this.handleModLock)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
post.locked
|
post.locked ? i18n.t('unlock') : i18n.t('lock')
|
||||||
? i18n.t('unlock')
|
|
||||||
: i18n.t('lock')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -689,9 +696,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="btn btn-link btn-animate text-muted"
|
class="btn btn-link btn-animate text-muted"
|
||||||
onClick={linkEvent(this, this.handleModSticky)}
|
onClick={linkEvent(this, this.handleModSticky)}
|
||||||
data-tippy-content={
|
data-tippy-content={
|
||||||
post.stickied
|
post.stickied ? i18n.t('unsticky') : i18n.t('sticky')
|
||||||
? i18n.t('unsticky')
|
|
||||||
: i18n.t('sticky')
|
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
@ -711,20 +716,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{!post.removed ? (
|
{!post.removed ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleModRemoveShow)}
|
||||||
this,
|
|
||||||
this.handleModRemoveShow
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t('remove')}
|
{i18n.t('remove')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleModRemoveSubmit)}
|
||||||
this,
|
|
||||||
this.handleModRemoveSubmit
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t('restore')}
|
{i18n.t('restore')}
|
||||||
</span>
|
</span>
|
||||||
|
@ -776,8 +775,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{/* Community creators and admins can transfer community to another mod */}
|
{/* Community creators and admins can transfer community to another mod */}
|
||||||
{(this.amCommunityCreator || this.canAdmin) &&
|
{(this.amCommunityCreator || this.canAdmin) && this.isMod && (
|
||||||
this.isMod && (
|
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
{!this.state.showConfirmTransferCommunity ? (
|
{!this.state.showConfirmTransferCommunity ? (
|
||||||
<span
|
<span
|
||||||
|
@ -807,8 +805,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
class="pointer d-inline-block"
|
class="pointer d-inline-block"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(
|
||||||
this,
|
this,
|
||||||
this
|
this.handleCancelShowConfirmTransferCommunity
|
||||||
.handleCancelShowConfirmTransferCommunity
|
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{i18n.t('no')}
|
{i18n.t('no')}
|
||||||
|
@ -825,20 +822,14 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
{!post.banned ? (
|
{!post.banned ? (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleModBanShow)}
|
||||||
this,
|
|
||||||
this.handleModBanShow
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t('ban_from_site')}
|
{i18n.t('ban_from_site')}
|
||||||
</span>
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span
|
<span
|
||||||
class="pointer"
|
class="pointer"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleModBanSubmit)}
|
||||||
this,
|
|
||||||
this.handleModBanSubmit
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t('unban_from_site')}
|
{i18n.t('unban_from_site')}
|
||||||
</span>
|
</span>
|
||||||
|
@ -879,10 +870,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span
|
||||||
class="pointer d-inline-block mr-1"
|
class="pointer d-inline-block mr-1"
|
||||||
onClick={linkEvent(
|
onClick={linkEvent(this, this.handleTransferSite)}
|
||||||
this,
|
|
||||||
this.handleTransferSite
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{i18n.t('yes')}
|
{i18n.t('yes')}
|
||||||
</span>
|
</span>
|
||||||
|
@ -904,6 +892,13 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeAndBanDialogs() {
|
||||||
|
let post = this.props.post;
|
||||||
|
return (
|
||||||
|
<>
|
||||||
{this.state.showRemoveDialog && (
|
{this.state.showRemoveDialog && (
|
||||||
<form
|
<form
|
||||||
class="form-inline"
|
class="form-inline"
|
||||||
|
@ -948,10 +943,91 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
)}
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
mobileThumbnail() {
|
||||||
|
return this.props.post.thumbnail_url || isImage(this.props.post.url) ? (
|
||||||
|
<div class="row">
|
||||||
|
<div className={`${this.state.imageExpanded ? 'col-12' : 'col-8'}`}>
|
||||||
|
{this.postTitleLine()}
|
||||||
|
</div>
|
||||||
|
<div class="col-4">
|
||||||
|
{/* Post body prev or thumbnail */}
|
||||||
|
{!this.state.imageExpanded && this.thumbnail()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
this.postTitleLine()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
showMobilePreview() {
|
||||||
|
return (
|
||||||
|
this.props.post.body &&
|
||||||
|
!this.props.showBody && (
|
||||||
|
<div
|
||||||
|
className="md-div mb-1"
|
||||||
|
dangerouslySetInnerHTML={{
|
||||||
|
__html: md.render(previewLines(this.props.post.body)),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
listing() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{/* The mobile view*/}
|
||||||
|
<div class="d-block d-sm-none">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
{this.createdLine()}
|
||||||
|
|
||||||
|
{/* If it has a thumbnail, do a right aligned thumbnail */}
|
||||||
|
{this.mobileThumbnail()}
|
||||||
|
|
||||||
|
{/* Show a preview of the post body */}
|
||||||
|
{this.showMobilePreview()}
|
||||||
|
|
||||||
|
{this.commentsLine(true)}
|
||||||
|
{this.duplicatesLine()}
|
||||||
|
{this.postActions()}
|
||||||
|
{this.removeAndBanDialogs()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* The larger view*/}
|
||||||
|
<div class="d-none d-sm-block">
|
||||||
|
<div class="row">
|
||||||
|
{this.voteBar()}
|
||||||
|
{!this.state.imageExpanded && (
|
||||||
|
<div class="col-sm-2 pr-0">
|
||||||
|
<div class="">{this.thumbnail()}</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div
|
||||||
|
class={`${
|
||||||
|
this.state.imageExpanded ? 'col-12' : 'col-12 col-sm-9'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div class="row">
|
||||||
|
<div className="col-12">
|
||||||
|
{this.postTitleLine()}
|
||||||
|
{this.createdLine()}
|
||||||
|
{this.commentsLine()}
|
||||||
|
{this.duplicatesLine()}
|
||||||
|
{this.postActions()}
|
||||||
|
{this.removeAndBanDialogs()}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
15
ui/src/components/post.tsx
vendored
15
ui/src/components/post.tsx
vendored
|
@ -39,6 +39,7 @@ import {
|
||||||
createPostLikeRes,
|
createPostLikeRes,
|
||||||
commentsToFlatNodes,
|
commentsToFlatNodes,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
|
favIconUrl,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { PostListing } from './post-listing';
|
import { PostListing } from './post-listing';
|
||||||
import { Sidebar } from './sidebar';
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<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 ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -332,6 +344,7 @@ export class Post extends Component<any, PostState> {
|
||||||
admins={this.state.siteRes.admins}
|
admins={this.state.siteRes.admins}
|
||||||
online={this.state.online}
|
online={this.state.online}
|
||||||
enableNsfw={this.state.siteRes.site.enable_nsfw}
|
enableNsfw={this.state.siteRes.site.enable_nsfw}
|
||||||
|
showIcon
|
||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: this.state.recipient.name,
|
name: this.state.recipient.name,
|
||||||
|
preferred_username: this.state.recipient
|
||||||
|
.preferred_username,
|
||||||
avatar: this.state.recipient.avatar,
|
avatar: this.state.recipient.avatar,
|
||||||
id: this.state.recipient.id,
|
id: this.state.recipient.id,
|
||||||
local: this.state.recipient.local,
|
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 { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
|
||||||
import { MomentTime } from './moment-time';
|
import { MomentTime } from './moment-time';
|
||||||
import { PrivateMessageForm } from './private-message-form';
|
import { PrivateMessageForm } from './private-message-form';
|
||||||
|
import { UserListing, UserOther } from './user-listing';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface PrivateMessageState {
|
interface PrivateMessageState {
|
||||||
|
@ -53,6 +54,26 @@ export class PrivateMessage extends Component<
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
let message = this.props.privateMessage;
|
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 (
|
return (
|
||||||
<div class="border-top border-light">
|
<div class="border-top border-light">
|
||||||
<div>
|
<div>
|
||||||
|
@ -62,33 +83,7 @@ export class PrivateMessage extends Component<
|
||||||
{this.mine ? i18n.t('to') : i18n.t('from')}
|
{this.mine ? i18n.t('to') : i18n.t('from')}
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<Link
|
<UserListing user={userOther} />
|
||||||
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>
|
|
||||||
</li>
|
</li>
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<span>
|
<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
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: (i.data as UserView).name,
|
name: (i.data as UserView).name,
|
||||||
|
preferred_username: (i.data as UserView)
|
||||||
|
.preferred_username,
|
||||||
avatar: (i.data as UserView).avatar,
|
avatar: (i.data as UserView).avatar,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
19
ui/src/components/sidebar.tsx
vendored
19
ui/src/components/sidebar.tsx
vendored
|
@ -9,10 +9,11 @@ import {
|
||||||
UserView,
|
UserView,
|
||||||
} from '../interfaces';
|
} from '../interfaces';
|
||||||
import { WebSocketService, UserService } from '../services';
|
import { WebSocketService, UserService } from '../services';
|
||||||
import { mdToHtml, getUnixTime } from '../utils';
|
import { mdToHtml, getUnixTime, pictrsAvatarThumbnail } from '../utils';
|
||||||
import { CommunityForm } from './community-form';
|
import { CommunityForm } from './community-form';
|
||||||
import { UserListing } from './user-listing';
|
import { UserListing } from './user-listing';
|
||||||
import { CommunityLink } from './community-link';
|
import { CommunityLink } from './community-link';
|
||||||
|
import { BannerIconHeader } from './banner-icon-header';
|
||||||
import { i18n } from '../i18next';
|
import { i18n } from '../i18next';
|
||||||
|
|
||||||
interface SidebarProps {
|
interface SidebarProps {
|
||||||
|
@ -21,6 +22,7 @@ interface SidebarProps {
|
||||||
admins: Array<UserView>;
|
admins: Array<UserView>;
|
||||||
online: number;
|
online: number;
|
||||||
enableNsfw: boolean;
|
enableNsfw: boolean;
|
||||||
|
showIcon?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SidebarState {
|
interface SidebarState {
|
||||||
|
@ -86,7 +88,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
communityTitle() {
|
communityTitle() {
|
||||||
let community = this.props.community;
|
let community = this.props.community;
|
||||||
return (
|
return (
|
||||||
<h5 className="mb-2">
|
<div>
|
||||||
|
<h5 className="mb-0">
|
||||||
|
{this.props.showIcon && (
|
||||||
|
<BannerIconHeader icon={community.icon} banner={community.banner} />
|
||||||
|
)}
|
||||||
<span>{community.title}</span>
|
<span>{community.title}</span>
|
||||||
{community.removed && (
|
{community.removed && (
|
||||||
<small className="ml-2 text-muted font-italic">
|
<small className="ml-2 text-muted font-italic">
|
||||||
|
@ -104,6 +110,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
</small>
|
</small>
|
||||||
)}
|
)}
|
||||||
</h5>
|
</h5>
|
||||||
|
<CommunityLink
|
||||||
|
community={community}
|
||||||
|
realLink
|
||||||
|
useApubName
|
||||||
|
muted
|
||||||
|
hideAvatar
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,6 +174,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
|
||||||
<UserListing
|
<UserListing
|
||||||
user={{
|
user={{
|
||||||
name: mod.user_name,
|
name: mod.user_name,
|
||||||
|
preferred_username: mod.user_preferred_username,
|
||||||
avatar: mod.avatar,
|
avatar: mod.avatar,
|
||||||
id: mod.user_id,
|
id: mod.user_id,
|
||||||
local: mod.user_local,
|
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 { Component, linkEvent } from 'inferno';
|
||||||
import { Prompt } from 'inferno-router';
|
import { Prompt } from 'inferno-router';
|
||||||
import { MarkdownTextArea } from './markdown-textarea';
|
import { MarkdownTextArea } from './markdown-textarea';
|
||||||
|
import { ImageUploadForm } from './image-upload-form';
|
||||||
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
import { Site, SiteForm as SiteFormI } from '../interfaces';
|
||||||
import { WebSocketService } from '../services';
|
import { WebSocketService } from '../services';
|
||||||
import { capitalizeFirstLetter, randomStr } from '../utils';
|
import { capitalizeFirstLetter, randomStr } from '../utils';
|
||||||
|
@ -24,6 +25,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
open_registration: true,
|
open_registration: true,
|
||||||
enable_nsfw: true,
|
enable_nsfw: true,
|
||||||
name: null,
|
name: null,
|
||||||
|
icon: null,
|
||||||
|
banner: null,
|
||||||
},
|
},
|
||||||
loading: false,
|
loading: false,
|
||||||
};
|
};
|
||||||
|
@ -36,6 +39,12 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
this
|
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) {
|
if (this.props.site) {
|
||||||
this.state.siteForm = {
|
this.state.siteForm = {
|
||||||
name: this.props.site.name,
|
name: this.props.site.name,
|
||||||
|
@ -43,6 +52,8 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
enable_downvotes: this.props.site.enable_downvotes,
|
enable_downvotes: this.props.site.enable_downvotes,
|
||||||
open_registration: this.props.site.open_registration,
|
open_registration: this.props.site.open_registration,
|
||||||
enable_nsfw: this.props.site.enable_nsfw,
|
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>
|
</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">
|
<div class="form-group row">
|
||||||
<label class="col-12 col-form-label" htmlFor={this.id}>
|
<label class="col-12 col-form-label" htmlFor={this.id}>
|
||||||
{i18n.t('sidebar')}
|
{i18n.t('sidebar')}
|
||||||
|
@ -247,4 +277,24 @@ export class SiteForm extends Component<SiteFormProps, SiteFormState> {
|
||||||
handleCancel(i: SiteForm) {
|
handleCancel(i: SiteForm) {
|
||||||
i.props.onCancel();
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
3
ui/src/components/sort-select.tsx
vendored
3
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>
|
<option disabled>{i18n.t('sort_type')}</option>
|
||||||
{!this.props.hideHot && (
|
{!this.props.hideHot && (
|
||||||
|
<>
|
||||||
|
<option value={SortType.Active}>{i18n.t('active')}</option>
|
||||||
<option value={SortType.Hot}>{i18n.t('hot')}</option>
|
<option value={SortType.Hot}>{i18n.t('hot')}</option>
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
<option value={SortType.New}>{i18n.t('new')}</option>
|
<option value={SortType.New}>{i18n.t('new')}</option>
|
||||||
<option disabled>─────</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"
|
xmlnsXlink="http://www.w3.org/1999/xlink"
|
||||||
>
|
>
|
||||||
<defs>
|
<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">
|
<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>
|
<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>
|
</symbol>
|
||||||
|
|
30
ui/src/components/user-listing.tsx
vendored
30
ui/src/components/user-listing.tsx
vendored
|
@ -9,8 +9,9 @@ import {
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { CakeDay } from './cake-day';
|
import { CakeDay } from './cake-day';
|
||||||
|
|
||||||
interface UserOther {
|
export interface UserOther {
|
||||||
name: string;
|
name: string;
|
||||||
|
preferred_username?: string;
|
||||||
id?: number; // Necessary if its federated
|
id?: number; // Necessary if its federated
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
local?: boolean;
|
local?: boolean;
|
||||||
|
@ -21,6 +22,9 @@ interface UserOther {
|
||||||
interface UserListingProps {
|
interface UserListingProps {
|
||||||
user: UserView | UserOther;
|
user: UserView | UserOther;
|
||||||
realLink?: boolean;
|
realLink?: boolean;
|
||||||
|
useApubName?: boolean;
|
||||||
|
muted?: boolean;
|
||||||
|
hideAvatar?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UserListing extends Component<UserListingProps, any> {
|
export class UserListing extends Component<UserListingProps, any> {
|
||||||
|
@ -31,30 +35,40 @@ export class UserListing extends Component<UserListingProps, any> {
|
||||||
render() {
|
render() {
|
||||||
let user = this.props.user;
|
let user = this.props.user;
|
||||||
let local = user.local == null ? true : user.local;
|
let local = user.local == null ? true : user.local;
|
||||||
let name_: string, link: string;
|
let apubName: string, link: string;
|
||||||
|
|
||||||
if (local) {
|
if (local) {
|
||||||
name_ = user.name;
|
apubName = `@${user.name}`;
|
||||||
link = `/u/${user.name}`;
|
link = `/u/${user.name}`;
|
||||||
} else {
|
} 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;
|
link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let displayName = this.props.useApubName
|
||||||
|
? apubName
|
||||||
|
: user.preferred_username
|
||||||
|
? user.preferred_username
|
||||||
|
: apubName;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link className="text-info" to={link}>
|
<Link
|
||||||
{user.avatar && showAvatars() && (
|
title={apubName}
|
||||||
|
className={this.props.muted ? 'text-muted' : 'text-info'}
|
||||||
|
to={link}
|
||||||
|
>
|
||||||
|
{!this.props.hideAvatar && user.avatar && showAvatars() && (
|
||||||
<img
|
<img
|
||||||
style="width: 2rem; height: 2rem;"
|
style="width: 2rem; height: 2rem;"
|
||||||
src={pictrsAvatarThumbnail(user.avatar)}
|
src={pictrsAvatarThumbnail(user.avatar)}
|
||||||
class="rounded-circle mr-2"
|
class="rounded-circle mr-2"
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<span>{name_}</span>
|
<span>{displayName}</span>
|
||||||
</Link>
|
</Link>
|
||||||
|
|
||||||
{isCakeDay(user.published) && <CakeDay creatorName={name_} />}
|
{isCakeDay(user.published) && <CakeDay creatorName={apubName} />}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
360
ui/src/components/user.tsx
vendored
360
ui/src/components/user.tsx
vendored
|
@ -27,11 +27,12 @@ import {
|
||||||
themes,
|
themes,
|
||||||
setTheme,
|
setTheme,
|
||||||
languages,
|
languages,
|
||||||
showAvatars,
|
|
||||||
toast,
|
toast,
|
||||||
setupTippy,
|
setupTippy,
|
||||||
getLanguage,
|
getLanguage,
|
||||||
mdToHtml,
|
mdToHtml,
|
||||||
|
elementUrl,
|
||||||
|
favIconUrl,
|
||||||
} from '../utils';
|
} from '../utils';
|
||||||
import { UserListing } from './user-listing';
|
import { UserListing } from './user-listing';
|
||||||
import { SortSelect } from './sort-select';
|
import { SortSelect } from './sort-select';
|
||||||
|
@ -41,6 +42,8 @@ import { i18n } from '../i18next';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
import { UserDetails } from './user-details';
|
import { UserDetails } from './user-details';
|
||||||
import { MarkdownTextArea } from './markdown-textarea';
|
import { MarkdownTextArea } from './markdown-textarea';
|
||||||
|
import { ImageUploadForm } from './image-upload-form';
|
||||||
|
import { BannerIconHeader } from './banner-icon-header';
|
||||||
|
|
||||||
interface UserState {
|
interface UserState {
|
||||||
user: UserView;
|
user: UserView;
|
||||||
|
@ -52,7 +55,6 @@ interface UserState {
|
||||||
sort: SortType;
|
sort: SortType;
|
||||||
page: number;
|
page: number;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
avatarLoading: boolean;
|
|
||||||
userSettingsForm: UserSettingsForm;
|
userSettingsForm: UserSettingsForm;
|
||||||
userSettingsLoading: boolean;
|
userSettingsLoading: boolean;
|
||||||
deleteAccountLoading: boolean;
|
deleteAccountLoading: boolean;
|
||||||
|
@ -98,7 +100,6 @@ export class User extends Component<any, UserState> {
|
||||||
follows: [],
|
follows: [],
|
||||||
moderates: [],
|
moderates: [],
|
||||||
loading: true,
|
loading: true,
|
||||||
avatarLoading: false,
|
|
||||||
view: User.getViewFromProps(this.props.match.view),
|
view: User.getViewFromProps(this.props.match.view),
|
||||||
sort: User.getSortTypeFromProps(this.props.match.sort),
|
sort: User.getSortTypeFromProps(this.props.match.sort),
|
||||||
page: User.getPageFromProps(this.props.match.page),
|
page: User.getPageFromProps(this.props.match.page),
|
||||||
|
@ -112,6 +113,7 @@ export class User extends Component<any, UserState> {
|
||||||
send_notifications_to_email: null,
|
send_notifications_to_email: null,
|
||||||
auth: null,
|
auth: null,
|
||||||
bio: null,
|
bio: null,
|
||||||
|
preferred_username: null,
|
||||||
},
|
},
|
||||||
userSettingsLoading: null,
|
userSettingsLoading: null,
|
||||||
deleteAccountLoading: null,
|
deleteAccountLoading: null,
|
||||||
|
@ -136,6 +138,9 @@ export class User extends Component<any, UserState> {
|
||||||
enable_downvotes: undefined,
|
enable_downvotes: undefined,
|
||||||
open_registration: undefined,
|
open_registration: undefined,
|
||||||
enable_nsfw: undefined,
|
enable_nsfw: undefined,
|
||||||
|
icon: undefined,
|
||||||
|
banner: undefined,
|
||||||
|
creator_preferred_username: undefined,
|
||||||
},
|
},
|
||||||
version: undefined,
|
version: undefined,
|
||||||
},
|
},
|
||||||
|
@ -157,6 +162,12 @@ export class User extends Component<any, UserState> {
|
||||||
this
|
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.user_id = Number(this.props.match.params.id) || null;
|
||||||
this.state.username = this.props.match.params.username;
|
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() {
|
render() {
|
||||||
return (
|
return (
|
||||||
<div class="container">
|
<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="row">
|
||||||
<div class="col-12 col-md-8">
|
<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 ? (
|
{this.state.loading ? (
|
||||||
<h5>
|
<h5>
|
||||||
<svg class="icon icon-spinner spin">
|
<svg class="icon icon-spinner spin">
|
||||||
|
@ -250,8 +265,12 @@ export class User extends Component<any, UserState> {
|
||||||
</svg>
|
</svg>
|
||||||
</h5>
|
</h5>
|
||||||
) : (
|
) : (
|
||||||
this.selects()
|
<>
|
||||||
|
{this.userInfo()}
|
||||||
|
<hr />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
{!this.state.loading && this.selects()}
|
||||||
<UserDetails
|
<UserDetails
|
||||||
user_id={this.state.user_id}
|
user_id={this.state.user_id}
|
||||||
username={this.state.username}
|
username={this.state.username}
|
||||||
|
@ -268,7 +287,6 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
{!this.state.loading && (
|
{!this.state.loading && (
|
||||||
<div class="col-12 col-md-4">
|
<div class="col-12 col-md-4">
|
||||||
{this.userInfo()}
|
|
||||||
{this.isCurrentUser && this.userSettings()}
|
{this.isCurrentUser && this.userSettings()}
|
||||||
{this.moderates()}
|
{this.moderates()}
|
||||||
{this.follows()}
|
{this.follows()}
|
||||||
|
@ -365,14 +383,29 @@ export class User extends Component<any, UserState> {
|
||||||
|
|
||||||
userInfo() {
|
userInfo() {
|
||||||
let user = this.state.user;
|
let user = this.state.user;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div class="card bg-transparent border-secondary mb-3">
|
<BannerIconHeader
|
||||||
<div class="card-body">
|
banner={this.state.user.banner}
|
||||||
<h5>
|
icon={this.state.user.avatar}
|
||||||
<ul class="list-inline mb-0">
|
/>
|
||||||
|
<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 class="list-inline mb-2">
|
||||||
<li className="list-inline-item">
|
<li className="list-inline-item">
|
||||||
<UserListing user={user} realLink />
|
<UserListing
|
||||||
|
user={user}
|
||||||
|
realLink
|
||||||
|
useApubName
|
||||||
|
muted
|
||||||
|
hideAvatar
|
||||||
|
/>
|
||||||
</li>
|
</li>
|
||||||
{user.banned && (
|
{user.banned && (
|
||||||
<li className="list-inline-item badge badge-danger">
|
<li className="list-inline-item badge badge-danger">
|
||||||
|
@ -380,65 +413,11 @@ export class User extends Component<any, UserState> {
|
||||||
</li>
|
</li>
|
||||||
)}
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
</h5>
|
|
||||||
{user.bio && (
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
|
||||||
<div
|
|
||||||
className="md-div"
|
|
||||||
dangerouslySetInnerHTML={mdToHtml(user.bio)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="d-flex align-items-center mb-2">
|
|
||||||
<svg class="icon">
|
|
||||||
<use xlinkHref="#icon-cake"></use>
|
|
||||||
</svg>
|
|
||||||
<span className="ml-2">
|
|
||||||
{i18n.t('cake_day_title')}{' '}
|
|
||||||
{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>
|
</div>
|
||||||
|
<div className="flex-grow-1 unselectable pointer mx-2"></div>
|
||||||
{this.isCurrentUser ? (
|
{this.isCurrentUser ? (
|
||||||
<button
|
<button
|
||||||
class="btn btn-block btn-secondary mt-3"
|
class="d-flex align-self-start btn btn-secondary ml-2"
|
||||||
onClick={linkEvent(this, this.handleLogoutClick)}
|
onClick={linkEvent(this, this.handleLogoutClick)}
|
||||||
>
|
>
|
||||||
{i18n.t('logout')}
|
{i18n.t('logout')}
|
||||||
|
@ -446,8 +425,8 @@ export class User extends Component<any, UserState> {
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<a
|
<a
|
||||||
className={`btn btn-block btn-secondary mt-3 ${
|
className={`d-flex align-self-start btn btn-secondary ml-2 ${
|
||||||
!this.state.user.matrix_user_id && 'disabled'
|
!this.state.user.matrix_user_id && 'invisible'
|
||||||
}`}
|
}`}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener"
|
rel="noopener"
|
||||||
|
@ -456,7 +435,7 @@ export class User extends Component<any, UserState> {
|
||||||
{i18n.t('send_secure_message')}
|
{i18n.t('send_secure_message')}
|
||||||
</a>
|
</a>
|
||||||
<Link
|
<Link
|
||||||
class="btn btn-block btn-secondary mt-3"
|
class="d-flex align-self-start btn btn-secondary ml-2"
|
||||||
to={`/create_private_message?recipient_id=${this.state.user.id}`}
|
to={`/create_private_message?recipient_id=${this.state.user.id}`}
|
||||||
>
|
>
|
||||||
{i18n.t('send_message')}
|
{i18n.t('send_message')}
|
||||||
|
@ -464,6 +443,39 @@ export class User extends Component<any, UserState> {
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
{user.bio && (
|
||||||
|
<div className="d-flex align-items-center mb-2">
|
||||||
|
<div
|
||||||
|
className="md-div"
|
||||||
|
dangerouslySetInnerHTML={mdToHtml(user.bio)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<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>
|
||||||
|
<span className="ml-2">
|
||||||
|
{i18n.t('cake_day_title')}{' '}
|
||||||
|
{moment.utc(user.published).local().format('MMM DD, YYYY')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
@ -478,47 +490,23 @@ export class User extends Component<any, UserState> {
|
||||||
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{i18n.t('avatar')}</label>
|
<label>{i18n.t('avatar')}</label>
|
||||||
<form class="d-inline">
|
<ImageUploadForm
|
||||||
<label
|
uploadTitle={i18n.t('upload_avatar')}
|
||||||
htmlFor="file-upload"
|
imageSrc={this.state.userSettingsForm.avatar}
|
||||||
class="pointer ml-4 text-muted small font-weight-bold"
|
onUpload={this.handleAvatarUpload}
|
||||||
>
|
onRemove={this.handleAvatarRemove}
|
||||||
{!this.checkSettingsAvatar ? (
|
rounded
|
||||||
<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>
|
|
||||||
</div>
|
</div>
|
||||||
{this.checkSettingsAvatar && (
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<button
|
<label>{i18n.t('banner')}</label>
|
||||||
class="btn btn-secondary btn-block"
|
<ImageUploadForm
|
||||||
onClick={linkEvent(this, this.removeAvatar)}
|
uploadTitle={i18n.t('upload_banner')}
|
||||||
>
|
imageSrc={this.state.userSettingsForm.banner}
|
||||||
{`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
|
onUpload={this.handleBannerUpload}
|
||||||
'avatar'
|
onRemove={this.handleBannerRemove}
|
||||||
)}`}
|
/>
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label>{i18n.t('language')}</label>
|
<label>{i18n.t('language')}</label>
|
||||||
<select
|
<select
|
||||||
|
@ -565,6 +553,38 @@ export class User extends Component<any, UserState> {
|
||||||
onChange={this.handleUserSettingsSortTypeChange}
|
onChange={this.handleUserSettingsSortTypeChange}
|
||||||
/>
|
/>
|
||||||
</form>
|
</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">
|
<div class="form-group row">
|
||||||
<label class="col-lg-3 col-form-label" htmlFor="user-email">
|
<label class="col-lg-3 col-form-label" htmlFor="user-email">
|
||||||
{i18n.t('email')}
|
{i18n.t('email')}
|
||||||
|
@ -584,26 +604,9 @@ export class User extends Component<any, UserState> {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<div class="form-group row">
|
||||||
<label class="col-lg-5 col-form-label">
|
<label class="col-lg-5 col-form-label">
|
||||||
<a
|
<a href={elementUrl} target="_blank" rel="noopener">
|
||||||
href="https://about.riot.im/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener"
|
|
||||||
>
|
|
||||||
{i18n.t('matrix_user_id')}
|
{i18n.t('matrix_user_id')}
|
||||||
</a>
|
</a>
|
||||||
</label>
|
</label>
|
||||||
|
@ -932,6 +935,31 @@ export class User extends Component<any, UserState> {
|
||||||
this.setState(this.state);
|
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) {
|
handleUserSettingsMatrixUserIdChange(i: User, event: any) {
|
||||||
i.state.userSettingsForm.matrix_user_id = event.target.value;
|
i.state.userSettingsForm.matrix_user_id = event.target.value;
|
||||||
if (
|
if (
|
||||||
|
@ -967,59 +995,6 @@ export class User extends Component<any, UserState> {
|
||||||
i.setState(i.state);
|
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) {
|
handleUserSettingsSubmit(i: User, event: any) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
i.state.userSettingsLoading = true;
|
i.state.userSettingsLoading = true;
|
||||||
|
@ -1062,7 +1037,6 @@ export class User extends Component<any, UserState> {
|
||||||
}
|
}
|
||||||
this.setState({
|
this.setState({
|
||||||
deleteAccountLoading: false,
|
deleteAccountLoading: false,
|
||||||
avatarLoading: false,
|
|
||||||
userSettingsLoading: false,
|
userSettingsLoading: false,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
@ -1088,6 +1062,9 @@ export class User extends Component<any, UserState> {
|
||||||
UserService.Instance.user.default_listing_type;
|
UserService.Instance.user.default_listing_type;
|
||||||
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
|
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
|
||||||
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
|
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.email = this.state.user.email;
|
||||||
this.state.userSettingsForm.bio = this.state.user.bio;
|
this.state.userSettingsForm.bio = this.state.user.bio;
|
||||||
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
|
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;
|
const data = res.data as LoginResponse;
|
||||||
UserService.Instance.login(data);
|
UserService.Instance.login(data);
|
||||||
this.state.user.bio = this.state.userSettingsForm.bio;
|
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.state.userSettingsLoading = false;
|
||||||
this.setState(this.state);
|
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 {
|
export enum SortType {
|
||||||
|
Active,
|
||||||
Hot,
|
Hot,
|
||||||
New,
|
New,
|
||||||
TopDay,
|
TopDay,
|
||||||
|
@ -112,6 +113,7 @@ export interface User {
|
||||||
preferred_username?: string;
|
preferred_username?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
banner?: string;
|
||||||
admin: boolean;
|
admin: boolean;
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
published: string;
|
published: string;
|
||||||
|
@ -134,7 +136,9 @@ export interface UserView {
|
||||||
id: number;
|
id: number;
|
||||||
actor_id: string;
|
actor_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
|
preferred_username?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
banner?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
matrix_user_id?: string;
|
matrix_user_id?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
|
@ -155,11 +159,13 @@ export interface CommunityUser {
|
||||||
user_actor_id: string;
|
user_actor_id: string;
|
||||||
user_local: boolean;
|
user_local: boolean;
|
||||||
user_name: string;
|
user_name: string;
|
||||||
|
user_preferred_username?: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
community_id: number;
|
community_id: number;
|
||||||
community_actor_id: string;
|
community_actor_id: string;
|
||||||
community_local: boolean;
|
community_local: boolean;
|
||||||
community_name: string;
|
community_name: string;
|
||||||
|
community_icon?: string;
|
||||||
published: string;
|
published: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,6 +175,8 @@ export interface Community {
|
||||||
local: boolean;
|
local: boolean;
|
||||||
name: string;
|
name: string;
|
||||||
title: string;
|
title: string;
|
||||||
|
icon?: string;
|
||||||
|
banner?: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
creator_id: number;
|
creator_id: number;
|
||||||
|
@ -181,6 +189,7 @@ export interface Community {
|
||||||
creator_local: boolean;
|
creator_local: boolean;
|
||||||
last_refreshed_at: string;
|
last_refreshed_at: string;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
creator_preferred_username?: string;
|
||||||
creator_avatar?: string;
|
creator_avatar?: string;
|
||||||
category_name: string;
|
category_name: string;
|
||||||
number_of_subscribers: number;
|
number_of_subscribers: number;
|
||||||
|
@ -215,11 +224,13 @@ export interface Post {
|
||||||
creator_actor_id: string;
|
creator_actor_id: string;
|
||||||
creator_local: boolean;
|
creator_local: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
creator_preferred_username?: string;
|
||||||
creator_published: string;
|
creator_published: string;
|
||||||
creator_avatar?: string;
|
creator_avatar?: string;
|
||||||
community_actor_id: string;
|
community_actor_id: string;
|
||||||
community_local: boolean;
|
community_local: boolean;
|
||||||
community_name: string;
|
community_name: string;
|
||||||
|
community_icon?: string;
|
||||||
community_removed: boolean;
|
community_removed: boolean;
|
||||||
community_deleted: boolean;
|
community_deleted: boolean;
|
||||||
community_nsfw: boolean;
|
community_nsfw: boolean;
|
||||||
|
@ -228,6 +239,7 @@ export interface Post {
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
hot_rank: number;
|
hot_rank: number;
|
||||||
|
hot_rank_active: number;
|
||||||
newest_activity_time: string;
|
newest_activity_time: string;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
|
@ -255,17 +267,20 @@ export interface Comment {
|
||||||
community_actor_id: string;
|
community_actor_id: string;
|
||||||
community_local: boolean;
|
community_local: boolean;
|
||||||
community_name: string;
|
community_name: string;
|
||||||
|
community_icon?: string;
|
||||||
banned: boolean;
|
banned: boolean;
|
||||||
banned_from_community: boolean;
|
banned_from_community: boolean;
|
||||||
creator_actor_id: string;
|
creator_actor_id: string;
|
||||||
creator_local: boolean;
|
creator_local: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
creator_preferred_username?: string;
|
||||||
creator_avatar?: string;
|
creator_avatar?: string;
|
||||||
creator_published: string;
|
creator_published: string;
|
||||||
score: number;
|
score: number;
|
||||||
upvotes: number;
|
upvotes: number;
|
||||||
downvotes: number;
|
downvotes: number;
|
||||||
hot_rank: number;
|
hot_rank: number;
|
||||||
|
hot_rank_active: number;
|
||||||
user_id?: number;
|
user_id?: number;
|
||||||
my_vote?: number;
|
my_vote?: number;
|
||||||
subscribed?: number;
|
subscribed?: number;
|
||||||
|
@ -290,6 +305,7 @@ export interface Site {
|
||||||
published: string;
|
published: string;
|
||||||
updated?: string;
|
updated?: string;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
creator_preferred_username?: string;
|
||||||
number_of_users: number;
|
number_of_users: number;
|
||||||
number_of_posts: number;
|
number_of_posts: number;
|
||||||
number_of_comments: number;
|
number_of_comments: number;
|
||||||
|
@ -297,6 +313,8 @@ export interface Site {
|
||||||
enable_downvotes: boolean;
|
enable_downvotes: boolean;
|
||||||
open_registration: boolean;
|
open_registration: boolean;
|
||||||
enable_nsfw: boolean;
|
enable_nsfw: boolean;
|
||||||
|
icon?: string;
|
||||||
|
banner?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface PrivateMessage {
|
export interface PrivateMessage {
|
||||||
|
@ -311,10 +329,12 @@ export interface PrivateMessage {
|
||||||
ap_id: string;
|
ap_id: string;
|
||||||
local: boolean;
|
local: boolean;
|
||||||
creator_name: string;
|
creator_name: string;
|
||||||
|
creator_preferred_username?: string;
|
||||||
creator_avatar?: string;
|
creator_avatar?: string;
|
||||||
creator_actor_id: string;
|
creator_actor_id: string;
|
||||||
creator_local: boolean;
|
creator_local: boolean;
|
||||||
recipient_name: string;
|
recipient_name: string;
|
||||||
|
recipient_preferred_username?: string;
|
||||||
recipient_avatar?: string;
|
recipient_avatar?: string;
|
||||||
recipient_actor_id: string;
|
recipient_actor_id: string;
|
||||||
recipient_local: boolean;
|
recipient_local: boolean;
|
||||||
|
@ -596,6 +616,8 @@ export interface UserSettingsForm {
|
||||||
default_listing_type: ListingType;
|
default_listing_type: ListingType;
|
||||||
lang: string;
|
lang: string;
|
||||||
avatar?: string;
|
avatar?: string;
|
||||||
|
banner?: string;
|
||||||
|
preferred_username?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
bio?: string;
|
bio?: string;
|
||||||
matrix_user_id?: string;
|
matrix_user_id?: string;
|
||||||
|
@ -612,6 +634,8 @@ export interface CommunityForm {
|
||||||
edit_id?: number;
|
edit_id?: number;
|
||||||
title: string;
|
title: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
banner?: string;
|
||||||
category_id: number;
|
category_id: number;
|
||||||
nsfw: boolean;
|
nsfw: boolean;
|
||||||
auth?: string;
|
auth?: string;
|
||||||
|
@ -814,6 +838,8 @@ export interface CreatePostLikeForm {
|
||||||
export interface SiteForm {
|
export interface SiteForm {
|
||||||
name: string;
|
name: string;
|
||||||
description?: string;
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
banner?: string;
|
||||||
enable_downvotes: boolean;
|
enable_downvotes: boolean;
|
||||||
open_registration: boolean;
|
open_registration: boolean;
|
||||||
enable_nsfw: boolean;
|
enable_nsfw: boolean;
|
||||||
|
|
35
ui/src/utils.ts
vendored
35
ui/src/utils.ts
vendored
|
@ -58,11 +58,13 @@ import Toastify from 'toastify-js';
|
||||||
import tippy from 'tippy.js';
|
import tippy from 'tippy.js';
|
||||||
import moment from 'moment';
|
import moment from 'moment';
|
||||||
|
|
||||||
|
export const favIconUrl = '/static/assets/favicon.svg';
|
||||||
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
|
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
|
||||||
export const helpGuideUrl = '/docs/about_guide.html';
|
export const helpGuideUrl = '/docs/about_guide.html';
|
||||||
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
|
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
|
||||||
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
|
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
|
||||||
export const archiveUrl = 'https://archive.is';
|
export const archiveUrl = 'https://archive.is';
|
||||||
|
export const elementUrl = 'https://element.io/';
|
||||||
|
|
||||||
export const postRefetchSeconds: number = 60 * 1000;
|
export const postRefetchSeconds: number = 60 * 1000;
|
||||||
export const fetchLimit: number = 20;
|
export const fetchLimit: number = 20;
|
||||||
|
@ -273,6 +275,8 @@ export function routeSortTypeToEnum(sort: string): SortType {
|
||||||
return SortType.New;
|
return SortType.New;
|
||||||
} else if (sort == 'hot') {
|
} else if (sort == 'hot') {
|
||||||
return SortType.Hot;
|
return SortType.Hot;
|
||||||
|
} else if (sort == 'active') {
|
||||||
|
return SortType.Active;
|
||||||
} else if (sort == 'topday') {
|
} else if (sort == 'topday') {
|
||||||
return SortType.TopDay;
|
return SortType.TopDay;
|
||||||
} else if (sort == 'topweek') {
|
} else if (sort == 'topweek') {
|
||||||
|
@ -754,7 +758,7 @@ export function getSortTypeFromProps(props: any): SortType {
|
||||||
? routeSortTypeToEnum(props.match.params.sort)
|
? routeSortTypeToEnum(props.match.params.sort)
|
||||||
: UserService.Instance.user
|
: UserService.Instance.user
|
||||||
? UserService.Instance.user.default_sort_type
|
? UserService.Instance.user.default_sort_type
|
||||||
: SortType.Hot;
|
: SortType.Active;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getPageFromProps(props: any): number {
|
export function getPageFromProps(props: any): number {
|
||||||
|
@ -905,7 +909,7 @@ function convertCommentSortType(sort: SortType): CommentSortType {
|
||||||
return CommentSortType.Top;
|
return CommentSortType.Top;
|
||||||
} else if (sort == SortType.New) {
|
} else if (sort == SortType.New) {
|
||||||
return CommentSortType.New;
|
return CommentSortType.New;
|
||||||
} else if (sort == SortType.Hot) {
|
} else if (sort == SortType.Hot || sort == SortType.Active) {
|
||||||
return CommentSortType.Hot;
|
return CommentSortType.Hot;
|
||||||
} else {
|
} else {
|
||||||
return CommentSortType.Hot;
|
return CommentSortType.Hot;
|
||||||
|
@ -948,6 +952,14 @@ export function postSort(
|
||||||
(communityType && +b.stickied - +a.stickied) ||
|
(communityType && +b.stickied - +a.stickied) ||
|
||||||
b.hot_rank - a.hot_rank
|
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 {
|
export function previewLines(text: string, lines: number = 3): string {
|
||||||
// Use lines * 2 because markdown requires 2 lines
|
// Use lines * 2 because markdown requires 2 lines
|
||||||
return text
|
return (
|
||||||
|
text
|
||||||
.split('\n')
|
.split('\n')
|
||||||
.slice(0, lines * 2)
|
.slice(0, lines * 2)
|
||||||
.join('\n');
|
.join('\n') + '...'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function hostname(url: string): string {
|
export function hostname(url: string): string {
|
||||||
|
@ -1008,3 +1022,16 @@ export function validTitle(title?: string): boolean {
|
||||||
|
|
||||||
return regex.test(title);
|
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",
|
"upload_image": "upload image",
|
||||||
"avatar": "Avatar",
|
"avatar": "Avatar",
|
||||||
"upload_avatar": "Upload Avatar",
|
"upload_avatar": "Upload Avatar",
|
||||||
|
"banner": "Banner",
|
||||||
|
"upload_banner": "Upload Banner",
|
||||||
|
"icon": "Icon",
|
||||||
|
"upload_icon": "Upload Icon",
|
||||||
"show_avatars": "Show Avatars",
|
"show_avatars": "Show Avatars",
|
||||||
"show_context": "Show context",
|
"show_context": "Show context",
|
||||||
"formatting_help": "formatting help",
|
"formatting_help": "formatting help",
|
||||||
|
@ -125,6 +129,7 @@
|
||||||
"sidebar": "Sidebar",
|
"sidebar": "Sidebar",
|
||||||
"sort_type": "Sort type",
|
"sort_type": "Sort type",
|
||||||
"hot": "Hot",
|
"hot": "Hot",
|
||||||
|
"active": "Active",
|
||||||
"new": "New",
|
"new": "New",
|
||||||
"old": "Old",
|
"old": "Old",
|
||||||
"top_day": "Top day",
|
"top_day": "Top day",
|
||||||
|
@ -231,7 +236,7 @@
|
||||||
"landing_0":
|
"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.",
|
"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.",
|
"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.",
|
"logged_in": "Logged in.",
|
||||||
"must_login": "You must <1>log in or register</1> to comment.",
|
"must_login": "You must <1>log in or register</1> to comment.",
|
||||||
"site_saved": "Site Saved.",
|
"site_saved": "Site Saved.",
|
||||||
|
|
21
ui/translations/it.json
vendored
21
ui/translations/it.json
vendored
|
@ -84,7 +84,7 @@
|
||||||
"category": "Categoria",
|
"category": "Categoria",
|
||||||
"subscribers": "Iscritti",
|
"subscribers": "Iscritti",
|
||||||
"both": "Entrambi",
|
"both": "Entrambi",
|
||||||
"saved": "Salvato",
|
"saved": "Salvati",
|
||||||
"unsubscribe": "Disiscriviti",
|
"unsubscribe": "Disiscriviti",
|
||||||
"subscribe": "Iscriviti",
|
"subscribe": "Iscriviti",
|
||||||
"subscribed": "Iscritto",
|
"subscribed": "Iscritto",
|
||||||
|
@ -205,7 +205,7 @@
|
||||||
"old_password": "Vecchia Password",
|
"old_password": "Vecchia Password",
|
||||||
"forgot_password": "password dimenticata",
|
"forgot_password": "password dimenticata",
|
||||||
"new_password": "Nuova Password",
|
"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",
|
"language": "Lingua",
|
||||||
"enable_downvotes": "Abilita voti negativi",
|
"enable_downvotes": "Abilita voti negativi",
|
||||||
"enable_nsfw": "Abilita NSFW",
|
"enable_nsfw": "Abilita NSFW",
|
||||||
|
@ -217,7 +217,7 @@
|
||||||
"downvotes_disabled": "Voti negativi disabilitati",
|
"downvotes_disabled": "Voti negativi disabilitati",
|
||||||
"post_title_too_long": "Titolo della pubblicazione troppo lungo.",
|
"post_title_too_long": "Titolo della pubblicazione troppo lungo.",
|
||||||
"email_already_exists": "Indirizzo email già presente.",
|
"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",
|
"support_on_open_collective": "Sostieni su OpenCollective",
|
||||||
"admin_settings": "Impostazioni per Admin",
|
"admin_settings": "Impostazioni per Admin",
|
||||||
"site_config": "Configurazione del sito",
|
"site_config": "Configurazione del sito",
|
||||||
|
@ -260,7 +260,18 @@
|
||||||
"what_is": "Cos'è",
|
"what_is": "Cos'è",
|
||||||
"must_login": "Devi <1>effettuare l'accesso o registrarti</1> per commentare.",
|
"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.",
|
"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 }}!",
|
"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