From 424be43865179a13152b4b204853ec9b431245d0 Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Tue, 3 Jul 2018 13:16:48 -0400 Subject: [PATCH] Add chat_channel_membership roles and invite functionality (#541) --- Gemfile | 2 +- Gemfile.lock | 4 +- .../admin/application_controller.rb | 1 - .../chat_channel_memberships_controller.rb | 14 ++- app/controllers/chat_channels_controller.rb | 2 +- app/javascript/chat/actions.js | 4 +- app/javascript/chat/channelDetails.jsx | 85 +++++++++++++++++++ app/javascript/chat/chat.jsx | 2 + app/labor/profile_image.rb | 3 +- app/models/chat_channel.rb | 35 +++++--- app/models/chat_channel_membership.rb | 1 + app/policies/chat_channel_policy.rb | 3 +- app/views/layouts/application.html.erb | 4 + ...43_add_role_to_chat_channel_memberships.rb | 5 ++ db/schema.rb | 3 +- .../requests/chat_channel_memberships_spec.rb | 23 +++-- 16 files changed, 163 insertions(+), 28 deletions(-) create mode 100644 db/migrate/20180703142743_add_role_to_chat_channel_memberships.rb diff --git a/Gemfile b/Gemfile index 902508044..70fd33c24 100644 --- a/Gemfile +++ b/Gemfile @@ -21,7 +21,7 @@ gem "bourbon", "~> 1.4" gem "buffer", github: "bufferapp/buffer-ruby" gem "carrierwave", "~> 1.2" gem "carrierwave-bombshelter", "~> 0.2" -gem "cloudinary", "~> 1.8" +gem "cloudinary", "~> 1.9" gem "counter_culture", "~> 1.9" gem "csv_shaper", "~> 1.3" gem "dalli", "~> 2.7" diff --git a/Gemfile.lock b/Gemfile.lock index 7707f9f1e..af8c30587 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -169,7 +169,7 @@ GEM chromedriver-helper (1.2.0) archive-zip (~> 0.10) nokogiri (~> 1.8) - cloudinary (1.8.3) + cloudinary (1.9.1) aws_cf_signer rest-client coderay (1.1.2) @@ -930,7 +930,7 @@ DEPENDENCIES carrierwave (~> 1.2) carrierwave-bombshelter (~> 0.2) chromedriver-helper (~> 1.2) - cloudinary (~> 1.8) + cloudinary (~> 1.9) counter_culture (~> 1.9) csv_shaper (~> 1.3) dalli (~> 2.7) diff --git a/app/controllers/admin/application_controller.rb b/app/controllers/admin/application_controller.rb index a1ccb06ce..aab00b3c7 100644 --- a/app/controllers/admin/application_controller.rb +++ b/app/controllers/admin/application_controller.rb @@ -20,7 +20,6 @@ module Admin request.origin.nil? || request.origin.gsub("https", "http") == request.base_url.gsub("https", "http") end - private def authorize_admin diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 8f9665ca6..236a89420 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -1,26 +1,34 @@ class ChatChannelMembershipsController < ApplicationController + after_action :verify_authorized + def create - @chat_channel = ChatChannel.find(params[:chat_channel_id]) + @chat_channel = ChatChannel.find(permitted_params[:chat_channel_id]) authorize @chat_channel, :update? ChatChannelMembership.create( - user_id: params[:user_id], + user_id: permitted_params[:user_id], chat_channel_id: @chat_channel.id, status: "pending" ) + @chat_channel.index! end def update @chat_channel_membership = ChatChannelMembership.find(params[:id]) authorize @chat_channel_membership - if params[:user_action] == "accept" + if permitted_params[:user_action] == "accept" @chat_channel_membership.update(status: "active") else @chat_channel_membership.update(status: "rejected") end + @chat_channel_membership.chat_channel.index! @chat_channels_memberships = current_user. chat_channel_memberships.includes(:chat_channel). where(status: "pending"). order("chat_channel_memberships.updated_at DESC") render "chat_channels/index.json" end + + def permitted_params + params.require(:chat_channel_membership).permit(:user_id, :chat_channel_id, :user_action, :id) + end end \ No newline at end of file diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 16e929f11..cae47719f 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -91,7 +91,7 @@ class ChatChannelsController < ApplicationController if current_user @chat_channels_memberships = current_user. chat_channel_memberships.includes(:chat_channel). - where(chat_channels: {channel_type: "direct"}, has_unopened_messages: true). + where(has_unopened_messages: true). order("chat_channel_memberships.updated_at DESC") else @chat_channels_memberships = [] diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index 19d6c9a04..aa663b89f 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -164,7 +164,9 @@ export function sendChannelInviteAction(id, action, successCb, failureCb) { 'Content-Type': 'application/json', }, body: JSON.stringify({ - user_action: action, + chat_channel_membership: { + user_action: action, + } }), credentials: 'same-origin', }) diff --git a/app/javascript/chat/channelDetails.jsx b/app/javascript/chat/channelDetails.jsx index e5a852532..4b7cb055e 100644 --- a/app/javascript/chat/channelDetails.jsx +++ b/app/javascript/chat/channelDetails.jsx @@ -2,7 +2,72 @@ import { h, Component } from 'preact'; +const algoliaId = document.querySelector("meta[name='algolia-public-id']").content +const algoliaKey = document.querySelector("meta[name='algolia-public-key']").content +const env = document.querySelector("meta[name='environment']").content +const client = algoliasearch(algoliaId, algoliaKey); +const index = client.initIndex('searchables_'+env); + export default class ChannelDetails extends Component { + constructor(props) { + super(props); + this.state = { + searchedUsers: [], + invitations: [] + } + } + + triggerUserSearch = e => { + const component = this; + const query = e.target.value; + const filters = { + hitsPerPage: 20, + attributesToRetrieve: [ + 'id', + 'title', + 'path' + ], + attributesToHighlight: [], + filters: 'class_name:User', + } + if (query.length > 0) { + index.search(query, filters) + .then(function(content) { + component.setState({searchedUsers:content.hits}) + }); + } else { + component.setState({searchedUsers:[]}) + } + } + + triggerInvite = e => { + const component = this; + const id = e.target.dataset.content; + console.log(e.target) + e.target.style.display = 'none' + fetch(`/chat_channel_memberships`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + chat_channel_membership: { + user_id: id, + chat_channel_id: component.props.channel.id, + } + }), + credentials: 'same-origin', + }) + .then(response => response) + .then(this.handleInvitationSuccess) + .catch(null); + } + + handleInvitationSuccess = response => { + console.log(response) + } render() { const channel = this.props.channel @@ -21,10 +86,30 @@ export default class ChannelDetails extends Component { if (users.length === 80) { subHeader =

Recently Active Members

} + let modSection = '' + let searchedUsers = []; + let pendingInvites = []; + if (channel.channel_mod_ids.includes(window.currentUser.id)) { + searchedUsers = this.state.searchedUsers.map((user) => { + return
{user.title}
+ }) + pendingInvites = channel.pending_usernames.map((username) => { + return
@{username}
+ }) + modSection =
+

Invite Members

+ + {searchedUsers} +

Pending Invites:

+ {pendingInvites} +
Channels can have a maximum of 128 members, including outstanding invites. All functionality is early beta. Contact us if you need help with anything or to have any restrictions lifted.
+
+ } return

