]> Untitled Git - lemmy.git/commitdiff
Adding subscribe to communities.
authorDessalines <tyhou13@gmx.com>
Fri, 5 Apr 2019 06:26:38 +0000 (23:26 -0700)
committerDessalines <tyhou13@gmx.com>
Fri, 5 Apr 2019 06:26:38 +0000 (23:26 -0700)
- Adding subscribe. Fixes #12. Fixes #27.

server/migrations/2019-04-03-155205_create_community_view/up.sql
server/src/actions/community_view.rs
server/src/actions/post_view.rs
server/src/websocket_server/server.rs
ui/src/components/communities.tsx
ui/src/components/community.tsx
ui/src/components/post.tsx
ui/src/components/sidebar.tsx
ui/src/interfaces.ts
ui/src/services/WebSocketService.ts

index f2f4a76647593ef58b642dfe8d6a473535e65937..7c60874288e0540b2ff7ebedfaf4cc691c0e3f5b 100644 (file)
@@ -1,11 +1,31 @@
 create view community_view as 
-select *,
-(select name from user_ u where c.creator_id = u.id) as creator_name,
-(select name from category ct where c.category_id = ct.id) as category_name,
-(select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
-(select count(*) from post p where p.community_id = c.id) as number_of_posts,
-(select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments
-from community c;
+with all_community as
+(
+  select *,
+  (select name from user_ u where c.creator_id = u.id) as creator_name,
+  (select name from category ct where c.category_id = ct.id) as category_name,
+  (select count(*) from community_follower cf where cf.community_id = c.id) as number_of_subscribers,
+  (select count(*) from post p where p.community_id = c.id) as number_of_posts,
+  (select count(*) from comment co, post p where c.id = p.community_id and p.id = co.post_id) as number_of_comments
+  from community c
+)
+
+select
+ac.*,
+u.id as user_id,
+cf.id::boolean as subscribed
+from user_ u
+cross join all_community ac
+left join community_follower cf on u.id = cf.user_id and ac.id = cf.community_id
+
+union all
+
+select 
+ac.*,
+null as user_id,
+null as subscribed
+from all_community ac
+;
 
 create view community_moderator_view as 
 select *,
index eafda161db5b6734ff4512521f77e7033f332e1f..7eb07a162ab93ddcfc6652671134618c6bb186a1 100644 (file)
@@ -18,6 +18,8 @@ table! {
     number_of_subscribers -> BigInt,
     number_of_posts -> BigInt,
     number_of_comments -> BigInt,
+    user_id -> Nullable<Int4>,
+    subscribed -> Nullable<Bool>,
   }
 }
 
@@ -58,18 +60,43 @@ pub struct CommunityView {
   pub category_name: String,
   pub number_of_subscribers: i64,
   pub number_of_posts: i64,
-  pub number_of_comments: i64
+  pub number_of_comments: i64,
+  pub user_id: Option<i32>,
+  pub subscribed: Option<bool>,
 }
 
 impl CommunityView {
-  pub fn read(conn: &PgConnection, from_community_id: i32) -> Result<Self, Error> {
+  pub fn read(conn: &PgConnection, from_community_id: i32, from_user_id: Option<i32>) -> Result<Self, Error> {
     use actions::community_view::community_view::dsl::*;
-    community_view.find(from_community_id).first::<Self>(conn)
+
+    let mut query = community_view.into_boxed();
+
+    query = query.filter(id.eq(from_community_id));
+
+    // The view lets you pass a null user_id, if you're not logged in
+    if let Some(from_user_id) = from_user_id {
+      query = query.filter(user_id.eq(from_user_id));
+    } else {
+      query = query.filter(user_id.is_null());
+    };
+
+    query.first::<Self>(conn)
   }
 
-  pub fn list_all(conn: &PgConnection) -> Result<Vec<Self>, Error> {
+  pub fn list_all(conn: &PgConnection, from_user_id: Option<i32>) -> Result<Vec<Self>, Error> {
     use actions::community_view::community_view::dsl::*;
-    community_view.load::<Self>(conn)
+    let mut query = community_view.into_boxed();
+
+    // The view lets you pass a null user_id, if you're not logged in
+    if let Some(from_user_id) = from_user_id {
+      query = query.filter(user_id.eq(from_user_id))
+        .order_by((subscribed.desc(), number_of_subscribers.desc()));
+    } else {
+      query = query.filter(user_id.is_null())
+        .order_by(number_of_subscribers.desc());
+    };
+
+    query.load::<Self>(conn) 
   }
 }
 
index f53a9f0c6a081a54a4cd3867f8de118a3bf03fbb..c48c651e39a863b0b244f70d7ac06762cc5c7793 100644 (file)
@@ -113,7 +113,7 @@ impl PostView {
       query = query.filter(user_id.eq(from_user_id));
     } else {
       query = query.filter(user_id.is_null());
-    }
+    };
 
     query.first::<Self>(conn)
   }
index a0d129354c5c7f2bd6e906132171df7a6d2f5094..fe7cd0e66286c6575a8a9a0cb664fe999a673c9b 100644 (file)
@@ -22,7 +22,7 @@ use actions::community_view::*;
 
 #[derive(EnumString,ToString,Debug)]
 pub enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
 }
 
 #[derive(Serialize, Deserialize)]
