]> Untitled Git - lemmy.git/commitdiff
Squashed commit of the following:
authorDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 00:39:59 +0000 (19:39 -0500)
committerDessalines <tyhou13@gmx.com>
Fri, 24 Jan 2020 00:39:59 +0000 (19:39 -0500)
commit f5b75f342bdc35b6c770358b2408a7ffc9ce80ad
Merge: bd1fc2b 69389f6
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 19:17:42 2020 -0500

    Done merging http-api and private_message

commit bd1fc2b80b8c6a42f0df721d91863b8688b339ee
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 16:46:07 2020 -0500

    Remove danger from private-message.tsx

commit 69389f61c9944319b5c71d7fb398294f1f8b8e7c
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 11:21:21 2020 -0500

    Fixing http curl POST docs.

commit 7fdcae4f0739df79cbf8d7ea2138519d87f2aa32
Merge: dbe9ad0 752318f
Author: Dessalines <happydooby@gmail.com>
Date:   Thu Jan 23 11:01:06 2020 -0500

    Merge remote-tracking branch 'nutomic/http-api' into dessalines-http-api

commit 752318fdf3cf95633744e89bbfe22a761fecc53d
Author: Felix <me@nutomic.com>
Date:   Thu Jan 23 15:22:17 2020 +0100

    api fixes

commit 9ccff18f23509d309261670b57563b87e8f61f77
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 22:29:11 2020 -0500

    Adding a toaster to replace alerts. Fixes #457

commit 5197407dd29a895fff37d4f0982e44d5e727d426
Merge: bacb9ac 58f673a
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 21:20:38 2020 -0500

    Merge branch 'private_messaging' into dev

commit 58f673ab7856d2380cfaedbc46baf944fedfabb5
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 21:09:17 2020 -0500

    Adding message to comment node actions.

commit bacb9ac59ed3a6d8b4a80f37103ccf9c3ab751c9
Merge: 10c6505 7d3adda
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 20:37:08 2020 -0500

    Merge branch 'private_messaging' into dev

commit 10c65059686ff74fc2ce412572e2d6fc3497c43a
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 20:35:20 2020 -0500

    Adding correct hello_name to mail.

commit 7d3adda0cd4888e70b362cbad1b82f46d79c8d4c
Author: Dessalines <happydooby@gmail.com>
Date:   Wed Jan 22 16:35:29 2020 -0500

    Adding private messaging, and matrix user ids.

    - Fixes #244

commit dbe9ad0998b80d174130d82c11473555c7d6bc0e
Author: Dessalines <happydooby@gmail.com>
Date:   Mon Jan 20 18:49:54 2020 -0500

    Fixing last.

commit 20c9c54806429395f8a4c1a4e7a6a0f677d4e6b8
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 13:31:37 2020 -0500

    Updating API docs.

commit dc84ccaac94b9887f8512e3c32aeea0df89520be
Merge: 6c61dd2 3edd75e
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 10:06:25 2020 -0500

    Merge branch 'master' into dessalines-http-api

commit 6c61dd266bb6e389312ec56e7fc50f4a6b71b374
Merge: c5eecd0 e518954
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 09:09:00 2020 -0500

    Merge remote-tracking branch 'nutomic/websocket-generics' into dessalines-http-api

commit e518954bcab2553906cec9f2bcfa6d1b5fdc2f38
Author: Felix <me@nutomic.com>
Date:   Sun Jan 19 14:25:50 2020 +0100

    Use generics to reduce code duplication in websocket

commit c5eecd055e79510421aebe26e5d4f173a5916a3e
Author: Dessalines <happydooby@gmail.com>
Date:   Sun Jan 19 00:38:45 2020 -0500

    Strongly typing WebsocketJsonResponse. Forgot comment-form.tsx

commit 0c5eb471359929c532a4e6a63477a39ab6db67b6
Author: Dessalines <happydooby@gmail.com>
Date:   Sat Jan 18 23:54:10 2020 -0500

    First pass at fixing UI to work with new websocketresponses.

commit baf77bb6be88e796683bc21fd16a0359e68c7791
Author: Felix <me@nutomic.com>
Date:   Sat Jan 18 17:25:45 2020 +0100

    simplify json serialization code

commit 047ec97e1857888aeeac53629a76a4dc7e2f110b
Author: Felix <me@nutomic.com>
Date:   Sat Jan 18 14:22:25 2020 +0100

    rewrite api endpoint urls

commit 2fb4900b0cf586674d6212e0aa2c1b852d104a1a
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 17:04:37 2020 +0100

    fix typo

commit cba80815797b6578ab817b99fee35575cccef27d
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 16:47:38 2020 +0100

    fix formatting

commit d7285d8c25c9a88c33519ec12f3c730d6b45fcf5
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 16:09:01 2020 +0100

    small fix

commit 415040a1e9b90fcb71872a41fbb6e7e7131d966d
Author: Felix <me@nutomic.com>
Date:   Thu Jan 16 15:39:08 2020 +0100

    working!

commit 7a97c981a0b74cf6f66574690e58917a47508395
Author: Felix <me@nutomic.com>
Date:   Wed Jan 15 16:48:21 2020 +0100

    try to simplify code with higher order functions

