From 57d1bc48d56cabc0b9bc2aef2ef6e73f0cd0884c Mon Sep 17 00:00:00 2001 From: Ben Halpern Date: Mon, 30 Jul 2018 18:40:30 -0400 Subject: [PATCH] Fix navigation issue in connect and allow leaving channels (#635) * Fix navigation issue in connect and allow leaving channels * Fix chat channel memberships request spec --- .../chat_channel_memberships_controller.rb | 10 + app/javascript/chat/channelDetails.jsx | 183 ++++++++++++------ app/javascript/packs/chat.jsx | 44 ++++- .../chat_channel_membership_policy.rb | 7 +- config/routes.rb | 2 +- .../chat_channel_membership_policy_spec.rb | 4 +- .../requests/chat_channel_memberships_spec.rb | 16 +- 7 files changed, 191 insertions(+), 75 deletions(-) diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 236a89420..3ce538832 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -28,6 +28,16 @@ class ChatChannelMembershipsController < ApplicationController render "chat_channels/index.json" end + def destroy + @chat_channel_membership = ChatChannel.find(params[:id]). + chat_channel_memberships.where(user_id: current_user.id).first + authorize @chat_channel_membership + @chat_channel_membership.update(status: "left_channel") + @chat_channel_membership.chat_channel.index! + @chat_channels_memberships = [] + render json: { result: "left channel" }, status: 201 + end + def permitted_params params.require(:chat_channel_membership).permit(:user_id, :chat_channel_id, :user_action, :id) end diff --git a/app/javascript/chat/channelDetails.jsx b/app/javascript/chat/channelDetails.jsx index 51fe6a7d4..1c424fca4 100644 --- a/app/javascript/chat/channelDetails.jsx +++ b/app/javascript/chat/channelDetails.jsx @@ -1,20 +1,21 @@ - - 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 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); +const index = client.initIndex(`searchables_${ env}`); export default class ChannelDetails extends Component { constructor(props) { super(props); this.state = { searchedUsers: [], - invitations: [] - } + invitations: [], + hasLeftChannel: false, + }; } triggerUserSearch = e => { @@ -22,28 +23,23 @@ export default class ChannelDetails extends Component { const query = e.target.value; const filters = { hitsPerPage: 20, - attributesToRetrieve: [ - 'id', - 'title', - 'path' - ], + 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}) + index.search(query, filters).then((content) => { + component.setState({ searchedUsers: content.hits }); }); } else { - component.setState({searchedUsers:[]}) + component.setState({ searchedUsers: [] }); } - } + }; triggerInvite = e => { const component = this; const id = e.target.dataset.content; - e.target.style.display = 'none' + e.target.style.display = 'none'; fetch(`/chat_channel_memberships`, { method: 'POST', headers: { @@ -55,64 +51,131 @@ export default class ChannelDetails extends Component { chat_channel_membership: { user_id: id, chat_channel_id: component.props.channel.id, - } + }, }), credentials: 'same-origin', }) .then(response => response) .then(this.handleInvitationSuccess) .catch(null); - } + }; + + triggerLeaveChannel = e => { + e.preventDefault(); + const id = e.target.dataset.content; + fetch(`/chat_channel_memberships/${id}`, { + method: 'DELETE', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({}), + credentials: 'same-origin', + }) + .then(response => response) + .then(this.handleLeaveChannelSuccess) + .catch(null); + }; + + handleLeaveChannelSuccess = response => { + this.setState({ hasLeftChannel: true }); + }; handleInvitationSuccess = response => { - console.log(response) - } + console.log(response); + }; render() { - const channel = this.props.channel - const users = Object.values(channel.channel_users).map((user) => { - return
+ const channel = this.props.channel; + const users = Object.values(channel.channel_users).map(user => ( +
+ > {user.name}
- }); - let subHeader = '' + )); + let subHeader = ''; if (users.length === 80) { - subHeader =

Recently Active Members

+ subHeader =

Recently Active Members

; } - let modSection = '' + 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_users_select_fields.map((user) => { - return
@{user.username} - {user.name}
- }) - modSection =
-

Invite Members

- - {searchedUsers} -

Pending Invites:

- {pendingInvites} -
All functionality is early beta. Contact us if you need help with anything.
-
- } - return
-

{channel.channel_name}

