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
This commit is contained in:
parent
a3788dfbd1
commit
57d1bc48d5
7 changed files with 191 additions and 75 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 <div>
|
||||
const channel = this.props.channel;
|
||||
const users = Object.values(channel.channel_users).map(user => (
|
||||
<div>
|
||||
<a
|
||||
href={'/'+user.username}
|
||||
style={{color:user.darker_color,padding: "3px 0px"}}
|
||||
href={`/${ user.username}`}
|
||||
style={{ color: user.darker_color, padding: '3px 0px' }}
|
||||
data-content={`users/by_username?url=${user.username}`}
|
||||
>
|
||||
>
|
||||
{user.name}
|
||||
</a>
|
||||
</div>
|
||||
});
|
||||
let subHeader = ''
|
||||
));
|
||||
let subHeader = '';
|
||||
if (users.length === 80) {
|
||||
subHeader = <h3>Recently Active Members</h3>
|
||||
subHeader = <h3>Recently Active Members</h3>;
|
||||
}
|
||||
let modSection = ''
|
||||
let modSection = '';
|
||||
let searchedUsers = [];
|
||||
let pendingInvites = [];
|
||||
if (channel.channel_mod_ids.includes(window.currentUser.id)) {
|
||||
searchedUsers = this.state.searchedUsers.map((user) => {
|
||||
return <div><a href={user.path} target='_blank'>{user.title}</a> <button onClick={this.triggerInvite} data-content={user.id}>Invite</button></div>
|
||||
})
|
||||
pendingInvites = channel.pending_users_select_fields.map((user) => {
|
||||
return <div><a href={'/'+user.username} target='_blank' data-content={'users/'+user.id}>@{user.username} - {user.name}</a></div>
|
||||
})
|
||||
modSection = <div>
|
||||
<h2>Invite Members</h2>
|
||||
<input onKeyUp={this.triggerUserSearch} placeholder="Find users"/>
|
||||
{searchedUsers}
|
||||
<h2>Pending Invites:</h2>
|
||||
{pendingInvites}
|
||||
<div style={{marginTop: "10px"}}>All functionality is early beta. Contact us if you need help with anything.</div>
|
||||
</div>
|
||||
}
|
||||
return <div>
|
||||
<h1>{channel.channel_name}</h1>
|
||||
{subHeader}
|
||||
<div style={{marginBottom: '20px'}}>
|
||||
<em>{channel.description || ''}</em>
|
||||
</div>
|
||||
{users}
|
||||
{modSection}
|
||||
</div>
|
||||
searchedUsers = this.state.searchedUsers.map(user => (
|
||||
<div>
|
||||
<a href={user.path} target="_blank">
|
||||
{user.title}
|
||||
</a>{' '}
|
||||
<button onClick={this.triggerInvite} data-content={user.id}>
|
||||
Invite
|
||||
</button>
|
||||
</div>
|
||||
));
|
||||
pendingInvites = channel.pending_users_select_fields.map(user => (
|
||||
<div>
|
||||
<a
|
||||
href={`/${ user.username}`}
|
||||
target="_blank"
|
||||
data-content={`users/${ user.id}`}
|
||||
>
|
||||
@{user.username} - {user.name}
|
||||
</a>
|
||||
</div>
|
||||
));
|
||||
modSection = (
|
||||
<div>
|
||||
<h2>Invite Members</h2>
|
||||
<input onKeyUp={this.triggerUserSearch} placeholder="Find users" />
|
||||
{searchedUsers}
|
||||
<h2>Pending Invites:</h2>
|
||||
{pendingInvites}
|
||||
<div style={{ marginTop: '10px' }}>
|
||||
All functionality is early beta. Contact us if you need help with
|
||||
anything.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
} else if (this.state.hasLeftChannel) {
|
||||
modSection = (
|
||||
<div>
|
||||
<h2>Danger Zone</h2>
|
||||
<h3>You have left this channel 😢😢😢</h3>
|
||||
<h4>It may not be immediately in the sidebar</h4>
|
||||
<p>
|
||||
Contact the admins at <a href="mailto:yo@dev.to">yo@dev.to</a> if
|
||||
this was a mistake
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
modSection = (
|
||||
<div>
|
||||
<h2>Danger Zone</h2>
|
||||
<button
|
||||
onClick={this.triggerLeaveChannel}
|
||||
data-content={channel.id}
|
||||
>
|
||||
Leave Channel.
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div>
|
||||
<h1>{channel.channel_name}</h1>
|
||||
{subHeader}
|
||||
<div style={{ marginBottom: '20px' }}>
|
||||
<em>{channel.description || ''}</em>
|
||||
</div>
|
||||
{users}
|
||||
{modSection}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
<Chat
|
||||
pusherKey={pusherKey}
|
||||
|
|
@ -32,14 +43,29 @@ document.ready.then(
|
|||
algoliaIndex={algoliaIndex}
|
||||
githubToken={githubToken}
|
||||
/>,
|
||||
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(
|
||||
<Chat
|
||||
pusherKey={pusherKey}
|
||||
chatChannels={chatChannels}
|
||||
chatOptions={chatOptions}
|
||||
algoliaId={algoliaId}
|
||||
algoliaKey={algoliaKey}
|
||||
algoliaIndex={algoliaIndex}
|
||||
githubToken={githubToken}
|
||||
/>,
|
||||
renderTarget,
|
||||
renderTarget.firstChild,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
}),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
class ChatChannelMembershipPolicy < ApplicationPolicy
|
||||
|
||||
def update?
|
||||
record.present? && user.id == record.user_id
|
||||
end
|
||||
end
|
||||
|
||||
def destroy?
|
||||
record.present? && user.id == record.user_id
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -61,4 +61,18 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue