Time = time since submission (in hours)
Gravity = Decay gravity, 1.8 is default
```
-- For posts, in order to bring up active posts, it uses the latest comment time (limited to a max creation age of a month ago)
+- Lemmy uses the same `Rank` algorithm above, in two sorts: `Active`, and `Hot`.
+ - `Active` uses the post votes, and latest comment time (limited to two days).
+ - `Hot` uses the post votes, and the post published time.
- Use Max(1, score) to make sure all comments are affected by time decay.
- Add 3 to the score, so that everything that has less than 3 downvotes will seem new. Otherwise all new comments would stay at zero, near the bottom.
- The sign and abs of the score are necessary for dealing with the log of negative scores.
These go wherever there is a `sort` field. The available sort types are:
-- `Hot` - the hottest posts/communities, depending on votes, views, comments and publish date
+- `Active` - the hottest posts/communities, depending on votes, and newest comment publish date.
+- `Hot` - the hottest posts/communities, depending on votes and publish date.
- `New` - the newest posts/communities
- `TopDay` - the most upvoted posts/communities of the current day.
- `TopWeek` - the most upvoted posts/communities of the current week.
theme: String, // Default 'darkly'
default_sort_type: i16, // The Sort types from above, zero indexed as a number
default_listing_type: i16, // Post listing types are `All, Subscribed, Community`
- auth: String
+ lang: String,
+ avatar: Option<String>,
+ banner: Option<String>,
+ preferred_username: Option<String>,
+ email: Option<String>,
+ bio: Option<String>,
+ matrix_user_id: Option<String>,
+ new_password: Option<String>,
+ new_password_verify: Option<String>,
+ old_password: Option<String>,
+ show_avatars: bool,
+ send_notifications_to_email: bool,
+ auth: String,
}
}
```
data: {
name: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
auth: String
}
}
data: {
name: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
auth: String
}
}
name: String,
title: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
category_id: i32 ,
auth: String
}
edit_id: i32,
title: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
category_id: i32,
auth: String
}
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
- .set((
- deleted.eq(new_deleted),
- updated.eq(naive_now())
- ))
+ .set((deleted.eq(new_deleted), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
- .set((
- removed.eq(new_removed),
- updated.eq(naive_now())
- ))
+ .set((removed.eq(new_removed), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ banner: None,
+ icon: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub hot_rank_active: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub subscribed: Option<bool>,
SortType::Hot => query
.order_by(hot_rank.desc())
.then_order_by(published.desc()),
+ SortType::Active => query
+ .order_by(hot_rank_active.desc())
+ .then_order_by(published.desc()),
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(score.desc()),
SortType::TopYear => query
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Varchar>,
banned -> Bool,
banned_from_community -> Bool,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
creator_published -> Timestamp,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
subscribed -> Nullable<Bool>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
pub banned: bool,
pub banned_from_community: bool,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_avatar: Option<String>,
pub creator_published: chrono::NaiveDateTime,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub hot_rank_active: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub subscribed: Option<bool>,
}
query = match self.sort {
- // SortType::Hot => query.order_by(hot_rank.desc()),
+ // SortType::Hot => query.order_by(hot_rank.desc()), // TODO why is this commented
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(score.desc()),
SortType::TopYear => query
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
post_name: inserted_post.name.to_owned(),
community_id: inserted_community.id,
community_name: inserted_community.name.to_owned(),
+ community_icon: None,
parent_id: None,
removed: false,
deleted: false,
published: inserted_comment.published,
updated: None,
creator_name: inserted_user.name.to_owned(),
+ creator_preferred_username: None,
creator_published: inserted_user.published,
creator_avatar: None,
score: 1,
downvotes: 0,
hot_rank: 0,
+ hot_rank_active: 0,
upvotes: 1,
user_id: None,
my_vote: None,
post_name: inserted_post.name.to_owned(),
community_id: inserted_community.id,
community_name: inserted_community.name.to_owned(),
+ community_icon: None,
parent_id: None,
removed: false,
deleted: false,
published: inserted_comment.published,
updated: None,
creator_name: inserted_user.name.to_owned(),
+ creator_preferred_username: None,
creator_published: inserted_user.published,
creator_avatar: None,
score: 1,
downvotes: 0,
hot_rank: 0,
+ hot_rank_active: 0,
upvotes: 1,
user_id: Some(inserted_user.id),
my_vote: Some(1),
.list()
.unwrap();
read_comment_views_no_user[0].hot_rank = 0;
+ read_comment_views_no_user[0].hot_rank_active = 0;
let mut read_comment_views_with_user = CommentQueryBuilder::create(&conn)
.for_post_id(inserted_post.id)
.list()
.unwrap();
read_comment_views_with_user[0].hot_rank = 0;
+ read_comment_views_with_user[0].hot_rank_active = 0;
let like_removed = CommentLike::remove(&conn, &comment_like_form).unwrap();
let num_deleted = Comment::delete(&conn, inserted_comment.id).unwrap();
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
+ pub icon: Option<Option<String>>,
+ pub banner: Option<Option<String>>,
}
impl Crud<CommunityForm> for Community {
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
- .set((
- deleted.eq(new_deleted),
- updated.eq(naive_now())
- ))
+ .set((deleted.eq(new_deleted), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
- .set((
- removed.eq(new_removed),
- updated.eq(naive_now())
- ))
+ .set((removed.eq(new_removed), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
private_key: None,
public_key: None,
last_refreshed_at: inserted_community.published,
+ icon: None,
+ banner: None,
};
let community_follower_form = CommunityFollowerForm {
id -> Int4,
name -> Varchar,
title -> Varchar,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
category_name -> Varchar,
number_of_subscribers -> BigInt,
id -> Int4,
name -> Varchar,
title -> Varchar,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
description -> Nullable<Text>,
category_id -> Int4,
creator_id -> Int4,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
category_name -> Varchar,
number_of_subscribers -> BigInt,
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
+ user_preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
}
}
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
+ user_preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
}
}
user_actor_id -> Text,
user_local -> Bool,
user_name -> Varchar,
+ user_preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
}
}
pub id: i32,
pub name: String,
pub title: String,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
pub description: Option<String>,
pub category_id: i32,
pub creator_id: i32,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_avatar: Option<String>,
pub category_name: String,
pub number_of_subscribers: i64,
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
+ pub user_preferred_username: Option<String>,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
}
impl CommunityModeratorView {
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
+ pub user_preferred_username: Option<String>,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
}
impl CommunityFollowerView {
pub user_actor_id: String,
pub user_local: bool,
pub user_name: String,
+ pub user_preferred_username: Option<String>,
pub avatar: Option<String>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
}
impl CommunityUserBanView {
#[derive(EnumString, ToString, Debug, Serialize, Deserialize)]
pub enum SortType {
+ Active,
Hot,
New,
TopDay,
EMAIL_REGEX.is_match(test)
}
+pub fn diesel_option_overwrite(opt: &Option<String>) -> Option<Option<String>> {
+ match opt {
+ // An empty string is an erase
+ Some(unwrapped) => {
+ if !unwrapped.eq("") {
+ Some(Some(unwrapped.to_owned()))
+ } else {
+ Some(None)
+ }
+ }
+ None => None,
+ }
+}
+
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^[a-zA-Z0-9.!#$%&’*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
- .set((
- deleted.eq(new_deleted),
- updated.eq(naive_now())
- ))
+ .set((deleted.eq(new_deleted), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
- .set((
- removed.eq(new_removed),
- updated.eq(naive_now())
- ))
+ .set((removed.eq(new_removed), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
banned -> Bool,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
community_removed -> Bool,
community_deleted -> Bool,
community_nsfw -> Bool,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
newest_activity_time -> Timestamp,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
creator_actor_id -> Text,
creator_local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Timestamp,
creator_avatar -> Nullable<Text>,
banned -> Bool,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
community_removed -> Bool,
community_deleted -> Bool,
community_nsfw -> Bool,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
newest_activity_time -> Timestamp,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
pub creator_actor_id: String,
pub creator_local: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_published: chrono::NaiveDateTime,
pub creator_avatar: Option<String>,
pub banned: bool,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
pub community_removed: bool,
pub community_deleted: bool,
pub community_nsfw: bool,
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub hot_rank_active: i32,
pub newest_activity_time: chrono::NaiveDateTime,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
}
query = match self.sort {
+ SortType::Active => query
+ .then_order_by(hot_rank_active.desc())
+ .then_order_by(published.desc()),
SortType::Hot => query
.then_order_by(hot_rank.desc())
.then_order_by(published.desc()),
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
updated: None,
admin: false,
banned: false,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
body: None,
creator_id: inserted_user.id,
creator_name: user_name.to_owned(),
+ creator_preferred_username: None,
creator_published: inserted_user.published,
creator_avatar: None,
banned: false,
locked: false,
stickied: false,
community_name: community_name.to_owned(),
+ community_icon: None,
community_removed: false,
community_deleted: false,
community_nsfw: false,
upvotes: 1,
downvotes: 0,
hot_rank: read_post_listing_no_user.hot_rank,
+ hot_rank_active: read_post_listing_no_user.hot_rank_active,
published: inserted_post.published,
newest_activity_time: inserted_post.published,
updated: None,
stickied: false,
creator_id: inserted_user.id,
creator_name: user_name,
+ creator_preferred_username: None,
creator_published: inserted_user.published,
creator_avatar: None,
banned: false,
banned_from_community: false,
community_id: inserted_community.id,
community_name,
+ community_icon: None,
community_removed: false,
community_deleted: false,
community_nsfw: false,
upvotes: 1,
downvotes: 0,
hot_rank: read_post_listing_with_user.hot_rank,
+ hot_rank_active: read_post_listing_with_user.hot_rank_active,
published: inserted_post.published,
newest_activity_time: inserted_post.published,
updated: None,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
ap_id -> Text,
local -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
creator_actor_id -> Text,
creator_local -> Bool,
recipient_name -> Varchar,
+ recipient_preferred_username -> Nullable<Varchar>,
recipient_avatar -> Nullable<Text>,
recipient_actor_id -> Text,
recipient_local -> Bool,
pub ap_id: String,
pub local: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_avatar: Option<String>,
pub creator_actor_id: String,
pub creator_local: bool,
pub recipient_name: String,
+ pub recipient_preferred_username: Option<String>,
pub recipient_avatar: Option<String>,
pub recipient_actor_id: String,
pub recipient_local: bool,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
+ community_icon -> Nullable<Text>,
banned -> Nullable<Bool>,
banned_from_community -> Nullable<Bool>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>,
score -> Nullable<Int8>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
+ hot_rank_active -> Nullable<Int4>,
}
}
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
}
}
id -> Int4,
name -> Nullable<Varchar>,
title -> Nullable<Varchar>,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
description -> Nullable<Text>,
category_id -> Nullable<Int4>,
creator_id -> Nullable<Int4>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
category_name -> Nullable<Varchar>,
number_of_subscribers -> Nullable<Int8>,
creator_actor_id -> Nullable<Varchar>,
creator_local -> Nullable<Bool>,
creator_name -> Nullable<Varchar>,
+ creator_preferred_username -> Nullable<Varchar>,
creator_published -> Nullable<Timestamp>,
creator_avatar -> Nullable<Text>,
banned -> Nullable<Bool>,
community_actor_id -> Nullable<Varchar>,
community_local -> Nullable<Bool>,
community_name -> Nullable<Varchar>,
+ community_icon -> Nullable<Text>,
community_removed -> Nullable<Bool>,
community_deleted -> Nullable<Bool>,
community_nsfw -> Nullable<Bool>,
upvotes -> Nullable<Int8>,
downvotes -> Nullable<Int8>,
hot_rank -> Nullable<Int4>,
+ hot_rank_active -> Nullable<Int4>,
newest_activity_time -> Nullable<Timestamp>,
}
}
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
}
}
private_key -> Nullable<Text>,
public_key -> Nullable<Text>,
last_refreshed_at -> Timestamp,
+ banner -> Nullable<Text>,
}
}
id -> Int4,
actor_id -> Nullable<Varchar>,
name -> Nullable<Varchar>,
+ preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
+ banner -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
bio -> Nullable<Text>,
-use crate::{schema::site, Crud};
+use crate::{naive_now, schema::site, Crud};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
}
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize)]
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
+ // when you want to null out a column, you have to send Some(None)), since sending None means you just don't want to update that column.
+ pub icon: Option<Option<String>>,
+ pub banner: Option<Option<String>>,
}
impl Crud<SiteForm> for Site {
.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)
+ }
+}
enable_downvotes -> Bool,
open_registration -> Bool,
enable_nsfw -> Bool,
+ icon -> Nullable<Text>,
+ banner -> Nullable<Text>,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
number_of_users -> BigInt,
number_of_posts -> BigInt,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_avatar: Option<String>,
pub number_of_users: i64,
pub number_of_posts: i64,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: chrono::NaiveDateTime,
+ pub banner: Option<String>,
}
#[derive(Insertable, AsChangeset, Clone, Debug)]
pub admin: bool,
pub banned: bool,
pub email: Option<String>,
- pub avatar: Option<String>,
+ pub avatar: Option<Option<String>>,
pub updated: Option<chrono::NaiveDateTime>,
pub show_nsfw: bool,
pub theme: String,
pub private_key: Option<String>,
pub public_key: Option<String>,
pub last_refreshed_at: Option<chrono::NaiveDateTime>,
+ pub banner: Option<Option<String>>,
}
impl Crud<UserForm> for User_ {
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
published: inserted_user.published,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
email: None,
matrix_user_id: None,
avatar: None,
+ banner: None,
admin: false,
banned: false,
updated: None,
public_key: None,
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
let inserted_community = Community::create(&conn, &new_community).unwrap();
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
community_actor_id -> Text,
community_local -> Bool,
community_name -> Varchar,
+ community_icon -> Nullable<Text>,
banned -> Bool,
banned_from_community -> Bool,
creator_name -> Varchar,
+ creator_preferred_username -> Nullable<Varchar>,
creator_avatar -> Nullable<Text>,
score -> BigInt,
upvotes -> BigInt,
downvotes -> BigInt,
hot_rank -> Int4,
+ hot_rank_active -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
saved -> Nullable<Bool>,
pub community_actor_id: String,
pub community_local: bool,
pub community_name: String,
+ pub community_icon: Option<String>,
pub banned: bool,
pub banned_from_community: bool,
pub creator_name: String,
+ pub creator_preferred_username: Option<String>,
pub creator_avatar: Option<String>,
pub score: i64,
pub upvotes: i64,
pub downvotes: i64,
pub hot_rank: i32,
+ pub hot_rank_active: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
pub saved: Option<bool>,
SortType::Hot => query
.order_by(hot_rank.desc())
.then_order_by(published.desc()),
+ SortType::Active => query
+ .order_by(hot_rank_active.desc())
+ .then_order_by(published.desc()),
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(score.desc()),
SortType::TopYear => query
id -> Int4,
actor_id -> Text,
name -> Varchar,
+ preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
+ banner -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
bio -> Nullable<Text>,
id -> Int4,
actor_id -> Text,
name -> Varchar,
+ preferred_username -> Nullable<Varchar>,
avatar -> Nullable<Text>,
+ banner -> Nullable<Text>,
email -> Nullable<Text>,
matrix_user_id -> Nullable<Text>,
bio -> Nullable<Text>,
pub id: i32,
pub actor_id: String,
pub name: String,
+ pub preferred_username: Option<String>,
pub avatar: Option<String>,
+ pub banner: Option<String>,
pub email: Option<String>, // TODO this shouldn't be in this view
pub matrix_user_id: Option<String>,
pub bio: Option<String>,
SortType::Hot => query
.order_by(comment_score.desc())
.then_order_by(published.desc()),
+ SortType::Active => query
+ .order_by(comment_score.desc())
+ .then_order_by(published.desc()),
SortType::New => query.order_by(published.desc()),
SortType::TopAll => query.order_by(comment_score.desc()),
SortType::TopYear => query
id,
actor_id,
name,
+ preferred_username,
avatar,
+ banner,
"".into_sql::<Nullable<Text>>(),
matrix_user_id,
bio,
id,
actor_id,
name,
+ preferred_username,
avatar,
+ banner,
"".into_sql::<Nullable<Text>>(),
matrix_user_id,
bio,
--- /dev/null
+-- 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 $$;
--- /dev/null
+-- 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 $$;
},
DbPool,
};
-use lemmy_db::{naive_now, Bannable, Crud, Followable, Joinable, SortType};
+use lemmy_db::{
+ diesel_option_overwrite,
+ naive_now,
+ Bannable,
+ Crud,
+ Followable,
+ Joinable,
+ SortType,
+};
use lemmy_utils::{
generate_actor_keypair,
is_valid_community_name,
name: String,
title: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
category_id: i32,
nsfw: bool,
auth: String,
pub edit_id: i32,
title: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
category_id: i32,
nsfw: bool,
auth: String,
name: data.name.to_owned(),
title: data.title.to_owned(),
description: data.description.to_owned(),
+ icon: Some(data.icon.to_owned()),
+ banner: Some(data.banner.to_owned()),
category_id: data.category_id,
creator_id: user.id,
removed: None,
let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
+ let icon = diesel_option_overwrite(&data.icon);
+ let banner = diesel_option_overwrite(&data.banner);
+
let community_form = CommunityForm {
name: read_community.name,
title: data.title.to_owned(),
description: data.description.to_owned(),
+ icon,
+ banner,
category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id,
removed: Some(read_community.removed),
category::*,
comment_view::*,
community_view::*,
+ diesel_option_overwrite,
moderator::*,
moderator_views::*,
naive_now,
pub struct CreateSite {
pub name: String,
pub description: Option<String>,
+ pub icon: Option<String>,
+ pub banner: Option<String>,
pub enable_downvotes: bool,
pub open_registration: bool,
pub enable_nsfw: bool,
pub struct EditSite {
name: String,
description: Option<String>,
+ icon: Option<String>,
+ banner: Option<String>,
enable_downvotes: bool,
open_registration: bool,
enable_nsfw: bool,
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
+ icon: Some(data.icon.to_owned()),
+ banner: Some(data.banner.to_owned()),
creator_id: user.id,
enable_downvotes: data.enable_downvotes,
open_registration: data.open_registration,
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
+ let icon = diesel_option_overwrite(&data.icon);
+ let banner = diesel_option_overwrite(&data.banner);
+
let site_form = SiteForm {
name: data.name.to_owned(),
description: data.description.to_owned(),
+ icon,
+ banner,
creator_id: found_site.creator_id,
updated: Some(naive_now()),
enable_downvotes: data.enable_downvotes,
let create_site = CreateSite {
name: setup.site_name.to_owned(),
description: None,
+ icon: None,
+ banner: None,
enable_downvotes: true,
open_registration: true,
enable_nsfw: true,
return Err(APIError::err("not_an_admin").into());
}
- let site_form = SiteForm {
- name: read_site.name,
- description: read_site.description,
- creator_id: data.user_id,
- updated: Some(naive_now()),
- enable_downvotes: read_site.enable_downvotes,
- open_registration: read_site.open_registration,
- enable_nsfw: read_site.enable_nsfw,
- };
-
- let update_site = move |conn: &'_ _| Site::update(conn, 1, &site_form);
- if blocking(pool, update_site).await?.is_err() {
+ let new_creator_id = data.user_id;
+ let transfer_site = move |conn: &'_ _| Site::transfer(conn, new_creator_id);
+ if blocking(pool, transfer_site).await?.is_err() {
return Err(APIError::err("couldnt_update_site").into());
};
comment_view::*,
community::*,
community_view::*,
+ diesel_option_overwrite,
moderator::*,
naive_now,
password_reset_request::*,
default_listing_type: i16,
lang: String,
avatar: Option<String>,
+ banner: Option<String>,
+ preferred_username: Option<String>,
email: Option<String>,
bio: Option<String>,
matrix_user_id: Option<String>,
email: data.email.to_owned(),
matrix_user_id: None,
avatar: None,
+ banner: None,
password_encrypted: data.password.to_owned(),
preferred_username: None,
updated: None,
banned: false,
show_nsfw: data.show_nsfw,
theme: "darkly".into(),
- default_sort_type: SortType::Hot as i16,
+ default_sort_type: SortType::Active as i16,
default_listing_type: ListingType::Subscribed as i16,
lang: "browser".into(),
show_avatars: true,
public_key: Some(main_community_keypair.public_key),
last_refreshed_at: None,
published: None,
+ icon: None,
+ banner: None,
};
blocking(pool, move |conn| Community::create(conn, &community_form)).await??
}
None => read_user.bio,
};
- let avatar = match &data.avatar {
- Some(avatar) => Some(avatar.to_owned()),
- None => read_user.avatar,
+ let avatar = diesel_option_overwrite(&data.avatar);
+ let banner = diesel_option_overwrite(&data.banner);
+
+ // The DB constraint should stop too many characters
+ let preferred_username = match &data.preferred_username {
+ Some(preferred_username) => Some(preferred_username.to_owned()),
+ None => read_user.preferred_username,
};
let password_encrypted = match &data.new_password {
email,
matrix_user_id: data.matrix_user_id.to_owned(),
avatar,
+ banner,
password_encrypted,
- preferred_username: read_user.preferred_username,
+ preferred_username,
updated: Some(naive_now()),
admin: read_user.admin,
banned: read_user.banned,
base::{AnyBase, BaseExt},
collection::{OrderedCollection, UnorderedCollection},
context,
- object::Tombstone,
+ object::{Image, Tombstone},
prelude::*,
public,
};
let creator = get_or_fetch_and_upsert_user(creator_uri, client, pool).await?;
+ let icon = match group.icon() {
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().unwrap().clone())
+ .unwrap()
+ .unwrap()
+ .url()
+ .unwrap()
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
+ None => None,
+ };
+
+ let banner = match group.image() {
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().unwrap().clone())
+ .unwrap()
+ .unwrap()
+ .url()
+ .unwrap()
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
+ None => None,
+ };
+
Ok(CommunityForm {
name: group
.inner
private_key: None,
public_key: Some(group.ext_two.to_owned().public_key.public_key_pem),
last_refreshed_at: Some(naive_now()),
+ icon,
+ banner,
})
}
}
private_key: community.private_key,
public_key: community.public_key,
last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
};
let community_id = community.id;
private_key: community.private_key,
public_key: community.public_key,
last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
};
let community_id = community.id;
private_key: community.private_key,
public_key: community.public_key,
last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
};
let community_id = community.id;
private_key: community.private_key,
public_key: community.public_key,
last_refreshed_at: None,
+ icon: Some(community.icon.to_owned()),
+ banner: Some(community.banner.to_owned()),
};
let community_id = community.id;
person.set_icon(image.into_any_base()?);
}
+ if let Some(banner_url) = &self.banner {
+ let mut image = Image::new();
+ image.set_url(banner_url.to_owned());
+ person.set_image(image.into_any_base()?);
+ }
+
if let Some(bio) = &self.bio {
person.set_summary(bio.to_owned());
}
/// Parse an ActivityPub person received from another instance into a Lemmy user.
async fn from_apub(person: &PersonExt, _: &Client, _: &DbPool) -> Result<Self, LemmyError> {
let avatar = match person.icon() {
- Some(any_image) => Image::from_any_base(any_image.as_one().unwrap().clone())
- .unwrap()
- .unwrap()
- .url()
- .unwrap()
- .as_single_xsd_any_uri()
- .map(|u| u.to_string()),
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().unwrap().clone())
+ .unwrap()
+ .unwrap()
+ .url()
+ .unwrap()
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
+ None => None,
+ };
+
+ let banner = match person.image() {
+ Some(any_image) => Some(
+ Image::from_any_base(any_image.as_one().unwrap().clone())
+ .unwrap()
+ .unwrap()
+ .url()
+ .unwrap()
+ .as_single_xsd_any_uri()
+ .map(|u| u.to_string()),
+ ),
None => None,
};
banned: false,
email: None,
avatar,
+ banner,
updated: person.updated().map(|u| u.to_owned().naive_local()),
show_nsfw: false,
theme: "".to_string(),
name: cuser.name.to_owned(),
email: cuser.email.to_owned(),
matrix_user_id: cuser.matrix_user_id.to_owned(),
- avatar: cuser.avatar.to_owned(),
+ avatar: Some(cuser.avatar.to_owned()),
+ banner: Some(cuser.banner.to_owned()),
password_encrypted: cuser.password_encrypted.to_owned(),
preferred_username: cuser.preferred_username.to_owned(),
updated: None,
public_key: Some(keypair.public_key),
last_refreshed_at: Some(naive_now()),
published: None,
+ icon: Some(ccommunity.icon.to_owned()),
+ banner: Some(ccommunity.banner.to_owned()),
};
Community::update(&conn, ccommunity.id, &form)?;
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;
+}
let form: UserSettingsForm = {
show_nsfw: true,
theme: 'darkly',
- default_sort_type: SortType.Hot,
+ default_sort_type: SortType.Active,
default_listing_type: ListingType.All,
lang: 'en',
show_avatars: true,
<UserListing
user={{
name: admin.name,
+ preferred_username: admin.preferred_username,
avatar: admin.avatar,
id: admin.id,
local: admin.local,
<UserListing
user={{
name: banned.name,
+ preferred_username: banned.preferred_username,
avatar: banned.avatar,
id: banned.id,
local: banned.local,
--- /dev/null
+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>
+ );
+ }
+}
<UserListing
user={{
name: node.comment.creator_name,
+ preferred_username: node.comment.creator_preferred_username,
avatar: node.comment.creator_avatar,
id: node.comment.creator_id,
local: node.comment.creator_local,
id: node.comment.community_id,
local: node.comment.community_local,
actor_id: node.comment.community_actor_id,
+ icon: node.comment.community_icon,
}}
/>
<span class="mx-2">•</span>
<thead class="pointer">
<tr>
<th>{i18n.t('name')}</th>
- <th class="d-none d-lg-table-cell">{i18n.t('title')}</th>
<th>{i18n.t('category')}</th>
<th class="text-right">{i18n.t('subscribers')}</th>
<th class="text-right d-none d-lg-table-cell">
<td>
<CommunityLink community={community} />
</td>
- <td class="d-none d-lg-table-cell">{community.title}</td>
<td>{community.category_name}</td>
<td class="text-right">
{community.number_of_subscribers}
import { Community } from '../interfaces';
import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
interface CommunityFormProps {
community?: Community; // If a community is given, that means this is an edit
title: null,
category_id: null,
nsfw: false,
+ icon: null,
+ banner: null,
},
categories: [],
loading: false,
this
);
+ this.handleIconUpload = this.handleIconUpload.bind(this);
+ this.handleIconRemove = this.handleIconRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
if (this.props.community) {
this.state.communityForm = {
name: this.props.community.name,
description: this.props.community.description,
edit_id: this.props.community.id,
nsfw: this.props.community.nsfw,
+ icon: this.props.community.icon,
+ banner: this.props.community.banner,
auth: null,
};
}
/>
</div>
</div>
+ <div class="form-group">
+ <label>{i18n.t('icon')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_icon')}
+ imageSrc={this.state.communityForm.icon}
+ onUpload={this.handleIconUpload}
+ onRemove={this.handleIconRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.communityForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
+ </div>
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor={this.id}>
{i18n.t('sidebar')}
i.props.onCancel();
}
+ handleIconUpload(url: string) {
+ this.state.communityForm.icon = url;
+ this.setState(this.state);
+ }
+
+ handleIconRemove() {
+ this.state.communityForm.icon = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.communityForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.communityForm.banner = '';
+ this.setState(this.state);
+ }
+
parseMessage(msg: WebSocketJsonResponse) {
let res = wsJsonToRes(msg);
console.log(msg);
let data = res.data as CommunityResponse;
this.state.loading = false;
this.props.onCreate(data.community);
- }
- // TODO is this necessary
- else if (res.op == UserOperation.EditCommunity) {
+ } else if (res.op == UserOperation.EditCommunity) {
let data = res.data as CommunityResponse;
this.state.loading = false;
this.props.onEdit(data.community);
import { Component } from 'inferno';
import { Link } from 'inferno-router';
import { Community } from '../interfaces';
-import { hostname } from '../utils';
+import { hostname, pictrsAvatarThumbnail, showAvatars } from '../utils';
interface CommunityOther {
name: string;
id?: number; // Necessary if its federated
+ icon?: string;
local?: boolean;
actor_id?: string;
}
interface CommunityLinkProps {
community: Community | CommunityOther;
realLink?: boolean;
+ useApubName?: boolean;
+ muted?: boolean;
+ hideAvatar?: boolean;
}
export class CommunityLink extends Component<CommunityLinkProps, any> {
? `/community/${community.id}`
: community.actor_id;
}
- return <Link to={link}>{name_}</Link>;
+
+ let apubName = `!${name_}`;
+ let displayName = this.props.useApubName ? apubName : name_;
+ return (
+ <Link
+ title={apubName}
+ className={`${this.props.muted ? 'text-muted' : ''}`}
+ to={link}
+ >
+ {!this.props.hideAvatar && community.icon && showAvatars() && (
+ <img
+ style="width: 2rem; height: 2rem;"
+ src={pictrsAvatarThumbnail(community.icon)}
+ class="rounded-circle mr-2"
+ />
+ )}
+ <span>{displayName}</span>
+ </Link>
+ );
}
}
import { SortSelect } from './sort-select';
import { DataTypeSelect } from './data-type-select';
import { Sidebar } from './sidebar';
+import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
import {
wsJsonToRes,
fetchLimit,
editPostFindRes,
commentsToFlatNodes,
setupTippy,
+ favIconUrl,
} from '../utils';
import { i18n } from '../i18next';
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ creator_preferred_username: undefined,
},
};
}
}
+ get favIcon(): string {
+ return this.state.community.icon
+ ? this.state.community.icon
+ : this.state.site.icon
+ ? this.state.site.icon
+ : favIconUrl;
+ }
+
render() {
return (
<div class="container">
- <Helmet title={this.documentTitle} />
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
) : (
<div class="row">
<div class="col-12 col-md-8">
+ {this.communityInfo()}
{this.selects()}
{this.listings()}
{this.paginator()}
);
}
+ communityInfo() {
+ return (
+ <div>
+ <BannerIconHeader
+ banner={this.state.community.banner}
+ icon={this.state.community.icon}
+ />
+ <h5 class="mb-0">{this.state.community.title}</h5>
+ <CommunityLink
+ community={this.state.community}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ <hr />
+ </div>
+ );
+ }
+
selects() {
return (
<div class="mb-3">
--- /dev/null
+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();
+ }
+}
import { SiteForm } from './site-form';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
import {
wsJsonToRes,
repoUrl,
editPostFindRes,
commentsToFlatNodes,
setupTippy,
+ favIconUrl,
} from '../utils';
import { i18n } from '../i18next';
import { T } from 'inferno-i18next';
enable_downvotes: null,
open_registration: null,
enable_nsfw: null,
+ icon: null,
+ banner: null,
+ creator_preferred_username: null,
},
admins: [],
banned: [],
}
}
+ get favIcon(): string {
+ return this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
render() {
return (
<div class="container">
- <Helmet title={this.documentTitle} />
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
<div class="row">
<main role="main" class="col-12 col-md-8">
{this.posts()}
<div>
<div class="card bg-transparent border-secondary mb-3">
<div class="card-header bg-transparent border-secondary">
- {this.siteName()}
- {this.adminButtons()}
+ <div class="mb-2">
+ {this.siteName()}
+ {this.adminButtons()}
+ </div>
+ <BannerIconHeader banner={this.state.siteRes.site.banner} />
</div>
<div class="card-body">
{this.trendingCommunities()}
id: community.community_id,
local: community.community_local,
actor_id: community.community_actor_id,
+ icon: community.community_icon,
}}
/>
</li>
<UserListing
user={{
name: admin.name,
+ preferred_username: admin.preferred_username,
avatar: admin.avatar,
local: admin.local,
actor_id: admin.actor_id,
Comment,
CommentResponse,
PrivateMessage,
- UserView,
PrivateMessageResponse,
WebSocketJsonResponse,
} from '../interfaces';
mentions: Array<Comment>;
messages: Array<PrivateMessage>;
unreadCount: number;
- siteName: string;
- version: string;
- admins: Array<UserView>;
searchParam: string;
toggleSearch: boolean;
siteLoading: boolean;
+ siteRes: GetSiteResponse;
+ onSiteBanner?(url: string): any;
}
export class Navbar extends Component<any, NavbarState> {
mentions: [],
messages: [],
expanded: false,
- siteName: undefined,
- version: undefined,
- admins: [],
+ siteRes: {
+ site: {
+ id: null,
+ name: null,
+ creator_id: null,
+ creator_name: null,
+ published: null,
+ number_of_users: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ number_of_communities: null,
+ enable_downvotes: null,
+ open_registration: null,
+ enable_nsfw: null,
+ icon: null,
+ banner: null,
+ creator_preferred_username: null,
+ },
+ my_user: null,
+ admins: [],
+ banned: [],
+ online: null,
+ version: null,
+ },
searchParam: '',
toggleSearch: false,
siteLoading: true,
// TODO class active corresponding to current page
navbar() {
+ let user = UserService.Instance.user;
return (
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
<div class="container">
{!this.state.siteLoading ? (
- <Link title={this.state.version} class="navbar-brand" to="/">
- {this.state.siteName}
+ <Link
+ title={this.state.siteRes.version}
+ class="d-flex align-items-center navbar-brand mr-1"
+ to="/"
+ >
+ {this.state.siteRes.site.icon && showAvatars() && (
+ <img
+ src={pictrsAvatarThumbnail(this.state.siteRes.site.icon)}
+ height="32"
+ width="32"
+ class="rounded-circle mr-2"
+ />
+ )}
+ {this.state.siteRes.site.name}
</Link>
) : (
<div class="navbar-item">
<use xlinkHref="#icon-bell"></use>
</svg>
{this.state.unreadCount > 0 && (
- <span class="ml-1 badge badge-light">
+ <span class="mx-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
</Link>
)}
<button
- class="navbar-toggler border-0"
+ class="navbar-toggler border-0 p-1"
type="button"
aria-label="menu"
onClick={linkEvent(this, this.expandNavbar)}
!this.state.expanded && 'collapse'
} navbar-collapse`}
>
- <ul class="navbar-nav my-2 mr-auto">
+ <ul class="ml-3 navbar-nav my-2 mr-auto">
<li class="nav-item">
<Link
class="nav-link"
</Link>
</li>
</ul>
+ <ul class="navbar-nav my-2">
+ {this.canAdmin && (
+ <li className="nav-item">
+ <Link
+ class="nav-link"
+ to={`/admin`}
+ title={i18n.t('admin_settings')}
+ >
+ <svg class="icon">
+ <use xlinkHref="#icon-settings"></use>
+ </svg>
+ </Link>
+ </li>
+ )}
+ </ul>
{!this.context.router.history.location.pathname.match(
/^\/search/
) && (
<button
name="search-btn"
onClick={linkEvent(this, this.handleSearchBtn)}
- class="btn btn-link"
+ class="px-1 btn btn-link"
style="color: var(--gray)"
>
<svg class="icon">
</button>
</form>
)}
- <ul class="navbar-nav my-2">
- {this.canAdmin && (
- <li className="nav-item">
- <Link
- class="nav-link"
- to={`/admin`}
- title={i18n.t('admin_settings')}
- >
- <svg class="icon">
- <use xlinkHref="#icon-settings"></use>
- </svg>
- </Link>
- </li>
- )}
- </ul>
{this.state.isLoggedIn ? (
<>
<ul class="navbar-nav my-2">
<li className="nav-item">
<Link
class="nav-link"
- to={`/u/${UserService.Instance.user.name}`}
+ to={`/u/${user.name}`}
title={i18n.t('settings')}
>
<span>
- {UserService.Instance.user.avatar &&
- showAvatars() && (
- <img
- src={pictrsAvatarThumbnail(
- UserService.Instance.user.avatar
- )}
- height="32"
- width="32"
- class="rounded-circle mr-2"
- />
- )}
- {UserService.Instance.user.name}
+ {user.avatar && showAvatars() && (
+ <img
+ src={pictrsAvatarThumbnail(user.avatar)}
+ height="32"
+ width="32"
+ class="rounded-circle mr-2"
+ />
+ )}
+ {user.preferred_username
+ ? user.preferred_username
+ : user.name}
</span>
</Link>
</li>
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
- if (data.site && !this.state.siteName) {
- this.state.siteName = data.site.name;
- this.state.version = data.version;
- this.state.admins = data.admins;
- }
+ this.state.siteRes = data;
// The login
if (data.my_user) {
get canAdmin(): boolean {
return (
UserService.Instance.user &&
- this.state.admins.map(a => a.id).includes(UserService.Instance.user.id)
+ this.state.siteRes.admins
+ .map(a => a.id)
+ .includes(UserService.Instance.user.id)
);
}
<UserListing
user={{
name: post.creator_name,
+ preferred_username: post.creator_preferred_username,
avatar: post.creator_avatar,
id: post.creator_id,
local: post.creator_local,
id: post.community_id,
local: post.community_local,
actor_id: post.community_actor_id,
+ icon: post.community_icon,
}}
/>
</span>
createPostLikeRes,
commentsToFlatNodes,
setupTippy,
+ favIconUrl,
} from '../utils';
import { PostListing } from './post-listing';
import { Sidebar } from './sidebar';
}
}
+ get favIcon(): string {
+ return this.state.post ? this.state.post.community_icon : favIconUrl;
+ }
+
render() {
return (
<div class="container">
- <Helmet title={this.documentTitle} />
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
admins={this.state.siteRes.admins}
online={this.state.online}
enableNsfw={this.state.siteRes.site.enable_nsfw}
+ showIcon
/>
</div>
);
<UserListing
user={{
name: this.state.recipient.name,
+ preferred_username: this.state.recipient
+ .preferred_username,
avatar: this.state.recipient.avatar,
id: this.state.recipient.id,
local: this.state.recipient.local,
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
import { MomentTime } from './moment-time';
import { PrivateMessageForm } from './private-message-form';
+import { UserListing, UserOther } from './user-listing';
import { i18n } from '../i18next';
interface PrivateMessageState {
render() {
let message = this.props.privateMessage;
+ let userOther: UserOther = this.mine
+ ? {
+ name: message.recipient_name,
+ preferred_username: message.recipient_preferred_username,
+ id: message.id,
+ avatar: message.recipient_avatar,
+ local: message.recipient_local,
+ actor_id: message.recipient_actor_id,
+ published: message.published,
+ }
+ : {
+ name: message.creator_name,
+ preferred_username: message.creator_preferred_username,
+ id: message.id,
+ avatar: message.creator_avatar,
+ local: message.creator_local,
+ actor_id: message.creator_actor_id,
+ published: message.published,
+ };
+
return (
<div class="border-top border-light">
<div>
{this.mine ? i18n.t('to') : i18n.t('from')}
</li>
<li className="list-inline-item">
- <Link
- className="text-body font-weight-bold"
- to={
- this.mine
- ? `/u/${message.recipient_name}`
- : `/u/${message.creator_name}`
- }
- >
- {(this.mine
- ? message.recipient_avatar
- : message.creator_avatar) &&
- showAvatars() && (
- <img
- height="32"
- width="32"
- src={pictrsAvatarThumbnail(
- this.mine
- ? message.recipient_avatar
- : message.creator_avatar
- )}
- class="rounded-circle mr-1"
- />
- )}
- <span>
- {this.mine ? message.recipient_name : message.creator_name}
- </span>
- </Link>
+ <UserListing user={userOther} />
</li>
<li className="list-inline-item">
<span>
<UserListing
user={{
name: (i.data as UserView).name,
+ preferred_username: (i.data as UserView)
+ .preferred_username,
avatar: (i.data as UserView).avatar,
}}
/>
UserView,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
-import { mdToHtml, getUnixTime } from '../utils';
+import { mdToHtml, getUnixTime, pictrsAvatarThumbnail } from '../utils';
import { CommunityForm } from './community-form';
import { UserListing } from './user-listing';
import { CommunityLink } from './community-link';
+import { BannerIconHeader } from './banner-icon-header';
import { i18n } from '../i18next';
interface SidebarProps {
admins: Array<UserView>;
online: number;
enableNsfw: boolean;
+ showIcon?: boolean;
}
interface SidebarState {
communityTitle() {
let community = this.props.community;
return (
- <h5 className="mb-2">
- <span>{community.title}</span>
- {community.removed && (
- <small className="ml-2 text-muted font-italic">
- {i18n.t('removed')}
- </small>
- )}
- {community.deleted && (
- <small className="ml-2 text-muted font-italic">
- {i18n.t('deleted')}
- </small>
- )}
- {community.nsfw && (
- <small className="ml-2 text-muted font-italic">
- {i18n.t('nsfw')}
- </small>
- )}
- </h5>
+ <div>
+ <h5 className="mb-0">
+ {this.props.showIcon && (
+ <BannerIconHeader icon={community.icon} banner={community.banner} />
+ )}
+ <span>{community.title}</span>
+ {community.removed && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('removed')}
+ </small>
+ )}
+ {community.deleted && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('deleted')}
+ </small>
+ )}
+ {community.nsfw && (
+ <small className="ml-2 text-muted font-italic">
+ {i18n.t('nsfw')}
+ </small>
+ )}
+ </h5>
+ <CommunityLink
+ community={community}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ </div>
);
}
<UserListing
user={{
name: mod.user_name,
+ preferred_username: mod.user_preferred_username,
avatar: mod.avatar,
id: mod.user_id,
local: mod.user_local,
import { Component, linkEvent } from 'inferno';
import { Prompt } from 'inferno-router';
import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
import { Site, SiteForm as SiteFormI } from '../interfaces';
import { WebSocketService } from '../services';
import { capitalizeFirstLetter, randomStr } from '../utils';
open_registration: true,
enable_nsfw: true,
name: null,
+ icon: null,
+ banner: null,
},
loading: false,
};
this
);
+ this.handleIconUpload = this.handleIconUpload.bind(this);
+ this.handleIconRemove = this.handleIconRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
if (this.props.site) {
this.state.siteForm = {
name: this.props.site.name,
enable_downvotes: this.props.site.enable_downvotes,
open_registration: this.props.site.open_registration,
enable_nsfw: this.props.site.enable_nsfw,
+ icon: this.props.site.icon,
+ banner: this.props.site.banner,
};
}
}
/>
</div>
</div>
+ <div class="form-group">
+ <label>{i18n.t('icon')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_icon')}
+ imageSrc={this.state.siteForm.icon}
+ onUpload={this.handleIconUpload}
+ onRemove={this.handleIconRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.siteForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
+ </div>
<div class="form-group row">
<label class="col-12 col-form-label" htmlFor={this.id}>
{i18n.t('sidebar')}
handleCancel(i: SiteForm) {
i.props.onCancel();
}
+
+ handleIconUpload(url: string) {
+ this.state.siteForm.icon = url;
+ this.setState(this.state);
+ }
+
+ handleIconRemove() {
+ this.state.siteForm.icon = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.siteForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.siteForm.banner = '';
+ this.setState(this.state);
+ }
}
>
<option disabled>{i18n.t('sort_type')}</option>
{!this.props.hideHot && (
- <option value={SortType.Hot}>{i18n.t('hot')}</option>
+ <>
+ <option value={SortType.Active}>{i18n.t('active')}</option>
+ <option value={SortType.Hot}>{i18n.t('hot')}</option>
+ </>
)}
<option value={SortType.New}>{i18n.t('new')}</option>
<option disabled>─────</option>
xmlnsXlink="http://www.w3.org/1999/xlink"
>
<defs>
+ <symbol id="icon-x" viewBox="0 0 24 24">
+ <path d="M5.293 6.707l5.293 5.293-5.293 5.293c-0.391 0.391-0.391 1.024 0 1.414s1.024 0.391 1.414 0l5.293-5.293 5.293 5.293c0.391 0.391 1.024 0.391 1.414 0s0.391-1.024 0-1.414l-5.293-5.293 5.293-5.293c0.391-0.391 0.391-1.024 0-1.414s-1.024-0.391-1.414 0l-5.293 5.293-5.293-5.293c-0.391-0.391-1.024-0.391-1.414 0s-0.391 1.024 0 1.414z"></path>
+ </symbol>
<symbol id="icon-refresh-cw" viewBox="0 0 24 24">
<path d="M4.453 9.334c0.737-2.083 2.247-3.669 4.096-4.552s4.032-1.059 6.114-0.322c1.186 0.42 2.206 1.088 2.983 1.88l2.83 2.66h-3.476c-0.552 0-1 0.448-1 1s0.448 1 1 1h5.997c0.005 0 0.009 0 0.014 0 0.137-0.001 0.268-0.031 0.386-0.082 0.119-0.051 0.229-0.126 0.324-0.225 0.012-0.013 0.024-0.026 0.036-0.039 0.075-0.087 0.133-0.183 0.173-0.285s0.064-0.211 0.069-0.326c0.001-0.015 0.001-0.029 0.001-0.043v-6c0-0.552-0.448-1-1-1s-1 0.448-1 1v3.689l-2.926-2.749c-0.992-1.010-2.271-1.843-3.743-2.364-2.603-0.921-5.335-0.699-7.643 0.402s-4.199 3.086-5.12 5.689c-0.185 0.52 0.088 1.091 0.608 1.276s1.092-0.088 1.276-0.609zM2 16.312l2.955 2.777c1.929 1.931 4.49 2.908 7.048 2.909s5.119-0.975 7.072-2.927c1.104-1.104 1.901-2.407 2.361-3.745 0.18-0.522-0.098-1.091-0.621-1.271s-1.091 0.098-1.271 0.621c-0.361 1.050-0.993 2.091-1.883 2.981-1.563 1.562-3.609 2.342-5.657 2.342s-4.094-0.782-5.679-2.366l-2.8-2.633h3.475c0.552 0 1-0.448 1-1s-0.448-1-1-1h-5.997c-0.005 0-0.009 0-0.014 0-0.137 0.001-0.268 0.031-0.386 0.082-0.119 0.051-0.229 0.126-0.324 0.225-0.012 0.013-0.024 0.026-0.036 0.039-0.075 0.087-0.133 0.183-0.173 0.285s-0.064 0.211-0.069 0.326c-0.001 0.015-0.001 0.029-0.001 0.043v6c0 0.552 0.448 1 1 1s1-0.448 1-1z"></path>
</symbol>
} from '../utils';
import { CakeDay } from './cake-day';
-interface UserOther {
+export interface UserOther {
name: string;
+ preferred_username?: string;
id?: number; // Necessary if its federated
avatar?: string;
local?: boolean;
interface UserListingProps {
user: UserView | UserOther;
realLink?: boolean;
+ useApubName?: boolean;
+ muted?: boolean;
+ hideAvatar?: boolean;
}
export class UserListing extends Component<UserListingProps, any> {
render() {
let user = this.props.user;
let local = user.local == null ? true : user.local;
- let name_: string, link: string;
+ let apubName: string, link: string;
if (local) {
- name_ = user.name;
+ apubName = `@${user.name}`;
link = `/u/${user.name}`;
} else {
- name_ = `${user.name}@${hostname(user.actor_id)}`;
+ apubName = `@${user.name}@${hostname(user.actor_id)}`;
link = !this.props.realLink ? `/user/${user.id}` : user.actor_id;
}
+ let displayName = this.props.useApubName
+ ? apubName
+ : user.preferred_username
+ ? user.preferred_username
+ : apubName;
+
return (
<>
- <Link className="text-info" to={link}>
- {user.avatar && showAvatars() && (
+ <Link
+ title={apubName}
+ className={this.props.muted ? 'text-muted' : 'text-info'}
+ to={link}
+ >
+ {!this.props.hideAvatar && user.avatar && showAvatars() && (
<img
style="width: 2rem; height: 2rem;"
src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>
)}
- <span>{name_}</span>
+ <span>{displayName}</span>
</Link>
- {isCakeDay(user.published) && <CakeDay creatorName={name_} />}
+ {isCakeDay(user.published) && <CakeDay creatorName={apubName} />}
</>
);
}
themes,
setTheme,
languages,
- showAvatars,
toast,
setupTippy,
getLanguage,
mdToHtml,
+ elementUrl,
+ favIconUrl,
} from '../utils';
import { UserListing } from './user-listing';
import { SortSelect } from './sort-select';
import moment from 'moment';
import { UserDetails } from './user-details';
import { MarkdownTextArea } from './markdown-textarea';
+import { ImageUploadForm } from './image-upload-form';
+import { BannerIconHeader } from './banner-icon-header';
interface UserState {
user: UserView;
sort: SortType;
page: number;
loading: boolean;
- avatarLoading: boolean;
userSettingsForm: UserSettingsForm;
userSettingsLoading: boolean;
deleteAccountLoading: boolean;
follows: [],
moderates: [],
loading: true,
- avatarLoading: false,
view: User.getViewFromProps(this.props.match.view),
sort: User.getSortTypeFromProps(this.props.match.sort),
page: User.getPageFromProps(this.props.match.page),
send_notifications_to_email: null,
auth: null,
bio: null,
+ preferred_username: null,
},
userSettingsLoading: null,
deleteAccountLoading: null,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
+ icon: undefined,
+ banner: undefined,
+ creator_preferred_username: undefined,
},
version: undefined,
},
this
);
+ this.handleAvatarUpload = this.handleAvatarUpload.bind(this);
+ this.handleAvatarRemove = this.handleAvatarRemove.bind(this);
+
+ this.handleBannerUpload = this.handleBannerUpload.bind(this);
+ this.handleBannerRemove = this.handleBannerRemove.bind(this);
+
this.state.user_id = Number(this.props.match.params.id) || null;
this.state.username = this.props.match.params.username;
}
}
+ get favIcon(): string {
+ return this.state.user.avatar
+ ? this.state.user.avatar
+ : this.state.siteRes.site.icon
+ ? this.state.siteRes.site.icon
+ : favIconUrl;
+ }
+
render() {
return (
<div class="container">
- <Helmet title={this.documentTitle} />
+ <Helmet title={this.documentTitle}>
+ <link
+ id="favicon"
+ rel="icon"
+ type="image/x-icon"
+ href={this.favIcon}
+ />
+ </Helmet>
<div class="row">
<div class="col-12 col-md-8">
- <h5>
- {this.state.user.avatar && showAvatars() && (
- <img
- height="80"
- width="80"
- src={this.state.user.avatar}
- class="rounded-circle mr-2"
- />
- )}
- <span>@{this.state.username}</span>
- </h5>
{this.state.loading ? (
<h5>
<svg class="icon icon-spinner spin">
</svg>
</h5>
) : (
- this.selects()
+ <>
+ {this.userInfo()}
+ <hr />
+ </>
)}
+ {!this.state.loading && this.selects()}
<UserDetails
user_id={this.state.user_id}
username={this.state.username}
{!this.state.loading && (
<div class="col-12 col-md-4">
- {this.userInfo()}
{this.isCurrentUser && this.userSettings()}
{this.moderates()}
{this.follows()}
userInfo() {
let user = this.state.user;
+
return (
<div>
- <div class="card bg-transparent border-secondary mb-3">
- <div class="card-body">
- <h5>
- <ul class="list-inline mb-0">
- <li className="list-inline-item">
- <UserListing user={user} realLink />
- </li>
- {user.banned && (
- <li className="list-inline-item badge badge-danger">
- {i18n.t('banned')}
- </li>
+ <BannerIconHeader
+ banner={this.state.user.banner}
+ icon={this.state.user.avatar}
+ />
+ <div class="mb-3">
+ <div class="">
+ <div class="mb-0 d-flex flex-wrap">
+ <div>
+ {user.preferred_username && (
+ <h5 class="mb-0">{user.preferred_username}</h5>
)}
- </ul>
- </h5>
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item">
+ <UserListing
+ user={user}
+ realLink
+ useApubName
+ muted
+ hideAvatar
+ />
+ </li>
+ {user.banned && (
+ <li className="list-inline-item badge badge-danger">
+ {i18n.t('banned')}
+ </li>
+ )}
+ </ul>
+ </div>
+ <div className="flex-grow-1 unselectable pointer mx-2"></div>
+ {this.isCurrentUser ? (
+ <button
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ onClick={linkEvent(this, this.handleLogoutClick)}
+ >
+ {i18n.t('logout')}
+ </button>
+ ) : (
+ <>
+ <a
+ className={`d-flex align-self-start btn btn-secondary ml-2 ${
+ !this.state.user.matrix_user_id && 'invisible'
+ }`}
+ target="_blank"
+ rel="noopener"
+ href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+ >
+ {i18n.t('send_secure_message')}
+ </a>
+ <Link
+ class="d-flex align-self-start btn btn-secondary ml-2"
+ to={`/create_private_message?recipient_id=${this.state.user.id}`}
+ >
+ {i18n.t('send_message')}
+ </Link>
+ </>
+ )}
+ </div>
{user.bio && (
<div className="d-flex align-items-center mb-2">
<div
/>
</div>
)}
- <div className="d-flex align-items-center mb-2">
+ <div>
+ <ul class="list-inline mb-2">
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_posts', { count: user.number_of_posts })}
+ </li>
+ <li className="list-inline-item badge badge-light">
+ {i18n.t('number_of_comments', {
+ count: user.number_of_comments,
+ })}
+ </li>
+ </ul>
+ </div>
+ <div class="text-muted">
+ {i18n.t('joined')} <MomentTime data={user} showAgo />
+ </div>
+ <div className="d-flex align-items-center text-muted mb-2">
<svg class="icon">
<use xlinkHref="#icon-cake"></use>
</svg>
{moment.utc(user.published).local().format('MMM DD, YYYY')}
</span>
</div>
- <div>
- {i18n.t('joined')} <MomentTime data={user} showAgo />
- </div>
- <div class="table-responsive mt-1">
- <table class="table table-bordered table-sm mt-2 mb-0">
- {/*
- <tr>
- <td class="text-center" colSpan={2}>
- {i18n.t('number_of_points', {
- count: user.post_score + user.comment_score,
- })}
- </td>
- </tr>
- */}
- <tr>
- {/*
- <td>
- {i18n.t('number_of_points', { count: user.post_score })}
- </td>
- */}
- <td>
- {i18n.t('number_of_posts', { count: user.number_of_posts })}
- </td>
- {/*
- </tr>
- <tr>
- <td>
- {i18n.t('number_of_points', { count: user.comment_score })}
- </td>
- */}
- <td>
- {i18n.t('number_of_comments', {
- count: user.number_of_comments,
- })}
- </td>
- </tr>
- </table>
- </div>
- {this.isCurrentUser ? (
- <button
- class="btn btn-block btn-secondary mt-3"
- onClick={linkEvent(this, this.handleLogoutClick)}
- >
- {i18n.t('logout')}
- </button>
- ) : (
- <>
- <a
- className={`btn btn-block btn-secondary mt-3 ${
- !this.state.user.matrix_user_id && 'disabled'
- }`}
- target="_blank"
- rel="noopener"
- href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
- >
- {i18n.t('send_secure_message')}
- </a>
- <Link
- class="btn btn-block btn-secondary mt-3"
- to={`/create_private_message?recipient_id=${this.state.user.id}`}
- >
- {i18n.t('send_message')}
- </Link>
- </>
- )}
</div>
</div>
</div>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
<div class="form-group">
<label>{i18n.t('avatar')}</label>
- <form class="d-inline">
- <label
- htmlFor="file-upload"
- class="pointer ml-4 text-muted small font-weight-bold"
- >
- {!this.checkSettingsAvatar ? (
- <span class="btn btn-secondary">
- {i18n.t('upload_avatar')}
- </span>
- ) : (
- <img
- height="80"
- width="80"
- src={this.state.userSettingsForm.avatar}
- class="rounded-circle"
- />
- )}
- </label>
- <input
- id="file-upload"
- type="file"
- accept="image/*,video/*"
- name="file"
- class="d-none"
- disabled={!UserService.Instance.user}
- onChange={linkEvent(this, this.handleImageUpload)}
- />
- </form>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_avatar')}
+ imageSrc={this.state.userSettingsForm.avatar}
+ onUpload={this.handleAvatarUpload}
+ onRemove={this.handleAvatarRemove}
+ rounded
+ />
+ </div>
+ <div class="form-group">
+ <label>{i18n.t('banner')}</label>
+ <ImageUploadForm
+ uploadTitle={i18n.t('upload_banner')}
+ imageSrc={this.state.userSettingsForm.banner}
+ onUpload={this.handleBannerUpload}
+ onRemove={this.handleBannerRemove}
+ />
</div>
- {this.checkSettingsAvatar && (
- <div class="form-group">
- <button
- class="btn btn-secondary btn-block"
- onClick={linkEvent(this, this.removeAvatar)}
- >
- {`${capitalizeFirstLetter(i18n.t('remove'))} ${i18n.t(
- 'avatar'
- )}`}
- </button>
- </div>
- )}
<div class="form-group">
<label>{i18n.t('language')}</label>
<select
/>
</form>
<div class="form-group row">
- <label class="col-lg-3 col-form-label" htmlFor="user-email">
- {i18n.t('email')}
+ <label class="col-lg-5 col-form-label">
+ {i18n.t('display_name')}
</label>
- <div class="col-lg-9">
+ <div class="col-lg-7">
<input
- type="email"
- id="user-email"
+ type="text"
class="form-control"
placeholder={i18n.t('optional')}
- value={this.state.userSettingsForm.email}
+ value={this.state.userSettingsForm.preferred_username}
onInput={linkEvent(
this,
- this.handleUserSettingsEmailChange
+ this.handleUserSettingsPreferredUsernameChange
)}
minLength={3}
+ maxLength={20}
/>
</div>
</div>
/>
</div>
</div>
+ <div class="form-group row">
+ <label class="col-lg-3 col-form-label" htmlFor="user-email">
+ {i18n.t('email')}
+ </label>
+ <div class="col-lg-9">
+ <input
+ type="email"
+ id="user-email"
+ class="form-control"
+ placeholder={i18n.t('optional')}
+ value={this.state.userSettingsForm.email}
+ onInput={linkEvent(
+ this,
+ this.handleUserSettingsEmailChange
+ )}
+ minLength={3}
+ />
+ </div>
+ </div>
<div class="form-group row">
<label class="col-lg-5 col-form-label">
- <a
- href="https://about.riot.im/"
- target="_blank"
- rel="noopener"
- >
+ <a href={elementUrl} target="_blank" rel="noopener">
{i18n.t('matrix_user_id')}
</a>
</label>
this.setState(this.state);
}
+ handleAvatarUpload(url: string) {
+ this.state.userSettingsForm.avatar = url;
+ this.setState(this.state);
+ }
+
+ handleAvatarRemove() {
+ this.state.userSettingsForm.avatar = '';
+ this.setState(this.state);
+ }
+
+ handleBannerUpload(url: string) {
+ this.state.userSettingsForm.banner = url;
+ this.setState(this.state);
+ }
+
+ handleBannerRemove() {
+ this.state.userSettingsForm.banner = '';
+ this.setState(this.state);
+ }
+
+ handleUserSettingsPreferredUsernameChange(i: User, event: any) {
+ i.state.userSettingsForm.preferred_username = event.target.value;
+ i.setState(i.state);
+ }
+
handleUserSettingsMatrixUserIdChange(i: User, event: any) {
i.state.userSettingsForm.matrix_user_id = event.target.value;
if (
i.setState(i.state);
}
- handleImageUpload(i: User, event: any) {
- event.preventDefault();
- let file = event.target.files[0];
- const imageUploadUrl = `/pictrs/image`;
- const formData = new FormData();
- formData.append('images[]', file);
-
- i.state.avatarLoading = true;
- i.setState(i.state);
-
- fetch(imageUploadUrl, {
- method: 'POST',
- body: formData,
- })
- .then(res => res.json())
- .then(res => {
- console.log('pictrs upload:');
- console.log(res);
- if (res.msg == 'ok') {
- let hash = res.files[0].file;
- let url = `${window.location.origin}/pictrs/image/${hash}`;
- i.state.userSettingsForm.avatar = url;
- i.state.avatarLoading = false;
- i.setState(i.state);
- } else {
- i.state.avatarLoading = false;
- i.setState(i.state);
- toast(JSON.stringify(res), 'danger');
- }
- })
- .catch(error => {
- i.state.avatarLoading = false;
- i.setState(i.state);
- toast(error, 'danger');
- });
- }
-
- removeAvatar(i: User, event: any) {
- event.preventDefault();
- i.state.userSettingsLoading = true;
- i.state.userSettingsForm.avatar = '';
- i.setState(i.state);
-
- WebSocketService.Instance.saveUserSettings(i.state.userSettingsForm);
- }
-
- get checkSettingsAvatar(): boolean {
- return (
- this.state.userSettingsForm.avatar &&
- this.state.userSettingsForm.avatar != ''
- );
- }
-
handleUserSettingsSubmit(i: User, event: any) {
event.preventDefault();
i.state.userSettingsLoading = true;
}
this.setState({
deleteAccountLoading: false,
- avatarLoading: false,
userSettingsLoading: false,
});
return;
UserService.Instance.user.default_listing_type;
this.state.userSettingsForm.lang = UserService.Instance.user.lang;
this.state.userSettingsForm.avatar = UserService.Instance.user.avatar;
+ this.state.userSettingsForm.banner = UserService.Instance.user.banner;
+ this.state.userSettingsForm.preferred_username =
+ UserService.Instance.user.preferred_username;
this.state.userSettingsForm.email = this.state.user.email;
this.state.userSettingsForm.bio = this.state.user.bio;
this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
const data = res.data as LoginResponse;
UserService.Instance.login(data);
this.state.user.bio = this.state.userSettingsForm.bio;
+ this.state.user.preferred_username = this.state.userSettingsForm.preferred_username;
+ this.state.user.banner = this.state.userSettingsForm.banner;
+ this.state.user.avatar = this.state.userSettingsForm.avatar;
this.state.userSettingsLoading = false;
this.setState(this.state);
}
export enum SortType {
+ Active,
Hot,
New,
TopDay,
preferred_username?: string;
email?: string;
avatar?: string;
+ banner?: string;
admin: boolean;
banned: boolean;
published: string;
id: number;
actor_id: string;
name: string;
+ preferred_username?: string;
avatar?: string;
+ banner?: string;
email?: string;
matrix_user_id?: string;
bio?: string;
user_actor_id: string;
user_local: boolean;
user_name: string;
+ user_preferred_username?: string;
avatar?: string;
community_id: number;
community_actor_id: string;
community_local: boolean;
community_name: string;
+ community_icon?: string;
published: string;
}
local: boolean;
name: string;
title: string;
+ icon?: string;
+ banner?: string;
description?: string;
category_id: number;
creator_id: number;
creator_local: boolean;
last_refreshed_at: string;
creator_name: string;
+ creator_preferred_username?: string;
creator_avatar?: string;
category_name: string;
number_of_subscribers: number;
creator_actor_id: string;
creator_local: boolean;
creator_name: string;
+ creator_preferred_username?: string;
creator_published: string;
creator_avatar?: string;
community_actor_id: string;
community_local: boolean;
community_name: string;
+ community_icon?: string;
community_removed: boolean;
community_deleted: boolean;
community_nsfw: boolean;
upvotes: number;
downvotes: number;
hot_rank: number;
+ hot_rank_active: number;
newest_activity_time: string;
user_id?: number;
my_vote?: number;
community_actor_id: string;
community_local: boolean;
community_name: string;
+ community_icon?: string;
banned: boolean;
banned_from_community: boolean;
creator_actor_id: string;
creator_local: boolean;
creator_name: string;
+ creator_preferred_username?: string;
creator_avatar?: string;
creator_published: string;
score: number;
upvotes: number;
downvotes: number;
hot_rank: number;
+ hot_rank_active: number;
user_id?: number;
my_vote?: number;
subscribed?: number;
published: string;
updated?: string;
creator_name: string;
+ creator_preferred_username?: string;
number_of_users: number;
number_of_posts: number;
number_of_comments: number;
enable_downvotes: boolean;
open_registration: boolean;
enable_nsfw: boolean;
+ icon?: string;
+ banner?: string;
}
export interface PrivateMessage {
ap_id: string;
local: boolean;
creator_name: string;
+ creator_preferred_username?: string;
creator_avatar?: string;
creator_actor_id: string;
creator_local: boolean;
recipient_name: string;
+ recipient_preferred_username?: string;
recipient_avatar?: string;
recipient_actor_id: string;
recipient_local: boolean;
default_listing_type: ListingType;
lang: string;
avatar?: string;
+ banner?: string;
+ preferred_username?: string;
email?: string;
bio?: string;
matrix_user_id?: string;
edit_id?: number;
title: string;
description?: string;
+ icon?: string;
+ banner?: string;
category_id: number;
nsfw: boolean;
auth?: string;
export interface SiteForm {
name: string;
description?: string;
+ icon?: string;
+ banner?: string;
enable_downvotes: boolean;
open_registration: boolean;
enable_nsfw: boolean;
import tippy from 'tippy.js';
import moment from 'moment';
+export const favIconUrl = '/static/assets/favicon.svg';
export const repoUrl = 'https://github.com/LemmyNet/lemmy';
export const helpGuideUrl = '/docs/about_guide.html';
export const markdownHelpUrl = `${helpGuideUrl}#markdown-guide`;
export const sortingHelpUrl = `${helpGuideUrl}#sorting`;
export const archiveUrl = 'https://archive.is';
+export const elementUrl = 'https://element.io/';
export const postRefetchSeconds: number = 60 * 1000;
export const fetchLimit: number = 20;
return SortType.New;
} else if (sort == 'hot') {
return SortType.Hot;
+ } else if (sort == 'active') {
+ return SortType.Active;
} else if (sort == 'topday') {
return SortType.TopDay;
} else if (sort == 'topweek') {
? routeSortTypeToEnum(props.match.params.sort)
: UserService.Instance.user
? UserService.Instance.user.default_sort_type
- : SortType.Hot;
+ : SortType.Active;
}
export function getPageFromProps(props: any): number {
return CommentSortType.Top;
} else if (sort == SortType.New) {
return CommentSortType.New;
- } else if (sort == SortType.Hot) {
+ } else if (sort == SortType.Hot || sort == SortType.Active) {
return CommentSortType.Hot;
} else {
return CommentSortType.Hot;
(communityType && +b.stickied - +a.stickied) ||
b.hot_rank - a.hot_rank
);
+ } else if (sort == SortType.Active) {
+ posts.sort(
+ (a, b) =>
+ +a.removed - +b.removed ||
+ +a.deleted - +b.deleted ||
+ (communityType && +b.stickied - +a.stickied) ||
+ b.hot_rank_active - a.hot_rank_active
+ );
}
}
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; \
+ `;
+}
"upload_image": "upload image",
"avatar": "Avatar",
"upload_avatar": "Upload Avatar",
+ "banner": "Banner",
+ "upload_banner": "Upload Banner",
+ "icon": "Icon",
+ "upload_icon": "Upload Icon",
"show_avatars": "Show Avatars",
"show_context": "Show context",
"formatting_help": "formatting help",
"sidebar": "Sidebar",
"sort_type": "Sort type",
"hot": "Hot",
+ "active": "Active",
"new": "New",
"old": "Old",
"top_day": "Top day",
"landing_0":
"Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>. <14></14> <15>Thank you to our contributors: </15> dessalines, Nutomic, asonix, zacanger, and iav.",
"not_logged_in": "Not logged in.",
- "bio_length_overflow": "User bio cannot exceed 300 characters!",
+ "bio_length_overflow": "User bio cannot exceed 300 characters.",
"logged_in": "Logged in.",
"must_login": "You must <1>log in or register</1> to comment.",
"site_saved": "Site Saved.",
"invalid_post_title": "Invalid post title",
"invalid_url": "Invalid URL.",
"play_captcha_audio": "Play Captcha Audio",
- "bio": "Bio"
+ "bio": "Bio",
+ "display_name": "Display Name"
}