{channel.channel_name}

{subHeader} {users} + {modSection}
} diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index 66e4aae40..4bebf766f 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -81,6 +81,7 @@ export default class Chat extends Component { document.getElementById('chatchannels__channelslist').addEventListener('scroll', this.handleChannelScroll); } getChannelInvites(this.handleChannelInvites,null); + } componentDidUpdate() { @@ -674,6 +675,7 @@ export default class Chat extends Component { render() { + console.log(this.state.activeChannel) let channelHeader =
 
let channelHeaderInner = '' const currentChannel = this.state.activeChannel diff --git a/app/labor/profile_image.rb b/app/labor/profile_image.rb index 14ffea9b8..e1e78626a 100644 --- a/app/labor/profile_image.rb +++ b/app/labor/profile_image.rb @@ -1,4 +1,5 @@ class ProfileImage + include CloudinaryHelper attr_accessor :resource, :image_link, :backup_link def initialize(resource) @@ -8,7 +9,7 @@ class ProfileImage end def get(width = 120) - CloudinaryHelper.cl_image_path(get_link, + cl_image_path(get_link, type: "fetch", crop: "fill", width: width, diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index 784acffe6..eaa51ae71 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -9,9 +9,11 @@ class ChatChannel < ApplicationRecord has_many :active_memberships, -> { where status: "active" }, class_name: 'ChatChannelMembership' has_many :pending_memberships, -> { where status: "pending" }, class_name: 'ChatChannelMembership' has_many :rejected_memberships, -> { where status: "rejected" }, class_name: 'ChatChannelMembership' + has_many :mod_memberships, -> { where role: "mod" }, class_name: 'ChatChannelMembership' has_many :active_users, :through => :active_memberships, class_name: "User", :source => :user has_many :pending_users, :through => :pending_memberships, class_name: "User", :source => :user has_many :rejected_users, :through => :rejected_memberships, class_name: "User", :source => :user + has_many :mod_users, :through => :mod_memberships, class_name: "User", :source => :user validates :channel_type, presence: true, inclusion: { in: %w(open invite_only direct) } validates :status, presence: true, inclusion: { in: %w(active inactive) } @@ -20,7 +22,7 @@ class ChatChannel < ApplicationRecord algoliasearch index_name: "SecuredChatChannel_#{Rails.env}" do attribute :id, :viewable_by, :slug, :channel_type, :channel_name, :channel_users, :last_message_at, :status, - :messages_count, :channel_human_names + :messages_count, :channel_human_names, :channel_mod_ids, :pending_usernames searchableAttributes [:channel_name, :channel_slug, :channel_human_names] attributesForFaceting ["filterOnly(viewable_by)", "filterOnly(status)", "filterOnly(channel_type)"] ranking ["desc(last_message_at)"] @@ -114,17 +116,30 @@ class ChatChannel < ApplicationRecord def channel_users # Purely for algolia indexing - pics_obj = {} + obj = {} active_memberships. order("last_opened_at DESC").limit(80).includes(:user).each_with_index do |m, i| - pics_obj[m.user.username] = { - profile_image: i < 11 ? ProfileImage.new(m.user).get(90) : nil, - darker_color: m.user.decorate.darker_color, - name: m.user.name, - last_opened_at: m.last_opened_at, - username: m.user.username, - } + obj[m.user.username] = user_obj(m, i) end - pics_obj + obj + end + + def pending_usernames + pending_users.pluck(:username) + end + + def channel_mod_ids + mod_users.pluck(:id) + end + + def user_obj(m, i) + { + profile_image: i < 11 ? ProfileImage.new(m.user).get(90) : nil, + darker_color: m.user.decorate.darker_color, + name: m.user.name, + last_opened_at: m.last_opened_at, + username: m.user.username, + id: m.user_id, + } end end diff --git a/app/models/chat_channel_membership.rb b/app/models/chat_channel_membership.rb index 48959af92..4d19c7f54 100644 --- a/app/models/chat_channel_membership.rb +++ b/app/models/chat_channel_membership.rb @@ -5,6 +5,7 @@ class ChatChannelMembership < ApplicationRecord validates :user_id, presence: true, uniqueness: { scope: :chat_channel_id } validates :chat_channel_id, presence: true, uniqueness: { scope: :user_id } validates :status, inclusion: { in: %w{active inactive pending rejected left_channel} } + validates :role, inclusion: { in: %w{member mod} } validate :permission private diff --git a/app/policies/chat_channel_policy.rb b/app/policies/chat_channel_policy.rb index 34eb5a1c4..62a88d402 100644 --- a/app/policies/chat_channel_policy.rb +++ b/app/policies/chat_channel_policy.rb @@ -32,7 +32,8 @@ class ChatChannelPolicy < ApplicationPolicy private def user_can_edit_channel - record.present? && user.has_role?(:super_admin) + record.present? && + (user.has_role?(:super_admin) || record.channel_mod_ids.include?(user.id)) end def user_part_of_channel_or_open diff --git a/app/views/layouts/application.html.erb b/app/views/layouts/application.html.erb index 761b00e0a..e7109b313 100644 --- a/app/views/layouts/application.html.erb +++ b/app/views/layouts/application.html.erb @@ -13,6 +13,10 @@ <% else %> + "> + "> + + <%= render "layouts/styles" %>