+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
[[package]]
name = "activitypub"
version = "0.1.4"
(
select
p.*,
- (select name from user_ where p.creator_id = user_.id) creator_name,
+ (select name from user_ where p.creator_id = user_.id) as creator_name,
(select name from community where p.community_id = community.id) as community_name,
(select count(*) from comment where comment.post_id = p.id) as number_of_comments,
coalesce(sum(pl.score), 0) as score,
select
ap.*,
u.id as user_id,
-coalesce(pl.score, 0) as my_vote
+coalesce(pl.score, 0) as my_vote,
+(select cf.id::bool from community_follower cf where u.id = cf.user_id and cf.community_id = ap.community_id) as subscribed
from user_ u
cross join all_post ap
left join post_like pl on u.id = pl.user_id and ap.id = pl.post_id
select
ap.*,
null as user_id,
-null as my_vote
+null as my_vote,
+null as subscribed
from all_post ap
;
}
}
+#[derive(Queryable, Identifiable, PartialEq, Debug, Serialize, Deserialize,QueryableByName,Clone)]
+#[table_name="community_follower_view"]
+pub struct CommunityFollowerView {
+ pub id: i32,
+ pub community_id: i32,
+ pub user_id: i32,
+ pub published: chrono::NaiveDateTime,
+ pub user_name : String,
+ pub community_name: String,
+}
+
+impl CommunityFollowerView {
+ pub fn for_community(conn: &PgConnection, from_community_id: i32) -> Result<Vec<Self>, Error> {
+ use actions::community_view::community_follower_view::dsl::*;
+ community_follower_view.filter(community_id.eq(from_community_id)).load::<Self>(conn)
+ }
+
+ pub fn for_user(conn: &PgConnection, from_user_id: i32) -> Result<Vec<Self>, Error> {
+ use actions::community_view::community_follower_view::dsl::*;
+ community_follower_view.filter(user_id.eq(from_user_id)).load::<Self>(conn)
+ }
+}
hot_rank -> Int4,
user_id -> Nullable<Int4>,
my_vote -> Nullable<Int4>,
+ subscribed -> Nullable<Bool>,
}
}
pub hot_rank: i32,
pub user_id: Option<i32>,
pub my_vote: Option<i32>,
+ pub subscribed: Option<bool>,
}
impl PostView {
query = query.filter(community_id.eq(from_community_id));
};
+ match type_ {
+ ListingType::Subscribed => {
+ query = query.filter(subscribed.eq(true));
+ },
+ _ => {}
+ };
+
// 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));
#[derive(EnumString,ToString,Debug)]
pub enum UserOperation {
- Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
+ Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
}
#[derive(Serialize, Deserialize)]
auth: String
}
+#[derive(Serialize, Deserialize)]
+pub struct GetFollowedCommunities {
+ auth: String
+}
+
+#[derive(Serialize, Deserialize)]
+pub struct GetFollowedCommunitiesResponse {
+ op: String,
+ communities: Vec<CommunityFollowerView>
+}
+
+
/// `ChatServer` manages chat rooms and responsible for coordinating chat
/// session. implementation is super primitive
pub struct ChatServer {
let follow_community: FollowCommunity = serde_json::from_str(&data.to_string()).unwrap();
follow_community.perform(self, msg.id)
},
+ UserOperation::GetFollowedCommunities => {
+ let followed_communities: GetFollowedCommunities = serde_json::from_str(&data.to_string()).unwrap();
+ followed_communities.perform(self, msg.id)
+ },
_ => {
let e = ErrorMessage {
op: "Unknown".to_string(),
let conn = establish_connection();
- println!("{:?}", self.auth);
-
let user_id: Option<i32> = match &self.auth {
Some(auth) => {
match Claims::decode(&auth) {
}
}
+impl Perform for GetFollowedCommunities {
+ fn op_type(&self) -> UserOperation {
+ UserOperation::GetFollowedCommunities
+ }
+
+ 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 communities: Vec<CommunityFollowerView> = CommunityFollowerView::for_user(&conn, user_id).unwrap();
+
+ // Return the jwt
+ serde_json::to_string(
+ &GetFollowedCommunitiesResponse {
+ op: self.op_type().to_string(),
+ communities: communities
+ }
+ )
+ .unwrap()
+ }
+}
// impl Handler<Login> for ChatServer {
<div class="container-fluid">
<h4>Communities</h4>
<div class="table-responsive">
- <table id="community_table" class="table table-sm table-hover" data-sortable>
- <thead>
+ <table id="community_table" class="table table-sm table-hover">
+ <thead class="pointer">
<tr>
<th>Name</th>
<th>Title</th>
import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
import { WebSocketService, UserService } from '../services';
import { MomentTime } from './moment-time';
-import { PostListing } from './post-listing';
+import { PostListings } from './post-listings';
import { Sidebar } from './sidebar';
import { msgOp, mdToHtml } from '../utils';
interface State {
community: CommunityI;
+ communityId: number;
moderators: Array<CommunityUser>;
- posts: Array<Post>;
- sortType: ListingSortType;
}
export class Community extends Component<any, State> {
published: null
},
moderators: [],
- posts: [],
- sortType: ListingSortType.Hot,
+ communityId: Number(this.props.match.params.id)
}
constructor(props, context) {
() => console.log('complete')
);
- let communityId = Number(this.props.match.params.id);
- WebSocketService.Instance.getCommunity(communityId);
-
- let getPostsForm: GetPostsForm = {
- community_id: communityId,
- limit: 10,
- sort: ListingSortType[ListingSortType.Hot],
- type_: ListingType[ListingType.Community]
- }
- WebSocketService.Instance.getPosts(getPostsForm);
+ WebSocketService.Instance.getCommunity(this.state.communityId);
}
componentWillUnmount() {
<div class="row">
<div class="col-12 col-lg-9">
<h4>/f/{this.state.community.name}</h4>
- <div>{this.selects()}</div>
- {this.state.posts.length > 0
- ? this.state.posts.map(post =>
- <PostListing post={post} />)
- : <div>no listings</div>
- }
+ <PostListings communityId={this.state.communityId} />
</div>
<div class="col-12 col-lg-3">
<Sidebar community={this.state.community} moderators={this.state.moderators} />
)
}
- selects() {
- return (
- <div className="mb-2">
- <select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
- <option disabled>Sort Type</option>
- <option value={ListingSortType.Hot}>Hot</option>
- <option value={ListingSortType.New}>New</option>
- <option disabled>──────────</option>
- <option value={ListingSortType.TopDay}>Top Day</option>
- <option value={ListingSortType.TopWeek}>Week</option>
- <option value={ListingSortType.TopMonth}>Month</option>
- <option value={ListingSortType.TopYear}>Year</option>
- <option value={ListingSortType.TopAll}>All</option>
- </select>
- </div>
- )
-
- }
-
- handleSortChange(i: Community, event) {
- i.state.sortType = Number(event.target.value);
- i.setState(i.state);
-
- let getPostsForm: GetPostsForm = {
- community_id: i.state.community.id,
- limit: 10,
- sort: ListingSortType[i.state.sortType],
- type_: ListingType[ListingType.Community]
- }
- WebSocketService.Instance.getPosts(getPostsForm);
- }
parseMessage(msg: any) {
console.log(msg);
this.state.community = res.community;
this.state.moderators = res.moderators;
this.setState(this.state);
- } else if (op == UserOperation.GetPosts) {
- let res: GetPostsResponse = msg;
- this.state.posts = res.posts;
- 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;
- this.setState(this.state);
} else if (op == UserOperation.EditCommunity) {
let res: CommunityResponse = msg;
this.state.community = res.community;
}
}
-
import { Component } from 'inferno';
import { repoUrl } from '../utils';
+import { Main } from './main';
export class Home extends Component<any, any> {
render() {
return (
- <div class="container">
- hola this is me.
- </div>
+ <Main />
)
}
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser, GetFollowedCommunitiesResponse } from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { MomentTime } from './moment-time';
+import { PostListings } from './post-listings';
+import { Sidebar } from './sidebar';
+import { msgOp, mdToHtml } from '../utils';
+
+interface State {
+ subscribedCommunities: Array<CommunityUser>;
+}
+
+export class Main extends Component<any, State> {
+
+ private subscription: Subscription;
+ private emptyState: State = {
+ subscribedCommunities: []
+ }
+
+ constructor(props, context) {
+ super(props, context);
+
+ this.state = this.emptyState;
+
+ 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')
+ );
+
+ if (UserService.Instance.loggedIn) {
+ WebSocketService.Instance.getFollowedCommunities();
+ }
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div class="container">
+ <div class="row">
+ <div class="col-12 col-lg-9">
+ <PostListings />
+ </div>
+ <div class="col-12 col-lg-3">
+ <h4>A Landing message</h4>
+ {UserService.Instance.loggedIn &&
+ <div>
+ <hr />
+ <h4>Subscribed forums</h4>
+ <ul class="list-unstyled">
+ {this.state.subscribedCommunities.map(community =>
+ <li><Link to={`/community/${community.community_id}`}>{community.community_name}</Link></li>
+ )}
+ </ul>
+ </div>
+ }
+ </div>
+ </div>
+ </div>
+ )
+ }
+
+
+ parseMessage(msg: any) {
+ console.log(msg);
+ let op: UserOperation = msgOp(msg);
+ if (msg.error) {
+ alert(msg.error);
+ return;
+ } else if (op == UserOperation.GetFollowedCommunities) {
+ let res: GetFollowedCommunitiesResponse = msg;
+ this.state.subscribedCommunities = res.communities;
+ this.setState(this.state);
+ }
+ }
+}
+
</div>
<div className="ml-4">
{post.url
- ? <h4 className="mb-0">
- <a className="text-white" href={post.url}>{post.name}</a>
+ ? <div className="mb-0">
+ <h4 className="d-inline"><a className="text-white" href={post.url}>{post.name}</a></h4>
<small><a className="ml-2 text-muted font-italic" href={post.url}>{(new URL(post.url)).hostname}</a></small>
{ !this.state.iframeExpanded
? <span class="pointer ml-2 text-muted small" title="Expand here" onClick={linkEvent(this, this.handleIframeExpandClick)}>+</span>
</div>
</span>
}
- </h4>
+ </div>
: <h4 className="mb-0"><Link className="text-white" to={`/post/${post.id}`}>{post.name}</Link></h4>
}
</div>
<ul class="list-inline mb-0 text-muted small">
<li className="list-inline-item">
<span>by </span>
- <a href={post.creator_id.toString()}>{post.creator_name}</a>
+ <Link to={`/user/${post.creator_id}`}>{post.creator_name}</Link>
{this.props.showCommunity &&
<span>
<span> to </span>
--- /dev/null
+import { Component, linkEvent } from 'inferno';
+import { Link } from 'inferno-router';
+import { Subscription } from "rxjs";
+import { retryWhen, delay, take } from 'rxjs/operators';
+import { UserOperation, Community as CommunityI, GetCommunityResponse, CommunityResponse, Post, GetPostsForm, ListingSortType, ListingType, GetPostsResponse, CreatePostLikeForm, CreatePostLikeResponse, CommunityUser} from '../interfaces';
+import { WebSocketService, UserService } from '../services';
+import { MomentTime } from './moment-time';
+import { PostListing } from './post-listing';
+import { Sidebar } from './sidebar';
+import { msgOp, mdToHtml } from '../utils';
+
+
+interface PostListingsProps {
+ communityId?: number;
+}
+
+interface PostListingsState {
+ community: CommunityI;
+ moderators: Array<CommunityUser>;
+ posts: Array<Post>;
+ sortType: ListingSortType;
+ type_: ListingType;
+}
+
+export class PostListings extends Component<PostListingsProps, PostListingsState> {
+
+ private subscription: Subscription;
+ private emptyState: PostListingsState = {
+ community: {
+ id: null,
+ name: null,
+ title: null,
+ category_id: null,
+ category_name: null,
+ creator_id: null,
+ creator_name: null,
+ number_of_subscribers: null,
+ number_of_posts: null,
+ number_of_comments: null,
+ published: null
+ },
+ moderators: [],
+ posts: [],
+ sortType: ListingSortType.Hot,
+ type_: this.props.communityId
+ ? ListingType.Community
+ : UserService.Instance.loggedIn
+ ? ListingType.Subscribed
+ : ListingType.All
+ }
+
+ constructor(props, context) {
+ super(props, context);
+
+
+ this.state = this.emptyState;
+
+ 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')
+ );
+
+ let getPostsForm: GetPostsForm = {
+ type_: ListingType[this.state.type_],
+ community_id: this.props.communityId,
+ limit: 10,
+ sort: ListingSortType[ListingSortType.Hot],
+ }
+ WebSocketService.Instance.getPosts(getPostsForm);
+ }
+
+ componentWillUnmount() {
+ this.subscription.unsubscribe();
+ }
+
+ render() {
+ return (
+ <div>
+ <div>{this.selects()}</div>
+ {this.state.posts.length > 0
+ ? this.state.posts.map(post =>
+ <PostListing post={post} showCommunity={!this.props.communityId}/>)
+ : <div>No Listings</div>
+ }
+ </div>
+ )
+ }
+
+ selects() {
+ return (
+ <div className="mb-2">
+ <select value={this.state.sortType} onChange={linkEvent(this, this.handleSortChange)} class="custom-select w-auto">
+ <option disabled>Sort Type</option>
+ <option value={ListingSortType.Hot}>Hot</option>
+ <option value={ListingSortType.New}>New</option>
+ <option disabled>──────────</option>
+ <option value={ListingSortType.TopDay}>Top Day</option>
+ <option value={ListingSortType.TopWeek}>Week</option>
+ <option value={ListingSortType.TopMonth}>Month</option>
+ <option value={ListingSortType.TopYear}>Year</option>
+ <option value={ListingSortType.TopAll}>All</option>
+ </select>
+ {!this.props.communityId &&
+ UserService.Instance.loggedIn &&
+ <select value={this.state.type_} onChange={linkEvent(this, this.handleTypeChange)} class="ml-2 custom-select w-auto">
+ <option disabled>Type</option>
+ <option value={ListingType.All}>All</option>
+ <option value={ListingType.Subscribed}>Subscribed</option>
+ </select>
+
+ }
+ </div>
+ )
+
+ }
+
+ handleSortChange(i: PostListings, event) {
+ i.state.sortType = Number(event.target.value);
+ i.setState(i.state);
+
+ let getPostsForm: GetPostsForm = {
+ community_id: i.state.community.id,
+ limit: 10,
+ sort: ListingSortType[i.state.sortType],
+ type_: ListingType[ListingType.Community]
+ }
+ WebSocketService.Instance.getPosts(getPostsForm);
+ }
+
+ handleTypeChange(i: PostListings, event) {
+ i.state.type_ = Number(event.target.value);
+ i.setState(i.state);
+
+ let getPostsForm: GetPostsForm = {
+ limit: 10,
+ sort: ListingSortType[i.state.sortType],
+ type_: ListingType[i.state.type_]
+ }
+ WebSocketService.Instance.getPosts(getPostsForm);
+ }
+
+ parseMessage(msg: any) {
+ console.log(msg);
+ let op: UserOperation = msgOp(msg);
+ if (msg.error) {
+ alert(msg.error);
+ return;
+ } else if (op == UserOperation.GetPosts) {
+ let res: GetPostsResponse = msg;
+ this.state.posts = res.posts;
+ 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;
+ this.setState(this.state);
+ }
+ }
+}
+
+
<link rel="stylesheet" href="https://bootswatch.com/4/darkly/bootstrap.min.css">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/balloon-css/0.5.0/balloon.min.css">
<link href="https://fonts.googleapis.com/css?family=Open+Sans:300,400,400i,700,800" rel="stylesheet">
- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/css/sortable-theme-minimal.min.css" />
+ <!-- <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/css/sortable-theme-minimal.min.css" /> -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/sortable/0.8.0/js/sortable.min.js"></script>
</head>
export enum UserOperation {
- Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity
+ Login, Register, CreateCommunity, CreatePost, ListCommunities, ListCategories, GetPost, GetCommunity, CreateComment, EditComment, CreateCommentLike, GetPosts, CreatePostLike, EditPost, EditCommunity, FollowCommunity, GetFollowedCommunities
}
export interface User {
auth?: string;
}
+export interface GetFollowedCommunitiesResponse {
+ op: string;
+ communities: Array<CommunityUser>;
+}
+
export interface LoginForm {
username_or_email: string;
password: string;
this.subject.next(this.wsSendWrapper(UserOperation.ListCommunities, data));
}
+ public getFollowedCommunities() {
+ let data = {auth: UserService.Instance.auth };
+ this.subject.next(this.wsSendWrapper(UserOperation.GetFollowedCommunities, data));
+ }
+
public listCategories() {
this.subject.next(this.wsSendWrapper(UserOperation.ListCategories, undefined));
}