commit c41082f98f459da2cc48dcfd7d9d5c2ac08865d0
Author: Felix <me@nutomic.com>
Date:   Wed Jan 15 16:37:25 2020 +0100

    Implement HTTP API using generics (fixes #380)

63 files changed:
README.md
docs/src/SUMMARY.md
docs/src/contributing_websocket_http_api.md [moved from docs/src/contributing_websocket_api.md with 68% similarity]
server/.rustfmt.toml
server/migrations/2020-01-21-001001_create_private_message/down.sql [new file with mode: 0644]
server/migrations/2020-01-21-001001_create_private_message/up.sql [new file with mode: 0644]
server/src/api/comment.rs
server/src/api/community.rs
server/src/api/mod.rs
server/src/api/post.rs
server/src/api/site.rs
server/src/api/user.rs
server/src/apub/mod.rs
server/src/db/comment.rs
server/src/db/comment_view.rs
server/src/db/community.rs
server/src/db/mod.rs
server/src/db/moderator.rs
server/src/db/password_reset_request.rs
server/src/db/post.rs
server/src/db/post_view.rs
server/src/db/private_message.rs [new file with mode: 0644]
server/src/db/private_message_view.rs [new file with mode: 0644]
server/src/db/user.rs
server/src/db/user_mention.rs
server/src/db/user_view.rs
server/src/lib.rs
server/src/main.rs
server/src/routes/api.rs [new file with mode: 0644]
server/src/routes/index.rs
server/src/routes/mod.rs
server/src/schema.rs
server/src/websocket/mod.rs
server/src/websocket/server.rs
ui/assets/css/toastify.css [new file with mode: 0644]
ui/package.json
ui/src/components/comment-form.tsx
ui/src/components/comment-node.tsx
ui/src/components/communities.tsx
ui/src/components/community-form.tsx
ui/src/components/community.tsx
ui/src/components/create-private-message.tsx [new file with mode: 0644]
ui/src/components/inbox.tsx
ui/src/components/login.tsx
ui/src/components/main.tsx
ui/src/components/modlog.tsx
ui/src/components/moment-time.tsx
ui/src/components/navbar.tsx
ui/src/components/password_change.tsx
ui/src/components/post-form.tsx
ui/src/components/post.tsx
ui/src/components/private-message-form.tsx [new file with mode: 0644]
ui/src/components/private-message.tsx [new file with mode: 0644]
ui/src/components/search.tsx
ui/src/components/setup.tsx
ui/src/components/user.tsx
ui/src/index.html
ui/src/index.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts
ui/src/translations/en.ts
ui/src/utils.ts
ui/yarn.lock

index e4a7d2f6e6a61c9e800fd2966264e2e99a57ed02..e9be459db04dd409578b3325f4cd8af75894534d 100644 (file)
--- a/README.md
+++ b/README.md
@@ -157,15 +157,15 @@ If you'd like to add translations, take a look a look at the [English translatio
 
 lang | done | missing
 --- | --- | ---
-de | 93% | avatar,upload_avatar,show_avatars,docs,old_password,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists 
-eo | 80% | number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,are_you_sure,yes,no,email_already_exists 
-es | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists 
-fr | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists 
-it | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists 
-nl | 99% | donate_to_lemmy,donate,email_already_exists 
-ru | 77% | cross_posts,cross_post,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists 
-sv | 89% | avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,email_already_exists 
-zh | 75% | cross_posts,cross_post,users,number_of_communities,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,transfer_community,transfer_site,are_you_sure,yes,no,email_already_exists 
+de | 88% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,docs,message_sent,messages,old_password,matrix_user_id,private_message_disclaimer,send_notifications_to_email,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+eo | 76% | number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,theme,donate_to_lemmy,donate,from,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+es | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+fr | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+it | 84% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+nl | 93% | create_private_message,send_secure_message,send_message,message,message_sent,messages,matrix_user_id,private_message_disclaimer,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+ru | 72% | cross_posts,cross_post,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+sv | 83% | create_private_message,send_secure_message,send_message,message,avatar,upload_avatar,show_avatars,archive_link,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,donate_to_lemmy,donate,from,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
+zh | 70% | cross_posts,cross_post,users,number_of_communities,create_private_message,send_secure_message,send_message,message,preview,upload_image,avatar,upload_avatar,show_avatars,formatting_help,view_source,sticky,unsticky,archive_link,settings,stickied,delete_account,delete_account_confirm,banned,creator,number_online,docs,replies,mentions,message_sent,messages,old_password,forgot_password,reset_password_mail_sent,password_change,new_password,no_email_setup,matrix_user_id,private_message_disclaimer,send_notifications_to_email,language,browser_default,downvotes_disabled,enable_downvotes,open_registration,registration_closed,enable_nsfw,recent_comments,nsfw,show_nsfw,theme,donate_to_lemmy,donate,monero,by,to,from,transfer_community,transfer_site,are_you_sure,yes,no,logged_in,email_already_exists,couldnt_create_private_message,no_private_message_edit_allowed,couldnt_update_private_message 
 
 <!-- translationsstop -->
 
index d891697464c95745699ee86215c9a262373e60e9..c2df6223e6d09c618c30fe52e8544c94de0cfba3 100644 (file)
@@ -12,5 +12,5 @@
 - [Contributing](contributing.md)
   - [Docker Development](contributing_docker_development.md)
   - [Local Development](contributing_local_development.md)
-  - [Websocket API](contributing_websocket_api.md)
+  - [Websocket/HTTP API](contributing_websocket_http_api.md)
   - [ActivityPub API Outline](contributing_apub_api_outline.md)
similarity index 68%
rename from docs/src/contributing_websocket_api.md
rename to docs/src/contributing_websocket_http_api.md
index 16383d532b4be7ae33faf81a036e9e7c2f06101e..9e87d4faa1660a3761e85c061f33ca228c77b958 100644 (file)
 
 - [Data types](#data-types)
 - [Basic usage](#basic-usage)
-  * [WebSocket Endpoint](#websocket-endpoint)
-  * [Testing with Websocat](#testing-with-websocat)
-  * [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api)
+  * [WebSocket](#websocket)
+    + [Testing with Websocat](#testing-with-websocat)
+    + [Testing with the WebSocket JavaScript API](#testing-with-the-websocket-javascript-api)
+  * [HTTP](#http)
+    + [Testing with Curl](#testing-with-curl)
+      - [Get Example](#get-example)
+      - [Post Example](#post-example)
 - [Rate limits](#rate-limits)
 - [Errors](#errors)
 - [API documentation](#api-documentation)
   * [Sort Types](#sort-types)
+  * [Websocket vs HTTP](#websocket-vs-http)
   * [User / Authentication / Admin actions](#user--authentication--admin-actions)
     + [Login](#login)
       - [Request](#request)
       - [Response](#response)
+      - [HTTP](#http-1)
     + [Register](#register)
       - [Request](#request-1)
       - [Response](#response-1)
+      - [HTTP](#http-2)
     + [Get User Details](#get-user-details)
       - [Request](#request-2)
       - [Response](#response-2)
+      - [HTTP](#http-3)
     + [Save User Settings](#save-user-settings)
       - [Request](#request-3)
       - [Response](#response-3)
+      - [HTTP](#http-4)
     + [Get Replies / Inbox](#get-replies--inbox)
       - [Request](#request-4)
       - [Response](#response-4)
+      - [HTTP](#http-5)
     + [Get User Mentions](#get-user-mentions)
       - [Request](#request-5)
       - [Response](#response-5)
-    + [Mark All As Read](#mark-all-as-read)
+      - [HTTP](#http-6)
+    + [Edit User Mention](#edit-user-mention)
       - [Request](#request-6)
       - [Response](#response-6)
-    + [Delete Account](#delete-account)
+      - [HTTP](#http-7)
+    + [Mark All As Read](#mark-all-as-read)
       - [Request](#request-7)
       - [Response](#response-7)
-    + [Add admin](#add-admin)
+      - [HTTP](#http-8)
+    + [Delete Account](#delete-account)
       - [Request](#request-8)
       - [Response](#response-8)
-    + [Ban user](#ban-user)
+      - [HTTP](#http-9)
+    + [Add admin](#add-admin)
       - [Request](#request-9)
       - [Response](#response-9)
-  * [Site](#site)
-    + [List Categories](#list-categories)
+      - [HTTP](#http-10)
+    + [Ban user](#ban-user)
       - [Request](#request-10)
       - [Response](#response-10)
-    + [Search](#search)
+      - [HTTP](#http-11)
+  * [Site](#site)
+    + [List Categories](#list-categories)
       - [Request](#request-11)
       - [Response](#response-11)
-    + [Get Modlog](#get-modlog)
+      - [HTTP](#http-12)
+    + [Search](#search)
       - [Request](#request-12)
       - [Response](#response-12)
-    + [Create Site](#create-site)
+      - [HTTP](#http-13)
+    + [Get Modlog](#get-modlog)
       - [Request](#request-13)
       - [Response](#response-13)
-    + [Edit Site](#edit-site)
+      - [HTTP](#http-14)
+    + [Create Site](#create-site)
       - [Request](#request-14)
       - [Response](#response-14)
-    + [Get Site](#get-site)
+      - [HTTP](#http-15)
+    + [Edit Site](#edit-site)
       - [Request](#request-15)
       - [Response](#response-15)
-    + [Transfer Site](#transfer-site)
+      - [HTTP](#http-16)
+    + [Get Site](#get-site)
       - [Request](#request-16)
       - [Response](#response-16)
-  * [Community](#community)
-    + [Get Community](#get-community)
+      - [HTTP](#http-17)
+    + [Transfer Site](#transfer-site)
       - [Request](#request-17)
       - [Response](#response-17)
-    + [Create Community](#create-community)
+      - [HTTP](#http-18)
+  * [Community](#community)
+    + [Get Community](#get-community)
       - [Request](#request-18)
       - [Response](#response-18)
-    + [List Communities](#list-communities)
+      - [HTTP](#http-19)
+    + [Create Community](#create-community)
       - [Request](#request-19)
       - [Response](#response-19)
-    + [Ban from Community](#ban-from-community)
+      - [HTTP](#http-20)
+    + [List Communities](#list-communities)
       - [Request](#request-20)
       - [Response](#response-20)
-    + [Add Mod to Community](#add-mod-to-community)
+      - [HTTP](#http-21)
+    + [Ban from Community](#ban-from-community)
       - [Request](#request-21)
       - [Response](#response-21)
-    + [Edit Community](#edit-community)
+      - [HTTP](#http-22)
+    + [Add Mod to Community](#add-mod-to-community)
       - [Request](#request-22)
       - [Response](#response-22)
-    + [Follow Community](#follow-community)
+      - [HTTP](#http-23)
+    + [Edit Community](#edit-community)
       - [Request](#request-23)
       - [Response](#response-23)
-    + [Get Followed Communities](#get-followed-communities)
+      - [HTTP](#http-24)
+    + [Follow Community](#follow-community)
       - [Request](#request-24)
       - [Response](#response-24)
-    + [Transfer Community](#transfer-community)
+      - [HTTP](#http-25)
+    + [Get Followed Communities](#get-followed-communities)
       - [Request](#request-25)
       - [Response](#response-25)
-  * [Post](#post)
-    + [Create Post](#create-post)
+      - [HTTP](#http-26)
+    + [Transfer Community](#transfer-community)
       - [Request](#request-26)
       - [Response](#response-26)
-    + [Get Post](#get-post)
+      - [HTTP](#http-27)
+  * [Post](#post)
+    + [Create Post](#create-post)
       - [Request](#request-27)
       - [Response](#response-27)
-    + [Get Posts](#get-posts)
+      - [HTTP](#http-28)
+    + [Get Post](#get-post)
       - [Request](#request-28)
       - [Response](#response-28)
-    + [Create Post Like](#create-post-like)
+      - [HTTP](#http-29)
+    + [Get Posts](#get-posts)
       - [Request](#request-29)
       - [Response](#response-29)
-    + [Edit Post](#edit-post)
+      - [HTTP](#http-30)
+    + [Create Post Like](#create-post-like)
       - [Request](#request-30)
       - [Response](#response-30)
-    + [Save Post](#save-post)
+      - [HTTP](#http-31)
+    + [Edit Post](#edit-post)
       - [Request](#request-31)
       - [Response](#response-31)
-  * [Comment](#comment)
-    + [Create Comment](#create-comment)
+      - [HTTP](#http-32)
+    + [Save Post](#save-post)
       - [Request](#request-32)
       - [Response](#response-32)
-    + [Edit Comment](#edit-comment)
+      - [HTTP](#http-33)
+  * [Comment](#comment)
+    + [Create Comment](#create-comment)
       - [Request](#request-33)
       - [Response](#response-33)
-    + [Save Comment](#save-comment)
+      - [HTTP](#http-34)
+    + [Edit Comment](#edit-comment)
       - [Request](#request-34)
       - [Response](#response-34)
-    + [Create Comment Like](#create-comment-like)
+      - [HTTP](#http-35)
+    + [Save Comment](#save-comment)
       - [Request](#request-35)
       - [Response](#response-35)
+      - [HTTP](#http-36)
+    + [Create Comment Like](#create-comment-like)
+      - [Request](#request-36)
+      - [Response](#response-36)
+      - [HTTP](#http-37)
   * [RSS / Atom feeds](#rss--atom-feeds)
     + [All](#all)
     + [Community](#community-1)
 
 Request and response strings are in [JSON format](https://www.json.org).
 
-### WebSocket Endpoint
+### WebSocket
 
 Connect to <code>ws://***host***/api/v1/ws</code> to get started.
 
 If the ***`host`*** supports secure connections, you can use <code>wss://***host***/api/v1/ws</code>.
 
-### Testing with Websocat
+#### Testing with Websocat
 
 [Websocat link](https://github.com/vi/websocat)
 
@@ -159,7 +204,7 @@ If the ***`host`*** supports secure connections, you can use <code>wss://***host
 A simple test command:
 `{"op": "ListCategories"}`
 
-### Testing with the WebSocket JavaScript API
+#### Testing with the WebSocket JavaScript API
 
 [WebSocket JavaScript API](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API)
 ```javascript
@@ -171,6 +216,32 @@ ws.onopen = function () {
   }));
 };
 ```
+### HTTP
+
+Endpoints are at <code>http://***host***/api/v1/***endpoint***</code>. They'll be listed below for each action.
+
+#### Testing with Curl
+
+##### Get Example
+
+```
+curl /community/list?sort=Hot
+```
+
+##### Post Example
+
+```
+curl -i -H \
+"Content-Type: application/json" \
+-X POST \
+-d '{
+  "comment_id": X,
+  "post_id": X,
+  "score": X,
+  "auth": "..."
+}' \
+/comment/like
+```
 
 ## Rate limits
 
@@ -201,6 +272,11 @@ 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.
 
+### Websocket vs HTTP
+
+- Below are the websocket JSON requests / responses. For HTTP, ignore all fields except those inside `data`.
+- For example, an http login will be a `POST` `{username_or_email: X, password: X}`
+
 ### User / Authentication / Admin actions
 
 #### Login
@@ -220,13 +296,19 @@ The `jwt` string should be stored and used anywhere `auth` is called for.
 ##### Response
 ```rust
 {
-  op: String,
-  jwt: String
+  op: "Login",
+  data: {
+    jwt: String,
+  }
 }
 ```
 
+##### HTTP
+
+`POST /user/login`
 
 #### Register
+
 Only the first user will be able to be the admin.
 
 ##### Request
@@ -245,11 +327,17 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: String,
-  jwt: String
+  op: "Register",
+  data: {
+    jwt: String,
+  }
 }
 ```
 
+##### HTTP
+
+`POST /user/register`
+
 #### Get User Details
 ##### Request
 ```rust
@@ -270,14 +358,20 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: String,
-  user: UserView,
-  follows: Vec<CommunityFollowerView>,
-  moderates: Vec<CommunityModeratorView>,
-  comments: Vec<CommentView>,
-  posts: Vec<PostView>,
+  op: "GetUserDetails",
+  data: {
+    user: UserView,
+    follows: Vec<CommunityFollowerView>,
+    moderates: Vec<CommunityModeratorView>,
+    comments: Vec<CommentView>,
+    posts: Vec<PostView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /user`
+
 #### Save User Settings
 ##### Request
 ```rust
@@ -295,10 +389,16 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: String,
-  jwt: String
+  op: "SaveUserSettings",
+  data: {
+    jwt: String
+  }
 }
 ```
+##### HTTP
+
+`PUT /save_user_settings`
+
 #### Get Replies / Inbox
 ##### Request
 ```rust
@@ -316,10 +416,16 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: String,
-  replies: Vec<ReplyView>,
+  op: "GetReplies",
+  data: {
+    replies: Vec<ReplyView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /user/replies`
+
 
 #### Get User Mentions
 ##### Request
@@ -338,10 +444,41 @@ Only the first user will be able to be the admin.
 ##### Response
 ```rust
 {
-  op: String,
-  mentions: Vec<UserMentionView>,
+  op: "GetUserMentions",
+  data: {
+    mentions: Vec<UserMentionView>,
+  }
+}
+```
+
+##### HTTP
+
+`GET /user/mentions`
+
+#### Edit User Mention
+##### Request
+```rust
+{
+  op: "EditUserMention",
+  data: {
+    user_mention_id: i32,
+    read: Option<bool>,
+    auth: String,
+  }
+}
+```
+##### Response
+```rust
+{
+  op: "EditUserMention",
+  data: {
+    mention: UserMentionView,
+  }
 }
 ```
+##### HTTP
+
+`PUT /user/mention`
 
 #### Mark All As Read
 
@@ -359,11 +496,17 @@ Marks all user replies and mentions as read.
 ##### Response
 ```rust
 {
-  op: String,
-  replies: Vec<ReplyView>,
+  op: "MarkAllAsRead",
+  data: {
+    replies: Vec<ReplyView>,
+  }
 }
 ```
 
+##### HTTP
+
+`POST /user/mark_all_as_read`
+
 #### Delete Account
 
 *Permananently deletes your posts and comments*
@@ -381,11 +524,17 @@ Marks all user replies and mentions as read.
 ##### Response
 ```rust
 {
-  op: String,
-  jwt: String,
+  op: "DeleteAccount",
+  data: {
+    jwt: String,
+  }
 }
 ```
 
+##### HTTP
+
+`POST /user/delete_account`
+
 #### Add admin
 ##### Request
 ```rust
@@ -401,10 +550,15 @@ Marks all user replies and mentions as read.
 ##### Response
 ```rust
 {
-  op: String,
-  admins: Vec<UserView>,
+  op: "AddAdmin",
+  data: {
+    admins: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`POST /admin/add`
 
 #### Ban user
 ##### Request
@@ -423,11 +577,16 @@ Marks all user replies and mentions as read.
 ##### Response
 ```rust
 {
-  op: String,
-  user: UserView,
-  banned: bool,
+  op: "BanUser",
+  data: {
+    user: UserView,
+    banned: bool,
+  }
 }
 ```
+##### HTTP
+
+`POST /user/ban`
 
 ### Site
 #### List Categories
@@ -440,13 +599,19 @@ Marks all user replies and mentions as read.
 ##### Response
 ```rust
 {
-  op: String,
-  categories: Vec<Category>
+  op: "ListCategories",
+  data: {
+    categories: Vec<Category>
+  }
 }
 ```
+##### HTTP
+
+`GET /categories`
 
 #### Search
-Search types are `Both, Comments, Posts`.
+
+Search types are `All, Comments, Posts, Communities, Users, Url`
 
 ##### Request
 ```rust
@@ -459,17 +624,26 @@ Search types are `Both, Comments, Posts`.
     sort: String,
     page: Option<i64>,
     limit: Option<i64>,
+    auth?: Option<String>,
   }
 }
 ```
 ##### Response
 ```rust
 {
-  op: String,
-  comments: Vec<CommentView>,
-  posts: Vec<PostView>,
+  op: "Search",
+  data: {
+    type_: String,
+    comments: Vec<CommentView>,
+    posts: Vec<PostView>,
+    communities: Vec<CommunityView>,
+    users: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`POST /search`
 
 #### Get Modlog
 ##### Request
@@ -487,18 +661,24 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  removed_posts: Vec<ModRemovePostView>,
-  locked_posts: Vec<ModLockPostView>,
-  removed_comments: Vec<ModRemoveCommentView>,
-  removed_communities: Vec<ModRemoveCommunityView>,
-  banned_from_community: Vec<ModBanFromCommunityView>,
-  banned: Vec<ModBanView>,
-  added_to_community: Vec<ModAddCommunityView>,
-  added: Vec<ModAddView>,
+  op: "GetModlog",
+  data: {
+    removed_posts: Vec<ModRemovePostView>,
+    locked_posts: Vec<ModLockPostView>,
+    removed_comments: Vec<ModRemoveCommentView>,
+    removed_communities: Vec<ModRemoveCommunityView>,
+    banned_from_community: Vec<ModBanFromCommunityView>,
+    banned: Vec<ModBanView>,
+    added_to_community: Vec<ModAddCommunityView>,
+    added: Vec<ModAddView>,
+  }
 }
 ```
 
+##### HTTP
+
+`GET /modlog`
+
 #### Create Site
 ##### Request
 ```rust
@@ -514,11 +694,17 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  site: SiteView,
+  op: "CreateSite",
+    data: {
+    site: SiteView,
+  }
 }
 ```
 
+##### HTTP
+
+`POST /site`
+
 #### Edit Site
 ##### Request
 ```rust
@@ -534,10 +720,15 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  site: SiteView,
+  op: "EditSite",
+  data: {
+    site: SiteView,
+  }
 }
 ```
+##### HTTP
+
+`PUT /site`
 
 #### Get Site
 ##### Request
@@ -549,12 +740,17 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  site: Option<SiteView>,
-  admins: Vec<UserView>,
-  banned: Vec<UserView>,
+  op: "GetSite",
+  data: {
+    site: Option<SiteView>,
+    admins: Vec<UserView>,
+    banned: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /site`
 
 #### Transfer Site
 ##### Request
@@ -570,12 +766,17 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  site: Option<SiteView>,
-  admins: Vec<UserView>,
-  banned: Vec<UserView>,
+  op: "TransferSite",
+  data: {
+    site: Option<SiteView>,
+    admins: Vec<UserView>,
+    banned: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`POST /site/transfer`
 
 ### Community
 #### Get Community
@@ -593,12 +794,17 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  community: CommunityView,
-  moderators: Vec<CommunityModeratorView>,
-  admins: Vec<UserView>,
+  op: "GetCommunity",
+  data: {
+    community: CommunityView,
+    moderators: Vec<CommunityModeratorView>,
+    admins: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /community`
 
 #### Create Community
 ##### Request
@@ -617,10 +823,15 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  community: CommunityView
+  op: "CreateCommunity",
+  data: {
+    community: CommunityView
+  }
 }
 ```
+##### HTTP
+
+`POST /community`
 
 #### List Communities
 ##### Request
@@ -638,10 +849,15 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  communities: Vec<CommunityView>
+  op: "ListCommunities",
+  data: {
+    communities: Vec<CommunityView>
+  }
 }
 ```
+##### HTTP
+
+`GET /community/list`
 
 #### Ban from Community
 ##### Request
@@ -661,11 +877,16 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  user: UserView,
-  banned: bool,
+  op: "BanFromCommunity",
+  data: {
+    user: UserView,
+    banned: bool,
+  }
 }
 ```
+##### HTTP
+
+`POST /community/ban_user`
 
 #### Add Mod to Community
 ##### Request
@@ -683,10 +904,15 @@ Search types are `Both, Comments, Posts`.
 ##### Response
 ```rust
 {
-  op: String,
-  moderators: Vec<CommunityModeratorView>,
+  op: "AddModToCommunity",
+  data: {
+    moderators: Vec<CommunityModeratorView>,
+  }
 }
 ```
+##### HTTP
+
+`POST /community/mod`
 
 #### Edit Community
 Mods and admins can remove and lock a community, creators can delete it.
@@ -712,10 +938,15 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  community: CommunityView
+  op: "EditCommunity",
+  data: {
+    community: CommunityView
+  }
 }
 ```
+##### HTTP
+
+`PUT /community`
 
 #### Follow Community
 ##### Request
@@ -732,10 +963,15 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  community: CommunityView
+  op: "FollowCommunity",
+  data: {
+    community: CommunityView
+  }
 }
 ```
+##### HTTP
+
+`POST /community/follow`
 
 #### Get Followed Communities
 ##### Request
@@ -750,10 +986,15 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  communities: Vec<CommunityFollowerView>
+  op: "GetFollowedCommunities",
+  data: {
+    communities: Vec<CommunityFollowerView>
+  }
 }
 ```
+##### HTTP
+
+`GET /user/followed_communities`
 
 #### Transfer Community
 ##### Request
@@ -770,12 +1011,17 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  community: CommunityView,
-  moderators: Vec<CommunityModeratorView>,
-  admins: Vec<UserView>,
+  op: "TransferCommunity",
+  data: {
+    community: CommunityView,
+    moderators: Vec<CommunityModeratorView>,
+    admins: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`POST /community/transfer`
 
 ### Post
 #### Create Post
@@ -795,10 +1041,15 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  post: PostView
+  op: "CreatePost",
+  data: {
+    post: PostView
+  }
 }
 ```
+##### HTTP
+
+`POST /post`
 
 #### Get Post
 ##### Request
@@ -814,16 +1065,22 @@ Mods and admins can remove and lock a community, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  post: PostView,
-  comments: Vec<CommentView>,
-  community: CommunityView,
-  moderators: Vec<CommunityModeratorView>,
-  admins: Vec<UserView>,
+  op: "GetPost",
+  data: {
+    post: PostView,
+    comments: Vec<CommentView>,
+    community: CommunityView,
+    moderators: Vec<CommunityModeratorView>,
+    admins: Vec<UserView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /post`
 
 #### Get Posts
+
 Post listing types are `All, Subscribed, Community`
 
 ##### Request
@@ -843,12 +1100,18 @@ Post listing types are `All, Subscribed, Community`
 ##### Response
 ```rust
 {
-  op: String,
-  posts: Vec<PostView>,
+  op: "GetPosts",
+  data: {
+    posts: Vec<PostView>,
+  }
 }
 ```
+##### HTTP
+
+`GET /post/list`
 
 #### Create Post Like
+
 `score` can be 0, -1, or 1
 
 ##### Request
@@ -865,12 +1128,18 @@ Post listing types are `All, Subscribed, Community`
 ##### Response
 ```rust
 {
-  op: String,
-  post: PostView
+  op: "CreatePostLike",
+  data: {
+    post: PostView
+  }
 }
 ```
+##### HTTP
+
+`POST /post/like`
 
 #### Edit Post
+
 Mods and admins can remove and lock a post, creators can delete it.
 
 ##### Request
@@ -895,11 +1164,17 @@ Mods and admins can remove and lock a post, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  post: PostView
+  op: "EditPost",
+  data: {
+    post: PostView
+  }
 }
 ```
 
+##### HTTP
+
+`PUT /post`
+
 #### Save Post
 ##### Request
 ```rust
@@ -915,10 +1190,15 @@ Mods and admins can remove and lock a post, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  post: PostView
+  op: "SavePost",
+  data: {
+    post: PostView
+  }
 }
 ```
+##### HTTP
+
+`POST /post/save`
 
 ### Comment
 #### Create Comment
@@ -938,12 +1218,19 @@ Mods and admins can remove and lock a post, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  comment: CommentView
+  op: "CreateComment",
+  data: {
+    comment: CommentView
+  }
 }
 ```
 
+##### HTTP
+
+`POST /comment`
+
 #### Edit Comment
+
 Mods and admins can remove a comment, creators can delete it.
 
 ##### Request
@@ -967,10 +1254,15 @@ Mods and admins can remove a comment, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  comment: CommentView
+  op: "EditComment",
+  data: {
+    comment: CommentView
+  }
 }
 ```
+##### HTTP
+
+`PUT /comment`
 
 #### Save Comment
 ##### Request
@@ -987,12 +1279,18 @@ Mods and admins can remove a comment, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  comment: CommentView
+  op: "SaveComment",
+  data: {
+    comment: CommentView
+  }
 }
 ```
+##### HTTP
+
+`POST /comment/save`
 
 #### Create Comment Like
+
 `score` can be 0, -1, or 1
 
 ##### Request
@@ -1010,10 +1308,15 @@ Mods and admins can remove a comment, creators can delete it.
 ##### Response
 ```rust
 {
-  op: String,
-  comment: CommentView
+  op: "CreateCommentLike",
+  data: {
+    comment: CommentView
+  }
 }
 ```
+##### HTTP
+
+`POST /comment/like`
 
 ### RSS / Atom feeds
 
index b1fce9c9a5718f2922ca5593fdd64b58d1920da8..684a7f8a27ae738ba679bcbdc8a3fc4ac1b9c305 100644 (file)
@@ -1,2 +1,2 @@
 tab_spaces = 2
-edition="2018"
+edition="2018"
\ No newline at end of file
diff --git a/server/migrations/2020-01-21-001001_create_private_message/down.sql b/server/migrations/2020-01-21-001001_create_private_message/down.sql
new file mode 100644 (file)
index 0000000..0d951e3
--- /dev/null
@@ -0,0 +1,34 @@
+-- Drop the triggers
+drop trigger refresh_private_message on private_message;
+drop function refresh_private_message();
+
+-- Drop the view and table
+drop view private_message_view cascade;
+drop table private_message;
+
+-- Rebuild the old views
+drop view user_view cascade;
+create view user_view as 
+select 
+u.id,
+u.name,
+u.avatar,
+u.email,
+u.fedi_name,
+u.admin,
+u.banned,
+u.show_avatars,
+u.send_notifications_to_email,
+u.published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
+
+-- Drop the columns
+alter table user_ drop column matrix_user_id;
diff --git a/server/migrations/2020-01-21-001001_create_private_message/up.sql b/server/migrations/2020-01-21-001001_create_private_message/up.sql
new file mode 100644 (file)
index 0000000..48e16dd
--- /dev/null
@@ -0,0 +1,90 @@
+-- Creating private message
+create table private_message (
+  id serial primary key,
+  creator_id int references user_ on update cascade on delete cascade not null,
+  recipient_id int references user_ on update cascade on delete cascade not null,
+  content text not null,
+  deleted boolean default false not null,
+  read boolean default false not null,
+  published timestamp not null default now(),
+  updated timestamp
+);
+
+-- Create the view and materialized view which has the avatar and creator name
+create view private_message_view as 
+select        
+pm.*,
+u.name as creator_name,
+u.avatar as creator_avatar,
+u2.name as recipient_name,
+u2.avatar as recipient_avatar
+from private_message pm
+inner join user_ u on u.id = pm.creator_id
+inner join user_ u2 on u2.id = pm.recipient_id;
+
+create materialized view private_message_mview as select * from private_message_view;
+
+create unique index idx_private_message_mview_id on private_message_mview (id);
+
+-- Create the triggers
+create or replace function refresh_private_message()
+returns trigger language plpgsql
+as $$
+begin
+  refresh materialized view concurrently private_message_mview;
+  return null;
+end $$;
+
+create trigger refresh_private_message
+after insert or update or delete or truncate
+on private_message
+for each statement
+execute procedure refresh_private_message();
+
+-- Update user to include matrix id
+alter table user_ add column matrix_user_id text unique;
+
+drop view user_view cascade;
+create view user_view as 
+select 
+u.id,
+u.name,
+u.avatar,
+u.email,
+u.matrix_user_id,
+u.fedi_name,
+u.admin,
+u.banned,
+u.show_avatars,
+u.send_notifications_to_email,
+u.published,
+(select count(*) from post p where p.creator_id = u.id) as number_of_posts,
+(select coalesce(sum(score), 0) from post p, post_like pl where u.id = p.creator_id and p.id = pl.post_id) as post_score,
+(select count(*) from comment c where c.creator_id = u.id) as number_of_comments,
+(select coalesce(sum(score), 0) from comment c, comment_like cl where u.id = c.creator_id and c.id = cl.comment_id) as comment_score
+from user_ u;
+
+create materialized view user_mview as select * from user_view;
+
+create unique index idx_user_mview_id on user_mview (id);
+
+-- This is what a group pm table would look like
+-- Not going to do it now because of the complications
+-- 
+-- create table private_message (
+--   id serial primary key,
+--   creator_id int references user_ on update cascade on delete cascade not null,
+--   content text not null,
+--   deleted boolean default false not null,
+--   published timestamp not null default now(),
+--   updated timestamp
+-- );
+-- 
+-- create table private_message_recipient (
+--   id serial primary key,
+--   private_message_id int references private_message on update cascade on delete cascade not null,
+--   recipient_id int references user_ on update cascade on delete cascade not null,
+--   read boolean default false not null,
+--   published timestamp not null default now(),
+--   unique(private_message_id, recipient_id)
+-- )
index 61cc95063344c5af6618b95a3ae157cf6835bb21..8efb30fbde4c2840d50cba3da47b68f0fb003c32 100644 (file)
@@ -7,7 +7,7 @@ use diesel::PgConnection;
 pub struct CreateComment {
   content: String,
   parent_id: Option<i32>,
-  edit_id: Option<i32>,
+  edit_id: Option<i32>, // TODO this isn't used
   pub post_id: i32,
   auth: String,
 }
@@ -15,7 +15,7 @@ pub struct CreateComment {
 #[derive(Serialize, Deserialize)]
 pub struct EditComment {
   content: String,
-  parent_id: Option<i32>,
+  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,
@@ -35,7 +35,6 @@ pub struct SaveComment {
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct CommentResponse {
-  op: String,
   pub comment: CommentView,
 }
 
@@ -53,7 +52,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -63,12 +62,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "community_ban").into());
+      return Err(APIError::err("community_ban").into());
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     let content_slurs_removed = remove_slurs(&data.content.to_owned());
@@ -86,7 +85,7 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
 
     let inserted_comment = match Comment::create(&conn, &comment_form) {
       Ok(comment) => comment,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_comment").into()),
+      Err(_e) => return Err(APIError::err("couldnt_create_comment").into()),
     };
 
     // Scan the comment for user mentions, add those rows
@@ -193,13 +192,12 @@ impl Perform<CommentResponse> for Oper<CreateComment> {
 
     let _inserted_like = match CommentLike::like(&conn, &like_form) {
       Ok(like) => like,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
+      Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
     };
 
     let comment_view = CommentView::read(&conn, inserted_comment.id, Some(user_id))?;
 
     Ok(CommentResponse {
-      op: self.op.to_string(),
       comment: comment_view,
     })
   }
@@ -211,7 +209,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -231,17 +229,17 @@ impl Perform<CommentResponse> for Oper<EditComment> {
       editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
 
       if !editors.contains(&user_id) {
-        return Err(APIError::err(&self.op, "no_comment_edit_allowed").into());
+        return Err(APIError::err("no_comment_edit_allowed").into());
       }
 
       // Check for a community ban
       if CommunityUserBanView::get(&conn, user_id, orig_comment.community_id).is_ok() {
-        return Err(APIError::err(&self.op, "community_ban").into());
+        return Err(APIError::err("community_ban").into());
       }
 
       // Check for a site ban
       if UserView::read(&conn, user_id)?.banned {
-        return Err(APIError::err(&self.op, "site_ban").into());
+        return Err(APIError::err("site_ban").into());
       }
     }
 
@@ -264,7 +262,7 @@ impl Perform<CommentResponse> for Oper<EditComment> {
 
     let _updated_comment = match Comment::update(&conn, data.edit_id, &comment_form) {
       Ok(comment) => comment,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
     };
 
     // Scan the comment for user mentions, add those rows
@@ -310,7 +308,6 @@ impl Perform<CommentResponse> for Oper<EditComment> {
     let comment_view = CommentView::read(&conn, data.edit_id, Some(user_id))?;
 
     Ok(CommentResponse {
-      op: self.op.to_string(),
       comment: comment_view,
     })
   }
@@ -322,7 +319,7 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -335,19 +332,18 @@ impl Perform<CommentResponse> for Oper<SaveComment> {
     if data.save {
       match CommentSaved::save(&conn, &comment_saved_form) {
         Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
       };
     } else {
       match CommentSaved::unsave(&conn, &comment_saved_form) {
         Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_save_comment").into()),
       };
     }
 
     let comment_view = CommentView::read(&conn, data.comment_id, Some(user_id))?;
 
     Ok(CommentResponse {
-      op: self.op.to_string(),
       comment: comment_view,
     })
   }
@@ -359,7 +355,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -368,19 +364,19 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     if data.score == -1 {
       let site = SiteView::read(&conn)?;
       if !site.enable_downvotes {
-        return Err(APIError::err(&self.op, "downvotes_disabled").into());
+        return Err(APIError::err("downvotes_disabled").into());
       }
     }
 
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "community_ban").into());
+      return Err(APIError::err("community_ban").into());
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     let like_form = CommentLikeForm {
@@ -398,7 +394,7 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     if do_add {
       let _inserted_like = match CommentLike::like(&conn, &like_form) {
         Ok(like) => like,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_like_comment").into()),
       };
     }
 
@@ -406,7 +402,6 @@ impl Perform<CommentResponse> for Oper<CreateCommentLike> {
     let liked_comment = CommentView::read(&conn, data.comment_id, Some(user_id))?;
 
     Ok(CommentResponse {
-      op: self.op.to_string(),
       comment: liked_comment,
     })
   }
index 0bf846c3d9dd4488a1bf62811657b339114fdce9..c765aa9dd84587a2e184fdb6b7b4bee510ab52d7 100644 (file)
@@ -11,7 +11,6 @@ pub struct GetCommunity {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetCommunityResponse {
-  op: String,
   community: CommunityView,
   moderators: Vec<CommunityModeratorView>,
   admins: Vec<UserView>,
@@ -29,7 +28,6 @@ pub struct CreateCommunity {
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct CommunityResponse {
-  op: String,
   pub community: CommunityView,
 }
 
@@ -43,7 +41,6 @@ pub struct ListCommunities {
 
 #[derive(Serialize, Deserialize)]
 pub struct ListCommunitiesResponse {
-  op: String,
   communities: Vec<CommunityView>,
 }
 
@@ -59,7 +56,6 @@ pub struct BanFromCommunity {
 
 #[derive(Serialize, Deserialize)]
 pub struct BanFromCommunityResponse {
-  op: String,
   user: UserView,
   banned: bool,
 }
@@ -74,7 +70,6 @@ pub struct AddModToCommunity {
 
 #[derive(Serialize, Deserialize)]
 pub struct AddModToCommunityResponse {
-  op: String,
   moderators: Vec<CommunityModeratorView>,
 }
 
@@ -107,7 +102,6 @@ pub struct GetFollowedCommunities {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetFollowedCommunitiesResponse {
-  op: String,
   communities: Vec<CommunityFollowerView>,
 }
 
@@ -141,19 +135,19 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
           data.name.to_owned().unwrap_or_else(|| "main".to_string()),
         ) {
           Ok(community) => community.id,
-          Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
+          Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
         }
       }
     };
 
     let community_view = match CommunityView::read(&conn, community_id, user_id) {
       Ok(community) => community,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
     let moderators = match CommunityModeratorView::for_community(&conn, community_id) {
       Ok(moderators) => moderators,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
     let site_creator_id = Site::read(&conn, 1)?.creator_id;
@@ -164,7 +158,6 @@ impl Perform<GetCommunityResponse> for Oper<GetCommunity> {
 
     // Return the jwt
     Ok(GetCommunityResponse {
-      op: self.op.to_string(),
       community: community_view,
       moderators,
       admins,
@@ -178,21 +171,21 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     if has_slurs(&data.name)
       || has_slurs(&data.title)
       || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
     {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let user_id = claims.id;
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     // When you create a community, make sure the user becomes a moderator and a follower
@@ -210,7 +203,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
 
     let inserted_community = match Community::create(&conn, &community_form) {
       Ok(community) => community,
-      Err(_e) => return Err(APIError::err(&self.op, "community_already_exists").into()),
+      Err(_e) => return Err(APIError::err("community_already_exists").into()),
     };
 
     let community_moderator_form = CommunityModeratorForm {
@@ -221,9 +214,7 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
     let _inserted_community_moderator =
       match CommunityModerator::join(&conn, &community_moderator_form) {
         Ok(user) => user,
-        Err(_e) => {
-          return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
-        }
+        Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
       };
 
     let community_follower_form = CommunityFollowerForm {
@@ -234,13 +225,12 @@ impl Perform<CommunityResponse> for Oper<CreateCommunity> {
     let _inserted_community_follower =
       match CommunityFollower::follow(&conn, &community_follower_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
+        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
       };
 
     let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id))?;
 
     Ok(CommunityResponse {
-      op: self.op.to_string(),
       community: community_view,
     })
   }
@@ -251,19 +241,19 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
     let data: &EditCommunity = &self.data;
 
     if has_slurs(&data.name) || has_slurs(&data.title) {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     // Verify its a mod
@@ -276,7 +266,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
     );
     editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
     if !editors.contains(&user_id) {
-      return Err(APIError::err(&self.op, "no_community_edit_allowed").into());
+      return Err(APIError::err("no_community_edit_allowed").into());
     }
 
     let community_form = CommunityForm {
@@ -293,7 +283,7 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
 
     let _updated_community = match Community::update(&conn, data.edit_id, &community_form) {
       Ok(community) => community,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
     };
 
     // Mod tables
@@ -315,7 +305,6 @@ impl Perform<CommunityResponse> for Oper<EditCommunity> {
     let community_view = CommunityView::read(&conn, data.edit_id, Some(user_id))?;
 
     Ok(CommunityResponse {
-      op: self.op.to_string(),
       community: community_view,
     })
   }
@@ -354,10 +343,7 @@ impl Perform<ListCommunitiesResponse> for Oper<ListCommunities> {
       .list()?;
 
     // Return the jwt
-    Ok(ListCommunitiesResponse {
-      op: self.op.to_string(),
-      communities,
-    })
+    Ok(ListCommunitiesResponse { communities })
   }
 }
 
@@ -367,7 +353,7 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -380,19 +366,18 @@ impl Perform<CommunityResponse> for Oper<FollowCommunity> {
     if data.follow {
       match CommunityFollower::follow(&conn, &community_follower_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
+        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
       };
     } else {
       match CommunityFollower::ignore(&conn, &community_follower_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
+        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
       };
     }
 
     let community_view = CommunityView::read(&conn, data.community_id, Some(user_id))?;
 
     Ok(CommunityResponse {
-      op: self.op.to_string(),
       community: community_view,
     })
   }
@@ -404,7 +389,7 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -412,14 +397,11 @@ impl Perform<GetFollowedCommunitiesResponse> for Oper<GetFollowedCommunities> {
     let communities: Vec<CommunityFollowerView> =
       match CommunityFollowerView::for_user(&conn, user_id) {
         Ok(communities) => communities,
-        Err(_e) => return Err(APIError::err(&self.op, "system_err_login").into()),
+        Err(_e) => return Err(APIError::err("system_err_login").into()),
       };
 
     // Return the jwt
-    Ok(GetFollowedCommunitiesResponse {
-      op: self.op.to_string(),
-      communities,
-    })
+    Ok(GetFollowedCommunitiesResponse { communities })
   }
 }
 
@@ -429,7 +411,7 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -442,12 +424,12 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
     if data.ban {
       match CommunityUserBan::ban(&conn, &community_user_ban_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
+        Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
       };
     } else {
       match CommunityUserBan::unban(&conn, &community_user_ban_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_user_already_banned").into()),
+        Err(_e) => return Err(APIError::err("community_user_already_banned").into()),
       };
     }
 
@@ -470,7 +452,6 @@ impl Perform<BanFromCommunityResponse> for Oper<BanFromCommunity> {
     let user_view = UserView::read(&conn, data.user_id)?;
 
     Ok(BanFromCommunityResponse {
-      op: self.op.to_string(),
       user: user_view,
       banned: data.ban,
     })
@@ -483,7 +464,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -496,16 +477,12 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
     if data.added {
       match CommunityModerator::join(&conn, &community_moderator_form) {
         Ok(user) => user,
-        Err(_e) => {
-          return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
-        }
+        Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
       };
     } else {
       match CommunityModerator::leave(&conn, &community_moderator_form) {
         Ok(user) => user,
-        Err(_e) => {
-          return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
-        }
+        Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
       };
     }
 
@@ -520,10 +497,7 @@ impl Perform<AddModToCommunityResponse> for Oper<AddModToCommunity> {
 
     let moderators = CommunityModeratorView::for_community(&conn, data.community_id)?;
 
-    Ok(AddModToCommunityResponse {
-      op: self.op.to_string(),
-      moderators,
-    })
+    Ok(AddModToCommunityResponse { moderators })
   }
 }
 
@@ -533,7 +507,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -548,7 +522,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
 
     // Make sure user is the creator, or an admin
     if user_id != read_community.creator_id && !admins.iter().map(|a| a.id).any(|x| x == user_id) {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let community_form = CommunityForm {
@@ -565,7 +539,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
 
     let _updated_community = match Community::update(&conn, data.community_id, &community_form) {
       Ok(community) => community,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_community").into()),
     };
 
     // You also have to re-do the community_moderator table, reordering it.
@@ -588,9 +562,7 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
       let _inserted_community_moderator =
         match CommunityModerator::join(&conn, &community_moderator_form) {
           Ok(user) => user,
-          Err(_e) => {
-            return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
-          }
+          Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
         };
     }
 
@@ -605,17 +577,16 @@ impl Perform<GetCommunityResponse> for Oper<TransferCommunity> {
 
     let community_view = match CommunityView::read(&conn, data.community_id, Some(user_id)) {
       Ok(community) => community,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
     let moderators = match CommunityModeratorView::for_community(&conn, data.community_id) {
       Ok(moderators) => moderators,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_community").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_community").into()),
     };
 
     // Return the jwt
     Ok(GetCommunityResponse {
-      op: self.op.to_string(),
       community: community_view,
       moderators,
       admins,
index e35804476850de62cc00bb2648a8afdaf0bf155d..cb09d7fa0344e6ace36ec972d2bf46d812bf5071 100644 (file)
@@ -8,6 +8,8 @@ use crate::db::moderator_views::*;
 use crate::db::password_reset_request::*;
 use crate::db::post::*;
 use crate::db::post_view::*;
+use crate::db::private_message::*;
+use crate::db::private_message_view::*;
 use crate::db::site::*;
 use crate::db::site_view::*;
 use crate::db::user::*;
@@ -26,73 +28,27 @@ pub mod post;
 pub mod site;
 pub mod user;
 
-#[derive(EnumString, ToString, Debug)]
-pub enum UserOperation {
-  Login,
-  Register,
-  CreateCommunity,
-  CreatePost,
-  ListCommunities,
-  ListCategories,
-  GetPost,
-  GetCommunity,
-  CreateComment,
-  EditComment,
-  SaveComment,
-  CreateCommentLike,
-  GetPosts,
-  CreatePostLike,
-  EditPost,
-  SavePost,
-  EditCommunity,
-  FollowCommunity,
-  GetFollowedCommunities,
-  GetUserDetails,
-  GetReplies,
-  GetUserMentions,
-  EditUserMention,
-  GetModlog,
-  BanFromCommunity,
-  AddModToCommunity,
-  CreateSite,
-  EditSite,
-  GetSite,
-  AddAdmin,
-  BanUser,
-  Search,
-  MarkAllAsRead,
-  SaveUserSettings,
-  TransferCommunity,
-  TransferSite,
-  DeleteAccount,
-  PasswordReset,
-  PasswordChange,
-}
-
 #[derive(Fail, Debug)]
-#[fail(display = "{{\"op\":\"{}\", \"error\":\"{}\"}}", op, message)]
+#[fail(display = "{{\"error\":\"{}\"}}", message)]
 pub struct APIError {
-  pub op: String,
   pub message: String,
 }
 
 impl APIError {
-  pub fn err(op: &UserOperation, msg: &str) -> Self {
+  pub fn err(msg: &str) -> Self {
     APIError {
-      op: op.to_string(),
       message: msg.to_string(),
     }
   }
 }
 
 pub struct Oper<T> {
-  op: UserOperation,
   data: T,
 }
 
 impl<T> Oper<T> {
-  pub fn new(op: UserOperation, data: T) -> Oper<T> {
-    Oper { op, data }
+  pub fn new(data: T) -> Oper<T> {
+    Oper { data }
   }
 }
 
index b0fcdd0c1b9f24023b106d1395232462d79ecd58..3f211453c9aec9ccb4a2ed3f97e3262237efadc6 100644 (file)
@@ -14,7 +14,6 @@ pub struct CreatePost {
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct PostResponse {
-  op: String,
   pub post: PostView,
 }
 
@@ -26,7 +25,6 @@ pub struct GetPost {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetPostResponse {
-  op: String,
   post: PostView,
   comments: Vec<CommentView>,
   community: CommunityView,
@@ -46,7 +44,6 @@ pub struct GetPosts {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetPostsResponse {
-  op: String,
   posts: Vec<PostView>,
 }
 
@@ -59,7 +56,6 @@ pub struct CreatePostLike {
 
 #[derive(Serialize, Deserialize)]
 pub struct CreatePostLikeResponse {
-  op: String,
   post: PostView,
 }
 
@@ -93,23 +89,23 @@ impl Perform<PostResponse> for Oper<CreatePost> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let user_id = claims.id;
 
     // Check for a community ban
     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "community_ban").into());
+      return Err(APIError::err("community_ban").into());
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     let post_form = PostForm {
@@ -128,7 +124,7 @@ impl Perform<PostResponse> for Oper<CreatePost> {
 
     let inserted_post = match Post::create(&conn, &post_form) {
       Ok(post) => post,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_create_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_create_post").into()),
     };
 
     // They like their own post by default
@@ -141,19 +137,16 @@ impl Perform<PostResponse> for Oper<CreatePost> {
     // Only add the like if the score isnt 0
     let _inserted_like = match PostLike::like(&conn, &like_form) {
       Ok(like) => like,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
     };
 
     // Refetch the view
     let post_view = match PostView::read(&conn, inserted_post.id, Some(user_id)) {
       Ok(post) => post,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
     };
 
-    Ok(PostResponse {
-      op: self.op.to_string(),
-      post: post_view,
-    })
+    Ok(PostResponse { post: post_view })
   }
 }
 
@@ -174,7 +167,7 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
 
     let post_view = match PostView::read(&conn, data.id, user_id) {
       Ok(post) => post,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
     };
 
     let comments = CommentQueryBuilder::create(&conn)
@@ -195,7 +188,6 @@ impl Perform<GetPostResponse> for Oper<GetPost> {
 
     // Return the jwt
     Ok(GetPostResponse {
-      op: self.op.to_string(),
       post: post_view,
       comments,
       community,
@@ -241,13 +233,10 @@ impl Perform<GetPostsResponse> for Oper<GetPosts> {
       .list()
     {
       Ok(posts) => posts,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_get_posts").into()),
+      Err(_e) => return Err(APIError::err("couldnt_get_posts").into()),
     };
 
-    Ok(GetPostsResponse {
-      op: self.op.to_string(),
-      posts,
-    })
+    Ok(GetPostsResponse { posts })
   }
 }
 
@@ -257,7 +246,7 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -266,19 +255,19 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
     if data.score == -1 {
       let site = SiteView::read(&conn)?;
       if !site.enable_downvotes {
-        return Err(APIError::err(&self.op, "downvotes_disabled").into());
+        return Err(APIError::err("downvotes_disabled").into());
       }
     }
 
     // Check for a community ban
     let post = Post::read(&conn, data.post_id)?;
     if CommunityUserBanView::get(&conn, user_id, post.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "community_ban").into());
+      return Err(APIError::err("community_ban").into());
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     let like_form = PostLikeForm {
@@ -295,20 +284,17 @@ impl Perform<CreatePostLikeResponse> for Oper<CreatePostLike> {
     if do_add {
       let _inserted_like = match PostLike::like(&conn, &like_form) {
         Ok(like) => like,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_like_post").into()),
+        Err(_e) => return Err(APIError::err("couldnt_like_post").into()),
       };
     }
 
     let post_view = match PostView::read(&conn, data.post_id, Some(user_id)) {
       Ok(post) => post,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_post").into()),
     };
 
     // just output the score
-    Ok(CreatePostLikeResponse {
-      op: self.op.to_string(),
-      post: post_view,
-    })
+    Ok(CreatePostLikeResponse { post: post_view })
   }
 }
 
@@ -316,12 +302,12 @@ impl Perform<PostResponse> for Oper<EditPost> {
   fn perform(&self, conn: &PgConnection) -> Result<PostResponse, Error> {
     let data: &EditPost = &self.data;
     if has_slurs(&data.name) || (data.body.is_some() && has_slurs(&data.body.to_owned().unwrap())) {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -336,17 +322,17 @@ impl Perform<PostResponse> for Oper<EditPost> {
     );
     editors.append(&mut UserView::admins(&conn)?.into_iter().map(|a| a.id).collect());
     if !editors.contains(&user_id) {
-      return Err(APIError::err(&self.op, "no_post_edit_allowed").into());
+      return Err(APIError::err("no_post_edit_allowed").into());
     }
 
     // Check for a community ban
     if CommunityUserBanView::get(&conn, user_id, data.community_id).is_ok() {
-      return Err(APIError::err(&self.op, "community_ban").into());
+      return Err(APIError::err("community_ban").into());
     }
 
     // Check for a site ban
     if UserView::read(&conn, user_id)?.banned {
-      return Err(APIError::err(&self.op, "site_ban").into());
+      return Err(APIError::err("site_ban").into());
     }
 
     let post_form = PostForm {
@@ -365,7 +351,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
 
     let _updated_post = match Post::update(&conn, data.edit_id, &post_form) {
       Ok(post) => post,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
     };
 
     // Mod tables
@@ -399,10 +385,7 @@ impl Perform<PostResponse> for Oper<EditPost> {
 
     let post_view = PostView::read(&conn, data.edit_id, Some(user_id))?;
 
-    Ok(PostResponse {
-      op: self.op.to_string(),
-      post: post_view,
-    })
+    Ok(PostResponse { post: post_view })
   }
 }
 
@@ -412,7 +395,7 @@ impl Perform<PostResponse> for Oper<SavePost> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -425,20 +408,17 @@ impl Perform<PostResponse> for Oper<SavePost> {
     if data.save {
       match PostSaved::save(&conn, &post_saved_form) {
         Ok(post) => post,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
+        Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
       };
     } else {
       match PostSaved::unsave(&conn, &post_saved_form) {
         Ok(post) => post,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_save_post").into()),
+        Err(_e) => return Err(APIError::err("couldnt_save_post").into()),
       };
     }
 
     let post_view = PostView::read(&conn, data.post_id, Some(user_id))?;
 
-    Ok(PostResponse {
-      op: self.op.to_string(),
-      post: post_view,
-    })
+    Ok(PostResponse { post: post_view })
   }
 }
index ce07724a38e862d0c00b6f528cf4bc0e15c5016f..a5faf34dd820a02524777418228da69d9c6e1a89 100644 (file)
@@ -7,7 +7,6 @@ pub struct ListCategories;
 
 #[derive(Serialize, Deserialize)]
 pub struct ListCategoriesResponse {
-  op: String,
   categories: Vec<Category>,
 }
 
@@ -24,7 +23,6 @@ pub struct Search {
 
 #[derive(Serialize, Deserialize)]
 pub struct SearchResponse {
-  op: String,
   type_: String,
   comments: Vec<CommentView>,
   posts: Vec<PostView>,
@@ -42,7 +40,6 @@ pub struct GetModlog {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetModlogResponse {
-  op: String,
   removed_posts: Vec<ModRemovePostView>,
   locked_posts: Vec<ModLockPostView>,
   stickied_posts: Vec<ModStickyPostView>,
@@ -79,13 +76,11 @@ pub struct GetSite;
 
 #[derive(Serialize, Deserialize)]
 pub struct SiteResponse {
-  op: String,
   site: SiteView,
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct GetSiteResponse {
-  op: String,
   site: Option<SiteView>,
   admins: Vec<UserView>,
   banned: Vec<UserView>,
@@ -105,10 +100,7 @@ impl Perform<ListCategoriesResponse> for Oper<ListCategories> {
     let categories: Vec<Category> = Category::list_all(&conn)?;
 
     // Return the jwt
-    Ok(ListCategoriesResponse {
-      op: self.op.to_string(),
-      categories,
-    })
+    Ok(ListCategoriesResponse { categories })
   }
 }
 
@@ -172,7 +164,6 @@ impl Perform<GetModlogResponse> for Oper<GetModlog> {
 
     // Return the jwt
     Ok(GetModlogResponse {
-      op: self.op.to_string(),
       removed_posts,
       locked_posts,
       stickied_posts,
@@ -192,20 +183,20 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     if has_slurs(&data.name)
       || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
     {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if !UserView::read(&conn, user_id)?.admin {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let site_form = SiteForm {
@@ -220,15 +211,12 @@ impl Perform<SiteResponse> for Oper<CreateSite> {
 
     match Site::create(&conn, &site_form) {
       Ok(site) => site,
-      Err(_e) => return Err(APIError::err(&self.op, "site_already_exists").into()),
+      Err(_e) => return Err(APIError::err("site_already_exists").into()),
     };
 
     let site_view = SiteView::read(&conn)?;
 
-    Ok(SiteResponse {
-      op: self.op.to_string(),
-      site: site_view,
-    })
+    Ok(SiteResponse { site: site_view })
   }
 }
 
@@ -238,20 +226,20 @@ impl Perform<SiteResponse> for Oper<EditSite> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     if has_slurs(&data.name)
       || (data.description.is_some() && has_slurs(&data.description.to_owned().unwrap()))
     {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if !UserView::read(&conn, user_id)?.admin {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let found_site = Site::read(&conn, 1)?;
@@ -268,15 +256,12 @@ impl Perform<SiteResponse> for Oper<EditSite> {
 
     match Site::update(&conn, 1, &site_form) {
       Ok(site) => site,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
     };
 
     let site_view = SiteView::read(&conn)?;
 
-    Ok(SiteResponse {
-      op: self.op.to_string(),
-      site: site_view,
-    })
+    Ok(SiteResponse { site: site_view })
   }
 }
 
@@ -301,7 +286,6 @@ impl Perform<GetSiteResponse> for Oper<GetSite> {
     let banned = UserView::banned(&conn)?;
 
     Ok(GetSiteResponse {
-      op: self.op.to_string(),
       site: site_view,
       admins,
       banned,
@@ -419,7 +403,6 @@ impl Perform<SearchResponse> for Oper<Search> {
 
     // Return the jwt
     Ok(SearchResponse {
-      op: self.op.to_string(),
       type_: data.type_.to_owned(),
       comments,
       posts,
@@ -435,7 +418,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -444,7 +427,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
 
     // Make sure user is the creator
     if read_site.creator_id != user_id {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let site_form = SiteForm {
@@ -459,7 +442,7 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
 
     match Site::update(&conn, 1, &site_form) {
       Ok(site) => site,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_site").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_site").into()),
     };
 
     // Mod tables
@@ -484,7 +467,6 @@ impl Perform<GetSiteResponse> for Oper<TransferSite> {
     let banned = UserView::banned(&conn)?;
 
     Ok(GetSiteResponse {
-      op: self.op.to_string(),
       site: Some(site_view),
       admins,
       banned,
index ac700acad53f73bdd4affbd9fd39dfe857a7fede..8d2db104cd2caaa44fe10f2266e73425d852b4e5 100644 (file)
@@ -30,6 +30,7 @@ pub struct SaveUserSettings {
   lang: String,
   avatar: Option<String>,
   email: Option<String>,
+  matrix_user_id: Option<String>,
   new_password: Option<String>,
   new_password_verify: Option<String>,
   old_password: Option<String>,
@@ -40,7 +41,6 @@ pub struct SaveUserSettings {
 
 #[derive(Serialize, Deserialize)]
 pub struct LoginResponse {
-  op: String,
   jwt: String,
 }
 
@@ -58,7 +58,6 @@ pub struct GetUserDetails {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetUserDetailsResponse {
-  op: String,
   user: UserView,
   follows: Vec<CommunityFollowerView>,
   moderates: Vec<CommunityModeratorView>,
@@ -69,13 +68,11 @@ pub struct GetUserDetailsResponse {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetRepliesResponse {
-  op: String,
   replies: Vec<ReplyView>,
 }
 
 #[derive(Serialize, Deserialize)]
 pub struct GetUserMentionsResponse {
-  op: String,
   mentions: Vec<UserMentionView>,
 }
 
@@ -93,7 +90,6 @@ pub struct AddAdmin {
 
 #[derive(Serialize, Deserialize)]
 pub struct AddAdminResponse {
-  op: String,
   admins: Vec<UserView>,
 }
 
@@ -108,7 +104,6 @@ pub struct BanUser {
 
 #[derive(Serialize, Deserialize)]
 pub struct BanUserResponse {
-  op: String,
   user: UserView,
   banned: bool,
 }
@@ -140,7 +135,6 @@ pub struct EditUserMention {
 
 #[derive(Serialize, Deserialize, Clone)]
 pub struct UserMentionResponse {
-  op: String,
   mention: UserMentionView,
 }
 
@@ -156,9 +150,7 @@ pub struct PasswordReset {
 }
 
 #[derive(Serialize, Deserialize, Clone)]
-pub struct PasswordResetResponse {
-  op: String,
-}
+pub struct PasswordResetResponse {}
 
 #[derive(Serialize, Deserialize)]
 pub struct PasswordChange {
@@ -167,6 +159,40 @@ pub struct PasswordChange {
   password_verify: String,
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct CreatePrivateMessage {
+  content: String,
+  recipient_id: i32,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct EditPrivateMessage {
+  edit_id: i32,
+  content: Option<String>,
+  deleted: Option<bool>,
+  read: Option<bool>,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetPrivateMessages {
+  unread_only: bool,
+  page: Option<i64>,
+  limit: Option<i64>,
+  auth: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct PrivateMessagesResponse {
+  messages: Vec<PrivateMessageView>,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+pub struct PrivateMessageResponse {
+  message: PrivateMessageView,
+}
+
 impl Perform<LoginResponse> for Oper<Login> {
   fn perform(&self, conn: &PgConnection) -> Result<LoginResponse, Error> {
     let data: &Login = &self.data;
@@ -174,20 +200,17 @@ impl Perform<LoginResponse> for Oper<Login> {
     // Fetch that username / email
     let user: User_ = match User_::find_by_email_or_username(&conn, &data.username_or_email) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
     };
 
     // Verify the password
     let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
     if !valid {
-      return Err(APIError::err(&self.op, "password_incorrect").into());
+      return Err(APIError::err("password_incorrect").into());
     }
 
     // Return the jwt
-    Ok(LoginResponse {
-      op: self.op.to_string(),
-      jwt: user.jwt(),
-    })
+    Ok(LoginResponse { jwt: user.jwt() })
   }
 }
 
@@ -198,22 +221,22 @@ impl Perform<LoginResponse> for Oper<Register> {
     // Make sure site has open registration
     if let Ok(site) = SiteView::read(&conn) {
       if !site.open_registration {
-        return Err(APIError::err(&self.op, "registration_closed").into());
+        return Err(APIError::err("registration_closed").into());
       }
     }
 
     // Make sure passwords match
     if data.password != data.password_verify {
-      return Err(APIError::err(&self.op, "passwords_dont_match").into());
+      return Err(APIError::err("passwords_dont_match").into());
     }
 
     if has_slurs(&data.username) {
-      return Err(APIError::err(&self.op, "no_slurs").into());
+      return Err(APIError::err("no_slurs").into());
     }
 
     // Make sure there are no admins
     if data.admin && !UserView::admins(&conn)?.is_empty() {
-      return Err(APIError::err(&self.op, "admin_already_created").into());
+      return Err(APIError::err("admin_already_created").into());
     }
 
     // Register the new user
@@ -221,6 +244,7 @@ impl Perform<LoginResponse> for Oper<Register> {
       name: data.username.to_owned(),
       fedi_name: Settings::get().hostname.to_owned(),
       email: data.email.to_owned(),
+      matrix_user_id: None,
       avatar: None,
       password_encrypted: data.password.to_owned(),
       preferred_username: None,
@@ -248,7 +272,7 @@ impl Perform<LoginResponse> for Oper<Register> {
           "user_already_exists"
         };
 
-        return Err(APIError::err(&self.op, err_type).into());
+        return Err(APIError::err(err_type).into());
       }
     };
 
@@ -280,7 +304,7 @@ impl Perform<LoginResponse> for Oper<Register> {
     let _inserted_community_follower =
       match CommunityFollower::follow(&conn, &community_follower_form) {
         Ok(user) => user,
-        Err(_e) => return Err(APIError::err(&self.op, "community_follower_already_exists").into()),
+        Err(_e) => return Err(APIError::err("community_follower_already_exists").into()),
       };
 
     // If its an admin, add them as a mod and follower to main
@@ -293,15 +317,12 @@ impl Perform<LoginResponse> for Oper<Register> {
       let _inserted_community_moderator =
         match CommunityModerator::join(&conn, &community_moderator_form) {
           Ok(user) => user,
-          Err(_e) => {
-            return Err(APIError::err(&self.op, "community_moderator_already_exists").into())
-          }
+          Err(_e) => return Err(APIError::err("community_moderator_already_exists").into()),
         };
     }
 
     // Return the jwt
     Ok(LoginResponse {
-      op: self.op.to_string(),
       jwt: inserted_user.jwt(),
     })
   }
@@ -313,7 +334,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -331,7 +352,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
           Some(new_password_verify) => {
             // Make sure passwords match
             if new_password != new_password_verify {
-              return Err(APIError::err(&self.op, "passwords_dont_match").into());
+              return Err(APIError::err("passwords_dont_match").into());
             }
 
             // Check the old password
@@ -340,14 +361,14 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
                 let valid: bool =
                   verify(old_password, &read_user.password_encrypted).unwrap_or(false);
                 if !valid {
-                  return Err(APIError::err(&self.op, "password_incorrect").into());
+                  return Err(APIError::err("password_incorrect").into());
                 }
                 User_::update_password(&conn, user_id, &new_password)?.password_encrypted
               }
-              None => return Err(APIError::err(&self.op, "password_incorrect").into()),
+              None => return Err(APIError::err("password_incorrect").into()),
             }
           }
-          None => return Err(APIError::err(&self.op, "passwords_dont_match").into()),
+          None => return Err(APIError::err("passwords_dont_match").into()),
         }
       }
       None => read_user.password_encrypted,
@@ -357,6 +378,7 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
       name: read_user.name,
       fedi_name: read_user.fedi_name,
       email,
+      matrix_user_id: data.matrix_user_id.to_owned(),
       avatar: data.avatar.to_owned(),
       password_encrypted,
       preferred_username: read_user.preferred_username,
@@ -383,13 +405,12 @@ impl Perform<LoginResponse> for Oper<SaveUserSettings> {
           "user_already_exists"
         };
 
-        return Err(APIError::err(&self.op, err_type).into());
+        return Err(APIError::err(err_type).into());
       }
     };
 
     // Return the jwt
     Ok(LoginResponse {
-      op: self.op.to_string(),
       jwt: updated_user.jwt(),
     })
   }
@@ -430,9 +451,7 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
             .unwrap_or_else(|| "admin".to_string()),
         ) {
           Ok(user) => user.id,
-          Err(_e) => {
-            return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into())
-          }
+          Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
         }
       }
     };
@@ -475,7 +494,6 @@ impl Perform<GetUserDetailsResponse> for Oper<GetUserDetails> {
 
     // Return the jwt
     Ok(GetUserDetailsResponse {
-      op: self.op.to_string(),
       user: user_view,
       follows,
       moderates,
@@ -492,22 +510,24 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if !UserView::read(&conn, user_id)?.admin {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let read_user = User_::read(&conn, data.user_id)?;
 
+    // TODO make addadmin easier
     let user_form = UserForm {
       name: read_user.name,
       fedi_name: read_user.fedi_name,
       email: read_user.email,
+      matrix_user_id: read_user.matrix_user_id,
       avatar: read_user.avatar,
       password_encrypted: read_user.password_encrypted,
       preferred_username: read_user.preferred_username,
@@ -525,7 +545,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
 
     match User_::update(&conn, data.user_id, &user_form) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
 
     // Mod tables
@@ -543,10 +563,7 @@ impl Perform<AddAdminResponse> for Oper<AddAdmin> {
     let creator_user = admins.remove(creator_index);
     admins.insert(0, creator_user);
 
-    Ok(AddAdminResponse {
-      op: self.op.to_string(),
-      admins,
-    })
+    Ok(AddAdminResponse { admins })
   }
 }
 
@@ -556,22 +573,24 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
 
     // Make sure user is an admin
     if !UserView::read(&conn, user_id)?.admin {
-      return Err(APIError::err(&self.op, "not_an_admin").into());
+      return Err(APIError::err("not_an_admin").into());
     }
 
     let read_user = User_::read(&conn, data.user_id)?;
 
+    // TODO make bans and addadmins easier
     let user_form = UserForm {
       name: read_user.name,
       fedi_name: read_user.fedi_name,
       email: read_user.email,
+      matrix_user_id: read_user.matrix_user_id,
       avatar: read_user.avatar,
       password_encrypted: read_user.password_encrypted,
       preferred_username: read_user.preferred_username,
@@ -589,7 +608,7 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
 
     match User_::update(&conn, data.user_id, &user_form) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
 
     // Mod tables
@@ -611,7 +630,6 @@ impl Perform<BanUserResponse> for Oper<BanUser> {
     let user_view = UserView::read(&conn, data.user_id)?;
 
     Ok(BanUserResponse {
-      op: self.op.to_string(),
       user: user_view,
       banned: data.ban,
     })
@@ -624,7 +642,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -638,10 +656,7 @@ impl Perform<GetRepliesResponse> for Oper<GetReplies> {
       .limit(data.limit)
       .list()?;
 
-    Ok(GetRepliesResponse {
-      op: self.op.to_string(),
-      replies,
-    })
+    Ok(GetRepliesResponse { replies })
   }
 }
 
@@ -651,7 +666,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -665,10 +680,7 @@ impl Perform<GetUserMentionsResponse> for Oper<GetUserMentions> {
       .limit(data.limit)
       .list()?;
 
-    Ok(GetUserMentionsResponse {
-      op: self.op.to_string(),
-      mentions,
-    })
+    Ok(GetUserMentionsResponse { mentions })
   }
 }
 
@@ -678,7 +690,7 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -694,13 +706,12 @@ impl Perform<UserMentionResponse> for Oper<EditUserMention> {
     let _updated_user_mention =
       match UserMention::update(&conn, user_mention.id, &user_mention_form) {
         Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
 
     let user_mention_view = UserMentionView::read(&conn, user_mention.id, user_id)?;
 
     Ok(UserMentionResponse {
-      op: self.op.to_string(),
       mention: user_mention_view,
     })
   }
@@ -712,7 +723,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -737,7 +748,7 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
 
       let _updated_comment = match Comment::update(&conn, reply.id, &comment_form) {
         Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
     }
 
@@ -758,14 +769,35 @@ impl Perform<GetRepliesResponse> for Oper<MarkAllAsRead> {
       let _updated_mention =
         match UserMention::update(&conn, mention.user_mention_id, &mention_form) {
           Ok(mention) => mention,
-          Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
+          Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
         };
     }
 
-    Ok(GetRepliesResponse {
-      op: self.op.to_string(),
-      replies: vec![],
-    })
+    // messages
+    let messages = PrivateMessageQueryBuilder::create(&conn, user_id)
+      .page(1)
+      .limit(999)
+      .unread_only(true)
+      .list()?;
+
+    for message in &messages {
+      let private_message_form = PrivateMessageForm {
+        content: None,
+        creator_id: message.to_owned().creator_id,
+        recipient_id: message.to_owned().recipient_id,
+        deleted: None,
+        read: Some(true),
+        updated: None,
+      };
+
+      let _updated_message = match PrivateMessage::update(&conn, message.id, &private_message_form)
+      {
+        Ok(message) => message,
+        Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
+      };
+    }
+
+    Ok(GetRepliesResponse { replies: vec![] })
   }
 }
 
@@ -775,7 +807,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
 
     let claims = match Claims::decode(&data.auth) {
       Ok(claims) => claims.claims,
-      Err(_e) => return Err(APIError::err(&self.op, "not_logged_in").into()),
+      Err(_e) => return Err(APIError::err("not_logged_in").into()),
     };
 
     let user_id = claims.id;
@@ -785,7 +817,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
     // Verify the password
     let valid: bool = verify(&data.password, &user.password_encrypted).unwrap_or(false);
     if !valid {
-      return Err(APIError::err(&self.op, "password_incorrect").into());
+      return Err(APIError::err("password_incorrect").into());
     }
 
     // Comments
@@ -808,7 +840,7 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
 
       let _updated_comment = match Comment::update(&conn, comment.id, &comment_form) {
         Ok(comment) => comment,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_comment").into()),
+        Err(_e) => return Err(APIError::err("couldnt_update_comment").into()),
       };
     }
 
@@ -836,12 +868,11 @@ impl Perform<LoginResponse> for Oper<DeleteAccount> {
 
       let _updated_post = match Post::update(&conn, post.id, &post_form) {
         Ok(post) => post,
-        Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_post").into()),
+        Err(_e) => return Err(APIError::err("couldnt_update_post").into()),
       };
     }
 
     Ok(LoginResponse {
-      op: self.op.to_string(),
       jwt: data.auth.to_owned(),
     })
   }
@@ -854,7 +885,7 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
     // Fetch that email
     let user: User_ = match User_::find_by_email(&conn, &data.email) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_find_that_username_or_email").into()),
+      Err(_e) => return Err(APIError::err("couldnt_find_that_username_or_email").into()),
     };
 
     // Generate a random token
@@ -871,12 +902,10 @@ impl Perform<PasswordResetResponse> for Oper<PasswordReset> {
     let html = &format!("<h1>Password Reset Request for {}</h1><br><a href={}/password_change/{}>Click here to reset your password</a>", user.name, hostname, &token);
     match send_email(subject, user_email, &user.name, html) {
       Ok(_o) => _o,
-      Err(_e) => return Err(APIError::err(&self.op, &_e).into()),
+      Err(_e) => return Err(APIError::err(&_e).into()),
     };
 
-    Ok(PasswordResetResponse {
-      op: self.op.to_string(),
-    })
+    Ok(PasswordResetResponse {})
   }
 }
 
@@ -889,19 +918,156 @@ impl Perform<LoginResponse> for Oper<PasswordChange> {
 
     // Make sure passwords match
     if data.password != data.password_verify {
-      return Err(APIError::err(&self.op, "passwords_dont_match").into());
+      return Err(APIError::err("passwords_dont_match").into());
     }
 
     // Update the user with the new password
     let updated_user = match User_::update_password(&conn, user_id, &data.password) {
       Ok(user) => user,
-      Err(_e) => return Err(APIError::err(&self.op, "couldnt_update_user").into()),
+      Err(_e) => return Err(APIError::err("couldnt_update_user").into()),
     };
 
     // Return the jwt
     Ok(LoginResponse {
-      op: self.op.to_string(),
       jwt: updated_user.jwt(),
     })
   }
 }
+
+impl Perform<PrivateMessageResponse> for Oper<CreatePrivateMessage> {
+  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
+    let data: &CreatePrivateMessage = &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 hostname = &format!("https://{}", Settings::get().hostname);
+
+    // Check for a site ban
+    if UserView::read(&conn, user_id)?.banned {
+      return Err(APIError::err("site_ban").into());
+    }
+
+    let content_slurs_removed = remove_slurs(&data.content.to_owned());
+
+    let private_message_form = PrivateMessageForm {
+      content: Some(content_slurs_removed.to_owned()),
+      creator_id: user_id,
+      recipient_id: data.recipient_id,
+      deleted: None,
+      read: None,
+      updated: None,
+    };
+
+    let inserted_private_message = match PrivateMessage::create(&conn, &private_message_form) {
+      Ok(private_message) => private_message,
+      Err(_e) => {
+        return Err(APIError::err("couldnt_create_private_message").into());
+      }
+    };
+
+    // Send notifications to the recipient
+    let recipient_user = User_::read(&conn, data.recipient_id)?;
+    if recipient_user.send_notifications_to_email {
+      if let Some(email) = recipient_user.email {
+        let subject = &format!(
+          "{} - Private Message from {}",
+          Settings::get().hostname,
+          claims.username
+        );
+        let html = &format!(
+          "<h1>Private Message</h1><br><div>{} - {}</div><br><a href={}/inbox>inbox</a>",
+          claims.username, &content_slurs_removed, hostname
+        );
+        match send_email(subject, &email, &recipient_user.name, html) {
+          Ok(_o) => _o,
+          Err(e) => eprintln!("{}", e),
+        };
+      }
+    }
+
+    let message = PrivateMessageView::read(&conn, inserted_private_message.id)?;
+
+    Ok(PrivateMessageResponse { message })
+  }
+}
+
+impl Perform<PrivateMessageResponse> for Oper<EditPrivateMessage> {
+  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessageResponse, Error> {
+    let data: &EditPrivateMessage = &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 orig_private_message = PrivateMessage::read(&conn, data.edit_id)?;
+
+    // Check for a site ban
+    if UserView::read(&conn, user_id)?.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))
+    {
+      return Err(APIError::err("no_private_message_edit_allowed").into());
+    }
+
+    let content_slurs_removed = match &data.content {
+      Some(content) => Some(remove_slurs(content)),
+      None => None,
+    };
+
+    let private_message_form = 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: data.read.to_owned(),
+      updated: if data.read.is_some() {
+        orig_private_message.updated
+      } else {
+        Some(naive_now())
+      },
+    };
+
+    let _updated_private_message =
+      match PrivateMessage::update(&conn, data.edit_id, &private_message_form) {
+        Ok(private_message) => private_message,
+        Err(_e) => return Err(APIError::err("couldnt_update_private_message").into()),
+      };
+
+    let message = PrivateMessageView::read(&conn, data.edit_id)?;
+
+    Ok(PrivateMessageResponse { message })
+  }
+}
+
+impl Perform<PrivateMessagesResponse> for Oper<GetPrivateMessages> {
+  fn perform(&self, conn: &PgConnection) -> Result<PrivateMessagesResponse, Error> {
+    let data: &GetPrivateMessages = &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 messages = PrivateMessageQueryBuilder::create(&conn, user_id)
+      .page(data.page)
+      .limit(data.limit)
+      .unread_only(data.unread_only)
+      .list()?;
+
+    Ok(PrivateMessagesResponse { messages })
+  }
+}
index 2d2e5ad301c8078f972bd5e4d67e320288579795..c5a0b2f029868155d5a36f1ce28f2eb390d26bdd 100644 (file)
@@ -22,6 +22,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "here".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       published: naive_now(),
       admin: false,
index a9c7d81ddc04ad58af9401e1c678b41f0c289900..efba07a5186705700c8762acb749403d8b5fec19 100644 (file)
@@ -174,6 +174,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index 3b06e8e347f387636bc392f6dc156f5331a136c0..d4a65c9a9703508ee42848f0734fed1cf0871d74 100644 (file)
@@ -398,6 +398,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index b482ca4a91d5f78df33086688dc32c1cdc9bdf13..6350096358bd7a8f16092a0f35db1cedb90bfb53 100644 (file)
@@ -220,6 +220,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index fef3ffce5df79b6559bebc8df9098c142e9d9b3f..dacdb6f6a92bc2b8d72ee5366a3e1a026881cbaf 100644 (file)
@@ -15,6 +15,8 @@ pub mod moderator_views;
 pub mod password_reset_request;
 pub mod post;
 pub mod post_view;
+pub mod private_message;
+pub mod private_message_view;
 pub mod site;
 pub mod site_view;
 pub mod user;
index 3c6233cb99d43a27c79f82491feb5df127ffb356..4fd532afdbe663fdb62cb62b777acc83ee9156ec 100644 (file)
@@ -442,6 +442,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
@@ -463,6 +464,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index fa060a591bfc6feb27f04787ae850b9ed75b2f60..6951fd39936912c789866bd53d12c13c2554aa6f 100644 (file)
@@ -92,6 +92,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index d3fba4dad0ff7e009c523f0478d16dbce194baa8..9e7a43410bd3ff61b4ce26c23acdb7a2d9a2cef6 100644 (file)
@@ -187,6 +187,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index f6cc274f0890bedae1b65b38c5dc8ede72ef9664..c80d16967202fc98a8faff9dbd49edc757365c69 100644 (file)
@@ -339,6 +339,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       updated: None,
       admin: false,
diff --git a/server/src/db/private_message.rs b/server/src/db/private_message.rs
new file mode 100644 (file)
index 0000000..cc073b5
--- /dev/null
@@ -0,0 +1,144 @@
+use super::*;
+use crate::schema::private_message;
+
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize)]
+#[table_name = "private_message"]
+pub struct PrivateMessage {
+  pub id: i32,
+  pub creator_id: i32,
+  pub recipient_id: i32,
+  pub content: String,
+  pub deleted: bool,
+  pub read: bool,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+#[derive(Insertable, AsChangeset, Clone)]
+#[table_name = "private_message"]
+pub struct PrivateMessageForm {
+  pub creator_id: i32,
+  pub recipient_id: i32,
+  pub content: Option<String>,
+  pub deleted: Option<bool>,
+  pub read: Option<bool>,
+  pub updated: Option<chrono::NaiveDateTime>,
+}
+
+impl Crud<PrivateMessageForm> for PrivateMessage {
+  fn read(conn: &PgConnection, private_message_id: i32) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    private_message.find(private_message_id).first::<Self>(conn)
+  }
+
+  fn delete(conn: &PgConnection, private_message_id: i32) -> Result<usize, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::delete(private_message.find(private_message_id)).execute(conn)
+  }
+
+  fn create(conn: &PgConnection, private_message_form: &PrivateMessageForm) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    insert_into(private_message)
+      .values(private_message_form)
+      .get_result::<Self>(conn)
+  }
+
+  fn update(
+    conn: &PgConnection,
+    private_message_id: i32,
+    private_message_form: &PrivateMessageForm,
+  ) -> Result<Self, Error> {
+    use crate::schema::private_message::dsl::*;
+    diesel::update(private_message.find(private_message_id))
+      .set(private_message_form)
+      .get_result::<Self>(conn)
+  }
+}
+
+#[cfg(test)]
+mod tests {
+  use super::super::user::*;
+  use super::*;
+  #[test]
+  fn test_crud() {
+    let conn = establish_unpooled_connection();
+
+    let creator_form = UserForm {
+      name: "creator_pm".into(),
+      fedi_name: "rrf".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      matrix_user_id: None,
+      avatar: None,
+      admin: false,
+      banned: false,
+      updated: None,
+      show_nsfw: false,
+      theme: "darkly".into(),
+      default_sort_type: SortType::Hot as i16,
+      default_listing_type: ListingType::Subscribed as i16,
+      lang: "browser".into(),
+      show_avatars: true,
+      send_notifications_to_email: false,
+    };
+
+    let inserted_creator = User_::create(&conn, &creator_form).unwrap();
+
+    let recipient_form = UserForm {
+      name: "recipient_pm".into(),
+      fedi_name: "rrf".into(),
+      preferred_username: None,
+      password_encrypted: "nope".into(),
+      email: None,
+      matrix_user_id: None,
+      avatar: None,
+      admin: false,
+      banned: false,
+      updated: None,
+      show_nsfw: false,
+      theme: "darkly".into(),
+      default_sort_type: SortType::Hot as i16,
+      default_listing_type: ListingType::Subscribed as i16,
+      lang: "browser".into(),
+      show_avatars: true,
+      send_notifications_to_email: false,
+    };
+
+    let inserted_recipient = User_::create(&conn, &recipient_form).unwrap();
+
+    let private_message_form = PrivateMessageForm {
+      content: Some("A test private message".into()),
+      creator_id: inserted_creator.id,
+      recipient_id: inserted_recipient.id,
+      deleted: None,
+      read: None,
+      updated: None,
+    };
+
+    let inserted_private_message = PrivateMessage::create(&conn, &private_message_form).unwrap();
+
+    let expected_private_message = PrivateMessage {
+      id: inserted_private_message.id,
+      content: "A test private message".into(),
+      creator_id: inserted_creator.id,
+      recipient_id: inserted_recipient.id,
+      deleted: false,
+      read: false,
+      updated: None,
+      published: inserted_private_message.published,
+    };
+
+    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 num_deleted = PrivateMessage::delete(&conn, inserted_private_message.id).unwrap();
+    User_::delete(&conn, inserted_creator.id).unwrap();
+    User_::delete(&conn, inserted_recipient.id).unwrap();
+
+    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_eq!(1, num_deleted);
+  }
+}
diff --git a/server/src/db/private_message_view.rs b/server/src/db/private_message_view.rs
new file mode 100644 (file)
index 0000000..59a573f
--- /dev/null
@@ -0,0 +1,140 @@
+use super::*;
+use diesel::pg::Pg;
+
+// The faked schema since diesel doesn't do views
+table! {
+  private_message_view (id) {
+    id -> Int4,
+    creator_id -> Int4,
+    recipient_id -> Int4,
+    content -> Text,
+    deleted -> Bool,
+    read -> Bool,
+    published -> Timestamp,
+    updated -> Nullable<Timestamp>,
+    creator_name -> Varchar,
+    creator_avatar -> Nullable<Text>,
+    recipient_name -> Varchar,
+    recipient_avatar -> Nullable<Text>,
+  }
+}
+
+table! {
+  private_message_mview (id) {
+    id -> Int4,
+    creator_id -> Int4,
+    recipient_id -> Int4,
+    content -> Text,
+    deleted -> Bool,
+    read -> Bool,
+    published -> Timestamp,
+    updated -> Nullable<Timestamp>,
+    creator_name -> Varchar,
+    creator_avatar -> Nullable<Text>,
+    recipient_name -> Varchar,
+    recipient_avatar -> Nullable<Text>,
+  }
+}
+
+#[derive(
+  Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize, QueryableByName, Clone,
+)]
+#[table_name = "private_message_view"]
+pub struct PrivateMessageView {
+  pub id: i32,
+  pub creator_id: i32,
+  pub recipient_id: i32,
+  pub content: String,
+  pub deleted: bool,
+  pub read: bool,
+  pub published: chrono::NaiveDateTime,
+  pub updated: Option<chrono::NaiveDateTime>,
+  pub creator_name: String,
+  pub creator_avatar: Option<String>,
+  pub recipient_name: String,
+  pub recipient_avatar: Option<String>,
+}
+
+pub struct PrivateMessageQueryBuilder<'a> {
+  conn: &'a PgConnection,
+  query: super::private_message_view::private_message_mview::BoxedQuery<'a, Pg>,
+  for_recipient_id: i32,
+  unread_only: bool,
+  page: Option<i64>,
+  limit: Option<i64>,
+}
+
+impl<'a> PrivateMessageQueryBuilder<'a> {
+  pub fn create(conn: &'a PgConnection, for_recipient_id: i32) -> Self {
+    use super::private_message_view::private_message_mview::dsl::*;
+
+    let query = private_message_mview.into_boxed();
+
+    PrivateMessageQueryBuilder {
+      conn,
+      query,
+      for_recipient_id,
+      unread_only: false,
+      page: None,
+      limit: None,
+    }
+  }
+
+  pub fn unread_only(mut self, unread_only: bool) -> Self {
+    self.unread_only = unread_only;
+    self
+  }
+
+  pub fn page<T: MaybeOptional<i64>>(mut self, page: T) -> Self {
+    self.page = page.get_optional();
+    self
+  }
+
+  pub fn limit<T: MaybeOptional<i64>>(mut self, limit: T) -> Self {
+    self.limit = limit.get_optional();
+    self
+  }
+
+  pub fn list(self) -> Result<Vec<PrivateMessageView>, Error> {
+    use super::private_message_view::private_message_mview::dsl::*;
+
+    let mut query = self.query;
+
+    // If its unread, I only want the ones to me
+    if self.unread_only {
+      query = query
+        .filter(read.eq(false))
+        .filter(recipient_id.eq(self.for_recipient_id));
+    }
+    // Otherwise, I want the ALL view to show both sent and received
+    else {
+      query = query.filter(
+        recipient_id
+          .eq(self.for_recipient_id)
+          .or(creator_id.eq(self.for_recipient_id)),
+      )
+    }
+
+    let (limit, offset) = limit_and_offset(self.page, self.limit);
+
+    query
+      .limit(limit)
+      .offset(offset)
+      .order_by(published.desc())
+      .load::<PrivateMessageView>(self.conn)
+  }
+}
+
+impl PrivateMessageView {
+  pub fn read(conn: &PgConnection, from_private_message_id: i32) -> Result<Self, Error> {
+    use super::private_message_view::private_message_view::dsl::*;
+
+    let mut query = private_message_view.into_boxed();
+
+    query = query
+      .filter(id.eq(from_private_message_id))
+      .order_by(published.desc());
+
+    query.first::<Self>(conn)
+  }
+}
index 71b63d742c8a62c0a51bb9df44fcf90b71497821..b36c07bea703f25d956e58056a32397cb36b6226 100644 (file)
@@ -26,6 +26,7 @@ pub struct User_ {
   pub lang: String,
   pub show_avatars: bool,
   pub send_notifications_to_email: bool,
+  pub matrix_user_id: Option<String>,
 }
 
 #[derive(Insertable, AsChangeset, Clone)]
@@ -47,6 +48,7 @@ pub struct UserForm {
   pub lang: String,
   pub show_avatars: bool,
   pub send_notifications_to_email: bool,
+  pub matrix_user_id: Option<String>,
 }
 
 impl Crud<UserForm> for User_ {
@@ -184,6 +186,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
@@ -206,6 +209,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index 21dd1675d3591bba1addb7c21d15cac86dcdd442..3b10fd0ff42d2f0381ed298b64db48d62fe7c48b 100644 (file)
@@ -68,6 +68,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
@@ -89,6 +90,7 @@ mod tests {
       preferred_username: None,
       password_encrypted: "nope".into(),
       email: None,
+      matrix_user_id: None,
       avatar: None,
       admin: false,
       banned: false,
index 23e47d4bef4079ede7095f3e6101d23b37f206e7..3ea506e7f9c1a0ed252414260b53f79aaf632fc0 100644 (file)
@@ -8,6 +8,7 @@ table! {
     name -> Varchar,
     avatar -> Nullable<Text>,
     email -> Nullable<Text>,
+    matrix_user_id -> Nullable<Text>,
     fedi_name -> Varchar,
     admin -> Bool,
     banned -> Bool,
@@ -27,6 +28,7 @@ table! {
     name -> Varchar,
     avatar -> Nullable<Text>,
     email -> Nullable<Text>,
+    matrix_user_id -> Nullable<Text>,
     fedi_name -> Varchar,
     admin -> Bool,
     banned -> Bool,
@@ -49,6 +51,7 @@ pub struct UserView {
   pub name: String,
   pub avatar: Option<String>,
   pub email: Option<String>,
+  pub matrix_user_id: Option<String>,
   pub fedi_name: String,
   pub admin: bool,
   pub banned: bool,
index 4f5554fa2680e6cfa45c9a6d33ee21ceb99bb4f8..a9bd5dac7d3d2282cfcb77e29e5fded219197917 100644 (file)
@@ -105,7 +105,7 @@ pub fn send_email(
 
   let mut mailer = SmtpClient::new_simple(&email_config.smtp_server)
     .unwrap()
-    .hello_name(ClientId::Domain("localhost".to_string()))
+    .hello_name(ClientId::Domain(Settings::get().hostname.to_owned()))
     .credentials(Credentials::new(
       email_config.smtp_login.to_owned(),
       email_config.smtp_password.to_owned(),
@@ -117,6 +117,8 @@ pub fn send_email(
 
   let result = mailer.send(email.into());
 
+  mailer.close();
+
   match result {
     Ok(_) => Ok(()),
     Err(_) => Err("no_email_setup".to_string()),
index 636182aa45c5b1a7a86348fe285a7b72ced9b064..601c2e0dc019f8edabf3af57fef1cfe683466760 100644 (file)
@@ -6,7 +6,7 @@ use actix::prelude::*;
 use actix_web::*;
 use diesel::r2d2::{ConnectionManager, Pool};
 use diesel::PgConnection;
-use lemmy_server::routes::{federation, feeds, index, nodeinfo, webfinger, websocket};
+use lemmy_server::routes::{api, federation, feeds, index, nodeinfo, webfinger, websocket};
 use lemmy_server::settings::Settings;
 use lemmy_server::websocket::server::*;
 use std::io;
@@ -44,6 +44,7 @@ async fn main() -> io::Result<()> {
       .data(pool.clone())
       .data(server.clone())
       // The routes
+      .configure(api::config)
       .configure(federation::config)
       .configure(feeds::config)
       .configure(index::config)
diff --git a/server/src/routes/api.rs b/server/src/routes/api.rs
new file mode 100644 (file)
index 0000000..5121d24
--- /dev/null
@@ -0,0 +1,103 @@
+use crate::api::comment::*;
+use crate::api::community::*;
+use crate::api::post::*;
+use crate::api::site::*;
+use crate::api::user::*;
+use crate::api::{Oper, Perform};
+use actix_web::{web, HttpResponse};
+use diesel::r2d2::{ConnectionManager, Pool};
+use diesel::PgConnection;
+use failure::Error;
+use serde::Serialize;
+
+type DbParam = web::Data<Pool<ConnectionManager<PgConnection>>>;
+
+#[rustfmt::skip]
+pub fn config(cfg: &mut web::ServiceConfig) {
+  cfg
+    // Site
+    .route("/api/v1/site", web::get().to(route_get::<GetSite, GetSiteResponse>))
+    .route("/api/v1/categories", web::get().to(route_get::<ListCategories, ListCategoriesResponse>))
+    .route("/api/v1/modlog", web::get().to(route_get::<GetModlog, GetModlogResponse>))
+    .route("/api/v1/search", web::get().to(route_get::<Search, SearchResponse>))
+    // Community
+    .route("/api/v1/community", web::post().to(route_post::<CreateCommunity, CommunityResponse>))
+    .route("/api/v1/community", web::get().to(route_get::<GetCommunity, GetCommunityResponse>))
+    .route("/api/v1/community", web::put().to(route_post::<EditCommunity, CommunityResponse>))
+    .route("/api/v1/community/list", web::get().to(route_get::<ListCommunities, ListCommunitiesResponse>))
+    .route("/api/v1/community/follow", web::post().to(route_post::<FollowCommunity, CommunityResponse>))
+    // Post
+    .route("/api/v1/post", web::post().to(route_post::<CreatePost, PostResponse>))
+    .route("/api/v1/post", web::put().to(route_post::<EditPost, PostResponse>))
+    .route("/api/v1/post", web::get().to(route_get::<GetPost, GetPostResponse>))
+    .route("/api/v1/post/list", web::get().to(route_get::<GetPosts, GetPostsResponse>))
+    .route("/api/v1/post/like", web::post().to(route_post::<CreatePostLike, CreatePostLikeResponse>))
+    .route("/api/v1/post/save", web::put().to(route_post::<SavePost, PostResponse>))
+    // Comment
+    .route("/api/v1/comment", web::post().to(route_post::<CreateComment, CommentResponse>))
+    .route("/api/v1/comment", web::put().to(route_post::<EditComment, CommentResponse>))
+    .route("/api/v1/comment/like", web::post().to(route_post::<CreateCommentLike, CommentResponse>))
+    .route("/api/v1/comment/save", web::put().to(route_post::<SaveComment, CommentResponse>))
+    // User
+    .route("/api/v1/user", web::get().to(route_get::<GetUserDetails, GetUserDetailsResponse>))
+    .route("/api/v1/user/mention", web::get().to(route_get::<GetUserMentions, GetUserMentionsResponse>))
+    .route("/api/v1/user/mention", web::put().to(route_post::<EditUserMention, UserMentionResponse>))
+    .route("/api/v1/user/replies", web::get().to(route_get::<GetReplies, GetRepliesResponse>))
+    .route("/api/v1/user/followed_communities", web::get().to(route_get::<GetFollowedCommunities, GetFollowedCommunitiesResponse>))
+    // Mod actions
+    .route("/api/v1/community/transfer", web::post().to(route_post::<TransferCommunity, GetCommunityResponse>))
+    .route("/api/v1/community/ban_user", web::post().to(route_post::<BanFromCommunity, BanFromCommunityResponse>))
+    .route("/api/v1/community/mod", web::post().to(route_post::<AddModToCommunity, AddModToCommunityResponse>))
+    // Admin actions
+    .route("/api/v1/site", web::post().to(route_post::<CreateSite, SiteResponse>))
+    .route("/api/v1/site", web::put().to(route_post::<EditSite, SiteResponse>))
+    .route("/api/v1/site/transfer", web::post().to(route_post::<TransferSite, GetSiteResponse>))
+    .route("/api/v1/admin/add", web::post().to(route_post::<AddAdmin, AddAdminResponse>))
+    .route("/api/v1/user/ban", web::post().to(route_post::<BanUser, BanUserResponse>))
+    // User account actions
+    .route("/api/v1/user/login", web::post().to(route_post::<Login, LoginResponse>))
+    .route("/api/v1/user/register", web::post().to(route_post::<Register, LoginResponse>))
+    .route("/api/v1/user/delete_account", web::post().to(route_post::<DeleteAccount, LoginResponse>))
+    .route("/api/v1/user/password_reset", web::post().to(route_post::<PasswordReset, PasswordResetResponse>))
+    .route("/api/v1/user/password_change", web::post().to(route_post::<PasswordChange, LoginResponse>))
+    .route("/api/v1/user/mark_all_as_read", web::post().to(route_post::<MarkAllAsRead, GetRepliesResponse>))
+    .route("/api/v1/user/save_user_settings", web::put().to(route_post::<SaveUserSettings, LoginResponse>));
+}
+
+fn perform<Request, Response>(data: Request, db: DbParam) -> Result<HttpResponse, Error>
+where
+  Response: Serialize,
+  Oper<Request>: Perform<Response>,
+{
+  let conn = match db.get() {
+    Ok(c) => c,
+    Err(e) => return Err(format_err!("{}", e)),
+  };
+  let oper: Oper<Request> = Oper::new(data);
+  let response = oper.perform(&conn);
+  Ok(HttpResponse::Ok().json(response?))
+}
+
+async fn route_get<Data, Response>(
+  data: web::Query<Data>,
+  db: DbParam,
+) -> Result<HttpResponse, Error>
+where
+  Data: Serialize,
+  Response: Serialize,
+  Oper<Data>: Perform<Response>,
+{
+  perform::<Data, Response>(data.0, db)
+}
+
+async fn route_post<Data, Response>(
+  data: web::Json<Data>,
+  db: DbParam,
+) -> Result<HttpResponse, Error>
+where
+  Data: Serialize,
+  Response: Serialize,
+  Oper<Data>: Perform<Response>,
+{
+  perform::<Data, Response>(data.0, db)
+}
index 2453a1b24bb8e74c091963ebdb0a9b4be8cb8355..b044833efb1b88687363e97e7e3cb480a3175fa1 100644 (file)
@@ -12,6 +12,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
     .route("/login", web::get().to(index))
     .route("/create_post", web::get().to(index))
     .route("/create_community", web::get().to(index))
+    .route("/create_private_message", web::get().to(index))
     .route("/communities/page/{page}", web::get().to(index))
     .route("/communities", web::get().to(index))
     .route("/post/{id}/comment/{id2}", web::get().to(index))
index 6556c8d58850f595f248324c22b5006e07324747..27d9ea1be59dd7d575d7e8541bfe1a44c1180d9e 100644 (file)
@@ -1,3 +1,4 @@
+pub mod api;
 pub mod federation;
 pub mod feeds;
 pub mod index;
index 61957067c51b2a071d69dad9cc320dc3221e9a10..5330ed070de1617376be964206d76a6e3226a18a 100644 (file)
@@ -238,6 +238,19 @@ table! {
     }
 }
 
+table! {
+    private_message (id) {
+        id -> Int4,
+        creator_id -> Int4,
+        recipient_id -> Int4,
+        content -> Text,
+        deleted -> Bool,
+        read -> Bool,
+        published -> Timestamp,
+        updated -> Nullable<Timestamp>,
+    }
+}
+
 table! {
     site (id) {
         id -> Int4,
@@ -272,6 +285,7 @@ table! {
         lang -> Varchar,
         show_avatars -> Bool,
         send_notifications_to_email -> Bool,
+        matrix_user_id -> Nullable<Text>,
     }
 }
 
@@ -357,6 +371,7 @@ allow_tables_to_appear_in_same_query!(
   post_like,
   post_read,
   post_saved,
+  private_message,
   site,
   user_,
   user_ban,
index 74f47ad347dafef1f192a5ef058f084e1f99956e..021bcb41ee9ee8e846e0d6c80080d0b6859afdf3 100644 (file)
@@ -1 +1,47 @@
 pub mod server;
+
+#[derive(EnumString, ToString, Debug)]
+pub enum UserOperation {
+  Login,
+  Register,
+  CreateCommunity,
+  CreatePost,
+  ListCommunities,
+  ListCategories,
+  GetPost,
+  GetCommunity,
+  CreateComment,
+  EditComment,
+  SaveComment,
+  CreateCommentLike,
+  GetPosts,
+  CreatePostLike,
+  EditPost,
+  SavePost,
+  EditCommunity,
+  FollowCommunity,
+  GetFollowedCommunities,
+  GetUserDetails,
+  GetReplies,
+  GetUserMentions,
+  EditUserMention,
+  GetModlog,
+  BanFromCommunity,
+  AddModToCommunity,
+  CreateSite,
+  EditSite,
+  GetSite,
+  AddAdmin,
+  BanUser,
+  Search,
+  MarkAllAsRead,
+  SaveUserSettings,
+  TransferCommunity,
+  TransferSite,
+  DeleteAccount,
+  PasswordReset,
+  PasswordChange,
+  CreatePrivateMessage,
+  EditPrivateMessage,
+  GetPrivateMessages,
+}
index 957c5f643296e64c8084fee161485e9dd5ce1153..b1d4f1387ec78b7fe524e2bebf02603ad11f5a61 100644 (file)
@@ -3,7 +3,7 @@
 //! room through `ChatServer`.
 
 use actix::prelude::*;
-use diesel::r2d2::{ConnectionManager, Pool};
+use diesel::r2d2::{ConnectionManager, Pool, PooledConnection};
 use diesel::PgConnection;
 use failure::Error;
 use rand::{rngs::ThreadRng, Rng};
@@ -19,6 +19,7 @@ use crate::api::post::*;
 use crate::api::site::*;
 use crate::api::user::*;
 use crate::api::*;
+use crate::websocket::UserOperation;
 use crate::Settings;
 
 /// Chat server sends this messages to session
@@ -201,7 +202,6 @@ impl ChatServer {
           );
           Err(
             APIError {
-              op: "Rate Limit".to_string(),
               message: format!("Too many requests. {} per {} seconds", rate, per),
             }
             .into(),
@@ -295,11 +295,42 @@ impl Handler<StandardMessage> for ChatServer {
   }
 }
 
+#[derive(Serialize)]
+struct WebsocketResponse<T> {
+  op: String,
+  data: T,
+}
+
+fn to_json_string<T>(op: &UserOperation, data: T) -> Result<String, Error>
+where
+  T: Serialize,
+{
+  let response = WebsocketResponse {
+    op: op.to_string(),
+    data,
+  };
+  Ok(serde_json::to_string(&response)?)
+}
+
+fn do_user_operation<'a, Data, Response>(
+  op: UserOperation,
+  data: &str,
+  conn: &PooledConnection<ConnectionManager<PgConnection>>,
+) -> Result<String, Error>
+where
+  for<'de> Data: Deserialize<'de> + 'a,
+  Response: Serialize,
+  Oper<Data>: Perform<Response>,
+{
+  let parsed_data: Data = serde_json::from_str(data)?;
+  let res = Oper::new(parsed_data).perform(&conn)?;
+  to_json_string(&op, &res)
+}
+
 fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<String, Error> {
   let json: Value = serde_json::from_str(&msg.msg)?;
   let data = &json["data"].to_string();
   let op = &json["op"].as_str().ok_or(APIError {
-    op: "Unknown op type".to_string(),
     message: "Unknown op type".to_string(),
   })?;
 
@@ -307,245 +338,194 @@ fn parse_json_message(chat: &mut ChatServer, msg: StandardMessage) -> Result<Str
 
   let user_operation: UserOperation = UserOperation::from_str(&op)?;
 
+  // TODO: none of the chat messages are going to work if stuff is submitted via http api,
+  //       need to move that handling elsewhere
   match user_operation {
-    UserOperation::Login => {
-      let login: Login = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, login).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
-    }
+    UserOperation::Login => do_user_operation::<Login, LoginResponse>(user_operation, data, &conn),
     UserOperation::Register => {
-      let register: Register = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, register).perform(&conn);
-      if res.is_ok() {
-        chat.check_rate_limit_register(msg.id)?;
-      }
-      Ok(serde_json::to_string(&res?)?)
+      chat.check_rate_limit_register(msg.id)?;
+      do_user_operation::<Register, LoginResponse>(user_operation, data, &conn)
     }
     UserOperation::GetUserDetails => {
-      let get_user_details: GetUserDetails = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_user_details).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetUserDetails, GetUserDetailsResponse>(user_operation, data, &conn)
     }
     UserOperation::SaveUserSettings => {
-      let save_user_settings: SaveUserSettings = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, save_user_settings).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<SaveUserSettings, LoginResponse>(user_operation, data, &conn)
     }
     UserOperation::AddAdmin => {
-      let add_admin: AddAdmin = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, add_admin).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<AddAdmin, AddAdminResponse>(user_operation, data, &conn)
     }
     UserOperation::BanUser => {
-      let ban_user: BanUser = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, ban_user).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<BanUser, BanUserResponse>(user_operation, data, &conn)
     }
     UserOperation::GetReplies => {
-      let get_replies: GetReplies = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_replies).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetReplies, GetRepliesResponse>(user_operation, data, &conn)
     }
     UserOperation::GetUserMentions => {
-      let get_user_mentions: GetUserMentions = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_user_mentions).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetUserMentions, GetUserMentionsResponse>(user_operation, data, &conn)
     }
     UserOperation::EditUserMention => {
-      let edit_user_mention: EditUserMention = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, edit_user_mention).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<EditUserMention, UserMentionResponse>(user_operation, data, &conn)
     }
     UserOperation::MarkAllAsRead => {
-      let mark_all_as_read: MarkAllAsRead = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, mark_all_as_read).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<MarkAllAsRead, GetRepliesResponse>(user_operation, data, &conn)
     }
     UserOperation::GetCommunity => {
-      let get_community: GetCommunity = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_community).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetCommunity, GetCommunityResponse>(user_operation, data, &conn)
     }
     UserOperation::ListCommunities => {
-      let list_communities: ListCommunities = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, list_communities).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<ListCommunities, ListCommunitiesResponse>(user_operation, data, &conn)
     }
     UserOperation::CreateCommunity => {
       chat.check_rate_limit_register(msg.id)?;
-      let create_community: CreateCommunity = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, create_community).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<CreateCommunity, CommunityResponse>(user_operation, data, &conn)
     }
     UserOperation::EditCommunity => {
       let edit_community: EditCommunity = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, edit_community).perform(&conn)?;
+      let res = Oper::new(edit_community).perform(&conn)?;
       let mut community_sent: CommunityResponse = res.clone();
       community_sent.community.user_id = None;
       community_sent.community.subscribed = None;
-      let community_sent_str = serde_json::to_string(&community_sent)?;
+      let community_sent_str = to_json_string(&user_operation, &community_sent)?;
       chat.send_community_message(community_sent.community.id, &community_sent_str, msg.id)?;
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::FollowCommunity => {
-      let follow_community: FollowCommunity = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, follow_community).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
-    }
-    UserOperation::GetFollowedCommunities => {
-      let followed_communities: GetFollowedCommunities = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, followed_communities).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<FollowCommunity, CommunityResponse>(user_operation, data, &conn)
     }
+    UserOperation::GetFollowedCommunities => do_user_operation::<
+      GetFollowedCommunities,
+      GetFollowedCommunitiesResponse,
+    >(user_operation, data, &conn),
     UserOperation::BanFromCommunity => {
       let ban_from_community: BanFromCommunity = serde_json::from_str(data)?;
       let community_id = ban_from_community.community_id;
-      let res = Oper::new(user_operation, ban_from_community).perform(&conn)?;
-      let res_str = serde_json::to_string(&res)?;
+      let res = Oper::new(ban_from_community).perform(&conn)?;
+      let res_str = to_json_string(&user_operation, &res)?;
       chat.send_community_message(community_id, &res_str, msg.id)?;
       Ok(res_str)
     }
     UserOperation::AddModToCommunity => {
       let mod_add_to_community: AddModToCommunity = serde_json::from_str(data)?;
       let community_id = mod_add_to_community.community_id;
-      let res = Oper::new(user_operation, mod_add_to_community).perform(&conn)?;
-      let res_str = serde_json::to_string(&res)?;
+      let res = Oper::new(mod_add_to_community).perform(&conn)?;
+      let res_str = to_json_string(&user_operation, &res)?;
       chat.send_community_message(community_id, &res_str, msg.id)?;
       Ok(res_str)
     }
     UserOperation::ListCategories => {
-      let list_categories: ListCategories = ListCategories;
-      let res = Oper::new(user_operation, list_categories).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<ListCategories, ListCategoriesResponse>(user_operation, data, &conn)
     }
     UserOperation::CreatePost => {
       chat.check_rate_limit_post(msg.id)?;
-      let create_post: CreatePost = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, create_post).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<CreatePost, PostResponse>(user_operation, data, &conn)
     }
     UserOperation::GetPost => {
       let get_post: GetPost = serde_json::from_str(data)?;
       chat.join_room(get_post.id, msg.id);
-      let res = Oper::new(user_operation, get_post).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      let res = Oper::new(get_post).perform(&conn)?;
+      to_json_string(&user_operation, &res)
     }
     UserOperation::GetPosts => {
-      let get_posts: GetPosts = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_posts).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetPosts, GetPostsResponse>(user_operation, data, &conn)
     }
     UserOperation::CreatePostLike => {
       chat.check_rate_limit_message(msg.id)?;
-      let create_post_like: CreatePostLike = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, create_post_like).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<CreatePostLike, CreatePostLikeResponse>(user_operation, data, &conn)
     }
     UserOperation::EditPost => {
       let edit_post: EditPost = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, edit_post).perform(&conn)?;
+      let res = Oper::new(edit_post).perform(&conn)?;
       let mut post_sent = res.clone();
       post_sent.post.my_vote = None;
-      let post_sent_str = serde_json::to_string(&post_sent)?;
+      let post_sent_str = to_json_string(&user_operation, &post_sent)?;
       chat.send_room_message(post_sent.post.id, &post_sent_str, msg.id);
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::SavePost => {
-      let save_post: SavePost = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, save_post).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<SavePost, PostResponse>(user_operation, data, &conn)
     }
     UserOperation::CreateComment => {
       chat.check_rate_limit_message(msg.id)?;
       let create_comment: CreateComment = serde_json::from_str(data)?;
       let post_id = create_comment.post_id;
-      let res = Oper::new(user_operation, create_comment).perform(&conn)?;
+      let res = Oper::new(create_comment).perform(&conn)?;
       let mut comment_sent = res.clone();
       comment_sent.comment.my_vote = None;
       comment_sent.comment.user_id = None;
-      let comment_sent_str = serde_json::to_string(&comment_sent)?;
+      let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
       chat.send_room_message(post_id, &comment_sent_str, msg.id);
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::EditComment => {
       let edit_comment: EditComment = serde_json::from_str(data)?;
       let post_id = edit_comment.post_id;
-      let res = Oper::new(user_operation, edit_comment).perform(&conn)?;
+      let res = Oper::new(edit_comment).perform(&conn)?;
       let mut comment_sent = res.clone();
       comment_sent.comment.my_vote = None;
       comment_sent.comment.user_id = None;
-      let comment_sent_str = serde_json::to_string(&comment_sent)?;
+      let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
       chat.send_room_message(post_id, &comment_sent_str, msg.id);
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::SaveComment => {
-      let save_comment: SaveComment = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, save_comment).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<SaveComment, CommentResponse>(user_operation, data, &conn)
     }
     UserOperation::CreateCommentLike => {
       chat.check_rate_limit_message(msg.id)?;
       let create_comment_like: CreateCommentLike = serde_json::from_str(data)?;
       let post_id = create_comment_like.post_id;
-      let res = Oper::new(user_operation, create_comment_like).perform(&conn)?;
+      let res = Oper::new(create_comment_like).perform(&conn)?;
       let mut comment_sent = res.clone();
       comment_sent.comment.my_vote = None;
       comment_sent.comment.user_id = None;
-      let comment_sent_str = serde_json::to_string(&comment_sent)?;
+      let comment_sent_str = to_json_string(&user_operation, &comment_sent)?;
       chat.send_room_message(post_id, &comment_sent_str, msg.id);
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::GetModlog => {
-      let get_modlog: GetModlog = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, get_modlog).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<GetModlog, GetModlogResponse>(user_operation, data, &conn)
     }
     UserOperation::CreateSite => {
-      let create_site: CreateSite = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, create_site).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<CreateSite, SiteResponse>(user_operation, data, &conn)
     }
     UserOperation::EditSite => {
-      let edit_site: EditSite = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, edit_site).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<EditSite, SiteResponse>(user_operation, data, &conn)
     }
     UserOperation::GetSite => {
       let online: usize = chat.sessions.len();
       let get_site: GetSite = serde_json::from_str(data)?;
-      let mut res = Oper::new(user_operation, get_site).perform(&conn)?;
+      let mut res = Oper::new(get_site).perform(&conn)?;
       res.online = online;
-      Ok(serde_json::to_string(&res)?)
+      to_json_string(&user_operation, &res)
     }
     UserOperation::Search => {
-      let search: Search = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, search).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<Search, SearchResponse>(user_operation, data, &conn)
     }
     UserOperation::TransferCommunity => {
-      let transfer_community: TransferCommunity = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, transfer_community).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<TransferCommunity, GetCommunityResponse>(user_operation, data, &conn)
     }
     UserOperation::TransferSite => {
-      let transfer_site: TransferSite = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, transfer_site).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<TransferSite, GetSiteResponse>(user_operation, data, &conn)
     }
     UserOperation::DeleteAccount => {
-      let delete_account: DeleteAccount = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, delete_account).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<DeleteAccount, LoginResponse>(user_operation, data, &conn)
     }
     UserOperation::PasswordReset => {
-      let password_reset: PasswordReset = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, password_reset).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<PasswordReset, PasswordResetResponse>(user_operation, data, &conn)
     }
     UserOperation::PasswordChange => {
-      let password_change: PasswordChange = serde_json::from_str(data)?;
-      let res = Oper::new(user_operation, password_change).perform(&conn)?;
-      Ok(serde_json::to_string(&res)?)
+      do_user_operation::<PasswordChange, LoginResponse>(user_operation, data, &conn)
+    }
+    UserOperation::CreatePrivateMessage => {
+      chat.check_rate_limit_message(msg.id)?;
+      do_user_operation::<CreatePrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
+    }
+    UserOperation::EditPrivateMessage => {
+      do_user_operation::<EditPrivateMessage, PrivateMessageResponse>(user_operation, data, &conn)
+    }
+    UserOperation::GetPrivateMessages => {
+      do_user_operation::<GetPrivateMessages, PrivateMessagesResponse>(user_operation, data, &conn)
     }
   }
 }
diff --git a/ui/assets/css/toastify.css b/ui/assets/css/toastify.css
new file mode 100644 (file)
index 0000000..8804e22
--- /dev/null
@@ -0,0 +1,78 @@
+/*!
+ * Toastify js 1.6.2
+ * https://github.com/apvarun/toastify-js
+ * @license MIT licensed
+ *
+ * Copyright (C) 2018 Varun A P
+ */
+
+.toastify {
+    padding: 12px 20px;
+    color: #ffffff;
+    display: inline-block;
+    box-shadow: 0 3px 6px -1px rgba(0, 0, 0, 0.12), 0 10px 36px -4px rgba(77, 96, 232, 0.3);
+    background: -webkit-linear-gradient(315deg, #73a5ff, #5477f5);
+    background: linear-gradient(135deg, #73a5ff, #5477f5);
+    position: fixed;
+    opacity: 0;
+    transition: all 0.4s cubic-bezier(0.215, 0.61, 0.355, 1);
+    border-radius: 2px;
+    cursor: pointer;
+    text-decoration: none;
+    max-width: calc(50% - 20px);
+    z-index: 2147483647;
+}
+
+.toastify.on {
+    opacity: 1;
+}
+
+.toast-close {
+    opacity: 0.4;
+    padding: 0 5px;
+}
+
+.toastify-right {
+    right: 15px;
+}
+
+.toastify-left {
+    left: 15px;
+}
+
+.toastify-top {
+    top: -150px;
+}
+
+.toastify-bottom {
+    bottom: -150px;
+}
+
+.toastify-rounded {
+    border-radius: 25px;
+}
+
+.toastify-avatar {
+    width: 1.5em;
+    height: 1.5em;
+    margin: 0 5px;
+    border-radius: 2px;
+}
+
+.toastify-center {
+    margin-left: auto;
+    margin-right: auto;
+    left: 0;
+    right: 0;
+    max-width: fit-content;
+}
+
+@media only screen and (max-width: 360px) {
+    .toastify-right, .toastify-left {
+        margin-left: auto;
+        margin-right: auto;
+        left: 0;
+        right: 0;
+        max-width: fit-content;
+    }
+}
index ea6343da9afcf9fe51be1e7b4347d1f2e77214e5..41f47088f88c7d94a875c32b23a78d9bbb48c4f8 100644 (file)
@@ -36,6 +36,7 @@
     "prettier": "^1.18.2",
     "rxjs": "^6.4.0",
     "terser": "^4.6.0",
+    "toastify-js": "^1.6.2",
     "tributejs": "^4.1.1",
     "twemoji": "^12.1.2",
     "ws": "^7.0.0"
index f58168992a25339d590707a8e8a99e826a5633a5..b8ea0a5a3f510adb6100e1e248551755e0ef58f6 100644 (file)
@@ -10,12 +10,13 @@ import {
 } from '../interfaces';
 import { Subscription } from 'rxjs';
 import {
+  wsJsonToRes,
   capitalizeFirstLetter,
   mentionDropdownFetchLimit,
-  msgOp,
   mdToHtml,
   randomStr,
   markdownHelpUrl,
+  toast,
 } from '../utils';
 import { WebSocketService, UserService } from '../services';
 import autosize from 'autosize';
@@ -293,7 +294,7 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
       .catch(error => {
         i.state.imageLoading = false;
         i.setState(i.state);
-        alert(error);
+        toast(error, 'danger');
       });
   }
 
@@ -311,10 +312,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
       this.userSub = WebSocketService.Instance.subject.subscribe(
         msg => {
-          let op: UserOperation = msgOp(msg);
-          if (op == UserOperation.Search) {
-            let res: SearchResponse = msg;
-            let users = res.users.map(u => {
+          let res = wsJsonToRes(msg);
+          if (res.op == UserOperation.Search) {
+            let data = res.data as SearchResponse;
+            let users = data.users.map(u => {
               return { key: u.name };
             });
             cb(users);
@@ -343,10 +344,10 @@ export class CommentForm extends Component<CommentFormProps, CommentFormState> {
 
       this.communitySub = WebSocketService.Instance.subject.subscribe(
         msg => {
-          let op: UserOperation = msgOp(msg);
-          if (op == UserOperation.Search) {
-            let res: SearchResponse = msg;
-            let communities = res.communities.map(u => {
+          let res = wsJsonToRes(msg);
+          if (res.op == UserOperation.Search) {
+            let data = res.data as SearchResponse;
+            let communities = data.communities.map(u => {
               return { key: u.name };
             });
             cb(communities);
index baaf63e90e31928bb773972e266ff494d63ab54e..046fc88d3f05ba46d503a0ef6edbf96ed0904d07 100644 (file)
@@ -293,6 +293,16 @@ export class CommentNode extends Component<CommentNodeProps, CommentNodeState> {
                         </li>
                       </>
                     )}
+                    {!this.myComment && (
+                      <li className="list-inline-item">
+                        <Link
+                          class="text-muted"
+                          to={`/create_private_message?recipient_id=${node.comment.creator_id}`}
+                        >
+                          {i18n.t('message').toLowerCase()}
+                        </Link>
+                      </li>
+                    )}
                     <li className="list-inline-item">•</li>
                     <li className="list-inline-item">
                       <span
index 598a5dad63bffc20f4c81dcda76702d81f164cf6..867cfd818ccca8f442992bd3944495e63e7b8dbd 100644 (file)
@@ -10,9 +10,10 @@ import {
   FollowCommunityForm,
   ListCommunitiesForm,
   SortType,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import { msgOp } from '../utils';
+import { wsJsonToRes, toast } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -231,15 +232,15 @@ export class Communities extends Component<any, CommunitiesState> {
     WebSocketService.Instance.listCommunities(listCommunitiesForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.ListCommunities) {
-      let res: ListCommunitiesResponse = msg;
-      this.state.communities = res.communities;
+    } else if (res.op == UserOperation.ListCommunities) {
+      let data = res.data as ListCommunitiesResponse;
+      this.state.communities = data.communities;
       this.state.communities.sort(
         (a, b) => b.number_of_subscribers - a.number_of_subscribers
       );
@@ -248,11 +249,11 @@ export class Communities extends Component<any, CommunitiesState> {
       this.setState(this.state);
       let table = document.querySelector('#community_table');
       Sortable.initTable(table);
-    } else if (op == UserOperation.FollowCommunity) {
-      let res: CommunityResponse = msg;
-      let found = this.state.communities.find(c => c.id == res.community.id);
-      found.subscribed = res.community.subscribed;
-      found.number_of_subscribers = res.community.number_of_subscribers;
+    } else if (res.op == UserOperation.FollowCommunity) {
+      let data = res.data as CommunityResponse;
+      let found = this.state.communities.find(c => c.id == data.community.id);
+      found.subscribed = data.community.subscribed;
+      found.number_of_subscribers = data.community.number_of_subscribers;
       this.setState(this.state);
     }
   }
index 2085da2895ae2e4ceebfde20338179196d352483..4dc7bfcbb23411417128a6f0118ed1aa68ff0851 100644 (file)
@@ -8,10 +8,11 @@ import {
   ListCategoriesResponse,
   CommunityResponse,
   GetSiteResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import { msgOp, capitalizeFirstLetter } from '../utils';
-import * as autosize from 'autosize';
+import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
+import autosize from 'autosize';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -67,14 +68,7 @@ export class CommunityForm extends Component<
     }
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -246,34 +240,34 @@ export class CommunityForm extends Component<
     i.props.onCancel();
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
     console.log(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.state.loading = false;
       this.setState(this.state);
       return;
-    } else if (op == UserOperation.ListCategories) {
-      let res: ListCategoriesResponse = msg;
-      this.state.categories = res.categories;
+    } else if (res.op == UserOperation.ListCategories) {
+      let data = res.data as ListCategoriesResponse;
+      this.state.categories = data.categories;
       if (!this.props.community) {
-        this.state.communityForm.category_id = res.categories[0].id;
+        this.state.communityForm.category_id = data.categories[0].id;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommunity) {
-      let res: CommunityResponse = msg;
+    } else if (res.op == UserOperation.CreateCommunity) {
+      let data = res.data as CommunityResponse;
       this.state.loading = false;
-      this.props.onCreate(res.community);
+      this.props.onCreate(data.community);
     }
-    // TODO is ths necessary
-    else if (op == UserOperation.EditCommunity) {
-      let res: CommunityResponse = msg;
+    // TODO is this necessary
+    else if (res.op == UserOperation.EditCommunity) {
+      let data = res.data as CommunityResponse;
       this.state.loading = false;
-      this.props.onEdit(res.community);
-    } else if (op == UserOperation.GetSite) {
-      let res: GetSiteResponse = msg;
-      this.state.enable_nsfw = res.site.enable_nsfw;
+      this.props.onEdit(data.community);
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.state.enable_nsfw = data.site.enable_nsfw;
       this.setState(this.state);
     }
   }
index 873b5a8a82b757dad333ffd18288e54a94fec520..9d02dd8661634042df187de99de402fb8e72f6f3 100644 (file)
@@ -14,16 +14,18 @@ import {
   ListingType,
   GetPostsResponse,
   CreatePostLikeResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
 import { SortSelect } from './sort-select';
 import { Sidebar } from './sidebar';
 import {
-  msgOp,
+  wsJsonToRes,
   routeSortTypeToEnum,
   fetchLimit,
   postRefetchSeconds,
+  toast,
 } from '../utils';
 import { T } from 'inferno-i18next';
 import { i18n } from '../i18next';
@@ -253,43 +255,43 @@ export class Community extends Component<any, State> {
     WebSocketService.Instance.getPosts(getPostsForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.context.router.history.push('/');
       return;
-    } else if (op == UserOperation.GetCommunity) {
-      let res: GetCommunityResponse = msg;
-      this.state.community = res.community;
-      this.state.moderators = res.moderators;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.GetCommunity) {
+      let data = res.data as GetCommunityResponse;
+      this.state.community = data.community;
+      this.state.moderators = data.moderators;
+      this.state.admins = data.admins;
       document.title = `/c/${this.state.community.name} - ${WebSocketService.Instance.site.name}`;
       this.setState(this.state);
       this.keepFetchingPosts();
-    } else if (op == UserOperation.EditCommunity) {
-      let res: CommunityResponse = msg;
-      this.state.community = res.community;
+    } else if (res.op == UserOperation.EditCommunity) {
+      let data = res.data as CommunityResponse;
+      this.state.community = data.community;
       this.setState(this.state);
-    } else if (op == UserOperation.FollowCommunity) {
-      let res: CommunityResponse = msg;
-      this.state.community.subscribed = res.community.subscribed;
+    } else if (res.op == UserOperation.FollowCommunity) {
+      let data = res.data as CommunityResponse;
+      this.state.community.subscribed = data.community.subscribed;
       this.state.community.number_of_subscribers =
-        res.community.number_of_subscribers;
+        data.community.number_of_subscribers;
       this.setState(this.state);
-    } else if (op == UserOperation.GetPosts) {
-      let res: GetPostsResponse = msg;
-      this.state.posts = res.posts;
+    } else if (res.op == UserOperation.GetPosts) {
+      let data = res.data as GetPostsResponse;
+      this.state.posts = data.posts;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let res: CreatePostLikeResponse = msg;
-      let found = this.state.posts.find(c => c.id == res.post.id);
-      found.my_vote = res.post.my_vote;
-      found.score = res.post.score;
-      found.upvotes = res.post.upvotes;
-      found.downvotes = res.post.downvotes;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as CreatePostLikeResponse;
+      let found = this.state.posts.find(c => c.id == data.post.id);
+      found.my_vote = data.post.my_vote;
+      found.score = data.post.score;
+      found.upvotes = data.post.upvotes;
+      found.downvotes = data.post.downvotes;
       this.setState(this.state);
     }
   }
diff --git a/ui/src/components/create-private-message.tsx b/ui/src/components/create-private-message.tsx
new file mode 100644 (file)
index 0000000..7160bc5
--- /dev/null
@@ -0,0 +1,53 @@
+import { Component } from 'inferno';
+import { PrivateMessageForm } from './private-message-form';
+import { WebSocketService } from '../services';
+import { PrivateMessageFormParams } from '../interfaces';
+import { toast } from '../utils';
+import { i18n } from '../i18next';
+
+export class CreatePrivateMessage extends Component<any, any> {
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+      this
+    );
+  }
+
+  componentDidMount() {
+    document.title = `${i18n.t('create_private_message')} - ${
+      WebSocketService.Instance.site.name
+    }`;
+  }
+
+  render() {
+    return (
+      <div class="container">
+        <div class="row">
+          <div class="col-12 col-lg-6 offset-lg-3 mb-4">
+            <h5>{i18n.t('create_private_message')}</h5>
+            <PrivateMessageForm
+              onCreate={this.handlePrivateMessageCreate}
+              params={this.params}
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  get params(): PrivateMessageFormParams {
+    let urlParams = new URLSearchParams(this.props.location.search);
+    let params: PrivateMessageFormParams = {
+      recipient_id: Number(urlParams.get('recipient_id')),
+    };
+
+    return params;
+  }
+
+  handlePrivateMessageCreate() {
+    toast(i18n.t('message_sent'));
+
+    // Navigate to the front
+    this.props.history.push(`/`);
+  }
+}
index a302b8345906fd54ecb827c3e0c0ea07e4c58b1f..5c3ff6d2b325245c86fce3b968a6b9eb28f46380 100644 (file)
@@ -12,10 +12,16 @@ import {
   GetUserMentionsResponse,
   UserMentionResponse,
   CommentResponse,
+  WebSocketJsonResponse,
+  PrivateMessage as PrivateMessageI,
+  GetPrivateMessagesForm,
+  PrivateMessagesResponse,
+  PrivateMessageResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, fetchLimit } from '../utils';
+import { wsJsonToRes, fetchLimit, isCommentType, toast } from '../utils';
 import { CommentNodes } from './comment-nodes';
+import { PrivateMessage } from './private-message';
 import { SortSelect } from './sort-select';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -26,9 +32,10 @@ enum UnreadOrAll {
 }
 
 enum UnreadType {
-  Both,
+  All,
   Replies,
   Mentions,
+  Messages,
 }
 
 interface InboxState {
@@ -36,6 +43,7 @@ interface InboxState {
   unreadType: UnreadType;
   replies: Array<Comment>;
   mentions: Array<Comment>;
+  messages: Array<PrivateMessageI>;
   sort: SortType;
   page: number;
 }
@@ -44,9 +52,10 @@ export class Inbox extends Component<any, InboxState> {
   private subscription: Subscription;
   private emptyState: InboxState = {
     unreadOrAll: UnreadOrAll.Unread,
-    unreadType: UnreadType.Both,
+    unreadType: UnreadType.All,
     replies: [],
     mentions: [],
+    messages: [],
     sort: SortType.New,
     page: 1,
   };
@@ -103,7 +112,10 @@ export class Inbox extends Component<any, InboxState> {
                 </a>
               </small>
             </h5>
-            {this.state.replies.length + this.state.mentions.length > 0 &&
+            {this.state.replies.length +
+              this.state.mentions.length +
+              this.state.messages.length >
+              0 &&
               this.state.unreadOrAll == UnreadOrAll.Unread && (
                 <ul class="list-inline mb-1 text-muted small font-weight-bold">
                   <li className="list-inline-item">
@@ -114,9 +126,10 @@ export class Inbox extends Component<any, InboxState> {
                 </ul>
               )}
             {this.selects()}
-            {this.state.unreadType == UnreadType.Both && this.both()}
+            {this.state.unreadType == UnreadType.All && this.all()}
             {this.state.unreadType == UnreadType.Replies && this.replies()}
             {this.state.unreadType == UnreadType.Mentions && this.mentions()}
+            {this.state.unreadType == UnreadType.Messages && this.messages()}
             {this.paginator()}
           </div>
         </div>
@@ -150,8 +163,8 @@ export class Inbox extends Component<any, InboxState> {
           <option disabled>
             <T i18nKey="type">#</T>
           </option>
-          <option value={UnreadType.Both}>
-            <T i18nKey="both">#</T>
+          <option value={UnreadType.All}>
+            <T i18nKey="all">#</T>
           </option>
           <option value={UnreadType.Replies}>
             <T i18nKey="replies">#</T>
@@ -159,6 +172,9 @@ export class Inbox extends Component<any, InboxState> {
           <option value={UnreadType.Mentions}>
             <T i18nKey="mentions">#</T>
           </option>
+          <option value={UnreadType.Messages}>
+            <T i18nKey="messages">#</T>
+          </option>
         </select>
         <SortSelect
           sort={this.state.sort}
@@ -169,33 +185,25 @@ export class Inbox extends Component<any, InboxState> {
     );
   }
 
-  both() {
-    let combined: Array<{
-      type_: string;
-      data: Comment;
-    }> = [];
-    let replies = this.state.replies.map(e => {
-      return { type_: 'replies', data: e };
-    });
-    let mentions = this.state.mentions.map(e => {
-      return { type_: 'mentions', data: e };
-    });
+  all() {
+    let combined: Array<Comment | PrivateMessageI> = [];
 
-    combined.push(...replies);
-    combined.push(...mentions);
+    combined.push(...this.state.replies);
+    combined.push(...this.state.mentions);
+    combined.push(...this.state.messages);
 
     // Sort it
-    if (this.state.sort == SortType.New) {
-      combined.sort((a, b) => b.data.published.localeCompare(a.data.published));
-    } else {
-      combined.sort((a, b) => b.data.score - a.data.score);
-    }
+    combined.sort((a, b) => b.published.localeCompare(a.published));
 
     return (
       <div>
-        {combined.map(i => (
-          <CommentNodes nodes={[{ comment: i.data }]} noIndent markable />
-        ))}
+        {combined.map(i =>
+          isCommentType(i) ? (
+            <CommentNodes nodes={[{ comment: i }]} noIndent markable />
+          ) : (
+            <PrivateMessage privateMessage={i} />
+          )
+        )}
       </div>
     );
   }
@@ -220,6 +228,16 @@ export class Inbox extends Component<any, InboxState> {
     );
   }
 
+  messages() {
+    return (
+      <div>
+        {this.state.messages.map(message => (
+          <PrivateMessage privateMessage={message} />
+        ))}
+      </div>
+    );
+  }
+
   paginator() {
     return (
       <div class="mt-2">
@@ -283,6 +301,13 @@ export class Inbox extends Component<any, InboxState> {
       limit: fetchLimit,
     };
     WebSocketService.Instance.getUserMentions(userMentionsForm);
+
+    let privateMessagesForm: GetPrivateMessagesForm = {
+      unread_only: this.state.unreadOrAll == UnreadOrAll.Unread,
+      page: this.state.page,
+      limit: fetchLimit,
+    };
+    WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
   }
 
   handleSortChange(val: SortType) {
@@ -296,94 +321,122 @@ export class Inbox extends Component<any, InboxState> {
     WebSocketService.Instance.markAllAsRead();
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.GetReplies) {
-      let res: GetRepliesResponse = msg;
-      this.state.replies = res.replies;
+    } else if (res.op == UserOperation.GetReplies) {
+      let data = res.data as GetRepliesResponse;
+      this.state.replies = data.replies;
+      this.sendUnreadCount();
+      window.scrollTo(0, 0);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.GetUserMentions) {
+      let data = res.data as GetUserMentionsResponse;
+      this.state.mentions = data.mentions;
+      this.sendUnreadCount();
+      window.scrollTo(0, 0);
+      this.setState(this.state);
+    } else if (res.op == UserOperation.GetPrivateMessages) {
+      let data = res.data as PrivateMessagesResponse;
+      this.state.messages = data.messages;
       this.sendUnreadCount();
       window.scrollTo(0, 0);
       this.setState(this.state);
-    } else if (op == UserOperation.GetUserMentions) {
-      let res: GetUserMentionsResponse = msg;
-      this.state.mentions = res.mentions;
+    } else if (res.op == UserOperation.EditPrivateMessage) {
+      let data = res.data as PrivateMessageResponse;
+      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;
+      }
       this.sendUnreadCount();
       window.scrollTo(0, 0);
       this.setState(this.state);
-    } else if (op == UserOperation.MarkAllAsRead) {
+    } else if (res.op == UserOperation.MarkAllAsRead) {
       this.state.replies = [];
       this.state.mentions = [];
+      this.state.messages = [];
+      this.sendUnreadCount();
       window.scrollTo(0, 0);
       this.setState(this.state);
-    } else if (op == UserOperation.EditComment) {
-      let res: CommentResponse = msg;
-
-      let found = this.state.replies.find(c => c.id == res.comment.id);
-      found.content = res.comment.content;
-      found.updated = res.comment.updated;
-      found.removed = res.comment.removed;
-      found.deleted = res.comment.deleted;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      found.score = res.comment.score;
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
+
+      let found = this.state.replies.find(c => c.id == data.comment.id);
+      found.content = data.comment.content;
+      found.updated = data.comment.updated;
+      found.removed = data.comment.removed;
+      found.deleted = data.comment.deleted;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      found.score = data.comment.score;
 
       // If youre in the unread view, just remove it from the list
-      if (this.state.unreadOrAll == UnreadOrAll.Unread && res.comment.read) {
+      if (this.state.unreadOrAll == UnreadOrAll.Unread && data.comment.read) {
         this.state.replies = this.state.replies.filter(
-          r => r.id !== res.comment.id
+          r => r.id !== data.comment.id
         );
       } else {
-        let found = this.state.replies.find(c => c.id == res.comment.id);
-        found.read = res.comment.read;
+        let found = this.state.replies.find(c => c.id == data.comment.id);
+        found.read = data.comment.read;
       }
       this.sendUnreadCount();
       this.setState(this.state);
-    } else if (op == UserOperation.EditUserMention) {
-      let res: UserMentionResponse = msg;
-
-      let found = this.state.mentions.find(c => c.id == res.mention.id);
-      found.content = res.mention.content;
-      found.updated = res.mention.updated;
-      found.removed = res.mention.removed;
-      found.deleted = res.mention.deleted;
-      found.upvotes = res.mention.upvotes;
-      found.downvotes = res.mention.downvotes;
-      found.score = res.mention.score;
+    } else if (res.op == UserOperation.EditUserMention) {
+      let data = res.data as UserMentionResponse;
+
+      let found = this.state.mentions.find(c => c.id == data.mention.id);
+      found.content = data.mention.content;
+      found.updated = data.mention.updated;
+      found.removed = data.mention.removed;
+      found.deleted = data.mention.deleted;
+      found.upvotes = data.mention.upvotes;
+      found.downvotes = data.mention.downvotes;
+      found.score = data.mention.score;
 
       // If youre in the unread view, just remove it from the list
-      if (this.state.unreadOrAll == UnreadOrAll.Unread && res.mention.read) {
+      if (this.state.unreadOrAll == UnreadOrAll.Unread && data.mention.read) {
         this.state.mentions = this.state.mentions.filter(
-          r => r.id !== res.mention.id
+          r => r.id !== data.mention.id
         );
       } else {
-        let found = this.state.mentions.find(c => c.id == res.mention.id);
-        found.read = res.mention.read;
+        let found = this.state.mentions.find(c => c.id == data.mention.id);
+        found.read = data.mention.read;
       }
       this.sendUnreadCount();
       this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
+    } else if (res.op == UserOperation.CreateComment) {
       // let res: CommentResponse = msg;
-      alert(i18n.t('reply_sent'));
+      toast(i18n.t('reply_sent'));
       // this.state.replies.unshift(res.comment); // TODO do this right
       // this.setState(this.state);
-    } else if (op == UserOperation.SaveComment) {
-      let res: CommentResponse = msg;
-      let found = this.state.replies.find(c => c.id == res.comment.id);
-      found.saved = res.comment.saved;
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      let found = this.state.replies.find(c => c.id == data.comment.id);
+      found.saved = data.comment.saved;
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let res: CommentResponse = msg;
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
       let found: Comment = this.state.replies.find(
-        c => c.id === res.comment.id
+        c => c.id === data.comment.id
       );
-      found.score = res.comment.score;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+      found.score = data.comment.score;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
       this.setState(this.state);
     }
   }
@@ -391,7 +444,10 @@ export class Inbox extends Component<any, InboxState> {
   sendUnreadCount() {
     let count =
       this.state.replies.filter(r => !r.read).length +
-      this.state.mentions.filter(r => !r.read).length;
+      this.state.mentions.filter(r => !r.read).length +
+      this.state.messages.filter(
+        r => !r.read && r.creator_id !== UserService.Instance.user.id
+      ).length;
     UserService.Instance.sub.next({
       user: UserService.Instance.user,
       unreadCount: count,
index 53b7a22ff278d07ef27900e5555fcd0b7c375f18..ac60ba74bce85f84e4bb16dbb2f384bad726d24e 100644 (file)
@@ -8,9 +8,10 @@ import {
   UserOperation,
   PasswordResetForm,
   GetSiteResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, validEmail } from '../utils';
+import { wsJsonToRes, validEmail, toast } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -48,14 +49,7 @@ export class Login extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -299,31 +293,32 @@ export class Login extends Component<any, State> {
     WebSocketService.Instance.passwordReset(resetForm);
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.state = this.emptyState;
       this.setState(this.state);
       return;
     } else {
-      if (op == UserOperation.Login) {
+      if (res.op == UserOperation.Login) {
+        let data = res.data as LoginResponse;
         this.state = this.emptyState;
         this.setState(this.state);
-        let res: LoginResponse = msg;
-        UserService.Instance.login(res);
+        UserService.Instance.login(data);
+        toast(i18n.t('logged_in'));
         this.props.history.push('/');
-      } else if (op == UserOperation.Register) {
+      } else if (res.op == UserOperation.Register) {
+        let data = res.data as LoginResponse;
         this.state = this.emptyState;
         this.setState(this.state);
-        let res: LoginResponse = msg;
-        UserService.Instance.login(res);
+        UserService.Instance.login(data);
         this.props.history.push('/communities');
-      } else if (op == UserOperation.PasswordReset) {
-        alert(i18n.t('reset_password_mail_sent'));
-      } else if (op == UserOperation.GetSite) {
-        let res: GetSiteResponse = msg;
-        this.state.enable_nsfw = res.site.enable_nsfw;
+      } else if (res.op == UserOperation.PasswordReset) {
+        toast(i18n.t('reset_password_mail_sent'));
+      } else if (res.op == UserOperation.GetSite) {
+        let data = res.data as GetSiteResponse;
+        this.state.enable_nsfw = data.site.enable_nsfw;
         this.setState(this.state);
         document.title = `${i18n.t('login')} - ${
           WebSocketService.Instance.site.name
index 5fdf23bbf3fee6c0dfdd494f602a56d09df5c416..9f16edb5d340983692810adb477f8be307f19c99 100644 (file)
@@ -17,6 +17,7 @@ import {
   CreatePostLikeResponse,
   Post,
   GetPostsForm,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { PostListings } from './post-listings';
@@ -24,7 +25,7 @@ import { SortSelect } from './sort-select';
 import { ListingTypeSelect } from './listing-type-select';
 import { SiteForm } from './site-form';
 import {
-  msgOp,
+  wsJsonToRes,
   repoUrl,
   mdToHtml,
   fetchLimit,
@@ -33,6 +34,7 @@ import {
   postRefetchSeconds,
   pictshareAvatarThumbnail,
   showAvatars,
+  toast,
 } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -56,7 +58,6 @@ export class Main extends Component<any, MainState> {
     subscribedCommunities: [],
     trendingCommunities: [],
     site: {
-      op: null,
       site: {
         id: null,
         name: null,
@@ -562,50 +563,50 @@ export class Main extends Component<any, MainState> {
     WebSocketService.Instance.getPosts(getPostsForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.GetFollowedCommunities) {
-      let res: GetFollowedCommunitiesResponse = msg;
-      this.state.subscribedCommunities = res.communities;
+    } else if (res.op == UserOperation.GetFollowedCommunities) {
+      let data = res.data as GetFollowedCommunitiesResponse;
+      this.state.subscribedCommunities = data.communities;
       this.setState(this.state);
-    } else if (op == UserOperation.ListCommunities) {
-      let res: ListCommunitiesResponse = msg;
-      this.state.trendingCommunities = res.communities;
+    } else if (res.op == UserOperation.ListCommunities) {
+      let data = res.data as ListCommunitiesResponse;
+      this.state.trendingCommunities = data.communities;
       this.setState(this.state);
-    } else if (op == UserOperation.GetSite) {
-      let res: GetSiteResponse = msg;
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
 
       // This means it hasn't been set up yet
-      if (!res.site) {
+      if (!data.site) {
         this.context.router.history.push('/setup');
       }
-      this.state.site.admins = res.admins;
-      this.state.site.site = res.site;
-      this.state.site.banned = res.banned;
-      this.state.site.online = res.online;
+      this.state.site.admins = data.admins;
+      this.state.site.site = data.site;
+      this.state.site.banned = data.banned;
+      this.state.site.online = data.online;
       this.setState(this.state);
       document.title = `${WebSocketService.Instance.site.name}`;
-    } else if (op == UserOperation.EditSite) {
-      let res: SiteResponse = msg;
-      this.state.site.site = res.site;
+    } else if (res.op == UserOperation.EditSite) {
+      let data = res.data as SiteResponse;
+      this.state.site.site = data.site;
       this.state.showEditSite = false;
       this.setState(this.state);
-    } else if (op == UserOperation.GetPosts) {
-      let res: GetPostsResponse = msg;
-      this.state.posts = res.posts;
+    } else if (res.op == UserOperation.GetPosts) {
+      let data = res.data as GetPostsResponse;
+      this.state.posts = data.posts;
       this.state.loading = false;
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let res: CreatePostLikeResponse = msg;
-      let found = this.state.posts.find(c => c.id == res.post.id);
-      found.my_vote = res.post.my_vote;
-      found.score = res.post.score;
-      found.upvotes = res.post.upvotes;
-      found.downvotes = res.post.downvotes;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as CreatePostLikeResponse;
+      let found = this.state.posts.find(c => c.id == data.post.id);
+      found.my_vote = data.post.my_vote;
+      found.score = data.post.score;
+      found.upvotes = data.post.upvotes;
+      found.downvotes = data.post.downvotes;
       this.setState(this.state);
     }
   }
index 425710dd1726076f317c48bba97596c0d033792f..dd651092887bd166622968e5537d5014dde575a9 100644 (file)
@@ -17,9 +17,9 @@ import {
   ModAdd,
 } from '../interfaces';
 import { WebSocketService } from '../services';
-import { msgOp, addTypeInfo, fetchLimit } from '../utils';
+import { wsJsonToRes, addTypeInfo, fetchLimit, toast } from '../utils';
 import { MomentTime } from './moment-time';
-import * as moment from 'moment';
+import moment from 'moment';
 import { i18n } from '../i18next';
 
 interface ModlogState {
@@ -55,14 +55,7 @@ export class Modlog extends Component<any, ModlogState> {
       ? Number(this.props.match.params.community_id)
       : undefined;
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -429,17 +422,17 @@ export class Modlog extends Component<any, ModlogState> {
     WebSocketService.Instance.getModlog(modlogForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.GetModlog) {
-      let res: GetModlogResponse = msg;
+    } else if (res.op == UserOperation.GetModlog) {
+      let data = res.data as GetModlogResponse;
       this.state.loading = false;
       window.scrollTo(0, 0);
-      this.setCombined(res);
+      this.setCombined(data);
     }
   }
 }
index 6bb4d99cfc5365f4cf3b76fa4e61bd081bb04fa7..fd2a7efadf10db0ba74f779b7d2900811472c2a7 100644 (file)
@@ -1,5 +1,5 @@
 import { Component } from 'inferno';
-import * as moment from 'moment';
+import moment from 'moment';
 import { getMomentLanguage } from '../utils';
 import { i18n } from '../i18next';
 
index b1dcf096505891757725703699906bca26eed714..849822af16d6ac220e07168641e11be8e33a623d 100644 (file)
@@ -9,15 +9,21 @@ import {
   GetRepliesResponse,
   GetUserMentionsForm,
   GetUserMentionsResponse,
+  GetPrivateMessagesForm,
+  PrivateMessagesResponse,
   SortType,
   GetSiteResponse,
   Comment,
+  PrivateMessage,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import {
-  msgOp,
+  wsJsonToRes,
   pictshareAvatarThumbnail,
   showAvatars,
   fetchLimit,
+  isCommentType,
+  toast,
 } from '../utils';
 import { version } from '../version';
 import { i18n } from '../i18next';
@@ -28,6 +34,7 @@ interface NavbarState {
   expanded: boolean;
   replies: Array<Comment>;
   mentions: Array<Comment>;
+  messages: Array<PrivateMessage>;
   fetchCount: number;
   unreadCount: number;
   siteName: string;
@@ -42,6 +49,7 @@ export class Navbar extends Component<any, NavbarState> {
     fetchCount: 0,
     replies: [],
     mentions: [],
+    messages: [],
     expanded: false,
     siteName: undefined,
   };
@@ -192,17 +200,17 @@ export class Navbar extends Component<any, NavbarState> {
     i.setState(i.state);
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      if (msg.error == 'not_logged_in') {
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      if (res.error == 'not_logged_in') {
         UserService.Instance.logout();
         location.reload();
       }
       return;
-    } else if (op == UserOperation.GetReplies) {
-      let res: GetRepliesResponse = msg;
-      let unreadReplies = res.replies.filter(r => !r.read);
+    } else if (res.op == UserOperation.GetReplies) {
+      let data = res.data as GetRepliesResponse;
+      let unreadReplies = data.replies.filter(r => !r.read);
       if (
         unreadReplies.length > 0 &&
         this.state.fetchCount > 1 &&
@@ -214,9 +222,9 @@ export class Navbar extends Component<any, NavbarState> {
       this.state.replies = unreadReplies;
       this.setState(this.state);
       this.sendUnreadCount();
-    } else if (op == UserOperation.GetUserMentions) {
-      let res: GetUserMentionsResponse = msg;
-      let unreadMentions = res.mentions.filter(r => !r.read);
+    } else if (res.op == UserOperation.GetUserMentions) {
+      let data = res.data as GetUserMentionsResponse;
+      let unreadMentions = data.mentions.filter(r => !r.read);
       if (
         unreadMentions.length > 0 &&
         this.state.fetchCount > 1 &&
@@ -228,12 +236,26 @@ export class Navbar extends Component<any, NavbarState> {
       this.state.mentions = unreadMentions;
       this.setState(this.state);
       this.sendUnreadCount();
-    } else if (op == UserOperation.GetSite) {
-      let res: GetSiteResponse = msg;
+    } else if (res.op == UserOperation.GetPrivateMessages) {
+      let data = res.data as PrivateMessagesResponse;
+      let unreadMessages = data.messages.filter(r => !r.read);
+      if (
+        unreadMessages.length > 0 &&
+        this.state.fetchCount > 1 &&
+        JSON.stringify(this.state.messages) !== JSON.stringify(unreadMessages)
+      ) {
+        this.notify(unreadMessages);
+      }
+
+      this.state.messages = unreadMessages;
+      this.setState(this.state);
+      this.sendUnreadCount();
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
 
-      if (res.site) {
-        this.state.siteName = res.site.name;
-        WebSocketService.Instance.site = res.site;
+      if (data.site) {
+        this.state.siteName = data.site.name;
+        WebSocketService.Instance.site = data.site;
         this.setState(this.state);
       }
     }
@@ -259,9 +281,17 @@ export class Navbar extends Component<any, NavbarState> {
         page: 1,
         limit: fetchLimit,
       };
+
+      let privateMessagesForm: GetPrivateMessagesForm = {
+        unread_only: true,
+        page: 1,
+        limit: fetchLimit,
+      };
+
       if (this.currentLocation !== '/inbox') {
         WebSocketService.Instance.getReplies(repliesForm);
         WebSocketService.Instance.getUserMentions(userMentionsForm);
+        WebSocketService.Instance.getPrivateMessages(privateMessagesForm);
         this.state.fetchCount++;
       }
     }
@@ -281,7 +311,8 @@ export class Navbar extends Component<any, NavbarState> {
   get unreadCount() {
     return (
       this.state.replies.filter(r => !r.read).length +
-      this.state.mentions.filter(r => !r.read).length
+      this.state.mentions.filter(r => !r.read).length +
+      this.state.messages.filter(r => !r.read).length
     );
   }
 
@@ -289,7 +320,7 @@ export class Navbar extends Component<any, NavbarState> {
     if (UserService.Instance.user) {
       document.addEventListener('DOMContentLoaded', function() {
         if (!Notification) {
-          alert(i18n.t('notifications_error'));
+          toast(i18n.t('notifications_error'), 'danger');
           return;
         }
 
@@ -299,21 +330,25 @@ export class Navbar extends Component<any, NavbarState> {
     }
   }
 
-  notify(replies: Array<Comment>) {
+  notify(replies: Array<Comment | PrivateMessage>) {
     let recentReply = replies[0];
     if (Notification.permission !== 'granted') Notification.requestPermission();
     else {
       var notification = new Notification(
         `${replies.length} ${i18n.t('unread_messages')}`,
         {
-          icon: `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
+          icon: recentReply.creator_avatar
+            ? recentReply.creator_avatar
+            : `${window.location.protocol}//${window.location.host}/static/assets/apple-touch-icon.png`,
           body: `${recentReply.creator_name}: ${recentReply.content}`,
         }
       );
 
       notification.onclick = () => {
         this.context.router.history.push(
-          `/post/${recentReply.post_id}/comment/${recentReply.id}`
+          isCommentType(recentReply)
+            ? `/post/${recentReply.post_id}/comment/${recentReply.id}`
+            : `/inbox`
         );
       };
     }
index 3e542f7b520593dd20783e4c068d924843061ab2..10b6867c9e5a35aab99791321699ade4a46c3bea 100644 (file)
@@ -5,9 +5,10 @@ import {
   UserOperation,
   LoginResponse,
   PasswordChangeForm,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, capitalizeFirstLetter } from '../utils';
+import { wsJsonToRes, capitalizeFirstLetter, toast } from '../utils';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
 
@@ -34,14 +35,7 @@ export class PasswordChange extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -140,19 +134,19 @@ export class PasswordChange extends Component<any, State> {
     WebSocketService.Instance.passwordChange(i.state.passwordChangeForm);
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
     if (msg.error) {
-      alert(i18n.t(msg.error));
+      toast(i18n.t(msg.error), 'danger');
       this.state.loading = false;
       this.setState(this.state);
       return;
     } else {
-      if (op == UserOperation.PasswordChange) {
+      if (res.op == UserOperation.PasswordChange) {
+        let data = res.data as LoginResponse;
         this.state = this.emptyState;
         this.setState(this.state);
-        let res: LoginResponse = msg;
-        UserService.Instance.login(res);
+        UserService.Instance.login(data);
         this.props.history.push('/');
       }
     }
index fe633a01c61ac75940ddd0123fe2b039f3470aa9..440617743e3bd1a904e5eb754e67025c8608b767 100644 (file)
@@ -16,10 +16,11 @@ import {
   SearchType,
   SearchResponse,
   GetSiteResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
-  msgOp,
+  wsJsonToRes,
   getPageTitle,
   validURL,
   capitalizeFirstLetter,
@@ -28,6 +29,7 @@ import {
   mdToHtml,
   debounce,
   isImage,
+  toast,
 } from '../utils';
 import autosize from 'autosize';
 import { i18n } from '../i18next';
@@ -453,51 +455,51 @@ export class PostForm extends Component<PostFormProps, PostFormState> {
       .catch(error => {
         i.state.imageLoading = false;
         i.setState(i.state);
-        alert(error);
+        toast(error, 'danger');
       });
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.state.loading = false;
       this.setState(this.state);
       return;
-    } else if (op == UserOperation.ListCommunities) {
-      let res: ListCommunitiesResponse = msg;
-      this.state.communities = res.communities;
+    } else if (res.op == UserOperation.ListCommunities) {
+      let data = res.data as ListCommunitiesResponse;
+      this.state.communities = data.communities;
       if (this.props.post) {
         this.state.postForm.community_id = this.props.post.community_id;
       } else if (this.props.params && this.props.params.community) {
-        let foundCommunityId = res.communities.find(
+        let foundCommunityId = data.communities.find(
           r => r.name == this.props.params.community
         ).id;
         this.state.postForm.community_id = foundCommunityId;
       } else {
-        this.state.postForm.community_id = res.communities[0].id;
+        this.state.postForm.community_id = data.communities[0].id;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePost) {
+    } else if (res.op == UserOperation.CreatePost) {
+      let data = res.data as PostResponse;
       this.state.loading = false;
-      let res: PostResponse = msg;
-      this.props.onCreate(res.post.id);
-    } else if (op == UserOperation.EditPost) {
+      this.props.onCreate(data.post.id);
+    } else if (res.op == UserOperation.EditPost) {
+      let data = res.data as PostResponse;
       this.state.loading = false;
-      let res: PostResponse = msg;
-      this.props.onEdit(res.post);
-    } else if (op == UserOperation.Search) {
-      let res: SearchResponse = msg;
-
-      if (res.type_ == SearchType[SearchType.Posts]) {
-        this.state.suggestedPosts = res.posts;
-      } else if (res.type_ == SearchType[SearchType.Url]) {
-        this.state.crossPosts = res.posts;
+      this.props.onEdit(data.post);
+    } else if (res.op == UserOperation.Search) {
+      let data = res.data as SearchResponse;
+
+      if (data.type_ == SearchType[SearchType.Posts]) {
+        this.state.suggestedPosts = data.posts;
+      } else if (data.type_ == SearchType[SearchType.Url]) {
+        this.state.crossPosts = data.posts;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.GetSite) {
-      let res: GetSiteResponse = msg;
-      this.state.enable_nsfw = res.site.enable_nsfw;
+    } else if (res.op == UserOperation.GetSite) {
+      let data = res.data as GetSiteResponse;
+      this.state.enable_nsfw = data.site.enable_nsfw;
       this.setState(this.state);
     }
   }
index 2005cc173048396b05ceb59f25be9df80ee86626..931ced2d1d5c0d7399c408eb794ab14f701c9140 100644 (file)
@@ -26,9 +26,10 @@ import {
   SearchResponse,
   GetSiteResponse,
   GetCommunityResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp, hotRank } from '../utils';
+import { wsJsonToRes, hotRank, toast } from '../utils';
 import { PostListing } from './post-listing';
 import { PostListings } from './post-listings';
 import { Sidebar } from './sidebar';
@@ -341,19 +342,19 @@ export class Post extends Component<any, PostState> {
     );
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.GetPost) {
-      let res: GetPostResponse = msg;
-      this.state.post = res.post;
-      this.state.comments = res.comments;
-      this.state.community = res.community;
-      this.state.moderators = res.moderators;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.GetPost) {
+      let data = res.data as GetPostResponse;
+      this.state.post = data.post;
+      this.state.comments = data.comments;
+      this.state.community = data.community;
+      this.state.moderators = data.moderators;
+      this.state.admins = data.admins;
       this.state.loading = false;
       document.title = `${this.state.post.name} - ${WebSocketService.Instance.site.name}`;
 
@@ -370,111 +371,111 @@ export class Post extends Component<any, PostState> {
       }
 
       this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
-      let res: CommentResponse = msg;
-      this.state.comments.unshift(res.comment);
+    } else if (res.op == UserOperation.CreateComment) {
+      let data = res.data as CommentResponse;
+      this.state.comments.unshift(data.comment);
       this.setState(this.state);
-    } else if (op == UserOperation.EditComment) {
-      let res: CommentResponse = msg;
-      let found = this.state.comments.find(c => c.id == res.comment.id);
-      found.content = res.comment.content;
-      found.updated = res.comment.updated;
-      found.removed = res.comment.removed;
-      found.deleted = res.comment.deleted;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      found.score = res.comment.score;
-      found.read = res.comment.read;
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
+      let found = this.state.comments.find(c => c.id == data.comment.id);
+      found.content = data.comment.content;
+      found.updated = data.comment.updated;
+      found.removed = data.comment.removed;
+      found.deleted = data.comment.deleted;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      found.score = data.comment.score;
+      found.read = data.comment.read;
 
       this.setState(this.state);
-    } else if (op == UserOperation.SaveComment) {
-      let res: CommentResponse = msg;
-      let found = this.state.comments.find(c => c.id == res.comment.id);
-      found.saved = res.comment.saved;
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      let found = this.state.comments.find(c => c.id == data.comment.id);
+      found.saved = data.comment.saved;
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let res: CommentResponse = msg;
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
       let found: Comment = this.state.comments.find(
-        c => c.id === res.comment.id
+        c => c.id === data.comment.id
       );
-      found.score = res.comment.score;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      if (res.comment.my_vote !== null) {
-        found.my_vote = res.comment.my_vote;
+      found.score = data.comment.score;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      if (data.comment.my_vote !== null) {
+        found.my_vote = data.comment.my_vote;
         found.upvoteLoading = false;
         found.downvoteLoading = false;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let res: CreatePostLikeResponse = msg;
-      this.state.post.my_vote = res.post.my_vote;
-      this.state.post.score = res.post.score;
-      this.state.post.upvotes = res.post.upvotes;
-      this.state.post.downvotes = res.post.downvotes;
-      this.state.post.upvoteLoading = false;
-      this.state.post.downvoteLoading = false;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as CreatePostLikeResponse;
+      this.state.post.my_vote = data.post.my_vote;
+      this.state.post.score = data.post.score;
+      this.state.post.upvotes = data.post.upvotes;
+      this.state.post.downvotes = data.post.downvotes;
       this.setState(this.state);
-    } else if (op == UserOperation.EditPost) {
-      let res: PostResponse = msg;
-      this.state.post = res.post;
+    } else if (res.op == UserOperation.EditPost) {
+      let data = res.data as PostResponse;
+      this.state.post = data.post;
       this.setState(this.state);
-    } else if (op == UserOperation.SavePost) {
-      let res: PostResponse = msg;
-      this.state.post = res.post;
+    } else if (res.op == UserOperation.SavePost) {
+      let data = res.data as PostResponse;
+      this.state.post = data.post;
       this.setState(this.state);
-    } else if (op == UserOperation.EditCommunity) {
-      let res: CommunityResponse = msg;
-      this.state.community = res.community;
-      this.state.post.community_id = res.community.id;
-      this.state.post.community_name = res.community.name;
+    } else if (res.op == UserOperation.EditCommunity) {
+      let data = res.data as CommunityResponse;
+      this.state.community = data.community;
+      this.state.post.community_id = data.community.id;
+      this.state.post.community_name = data.community.name;
       this.setState(this.state);
-    } else if (op == UserOperation.FollowCommunity) {
-      let res: CommunityResponse = msg;
-      this.state.community.subscribed = res.community.subscribed;
+    } else if (res.op == UserOperation.FollowCommunity) {
+      let data = res.data as CommunityResponse;
+      this.state.community.subscribed = data.community.subscribed;
       this.state.community.number_of_subscribers =
-        res.community.number_of_subscribers;
+        data.community.number_of_subscribers;
       this.setState(this.state);
-    } else if (op == UserOperation.BanFromCommunity) {
-      let res: BanFromCommunityResponse = msg;
+    } else if (res.op == UserOperation.BanFromCommunity) {
+      let data = res.data as BanFromCommunityResponse;
       this.state.comments
-        .filter(c => c.creator_id == res.user.id)
-        .forEach(c => (c.banned_from_community = res.banned));
-      if (this.state.post.creator_id == res.user.id) {
-        this.state.post.banned_from_community = res.banned;
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned_from_community = data.banned));
+      if (this.state.post.creator_id == data.user.id) {
+        this.state.post.banned_from_community = data.banned;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.AddModToCommunity) {
-      let res: AddModToCommunityResponse = msg;
-      this.state.moderators = res.moderators;
+    } else if (res.op == UserOperation.AddModToCommunity) {
+      let data = res.data as AddModToCommunityResponse;
+      this.state.moderators = data.moderators;
       this.setState(this.state);
-    } else if (op == UserOperation.BanUser) {
-      let res: BanUserResponse = msg;
+    } else if (res.op == UserOperation.BanUser) {
+      let data = res.data as BanUserResponse;
       this.state.comments
-        .filter(c => c.creator_id == res.user.id)
-        .forEach(c => (c.banned = res.banned));
-      if (this.state.post.creator_id == res.user.id) {
-        this.state.post.banned = res.banned;
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
+      if (this.state.post.creator_id == data.user.id) {
+        this.state.post.banned = data.banned;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.AddAdmin) {
-      let res: AddAdminResponse = msg;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.AddAdmin) {
+      let data = res.data as AddAdminResponse;
+      this.state.admins = data.admins;
       this.setState(this.state);
-    } else if (op == UserOperation.Search) {
-      let res: SearchResponse = msg;
-      this.state.crossPosts = res.posts.filter(p => p.id != this.state.post.id);
+    } else if (res.op == UserOperation.Search) {
+      let data = res.data as SearchResponse;
+      this.state.crossPosts = data.posts.filter(
+        p => p.id != this.state.post.id
+      );
       this.setState(this.state);
-    } else if (op == UserOperation.TransferSite) {
-      let res: GetSiteResponse = msg;
+    } else if (res.op == UserOperation.TransferSite) {
+      let data = res.data as GetSiteResponse;
 
-      this.state.admins = res.admins;
+      this.state.admins = data.admins;
       this.setState(this.state);
-    } else if (op == UserOperation.TransferCommunity) {
-      let res: GetCommunityResponse = msg;
-      this.state.community = res.community;
-      this.state.moderators = res.moderators;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.TransferCommunity) {
+      let data = res.data as GetCommunityResponse;
+      this.state.community = data.community;
+      this.state.moderators = data.moderators;
+      this.state.admins = data.admins;
       this.setState(this.state);
     }
   }
diff --git a/ui/src/components/private-message-form.tsx b/ui/src/components/private-message-form.tsx
new file mode 100644 (file)
index 0000000..5ee1c1f
--- /dev/null
@@ -0,0 +1,293 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from 'rxjs';
+import { retryWhen, delay, take } from 'rxjs/operators';
+import {
+  PrivateMessageForm as PrivateMessageFormI,
+  EditPrivateMessageForm,
+  PrivateMessageFormParams,
+  PrivateMessage,
+  PrivateMessageResponse,
+  UserView,
+  UserOperation,
+  UserDetailsResponse,
+  GetUserDetailsForm,
+  SortType,
+  WebSocketJsonResponse,
+} from '../interfaces';
+import { WebSocketService } from '../services';
+import {
+  capitalizeFirstLetter,
+  markdownHelpUrl,
+  mdToHtml,
+  showAvatars,
+  pictshareAvatarThumbnail,
+  wsJsonToRes,
+  toast,
+} from '../utils';
+import autosize from 'autosize';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageFormProps {
+  privateMessage?: PrivateMessage; // If a pm is given, that means this is an edit
+  params?: PrivateMessageFormParams;
+  onCancel?(): any;
+  onCreate?(message: PrivateMessage): any;
+  onEdit?(message: PrivateMessage): any;
+}
+
+interface PrivateMessageFormState {
+  privateMessageForm: PrivateMessageFormI;
+  recipient: UserView;
+  loading: boolean;
+  previewMode: boolean;
+  showDisclaimer: boolean;
+}
+
+export class PrivateMessageForm extends Component<
+  PrivateMessageFormProps,
+  PrivateMessageFormState
+> {
+  private subscription: Subscription;
+  private emptyState: PrivateMessageFormState = {
+    privateMessageForm: {
+      content: null,
+      recipient_id: null,
+    },
+    recipient: null,
+    loading: false,
+    previewMode: false,
+    showDisclaimer: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+
+    if (this.props.privateMessage) {
+      this.state.privateMessageForm = {
+        content: this.props.privateMessage.content,
+        recipient_id: this.props.privateMessage.recipient_id,
+      };
+    }
+
+    if (this.props.params) {
+      this.state.privateMessageForm.recipient_id = this.props.params.recipient_id;
+      let form: GetUserDetailsForm = {
+        user_id: this.state.privateMessageForm.recipient_id,
+        sort: SortType[SortType.New],
+        saved_only: false,
+      };
+      WebSocketService.Instance.getUserDetails(form);
+    }
+
+    this.subscription = WebSocketService.Instance.subject
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
+      .subscribe(
+        msg => this.parseMessage(msg),
+        err => console.error(err),
+        () => console.log('complete')
+      );
+  }
+
+  componentDidMount() {
+    autosize(document.querySelectorAll('textarea'));
+  }
+
+  componentWillUnmount() {
+    this.subscription.unsubscribe();
+  }
+
+  render() {
+    return (
+      <div>
+        <form onSubmit={linkEvent(this, this.handlePrivateMessageSubmit)}>
+          {!this.props.privateMessage && (
+            <div class="form-group row">
+              <label class="col-sm-2 col-form-label">
+                {capitalizeFirstLetter(i18n.t('to'))}
+              </label>
+
+              {this.state.recipient && (
+                <div class="col-sm-10 form-control-plaintext">
+                  <Link
+                    className="text-info"
+                    to={`/u/${this.state.recipient.name}`}
+                  >
+                    {this.state.recipient.avatar && showAvatars() && (
+                      <img
+                        height="32"
+                        width="32"
+                        src={pictshareAvatarThumbnail(
+                          this.state.recipient.avatar
+                        )}
+                        class="rounded-circle mr-1"
+                      />
+                    )}
+                    <span>{this.state.recipient.name}</span>
+                  </Link>
+                </div>
+              )}
+            </div>
+          )}
+          <div class="form-group row">
+            <label class="col-sm-2 col-form-label">{i18n.t('message')}</label>
+            <div class="col-sm-10">
+              <textarea
+                value={this.state.privateMessageForm.content}
+                onInput={linkEvent(this, this.handleContentChange)}
+                className={`form-control ${this.state.previewMode && 'd-none'}`}
+                rows={4}
+                maxLength={10000}
+              />
+              {this.state.previewMode && (
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(
+                    this.state.privateMessageForm.content
+                  )}
+                />
+              )}
+
+              {this.state.privateMessageForm.content && (
+                <button
+                  className={`mt-1 mr-2 btn btn-sm btn-secondary ${this.state
+                    .previewMode && 'active'}`}
+                  onClick={linkEvent(this, this.handlePreviewToggle)}
+                >
+                  {i18n.t('preview')}
+                </button>
+              )}
+              <ul class="float-right list-inline mb-1 text-muted small font-weight-bold">
+                <li class="list-inline-item">
+                  <span
+                    onClick={linkEvent(this, this.handleShowDisclaimer)}
+                    class="pointer"
+                  >
+                    {i18n.t('disclaimer')}
+                  </span>
+                </li>
+                <li class="list-inline-item">
+                  <a href={markdownHelpUrl} target="_blank" class="text-muted">
+                    {i18n.t('formatting_help')}
+                  </a>
+                </li>
+              </ul>
+            </div>
+          </div>
+
+          {this.state.showDisclaimer && (
+            <div class="form-group row">
+              <div class="col-sm-10">
+                <div class="alert alert-danger" role="alert">
+                  <T i18nKey="private_message_disclaimer">
+                    #
+                    <a
+                      class="alert-link"
+                      target="_blank"
+                      href="https://about.riot.im/"
+                    >
+                      #
+                    </a>
+                  </T>
+                </div>
+              </div>
+            </div>
+          )}
+          <div class="form-group row">
+            <div class="col-sm-10">
+              <button type="submit" class="btn btn-secondary mr-2">
+                {this.state.loading ? (
+                  <svg class="icon icon-spinner spin">
+                    <use xlinkHref="#icon-spinner"></use>
+                  </svg>
+                ) : this.props.privateMessage ? (
+                  capitalizeFirstLetter(i18n.t('save'))
+                ) : (
+                  capitalizeFirstLetter(i18n.t('send_message'))
+                )}
+              </button>
+              {this.props.privateMessage && (
+                <button
+                  type="button"
+                  class="btn btn-secondary"
+                  onClick={linkEvent(this, this.handleCancel)}
+                >
+                  {i18n.t('cancel')}
+                </button>
+              )}
+            </div>
+          </div>
+        </form>
+      </div>
+    );
+  }
+
+  handlePrivateMessageSubmit(i: PrivateMessageForm, event: any) {
+    event.preventDefault();
+    if (i.props.privateMessage) {
+      let editForm: EditPrivateMessageForm = {
+        edit_id: i.props.privateMessage.id,
+        content: i.state.privateMessageForm.content,
+      };
+      WebSocketService.Instance.editPrivateMessage(editForm);
+    } else {
+      WebSocketService.Instance.createPrivateMessage(
+        i.state.privateMessageForm
+      );
+    }
+    i.state.loading = true;
+    i.setState(i.state);
+  }
+
+  handleRecipientChange(i: PrivateMessageForm, event: any) {
+    i.state.recipient = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleContentChange(i: PrivateMessageForm, event: any) {
+    i.state.privateMessageForm.content = event.target.value;
+    i.setState(i.state);
+  }
+
+  handleCancel(i: PrivateMessageForm) {
+    i.props.onCancel();
+  }
+
+  handlePreviewToggle(i: PrivateMessageForm, event: any) {
+    event.preventDefault();
+    i.state.previewMode = !i.state.previewMode;
+    i.setState(i.state);
+  }
+
+  handleShowDisclaimer(i: PrivateMessageForm) {
+    i.state.showDisclaimer = !i.state.showDisclaimer;
+    i.setState(i.state);
+  }
+
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
+      this.state.loading = false;
+      this.setState(this.state);
+      return;
+    } else if (res.op == UserOperation.EditPrivateMessage) {
+      let data = res.data as PrivateMessageResponse;
+      this.state.loading = false;
+      this.props.onEdit(data.message);
+    } else if (res.op == UserOperation.GetUserDetails) {
+      let data = res.data as UserDetailsResponse;
+      this.state.recipient = data.user;
+      this.state.privateMessageForm.recipient_id = data.user.id;
+      this.setState(this.state);
+    } else if (res.op == UserOperation.CreatePrivateMessage) {
+      let data = res.data as PrivateMessageResponse;
+      this.state.loading = false;
+      this.props.onCreate(data.message);
+      this.setState(this.state);
+    }
+  }
+}
diff --git a/ui/src/components/private-message.tsx b/ui/src/components/private-message.tsx
new file mode 100644 (file)
index 0000000..5b6748d
--- /dev/null
@@ -0,0 +1,254 @@
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import {
+  PrivateMessage as PrivateMessageI,
+  EditPrivateMessageForm,
+} from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import {
+  mdToHtml,
+  pictshareAvatarThumbnail,
+  showAvatars,
+  toast,
+} from '../utils';
+import { MomentTime } from './moment-time';
+import { PrivateMessageForm } from './private-message-form';
+import { i18n } from '../i18next';
+import { T } from 'inferno-i18next';
+
+interface PrivateMessageState {
+  showReply: boolean;
+  showEdit: boolean;
+  collapsed: boolean;
+  viewSource: boolean;
+}
+
+interface PrivateMessageProps {
+  privateMessage: PrivateMessageI;
+}
+
+export class PrivateMessage extends Component<
+  PrivateMessageProps,
+  PrivateMessageState
+> {
+  private emptyState: PrivateMessageState = {
+    showReply: false,
+    showEdit: false,
+    collapsed: false,
+    viewSource: false,
+  };
+
+  constructor(props: any, context: any) {
+    super(props, context);
+
+    this.state = this.emptyState;
+    this.handleReplyCancel = this.handleReplyCancel.bind(this);
+    this.handlePrivateMessageCreate = this.handlePrivateMessageCreate.bind(
+      this
+    );
+    this.handlePrivateMessageEdit = this.handlePrivateMessageEdit.bind(this);
+  }
+
+  get mine(): boolean {
+    return UserService.Instance.user.id == this.props.privateMessage.creator_id;
+  }
+
+  render() {
+    let message = this.props.privateMessage;
+    return (
+      <div class="mb-2">
+        <div>
+          <ul class="list-inline mb-0 text-muted small">
+            <li className="list-inline-item">
+              {this.mine ? i18n.t('to') : i18n.t('from')}
+            </li>
+            <li className="list-inline-item">
+              <Link
+                className="text-info"
+                to={
+                  this.mine
+                    ? `/u/${message.recipient_name}`
+                    : `/u/${message.creator_name}`
+                }
+              >
+                {(this.mine
+                  ? message.recipient_avatar
+                  : message.creator_avatar) &&
+                  showAvatars() && (
+                    <img
+                      height="32"
+                      width="32"
+                      src={pictshareAvatarThumbnail(
+                        this.mine
+                          ? message.recipient_avatar
+                          : message.creator_avatar
+                      )}
+                      class="rounded-circle mr-1"
+                    />
+                  )}
+                <span>
+                  {this.mine ? message.recipient_name : message.creator_name}
+                </span>
+              </Link>
+            </li>
+            <li className="list-inline-item">
+              <span>
+                <MomentTime data={message} />
+              </span>
+            </li>
+            <li className="list-inline-item">
+              <div
+                className="pointer text-monospace"
+                onClick={linkEvent(this, this.handleMessageCollapse)}
+              >
+                {this.state.collapsed ? '[+]' : '[-]'}
+              </div>
+            </li>
+          </ul>
+          {this.state.showEdit && (
+            <PrivateMessageForm
+              privateMessage={message}
+              onEdit={this.handlePrivateMessageEdit}
+              onCancel={this.handleReplyCancel}
+            />
+          )}
+          {!this.state.showEdit && !this.state.collapsed && (
+            <div>
+              {this.state.viewSource ? (
+                <pre>{this.messageUnlessRemoved}</pre>
+              ) : (
+                <div
+                  className="md-div"
+                  dangerouslySetInnerHTML={mdToHtml(this.messageUnlessRemoved)}
+                />
+              )}
+              <ul class="list-inline mb-1 text-muted small font-weight-bold">
+                {!this.mine && (
+                  <>
+                    <li className="list-inline-item">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleMarkRead)}
+                      >
+                        {message.read
+                          ? i18n.t('mark_as_unread')
+                          : i18n.t('mark_as_read')}
+                      </span>
+                    </li>
+                    <li className="list-inline-item">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleReplyClick)}
+                      >
+                        <T i18nKey="reply">#</T>
+                      </span>
+                    </li>
+                  </>
+                )}
+                {this.mine && (
+                  <>
+                    <li className="list-inline-item">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleEditClick)}
+                      >
+                        <T i18nKey="edit">#</T>
+                      </span>
+                    </li>
+                    <li className="list-inline-item">
+                      <span
+                        class="pointer"
+                        onClick={linkEvent(this, this.handleDeleteClick)}
+                      >
+                        {!message.deleted
+                          ? i18n.t('delete')
+                          : i18n.t('restore')}
+                      </span>
+                    </li>
+                  </>
+                )}
+                <li className="list-inline-item">•</li>
+                <li className="list-inline-item">
+                  <span
+                    className="pointer"
+                    onClick={linkEvent(this, this.handleViewSource)}
+                  >
+                    <T i18nKey="view_source">#</T>
+                  </span>
+                </li>
+              </ul>
+            </div>
+          )}
+        </div>
+        {this.state.showReply && (
+          <PrivateMessageForm
+            params={{
+              recipient_id: this.props.privateMessage.creator_id,
+            }}
+            onCreate={this.handlePrivateMessageCreate}
+          />
+        )}
+        {/* A collapsed clearfix */}
+        {this.state.collapsed && <div class="row col-12"></div>}
+      </div>
+    );
+  }
+
+  get messageUnlessRemoved(): string {
+    let message = this.props.privateMessage;
+    return message.deleted ? `*${i18n.t('deleted')}*` : message.content;
+  }
+
+  handleReplyClick(i: PrivateMessage) {
+    i.state.showReply = true;
+    i.setState(i.state);
+  }
+
+  handleEditClick(i: PrivateMessage) {
+    i.state.showEdit = true;
+    i.setState(i.state);
+  }
+
+  handleDeleteClick(i: PrivateMessage) {
+    let form: EditPrivateMessageForm = {
+      edit_id: i.props.privateMessage.id,
+      deleted: !i.props.privateMessage.deleted,
+    };
+    WebSocketService.Instance.editPrivateMessage(form);
+  }
+
+  handleReplyCancel() {
+    this.state.showReply = false;
+    this.state.showEdit = false;
+    this.setState(this.state);
+  }
+
+  handleMarkRead(i: PrivateMessage) {
+    let form: EditPrivateMessageForm = {
+      edit_id: i.props.privateMessage.id,
+      read: !i.props.privateMessage.read,
+    };
+    WebSocketService.Instance.editPrivateMessage(form);
+  }
+
+  handleMessageCollapse(i: PrivateMessage) {
+    i.state.collapsed = !i.state.collapsed;
+    i.setState(i.state);
+  }
+
+  handleViewSource(i: PrivateMessage) {
+    i.state.viewSource = !i.state.viewSource;
+    i.setState(i.state);
+  }
+
+  handlePrivateMessageEdit() {
+    this.state.showEdit = false;
+    this.setState(this.state);
+  }
+
+  handlePrivateMessageCreate() {
+    this.state.showReply = false;
+    this.setState(this.state);
+    toast(i18n.t('message_sent'));
+  }
+}
index 94bbbdb9d2c53d0234abf0ca60e32ab5ad317973..18b5d34199a0b8e5dc3fe8a504479451d96fd0b6 100644 (file)
@@ -14,15 +14,17 @@ import {
   SearchType,
   CreatePostLikeResponse,
   CommentResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService } from '../services';
 import {
-  msgOp,
+  wsJsonToRes,
   fetchLimit,
   routeSearchTypeToEnum,
   routeSortTypeToEnum,
   pictshareAvatarThumbnail,
   showAvatars,
+  toast,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { SortSelect } from './sort-select';
@@ -47,7 +49,6 @@ export class Search extends Component<any, SearchState> {
     sort: this.getSortTypeFromProps(this.props),
     page: this.getPageFromProps(this.props),
     searchResponse: {
-      op: null,
       type_: null,
       posts: [],
       comments: [],
@@ -400,7 +401,6 @@ export class Search extends Component<any, SearchState> {
     return (
       <div>
         {res &&
-          res.op &&
           res.posts.length == 0 &&
           res.comments.length == 0 &&
           res.communities.length == 0 &&
@@ -476,44 +476,44 @@ export class Search extends Component<any, SearchState> {
     );
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       return;
-    } else if (op == UserOperation.Search) {
-      let res: SearchResponse = msg;
-      this.state.searchResponse = res;
+    } else if (res.op == UserOperation.Search) {
+      let data = res.data as SearchResponse;
+      this.state.searchResponse = data;
       this.state.loading = false;
       document.title = `${i18n.t('search')} - ${this.state.q} - ${
         WebSocketService.Instance.site.name
       }`;
       window.scrollTo(0, 0);
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let res: CommentResponse = msg;
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
       let found: Comment = this.state.searchResponse.comments.find(
-        c => c.id === res.comment.id
+        c => c.id === data.comment.id
       );
-      found.score = res.comment.score;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      if (res.comment.my_vote !== null) {
-        found.my_vote = res.comment.my_vote;
+      found.score = data.comment.score;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      if (data.comment.my_vote !== null) {
+        found.my_vote = data.comment.my_vote;
         found.upvoteLoading = false;
         found.downvoteLoading = false;
       }
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let res: CreatePostLikeResponse = msg;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as CreatePostLikeResponse;
       let found = this.state.searchResponse.posts.find(
-        c => c.id == res.post.id
+        c => c.id == data.post.id
       );
-      found.my_vote = res.post.my_vote;
-      found.score = res.post.score;
-      found.upvotes = res.post.upvotes;
-      found.downvotes = res.post.downvotes;
+      found.my_vote = data.post.my_vote;
+      found.score = data.post.score;
+      found.upvotes = data.post.upvotes;
+      found.downvotes = data.post.downvotes;
       this.setState(this.state);
     }
   }
index d421e46f8decabdeaea8f477e0ca18bf7b9320d5..26475a387ea72144f2685a2cd5f6b700c4505616 100644 (file)
@@ -1,9 +1,14 @@
 import { Component, linkEvent } from 'inferno';
 import { Subscription } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { RegisterForm, LoginResponse, UserOperation } from '../interfaces';
+import {
+  RegisterForm,
+  LoginResponse,
+  UserOperation,
+  WebSocketJsonResponse,
+} from '../interfaces';
 import { WebSocketService, UserService } from '../services';
-import { msgOp } from '../utils';
+import { wsJsonToRes, toast } from '../utils';
 import { SiteForm } from './site-form';
 import { i18n } from '../i18next';
 import { T } from 'inferno-i18next';
@@ -35,14 +40,7 @@ export class Setup extends Component<any, State> {
     this.state = this.emptyState;
 
     this.subscription = WebSocketService.Instance.subject
-      .pipe(
-        retryWhen(errors =>
-          errors.pipe(
-            delay(3000),
-            take(10)
-          )
-        )
-      )
+      .pipe(retryWhen(errors => errors.pipe(delay(3000), take(10))))
       .subscribe(
         msg => this.parseMessage(msg),
         err => console.error(err),
@@ -188,21 +186,20 @@ export class Setup extends Component<any, State> {
     i.setState(i.state);
   }
 
-  parseMessage(msg: any) {
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+  parseMessage(msg: WebSocketJsonResponse) {
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.state.userLoading = false;
       this.setState(this.state);
       return;
-    } else if (op == UserOperation.Register) {
+    } else if (res.op == UserOperation.Register) {
+      let data = res.data as LoginResponse;
       this.state.userLoading = false;
       this.state.doneRegisteringUser = true;
-      let res: LoginResponse = msg;
-      UserService.Instance.login(res);
-      console.log(res);
+      UserService.Instance.login(data);
       this.setState(this.state);
-    } else if (op == UserOperation.CreateSite) {
+    } else if (res.op == UserOperation.CreateSite) {
       this.props.history.push('/');
     }
   }
index 206fb8ff26c9a1d81d22b221c8f2a806c5777a99..09129d67ca06365be01b92e362d624df48b344c1 100644 (file)
@@ -19,10 +19,11 @@ import {
   AddAdminResponse,
   DeleteAccountForm,
   CreatePostLikeResponse,
+  WebSocketJsonResponse,
 } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import {
-  msgOp,
+  wsJsonToRes,
   fetchLimit,
   routeSortTypeToEnum,
   capitalizeFirstLetter,
@@ -30,6 +31,7 @@ import {
   setTheme,
   languages,
   showAvatars,
+  toast,
 } from '../utils';
 import { PostListing } from './post-listing';
 import { SortSelect } from './sort-select';
@@ -405,13 +407,30 @@ export class User extends Component<any, UserState> {
                 </tr>
               </table>
             </div>
-            {this.isCurrentUser && (
+            {this.isCurrentUser ? (
               <button
                 class="btn btn-block btn-secondary mt-3"
                 onClick={linkEvent(this, this.handleLogoutClick)}
               >
                 <T i18nKey="logout">#</T>
               </button>
+            ) : (
+              <>
+                <a
+                  className={`btn btn-block btn-secondary mt-3 ${!this.state
+                    .user.matrix_user_id && 'disabled'}`}
+                  target="_blank"
+                  href={`https://matrix.to/#/${this.state.user.matrix_user_id}`}
+                >
+                  {i18n.t('send_secure_message')}
+                </a>
+                <Link
+                  class="btn btn-block btn-secondary mt-3"
+                  to={`/create_private_message?recipient_id=${this.state.user.id}`}
+                >
+                  {i18n.t('send_message')}
+                </Link>
+              </>
             )}
           </div>
         </div>
@@ -539,6 +558,26 @@ export class User extends Component<any, UserState> {
                   />
                 </div>
               </div>
+              <div class="form-group row">
+                <label class="col-lg-5 col-form-label">
+                  <a href="https://about.riot.im/" target="_blank">
+                    {i18n.t('matrix_user_id')}
+                  </a>
+                </label>
+                <div class="col-lg-7">
+                  <input
+                    type="text"
+                    class="form-control"
+                    placeholder="@user:example.com"
+                    value={this.state.userSettingsForm.matrix_user_id}
+                    onInput={linkEvent(
+                      this,
+                      this.handleUserSettingsMatrixUserIdChange
+                    )}
+                    minLength={3}
+                  />
+                </div>
+              </div>
               <div class="form-group row">
                 <label class="col-lg-5 col-form-label">
                   <T i18nKey="new_password">#</T>
@@ -875,6 +914,17 @@ export class User extends Component<any, UserState> {
     i.setState(i.state);
   }
 
+  handleUserSettingsMatrixUserIdChange(i: User, event: any) {
+    i.state.userSettingsForm.matrix_user_id = event.target.value;
+    if (
+      i.state.userSettingsForm.matrix_user_id == '' &&
+      !i.state.user.matrix_user_id
+    ) {
+      i.state.userSettingsForm.matrix_user_id = undefined;
+    }
+    i.setState(i.state);
+  }
+
   handleUserSettingsNewPasswordChange(i: User, event: any) {
     i.state.userSettingsForm.new_password = event.target.value;
     if (i.state.userSettingsForm.new_password == '') {
@@ -927,7 +977,7 @@ export class User extends Component<any, UserState> {
       .catch(error => {
         i.state.avatarLoading = false;
         i.setState(i.state);
-        alert(error);
+        toast(error, 'danger');
       });
   }
 
@@ -963,27 +1013,27 @@ export class User extends Component<any, UserState> {
     WebSocketService.Instance.deleteAccount(i.state.deleteAccountForm);
   }
 
-  parseMessage(msg: any) {
+  parseMessage(msg: WebSocketJsonResponse) {
     console.log(msg);
-    let op: UserOperation = msgOp(msg);
-    if (msg.error) {
-      alert(i18n.t(msg.error));
+    let res = wsJsonToRes(msg);
+    if (res.error) {
+      toast(i18n.t(msg.error), 'danger');
       this.state.deleteAccountLoading = false;
       this.state.avatarLoading = false;
       this.state.userSettingsLoading = false;
-      if (msg.error == 'couldnt_find_that_username_or_email') {
+      if (res.error == 'couldnt_find_that_username_or_email') {
         this.context.router.history.push('/');
       }
       this.setState(this.state);
       return;
-    } else if (op == UserOperation.GetUserDetails) {
-      let res: UserDetailsResponse = msg;
-      this.state.user = res.user;
-      this.state.comments = res.comments;
-      this.state.follows = res.follows;
-      this.state.moderates = res.moderates;
-      this.state.posts = res.posts;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.GetUserDetails) {
+      let data = res.data as UserDetailsResponse;
+      this.state.user = data.user;
+      this.state.comments = data.comments;
+      this.state.follows = data.follows;
+      this.state.moderates = data.moderates;
+      this.state.posts = data.posts;
+      this.state.admins = data.admins;
       this.state.loading = false;
       if (this.isCurrentUser) {
         this.state.userSettingsForm.show_nsfw =
@@ -1001,71 +1051,72 @@ export class User extends Component<any, UserState> {
         this.state.userSettingsForm.send_notifications_to_email = this.state.user.send_notifications_to_email;
         this.state.userSettingsForm.show_avatars =
           UserService.Instance.user.show_avatars;
+        this.state.userSettingsForm.matrix_user_id = this.state.user.matrix_user_id;
       }
       document.title = `/u/${this.state.user.name} - ${WebSocketService.Instance.site.name}`;
       window.scrollTo(0, 0);
       this.setState(this.state);
-    } else if (op == UserOperation.EditComment) {
-      let res: CommentResponse = msg;
+    } else if (res.op == UserOperation.EditComment) {
+      let data = res.data as CommentResponse;
 
-      let found = this.state.comments.find(c => c.id == res.comment.id);
-      found.content = res.comment.content;
-      found.updated = res.comment.updated;
-      found.removed = res.comment.removed;
-      found.deleted = res.comment.deleted;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      found.score = res.comment.score;
+      let found = this.state.comments.find(c => c.id == data.comment.id);
+      found.content = data.comment.content;
+      found.updated = data.comment.updated;
+      found.removed = data.comment.removed;
+      found.deleted = data.comment.deleted;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      found.score = data.comment.score;
 
       this.setState(this.state);
-    } else if (op == UserOperation.CreateComment) {
+    } else if (res.op == UserOperation.CreateComment) {
       // let res: CommentResponse = msg;
-      alert(i18n.t('reply_sent'));
+      toast(i18n.t('reply_sent'));
       // this.state.comments.unshift(res.comment); // TODO do this right
       // this.setState(this.state);
-    } else if (op == UserOperation.SaveComment) {
-      let res: CommentResponse = msg;
-      let found = this.state.comments.find(c => c.id == res.comment.id);
-      found.saved = res.comment.saved;
+    } else if (res.op == UserOperation.SaveComment) {
+      let data = res.data as CommentResponse;
+      let found = this.state.comments.find(c => c.id == data.comment.id);
+      found.saved = data.comment.saved;
       this.setState(this.state);
-    } else if (op == UserOperation.CreateCommentLike) {
-      let res: CommentResponse = msg;
+    } else if (res.op == UserOperation.CreateCommentLike) {
+      let data = res.data as CommentResponse;
       let found: Comment = this.state.comments.find(
-        c => c.id === res.comment.id
+        c => c.id === data.comment.id
       );
-      found.score = res.comment.score;
-      found.upvotes = res.comment.upvotes;
-      found.downvotes = res.comment.downvotes;
-      if (res.comment.my_vote !== null) found.my_vote = res.comment.my_vote;
+      found.score = data.comment.score;
+      found.upvotes = data.comment.upvotes;
+      found.downvotes = data.comment.downvotes;
+      if (data.comment.my_vote !== null) found.my_vote = data.comment.my_vote;
       this.setState(this.state);
-    } else if (op == UserOperation.CreatePostLike) {
-      let res: CreatePostLikeResponse = msg;
-      let found = this.state.posts.find(c => c.id == res.post.id);
-      found.my_vote = res.post.my_vote;
-      found.score = res.post.score;
-      found.upvotes = res.post.upvotes;
-      found.downvotes = res.post.downvotes;
+    } else if (res.op == UserOperation.CreatePostLike) {
+      let data = res.data as CreatePostLikeResponse;
+      let found = this.state.posts.find(c => c.id == data.post.id);
+      found.my_vote = data.post.my_vote;
+      found.score = data.post.score;
+      found.upvotes = data.post.upvotes;
+      found.downvotes = data.post.downvotes;
       this.setState(this.state);
-    } else if (op == UserOperation.BanUser) {
-      let res: BanUserResponse = msg;
+    } else if (res.op == UserOperation.BanUser) {
+      let data = res.data as BanUserResponse;
       this.state.comments
-        .filter(c => c.creator_id == res.user.id)
-        .forEach(c => (c.banned = res.banned));
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
       this.state.posts
-        .filter(c => c.creator_id == res.user.id)
-        .forEach(c => (c.banned = res.banned));
+        .filter(c => c.creator_id == data.user.id)
+        .forEach(c => (c.banned = data.banned));
       this.setState(this.state);
-    } else if (op == UserOperation.AddAdmin) {
-      let res: AddAdminResponse = msg;
-      this.state.admins = res.admins;
+    } else if (res.op == UserOperation.AddAdmin) {
+      let data = res.data as AddAdminResponse;
+      this.state.admins = data.admins;
       this.setState(this.state);
-    } else if (op == UserOperation.SaveUserSettings) {
+    } else if (res.op == UserOperation.SaveUserSettings) {
+      let data = res.data as LoginResponse;
       this.state = this.emptyState;
       this.state.userSettingsLoading = false;
       this.setState(this.state);
-      let res: LoginResponse = msg;
-      UserService.Instance.login(res);
-    } else if (op == UserOperation.DeleteAccount) {
+      UserService.Instance.login(data);
+    } else if (res.op == UserOperation.DeleteAccount) {
       this.state.deleteAccountLoading = false;
       this.state.deleteAccountShowConfirm = false;
       this.setState(this.state);
index 933cdc68486be90d1202af8541ae6a6f643f376f..122783d4156e9612d9e2b03200efd28a6fee5c4f 100644 (file)
@@ -13,6 +13,7 @@
 
   <!-- Styles -->
   <link rel="stylesheet" type="text/css" href="/static/assets/css/tribute.css" />
+  <link rel="stylesheet" type="text/css" href="/static/assets/css/toastify.css" />
   <link rel="stylesheet" type="text/css" href="/static/assets/css/themes/darkly.min.css" id="darkly" />
   <link rel="stylesheet" type="text/css" href="/static/assets/css/main.css" />
 
index 5eeb3038b13380a561a74f79fcf0ba0820c114da..8a9aa3c387feced6c3540371775b0ec4176eaa63 100644 (file)
@@ -7,6 +7,7 @@ import { Footer } from './components/footer';
 import { Login } from './components/login';
 import { CreatePost } from './components/create-post';
 import { CreateCommunity } from './components/create-community';
+import { CreatePrivateMessage } from './components/create-private-message';
 import { PasswordChange } from './components/password_change';
 import { Post } from './components/post';
 import { Community } from './components/community';
@@ -46,6 +47,10 @@ class Index extends Component<any, any> {
               <Route path={`/login`} component={Login} />
               <Route path={`/create_post`} component={CreatePost} />
               <Route path={`/create_community`} component={CreateCommunity} />
+              <Route
+                path={`/create_private_message`}
+                component={CreatePrivateMessage}
+              />
               <Route path={`/communities/page/:page`} component={Communities} />
               <Route path={`/communities`} component={Communities} />
               <Route path={`/post/:id/comment/:comment_id`} component={Post} />
index c21d3d264983c0bd739bd7e44c5908bd4dd69923..bd954d20a4e1e2fdb2ee98ac5268c2d8fb6e255c 100644 (file)
@@ -38,6 +38,9 @@ export enum UserOperation {
   DeleteAccount,
   PasswordReset,
   PasswordChange,
+  CreatePrivateMessage,
+  EditPrivateMessage,
+  GetPrivateMessages,
 }
 
 export enum CommentSortType {
@@ -89,6 +92,7 @@ export interface UserView {
   name: string;
   avatar?: string;
   email?: string;
+  matrix_user_id?: string;
   fedi_name: string;
   published: string;
   number_of_posts: number;
@@ -218,6 +222,21 @@ export interface Site {
   enable_nsfw: boolean;
 }
 
+export interface PrivateMessage {
+  id: number;
+  creator_id: number;
+  recipient_id: number;
+  content: string;
+  deleted: boolean;
+  read: boolean;
+  published: string;
+  updated?: string;
+  creator_name: string;
+  creator_avatar?: string;
+  recipient_name: string;
+  recipient_avatar?: string;
+}
+
 export enum BanType {
   Community,
   Site,
@@ -230,7 +249,6 @@ export interface FollowCommunityForm {
 }
 
 export interface GetFollowedCommunitiesResponse {
-  op: string;
   communities: Array<CommunityUser>;
 }
 
@@ -245,7 +263,6 @@ export interface GetUserDetailsForm {
 }
 
 export interface UserDetailsResponse {
-  op: string;
   user: UserView;
   follows: Array<CommunityUser>;
   moderates: Array<CommunityUser>;
@@ -263,7 +280,6 @@ export interface GetRepliesForm {
 }
 
 export interface GetRepliesResponse {
-  op: string;
   replies: Array<Comment>;
 }
 
@@ -276,7 +292,6 @@ export interface GetUserMentionsForm {
 }
 
 export interface GetUserMentionsResponse {
-  op: string;
   mentions: Array<Comment>;
 }
 
@@ -287,7 +302,6 @@ export interface EditUserMentionForm {
 }
 
 export interface UserMentionResponse {
-  op: string;
   mention: Comment;
 }
 
@@ -301,7 +315,6 @@ export interface BanFromCommunityForm {
 }
 
 export interface BanFromCommunityResponse {
-  op: string;
   user: UserView;
   banned: boolean;
 }
@@ -325,7 +338,6 @@ export interface TransferSiteForm {
 }
 
 export interface AddModToCommunityResponse {
-  op: string;
   moderators: Array<CommunityUser>;
 }
 
@@ -337,7 +349,6 @@ export interface GetModlogForm {
 }
 
 export interface GetModlogResponse {
-  op: string;
   removed_posts: Array<ModRemovePost>;
   locked_posts: Array<ModLockPost>;
   stickied_posts: Array<ModStickyPost>;
@@ -478,7 +489,6 @@ export interface RegisterForm {
 }
 
 export interface LoginResponse {
-  op: string;
   jwt: string;
 }
 
@@ -490,6 +500,7 @@ export interface UserSettingsForm {
   lang: string;
   avatar?: string;
   email?: string;
+  matrix_user_id?: string;
   new_password?: string;
   new_password_verify?: string;
   old_password?: string;
@@ -513,14 +524,12 @@ export interface CommunityForm {
 }
 
 export interface GetCommunityResponse {
-  op: string;
   community: Community;
   moderators: Array<CommunityUser>;
   admins: Array<UserView>;
 }
 
 export interface CommunityResponse {
-  op: string;
   community: Community;
 }
 
@@ -532,12 +541,10 @@ export interface ListCommunitiesForm {
 }
 
 export interface ListCommunitiesResponse {
-  op: string;
   communities: Array<Community>;
 }
 
 export interface ListCategoriesResponse {
-  op: string;
   categories: Array<Category>;
 }
 
@@ -566,7 +573,6 @@ export interface PostFormParams {
 }
 
 export interface GetPostResponse {
-  op: string;
   post: Post;
   comments: Array<Comment>;
   community: Community;
@@ -581,7 +587,6 @@ export interface SavePostForm {
 }
 
 export interface PostResponse {
-  op: string;
   post: Post;
 }
 
@@ -605,7 +610,6 @@ export interface SaveCommentForm {
 }
 
 export interface CommentResponse {
-  op: string;
   comment: Comment;
 }
 
@@ -631,7 +635,6 @@ export interface GetPostsForm {
 }
 
 export interface GetPostsResponse {
-  op: string;
   posts: Array<Post>;
 }
 
@@ -642,7 +645,6 @@ export interface CreatePostLikeForm {
 }
 
 export interface CreatePostLikeResponse {
-  op: string;
   post: Post;
 }
 
@@ -656,7 +658,6 @@ export interface SiteForm {
 }
 
 export interface GetSiteResponse {
-  op: string;
   site: Site;
   admins: Array<UserView>;
   banned: Array<UserView>;
@@ -664,7 +665,6 @@ export interface GetSiteResponse {
 }
 
 export interface SiteResponse {
-  op: string;
   site: Site;
 }
 
@@ -677,7 +677,6 @@ export interface BanUserForm {
 }
 
 export interface BanUserResponse {
-  op: string;
   user: UserView;
   banned: boolean;
 }
@@ -689,7 +688,6 @@ export interface AddAdminForm {
 }
 
 export interface AddAdminResponse {
-  op: string;
   admins: Array<UserView>;
 }
 
@@ -704,7 +702,6 @@ export interface SearchForm {
 }
 
 export interface SearchResponse {
-  op: string;
   type_: string;
   posts?: Array<Post>;
   comments?: Array<Comment>;
@@ -720,12 +717,78 @@ export interface PasswordResetForm {
   email: string;
 }
 
-export interface PasswordResetResponse {
-  op: string;
-}
+// export interface PasswordResetResponse {
+// }
 
 export interface PasswordChangeForm {
   token: string;
   password: string;
   password_verify: string;
 }
+
+export interface PrivateMessageForm {
+  content: string;
+  recipient_id: number;
+  auth?: string;
+}
+
+export interface PrivateMessageFormParams {
+  recipient_id: number;
+}
+
+export interface EditPrivateMessageForm {
+  edit_id: number;
+  content?: string;
+  deleted?: boolean;
+  read?: boolean;
+  auth?: string;
+}
+
+export interface GetPrivateMessagesForm {
+  unread_only: boolean;
+  page?: number;
+  limit?: number;
+  auth?: string;
+}
+
+export interface PrivateMessagesResponse {
+  messages: Array<PrivateMessage>;
+}
+
+export interface PrivateMessageResponse {
+  message: PrivateMessage;
+}
+
+type ResponseType =
+  | SiteResponse
+  | GetFollowedCommunitiesResponse
+  | ListCommunitiesResponse
+  | GetPostsResponse
+  | CreatePostLikeResponse
+  | GetRepliesResponse
+  | GetUserMentionsResponse
+  | ListCategoriesResponse
+  | CommunityResponse
+  | CommentResponse
+  | UserMentionResponse
+  | LoginResponse
+  | GetModlogResponse
+  | SearchResponse
+  | BanFromCommunityResponse
+  | AddModToCommunityResponse
+  | BanUserResponse
+  | AddAdminResponse
+  | PrivateMessageResponse
+  | PrivateMessagesResponse;
+
+export interface WebSocketResponse {
+  op: UserOperation;
+  data: ResponseType;
+  error?: string;
+}
+
+export interface WebSocketJsonResponse {
+  op: string;
+  data: ResponseType;
+  error?: string;
+}
index a70fb2deff2acd399abb3d53a05a4fb19a6729a7..e72a28716ffdc8d1d17876aa7f40dc1829e2acf0 100644 (file)
@@ -32,12 +32,16 @@ import {
   DeleteAccountForm,
   PasswordResetForm,
   PasswordChangeForm,
+  PrivateMessageForm,
+  EditPrivateMessageForm,
+  GetPrivateMessagesForm,
 } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
-import { retryWhen, delay, take } from 'rxjs/operators';
+import { retryWhen, delay } from 'rxjs/operators';
 import { UserService } from './';
 import { i18n } from '../i18next';
+import { toast } from '../utils';
 
 export class WebSocketService {
   private static _instance: WebSocketService;
@@ -285,6 +289,27 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.PasswordChange, form));
   }
 
+  public createPrivateMessage(form: PrivateMessageForm) {
+    this.setAuth(form);
+    this.subject.next(
+      this.wsSendWrapper(UserOperation.CreatePrivateMessage, form)
+    );
+  }
+
+  public editPrivateMessage(form: EditPrivateMessageForm) {
+    this.setAuth(form);
+    this.subject.next(
+      this.wsSendWrapper(UserOperation.EditPrivateMessage, form)
+    );
+  }
+
+  public getPrivateMessages(form: GetPrivateMessagesForm) {
+    this.setAuth(form);
+    this.subject.next(
+      this.wsSendWrapper(UserOperation.GetPrivateMessages, form)
+    );
+  }
+
   private wsSendWrapper(op: UserOperation, data: any) {
     let send = { op: UserOperation[op], data: data };
     console.log(send);
@@ -294,7 +319,7 @@ export class WebSocketService {
   private setAuth(obj: any, throwErr: boolean = true) {
     obj.auth = UserService.Instance.auth;
     if (obj.auth == null && throwErr) {
-      alert(i18n.t('not_logged_in'));
+      toast(i18n.t('not_logged_in'), 'danger');
       throw 'Not logged in';
     }
   }
index be5f9e736d0d4c4f95242e449ef24f48f37bb7cb..c932014f84f33431ab1ca244e9b962fd21a6ebeb 100644 (file)
@@ -23,6 +23,10 @@ export const en = {
     list_of_communities: 'List of communities',
     number_of_communities: '{{count}} Communities',
     community_reqs: 'lowercase, underscores, and no spaces.',
+    create_private_message: 'Create Private Message',
+    send_secure_message: 'Send Secure Message',
+    send_message: 'Send Message',
+    message: 'Message',
     edit: 'edit',
     reply: 'reply',
     cancel: 'Cancel',
@@ -109,6 +113,7 @@ export const en = {
     replies: 'Replies',
     mentions: 'Mentions',
     reply_sent: 'Reply sent',
+    message_sent: 'Message sent',
     search: 'Search',
     overview: 'Overview',
     view: 'View',
@@ -119,6 +124,7 @@ export const en = {
     notifications_error:
       'Desktop notifications not available in your browser. Try Firefox or Chrome.',
     unread_messages: 'Unread Messages',
+    messages: 'Messages',
     password: 'Password',
     verify_password: 'Verify Password',
     old_password: 'Old Password',
@@ -128,6 +134,9 @@ export const en = {
     new_password: 'New Password',
     no_email_setup: "This server hasn't correctly set up email.",
     email: 'Email',
+    matrix_user_id: 'Matrix User',
+    private_message_disclaimer:
+      'Warning: Private messages in Lemmy are not secure. Please create an account on <1>Riot.im</1> for secure messaging.',
     send_notifications_to_email: 'Send notifications to Email',
     optional: 'Optional',
     expires: 'Expires',
@@ -172,6 +181,7 @@ export const en = {
     joined: 'Joined',
     by: 'by',
     to: 'to',
+    from: 'from',
     transfer_community: 'transfer community',
     transfer_site: 'transfer site',
     are_you_sure: 'are you sure?',
@@ -181,6 +191,7 @@ export const en = {
     landing_0:
       "Lemmy is a <1>link aggregator</1> / reddit alternative, intended to work in the <2>fediverse</2>.<3></3>It's self-hostable, has live-updating comment threads, and is tiny (<4>~80kB</4>). Federation into the ActivityPub network is on the roadmap. <5></5>This is a <6>very early beta version</6>, and a lot of features are currently broken or missing. <7></7>Suggest new features or report bugs <8>here.</8><9></9>Made with <10>Rust</10>, <11>Actix</11>, <12>Inferno</12>, <13>Typescript</13>.",
     not_logged_in: 'Not logged in.',
+    logged_in: 'Logged in.',
     community_ban: 'You have been banned from this community.',
     site_ban: 'You have been banned from the site',
     couldnt_create_comment: "Couldn't create comment.",
@@ -215,5 +226,8 @@ export const en = {
     email_already_exists: 'Email already exists.',
     couldnt_update_user: "Couldn't update user.",
     system_err_login: 'System error. Try logging out and back in.',
+    couldnt_create_private_message: "Couldn't create private message.",
+    no_private_message_edit_allowed: 'Not allowed to edit private message.',
+    couldnt_update_private_message: "Couldn't update private message.",
   },
 };
index a90afbd8bdccde0e58db833d42664d2379610c0c..0e4cd9d55e076c7f31edbd78b5e1dda75e4b8978 100644 (file)
@@ -11,10 +11,13 @@ import 'moment/locale/it';
 import {
   UserOperation,
   Comment,
+  PrivateMessage,
   User,
   SortType,
   ListingType,
   SearchType,
+  WebSocketResponse,
+  WebSocketJsonResponse,
 } from './interfaces';
 import { UserService } from './services/UserService';
 import markdown_it from 'markdown-it';
@@ -22,6 +25,7 @@ import markdownitEmoji from 'markdown-it-emoji/light';
 import markdown_it_container from 'markdown-it-container';
 import * as twemoji from 'twemoji';
 import * as emojiShortName from 'emoji-short-name';
+import Toastify from 'toastify-js';
 
 export const repoUrl = 'https://github.com/dessalines/lemmy';
 export const markdownHelpUrl = 'https://commonmark.org/help/';
@@ -38,9 +42,12 @@ export function randomStr() {
     .substr(2, 10);
 }
 
-export function msgOp(msg: any): UserOperation {
+export function wsJsonToRes(msg: WebSocketJsonResponse): WebSocketResponse {
   let opStr: string = msg.op;
-  return UserOperation[opStr];
+  return {
+    op: UserOperation[opStr],
+    data: msg.data,
+  };
 }
 
 export const md = new markdown_it({
@@ -361,3 +368,15 @@ export function imageThumbnailer(url: string): string {
     return url;
   }
 }
+
+export function isCommentType(item: Comment | PrivateMessage): item is Comment {
+  return (item as Comment).community_id !== undefined;
+}
+
+export function toast(text: string, background: string = 'success') {
+  let backgroundColor = `var(--${background})`;
+  Toastify({
+    text: text,
+    backgroundColor: backgroundColor,
+  }).showToast();
+}
index 0c878707721f57ace05d27ceef6124059c1ecfc4..b385b8b66ee6dadab4b626c90ae88ea5c1b3078e 100644 (file)
@@ -4622,6 +4622,11 @@ to-regex@^3.0.1, to-regex@^3.0.2:
     regex-not "^1.0.2"
     safe-regex "^1.1.0"
 
+toastify-js@^1.6.2:
+  version "1.6.2"
+  resolved "https://registry.yarnpkg.com/toastify-js/-/toastify-js-1.6.2.tgz#38af35625797d3d3f51fa09851f0bda449271423"
+  integrity sha512-ECQzgjTjxaElfwp/8e8qoIYx7U5rU2G54e5aiPMv+UtmGOYEitrtNp/Kr8uMgntnQNrDZEQJNGjBtoNnEgR5EA==
+
 toidentifier@1.0.0:
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.0.tgz#7e1be3470f1e77948bc43d94a3c8f4d7752ba553"