Merge branch 'main' into federate-sticky-flag

This commit is contained in:
Dessalines 2020-07-24 11:48:46 -04:00
commit f251936796
64 changed files with 2745 additions and 1360 deletions

2
ansible/VERSION vendored
View file

@ -1 +1 @@
v0.7.26
v0.7.28

View file

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

View file

@ -5,7 +5,7 @@ The configuration is based on the file
This file also contains documentation for all the available options. To override the defaults, you
can copy the options you want to change into your local `config.hjson` file.
To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`.
To use a different `config.hjson` location than the current directory, set the environment variable `LEMMY_CONFIG_LOCATION`. Make sure you copy the `defaults.hjson` if you do this, otherwise you will be missing settings.
Additionally, you can override any config files with environment variables. These have the same
name as the config options, and are prefixed with `LEMMY_`. For example, you can override the

View file

@ -17,6 +17,7 @@
- [Errors](#errors)
- [API documentation](#api-documentation)
* [Sort Types](#sort-types)
* [Undoing actions](#undoing-actions)
* [Websocket vs HTTP](#websocket-vs-http)
* [User / Authentication / Admin actions](#user--authentication--admin-actions)
+ [Login](#login)
@ -43,142 +44,198 @@
- [Request](#request-5)
- [Response](#response-5)
- [HTTP](#http-6)
+ [Edit User Mention](#edit-user-mention)
+ [Mark User Mention as read](#mark-user-mention-as-read)
- [Request](#request-6)
- [Response](#response-6)
- [HTTP](#http-7)
+ [Mark All As Read](#mark-all-as-read)
+ [Get Private Messages](#get-private-messages)
- [Request](#request-7)
- [Response](#response-7)
- [HTTP](#http-8)
+ [Delete Account](#delete-account)
+ [Create Private Message](#create-private-message)
- [Request](#request-8)
- [Response](#response-8)
- [HTTP](#http-9)
+ [Add admin](#add-admin)
+ [Edit Private Message](#edit-private-message)
- [Request](#request-9)
- [Response](#response-9)
- [HTTP](#http-10)
+ [Ban user](#ban-user)
+ [Delete Private Message](#delete-private-message)
- [Request](#request-10)
- [Response](#response-10)
- [HTTP](#http-11)
* [Site](#site)
+ [List Categories](#list-categories)
+ [Mark Private Message as Read](#mark-private-message-as-read)
- [Request](#request-11)
- [Response](#response-11)
- [HTTP](#http-12)
+ [Search](#search)
+ [Mark All As Read](#mark-all-as-read)
- [Request](#request-12)
- [Response](#response-12)
- [HTTP](#http-13)
+ [Get Modlog](#get-modlog)
+ [Delete Account](#delete-account)
- [Request](#request-13)
- [Response](#response-13)
- [HTTP](#http-14)
+ [Create Site](#create-site)
+ [Add admin](#add-admin)
- [Request](#request-14)
- [Response](#response-14)
- [HTTP](#http-15)
+ [Edit Site](#edit-site)
+ [Ban user](#ban-user)
- [Request](#request-15)
- [Response](#response-15)
- [HTTP](#http-16)
+ [Get Site](#get-site)
* [Site](#site)
+ [List Categories](#list-categories)
- [Request](#request-16)
- [Response](#response-16)
- [HTTP](#http-17)
+ [Transfer Site](#transfer-site)
+ [Search](#search)
- [Request](#request-17)
- [Response](#response-17)
- [HTTP](#http-18)
+ [Get Site Config](#get-site-config)
+ [Get Modlog](#get-modlog)
- [Request](#request-18)
- [Response](#response-18)
- [HTTP](#http-19)
+ [Save Site Config](#save-site-config)
+ [Create Site](#create-site)
- [Request](#request-19)
- [Response](#response-19)
- [HTTP](#http-20)
* [Community](#community)
+ [Get Community](#get-community)
+ [Edit Site](#edit-site)
- [Request](#request-20)
- [Response](#response-20)
- [HTTP](#http-21)
+ [Create Community](#create-community)
+ [Get Site](#get-site)
- [Request](#request-21)
- [Response](#response-21)
- [HTTP](#http-22)
+ [List Communities](#list-communities)
+ [Transfer Site](#transfer-site)
- [Request](#request-22)
- [Response](#response-22)
- [HTTP](#http-23)
+ [Ban from Community](#ban-from-community)
+ [Get Site Config](#get-site-config)
- [Request](#request-23)
- [Response](#response-23)
- [HTTP](#http-24)
+ [Add Mod to Community](#add-mod-to-community)
+ [Save Site Config](#save-site-config)
- [Request](#request-24)
- [Response](#response-24)
- [HTTP](#http-25)
+ [Edit Community](#edit-community)
* [Community](#community)
+ [Get Community](#get-community)
- [Request](#request-25)
- [Response](#response-25)
- [HTTP](#http-26)
+ [Follow Community](#follow-community)
+ [Create Community](#create-community)
- [Request](#request-26)
- [Response](#response-26)
- [HTTP](#http-27)
+ [Get Followed Communities](#get-followed-communities)
+ [List Communities](#list-communities)
- [Request](#request-27)
- [Response](#response-27)
- [HTTP](#http-28)
+ [Transfer Community](#transfer-community)
+ [Ban from Community](#ban-from-community)
- [Request](#request-28)
- [Response](#response-28)
- [HTTP](#http-29)
* [Post](#post)
+ [Create Post](#create-post)
+ [Add Mod to Community](#add-mod-to-community)
- [Request](#request-29)
- [Response](#response-29)
- [HTTP](#http-30)
+ [Get Post](#get-post)
+ [Edit Community](#edit-community)
- [Request](#request-30)
- [Response](#response-30)
- [HTTP](#http-31)
+ [Get Posts](#get-posts)
+ [Delete Community](#delete-community)
- [Request](#request-31)
- [Response](#response-31)
- [HTTP](#http-32)
+ [Create Post Like](#create-post-like)
+ [Remove Community](#remove-community)
- [Request](#request-32)
- [Response](#response-32)
- [HTTP](#http-33)
+ [Edit Post](#edit-post)
+ [Follow Community](#follow-community)
- [Request](#request-33)
- [Response](#response-33)
- [HTTP](#http-34)
+ [Save Post](#save-post)
+ [Get Followed Communities](#get-followed-communities)
- [Request](#request-34)
- [Response](#response-34)
- [HTTP](#http-35)
* [Comment](#comment)
+ [Create Comment](#create-comment)
+ [Transfer Community](#transfer-community)
- [Request](#request-35)
- [Response](#response-35)
- [HTTP](#http-36)
+ [Edit Comment](#edit-comment)
* [Post](#post)
+ [Create Post](#create-post)
- [Request](#request-36)
- [Response](#response-36)
- [HTTP](#http-37)
+ [Save Comment](#save-comment)
+ [Get Post](#get-post)
- [Request](#request-37)
- [Response](#response-37)
- [HTTP](#http-38)
+ [Create Comment Like](#create-comment-like)
+ [Get Posts](#get-posts)
- [Request](#request-38)
- [Response](#response-38)
- [HTTP](#http-39)
+ [Create Post Like](#create-post-like)
- [Request](#request-39)
- [Response](#response-39)
- [HTTP](#http-40)
+ [Edit Post](#edit-post)
- [Request](#request-40)
- [Response](#response-40)
- [HTTP](#http-41)
+ [Delete Post](#delete-post)
- [Request](#request-41)
- [Response](#response-41)
- [HTTP](#http-42)
+ [Remove Post](#remove-post)
- [Request](#request-42)
- [Response](#response-42)
- [HTTP](#http-43)
+ [Lock Post](#lock-post)
- [Request](#request-43)
- [Response](#response-43)
- [HTTP](#http-44)
+ [Sticky Post](#sticky-post)
- [Request](#request-44)
- [Response](#response-44)
- [HTTP](#http-45)
+ [Save Post](#save-post)
- [Request](#request-45)
- [Response](#response-45)
- [HTTP](#http-46)
* [Comment](#comment)
+ [Create Comment](#create-comment)
- [Request](#request-46)
- [Response](#response-46)
- [HTTP](#http-47)
+ [Edit Comment](#edit-comment)
- [Request](#request-47)
- [Response](#response-47)
- [HTTP](#http-48)
+ [Delete Comment](#delete-comment)
- [Request](#request-48)
- [Response](#response-48)
- [HTTP](#http-49)
+ [Remove Comment](#remove-comment)
- [Request](#request-49)
- [Response](#response-49)
- [HTTP](#http-50)
+ [Mark Comment as Read](#mark-comment-as-read)
- [Request](#request-50)
- [Response](#response-50)
- [HTTP](#http-51)
+ [Save Comment](#save-comment)
- [Request](#request-51)
- [Response](#response-51)
- [HTTP](#http-52)
+ [Create Comment Like](#create-comment-like)
- [Request](#request-52)
- [Response](#response-52)
- [HTTP](#http-53)
* [RSS / Atom feeds](#rss--atom-feeds)
+ [All](#all)
+ [Community](#community-1)
@ -281,6 +338,10 @@ These go wherever there is a `sort` field. The available sort types are:
- `TopYear` - the most upvoted posts/communities of the current year.
- `TopAll` - the most upvoted posts/communities on the current instance.
### Undoing actions
Whenever you see a `deleted: bool`, `removed: bool`, `read: bool`, `locked: bool`, etc, you can undo this action by sending `false`.
### Websocket vs HTTP
- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
@ -464,14 +525,17 @@ Only the first user will be able to be the admin.
`GET /user/mentions`
#### Edit User Mention
#### Mark User Mention as read
Only the recipient can do this.
##### Request
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
user_mention_id: i32,
read: Option<bool>,
read: bool,
auth: String,
}
}
@ -479,7 +543,7 @@ Only the first user will be able to be the admin.
##### Response
```rust
{
op: "EditUserMention",
op: "MarkUserMentionAsRead",
data: {
mention: UserMentionView,
}
@ -487,7 +551,141 @@ Only the first user will be able to be the admin.
```
##### HTTP
`PUT /user/mention`
`POST /user/mention/mark_as_read`
#### Get Private Messages
##### Request
```rust
{
op: "GetPrivateMessages",
data: {
unread_only: bool,
page: Option<i64>,
limit: Option<i64>,
auth: String,
}
}
```
##### Response
```rust
{
op: "GetPrivateMessages",
data: {
messages: Vec<PrivateMessageView>,
}
}
```
##### HTTP
`GET /private_message/list`
#### Create Private Message
##### Request
```rust
{
op: "CreatePrivateMessage",
data: {
content: String,
recipient_id: i32,
auth: String,
}
}
```
##### Response
```rust
{
op: "CreatePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message`
#### Edit Private Message
##### Request
```rust
{
op: "EditPrivateMessage",
data: {
edit_id: i32,
content: String,
auth: String,
}
}
```
##### Response
```rust
{
op: "EditPrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`PUT /private_message`
#### Delete Private Message
##### Request
```rust
{
op: "DeletePrivateMessage",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeletePrivateMessage",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/delete`
#### Mark Private Message as Read
Only the recipient can do this.
##### Request
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
edit_id: i32,
read: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "MarkPrivateMessageAsRead",
data: {
message: PrivateMessageView,
}
}
```
##### HTTP
`POST /private_message/mark_as_read`
#### Mark All As Read
@ -856,7 +1054,6 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
data: {
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -973,7 +1170,7 @@ Search types are `All, Comments, Posts, Communities, Users, Url`
`POST /community/mod`
#### Edit Community
Mods and admins can remove and lock a community, creators can delete it.
Only mods can edit a community.
##### Request
```rust
@ -984,10 +1181,6 @@ Mods and admins can remove and lock a community, creators can delete it.
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
expires: Option<i64>,
auth: String
}
}
@ -1005,6 +1198,62 @@ Mods and admins can remove and lock a community, creators can delete it.
`PUT /community`
#### Delete Community
Only a creator can delete a community
##### Request
```rust
{
op: "DeleteCommunity",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeleteCommunity",
data: {
community: CommunityView
}
}
```
##### HTTP
`POST /community/delete`
#### Remove Community
Only admins can remove a community.
##### Request
```rust
{
op: "RemoveCommunity",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemoveCommunity",
data: {
community: CommunityView
}
}
```
##### HTTP
`POST /community/remove`
#### Follow Community
##### Request
```rust
@ -1090,8 +1339,9 @@ Mods and admins can remove and lock a community, creators can delete it.
name: String,
url: Option<String>,
body: Option<String>,
nsfw: bool,
community_id: i32,
auth: String
auth: String,
}
}
```
@ -1128,7 +1378,6 @@ Mods and admins can remove and lock a community, creators can delete it.
comments: Vec<CommentView>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
}
}
```
@ -1197,25 +1446,17 @@ Post listing types are `All, Subscribed, Community`
`POST /post/like`
#### Edit Post
Mods and admins can remove and lock a post, creators can delete it.
##### Request
```rust
{
op: "EditPost",
data: {
edit_id: i32,
creator_id: i32,
community_id: i32,
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
locked: Option<bool>,
reason: Option<String>,
auth: String
nsfw: bool,
auth: String,
}
}
```
@ -1233,6 +1474,120 @@ Mods and admins can remove and lock a post, creators can delete it.
`PUT /post`
#### Delete Post
##### Request
```rust
{
op: "DeletePost",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeletePost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/delete`
#### Remove Post
Only admins and mods can remove a post.
##### Request
```rust
{
op: "RemovePost",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemovePost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/remove`
#### Lock Post
Only admins and mods can lock a post.
##### Request
```rust
{
op: "LockPost",
data: {
edit_id: i32,
locked: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "LockPost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/lock`
#### Sticky Post
Only admins and mods can sticky a post.
##### Request
```rust
{
op: "StickyPost",
data: {
edit_id: i32,
stickied: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "StickyPost",
data: {
post: PostView
}
}
```
##### HTTP
`POST /post/sticky`
#### Save Post
##### Request
```rust
@ -1267,8 +1622,8 @@ Mods and admins can remove and lock a post, creators can delete it.
data: {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>,
post_id: i32,
form_id: Option<String>, // An optional form id, so you know which message came back
auth: String
}
}
@ -1289,7 +1644,7 @@ Mods and admins can remove and lock a post, creators can delete it.
#### Edit Comment
Mods and admins can remove a comment, creators can delete it.
Only the creator can edit the comment.
##### Request
```rust
@ -1297,15 +1652,9 @@ Mods and admins can remove a comment, creators can delete it.
op: "EditComment",
data: {
content: String,
parent_id: Option<i32>,
edit_id: i32,
creator_id: i32,
post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
reason: Option<String>,
read: Option<bool>,
auth: String
form_id: Option<String>,
auth: String,
}
}
```
@ -1322,6 +1671,92 @@ Mods and admins can remove a comment, creators can delete it.
`PUT /comment`
#### Delete Comment
Only the creator can delete the comment.
##### Request
```rust
{
op: "DeleteComment",
data: {
edit_id: i32,
deleted: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "DeleteComment",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/delete`
#### Remove Comment
Only a mod or admin can remove the comment.
##### Request
```rust
{
op: "RemoveComment",
data: {
edit_id: i32,
removed: bool,
reason: Option<String>,
auth: String,
}
}
```
##### Response
```rust
{
op: "RemoveComment",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/remove`
#### Mark Comment as Read
Only the recipient can do this.
##### Request
```rust
{
op: "MarkCommentAsRead",
data: {
edit_id: i32,
read: bool,
auth: String,
}
}
```
##### Response
```rust
{
op: "MarkCommentAsRead",
data: {
comment: CommentView
}
}
```
##### HTTP
`POST /comment/mark_as_read`
#### Save Comment
##### Request
```rust
@ -1357,7 +1792,6 @@ Mods and admins can remove a comment, creators can delete it.
op: "CreateCommentLike",
data: {
comment_id: i32,
post_id: i32,
score: i16,
auth: String
}

2
server/Cargo.lock generated vendored
View file

@ -1559,7 +1559,9 @@ dependencies = [
"bcrypt",
"chrono",
"diesel",
"lazy_static",
"log",
"regex",
"serde 1.0.114",
"serde_json",
"sha2",

View file

@ -13,4 +13,6 @@ strum_macros = "0.18.0"
log = "0.4.0"
sha2 = "0.9"
bcrypt = "0.8.0"
url = { version = "2.1.1", features = ["serde"] }
url = { version = "2.1.1", features = ["serde"] }
lazy_static = "1.3.0"
regex = "1.3.5"

View file

@ -97,14 +97,6 @@ impl Comment {
comment.filter(ap_id.eq(object_id)).first::<Self>(conn)
}
pub fn mark_as_read(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(true))
.get_result::<Self>(conn)
}
pub fn permadelete(conn: &PgConnection, comment_id: i32) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
@ -116,6 +108,46 @@ impl Comment {
))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
comment_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
comment_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_read(conn: &PgConnection, comment_id: i32, new_read: bool) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
comment_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::comment::dsl::*;
diesel::update(comment.find(comment_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug, Clone)]

View file

@ -1,4 +1,5 @@
use crate::{
naive_now,
schema::{community, community_follower, community_moderator, community_user_ban},
Bannable,
Crud,
@ -29,7 +30,6 @@ pub struct Community {
pub last_refreshed_at: chrono::NaiveDateTime,
}
// TODO add better delete, remove, lock actions here.
#[derive(Insertable, AsChangeset, Clone, Serialize, Deserialize, Debug)]
#[table_name = "community"]
pub struct CommunityForm {
@ -99,6 +99,60 @@ impl Community {
use crate::schema::community::dsl::*;
community.filter(local.eq(true)).load::<Community>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
community_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
community_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_creator(
conn: &PgConnection,
community_id: i32,
new_creator_id: i32,
) -> Result<Self, Error> {
use crate::schema::community::dsl::*;
diesel::update(community.find(community_id))
.set((creator_id.eq(new_creator_id), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
fn community_mods_and_admins(
conn: &PgConnection,
community_id: i32,
) -> Result<Vec<i32>, Error> {
use crate::{community_view::CommunityModeratorView, user_view::UserView};
let mut mods_and_admins: Vec<i32> = Vec::new();
mods_and_admins.append(
&mut CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())?,
);
mods_and_admins
.append(&mut UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())?);
Ok(mods_and_admins)
}
pub fn is_mod_or_admin(conn: &PgConnection, user_id: i32, community_id: i32) -> bool {
Self::community_mods_and_admins(conn, community_id)
.unwrap_or_default()
.contains(&user_id)
}
}
#[derive(Identifiable, Queryable, Associations, PartialEq, Debug)]

View file

@ -295,18 +295,18 @@ pub struct CommunityModeratorView {
}
impl CommunityModeratorView {
pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
pub fn for_community(conn: &PgConnection, for_community_id: i32) -> Result<Vec<Self>, Error> {
use super::community_view::community_moderator_view::dsl::*;
community_moderator_view
.filter(community_id.eq(from_community_id))
.filter(community_id.eq(for_community_id))
.order_by(published)
.load::<Self>(conn)
}
pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
pub fn for_user(conn: &PgConnection, for_user_id: i32) -> Result<Vec<Self>, Error> {
use super::community_view::community_moderator_view::dsl::*;
community_moderator_view
.filter(user_id.eq(from_user_id))
.filter(user_id.eq(for_user_id))
.order_by(published)
.load::<Self>(conn)
}

View file

@ -2,9 +2,12 @@
pub extern crate diesel;
#[macro_use]
pub extern crate strum_macros;
#[macro_use]
pub extern crate lazy_static;
pub extern crate bcrypt;
pub extern crate chrono;
pub extern crate log;
pub extern crate regex;
pub extern crate serde;
pub extern crate serde_json;
pub extern crate sha2;
@ -12,6 +15,7 @@ pub extern crate strum;
use chrono::NaiveDateTime;
use diesel::{dsl::*, result::Error, *};
use regex::Regex;
use serde::{Deserialize, Serialize};
use std::{env, env::VarError};
@ -172,10 +176,19 @@ pub fn naive_now() -> NaiveDateTime {
chrono::prelude::Utc::now().naive_utc()
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
lazy_static! {
static ref EMAIL_REGEX: Regex =
Regex::new(r"^[a-zA-Z0-9.!#$%&*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$").unwrap();
}
#[cfg(test)]
mod tests {
use super::fuzzy_search;
use crate::get_database_url_from_env;
use crate::{get_database_url_from_env, is_email_regex};
use diesel::{Connection, PgConnection};
pub fn establish_unpooled_connection() -> PgConnection {
@ -194,4 +207,10 @@ mod tests {
let test = "This is a fuzzy search";
assert_eq!(fuzzy_search(test), "%This%is%a%fuzzy%search%".to_string());
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
}

View file

@ -108,6 +108,50 @@ impl Post {
))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
post_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_removed(
conn: &PgConnection,
post_id: i32,
new_removed: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(removed.eq(new_removed))
.get_result::<Self>(conn)
}
pub fn update_locked(conn: &PgConnection, post_id: i32, new_locked: bool) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(locked.eq(new_locked))
.get_result::<Self>(conn)
}
pub fn update_stickied(
conn: &PgConnection,
post_id: i32,
new_stickied: bool,
) -> Result<Self, Error> {
use crate::schema::post::dsl::*;
diesel::update(post.find(post_id))
.set(stickied.eq(new_stickied))
.get_result::<Self>(conn)
}
pub fn is_post_creator(user_id: i32, post_creator_id: i32) -> bool {
user_id == post_creator_id
}
}
impl Crud<PostForm> for Post {

View file

@ -1,4 +1,4 @@
use crate::{schema::private_message, Crud};
use crate::{naive_now, schema::private_message, Crud};
use diesel::{dsl::*, result::Error, *};
use serde::{Deserialize, Serialize};
@ -80,6 +80,50 @@ impl PrivateMessage {
.filter(ap_id.eq(object_id))
.first::<Self>(conn)
}
pub fn update_content(
conn: &PgConnection,
private_message_id: i32,
new_content: &str,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set((content.eq(new_content), updated.eq(naive_now())))
.get_result::<Self>(conn)
}
pub fn update_deleted(
conn: &PgConnection,
private_message_id: i32,
new_deleted: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(deleted.eq(new_deleted))
.get_result::<Self>(conn)
}
pub fn update_read(
conn: &PgConnection,
private_message_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(private_message.find(private_message_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::private_message::dsl::*;
diesel::update(
private_message
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)]
@ -180,6 +224,10 @@ mod tests {
let read_private_message = PrivateMessage::read(&conn, inserted_private_message.id).unwrap();
let updated_private_message =
PrivateMessage::update(&conn, inserted_private_message.id, &private_message_form).unwrap();
let deleted_private_message =
PrivateMessage::update_deleted(&conn, inserted_private_message.id, true).unwrap();
let marked_read_private_message =
PrivateMessage::update_read(&conn, inserted_private_message.id, true).unwrap();
let num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
User_::delete(&conn, inserted_creator.id).unwrap();
User_::delete(&conn, inserted_recipient.id).unwrap();
@ -187,6 +235,8 @@ mod tests {
assert_eq!(expected_private_message, read_private_message);
assert_eq!(expected_private_message, updated_private_message);
assert_eq!(expected_private_message, inserted_private_message);
assert!(deleted_private_message.deleted);
assert!(marked_read_private_message.read);
assert_eq!(1, num_deleted);
}
}

View file

@ -1,4 +1,5 @@
use crate::{
is_email_regex,
naive_now,
schema::{user_, user_::dsl::*},
Crud,
@ -125,9 +126,18 @@ impl User_ {
use crate::schema::user_::dsl::*;
user_.filter(actor_id.eq(object_id)).first::<Self>(conn)
}
}
impl User_ {
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<Self, Error> {
if is_email_regex(username_or_email) {
Self::find_by_email(conn, username_or_email)
} else {
Self::find_by_username(conn, username_or_email)
}
}
pub fn find_by_username(conn: &PgConnection, username: &str) -> Result<User_, Error> {
user_.filter(name.eq(username)).first::<User_>(conn)
}

View file

@ -52,6 +52,30 @@ impl Crud<UserMentionForm> for UserMention {
}
}
impl UserMention {
pub fn update_read(
conn: &PgConnection,
user_mention_id: i32,
new_read: bool,
) -> Result<Self, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(user_mention.find(user_mention_id))
.set(read.eq(new_read))
.get_result::<Self>(conn)
}
pub fn mark_all_as_read(conn: &PgConnection, for_recipient_id: i32) -> Result<Vec<Self>, Error> {
use crate::schema::user_mention::dsl::*;
diesel::update(
user_mention
.filter(recipient_id.eq(for_recipient_id))
.filter(read.eq(false)),
)
.set(read.eq(true))
.get_results::<Self>(conn)
}
}
#[cfg(test)]
mod tests {
use crate::{

View file

@ -19,4 +19,4 @@ serde_json = { version = "1.0.52", features = ["preserve_order"]}
comrak = "0.7"
lazy_static = "1.3.0"
openssl = "0.10"
url = { version = "2.1.1", features = ["serde"] }
url = { version = "2.1.1", features = ["serde"] }

View file

@ -44,10 +44,6 @@ pub fn convert_datetime(datetime: NaiveDateTime) -> DateTime<FixedOffset> {
DateTime::<FixedOffset>::from_utc(datetime, *now.offset())
}
pub fn is_email_regex(test: &str) -> bool {
EMAIL_REGEX.is_match(test)
}
pub fn remove_slurs(test: &str) -> String {
SLUR_REGEX.replace_all(test, "*removed*").to_string()
}
@ -165,7 +161,6 @@ pub fn is_valid_post_title(title: &str) -> bool {
#[cfg(test)]
mod tests {
use crate::{
is_email_regex,
is_valid_community_name,
is_valid_post_title,
is_valid_username,
@ -185,12 +180,6 @@ mod tests {
assert_eq!(mentions[1].domain, "lemmy-alpha:8540".to_string());
}
#[test]
fn test_email() {
assert!(is_email_regex("gush@gmail.com"));
assert!(!is_email_regex("nada_neutho"));
}
#[test]
fn test_valid_register_username() {
assert!(is_valid_username("Hello_98"));

View file

@ -81,9 +81,9 @@ impl Settings {
fn init() -> Result<Self, ConfigError> {
let mut s = Config::new();
s.merge(File::with_name(CONFIG_FILE_DEFAULTS))?;
s.merge(File::with_name(&Self::get_config_defaults_location()))?;
s.merge(File::with_name(&Self::get_config_location()).required(false))?;
s.merge(File::with_name(CONFIG_FILE).required(false))?;
// Add in settings from the environment (with a prefix of LEMMY)
// Eg.. `LEMMY_DEBUG=1 ./target/app` would set the `debug` key
@ -115,16 +115,16 @@ impl Settings {
format!("{}/api/v1", self.hostname)
}
pub fn get_config_location() -> String {
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE.to_string())
pub fn get_config_defaults_location() -> String {
env::var("LEMMY_CONFIG_LOCATION").unwrap_or_else(|_| CONFIG_FILE_DEFAULTS.to_string())
}
pub fn read_config_file() -> Result<String, Error> {
fs::read_to_string(Self::get_config_location())
fs::read_to_string(CONFIG_FILE)
}
pub fn save_config_file(data: &str) -> Result<String, Error> {
fs::write(Self::get_config_location(), data)?;
fs::write(CONFIG_FILE, data)?;
// Reload the new settings
// From https://stackoverflow.com/questions/29654927/how-do-i-assign-a-string-to-a-mutable-static-variable/47181804#47181804

View file

@ -1,7 +1,7 @@
use diesel::{result::Error, PgConnection};
use jsonwebtoken::{decode, encode, DecodingKey, EncodingKey, Header, TokenData, Validation};
use lemmy_db::{user::User_, Crud};
use lemmy_utils::{is_email_regex, settings::Settings};
use lemmy_utils::settings::Settings;
use serde::{Deserialize, Serialize};
type Jwt = String;
@ -54,18 +54,6 @@ impl Claims {
.unwrap()
}
// TODO: move these into user?
pub fn find_by_email_or_username(
conn: &PgConnection,
username_or_email: &str,
) -> Result<User_, Error> {
if is_email_regex(username_or_email) {
User_::find_by_email(conn, username_or_email)
} else {
User_::find_by_username(conn, username_or_email)
}
}
pub fn find_by_jwt(conn: &PgConnection, jwt: &str) -> Result<User_, Error> {
let claims: Claims = Claims::decode(&jwt).expect("Invalid token").claims;
User_::read(&conn, claims.id)

View file

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
blocking,
websocket::{
@ -15,12 +15,10 @@ use lemmy_db::{
comment_view::*,
community_view::*,
moderator::*,
naive_now,
post::*,
site_view::*,
user::*,
user_mention::*,
user_view::*,
Crud,
Likeable,
ListingType,
@ -44,22 +42,38 @@ use std::str::FromStr;
pub struct CreateComment {
content: String,
parent_id: Option<i32>,
edit_id: Option<i32>, // TODO this isn't used
pub post_id: i32,
form_id: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct EditComment {
content: String,
parent_id: Option<i32>, // TODO why are the parent_id, creator_id, post_id, etc fields required? They aren't going to change
edit_id: i32,
creator_id: i32,
pub post_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
form_id: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeleteComment {
edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemoveComment {
edit_id: i32,
removed: bool,
reason: Option<String>,
read: Option<bool>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkCommentAsRead {
edit_id: i32,
read: bool,
auth: String,
}
@ -74,12 +88,12 @@ pub struct SaveComment {
pub struct CommentResponse {
pub comment: CommentView,
pub recipient_ids: Vec<i32>,
pub form_id: Option<String>,
}
#[derive(Serialize, Deserialize)]
pub struct CreateCommentLike {
comment_id: i32,
pub post_id: i32,
score: i16,
auth: String,
}
@ -150,6 +164,12 @@ impl Perform for Oper<CreateComment> {
return Err(APIError::err("site_ban").into());
}
// Check if post is locked, no new comments
if post.locked {
return Err(APIError::err("locked").into());
}
// Create the comment
let comment_form2 = comment_form.clone();
let inserted_comment =
match blocking(pool, move |conn| Comment::create(&conn, &comment_form2)).await? {
@ -157,6 +177,7 @@ impl Perform for Oper<CreateComment> {
Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
};
// Necessary to update the ap_id
let inserted_comment_id = inserted_comment.id;
let updated_comment: Comment = match blocking(pool, move |conn| {
let apub_id =
@ -175,8 +196,15 @@ impl Perform for Oper<CreateComment> {
// Scan the comment for user mentions, add those rows
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids =
send_local_notifs(mentions, updated_comment.clone(), user.clone(), post, pool).await?;
let recipient_ids = send_local_notifs(
mentions,
updated_comment.clone(),
user.clone(),
post,
pool,
true,
)
.await?;
// You like your own comment by default
let like_form = CommentLikeForm {
@ -201,6 +229,7 @@ impl Perform for Oper<CreateComment> {
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
if let Some(ws) = websocket_info {
@ -237,122 +266,34 @@ impl Perform for Oper<EditComment> {
let user_id = claims.id;
let user = blocking(pool, move |conn| User_::read(&conn, user_id)).await??;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
let mut editors: Vec<i32> = vec![orig_comment.creator_id];
let mut moderators: Vec<i32> = vec![];
let community_id = orig_comment.community_id;
moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
editors.extend(&moderators);
// You are allowed to mark the comment as read even if you're banned.
if data.read.is_none() {
// Verify its the creator or a mod, or an admin
if !editors.contains(&user_id) {
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Check for a site ban
if user.banned {
return Err(APIError::err("site_ban").into());
}
} else {
// check that user can mark as read
let parent_id = orig_comment.parent_id;
match parent_id {
Some(pid) => {
let parent_comment =
blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
if user_id != parent_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
}
None => {
let parent_post_id = orig_comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
if user_id != parent_post.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
}
}
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the creator can edit
if user_id != orig_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Do the update
let content_slurs_removed = remove_slurs(&data.content.to_owned());
let edit_id = data.edit_id;
let read_comment = blocking(pool, move |conn| Comment::read(conn, edit_id)).await??;
let comment_form = {
if data.read.is_none() {
// the ban etc checks should been made and have passed
// the comment can be properly edited
let post_removed = if moderators.contains(&user_id) {
data.removed
} else {
Some(read_comment.removed)
};
CommentForm {
content: content_slurs_removed,
parent_id: read_comment.parent_id,
post_id: read_comment.post_id,
creator_id: read_comment.creator_id,
removed: post_removed.to_owned(),
deleted: data.deleted.to_owned(),
read: Some(read_comment.read),
published: None,
updated: Some(naive_now()),
ap_id: read_comment.ap_id,
local: read_comment.local,
}
} else {
// the only field that can be updated it the read field
CommentForm {
content: read_comment.content,
parent_id: read_comment.parent_id,
post_id: read_comment.post_id,
creator_id: read_comment.creator_id,
removed: Some(read_comment.removed).to_owned(),
deleted: Some(read_comment.deleted).to_owned(),
read: data.read.to_owned(),
published: None,
updated: orig_comment.updated,
ap_id: read_comment.ap_id,
local: read_comment.local,
}
}
};
let edit_id = data.edit_id;
let comment_form2 = comment_form.clone();
let updated_comment = match blocking(pool, move |conn| {
Comment::update(conn, edit_id, &comment_form2)
Comment::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
@ -360,54 +301,19 @@ impl Perform for Oper<EditComment> {
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
if data.read.is_none() {
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if moderators.contains(&user_id) {
if removed {
updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
}
}
} else {
updated_comment
.send_update(&user, &self.client, pool)
.await?;
}
// Send the apub update
updated_comment
.send_update(&user, &self.client, pool)
.await?;
// Mod tables
if moderators.contains(&user_id) {
if let Some(removed) = data.removed.to_owned() {
let form = ModRemoveCommentForm {
mod_user_id: user_id,
comment_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
}
}
}
let post_id = data.post_id;
// Do the mentions / recipients
let post_id = orig_comment.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&comment_form.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
let updated_comment_content = updated_comment.content.to_owned();
let mentions = scrape_text_for_mentions(&updated_comment_content);
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
@ -418,6 +324,7 @@ impl Perform for Oper<EditComment> {
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: data.form_id.to_owned(),
};
if let Some(ws) = websocket_info {
@ -436,6 +343,291 @@ impl Perform for Oper<EditComment> {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &DeleteComment = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the creator can delete
if user_id != orig_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
// Do the delete
let deleted = data.deleted;
let updated_comment = match blocking(pool, move |conn| {
Comment::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Send the apub message
if deleted {
updated_comment
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_delete(&user, &self.client, pool)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = vec![];
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::DeleteComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemoveComment> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &RemoveComment = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only a mod or admin can remove
is_mod_or_admin(pool, user_id, community_id).await?;
// Do the remove
let removed = data.removed;
let updated_comment = match blocking(pool, move |conn| {
Comment::update_removed(conn, edit_id, removed)
})
.await?
{
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Mod tables
let form = ModRemoveCommentForm {
mod_user_id: user_id,
comment_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemoveComment::create(conn, &form)).await??;
// Send the apub message
if removed {
updated_comment
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_comment
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Refetch it
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
// Build the recipients
let post_id = comment_view.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = vec![];
let recipient_ids =
send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
let mut res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendComment {
op: UserOperation::RemoveComment,
comment: res.clone(),
my_id: ws.id,
});
// strip out the recipient_ids, so that
// users don't get double notifs
res.recipient_ids = Vec::new();
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<MarkCommentAsRead> {
type Response = CommentResponse;
async fn perform(
&self,
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
) -> Result<CommentResponse, LemmyError> {
let data: &MarkCommentAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, edit_id, None)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_comment.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the recipient can mark as read
// Needs to fetch the parent comment / post to get the recipient
let parent_id = orig_comment.parent_id;
match parent_id {
Some(pid) => {
let parent_comment =
blocking(pool, move |conn| CommentView::read(&conn, pid, None)).await??;
if user_id != parent_comment.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
}
None => {
let parent_post_id = orig_comment.post_id;
let parent_post = blocking(pool, move |conn| Post::read(conn, parent_post_id)).await??;
if user_id != parent_post.creator_id {
return Err(APIError::err("no_comment_edit_allowed").into());
}
}
}
// Do the mark as read
let read = data.read;
match blocking(pool, move |conn| Comment::update_read(conn, edit_id, read)).await? {
Ok(comment) => comment,
Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
};
// Refetch it
let edit_id = data.edit_id;
let comment_view = blocking(pool, move |conn| {
CommentView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommentResponse {
comment: comment_view,
recipient_ids: Vec::new(),
form_id: None,
};
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SaveComment> {
type Response = CommentResponse;
@ -480,6 +672,7 @@ impl Perform for Oper<SaveComment> {
Ok(CommentResponse {
comment: comment_view,
recipient_ids: Vec::new(),
form_id: None,
})
}
}
@ -512,8 +705,12 @@ impl Perform for Oper<CreateCommentLike> {
}
}
let comment_id = data.comment_id;
let orig_comment =
blocking(pool, move |conn| CommentView::read(&conn, comment_id, None)).await??;
// Check for a community ban
let post_id = data.post_id;
let post_id = orig_comment.post_id;
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let community_id = post.community_id;
let is_banned =
@ -550,7 +747,7 @@ impl Perform for Oper<CreateCommentLike> {
let like_form = CommentLikeForm {
comment_id: data.comment_id,
post_id: data.post_id,
post_id,
user_id,
score: data.score,
};
@ -587,6 +784,7 @@ impl Perform for Oper<CreateCommentLike> {
let mut res = CommentResponse {
comment: liked_comment,
recipient_ids,
form_id: None,
};
if let Some(ws) = websocket_info {
@ -675,9 +873,10 @@ pub async fn send_local_notifs(
user: User_,
post: Post,
pool: &DbPool,
do_send_email: bool,
) -> Result<Vec<i32>, LemmyError> {
let ids = blocking(pool, move |conn| {
do_send_local_notifs(conn, &mentions, &comment, &user, &post)
do_send_local_notifs(conn, &mentions, &comment, &user, &post, do_send_email)
})
.await?;
@ -690,6 +889,7 @@ fn do_send_local_notifs(
comment: &Comment,
user: &User_,
post: &Post,
do_send_email: bool,
) -> Vec<i32> {
let mut recipient_ids = Vec::new();
let hostname = &format!("https://{}", Settings::get().hostname);
@ -720,7 +920,7 @@ fn do_send_local_notifs(
};
// Send an email to those users that have notifications on
if mention_user.send_notifications_to_email {
if do_send_email && mention_user.send_notifications_to_email {
if let Some(mention_email) = mention_user.email {
let subject = &format!("{} - Mentioned by {}", Settings::get().hostname, user.name,);
let html = &format!(
@ -744,7 +944,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, parent_comment.creator_id) {
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if do_send_email && parent_user.send_notifications_to_email {
if let Some(comment_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(
@ -767,7 +967,7 @@ fn do_send_local_notifs(
if let Ok(parent_user) = User_::read(&conn, post.creator_id) {
recipient_ids.push(parent_user.id);
if parent_user.send_notifications_to_email {
if do_send_email && parent_user.send_notifications_to_email {
if let Some(post_reply_email) = parent_user.email {
let subject = &format!("{} - Reply from {}", Settings::get().hostname, user.name,);
let html = &format!(

View file

@ -1,6 +1,6 @@
use super::*;
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, is_mod_or_admin, APIError, Oper, Perform},
apub::ActorType,
blocking,
websocket::{
@ -34,7 +34,6 @@ pub struct GetCommunity {
pub struct GetCommunityResponse {
pub community: CommunityView,
pub moderators: Vec<CommunityModeratorView>,
pub admins: Vec<UserView>,
pub online: usize,
}
@ -101,9 +100,21 @@ pub struct EditCommunity {
title: String,
description: Option<String>,
category_id: i32,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeleteCommunity {
pub edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemoveCommunity {
pub edit_id: i32,
removed: bool,
reason: Option<String>,
expires: Option<i64>,
auth: String,
@ -184,13 +195,6 @@ impl Perform for Oper<GetCommunity> {
Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
};
let site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
let site_creator_id = site.creator_id;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id {
ws.chatserver.do_send(JoinCommunityRoom {
@ -212,7 +216,6 @@ impl Perform for Oper<GetCommunity> {
let res = GetCommunityResponse {
community: community_view,
moderators,
admins,
online,
};
@ -366,24 +369,15 @@ impl Perform for Oper<EditCommunity> {
return Err(APIError::err("site_ban").into());
}
// Verify its a mod
// Verify its a mod (only mods can edit it)
let edit_id = data.edit_id;
let mut editors: Vec<i32> = Vec::new();
editors.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
editors.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !editors.contains(&user_id) {
return Err(APIError::err("no_community_edit_allowed").into());
let mods: Vec<i32> = blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, edit_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??;
if !mods.contains(&user_id) {
return Err(APIError::err("not_a_moderator").into());
}
let edit_id = data.edit_id;
@ -395,8 +389,8 @@ impl Perform for Oper<EditCommunity> {
description: data.description.to_owned(),
category_id: data.category_id.to_owned(),
creator_id: read_community.creator_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
removed: Some(read_community.removed),
deleted: Some(read_community.deleted),
nsfw: data.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
@ -408,7 +402,7 @@ impl Perform for Oper<EditCommunity> {
};
let edit_id = data.edit_id;
let updated_community = match blocking(pool, move |conn| {
match blocking(pool, move |conn| {
Community::update(conn, edit_id, &community_form)
})
.await?
@ -417,42 +411,77 @@ impl Perform for Oper<EditCommunity> {
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
if let Some(removed) = data.removed.to_owned() {
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_user_id: user_id,
community_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
// TODO there needs to be some kind of an apub update
// process for communities and users
let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse {
community: community_view,
};
send_community_websocket(&res, websocket_info, UserOperation::EditCommunity);
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeleteCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &DeleteCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_community
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Verify its the creator (only a creator can delete the community)
let edit_id = data.edit_id;
let read_community = blocking(pool, move |conn| Community::read(conn, edit_id)).await??;
if read_community.creator_id != user_id {
return Err(APIError::err("no_community_edit_allowed").into());
}
// Do the delete
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_community = match blocking(pool, move |conn| {
Community::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Send apub messages
if deleted {
updated_community
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_delete(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
@ -465,20 +494,88 @@ impl Perform for Oper<EditCommunity> {
community: community_view,
};
if let Some(ws) = websocket_info {
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
send_community_websocket(&res, websocket_info, UserOperation::DeleteCommunity);
ws.chatserver.do_send(SendCommunityRoomMessage {
op: UserOperation::EditCommunity,
response: res_sent,
community_id: data.edit_id,
my_id: ws.id,
});
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemoveCommunity> {
type Response = CommunityResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<CommunityResponse, LemmyError> {
let data: &RemoveCommunity = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Verify its an admin (only an admin can remove a community)
is_admin(pool, user_id).await?;
// Do the remove
let edit_id = data.edit_id;
let removed = data.removed;
let updated_community = match blocking(pool, move |conn| {
Community::update_removed(conn, edit_id, removed)
})
.await?
{
Ok(community) => community,
Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
};
// Mod tables
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
};
let form = ModRemoveCommunityForm {
mod_user_id: user_id,
community_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
expires,
};
blocking(pool, move |conn| ModRemoveCommunity::create(conn, &form)).await??;
// Apub messages
if removed {
updated_community
.send_remove(&user, &self.client, pool)
.await?;
} else {
updated_community
.send_undo_remove(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
let community_view = blocking(pool, move |conn| {
CommunityView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = CommunityResponse {
community: community_view,
};
send_community_websocket(&res, websocket_info, UserOperation::RemoveCommunity);
Ok(res)
}
}
@ -654,27 +751,10 @@ impl Perform for Oper<BanFromCommunity> {
let user_id = claims.id;
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id;
community_moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
// Verify that only mods or admins can ban
is_mod_or_admin(pool, user_id, community_id).await?;
let community_user_ban_form = CommunityUserBanForm {
community_id: data.community_id,
@ -694,6 +774,7 @@ impl Perform for Oper<BanFromCommunity> {
}
// Mod tables
// TODO eventually do correct expires
let expires = match data.expires {
Some(time) => Some(naive_from_unix(time)),
None => None,
@ -753,27 +834,10 @@ impl Perform for Oper<AddModToCommunity> {
user_id: data.user_id,
};
let mut community_moderators: Vec<i32> = vec![];
let community_id = data.community_id;
community_moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(&conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
community_moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
if !community_moderators.contains(&user_id) {
return Err(APIError::err("couldnt_update_community").into());
}
// Verify that only mods or admins can add mod
is_mod_or_admin(pool, user_id, community_id).await?;
if data.added {
let join = move |conn: &'_ _| CommunityModerator::join(conn, &community_moderator_form);
@ -852,26 +916,9 @@ impl Perform for Oper<TransferCommunity> {
return Err(APIError::err("not_an_admin").into());
}
let community_form = CommunityForm {
name: read_community.name,
title: read_community.title,
description: read_community.description,
category_id: read_community.category_id,
creator_id: data.user_id, // This makes the new user the community creator
removed: None,
deleted: None,
nsfw: read_community.nsfw,
updated: Some(naive_now()),
actor_id: read_community.actor_id,
local: read_community.local,
private_key: read_community.private_key,
public_key: read_community.public_key,
last_refreshed_at: None,
published: None,
};
let community_id = data.community_id;
let update = move |conn: &'_ _| Community::update(conn, community_id, &community_form);
let new_creator = data.user_id;
let update = move |conn: &'_ _| Community::update_creator(conn, community_id, new_creator);
if blocking(pool, update).await?.is_err() {
return Err(APIError::err("couldnt_update_community").into());
};
@ -941,8 +988,27 @@ impl Perform for Oper<TransferCommunity> {
Ok(GetCommunityResponse {
community: community_view,
moderators,
admins,
online: 0,
})
}
}
pub fn send_community_websocket(
res: &CommunityResponse,
websocket_info: Option<WebsocketInfo>,
op: UserOperation,
) {
if let Some(ws) = websocket_info {
// Strip out the user id and subscribed when sending to others
let mut res_sent = res.clone();
res_sent.community.user_id = None;
res_sent.community.subscribed = None;
ws.chatserver.do_send(SendCommunityRoomMessage {
op,
response: res_sent,
community_id: res.community.id,
my_id: ws.id,
});
}
}

View file

@ -1,6 +1,14 @@
use crate::{websocket::WebsocketInfo, DbPool, LemmyError};
use crate::{blocking, websocket::WebsocketInfo, DbPool, LemmyError};
use actix_web::client::Client;
use lemmy_db::{community::*, community_view::*, moderator::*, site::*, user::*, user_view::*};
use lemmy_db::{
community::*,
community_view::*,
moderator::*,
site::*,
user::*,
user_view::*,
Crud,
};
pub mod claims;
pub mod comment;
@ -44,3 +52,25 @@ pub trait Perform {
websocket_info: Option<WebsocketInfo>,
) -> Result<Self::Response, LemmyError>;
}
pub async fn is_mod_or_admin(
pool: &DbPool,
user_id: i32,
community_id: i32,
) -> Result<(), LemmyError> {
let is_mod_or_admin = blocking(pool, move |conn| {
Community::is_mod_or_admin(conn, user_id, community_id)
})
.await?;
if !is_mod_or_admin {
return Err(APIError::err("not_an_admin").into());
}
Ok(())
}
pub async fn is_admin(pool: &DbPool, user_id: i32) -> Result<(), LemmyError> {
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
Ok(())
}

View file

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_mod_or_admin, APIError, Oper, Perform},
apub::{ApubLikeableType, ApubObjectType},
blocking,
fetch_iframely_and_pictrs_data,
@ -18,10 +18,8 @@ use lemmy_db::{
naive_now,
post::*,
post_view::*,
site::*,
site_view::*,
user::*,
user_view::*,
Crud,
Likeable,
ListingType,
@ -66,7 +64,6 @@ pub struct GetPostResponse {
comments: Vec<CommentView>,
community: CommunityView,
moderators: Vec<CommunityModeratorView>,
admins: Vec<UserView>,
pub online: usize,
}
@ -96,20 +93,42 @@ pub struct CreatePostLike {
#[derive(Serialize, Deserialize)]
pub struct EditPost {
pub edit_id: i32,
creator_id: i32,
community_id: i32,
name: String,
url: Option<String>,
body: Option<String>,
removed: Option<bool>,
deleted: Option<bool>,
nsfw: bool,
locked: Option<bool>,
stickied: Option<bool>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeletePost {
pub edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct RemovePost {
pub edit_id: i32,
removed: bool,
reason: Option<String>,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct LockPost {
pub edit_id: i32,
locked: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct StickyPost {
pub edit_id: i32,
stickied: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct SavePost {
post_id: i32,
@ -311,14 +330,6 @@ impl Perform for Oper<GetPost> {
})
.await??;
let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
let online = if let Some(ws) = websocket_info {
if let Some(id) = ws.id {
ws.chatserver.do_send(JoinPostRoom {
@ -343,7 +354,6 @@ impl Perform for Oper<GetPost> {
comments,
community,
moderators,
admins,
online,
})
}
@ -549,35 +559,10 @@ impl Perform for Oper<EditPost> {
let user_id = claims.id;
let edit_id = data.edit_id;
let read_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Verify its the creator or a mod or admin
let community_id = read_post.community_id;
let mut editors: Vec<i32> = vec![read_post.creator_id];
let mut moderators: Vec<i32> = vec![];
moderators.append(
&mut blocking(pool, move |conn| {
CommunityModeratorView::for_community(conn, community_id)
.map(|v| v.into_iter().map(|m| m.user_id).collect())
})
.await??,
);
moderators.append(
&mut blocking(pool, move |conn| {
UserView::admins(conn).map(|v| v.into_iter().map(|a| a.id).collect())
})
.await??,
);
editors.extend(&moderators);
if !editors.contains(&user_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a community ban
let community_id = read_post.community_id;
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
@ -590,55 +575,34 @@ impl Perform for Oper<EditPost> {
return Err(APIError::err("site_ban").into());
}
// Verify that only the creator can edit
if !Post::is_post_creator(user_id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Fetch Iframely and Pictrs cached image
let (iframely_title, iframely_description, iframely_html, pictrs_thumbnail) =
fetch_iframely_and_pictrs_data(&self.client, data.url.to_owned()).await;
let post_form = {
// only modify some properties if they are a moderator
if moderators.contains(&user_id) {
PostForm {
name: data.name.trim().to_owned(),
url: data.url.to_owned(),
body: data.body.to_owned(),
creator_id: read_post.creator_id.to_owned(),
community_id: read_post.community_id,
removed: data.removed.to_owned(),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
locked: data.locked.to_owned(),
stickied: data.stickied.to_owned(),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id,
local: read_post.local,
published: None,
}
} else {
PostForm {
name: read_post.name.trim().to_owned(),
url: data.url.to_owned(),
body: data.body.to_owned(),
creator_id: read_post.creator_id.to_owned(),
community_id: read_post.community_id,
removed: Some(read_post.removed),
deleted: data.deleted.to_owned(),
nsfw: data.nsfw,
locked: Some(read_post.locked),
stickied: Some(read_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: read_post.ap_id,
local: read_post.local,
published: None,
}
}
let post_form = PostForm {
name: data.name.trim().to_owned(),
url: data.url.to_owned(),
body: data.body.to_owned(),
nsfw: data.nsfw,
creator_id: orig_post.creator_id.to_owned(),
community_id: orig_post.community_id,
removed: Some(orig_post.removed),
deleted: Some(orig_post.deleted),
locked: Some(orig_post.locked),
stickied: Some(orig_post.stickied),
updated: Some(naive_now()),
embed_title: iframely_title,
embed_description: iframely_description,
embed_html: iframely_html,
thumbnail_url: pictrs_thumbnail,
ap_id: orig_post.ap_id,
local: orig_post.local,
published: None,
};
let edit_id = data.edit_id;
@ -656,58 +620,8 @@ impl Perform for Oper<EditPost> {
}
};
if moderators.contains(&user_id) {
// Mod tables
if let Some(removed) = data.removed.to_owned() {
let form = ModRemovePostForm {
mod_user_id: user_id,
post_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
}
if let Some(locked) = data.locked.to_owned() {
let form = ModLockPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
locked: Some(locked),
};
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
}
if let Some(stickied) = data.stickied.to_owned() {
let form = ModStickyPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
stickied: Some(stickied),
};
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
}
}
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_post.send_delete(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else if let Some(removed) = data.removed.to_owned() {
if moderators.contains(&user_id) {
if removed {
updated_post.send_remove(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
}
}
} else {
updated_post.send_update(&user, &self.client, pool).await?;
}
// Send apub update
updated_post.send_update(&user, &self.client, pool).await?;
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
@ -729,6 +643,324 @@ impl Perform for Oper<EditPost> {
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeletePost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &DeletePost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the creator can delete
if !Post::is_post_creator(user_id, orig_post.creator_id) {
return Err(APIError::err("no_post_edit_allowed").into());
}
// Update the post
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_post = blocking(pool, move |conn| {
Post::update_deleted(conn, edit_id, deleted)
})
.await??;
// apub updates
if deleted {
updated_post.send_delete(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_delete(&user, &self.client, pool)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::DeletePost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<RemovePost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &RemovePost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can remove
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let removed = data.removed;
let updated_post = blocking(pool, move |conn| {
Post::update_removed(conn, edit_id, removed)
})
.await??;
// Mod tables
let form = ModRemovePostForm {
mod_user_id: user_id,
post_id: data.edit_id,
removed: Some(removed),
reason: data.reason.to_owned(),
};
blocking(pool, move |conn| ModRemovePost::create(conn, &form)).await??;
// apub updates
if removed {
updated_post.send_remove(&user, &self.client, pool).await?;
} else {
updated_post
.send_undo_remove(&user, &self.client, pool)
.await?;
}
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::RemovePost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<LockPost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &LockPost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can lock
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let locked = data.locked;
let updated_post =
blocking(pool, move |conn| Post::update_locked(conn, edit_id, locked)).await??;
// Mod tables
let form = ModLockPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
locked: Some(locked),
};
blocking(pool, move |conn| ModLockPost::create(conn, &form)).await??;
// apub updates
updated_post.send_update(&user, &self.client, pool).await?;
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::LockPost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<StickyPost> {
type Response = PostResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PostResponse, LemmyError> {
let data: &StickyPost = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_post = blocking(pool, move |conn| Post::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check for a community ban
let community_id = orig_post.community_id;
let is_banned =
move |conn: &'_ _| CommunityUserBanView::get(conn, user_id, community_id).is_ok();
if blocking(pool, is_banned).await? {
return Err(APIError::err("community_ban").into());
}
// Verify that only the mods can sticky
is_mod_or_admin(pool, user_id, community_id).await?;
// Update the post
let edit_id = data.edit_id;
let stickied = data.stickied;
let updated_post = blocking(pool, move |conn| {
Post::update_stickied(conn, edit_id, stickied)
})
.await??;
// Mod tables
let form = ModStickyPostForm {
mod_user_id: user_id,
post_id: data.edit_id,
stickied: Some(stickied),
};
blocking(pool, move |conn| ModStickyPost::create(conn, &form)).await??;
// Apub updates
// TODO stickied should pry work like locked for ease of use
updated_post.send_update(&user, &self.client, pool).await?;
// Refetch the post
let edit_id = data.edit_id;
let post_view = blocking(pool, move |conn| {
PostView::read(conn, edit_id, Some(user_id))
})
.await??;
let res = PostResponse { post: post_view };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendPost {
op: UserOperation::StickyPost,
post: res.clone(),
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<SavePost> {
type Response = PostResponse;

View file

@ -1,6 +1,6 @@
use super::user::Register;
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::fetcher::search_by_apub_id,
blocking,
version,
@ -257,10 +257,7 @@ impl Perform for Oper<CreateSite> {
let user_id = claims.id;
// Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let site_form = SiteForm {
name: data.name.to_owned(),
@ -311,10 +308,7 @@ impl Perform for Oper<EditSite> {
let user_id = claims.id;
// Make sure user is an admin
let user = blocking(pool, move |conn| UserView::read(conn, user_id)).await??;
if !user.admin {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let found_site = blocking(pool, move |conn| Site::read(conn, 1)).await??;
@ -693,12 +687,7 @@ impl Perform for Oper<GetSiteConfig> {
let user_id = claims.id;
// Only let admins read this
let admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let admin_ids: Vec<i32> = admins.into_iter().map(|m| m.id).collect();
if !admin_ids.contains(&user_id) {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let config_hjson = Settings::read_config_file()?;

View file

@ -1,5 +1,5 @@
use crate::{
api::{claims::Claims, APIError, Oper, Perform},
api::{claims::Claims, is_admin, APIError, Oper, Perform},
apub::ApubObjectType,
blocking,
websocket::{
@ -110,7 +110,6 @@ pub struct GetUserDetailsResponse {
moderates: Vec<CommunityModeratorView>,
comments: Vec<CommentView>,
posts: Vec<PostView>,
admins: Vec<UserView>,
}
#[derive(Serialize, Deserialize)]
@ -174,9 +173,9 @@ pub struct GetUserMentions {
}
#[derive(Serialize, Deserialize)]
pub struct EditUserMention {
pub struct MarkUserMentionAsRead {
user_mention_id: i32,
read: Option<bool>,
read: bool,
auth: String,
}
@ -216,9 +215,21 @@ pub struct CreatePrivateMessage {
#[derive(Serialize, Deserialize)]
pub struct EditPrivateMessage {
edit_id: i32,
content: Option<String>,
deleted: Option<bool>,
read: Option<bool>,
content: String,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct DeletePrivateMessage {
edit_id: i32,
deleted: bool,
auth: String,
}
#[derive(Serialize, Deserialize)]
pub struct MarkPrivateMessageAsRead {
edit_id: i32,
read: bool,
auth: String,
}
@ -264,7 +275,7 @@ impl Perform for Oper<Login> {
// Fetch that username / email
let username_or_email = data.username_or_email.clone();
let user = match blocking(pool, move |conn| {
Claims::find_by_email_or_username(conn, &username_or_email)
User_::find_by_email_or_username(conn, &username_or_email)
})
.await?
{
@ -631,14 +642,6 @@ impl Perform for Oper<GetUserDetails> {
})
.await??;
let site_creator_id =
blocking(pool, move |conn| Site::read(conn, 1).map(|s| s.creator_id)).await??;
let mut admins = blocking(pool, move |conn| UserView::admins(conn)).await??;
let creator_index = admins.iter().position(|r| r.id == site_creator_id).unwrap();
let creator_user = admins.remove(creator_index);
admins.insert(0, creator_user);
// If its not the same user, remove the email, and settings
// TODO an if let chain would be better here, but can't figure it out
// TODO separate out settings into its own thing
@ -653,7 +656,6 @@ impl Perform for Oper<GetUserDetails> {
moderates,
comments,
posts,
admins,
})
}
}
@ -677,10 +679,7 @@ impl Perform for Oper<AddAdmin> {
let user_id = claims.id;
// Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let added = data.added;
let added_user_id = data.user_id;
@ -739,10 +738,7 @@ impl Perform for Oper<BanUser> {
let user_id = claims.id;
// Make sure user is an admin
let is_admin = move |conn: &'_ _| UserView::read(conn, user_id).map(|u| u.admin);
if !blocking(pool, is_admin).await?? {
return Err(APIError::err("not_an_admin").into());
}
is_admin(pool, user_id).await?;
let ban = data.ban;
let banned_user_id = data.user_id;
@ -862,7 +858,7 @@ impl Perform for Oper<GetUserMentions> {
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<EditUserMention> {
impl Perform for Oper<MarkUserMentionAsRead> {
type Response = UserMentionResponse;
async fn perform(
@ -870,7 +866,7 @@ impl Perform for Oper<EditUserMention> {
pool: &DbPool,
_websocket_info: Option<WebsocketInfo>,
) -> Result<UserMentionResponse, LemmyError> {
let data: &EditUserMention = &self.data;
let data: &MarkUserMentionAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
@ -887,15 +883,9 @@ impl Perform for Oper<EditUserMention> {
return Err(APIError::err("couldnt_update_comment").into());
}
let user_mention_form = UserMentionForm {
recipient_id: read_user_mention.recipient_id,
comment_id: read_user_mention.comment_id,
read: data.read.to_owned(),
};
let user_mention_id = read_user_mention.id;
let update_mention =
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &user_mention_form);
let read = data.read;
let update_mention = move |conn: &'_ _| UserMention::update_read(conn, user_mention_id, read);
if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
};
@ -940,70 +930,26 @@ impl Perform for Oper<MarkAllAsRead> {
.await??;
// TODO: this should probably be a bulk operation
// Not easy to do as a bulk operation,
// because recipient_id isn't in the comment table
for reply in &replies {
let reply_id = reply.id;
let mark_as_read = move |conn: &'_ _| Comment::mark_as_read(conn, reply_id);
let mark_as_read = move |conn: &'_ _| Comment::update_read(conn, reply_id, true);
if blocking(pool, mark_as_read).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
}
}
// Mentions
let mentions = blocking(pool, move |conn| {
UserMentionQueryBuilder::create(conn, user_id)
.unread_only(true)
.page(1)
.limit(999)
.list()
})
.await??;
// TODO: this should probably be a bulk operation
for mention in &mentions {
let mention_form = UserMentionForm {
recipient_id: mention.to_owned().recipient_id,
comment_id: mention.to_owned().id,
read: Some(true),
};
let user_mention_id = mention.user_mention_id;
let update_mention =
move |conn: &'_ _| UserMention::update(conn, user_mention_id, &mention_form);
if blocking(pool, update_mention).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
}
// Mark all user mentions as read
let update_user_mentions = move |conn: &'_ _| UserMention::mark_all_as_read(conn, user_id);
if blocking(pool, update_user_mentions).await?.is_err() {
return Err(APIError::err("couldnt_update_comment").into());
}
// messages
let messages = blocking(pool, move |conn| {
PrivateMessageQueryBuilder::create(conn, user_id)
.page(1)
.limit(999)
.unread_only(true)
.list()
})
.await??;
// TODO: this should probably be a bulk operation
for message in &messages {
let private_message_form = PrivateMessageForm {
content: message.to_owned().content,
creator_id: message.to_owned().creator_id,
recipient_id: message.to_owned().recipient_id,
deleted: None,
read: Some(true),
updated: None,
ap_id: message.to_owned().ap_id,
local: message.local,
published: None,
};
let message_id = message.id;
let update_pm =
move |conn: &'_ _| PrivateMessage::update(conn, message_id, &private_message_form);
if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
}
// Mark all private_messages as read
let update_pm = move |conn: &'_ _| PrivateMessage::mark_all_as_read(conn, user_id);
if blocking(pool, update_pm).await?.is_err() {
return Err(APIError::err("couldnt_update_private_message").into());
}
Ok(GetRepliesResponse { replies: vec![] })
@ -1293,59 +1239,25 @@ impl Perform for Oper<EditPrivateMessage> {
let user_id = claims.id;
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Check to make sure they are the creator (or the recipient marking as read
if !(data.read.is_some() && orig_private_message.recipient_id.eq(&user_id)
|| orig_private_message.creator_id.eq(&user_id))
{
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
}
let content_slurs_removed = match &data.content {
Some(content) => remove_slurs(content),
None => orig_private_message.content.clone(),
};
let private_message_form = {
if data.read.is_some() {
PrivateMessageForm {
content: orig_private_message.content.to_owned(),
creator_id: orig_private_message.creator_id,
recipient_id: orig_private_message.recipient_id,
read: data.read.to_owned(),
updated: orig_private_message.updated,
deleted: Some(orig_private_message.deleted),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
} else {
PrivateMessageForm {
content: content_slurs_removed,
creator_id: orig_private_message.creator_id,
recipient_id: orig_private_message.recipient_id,
deleted: data.deleted.to_owned(),
read: Some(orig_private_message.read),
updated: Some(naive_now()),
ap_id: orig_private_message.ap_id,
local: orig_private_message.local,
published: None,
}
}
};
// Doing the update
let content_slurs_removed = remove_slurs(&data.content);
let edit_id = data.edit_id;
let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update(conn, edit_id, &private_message_form)
PrivateMessage::update_content(conn, edit_id, &content_slurs_removed)
})
.await?
{
@ -1353,30 +1265,14 @@ impl Perform for Oper<EditPrivateMessage> {
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
if data.read.is_none() {
if let Some(deleted) = data.deleted.to_owned() {
if deleted {
updated_private_message
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_private_message
.send_undo_delete(&user, &self.client, pool)
.await?;
}
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
} else {
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
}
// Send the apub update
updated_private_message
.send_update(&user, &self.client, pool)
.await?;
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
@ -1384,7 +1280,146 @@ impl Perform for Oper<EditPrivateMessage> {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::EditPrivateMessage,
response: res.clone(),
recipient_id: orig_private_message.recipient_id,
recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<DeletePrivateMessage> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &DeletePrivateMessage = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.creator_id {
return Err(APIError::err("no_private_message_edit_allowed").into());
}
// Doing the update
let edit_id = data.edit_id;
let deleted = data.deleted;
let updated_private_message = match blocking(pool, move |conn| {
PrivateMessage::update_deleted(conn, edit_id, deleted)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// Send the apub update
if data.deleted {
updated_private_message
.send_delete(&user, &self.client, pool)
.await?;
} else {
updated_private_message
.send_undo_delete(&user, &self.client, pool)
.await?;
}
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::DeletePrivateMessage,
response: res.clone(),
recipient_id,
my_id: ws.id,
});
}
Ok(res)
}
}
#[async_trait::async_trait(?Send)]
impl Perform for Oper<MarkPrivateMessageAsRead> {
type Response = PrivateMessageResponse;
async fn perform(
&self,
pool: &DbPool,
websocket_info: Option<WebsocketInfo>,
) -> Result<PrivateMessageResponse, LemmyError> {
let data: &MarkPrivateMessageAsRead = &self.data;
let claims = match Claims::decode(&data.auth) {
Ok(claims) => claims.claims,
Err(_e) => return Err(APIError::err("not_logged_in").into()),
};
let user_id = claims.id;
// Check for a site ban
let user = blocking(pool, move |conn| User_::read(conn, user_id)).await??;
if user.banned {
return Err(APIError::err("site_ban").into());
}
// Checking permissions
let edit_id = data.edit_id;
let orig_private_message =
blocking(pool, move |conn| PrivateMessage::read(conn, edit_id)).await??;
if user_id != orig_private_message.recipient_id {
return Err(APIError::err("couldnt_update_private_message").into());
}
// Doing the update
let edit_id = data.edit_id;
let read = data.read;
match blocking(pool, move |conn| {
PrivateMessage::update_read(conn, edit_id, read)
})
.await?
{
Ok(private_message) => private_message,
Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
};
// No need to send an apub update
let edit_id = data.edit_id;
let message = blocking(pool, move |conn| PrivateMessageView::read(conn, edit_id)).await??;
let recipient_id = message.recipient_id;
let res = PrivateMessageResponse { message };
if let Some(ws) = websocket_info {
ws.chatserver.do_send(SendUserRoomMessage {
op: UserOperation::MarkPrivateMessageAsRead,
response: res.clone(),
recipient_id,
my_id: ws.id,
});
}

View file

@ -393,7 +393,7 @@ async fn receive_create_comment(
// anyway.
let mentions = scrape_text_for_mentions(&inserted_comment.content);
let recipient_ids =
send_local_notifs(mentions, inserted_comment.clone(), user, post, pool).await?;
send_local_notifs(mentions, inserted_comment.clone(), user, post, pool, true).await?;
// Refetch the view
let comment_view = blocking(pool, move |conn| {
@ -404,6 +404,7 @@ async fn receive_create_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -558,7 +559,7 @@ async fn receive_update_comment(
let post = blocking(pool, move |conn| Post::read(conn, post_id)).await??;
let mentions = scrape_text_for_mentions(&updated_comment.content);
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool).await?;
let recipient_ids = send_local_notifs(mentions, updated_comment, user, post, pool, false).await?;
// Refetch the view
let comment_view =
@ -567,6 +568,7 @@ async fn receive_update_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -616,6 +618,7 @@ async fn receive_like_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -665,6 +668,7 @@ async fn receive_dislike_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -960,6 +964,7 @@ async fn receive_delete_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -1017,6 +1022,7 @@ async fn receive_remove_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -1108,6 +1114,7 @@ async fn receive_undo_delete_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -1165,6 +1172,7 @@ async fn receive_undo_remove_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {
@ -1464,6 +1472,7 @@ async fn receive_undo_like_comment(
let res = CommentResponse {
comment: comment_view,
recipient_ids,
form_id: None,
};
chat_server.do_send(SendComment {

View file

@ -1,5 +1,4 @@
use crate::{
api::claims::Claims,
apub::{
activities::send_activity,
create_apub_response,
@ -253,7 +252,7 @@ pub async fn get_apub_user_http(
) -> Result<HttpResponse<Body>, LemmyError> {
let user_name = info.into_inner().user_name;
let user = blocking(&db, move |conn| {
Claims::find_by_email_or_username(conn, &user_name)
User_::find_by_email_or_username(conn, &user_name)
})
.await??;
let u = user.to_apub(&db).await?;

View file

@ -53,7 +53,9 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.route("", web::put().to(route_post::<EditCommunity>))
.route("/list", web::get().to(route_get::<ListCommunities>))
.route("/follow", web::post().to(route_post::<FollowCommunity>))
.route("/delete", web::post().to(route_post::<DeleteCommunity>))
// Mod Actions
.route("/remove", web::post().to(route_post::<RemoveCommunity>))
.route("/transfer", web::post().to(route_post::<TransferCommunity>))
.route("/ban_user", web::post().to(route_post::<BanFromCommunity>))
.route("/mod", web::post().to(route_post::<AddModToCommunity>)),
@ -71,6 +73,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetPost>))
.route("", web::put().to(route_post::<EditPost>))
.route("/delete", web::post().to(route_post::<DeletePost>))
.route("/remove", web::post().to(route_post::<RemovePost>))
.route("/lock", web::post().to(route_post::<LockPost>))
.route("/sticky", web::post().to(route_post::<StickyPost>))
.route("/list", web::get().to(route_get::<GetPosts>))
.route("/like", web::post().to(route_post::<CreatePostLike>))
.route("/save", web::put().to(route_post::<SavePost>)),
@ -81,6 +87,12 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("", web::post().to(route_post::<CreateComment>))
.route("", web::put().to(route_post::<EditComment>))
.route("/delete", web::post().to(route_post::<DeleteComment>))
.route("/remove", web::post().to(route_post::<RemoveComment>))
.route(
"/mark_as_read",
web::post().to(route_post::<MarkCommentAsRead>),
)
.route("/like", web::post().to(route_post::<CreateCommentLike>))
.route("/save", web::put().to(route_post::<SaveComment>)),
)
@ -90,7 +102,15 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("/list", web::get().to(route_get::<GetPrivateMessages>))
.route("", web::post().to(route_post::<CreatePrivateMessage>))
.route("", web::put().to(route_post::<EditPrivateMessage>)),
.route("", web::put().to(route_post::<EditPrivateMessage>))
.route(
"/delete",
web::post().to(route_post::<DeletePrivateMessage>),
)
.route(
"/mark_as_read",
web::post().to(route_post::<MarkPrivateMessageAsRead>),
),
)
// User
.service(
@ -107,7 +127,10 @@ pub fn config(cfg: &mut web::ServiceConfig, rate_limit: &RateLimit) {
.wrap(rate_limit.message())
.route("", web::get().to(route_get::<GetUserDetails>))
.route("/mention", web::get().to(route_get::<GetUserMentions>))
.route("/mention", web::put().to(route_post::<EditUserMention>))
.route(
"/mention/mark_as_read",
web::post().to(route_post::<MarkUserMentionAsRead>),
)
.route("/replies", web::get().to(route_get::<GetReplies>))
.route(
"/followed_communities",

View file

@ -1 +1 @@
pub const VERSION: &str = "v0.7.26";
pub const VERSION: &str = "v0.7.28";

View file

@ -28,19 +28,28 @@ pub enum UserOperation {
GetCommunity,
CreateComment,
EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost,
EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
MarkUserMentionAsRead,
GetModlog,
BanFromCommunity,
AddModToCommunity,
@ -59,6 +68,8 @@ pub enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,

View file

@ -212,6 +212,9 @@ impl ChatServer {
// Also leave all communities
// This avoids double messages
// TODO found a bug, whereby community messages like
// delete and remove aren't sent, because
// you left the community room
for sessions in self.community_rooms.values_mut() {
sessions.remove(&id);
}
@ -443,18 +446,28 @@ impl ChatServer {
UserOperation::AddAdmin => do_user_operation::<AddAdmin>(args).await,
UserOperation::BanUser => do_user_operation::<BanUser>(args).await,
UserOperation::GetUserMentions => do_user_operation::<GetUserMentions>(args).await,
UserOperation::EditUserMention => do_user_operation::<EditUserMention>(args).await,
UserOperation::MarkUserMentionAsRead => {
do_user_operation::<MarkUserMentionAsRead>(args).await
}
UserOperation::MarkAllAsRead => do_user_operation::<MarkAllAsRead>(args).await,
UserOperation::DeleteAccount => do_user_operation::<DeleteAccount>(args).await,
UserOperation::PasswordReset => do_user_operation::<PasswordReset>(args).await,
UserOperation::PasswordChange => do_user_operation::<PasswordChange>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Private Message ops
UserOperation::CreatePrivateMessage => {
do_user_operation::<CreatePrivateMessage>(args).await
}
UserOperation::EditPrivateMessage => do_user_operation::<EditPrivateMessage>(args).await,
UserOperation::DeletePrivateMessage => {
do_user_operation::<DeletePrivateMessage>(args).await
}
UserOperation::MarkPrivateMessageAsRead => {
do_user_operation::<MarkPrivateMessageAsRead>(args).await
}
UserOperation::GetPrivateMessages => do_user_operation::<GetPrivateMessages>(args).await,
UserOperation::UserJoin => do_user_operation::<UserJoin>(args).await,
UserOperation::SaveUserSettings => do_user_operation::<SaveUserSettings>(args).await,
// Site ops
UserOperation::GetModlog => do_user_operation::<GetModlog>(args).await,
@ -473,6 +486,8 @@ impl ChatServer {
UserOperation::ListCommunities => do_user_operation::<ListCommunities>(args).await,
UserOperation::CreateCommunity => do_user_operation::<CreateCommunity>(args).await,
UserOperation::EditCommunity => do_user_operation::<EditCommunity>(args).await,
UserOperation::DeleteCommunity => do_user_operation::<DeleteCommunity>(args).await,
UserOperation::RemoveCommunity => do_user_operation::<RemoveCommunity>(args).await,
UserOperation::FollowCommunity => do_user_operation::<FollowCommunity>(args).await,
UserOperation::GetFollowedCommunities => {
do_user_operation::<GetFollowedCommunities>(args).await
@ -485,12 +500,19 @@ impl ChatServer {
UserOperation::GetPost => do_user_operation::<GetPost>(args).await,
UserOperation::GetPosts => do_user_operation::<GetPosts>(args).await,
UserOperation::EditPost => do_user_operation::<EditPost>(args).await,
UserOperation::DeletePost => do_user_operation::<DeletePost>(args).await,
UserOperation::RemovePost => do_user_operation::<RemovePost>(args).await,
UserOperation::LockPost => do_user_operation::<LockPost>(args).await,
UserOperation::StickyPost => do_user_operation::<StickyPost>(args).await,
UserOperation::CreatePostLike => do_user_operation::<CreatePostLike>(args).await,
UserOperation::SavePost => do_user_operation::<SavePost>(args).await,
// Comment ops
UserOperation::CreateComment => do_user_operation::<CreateComment>(args).await,
UserOperation::EditComment => do_user_operation::<EditComment>(args).await,
UserOperation::DeleteComment => do_user_operation::<DeleteComment>(args).await,
UserOperation::RemoveComment => do_user_operation::<RemoveComment>(args).await,
UserOperation::MarkCommentAsRead => do_user_operation::<MarkCommentAsRead>(args).await,
UserOperation::SaveComment => do_user_operation::<SaveComment>(args).await,
UserOperation::GetComments => do_user_operation::<GetComments>(args).await,
UserOperation::CreateCommentLike => do_user_operation::<CreateCommentLike>(args).await,

View file

@ -2,6 +2,11 @@
border: 0px;
}
.navbar-expand-lg .navbar-nav .nav-link {
padding-right: .75rem !important;
padding-left: .75rem !important;
}
.pointer {
cursor: pointer;
}
@ -134,12 +139,14 @@ blockquote {
.thumbnail {
object-fit: cover;
min-height: 60px;
max-height: 80px;
width: 100%;
}
svg.thumbnail {
height: 40px;
.thumbnail svg {
height: 1.25rem;
width: 1.25rem;
}
.no-s-hows {

View file

@ -1,17 +1,17 @@
$white: #ffffff;
$orange: #faa077;
$orange: #f1641e;
$cyan: #02bdc2;
$green: #d4e9d7;
$green: #00C853;
$secondary: $green;
$body-color: $gray-700;
$link-color: theme-color("danger");;
$link-color: theme-color("primary");;
$primary: $orange;
$red: #d8486a;
$border-radius: 1.5rem;
$border-radius-lg: 1.5rem;
$border-radius: 0.5rem;
$border-radius-lg: 0.5rem;
$border-radius-sm: 1rem;
$font-family-sans-serif: Guardian-EgypTT,serif,-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
$font-family-sans-serif: -apple-system,BlinkMacSystemFont,"Droid Sans","Segoe UI","Helvetica",Arial,sans-serif;
$headings-color: $gray-700;
$input-btn-focus-color: rgba($component-active-bg, .75);
$form-feedback-valid-color: theme-color("info");
@ -21,10 +21,13 @@ $navbar-dark-toggler-border-color: rgba($black, .1);
$navbar-light-active-color: $gray-900;
$card-color: $gray-700;
$card-cap-color: $gray-700;
$info: darken($green, 25%);;
$body-bg: #f2f0f0;
$success: darken($green, 25%);;
$info: $blue;
$body-bg: #fff;
$success: $indigo;
$danger: darken($primary, 25%);
$navbar-light-hover-color: $gray-900;
$card-bg: $gray-100;
$border-color: $gray-700;
$border-color: $gray-700;
$mark-bg: rgb(255, 252, 239);
$font-weight-bold: 600;
$rounded-pill: 0.25rem;

File diff suppressed because one or more lines are too long

View file

@ -4,22 +4,28 @@ import {
LoginForm,
LoginResponse,
PostForm,
DeletePostForm,
RemovePostForm,
// TODO need to test LockPost and StickyPost federated
PostResponse,
SearchResponse,
FollowCommunityForm,
CommunityResponse,
GetFollowedCommunitiesResponse,
GetPostForm,
GetPostResponse,
CommentForm,
DeleteCommentForm,
RemoveCommentForm,
CommentResponse,
CommunityForm,
GetCommunityForm,
DeleteCommunityForm,
RemoveCommunityForm,
GetCommunityResponse,
CommentLikeForm,
CreatePostLikeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
PrivateMessageResponse,
PrivateMessagesResponse,
GetUserMentionsResponse,
@ -97,7 +103,6 @@ describe('main', () => {
name,
auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};
@ -266,7 +271,6 @@ describe('main', () => {
name,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
@ -323,7 +327,6 @@ describe('main', () => {
edit_id: 2,
auth: lemmyAlphaAuth,
community_id: 3,
creator_id: 2,
nsfw: false,
};
@ -382,7 +385,6 @@ describe('main', () => {
let unlikeCommentForm: CommentLikeForm = {
comment_id: createResponse.comment.id,
score: 0,
post_id: 2,
auth: lemmyAlphaAuth,
};
@ -585,7 +587,6 @@ describe('main', () => {
name: postName,
auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false,
};
@ -620,19 +621,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta deletes the comment
let deleteCommentForm: CommentForm = {
content: commentContent,
let deleteCommentForm: DeleteCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: true,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let deleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -649,19 +647,16 @@ describe('main', () => {
expect(getPostRes.comments[0].deleted).toBe(true);
// lemmy_beta undeletes the comment
let undeleteCommentForm: CommentForm = {
content: commentContent,
let undeleteCommentForm: DeleteCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
deleted: false,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let undeleteCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -677,23 +672,22 @@ describe('main', () => {
expect(getPostUndeleteRes.comments[0].deleted).toBe(false);
// lemmy_beta deletes the post
let deletePostForm: PostForm = {
name: postName,
let deletePostForm: DeletePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: true,
auth: lemmyBetaAuth,
};
let deletePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(deletePostForm),
}).then(d => d.json());
let deletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post/delete`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(deletePostForm),
}
).then(d => d.json());
expect(deletePostRes.post.deleted).toBe(true);
// Make sure lemmy_alpha sees the post is deleted
@ -703,20 +697,16 @@ describe('main', () => {
expect(getPostResAgain.post.deleted).toBe(true);
// lemmy_beta undeletes the post
let undeletePostForm: PostForm = {
name: postName,
let undeletePostForm: DeletePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
deleted: false,
auth: lemmyBetaAuth,
};
let undeletePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`,
`${lemmyBetaApiUrl}/post/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -732,20 +722,16 @@ describe('main', () => {
expect(getPostResAgainTwo.post.deleted).toBe(false);
// lemmy_beta deletes the community
let deleteCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let deleteCommunityForm: DeleteCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: true,
auth: lemmyBetaAuth,
};
let deleteResponse: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -765,20 +751,16 @@ describe('main', () => {
expect(getCommunityRes.community.deleted).toBe(true);
// lemmy_beta undeletes the community
let undeleteCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let undeleteCommunityForm: DeleteCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
deleted: false,
auth: lemmyBetaAuth,
};
let undeleteCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -861,7 +843,6 @@ describe('main', () => {
name: postName,
auth: lemmyBetaAuth,
community_id: createCommunityRes.community.id,
creator_id: 2,
nsfw: false,
};
@ -896,19 +877,16 @@ describe('main', () => {
expect(createCommentRes.comment.content).toBe(commentContent);
// lemmy_beta removes the comment
let removeCommentForm: CommentForm = {
content: commentContent,
let removeCommentForm: RemoveCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: true,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let removeCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -925,19 +903,16 @@ describe('main', () => {
expect(getPostRes.comments[0].removed).toBe(true);
// lemmy_beta undeletes the comment
let unremoveCommentForm: CommentForm = {
content: commentContent,
let unremoveCommentForm: RemoveCommentForm = {
edit_id: createCommentRes.comment.id,
post_id: createPostRes.post.id,
removed: false,
auth: lemmyBetaAuth,
creator_id: createCommentRes.comment.creator_id,
};
let unremoveCommentRes: CommentResponse = await fetch(
`${lemmyBetaApiUrl}/comment`,
`${lemmyBetaApiUrl}/comment/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -953,23 +928,22 @@ describe('main', () => {
expect(getPostUnremoveRes.comments[0].removed).toBe(false);
// lemmy_beta deletes the post
let removePostForm: PostForm = {
name: postName,
let removePostForm: RemovePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: true,
auth: lemmyBetaAuth,
};
let removePostRes: PostResponse = await fetch(`${lemmyBetaApiUrl}/post`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(removePostForm),
}).then(d => d.json());
let removePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post/remove`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: wrapper(removePostForm),
}
).then(d => d.json());
expect(removePostRes.post.removed).toBe(true);
// Make sure lemmy_alpha sees the post is deleted
@ -979,20 +953,16 @@ describe('main', () => {
expect(getPostResAgain.post.removed).toBe(true);
// lemmy_beta unremoves the post
let unremovePostForm: PostForm = {
name: postName,
let unremovePostForm: RemovePostForm = {
edit_id: createPostRes.post.id,
auth: lemmyBetaAuth,
community_id: createPostRes.post.community_id,
creator_id: createPostRes.post.creator_id,
nsfw: false,
removed: false,
auth: lemmyBetaAuth,
};
let unremovePostRes: PostResponse = await fetch(
`${lemmyBetaApiUrl}/post`,
`${lemmyBetaApiUrl}/post/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1007,21 +977,17 @@ describe('main', () => {
}).then(d => d.json());
expect(getPostResAgainTwo.post.removed).toBe(false);
// lemmy_beta deletes the community
let removeCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
// lemmy_beta removes the community
let removeCommunityForm: RemoveCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
removed: true,
auth: lemmyBetaAuth,
};
let removeCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1029,7 +995,7 @@ describe('main', () => {
}
).then(d => d.json());
// Make sure the delete went through
// Make sure the remove went through
expect(removeCommunityRes.community.removed).toBe(true);
// Re-get it from alpha, make sure its removed there too
@ -1041,20 +1007,16 @@ describe('main', () => {
expect(getCommunityRes.community.removed).toBe(true);
// lemmy_beta unremoves the community
let unremoveCommunityForm: CommunityForm = {
name: communityName,
title: communityName,
category_id: 1,
let unremoveCommunityForm: RemoveCommunityForm = {
edit_id: createCommunityRes.community.id,
nsfw: false,
removed: false,
auth: lemmyBetaAuth,
};
let unremoveCommunityRes: CommunityResponse = await fetch(
`${lemmyBetaApiUrl}/community`,
`${lemmyBetaApiUrl}/community/remove`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1149,16 +1111,16 @@ describe('main', () => {
);
// lemmy alpha deletes the private message
let deletePrivateMessageForm: EditPrivateMessageForm = {
let deletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: true,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let deleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1182,16 +1144,16 @@ describe('main', () => {
expect(getPrivateMessagesDeletedRes.messages.length).toBe(0);
// lemmy alpha undeletes the private message
let undeletePrivateMessageForm: EditPrivateMessageForm = {
let undeletePrivateMessageForm: DeletePrivateMessageForm = {
deleted: false,
edit_id: createRes.message.id,
auth: lemmyAlphaAuth,
};
let undeleteRes: PrivateMessageResponse = await fetch(
`${lemmyAlphaApiUrl}/private_message`,
`${lemmyAlphaApiUrl}/private_message/delete`,
{
method: 'PUT',
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
@ -1252,7 +1214,6 @@ describe('main', () => {
name: postName,
auth: lemmyAlphaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};
@ -1363,7 +1324,6 @@ describe('main', () => {
name: betaPostName,
auth: lemmyBetaAuth,
community_id: 2,
creator_id: 2,
nsfw: false,
};

View file

@ -115,34 +115,9 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
);
}
handleFinished(op: UserOperation, data: CommentResponse) {
let isReply =
this.props.node !== undefined && data.comment.parent_id !== null;
let xor =
+!(data.comment.parent_id !== null) ^ +(this.props.node !== undefined);
if (
(data.comment.creator_id == UserService.Instance.user.id &&
((op == UserOperation.CreateComment &&
// If its a reply, make sure parent child match
isReply &&
data.comment.parent_id == this.props.node.comment.id) ||
// Otherwise, check the XOR of the two
(!isReply && xor))) ||
// If its a comment edit, only check that its from your user, and that its a
// text edit only
(data.comment.creator_id == UserService.Instance.user.id &&
op == UserOperation.EditComment &&
data.comment.content)
) {
this.state.finished = true;
this.setState(this.state);
}
}
handleCommentSubmit(val: string) {
this.state.commentForm.content = val;
handleCommentSubmit(msg: { val: string; formId: string }) {
this.state.commentForm.content = msg.val;
this.state.commentForm.form_id = msg.formId;
if (this.props.edit) {
WebSocketService.Instance.editComment(this.state.commentForm);
} else {
@ -160,12 +135,16 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
// Only do the showing and hiding if logged in
if (UserService.Instance.user) {
if (res.op == UserOperation.CreateComment) {
if (
res.op == UserOperation.CreateComment ||
res.op == UserOperation.EditComment
) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
} else if (res.op == UserOperation.EditComment) {
let data = res.data as CommentResponse;
this.handleFinished(res.op, data);
// This only finishes this form, if the randomly generated form_id matches the one received
if (this.state.commentForm.form_id == data.form_id) {
this.setState({ finished: true });
}
}
}
}

View file

@ -3,8 +3,10 @@ import { Link } from 'inferno-router';
import {
CommentNode as CommentNodeI,
CommentLikeForm,
CommentForm as CommentFormI,
EditUserMentionForm,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentAsReadForm,
MarkUserMentionAsReadForm,
SaveCommentForm,
BanFromCommunityForm,
BanUserForm,
@ -202,7 +204,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
</>
)}
<button
class="btn btn-sm text-muted"
class="btn text-muted"
onClick={linkEvent(this, this.handleCommentCollapse)}
>
{this.state.collapsed ? (
@ -218,7 +220,7 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
{/* This is an expanding spacer for mobile */}
<div className="mr-lg-4 flex-grow-1 flex-lg-grow-0 unselectable pointer mx-2"></div>
<button
className={`btn btn-sm p-0 unselectable pointer ${this.scoreColor}`}
className={`btn p-0 unselectable pointer ${this.scoreColor}`}
onClick={linkEvent(node, this.handleCommentUpvote)}
data-tippy-content={this.pointsTippy}
>
@ -848,16 +850,12 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
}
handleDeleteClick(i: CommentNode) {
let deleteForm: CommentFormI = {
content: i.props.node.comment.content,
let deleteForm: DeleteCommentForm = {
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
deleted: !i.props.node.comment.deleted,
auth: null,
};
WebSocketService.Instance.editComment(deleteForm);
WebSocketService.Instance.deleteComment(deleteForm);
}
handleSaveCommentClick(i: CommentNode) {
@ -901,7 +899,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = {
comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote,
};
@ -929,7 +926,6 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
let form: CommentLikeForm = {
comment_id: i.comment.id,
post_id: i.comment.post_id,
score: this.state.my_vote,
};
@ -950,17 +946,13 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleModRemoveSubmit(i: CommentNode) {
event.preventDefault();
let form: CommentFormI = {
content: i.props.node.comment.content,
let form: RemoveCommentForm = {
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
removed: !i.props.node.comment.removed,
reason: i.state.removeReason,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.removeComment(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
@ -969,22 +961,18 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
handleMarkRead(i: CommentNode) {
// if it has a user_mention_id field, then its a mention
if (i.props.node.comment.user_mention_id) {
let form: EditUserMentionForm = {
let form: MarkUserMentionAsReadForm = {
user_mention_id: i.props.node.comment.user_mention_id,
read: !i.props.node.comment.read,
};
WebSocketService.Instance.editUserMention(form);
WebSocketService.Instance.markUserMentionAsRead(form);
} else {
let form: CommentFormI = {
content: i.props.node.comment.content,
let form: MarkCommentAsReadForm = {
edit_id: i.props.node.comment.id,
creator_id: i.props.node.comment.creator_id,
post_id: i.props.node.comment.post_id,
parent_id: i.props.node.comment.parent_id,
read: !i.props.node.comment.read,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.markCommentAsRead(form);
}
i.state.readLoading = true;

View file

@ -157,7 +157,7 @@ export class Communities extends Component<any, CommunitiesState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -166,7 +166,7 @@ export class Communities extends Component<any, CommunitiesState> {
{this.state.communities.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}

View file

@ -271,7 +271,7 @@ export class Community extends Component<any, State> {
<div class="my-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -279,7 +279,7 @@ export class Community extends Component<any, State> {
)}
{this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -355,12 +355,15 @@ export class Community extends Component<any, State> {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.admins = data.admins;
this.state.online = data.online;
document.title = `/c/${this.state.community.name} - ${this.state.site.name}`;
this.setState(this.state);
this.fetchData();
} else if (res.op == UserOperation.EditCommunity) {
} else if (
res.op == UserOperation.EditCommunity ||
res.op == UserOperation.DeleteCommunity ||
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse;
this.state.community = data.community;
this.setState(this.state);
@ -376,7 +379,13 @@ export class Community extends Component<any, State> {
this.state.loading = false;
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditPost) {
} else if (
res.op == UserOperation.EditPost ||
res.op == UserOperation.DeletePost ||
res.op == UserOperation.RemovePost ||
res.op == UserOperation.LockPost ||
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse;
editPostFindRes(data, this.state.posts);
this.setState(this.state);
@ -405,7 +414,11 @@ export class Community extends Component<any, State> {
this.state.comments = data.comments;
this.state.loading = false;
this.setState(this.state);
} else if (res.op == UserOperation.EditComment) {
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -428,6 +441,7 @@ export class Community extends Component<any, State> {
} else if (res.op == UserOperation.GetSite) {
let data = res.data as GetSiteResponse;
this.state.site = data.site;
this.state.admins = data.admins;
this.setState(this.state);
}
}

View file

@ -35,7 +35,7 @@ export class DataTypeSelect extends Component<
return (
<div class="btn-group btn-group-toggle">
<label
className={`pointer btn btn-sm btn-secondary
className={`pointer btn btn-outline-secondary
${this.state.type_ == DataType.Post && 'active'}
`}
>
@ -48,7 +48,7 @@ export class DataTypeSelect extends Component<
{i18n.t('posts')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${
className={`pointer btn btn-outline-secondary ${
this.state.type_ == DataType.Comment && 'active'
}`}
>

View file

@ -29,7 +29,7 @@ export class IFramelyCard extends Component<
return (
<>
{post.embed_title && !this.state.expanded && (
<div class="card mt-3 mb-2">
<div class="card bg-transparent border-secondary mt-3 mb-2">
<div class="row">
<div class="col-12">
<div class="card-body">

View file

@ -147,7 +147,7 @@ export class Inbox extends Component<any, InboxState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.Unread && 'active'}
`}
>
@ -160,7 +160,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('unread')}
</label>
<label
className={`btn btn-sm btn-secondary pointer
className={`btn btn-outline-secondary pointer
${this.state.unreadOrAll == UnreadOrAll.All && 'active'}
`}
>
@ -180,7 +180,7 @@ export class Inbox extends Component<any, InboxState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.All && 'active'}
`}
>
@ -193,7 +193,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('all')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Replies && 'active'}
`}
>
@ -206,7 +206,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('replies')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Mentions && 'active'}
`}
>
@ -219,7 +219,7 @@ export class Inbox extends Component<any, InboxState> {
{i18n.t('mentions')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.messageType == MessageType.Messages && 'active'}
`}
>
@ -326,7 +326,7 @@ export class Inbox extends Component<any, InboxState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -334,7 +334,7 @@ export class Inbox extends Component<any, InboxState> {
)}
{this.unreadCount() > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -446,27 +446,54 @@ export class Inbox extends Component<any, InboxState> {
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
found.content = data.message.content;
found.updated = data.message.updated;
found.deleted = data.message.deleted;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
this.state.messages = this.state.messages.filter(
r => r.id !== data.message.id
);
} else {
let found = this.state.messages.find(c => c.id == data.message.id);
found.read = data.message.read;
if (found) {
found.content = data.message.content;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.DeletePrivateMessage) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.deleted = data.message.deleted;
found.updated = data.message.updated;
}
this.setState(this.state);
} else if (res.op == UserOperation.MarkPrivateMessageAsRead) {
let data = res.data as PrivateMessageResponse;
let found: PrivateMessageI = this.state.messages.find(
m => m.id === data.message.id
);
if (found) {
found.updated = data.message.updated;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.message.read) {
this.state.messages = this.state.messages.filter(
r => r.id !== data.message.id
);
} else {
let found = this.state.messages.find(c => c.id == data.message.id);
found.read = data.message.read;
}
}
this.sendUnreadCount();
window.scrollTo(0, 0);
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.MarkAllAsRead) {
// Moved to be instant
} else if (res.op == UserOperation.EditComment) {
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.replies);
this.setState(this.state);
} else if (res.op == UserOperation.MarkCommentAsRead) {
let data = res.data as CommentResponse;
// If youre in the unread view, just remove it from the list
if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
@ -480,7 +507,7 @@ export class Inbox extends Component<any, InboxState> {
this.sendUnreadCount();
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditUserMention) {
} else if (res.op == UserOperation.MarkUserMentionAsRead) {
let data = res.data as UserMentionResponse;
let found = this.state.mentions.find(c => c.id == data.mention.id);

View file

@ -36,7 +36,7 @@ export class ListingTypeSelect extends Component<
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary
className={`btn btn-outline-secondary
${this.state.type_ == ListingType.Subscribed && 'active'}
${UserService.Instance.user == undefined ? 'disabled' : 'pointer'}
`}
@ -51,7 +51,7 @@ export class ListingTypeSelect extends Component<
{i18n.t('subscribed')}
</label>
<label
className={`pointer btn btn-sm btn-secondary ${
className={`pointer btn btn-outline-secondary ${
this.state.type_ == ListingType.All && 'active'
}`}
>

View file

@ -195,7 +195,7 @@ export class Main extends Component<any, MainState> {
<div>
{!this.state.loading && (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
{this.trendingCommunities()}
{UserService.Instance.user &&
@ -226,7 +226,7 @@ export class Main extends Component<any, MainState> {
</div>
)}
<Link
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
to="/create_community"
>
{i18n.t('create_a_community')}
@ -295,7 +295,7 @@ export class Main extends Component<any, MainState> {
siteInfo() {
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5 class="mb-0">{`${this.state.siteRes.site.name}`}</h5>
{this.canAdmin && (
@ -315,32 +315,32 @@ export class Main extends Component<any, MainState> {
)}
<ul class="my-2 list-inline">
{/*
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_online', { count: this.state.siteRes.online })}
</li>
*/}
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_users', {
count: this.state.siteRes.site.number_of_users,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_communities', {
count: this.state.siteRes.site.number_of_communities,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_posts', {
count: this.state.siteRes.site.number_of_posts,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_comments', {
count: this.state.siteRes.site.number_of_comments,
})}
</li>
<li className="list-inline-item">
<Link className="badge badge-secondary" to="/modlog">
<Link className="badge badge-light" to="/modlog">
{i18n.t('modlog')}
</Link>
</li>
@ -364,7 +364,7 @@ export class Main extends Component<any, MainState> {
</div>
</div>
{this.state.siteRes.site.description && (
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<div
className="md-div"
@ -381,7 +381,7 @@ export class Main extends Component<any, MainState> {
landing() {
return (
<div class="card border-secondary">
<div class="card bg-transparent border-secondary">
<div class="card-body">
<h5>
{i18n.t('powered_by')}
@ -517,7 +517,7 @@ export class Main extends Component<any, MainState> {
<div class="my-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -525,7 +525,7 @@ export class Main extends Component<any, MainState> {
)}
{this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -702,7 +702,11 @@ export class Main extends Component<any, MainState> {
this.state.comments = data.comments;
this.state.loading = false;
this.setState(this.state);
} else if (res.op == UserOperation.EditComment) {
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);

View file

@ -21,7 +21,7 @@ interface MarkdownTextAreaProps {
replyType?: boolean;
focus?: boolean;
disabled?: boolean;
onSubmit?(val: string): any;
onSubmit?(msg: { val: string; formId: string }): any;
onContentChange?(val: string): any;
onReplyCancel?(): any;
}
@ -125,7 +125,7 @@ export class MarkdownTextArea extends Component<
/>
{this.state.previewMode && (
<div
className="card card-body md-div"
className="card bg-transparent border-secondary card-body md-div"
dangerouslySetInnerHTML={mdToHtml(this.state.content)}
/>
)}
@ -391,7 +391,8 @@ export class MarkdownTextArea extends Component<
event.preventDefault();
i.state.loading = true;
i.setState(i.state);
i.props.onSubmit(i.state.content);
let msg = { val: i.state.content, formId: i.formId };
i.props.onSubmit(msg);
}
handleReplyCancel(i: MarkdownTextArea) {

View file

@ -384,14 +384,14 @@ export class Modlog extends Component<any, ModlogState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
</button>
)}
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}

View file

@ -150,179 +150,181 @@ export class Navbar extends Component<any, NavbarState> {
// TODO class active corresponding to current page
navbar() {
return (
<nav class="container-fluid navbar navbar-expand-md navbar-light shadow p-0 px-3">
<Link title={this.state.version} class="navbar-brand" to="/">
{this.state.siteName}
</Link>
{this.state.isLoggedIn && (
<Link
class="ml-auto p-0 navbar-toggler nav-link"
to="/inbox"
title={i18n.t('inbox')}
>
<svg class="icon">
<use xlinkHref="#icon-bell"></use>
</svg>
{this.state.unreadCount > 0 && (
<span class="ml-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
<nav class="navbar navbar-expand-lg navbar-light shadow-sm p-0 px-3">
<div class="container">
<Link title={this.state.version} class="navbar-brand" to="/">
{this.state.siteName}
</Link>
)}
<button
class="navbar-toggler"
type="button"
aria-label="menu"
onClick={linkEvent(this, this.expandNavbar)}
data-tippy-content={i18n.t('expand_here')}
>
<span class="navbar-toggler-icon"></span>
</button>
<div
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
>
<ul class="navbar-nav my-2 mr-auto">
<li class="nav-item">
<Link
class="nav-link"
to="/communities"
title={i18n.t('communities')}
>
{i18n.t('communities')}
</Link>
</li>
<li class="nav-item">
<Link
class="nav-link"
to={{
pathname: '/create_post',
state: { prevPath: this.currentLocation },
}}
title={i18n.t('create_post')}
>
{i18n.t('create_post')}
</Link>
</li>
<li class="nav-item">
<Link
class="nav-link"
to="/create_community"
title={i18n.t('create_community')}
>
{i18n.t('create_community')}
</Link>
</li>
<li className="nav-item">
<Link
class="nav-link"
to="/sponsors"
title={i18n.t('donate_to_lemmy')}
>
<svg class="icon">
<use xlinkHref="#icon-coffee"></use>
</svg>
</Link>
</li>
</ul>
{!this.context.router.history.location.pathname.match(
/^\/search/
) && (
<form
class="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
{this.state.isLoggedIn && (
<Link
class="ml-auto p-0 navbar-toggler nav-link border-0"
to="/inbox"
title={i18n.t('inbox')}
>
<input
class={`form-control mr-0 search-input ${
this.state.toggleSearch ? 'show-input' : 'hide-input'
}`}
onInput={linkEvent(this, this.handleSearchParam)}
value={this.state.searchParam}
ref={this.searchTextField}
type="text"
placeholder={i18n.t('search')}
onBlur={linkEvent(this, this.handleSearchBlur)}
></input>
<button
name="search-btn"
onClick={linkEvent(this, this.handleSearchBtn)}
class="btn btn-link"
style="color: var(--gray)"
>
<svg class="icon">
<use xlinkHref="#icon-search"></use>
</svg>
</button>
</form>
<svg class="icon">
<use xlinkHref="#icon-bell"></use>
</svg>
{this.state.unreadCount > 0 && (
<span class="ml-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
</Link>
)}
<ul class="navbar-nav my-2">
{this.canAdmin && (
<button
class="navbar-toggler border-0"
type="button"
aria-label="menu"
onClick={linkEvent(this, this.expandNavbar)}
data-tippy-content={i18n.t('expand_here')}
>
<span class="navbar-toggler-icon"></span>
</button>
<div
className={`${!this.state.expanded && 'collapse'} navbar-collapse`}
>
<ul class="navbar-nav my-2 mr-auto">
<li class="nav-item">
<Link
class="nav-link"
to="/communities"
title={i18n.t('communities')}
>
{i18n.t('communities')}
</Link>
</li>
<li class="nav-item">
<Link
class="nav-link"
to={{
pathname: '/create_post',
state: { prevPath: this.currentLocation },
}}
title={i18n.t('create_post')}
>
{i18n.t('create_post')}
</Link>
</li>
<li class="nav-item">
<Link
class="nav-link"
to="/create_community"
title={i18n.t('create_community')}
>
{i18n.t('create_community')}
</Link>
</li>
<li className="nav-item">
<Link
class="nav-link"
to={`/admin`}
title={i18n.t('admin_settings')}
to="/sponsors"
title={i18n.t('donate_to_lemmy')}
>
<svg class="icon">
<use xlinkHref="#icon-settings"></use>
<use xlinkHref="#icon-coffee"></use>
</svg>
</Link>
</li>
</ul>
{!this.context.router.history.location.pathname.match(
/^\/search/
) && (
<form
class="form-inline"
onSubmit={linkEvent(this, this.handleSearchSubmit)}
>
<input
class={`form-control mr-0 search-input ${
this.state.toggleSearch ? 'show-input' : 'hide-input'
}`}
onInput={linkEvent(this, this.handleSearchParam)}
value={this.state.searchParam}
ref={this.searchTextField}
type="text"
placeholder={i18n.t('search')}
onBlur={linkEvent(this, this.handleSearchBlur)}
></input>
<button
name="search-btn"
onClick={linkEvent(this, this.handleSearchBtn)}
class="btn btn-link"
style="color: var(--gray)"
>
<svg class="icon">
<use xlinkHref="#icon-search"></use>
</svg>
</button>
</form>
)}
</ul>
{this.state.isLoggedIn ? (
<>
<ul class="navbar-nav my-2">
<li className="nav-item">
<Link class="nav-link" to="/inbox" title={i18n.t('inbox')}>
<svg class="icon">
<use xlinkHref="#icon-bell"></use>
</svg>
{this.state.unreadCount > 0 && (
<span class="ml-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
</Link>
</li>
</ul>
<ul class="navbar-nav">
<ul class="navbar-nav my-2">
{this.canAdmin && (
<li className="nav-item">
<Link
class="nav-link"
to={`/u/${UserService.Instance.user.username}`}
title={i18n.t('settings')}
to={`/admin`}
title={i18n.t('admin_settings')}
>
<span>
{UserService.Instance.user.avatar && showAvatars() && (
<img
src={pictrsAvatarThumbnail(
UserService.Instance.user.avatar
)}
height="32"
width="32"
class="rounded-circle mr-2"
/>
<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="/inbox" title={i18n.t('inbox')}>
<svg class="icon">
<use xlinkHref="#icon-bell"></use>
</svg>
{this.state.unreadCount > 0 && (
<span class="ml-1 badge badge-light">
{this.state.unreadCount}
</span>
)}
{UserService.Instance.user.username}
</span>
</Link>
</li>
</ul>
<ul class="navbar-nav">
<li className="nav-item">
<Link
class="nav-link"
to={`/u/${UserService.Instance.user.username}`}
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.username}
</span>
</Link>
</li>
</ul>
</>
) : (
<ul class="navbar-nav my-2">
<li className="nav-item">
<Link
class="btn btn-success"
to="/login"
title={i18n.t('login_sign_up')}
>
{i18n.t('login_sign_up')}
</Link>
</li>
</ul>
</>
) : (
<ul class="navbar-nav my-2">
<li className="nav-item">
<Link
class="nav-link"
to="/login"
title={i18n.t('login_sign_up')}
>
{i18n.t('login_sign_up')}
</Link>
</li>
</ul>
)}
)}
</div>
</div>
</nav>
);

View file

@ -71,9 +71,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
nsfw: false,
auth: null,
community_id: null,
creator_id: UserService.Instance.user
? UserService.Instance.user.id
: null,
},
communities: [],
loading: false,
@ -99,7 +96,6 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
name: this.props.post.name,
community_id: this.props.post.community_id,
edit_id: this.props.post.id,
creator_id: this.props.post.creator_id,
url: this.props.post.url,
nsfw: this.props.post.nsfw,
auth: null,

View file

@ -4,7 +4,10 @@ import { WebSocketService, UserService } from '../services';
import {
Post,
CreatePostLikeForm,
PostForm as PostFormI,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm,
CommunityUser,
UserView,
@ -33,7 +36,6 @@ import {
setupTippy,
hostname,
previewLines,
toast,
} from '../utils';
import { i18n } from '../i18next';
@ -238,9 +240,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
title={post.url}
rel="noopener"
>
<svg class="icon thumbnail">
<use xlinkHref="#icon-external-link"></use>
</svg>
<div class="thumbnail rounded bg-light d-flex justify-content-center">
<svg class="icon d-flex align-items-center">
<use xlinkHref="#icon-external-link"></use>
</svg>
</div>
</a>
);
}
@ -251,9 +255,11 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
to={`/post/${post.id}`}
title={i18n.t('comments')}
>
<svg class="icon thumbnail">
<use xlinkHref="#icon-message-square"></use>
</svg>
<div class="thumbnail rounded bg-light d-flex justify-content-center">
<svg class="icon d-flex align-items-center">
<use xlinkHref="#icon-message-square"></use>
</svg>
</div>
</Link>
);
}
@ -296,7 +302,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
)}
</div>
{!this.state.imageExpanded && (
<div class="col-3 col-sm-2 pr-0 mt-1">
<div class="col-3 col-sm-2 pr-0">
<div class="position-relative">{this.thumbnail()}</div>
</div>
)}
@ -560,7 +566,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleSavePostClick)}
data-tippy-content={
post.saved ? i18n.t('unsave') : i18n.t('save')
@ -577,7 +583,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<Link
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
to={`/create_post${this.crossPostParams}`}
title={i18n.t('cross_post')}
>
@ -592,7 +598,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
>
@ -603,7 +609,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={
!post.deleted
@ -626,7 +632,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{!this.state.showAdvanced && this.props.showBody ? (
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleShowAdvanced)}
data-tippy-content={i18n.t('more')}
>
@ -640,7 +646,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
{this.props.showBody && post.body && (
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
>
@ -658,7 +664,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
<>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModLock)}
data-tippy-content={
post.locked
@ -677,7 +683,7 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
</li>
<li className="list-inline-item">
<button
class="btn btn-sm btn-link btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleModSticky)}
data-tippy-content={
post.stickied
@ -1114,18 +1120,12 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
}
handleDeleteClick(i: PostListing) {
let deleteForm: PostFormI = {
body: i.props.post.body,
community_id: i.props.post.community_id,
name: i.props.post.name,
url: i.props.post.url,
let deleteForm: DeletePostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
deleted: !i.props.post.deleted,
nsfw: i.props.post.nsfw,
auth: null,
};
WebSocketService.Instance.editPost(deleteForm);
WebSocketService.Instance.deletePost(deleteForm);
}
handleSavePostClick(i: PostListing) {
@ -1163,46 +1163,34 @@ export class PostListing extends Component<PostListingProps, PostListingState> {
handleModRemoveSubmit(i: PostListing) {
event.preventDefault();
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: RemovePostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
removed: !i.props.post.removed,
reason: i.state.removeReason,
nsfw: i.props.post.nsfw,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.removePost(form);
i.state.showRemoveDialog = false;
i.setState(i.state);
}
handleModLock(i: PostListing) {
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: LockPostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
locked: !i.props.post.locked,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.lockPost(form);
}
handleModSticky(i: PostListing) {
let form: PostFormI = {
name: i.props.post.name,
community_id: i.props.post.community_id,
let form: StickyPostForm = {
edit_id: i.props.post.id,
creator_id: i.props.post.creator_id,
nsfw: i.props.post.nsfw,
stickied: !i.props.post.stickied,
auth: null,
};
WebSocketService.Instance.editPost(form);
WebSocketService.Instance.stickyPost(form);
}
handleModBanFromCommunityShow(i: PostListing) {

View file

@ -32,7 +32,7 @@ export class PostListings extends Component<PostListingsProps, any> {
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
/>
<hr class="my-2" />
<hr class="my-3" />
</>
))
) : (

View file

@ -8,7 +8,7 @@ import {
GetPostResponse,
PostResponse,
Comment,
CommentForm as CommentFormI,
MarkCommentAsReadForm,
CommentResponse,
CommentSortType,
CommentViewType,
@ -168,16 +168,12 @@ export class Post extends Component<any, PostState> {
UserService.Instance.user &&
UserService.Instance.user.id == parent_user_id
) {
let form: CommentFormI = {
content: found.content,
let form: MarkCommentAsReadForm = {
edit_id: found.id,
creator_id: found.creator_id,
post_id: found.post_id,
parent_id: found.parent_id,
read: true,
auth: null,
};
WebSocketService.Instance.editComment(form);
WebSocketService.Instance.markCommentAsRead(form);
UserService.Instance.user.unreadCount--;
UserService.Instance.sub.next({
user: UserService.Instance.user,
@ -229,7 +225,7 @@ export class Post extends Component<any, PostState> {
<>
<div class="btn-group btn-group-toggle mr-3 mb-2">
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Hot && 'active'
}`}
>
@ -242,7 +238,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Top && 'active'
}`}
>
@ -255,7 +251,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.New && 'active'
}`}
>
@ -268,7 +264,7 @@ export class Post extends Component<any, PostState> {
/>
</label>
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentSort === CommentSortType.Old && 'active'
}`}
>
@ -283,7 +279,7 @@ export class Post extends Component<any, PostState> {
</div>
<div class="btn-group btn-group-toggle mb-2">
<label
className={`btn btn-sm btn-secondary pointer ${
className={`btn btn-outline-secondary pointer ${
this.state.commentViewType === CommentViewType.Chat && 'active'
}`}
>
@ -409,7 +405,6 @@ export class Post extends Component<any, PostState> {
this.state.comments = data.comments;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.state.online = data.online;
this.state.loading = false;
document.title = `${this.state.post.name} - ${this.state.siteRes.site.name}`;
@ -436,7 +431,11 @@ export class Post extends Component<any, PostState> {
this.state.comments.unshift(data.comment);
this.setState(this.state);
}
} else if (res.op == UserOperation.EditComment) {
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
let data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState(this.state);
@ -453,7 +452,13 @@ export class Post extends Component<any, PostState> {
let data = res.data as PostResponse;
createPostLikeRes(data, this.state.post);
this.setState(this.state);
} else if (res.op == UserOperation.EditPost) {
} else if (
res.op == UserOperation.EditPost ||
res.op == UserOperation.DeletePost ||
res.op == UserOperation.RemovePost ||
res.op == UserOperation.LockPost ||
res.op == UserOperation.StickyPost
) {
let data = res.data as PostResponse;
this.state.post = data.post;
this.setState(this.state);
@ -463,7 +468,11 @@ export class Post extends Component<any, PostState> {
this.state.post = data.post;
this.setState(this.state);
setupTippy();
} else if (res.op == UserOperation.EditCommunity) {
} else if (
res.op == UserOperation.EditCommunity ||
res.op == UserOperation.DeleteCommunity ||
res.op == UserOperation.RemoveCommunity
) {
let data = res.data as CommunityResponse;
this.state.community = data.community;
this.state.post.community_id = data.community.id;
@ -521,7 +530,6 @@ export class Post extends Component<any, PostState> {
let data = res.data as GetCommunityResponse;
this.state.community = data.community;
this.state.moderators = data.moderators;
this.state.siteRes.admins = data.admins;
this.setState(this.state);
}
}

View file

@ -263,7 +263,11 @@ export class PrivateMessageForm extends Component<
this.state.loading = false;
this.setState(this.state);
return;
} else if (res.op == UserOperation.EditPrivateMessage) {
} else if (
res.op == UserOperation.EditPrivateMessage ||
res.op == UserOperation.DeletePrivateMessage ||
res.op == UserOperation.MarkPrivateMessageAsRead
) {
let data = res.data as PrivateMessageResponse;
this.state.loading = false;
this.props.onEdit(data.message);

View file

@ -2,7 +2,8 @@ import { Component, linkEvent } from 'inferno';
import { Link } from 'inferno-router';
import {
PrivateMessage as PrivateMessageI,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { mdToHtml, pictrsAvatarThumbnail, showAvatars, toast } from '../utils';
@ -130,7 +131,7 @@ export class PrivateMessage extends Component<
<>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleMarkRead)}
data-tippy-content={
message.read
@ -149,7 +150,7 @@ export class PrivateMessage extends Component<
</li>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleReplyClick)}
data-tippy-content={i18n.t('reply')}
>
@ -164,7 +165,7 @@ export class PrivateMessage extends Component<
<>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleEditClick)}
data-tippy-content={i18n.t('edit')}
>
@ -175,7 +176,7 @@ export class PrivateMessage extends Component<
</li>
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleDeleteClick)}
data-tippy-content={
!message.deleted
@ -196,7 +197,7 @@ export class PrivateMessage extends Component<
)}
<li className="list-inline-item">
<button
class="btn btn-link btn-sm btn-animate text-muted"
class="btn btn-link btn-animate text-muted"
onClick={linkEvent(this, this.handleViewSource)}
data-tippy-content={i18n.t('view_source')}
>
@ -243,11 +244,11 @@ export class PrivateMessage extends Component<
}
handleDeleteClick(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: DeletePrivateMessageForm = {
edit_id: i.props.privateMessage.id,
deleted: !i.props.privateMessage.deleted,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.deletePrivateMessage(form);
}
handleReplyCancel() {
@ -257,11 +258,11 @@ export class PrivateMessage extends Component<
}
handleMarkRead(i: PrivateMessage) {
let form: EditPrivateMessageForm = {
let form: MarkPrivateMessageAsReadForm = {
edit_id: i.props.privateMessage.id,
read: !i.props.privateMessage.read,
};
WebSocketService.Instance.editPrivateMessage(form);
WebSocketService.Instance.markPrivateMessageAsRead(form);
}
handleMessageCollapse(i: PrivateMessage) {

View file

@ -207,7 +207,7 @@ export class Search extends Component<any, SearchState> {
<select
value={this.state.type_}
onChange={linkEvent(this, this.handleTypeChange)}
class="custom-select custom-select-sm w-auto"
class="custom-select w-auto"
>
<option disabled>{i18n.t('type')}</option>
<option value={SearchType.All}>{i18n.t('all')}</option>
@ -402,7 +402,7 @@ export class Search extends Component<any, SearchState> {
<div class="mt-2">
{this.state.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -411,7 +411,7 @@ export class Search extends Component<any, SearchState> {
{this.resultsCount() > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}

View file

@ -4,7 +4,8 @@ import {
Community,
CommunityUser,
FollowCommunityForm,
CommunityForm as CommunityFormI,
DeleteCommunityForm,
RemoveCommunityForm,
UserView,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
@ -74,7 +75,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
}
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5 className="mb-0">
<span>{community.title}</span>
@ -176,33 +177,33 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
)}
<ul class="my-1 list-inline">
{/*
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_online', { count: this.props.online })}
</li>
*/}
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_subscribers', {
count: community.number_of_subscribers,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_posts', {
count: community.number_of_posts,
})}
</li>
<li className="list-inline-item badge badge-secondary">
<li className="list-inline-item badge badge-light">
{i18n.t('number_of_comments', {
count: community.number_of_comments,
})}
</li>
<li className="list-inline-item">
<Link className="badge badge-secondary" to="/communities">
<Link className="badge badge-light" to="/communities">
{community.category_name}
</Link>
</li>
<li className="list-inline-item">
<Link
className="badge badge-secondary"
className="badge badge-light"
to={`/modlog/community/${this.props.community.id}`}
>
{i18n.t('modlog')}
@ -227,7 +228,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</ul>
{/* TODO the to= needs to be able to handle community_ids as well, since they're federated */}
<Link
class={`btn btn-sm btn-secondary btn-block mb-3 ${
class={`btn btn-secondary btn-block mb-3 ${
(community.deleted || community.removed) && 'no-click'
}`}
to={`/create_post?community=${community.name}`}
@ -237,14 +238,14 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
<div>
{community.subscribed ? (
<button
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
onClick={linkEvent(community.id, this.handleUnsubscribe)}
>
{i18n.t('unsubscribe')}
</button>
) : (
<button
class="btn btn-sm btn-secondary btn-block"
class="btn btn-secondary btn-block"
onClick={linkEvent(community.id, this.handleSubscribe)}
>
{i18n.t('subscribe')}
@ -254,7 +255,7 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
</div>
</div>
{community.description && (
<div class="card border-secondary">
<div class="card bg-transparent border-secondary">
<div class="card-body">
<div
className="md-div"
@ -284,16 +285,11 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleDeleteClick(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
let deleteForm: DeleteCommunityForm = {
edit_id: i.props.community.id,
deleted: !i.props.community.deleted,
nsfw: i.props.community.nsfw,
auth: null,
};
WebSocketService.Instance.editCommunity(deleteForm);
WebSocketService.Instance.deleteCommunity(deleteForm);
}
handleUnsubscribe(communityId: number) {
@ -350,18 +346,13 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
handleModRemoveSubmit(i: Sidebar) {
event.preventDefault();
let deleteForm: CommunityFormI = {
name: i.props.community.name,
title: i.props.community.title,
category_id: i.props.community.category_id,
let removeForm: RemoveCommunityForm = {
edit_id: i.props.community.id,
removed: !i.props.community.removed,
reason: i.state.removeReason,
expires: getUnixTime(i.state.removeExpires),
nsfw: i.props.community.nsfw,
auth: null,
};
WebSocketService.Instance.editCommunity(deleteForm);
WebSocketService.Instance.removeCommunity(removeForm);
i.state.showRemoveDialog = false;
i.setState(i.state);

View file

@ -35,7 +35,7 @@ export class SortSelect extends Component<SortSelectProps, SortSelectState> {
<select
value={this.state.sort}
onChange={linkEvent(this, this.handleSortChange)}
class="custom-select custom-select-sm w-auto mr-2"
class="custom-select w-auto mr-2"
>
<option disabled>{i18n.t('sort_type')}</option>
{!this.props.hideHot && (

View file

@ -17,6 +17,7 @@ interface SilverUser {
}
let general = [
'William Moore',
'Rachel Schmitz',
'comradeda',
'ybaumy',
@ -108,7 +109,7 @@ export class Sponsors extends Component<any, any> {
<div class="container">
<h5>{i18n.t('sponsors')}</h5>
<p>{i18n.t('silver_sponsors')}</p>
<div class="row card-columns">
<div class="row justify-content-md-center card-columns">
{silver.map(s => (
<div class="card col-12 col-md-2">
<div>
@ -124,7 +125,7 @@ export class Sponsors extends Component<any, any> {
))}
</div>
<p>{i18n.t('general_sponsors')}</p>
<div class="row card-columns">
<div class="row justify-content-md-center card-columns">
{highlighted.map(s => (
<div class="card bg-primary col-12 col-md-2 font-weight-bold">
<div>{s}</div>

View file

@ -1,7 +1,7 @@
import { Component, linkEvent } from 'inferno';
import { WebSocketService, UserService } from '../services';
import { Subscription } from 'rxjs';
import { retryWhen, delay, take, last } from 'rxjs/operators';
import { retryWhen, delay, take } from 'rxjs/operators';
import { i18n } from '../i18next';
import {
UserOperation,
@ -16,7 +16,6 @@ import {
CommentResponse,
BanUserResponse,
PostResponse,
AddAdminResponse,
} from '../interfaces';
import {
wsJsonToRes,
@ -41,6 +40,7 @@ interface UserDetailsProps {
enableNsfw: boolean;
view: UserDetailsView;
onPageChange(page: number): number | any;
admins: Array<UserView>;
}
interface UserDetailsState {
@ -49,7 +49,6 @@ interface UserDetailsState {
comments: Array<Comment>;
posts: Array<Post>;
saved?: Array<Post>;
admins: Array<UserView>;
}
export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
@ -63,7 +62,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
comments: [],
posts: [],
saved: [],
admins: [],
};
this.subscription = WebSocketService.Instance.subject
@ -152,7 +150,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
{i.type === 'posts' ? (
<PostListing
post={i.data as Post}
admins={this.state.admins}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
@ -160,7 +158,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
) : (
<CommentNodes
nodes={[{ comment: i.data as Comment }]}
admins={this.state.admins}
admins={this.props.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
@ -177,7 +175,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div>
<CommentNodes
nodes={commentsToFlatNodes(this.state.comments)}
admins={this.state.admins}
admins={this.props.admins}
noIndent
showContext
enableDownvotes={this.props.enableDownvotes}
@ -192,7 +190,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
{this.state.posts.map(post => (
<PostListing
post={post}
admins={this.state.admins}
admins={this.props.admins}
showCommunity
enableDownvotes={this.props.enableDownvotes}
enableNsfw={this.props.enableNsfw}
@ -207,7 +205,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
<div class="my-2">
{this.props.page > 1 && (
<button
class="btn btn-sm btn-secondary mr-1"
class="btn btn-secondary mr-1"
onClick={linkEvent(this, this.prevPage)}
>
{i18n.t('prev')}
@ -215,7 +213,7 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
)}
{this.state.comments.length + this.state.posts.length > 0 && (
<button
class="btn btn-sm btn-secondary"
class="btn btn-secondary"
onClick={linkEvent(this, this.nextPage)}
>
{i18n.t('next')}
@ -252,7 +250,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
follows: data.follows,
moderates: data.moderates,
posts: data.posts,
admins: data.admins,
});
} else if (res.op == UserOperation.CreateCommentLike) {
const data = res.data as CommentResponse;
@ -260,7 +257,11 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
this.setState({
comments: this.state.comments,
});
} else if (res.op == UserOperation.EditComment) {
} else if (
res.op == UserOperation.EditComment ||
res.op == UserOperation.DeleteComment ||
res.op == UserOperation.RemoveComment
) {
const data = res.data as CommentResponse;
editCommentRes(data, this.state.comments);
this.setState({
@ -298,11 +299,6 @@ export class UserDetails extends Component<UserDetailsProps, UserDetailsState> {
posts: this.state.posts,
comments: this.state.comments,
});
} else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.setState({
admins: data.admins,
});
}
}
}

View file

@ -43,11 +43,10 @@ export class UserListing extends Component<UserListingProps, any> {
return (
<>
<Link className="text-body font-weight-bold" to={link}>
<Link className="text-info" to={link}>
{user.avatar && showAvatars() && (
<img
height="32"
width="32"
style="width: 2rem; height: 2rem;"
src={pictrsAvatarThumbnail(user.avatar)}
class="rounded-circle mr-2"
/>

View file

@ -13,9 +13,9 @@ import {
DeleteAccountForm,
WebSocketJsonResponse,
GetSiteResponse,
Site,
UserDetailsView,
UserDetailsResponse,
AddAdminResponse,
} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import {
@ -54,7 +54,7 @@ interface UserState {
deleteAccountLoading: boolean;
deleteAccountShowConfirm: boolean;
deleteAccountForm: DeleteAccountForm;
site: Site;
siteRes: GetSiteResponse;
}
interface UserProps {
@ -114,19 +114,25 @@ export class User extends Component<any, UserState> {
deleteAccountForm: {
password: null,
},
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
siteRes: {
admins: [],
banned: [],
online: undefined,
site: {
id: undefined,
name: undefined,
creator_id: undefined,
published: undefined,
creator_name: undefined,
number_of_users: undefined,
number_of_posts: undefined,
number_of_comments: undefined,
number_of_communities: undefined,
enable_downvotes: undefined,
open_registration: undefined,
enable_nsfw: undefined,
},
version: undefined,
},
};
@ -201,7 +207,7 @@ export class User extends Component<any, UserState> {
// Couldnt get a refresh working. This does for now.
location.reload();
}
document.title = `/u/${this.state.username} - ${this.state.site.name}`;
document.title = `/u/${this.state.username} - ${this.state.siteRes.site.name}`;
setupTippy();
}
@ -236,8 +242,9 @@ export class User extends Component<any, UserState> {
sort={SortType[this.state.sort]}
page={this.state.page}
limit={fetchLimit}
enableDownvotes={this.state.site.enable_downvotes}
enableNsfw={this.state.site.enable_nsfw}
enableDownvotes={this.state.siteRes.site.enable_downvotes}
enableNsfw={this.state.siteRes.site.enable_nsfw}
admins={this.state.siteRes.admins}
view={this.state.view}
onPageChange={this.handlePageChange}
/>
@ -260,7 +267,7 @@ export class User extends Component<any, UserState> {
return (
<div class="btn-group btn-group-toggle">
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Overview && 'active'}
`}
>
@ -273,7 +280,7 @@ export class User extends Component<any, UserState> {
{i18n.t('overview')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Comments && 'active'}
`}
>
@ -286,7 +293,7 @@ export class User extends Component<any, UserState> {
{i18n.t('comments')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Posts && 'active'}
`}
>
@ -299,7 +306,7 @@ export class User extends Component<any, UserState> {
{i18n.t('posts')}
</label>
<label
className={`btn btn-sm btn-secondary pointer btn-outline-light
className={`btn btn-outline-secondary pointer
${this.state.view == UserDetailsView.Saved && 'active'}
`}
>
@ -344,7 +351,7 @@ export class User extends Component<any, UserState> {
let user = this.state.user;
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>
<ul class="list-inline mb-0">
@ -441,7 +448,7 @@ export class User extends Component<any, UserState> {
userSettings() {
return (
<div>
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('settings')}</h5>
<form onSubmit={linkEvent(this, this.handleUserSettingsSubmit)}>
@ -453,7 +460,7 @@ export class User extends Component<any, UserState> {
class="pointer ml-4 text-muted small font-weight-bold"
>
{!this.checkSettingsAvatar ? (
<span class="btn btn-sm btn-secondary">
<span class="btn btn-secondary">
{i18n.t('upload_avatar')}
</span>
) : (
@ -493,7 +500,7 @@ export class User extends Component<any, UserState> {
<select
value={this.state.userSettingsForm.lang}
onChange={linkEvent(this, this.handleUserSettingsLangChange)}
class="ml-2 custom-select custom-select-sm w-auto"
class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('language')}</option>
<option value="browser">{i18n.t('browser_default')}</option>
@ -508,7 +515,7 @@ export class User extends Component<any, UserState> {
<select
value={this.state.userSettingsForm.theme}
onChange={linkEvent(this, this.handleUserSettingsThemeChange)}
class="ml-2 custom-select custom-select-sm w-auto"
class="ml-2 custom-select w-auto"
>
<option disabled>{i18n.t('theme')}</option>
{themes.map(theme => (
@ -637,7 +644,7 @@ export class User extends Component<any, UserState> {
/>
</div>
</div>
{this.state.site.enable_nsfw && (
{this.state.siteRes.site.enable_nsfw && (
<div class="form-group">
<div class="form-check">
<input
@ -769,7 +776,7 @@ export class User extends Component<any, UserState> {
return (
<div>
{this.state.moderates.length > 0 && (
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('moderates')}</h5>
<ul class="list-unstyled mb-0">
@ -792,7 +799,7 @@ export class User extends Component<any, UserState> {
return (
<div>
{this.state.follows.length > 0 && (
<div class="card border-secondary mb-3">
<div class="card bg-transparent border-secondary mb-3">
<div class="card-body">
<h5>{i18n.t('subscribed')}</h5>
<ul class="list-unstyled mb-0">
@ -1063,9 +1070,12 @@ export class User extends Component<any, UserState> {
this.context.router.history.push('/');
} else if (res.op == UserOperation.GetSite) {
const data = res.data as GetSiteResponse;
this.setState({
site: data.site,
});
this.state.siteRes = data;
this.setState(this.state);
} else if (res.op == UserOperation.AddAdmin) {
const data = res.data as AddAdminResponse;
this.state.siteRes.admins = data.admins;
this.setState(this.state);
}
}
}

123
ui/src/interfaces.ts vendored
View file

@ -9,19 +9,28 @@ export enum UserOperation {
GetCommunity,
CreateComment,
EditComment,
DeleteComment,
RemoveComment,
MarkCommentAsRead,
SaveComment,
CreateCommentLike,
GetPosts,
CreatePostLike,
EditPost,
DeletePost,
RemovePost,
LockPost,
StickyPost,
SavePost,
EditCommunity,
DeleteCommunity,
RemoveCommunity,
FollowCommunity,
GetFollowedCommunities,
GetUserDetails,
GetReplies,
GetUserMentions,
EditUserMention,
MarkUserMentionAsRead,
GetModlog,
BanFromCommunity,
AddModToCommunity,
@ -40,6 +49,8 @@ export enum UserOperation {
PasswordChange,
CreatePrivateMessage,
EditPrivateMessage,
DeletePrivateMessage,
MarkPrivateMessageAsRead,
GetPrivateMessages,
UserJoin,
GetComments,
@ -355,9 +366,9 @@ export interface GetUserMentionsResponse {
mentions: Array<Comment>;
}
export interface EditUserMentionForm {
export interface MarkUserMentionAsReadForm {
user_mention_id: number;
read?: boolean;
read: boolean;
auth?: string;
}
@ -571,13 +582,23 @@ export interface UserSettingsForm {
export interface CommunityForm {
name: string;
edit_id?: number;
title: string;
description?: string;
category_id: number;
edit_id?: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean;
auth?: string;
}
export interface DeleteCommunityForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface RemoveCommunityForm {
edit_id: number;
removed: boolean;
reason?: string;
expires?: number;
auth?: string;
@ -592,7 +613,6 @@ export interface GetCommunityForm {
export interface GetCommunityResponse {
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number;
}
@ -619,19 +639,37 @@ export interface PostForm {
name: string;
url?: string;
body?: string;
community_id: number;
updated?: number;
community_id?: number;
edit_id?: number;
creator_id: number;
removed?: boolean;
deleted?: boolean;
nsfw: boolean;
locked?: boolean;
stickied?: boolean;
auth: string;
}
export interface DeletePostForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemovePostForm {
edit_id: number;
removed: boolean;
reason?: string;
auth: string;
}
export interface LockPostForm {
edit_id: number;
locked: boolean;
auth: string;
}
export interface StickyPostForm {
edit_id: number;
stickied: boolean;
auth: string;
}
export interface PostFormParams {
name: string;
url?: string;
@ -649,7 +687,6 @@ export interface GetPostResponse {
comments: Array<Comment>;
community: Community;
moderators: Array<CommunityUser>;
admins: Array<UserView>;
online: number;
}
@ -665,14 +702,30 @@ export interface PostResponse {
export interface CommentForm {
content: string;
post_id: number;
post_id?: number;
parent_id?: number;
edit_id?: number;
creator_id?: number;
removed?: boolean;
deleted?: boolean;
form_id?: string;
auth: string;
}
export interface DeleteCommentForm {
edit_id: number;
deleted: boolean;
auth: string;
}
export interface RemoveCommentForm {
edit_id: number;
removed: boolean;
reason?: string;
read?: boolean;
auth: string;
}
export interface MarkCommentAsReadForm {
edit_id: number;
read: boolean;
auth: string;
}
@ -685,11 +738,11 @@ export interface SaveCommentForm {
export interface CommentResponse {
comment: Comment;
recipient_ids: Array<number>;
form_id?: string;
}
export interface CommentLikeForm {
comment_id: number;
post_id: number;
score: number;
auth?: string;
}
@ -835,9 +888,19 @@ export interface PrivateMessageFormParams {
export interface EditPrivateMessageForm {
edit_id: number;
content?: string;
deleted?: boolean;
read?: boolean;
content: string;
auth?: string;
}
export interface DeletePrivateMessageForm {
edit_id: number;
deleted: boolean;
auth?: string;
}
export interface MarkPrivateMessageAsReadForm {
edit_id: number;
read: boolean;
auth?: string;
}
@ -865,18 +928,26 @@ export interface UserJoinResponse {
}
export type MessageType =
| EditPrivateMessageForm
| LoginForm
| RegisterForm
| CommunityForm
| DeleteCommunityForm
| RemoveCommunityForm
| FollowCommunityForm
| ListCommunitiesForm
| GetFollowedCommunitiesForm
| PostForm
| DeletePostForm
| RemovePostForm
| LockPostForm
| StickyPostForm
| GetPostForm
| GetPostsForm
| GetCommunityForm
| CommentForm
| DeleteCommentForm
| RemoveCommentForm
| MarkCommentAsReadForm
| CommentLikeForm
| SaveCommentForm
| CreatePostLikeForm
@ -891,7 +962,7 @@ export type MessageType =
| GetUserDetailsForm
| GetRepliesForm
| GetUserMentionsForm
| EditUserMentionForm
| MarkUserMentionAsReadForm
| GetModlogForm
| SiteForm
| SearchForm
@ -901,6 +972,8 @@ export type MessageType =
| PasswordChangeForm
| PrivateMessageForm
| EditPrivateMessageForm
| DeletePrivateMessageForm
| MarkPrivateMessageAsReadForm
| GetPrivateMessagesForm
| SiteConfigForm;

View file

@ -4,9 +4,18 @@ import {
RegisterForm,
UserOperation,
CommunityForm,
DeleteCommunityForm,
RemoveCommunityForm,
PostForm,
DeletePostForm,
RemovePostForm,
LockPostForm,
StickyPostForm,
SavePostForm,
CommentForm,
DeleteCommentForm,
RemoveCommentForm,
MarkCommentAsReadForm,
SaveCommentForm,
CommentLikeForm,
GetPostForm,
@ -28,7 +37,7 @@ import {
UserView,
GetRepliesForm,
GetUserMentionsForm,
EditUserMentionForm,
MarkUserMentionAsReadForm,
SearchForm,
UserSettingsForm,
DeleteAccountForm,
@ -36,6 +45,8 @@ import {
PasswordChangeForm,
PrivateMessageForm,
EditPrivateMessageForm,
DeletePrivateMessageForm,
MarkPrivateMessageAsReadForm,
GetPrivateMessagesForm,
GetCommentsForm,
UserJoinForm,
@ -103,18 +114,24 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.Register, registerForm));
}
public createCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm);
this.ws.send(
this.wsSendWrapper(UserOperation.CreateCommunity, communityForm)
);
public createCommunity(form: CommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateCommunity, form));
}
public editCommunity(communityForm: CommunityForm) {
this.setAuth(communityForm);
this.ws.send(
this.wsSendWrapper(UserOperation.EditCommunity, communityForm)
);
public editCommunity(form: CommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditCommunity, form));
}
public deleteCommunity(form: DeleteCommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeleteCommunity, form));
}
public removeCommunity(form: RemoveCommunityForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemoveCommunity, form));
}
public followCommunity(followCommunityForm: FollowCommunityForm) {
@ -140,9 +157,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.ListCategories, {}));
}
public createPost(postForm: PostForm) {
this.setAuth(postForm);
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, postForm));
public createPost(form: PostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreatePost, form));
}
public getPost(form: GetPostForm) {
@ -155,14 +172,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetCommunity, form));
}
public createComment(commentForm: CommentForm) {
this.setAuth(commentForm);
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, commentForm));
public createComment(form: CommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.CreateComment, form));
}
public editComment(commentForm: CommentForm) {
this.setAuth(commentForm);
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, commentForm));
public editComment(form: CommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditComment, form));
}
public deleteComment(form: DeleteCommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeleteComment, form));
}
public removeComment(form: RemoveCommentForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemoveComment, form));
}
public markCommentAsRead(form: MarkCommentAsReadForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.MarkCommentAsRead, form));
}
public likeComment(form: CommentLikeForm) {
@ -190,9 +222,29 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.CreatePostLike, form));
}
public editPost(postForm: PostForm) {
this.setAuth(postForm);
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, postForm));
public editPost(form: PostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditPost, form));
}
public deletePost(form: DeletePostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeletePost, form));
}
public removePost(form: RemovePostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.RemovePost, form));
}
public lockPost(form: LockPostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.LockPost, form));
}
public stickyPost(form: StickyPostForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.StickyPost, form));
}
public savePost(form: SavePostForm) {
@ -245,9 +297,9 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.GetUserMentions, form));
}
public editUserMention(form: EditUserMentionForm) {
public markUserMentionAsRead(form: MarkUserMentionAsReadForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.EditUserMention, form));
this.ws.send(this.wsSendWrapper(UserOperation.MarkUserMentionAsRead, form));
}
public getModlog(form: GetModlogForm) {
@ -315,6 +367,18 @@ export class WebSocketService {
this.ws.send(this.wsSendWrapper(UserOperation.EditPrivateMessage, form));
}
public deletePrivateMessage(form: DeletePrivateMessageForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.DeletePrivateMessage, form));
}
public markPrivateMessageAsRead(form: MarkPrivateMessageAsReadForm) {
this.setAuth(form);
this.ws.send(
this.wsSendWrapper(UserOperation.MarkPrivateMessageAsRead, form)
);
}
public getPrivateMessages(form: GetPrivateMessagesForm) {
this.setAuth(form);
this.ws.send(this.wsSendWrapper(UserOperation.GetPrivateMessages, form));

View file

@ -256,6 +256,7 @@
"couldnt_save_post": "Couldn't save post.",
"no_slurs": "No slurs.",
"not_an_admin": "Not an admin.",
"not_a_moderator": "Not a moderator.",
"site_already_exists": "Site already exists.",
"couldnt_update_site": "Couldn't update site.",
"couldnt_find_that_username_or_email":

View file

@ -111,7 +111,7 @@
"all": "Tudo",
"top": "Top",
"api": "API",
"docs": "Docs",
"docs": "Documentação",
"inbox": "Caixa de entrada",
"inbox_for": "Caixa de entrada de <1>{{user}}</1>",
"mark_all_as_read": "marcar tudo como lido",
@ -261,5 +261,6 @@
"no_password_reset": "Você não conseguirá redefinir sua senha sem um e-mail.",
"invalid_post_title": "Título de publicação inválido",
"cake_day_info": "Hoje é o dia do bolo de {{ creator_name }}!",
"cake_day_title": "Dia do bolo:"
"cake_day_title": "Dia do bolo:",
"what_is": "Quanto é"
}

View file

@ -266,5 +266,8 @@
"emoji_picker": "Сборщик эмодзи",
"select_a_community": "Выбрать сообщество",
"invalid_username": "Неверное имя пользователя.",
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать."
"must_login": "Вы должны <1>авторизироваться или зарегестрироваться</1> что бы комментировать.",
"no_password_reset": "Вы не сможете сбросить ваш пароль без адреса электронной почты.",
"cake_day_title": "День торта:",
"what_is": "Что такое"
}