- {subHeader} -
- {channel.description || ''} -
- {users} - {modSection} -
+ searchedUsers = this.state.searchedUsers.map(user => ( +
+ + {user.title} + {' '} + +
+ )); + pendingInvites = channel.pending_users_select_fields.map(user => ( +
+ + @{user.username} - {user.name} + +
+ )); + modSection = ( +
+

Invite Members

+ + {searchedUsers} +

Pending Invites:

+ {pendingInvites} +
+ All functionality is early beta. Contact us if you need help with + anything. +
+
+ ); + } else if (this.state.hasLeftChannel) { + modSection = ( +
+

Danger Zone

+

You have left this channel 😢😢😢

+

It may not be immediately in the sidebar

+

+ Contact the admins at yo@dev.to if + this was a mistake +

+
+ ); + } else { + modSection = ( +
+

Danger Zone

+ +
+ ); + } + return ( +
+

{channel.channel_name}

+ {subHeader} +
+ {channel.description || ''} +
+ {users} + {modSection} +
+ ); } - -} \ No newline at end of file +} diff --git a/app/javascript/packs/chat.jsx b/app/javascript/packs/chat.jsx index c6625c2cf..0d265f26b 100644 --- a/app/javascript/packs/chat.jsx +++ b/app/javascript/packs/chat.jsx @@ -10,18 +10,29 @@ HTMLDocument.prototype.ready = new Promise(resolve => { return null; }); -document.ready.then( +loadChat(); // Load initially +window.InstantClick.on('change', () => { + loadChat(); // Load via instantclick nav +}); + +function loadChat() { getUserDataAndCsrfToken().then(currentUser => { if (document.getElementById('chat')) { - const { chatChannels, pusherKey, chatOptions, algoliaKey, algoliaId, algoliaIndex, githubToken } = document.getElementById( - 'chat', - ).dataset; + const { + chatChannels, + pusherKey, + chatOptions, + algoliaKey, + algoliaId, + algoliaIndex, + githubToken, + } = document.getElementById('chat').dataset; window.currentUser = currentUser; window.csrfToken = document.querySelector( "meta[name='csrf-token']", ).content; const renderTarget = document.getElementById('chat'); - const placeholder = document.getElementById('chat_placeholder') + const placeholder = document.getElementById('chat_placeholder'); const root = render( , - renderTarget, renderTarget.firstChild + renderTarget, + renderTarget.firstChild, ); if (placeholder) { renderTarget.removeChild(placeholder); } window.InstantClick.on('change', () => { - render('', document.body, root); + if (window.location.href.indexOf('/connect') != -1) { + const root = render( + , + renderTarget, + renderTarget.firstChild, + ); + } }); } - }), -); + }); +} diff --git a/app/policies/chat_channel_membership_policy.rb b/app/policies/chat_channel_membership_policy.rb index 9f93edaf8..bd6a32bc6 100644 --- a/app/policies/chat_channel_membership_policy.rb +++ b/app/policies/chat_channel_membership_policy.rb @@ -1,6 +1,9 @@ class ChatChannelMembershipPolicy < ApplicationPolicy - def update? record.present? && user.id == record.user_id end -end \ No newline at end of file + + def destroy? + record.present? && user.id == record.user_id + end +end diff --git a/config/routes.rb b/config/routes.rb index 519c456ac..9f30ede31 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -73,7 +73,7 @@ Rails.application.routes.draw do resources :messages, only: [:create] resources :chat_channels, only: [:index, :show, :create, :update] - resources :chat_channel_memberships, only: [ :create, :update] + resources :chat_channel_memberships, only: [ :create, :update, :destroy] resources :articles, only: [:update,:create,:destroy] resources :comments, only: [:create,:update,:destroy] resources :users, only: [:update] diff --git a/spec/policies/chat_channel_membership_policy_spec.rb b/spec/policies/chat_channel_membership_policy_spec.rb index f8d8980ea..afa75a17b 100644 --- a/spec/policies/chat_channel_membership_policy_spec.rb +++ b/spec/policies/chat_channel_membership_policy_spec.rb @@ -15,13 +15,13 @@ RSpec.describe ChatChannelMembershipPolicy do let(:user) { create(:user) } let(:chat_channel) { create(:chat_channel ) } let(:chat_channel_membership) { create(:chat_channel_membership, user_id: user.id, chat_channel_id: chat_channel.id ) } - it { is_expected.to permit_actions(%i[update]) } + it { is_expected.to permit_actions(%i[update destroy]) } end context "when user does not belong to membership" do let(:user) { create(:user) } let(:other_user) { create(:user) } let(:chat_channel) { create(:chat_channel ) } let(:chat_channel_membership) { create(:chat_channel_membership, user_id: other_user.id, chat_channel_id: chat_channel.id ) } - it { is_expected.to forbid_actions(%i[update]) } + it { is_expected.to forbid_actions(%i[update destroy]) } end end \ No newline at end of file diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb index acf6841c1..254d5dc04 100644 --- a/spec/requests/chat_channel_memberships_spec.rb +++ b/spec/requests/chat_channel_memberships_spec.rb @@ -61,4 +61,18 @@ RSpec.describe "ChatChannelMemberships", type: :request do end.to raise_error(Pundit::NotAuthorizedError) end end -end \ No newline at end of file + + describe "DELETE /chat_channel_memberships/:id" do + before do + user.add_role(:super_admin) + post "/chat_channel_memberships", params: { + chat_channel_membership: {user_id: second_user.id, chat_channel_id: chat_channel.id}} + end + it "leaves chat channel" do + membership = ChatChannelMembership.last + sign_in second_user + delete "/chat_channel_memberships/#{membership.chat_channel.id}", params: {} + expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel") + end + end +end