@@ -109,7 +109,9 @@ pub struct CommunityResponse {
 }
 
 #[derive(Serialize, Deserialize)]
-pub struct ListCommunities;
+pub struct ListCommunities {
+  auth: Option<String>
+}
 
 #[derive(Serialize, Deserialize)]
 pub struct ListCommunitiesResponse {
@@ -174,7 +176,8 @@ pub struct GetPostsResponse {
 
 #[derive(Serialize, Deserialize)]
 pub struct GetCommunity {
-  id: i32
+  id: i32,
+  auth: Option<String>
 }
 
 #[derive(Serialize, Deserialize)]
@@ -251,6 +254,13 @@ pub struct EditCommunity {
   auth: String
 }
 
+#[derive(Serialize, Deserialize)]
+pub struct FollowCommunity {
+  community_id: i32,
+  follow: bool,
+  auth: String
+}
+
 /// `ChatServer` manages chat rooms and responsible for coordinating chat
 /// session. implementation is super primitive
 pub struct ChatServer {
@@ -389,7 +399,7 @@ impl Handler<StandardMessage> for ChatServer {
         create_community.perform(self, msg.id)
       },
       UserOperation::ListCommunities => {
-        let list_communities: ListCommunities = ListCommunities;
+        let list_communities: ListCommunities = serde_json::from_str(&data.to_string()).unwrap();
         list_communities.perform(self, msg.id)
       },
       UserOperation::ListCategories => {
@@ -436,6 +446,10 @@ impl Handler<StandardMessage> for ChatServer {
         let edit_community: EditCommunity = serde_json::from_str(&data.to_string()).unwrap();
         edit_community.perform(self, msg.id)
       },
+      UserOperation::FollowCommunity => {
+        let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap();
+        follow_community.perform(self, msg.id)
+      },
       _ => {
         let e = ErrorMessage { 
           op: "Unknown".to_string(),
@@ -599,7 +613,7 @@ impl Perform for CreateCommunity {
       }
     };
 
-    let community_view = CommunityView::read(&conn, inserted_community.id).unwrap();
+    let community_view = CommunityView::read(&conn, inserted_community.id, Some(user_id)).unwrap();
 
     serde_json::to_string(
       &CommunityResponse {
@@ -620,7 +634,20 @@ impl Perform for ListCommunities {
 
     let conn = establish_connection();
 
-    let communities: Vec<CommunityView> = CommunityView::list_all(&conn).unwrap();
+    let user_id: Option<i32> = match &self.auth {
+      Some(auth) => {
+        match Claims::decode(&auth) {
+          Ok(claims) => {
+            let user_id = claims.claims.id;
+            Some(user_id)
+          }
+          Err(_e) => None
+        }
+      }
+      None => None
+    };
+
+    let communities: Vec<CommunityView> = CommunityView::list_all(&conn, user_id).unwrap();
 
     // Return the jwt
     serde_json::to_string(
@@ -767,7 +794,7 @@ impl Perform for GetPost {
 
     let comments = CommentView::list(&conn, self.id, user_id).unwrap();
 
-    let community = CommunityView::read(&conn, post_view.community_id).unwrap();
+    let community = CommunityView::read(&conn, post_view.community_id, user_id).unwrap();
 
     let moderators = CommunityModeratorView::for_community(&conn, post_view.community_id).unwrap();
 
@@ -794,7 +821,20 @@ impl Perform for GetCommunity {
 
     let conn = establish_connection();
 
-    let community_view = match CommunityView::read(&conn, self.id) {
+    let user_id: Option<i32> = match &self.auth {
+      Some(auth) => {
+        match Claims::decode(&auth) {
+          Ok(claims) => {
+            let user_id = claims.claims.id;
+            Some(user_id)
+          }
+          Err(_e) => None
+        }
+      }
+      None => None
+    };
+
+    let community_view = match CommunityView::read(&conn, self.id, user_id) {
       Ok(community) => community,
       Err(_e) => {
         return self.error("Couldn't find Community");
@@ -917,7 +957,7 @@ impl Perform for EditComment {
     // Verify its the creator
     let orig_comment = Comment::read(&conn, self.edit_id).unwrap();
     if user_id != orig_comment.creator_id {
-        return self.error("Incorrect creator.");
+      return self.error("Incorrect creator.");
     }
 
     let comment_form = CommentForm {
@@ -1158,7 +1198,7 @@ impl Perform for EditPost {
     // Verify its the creator
     let orig_post = Post::read(&conn, self.edit_id).unwrap();
     if user_id != orig_post.creator_id {
-        return self.error("Incorrect creator.");
+      return self.error("Incorrect creator.");
     }
 
     let post_form = PostForm {
@@ -1227,7 +1267,7 @@ impl Perform for EditCommunity {
     let moderator_view = CommunityModeratorView::for_community(&conn, self.edit_id).unwrap();
     let mod_ids: Vec<i32> = moderator_view.into_iter().map(|m| m.user_id).collect();
     if !mod_ids.contains(&user_id) {
-        return self.error("Incorrect creator.");
+      return self.error("Incorrect creator.");
     };
 
     let community_form = CommunityForm {
@@ -1246,7 +1286,7 @@ impl Perform for EditCommunity {
       }
     };
 
-    let community_view = CommunityView::read(&conn, self.edit_id).unwrap();
+    let community_view = CommunityView::read(&conn, self.edit_id, Some(user_id)).unwrap();
 
     // Do the subscriber stuff here
     // let mut community_sent = post_view.clone();
@@ -1273,6 +1313,61 @@ impl Perform for EditCommunity {
     community_out
   }
 }
+
+
+impl Perform for FollowCommunity {
+  fn op_type(&self) -> UserOperation {
+    UserOperation::FollowCommunity
+  }
+
+  fn perform(&self, _chat: &mut ChatServer, _addr: usize) -> String {
+
+    let conn = establish_connection();
+
+    let claims = match Claims::decode(&self.auth) {
+      Ok(claims) => claims.claims,
+      Err(_e) => {
+        return self.error("Not logged in.");
+      }
+    };
+
+    let user_id = claims.id;
+
+    let community_follower_form = CommunityFollowerForm {
+      community_id: self.community_id,
+      user_id: user_id
+    };
+
+    if self.follow {
+
+      match CommunityFollower::follow(&conn, &community_follower_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community follower already exists.");
+        }
+      };
+    } else {
+      match CommunityFollower::ignore(&conn, &community_follower_form) {
+        Ok(user) => user,
+        Err(_e) => {
+          return self.error("Community follower already exists.");
+        }
+      };
+    }
+
+    let community_view = CommunityView::read(&conn, self.community_id, Some(user_id)).unwrap();
+
+    serde_json::to_string(
+      &CommunityResponse {
+        op: self.op_type().to_string(), 
+        community: community_view
+      }
+      )
+      .unwrap()
+  }
+}
+
+
 // impl Handler<Login> for ChatServer {
 
 //   type Result = MessageResult<Login>;
index 80953aaa3e952bcc9b0d052f06f6923df7bb01d1..e8158a3655af7b2b68fdb21792890a30a613585d 100644 (file)
@@ -2,7 +2,7 @@ import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
 import { Subscription } from "rxjs";
 import { retryWhen, delay, take } from 'rxjs/operators';
-import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, ListCommunitiesResponse } from '../interfaces';
+import { UserOperation, Community, Post as PostI, GetPostResponse, PostResponse, Comment, CommentForm as CommentFormI, CommentResponse, CommentLikeForm, CommentSortType, CreatePostLikeResponse, ListCommunitiesResponse, CommunityResponse, FollowCommunityForm } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { msgOp, hotRank,mdToHtml } from '../utils';
 
@@ -29,6 +29,7 @@ export class Communities extends Component<any, CommunitiesState> {
         () => console.log('complete')
       );
     WebSocketService.Instance.listCommunities();
+
   }
 
   componentDidMount() {
@@ -50,6 +51,7 @@ export class Communities extends Component<any, CommunitiesState> {
                 <th class="text-right">Subscribers</th>
                 <th class="text-right">Posts</th>
                 <th class="text-right">Comments</th>
+                <th></th>
               </tr>
             </thead>
             <tbody>
@@ -61,6 +63,12 @@ export class Communities extends Component<any, CommunitiesState> {
                   <td class="text-right">{community.number_of_subscribers}</td>
                   <td class="text-right">{community.number_of_posts}</td>
                   <td class="text-right">{community.number_of_comments}</td>
+                  <td class="text-right">
+                    {community.subscribed 
+                      ? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
+                      : <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
+                    }
+                  </td>
                 </tr>
               )}
             </tbody>
@@ -70,8 +78,23 @@ export class Communities extends Component<any, CommunitiesState> {
     );
   }
 
+  handleUnsubscribe(communityId: number) {
+    let form: FollowCommunityForm = {
+      community_id: communityId,
+      follow: false
+    };
+    WebSocketService.Instance.followCommunity(form);
+  }
 
 
+  handleSubscribe(communityId: number) {
+    let form: FollowCommunityForm = {
+      community_id: communityId,
+      follow: true
+    };
+    WebSocketService.Instance.followCommunity(form);
+  }
+
   parseMessage(msg: any) {
     console.log(msg);
     let op: UserOperation = msgOp(msg);
@@ -83,6 +106,12 @@ export class Communities extends Component<any, CommunitiesState> {
       this.state.communities = res.communities;
       this.state.communities.sort((a, b) => b.number_of_subscribers - a.number_of_subscribers);
       this.setState(this.state);
+    } 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;
+      this.setState(this.state);
     }
   }
 }
index d5f75b45e7c6499481948dc0802753adc141f5ee..726055ba736a5da0590eb4a8e6c7b6df6b494781 100644 (file)
@@ -147,6 +147,11 @@ export class Community extends Component<any, State> {
       let res: CommunityResponse = msg;
       this.state.community = res.community;
       this.setState(this.state);
+    } else if (op == UserOperation.FollowCommunity) {
+      let res: CommunityResponse = msg;
+      this.state.community.subscribed = res.community.subscribed;
+      this.state.community.number_of_subscribers = res.community.number_of_subscribers;
+      this.setState(this.state);
     }
   }
 }
index 0075e9dffd850f05e67e8aa7ea89a8fb0a261cdb..2a870c4dc077ea0da29406e760ea2228d615a5f1 100644 (file)
@@ -229,6 +229,11 @@ export class Post extends Component<any, PostState> {
       this.state.post.community_id = res.community.id;
       this.state.post.community_name = res.community.name;
       this.setState(this.state);
+    } else if (op == UserOperation.FollowCommunity) {
+      let res: CommunityResponse = msg;
+      this.state.community.subscribed = res.community.subscribed;
+      this.state.community.number_of_subscribers = res.community.number_of_subscribers;
+      this.setState(this.state);
     }
 
   }
index 3f11749c89d01591cb2df186705df688a136a3f3..ad3eeccc36edf010bcbfbe9de7e052d46891cd92 100644 (file)
@@ -1,6 +1,6 @@
 import { Component, linkEvent } from 'inferno';
 import { Link } from 'inferno-router';
-import { Community, CommunityUser } from '../interfaces';
+import { Community, CommunityUser, FollowCommunityForm } from '../interfaces';
 import { WebSocketService, UserService } from '../services';
 import { mdToHtml } from '../utils';
 import { CommunityForm } from './community-form';
@@ -61,7 +61,12 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
           <li className="list-inline-item badge badge-light">{community.number_of_posts} Posts</li>
           <li className="list-inline-item badge badge-light">{community.number_of_comments} Comments</li>
         </ul>
-        <div><button type="button" class="btn btn-secondary mb-2">Subscribe</button></div>
+        <div>
+          {community.subscribed 
+            ? <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleUnsubscribe)}>Unsubscribe</button>
+            : <button class="btn btn-sm btn-secondary" onClick={linkEvent(community.id, this.handleSubscribe)}>Subscribe</button>
+          }
+        </div>
         {community.description && 
           <div>
             <hr />
@@ -96,6 +101,22 @@ export class Sidebar extends Component<SidebarProps, SidebarState> {
   handleDeleteClick(i: Sidebar, event) {
   }
 
+  handleUnsubscribe(communityId: number) {
+    let form: FollowCommunityForm = {
+      community_id: communityId,
+      follow: false
+    };
+    WebSocketService.Instance.followCommunity(form);
+  }
+
+  handleSubscribe(communityId: number) {
+    let form: FollowCommunityForm = {
+      community_id: communityId,
+      follow: true
+    };
+    WebSocketService.Instance.followCommunity(form);
+  }
+
   private get amCreator(): boolean {
     return UserService.Instance.loggedIn && this.props.community.creator_id == UserService.Instance.user.id;
   }
index 0505a3989583a1e08d0901a2466443e0a138a07d..f8007cbaf7b9e969652b9d4d531f025d878b3f6b 100644 (file)
@@ -1,5 +1,5 @@
 export enum UserOperation {
-  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity
+  Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
 }
 
 export interface User {
@@ -18,6 +18,8 @@ export interface CommunityUser {
 }
 
 export interface Community {
+  user_id?: number;
+  subscribed?: boolean;
   id: number;
   name: string;
   title: string;
@@ -171,6 +173,12 @@ export interface Category {
   name: string;
 }
 
+export interface FollowCommunityForm {
+  community_id: number;
+  follow: boolean;
+  auth?: string;
+}
+
 export interface LoginForm {
   username_or_email: string;
   password: string;
index d89d0128cd8e4d1da7f2c28c94251312960ba4a6..c8cc95570b6e366729ce99c229f704e1c7a39a8e 100644 (file)
@@ -1,5 +1,5 @@
 import { wsUri } from '../env';
-import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm } from '../interfaces';
+import { LoginForm, RegisterForm, UserOperation, CommunityForm, PostForm, CommentForm, CommentLikeForm, GetPostsForm, CreatePostLikeForm, FollowCommunityForm } from '../interfaces';
 import { webSocket } from 'rxjs/webSocket';
 import { Subject } from 'rxjs';
 import { retryWhen, delay, take } from 'rxjs/operators';
@@ -42,8 +42,14 @@ export class WebSocketService {
     this.subject.next(this.wsSendWrapper(UserOperation.EditCommunity, communityForm));
   }
 
+  public followCommunity(followCommunityForm: FollowCommunityForm) {
+    this.setAuth(followCommunityForm);
+    this.subject.next(this.wsSendWrapper(UserOperation.FollowCommunity, followCommunityForm));
+  }
+
   public listCommunities() {
-    this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, undefined));
+    let data = {auth: UserService.Instance.auth };
+    this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data));
   }
 
   public listCategories() {
@@ -61,7 +67,8 @@ export class WebSocketService {
   }
 
   public getCommunity(communityId: number) {
-    this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, {id: communityId}));
+    let data = {id: communityId, auth: UserService.Instance.auth };
+    this.subject.next(this.wsSendWrapper(UserOperation.GetCommunity, data));
   }
 
   public createComment(commentForm: CommentForm) {