diff --git a/server/migrations/2019-09-05-230317_add_mod_ban_views/down.sql b/server/migrations/2019-09-05-230317_add_mod_ban_views/down.sql new file mode 100644 index 000000000..c60b672c2 --- /dev/null +++ b/server/migrations/2019-09-05-230317_add_mod_ban_views/down.sql @@ -0,0 +1,44 @@ +-- Post view +drop view post_view; +create view post_view as +with all_post as +( + select + p.*, + (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 removed from community c where p.community_id = c.id) as community_removed, + (select deleted from community c where p.community_id = c.id) as community_deleted, + (select nsfw from community c where p.community_id = c.id) as community_nsfw, + (select count(*) from comment where comment.post_id = p.id) as number_of_comments, + coalesce(sum(pl.score), 0) as score, + count (case when pl.score = 1 then 1 else null end) as upvotes, + count (case when pl.score = -1 then 1 else null end) as downvotes, + hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank + from post p + left join post_like pl on p.id = pl.post_id + group by p.id +) + +select +ap.*, +u.id as user_id, +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, +(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read, +(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved +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 + +union all + +select +ap.*, +null as user_id, +null as my_vote, +null as subscribed, +null as read, +null as saved +from all_post ap +; diff --git a/server/migrations/2019-09-05-230317_add_mod_ban_views/up.sql b/server/migrations/2019-09-05-230317_add_mod_ban_views/up.sql new file mode 100644 index 000000000..d73b37208 --- /dev/null +++ b/server/migrations/2019-09-05-230317_add_mod_ban_views/up.sql @@ -0,0 +1,47 @@ +-- Create post view, adding banned_from_community + +drop view post_view; +create view post_view as +with all_post as +( + select + p.*, + (select u.banned from user_ u where p.creator_id = u.id) as banned, + (select cb.id::bool from community_user_ban cb where p.creator_id = cb.user_id and p.community_id = cb.community_id) as banned_from_community, + (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 removed from community c where p.community_id = c.id) as community_removed, + (select deleted from community c where p.community_id = c.id) as community_deleted, + (select nsfw from community c where p.community_id = c.id) as community_nsfw, + (select count(*) from comment where comment.post_id = p.id) as number_of_comments, + coalesce(sum(pl.score), 0) as score, + count (case when pl.score = 1 then 1 else null end) as upvotes, + count (case when pl.score = -1 then 1 else null end) as downvotes, + hot_rank(coalesce(sum(pl.score) , 0), p.published) as hot_rank + from post p + left join post_like pl on p.id = pl.post_id + group by p.id +) + +select +ap.*, +u.id as user_id, +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, +(select pr.id::bool from post_read pr where u.id = pr.user_id and pr.post_id = ap.id) as read, +(select ps.id::bool from post_saved ps where u.id = ps.user_id and ps.post_id = ap.id) as saved +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 + +union all + +select +ap.*, +null as user_id, +null as my_vote, +null as subscribed, +null as read, +null as saved +from all_post ap +; diff --git a/server/src/db/post_view.rs b/server/src/db/post_view.rs index c9d8cff7b..a3d327dc7 100644 --- a/server/src/db/post_view.rs +++ b/server/src/db/post_view.rs @@ -20,6 +20,8 @@ table! { updated -> Nullable, deleted -> Bool, nsfw -> Bool, + banned -> Bool, + banned_from_community -> Bool, creator_name -> Varchar, community_name -> Varchar, community_removed -> Bool, @@ -54,6 +56,8 @@ pub struct PostView { pub updated: Option, pub deleted: bool, pub nsfw: bool, + pub banned: bool, + pub banned_from_community: bool, pub creator_name: String, pub community_name: String, pub community_removed: bool, @@ -279,6 +283,8 @@ mod tests { body: None, creator_id: inserted_user.id, creator_name: user_name.to_owned(), + banned: false, + banned_from_community: false, community_id: inserted_community.id, removed: false, deleted: false, @@ -312,6 +318,8 @@ mod tests { locked: false, creator_id: inserted_user.id, creator_name: user_name.to_owned(), + banned: false, + banned_from_community: false, community_id: inserted_community.id, community_name: community_name.to_owned(), community_removed: false, diff --git a/ui/src/components/comment-node.tsx b/ui/src/components/comment-node.tsx index 3eff8c793..7dbaafdc2 100644 --- a/ui/src/components/comment-node.tsx +++ b/ui/src/components/comment-node.tsx @@ -1,6 +1,6 @@ import { Component, linkEvent } from 'inferno'; import { Link } from 'inferno-router'; -import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm } from '../interfaces'; +import { CommentNode as CommentNodeI, CommentLikeForm, CommentForm as CommentFormI, SaveCommentForm, BanFromCommunityForm, BanUserForm, CommunityUser, UserView, AddModToCommunityForm, AddAdminForm, TransferCommunityForm, TransferSiteForm, BanType } from '../interfaces'; import { WebSocketService, UserService } from '../services'; import { mdToHtml, getUnixTime, canMod, isMod } from '../utils'; import * as moment from 'moment'; @@ -10,8 +10,6 @@ import { CommentNodes } from './comment-nodes'; import { i18n } from '../i18next'; import { T } from 'inferno-i18next'; -enum BanType {Community, Site}; - interface CommentNodeState { showReply: boolean; showEdit: boolean; @@ -21,9 +19,9 @@ interface CommentNodeState { banReason: string; banExpires: string; banType: BanType; - collapsed: boolean; showConfirmTransferSite: boolean; showConfirmTransferCommunity: boolean; + collapsed: boolean; } interface CommentNodeProps { @@ -87,6 +85,9 @@ export class CommentNode extends Component { {this.isAdmin &&
  • #
  • } + {(node.comment.banned_from_community || node.comment.banned) && +
  • #
  • + }
  • ( +{node.comment.upvotes} @@ -122,7 +123,7 @@ export class CommentNode extends Component {
  • - {!this.props.node.comment.deleted ? i18n.t('delete') : i18n.t('restore')} + {!node.comment.deleted ? i18n.t('delete') : i18n.t('restore')}
  • @@ -130,7 +131,7 @@ export class CommentNode extends Component { {/* Admins and mods can remove comments */} {this.canMod &&
  • - {!this.props.node.comment.removed ? + {!node.comment.removed ? # : # } @@ -141,13 +142,13 @@ export class CommentNode extends Component { <> {!this.isMod &&
  • - {!this.props.node.comment.banned_from_community ? + {!node.comment.banned_from_community ? # : # }
  • } - {!this.props.node.comment.banned_from_community && + {!node.comment.banned_from_community &&
  • {this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')}
  • @@ -172,13 +173,13 @@ export class CommentNode extends Component { <> {!this.isAdmin &&
  • - {!this.props.node.comment.banned ? + {!node.comment.banned ? # : # }
  • } - {!this.props.node.comment.banned && + {!node.comment.banned &&
  • {this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')}
  • @@ -230,7 +231,7 @@ export class CommentNode extends Component { {/* */} {/* */}
    - +
    } @@ -241,9 +242,9 @@ export class CommentNode extends Component { disabled={this.props.locked} /> } - {this.props.node.children && !this.state.collapsed && + {node.children && !this.state.collapsed && { showEdit: false, showRemoveDialog: false, removeReason: null, - imageExpanded: false + showBanDialog: false, + banReason: null, + banExpires: null, + banType: BanType.Community, + showConfirmTransferSite: false, + showConfirmTransferCommunity: false, + imageExpanded: false, } constructor(props: any, context: any) { @@ -126,6 +138,9 @@ export class PostListing extends Component { {this.isAdmin && # } + {(post.banned_from_community || post.banned) && + # + } {this.props.showCommunity && {i18n.t('to')} @@ -169,17 +184,79 @@ export class PostListing extends Component { } {this.canMod && - + <>
  • - {!this.props.post.removed ? + {!post.removed ? # : # }
  • - {this.props.post.locked ? i18n.t('unlock') : i18n.t('lock')} + {post.locked ? i18n.t('unlock') : i18n.t('lock')}
  • -
    + + } + {/* Mods can ban from community, and appoint as mods to community */} + {this.canMod && + <> + {!this.isMod && +
  • + {!post.banned_from_community ? + # : + # + } +
  • + } + {!post.banned_from_community && +
  • + {this.isMod ? i18n.t('remove_as_mod') : i18n.t('appoint_as_mod')} +
  • + } + + } + {/* Community creators and admins can transfer community to another mod */} + {(this.amCommunityCreator || this.canAdmin) && this.isMod && +
  • + {!this.state.showConfirmTransferCommunity ? + # + : <> + # + # + # + + } +
  • + } + {/* Admins can ban from all, and appoint other admins */} + {this.canAdmin && + <> + {!this.isAdmin && +
  • + {!post.banned ? + # : + # + } +
  • + } + {!post.banned && +
  • + {this.isAdmin ? i18n.t('remove_as_admin') : i18n.t('appoint_as_admin')} +
  • + } + + } + {/* Site Creator can transfer to another admin */} + {this.amSiteCreator && this.isAdmin && +
  • + {!this.state.showConfirmTransferSite ? + # + : <> + # + # + # + + } +
  • } } @@ -189,7 +266,23 @@ export class PostListing extends Component { } - {this.props.showBody && this.props.post.body &&
    } + {this.state.showBanDialog && +
    +
    + + +
    + {/* TODO hold off on expires until later */} + {/*
    */} + {/* */} + {/* */} + {/*
    */} +
    + +
    +
    + } + {this.props.showBody && post.body &&
    }
    ) @@ -218,6 +311,24 @@ export class PostListing extends Component { } else return false; } + get canAdmin(): boolean { + return this.props.admins && canMod(UserService.Instance.user, this.props.admins.map(a => a.id), this.props.post.creator_id); + } + + get amCommunityCreator(): boolean { + return this.props.moderators && + UserService.Instance.user && + (this.props.post.creator_id != UserService.Instance.user.id) && + (UserService.Instance.user.id == this.props.moderators[0].user_id); + } + + get amSiteCreator(): boolean { + return this.props.admins && + UserService.Instance.user && + (this.props.post.creator_id != UserService.Instance.user.id) && + (UserService.Instance.user.id == this.props.admins[0].id); + } + handlePostLike(i: PostListing) { let form: CreatePostLikeForm = { @@ -328,6 +439,124 @@ export class PostListing extends Component { WebSocketService.Instance.editPost(form); } + handleModBanFromCommunityShow(i: PostListing) { + i.state.showBanDialog = true; + i.state.banType = BanType.Community; + i.setState(i.state); + } + + handleModBanShow(i: PostListing) { + i.state.showBanDialog = true; + i.state.banType = BanType.Site; + i.setState(i.state); + } + + handleModBanReasonChange(i: PostListing, event: any) { + i.state.banReason = event.target.value; + i.setState(i.state); + } + + handleModBanExpiresChange(i: PostListing, event: any) { + i.state.banExpires = event.target.value; + i.setState(i.state); + } + + handleModBanFromCommunitySubmit(i: PostListing) { + i.state.banType = BanType.Community; + i.setState(i.state); + i.handleModBanBothSubmit(i); + } + + handleModBanSubmit(i: PostListing) { + i.state.banType = BanType.Site; + i.setState(i.state); + i.handleModBanBothSubmit(i); + } + + handleModBanBothSubmit(i: PostListing) { + event.preventDefault(); + + if (i.state.banType == BanType.Community) { + let form: BanFromCommunityForm = { + user_id: i.props.post.creator_id, + community_id: i.props.post.community_id, + ban: !i.props.post.banned_from_community, + reason: i.state.banReason, + expires: getUnixTime(i.state.banExpires), + }; + WebSocketService.Instance.banFromCommunity(form); + } else { + let form: BanUserForm = { + user_id: i.props.post.creator_id, + ban: !i.props.post.banned, + reason: i.state.banReason, + expires: getUnixTime(i.state.banExpires), + }; + WebSocketService.Instance.banUser(form); + } + + i.state.showBanDialog = false; + i.setState(i.state); + } + + handleAddModToCommunity(i: PostListing) { + let form: AddModToCommunityForm = { + user_id: i.props.post.creator_id, + community_id: i.props.post.community_id, + added: !i.isMod, + }; + WebSocketService.Instance.addModToCommunity(form); + i.setState(i.state); + } + + handleAddAdmin(i: PostListing) { + let form: AddAdminForm = { + user_id: i.props.post.creator_id, + added: !i.isAdmin, + }; + WebSocketService.Instance.addAdmin(form); + i.setState(i.state); + } + + handleShowConfirmTransferCommunity(i: PostListing) { + i.state.showConfirmTransferCommunity = true; + i.setState(i.state); + } + + handleCancelShowConfirmTransferCommunity(i: PostListing) { + i.state.showConfirmTransferCommunity = false; + i.setState(i.state); + } + + handleTransferCommunity(i: PostListing) { + let form: TransferCommunityForm = { + community_id: i.props.post.community_id, + user_id: i.props.post.creator_id, + }; + WebSocketService.Instance.transferCommunity(form); + i.state.showConfirmTransferCommunity = false; + i.setState(i.state); + } + + handleShowConfirmTransferSite(i: PostListing) { + i.state.showConfirmTransferSite = true; + i.setState(i.state); + } + + handleCancelShowConfirmTransferSite(i: PostListing) { + i.state.showConfirmTransferSite = false; + i.setState(i.state); + } + + handleTransferSite(i: PostListing) { + let form: TransferSiteForm = { + user_id: i.props.post.creator_id, + }; + WebSocketService.Instance.transferSite(form); + i.state.showConfirmTransferSite = false; + i.setState(i.state); + } + handleImageExpandClick(i: PostListing) { i.state.imageExpanded = !i.state.imageExpanded; i.setState(i.state); diff --git a/ui/src/components/post.tsx b/ui/src/components/post.tsx index 91f8f4db7..7e2dbd621 100644 --- a/ui/src/components/post.tsx +++ b/ui/src/components/post.tsx @@ -351,6 +351,9 @@ export class Post extends Component { let res: BanFromCommunityResponse = msg; 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; + } this.setState(this.state); } else if (op == UserOperation.AddModToCommunity) { let res: AddModToCommunityResponse = msg; @@ -360,6 +363,9 @@ export class Post extends Component { let res: BanUserResponse = msg; 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; + } this.setState(this.state); } else if (op == UserOperation.AddAdmin) { let res: AddAdminResponse = msg; diff --git a/ui/src/components/user.tsx b/ui/src/components/user.tsx index 8b78917ed..c5ba974f0 100644 --- a/ui/src/components/user.tsx +++ b/ui/src/components/user.tsx @@ -45,6 +45,7 @@ export class User extends Component { post_score: null, number_of_comments: null, comment_score: null, + banned: null, }, user_id: null, username: null, @@ -234,7 +235,14 @@ export class User extends Component {
    -
    {user.name}
    +
    +
      +
    • {user.name}
    • + {user.banned && +
    • #
    • + } +
    +
    {i18n.t('joined')}
    diff --git a/ui/src/interfaces.ts b/ui/src/interfaces.ts index c9a647d61..f2675eb30 100644 --- a/ui/src/interfaces.ts +++ b/ui/src/interfaces.ts @@ -34,6 +34,7 @@ export interface UserView { post_score: number; number_of_comments: number; comment_score: number; + banned: boolean; } export interface CommunityUser { @@ -77,6 +78,8 @@ export interface Post { deleted: boolean; locked: boolean; nsfw: boolean; + banned: boolean; + banned_from_community: boolean; published: string; updated?: string; creator_name: string; @@ -138,6 +141,8 @@ export interface Site { number_of_communities: number; } +export enum BanType {Community, Site}; + export interface FollowCommunityForm { community_id: number; follow: boolean; diff --git a/ui/src/translations/en.ts b/ui/src/translations/en.ts index 1ddf087da..2d3523e96 100644 --- a/ui/src/translations/en.ts +++ b/ui/src/translations/en.ts @@ -56,6 +56,7 @@ export const en = { ban_from_site: 'ban from site', unban: 'unban', unban_from_site: 'unban from site', + banned: 'banned', save: 'save', unsave: 'unsave', create: 'create',