[deploy] Refactor 🚀 : Replaced Chat Channel Setting page with Preact component. (#8271)

* Feature 🚀 : Ability to delete messages in chat channels

- Sending message ID to frontend
- Deleting Message
- Use pusher to delete message realtime

* Minor Bug 🐞: Show message action only for current user

- User can delete or edit their own messages

* Test cases added

* Bug 🐞: Update message id for receiver

Message id was not sent to receiver by pusher

* Refactoring🛠: Message controller refactoring

* Test Cases📝 : Specs for Delete message added

* Feature 🚀 : Ability to edit messages

* Test Cases📝 : Specs for Edit message added

* 🐞 Channel List bug in mobile view

* changes in joining request responses

* 🛠  Request Managers Frontend ready

* changes in routes for adding and removing remembership

* 🚀 New routes implemented for request manager

* fix issues

* changes in api for remove membership

* Merge conflict resolved

* 🐞 Channel List bug in mobile view

* changes in joining request responses

* 🛠  Request Managers Frontend ready

* changes in routes for adding and removing remembership

* 🚀 New routes implemented for request manager

* fix issues

* changes in api for remove membership

* 🛠Optimizing for CodeClimate

* 🛠Optimizing again for CodeClimate

* 🛠Optimizing again 2 for CodeClimate

* 🛠 Added more test cases

* 🛠 Optimizing code

* 🚀 Action to open setting page added

* 🛠  Dummy page added for chat setting

* add JSON routes for chat channel settings

* Integrate chat channel settings API with UI

* 🐞 Channel List bug in mobile view

* 🛠  Request Managers Frontend ready

* changes in routes for adding and removing remembership

* 🚀 New routes implemented for request manager

* fix issues

* 🐞 Channel List bug in mobile view

* 🛠  Request Managers Frontend ready

* changes in routes for adding and removing remembership

* 🚀 New routes implemented for request manager

* fix issues

* 🛠Optimizing again for CodeClimate

* 🚀 Action to open setting page added

* 🛠  Dummy page added for chat setting

* add JSON routes for chat channel settings

* Integrate chat channel settings API with UI

* Fix PR requested changes

* Add JSDoc documentation to exported functions

* fix/add rspec test cases

* refactor channelSettinngs render part

* add test cases for chat channel settings component

* fix code-climate

* add rest component tests

* add more test coverage

* fix code-climate bugs

* add API function test

Co-authored-by: Sarthak Sharma <7lovesharma7@gmail.com>
Co-authored-by: Parasgr-code <paras.gaur@skynox.tech>
This commit is contained in:
narender2031 2020-06-11 02:28:37 +05:30 committed by GitHub
parent 2d2cfe4cab
commit e78a0078a7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 2677 additions and 353 deletions

View file

@ -1274,3 +1274,7 @@
.action button:nth-child(2n) {
margin-left: 10px;
}
.channel_details {
margin-top: 30px;
}

View file

@ -2,6 +2,9 @@ class ChatChannelMembershipsController < ApplicationController
after_action :verify_authorized, except: :join_channel
include MessagesHelper
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
def index
skip_authorization
@pending_invites = current_user.chat_channel_memberships.includes(:chat_channel).where(status: "pending")
@ -17,26 +20,28 @@ class ChatChannelMembershipsController < ApplicationController
)
end
def edit
def chat_channel_info
@membership = ChatChannelMembership.find(params[:id])
@channel = @membership.chat_channel
authorize @membership
@channel = @membership.chat_channel
data = ChatChannelDetailPresenter.new(@channel, @membership).as_json
render json: { success: true, result: data, message: "" }, success: :ok
end
def create
membership_params = params[:chat_channel_membership]
@chat_channel = ChatChannel.find(membership_params[:chat_channel_id])
authorize @chat_channel, :update?
usernames = membership_params[:invitation_usernames].split(",").map { |username| username.strip.delete("@") }
def create_membership_request
chat_channel = ChatChannel.find_by(id: channel_membership_request_params[:chat_channel_id])
authorize chat_channel, :update?
usernames = channel_membership_request_params[:invitation_usernames].split(",").map { |username| username.strip.delete("@") }
users = User.where(username: usernames)
invitations_sent = @chat_channel.invite_users(users: users, membership_role: "member", inviter: current_user)
flash[:settings_notice] = if invitations_sent.zero?
"No invitations sent. Check for username typos."
else
"#{invitations_sent} #{'invitation'.pluralize(invitations_sent)} sent."
end
membership = @chat_channel.chat_channel_memberships.find_by!(user: current_user)
redirect_to edit_chat_channel_membership_path(membership)
invitations_sent = chat_channel.invite_users(users: users, membership_role: "member", inviter: current_user)
message = if invitations_sent.zero?
"No invitations sent. Check for username typos."
else
"#{invitations_sent} #{'invitation'.pluralize(invitations_sent)} sent."
end
render json: { success: true, message: message, data: {} }, status: :ok
end
def join_channel
@ -50,7 +55,7 @@ class ChatChannelMembershipsController < ApplicationController
status = membership.save
end
if status
render json: { status: "success", message: "Request Sent" }
render json: { status: "success", message: "Request Sent" }, status: :ok
else
render json: { status: 400, message: "Unable to join channel" }, status: :bad_request
end
@ -60,20 +65,16 @@ class ChatChannelMembershipsController < ApplicationController
@chat_channel = ChatChannel.find(params[:chat_channel_id])
authorize @chat_channel, :update?
@chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id])
membership = ChatChannelMembership.find_by!(chat_channel_id: params[:chat_channel_id], user: current_user)
if params[:status] == "pending"
@chat_channel_membership.destroy
flash[:settings_notice] = "Invitation removed."
message = "Invitation removed."
else
send_chat_action_message("@#{current_user.username} removed @#{@chat_channel_membership.user.username} from #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "removed_from_channel")
@chat_channel_membership.update(status: "removed_from_channel")
flash[:settings_notice] = "Removed #{@chat_channel_membership.user.name}"
respond_to do |format|
format.html { redirect_to edit_chat_channel_membership_path(membership) }
format.json { render json: { status: "success", message: "Membership removed" } }
end && return
message = "Removed #{@chat_channel_membership.user.name}"
end
redirect_to edit_chat_channel_membership_path(membership)
render json: { status: "success", message: message, success: true }, status: :ok
end
def add_membership
@ -86,24 +87,32 @@ class ChatChannelMembershipsController < ApplicationController
def update
@chat_channel_membership = ChatChannelMembership.find(params[:id])
authorize @chat_channel_membership
if permitted_params[:user_action].present?
respond_to_invitation(@chat_channel_membership.status)
respond_to_invitation(@chat_channel_membership.status)
end
def update_membership
@chat_channel_membership = ChatChannelMembership.find(params[:id])
authorize @chat_channel_membership
@chat_channel_membership.update(permitted_params)
if @chat_channel_membership.errors.any?
render json: { success: false, errors: @chat_channel_membership.errors.full_messages, message: "Failed to update settings." }, status: :bad_request
else
@chat_channel_membership.update(permitted_params)
flash[:settings_notice] = "Personal settings updated."
redirect_to edit_chat_channel_membership_path(@chat_channel_membership.id)
render json: { success: true, message: "Personal settings updated." }, status: :ok
end
end
def destroy
@chat_channel_membership = ChatChannelMembership.find(params[:id])
authorize @chat_channel_membership
channel_name = @chat_channel_membership.chat_channel.channel_name
send_chat_action_message("@#{current_user.username} left #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "left_channel")
@chat_channel_membership.update(status: "left_channel")
@chat_channels_memberships = []
flash[:settings_notice] = "You have left the channel #{channel_name}. It may take a moment to be removed from your list."
redirect_to chat_channel_memberships_path
def leave_membership
chat_channel_membership = ChatChannelMembership.find_by(id: params[:id])
authorize chat_channel_membership
channel_name = chat_channel_membership.chat_channel.channel_name
send_chat_action_message("@#{current_user.username} left #{chat_channel_membership.channel_name}", current_user, chat_channel_membership.chat_channel_id, "left_channel")
chat_channel_membership.update(status: "left_channel")
message = "You have left the channel #{channel_name}. It may take a moment to be removed from your list."
if chat_channel_membership.errors.any?
render json: { success: false, message: "Failed to update membership", errors: chat_channel_membership.errors.full_messages }, status: :bad_request
else
render json: { success: true, message: message }, status: :ok
end
end
private
@ -112,6 +121,10 @@ class ChatChannelMembershipsController < ApplicationController
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
end
def channel_membership_request_params
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames)
end
def respond_to_invitation(previous_status)
if permitted_params[:user_action] == "accept"
@chat_channel_membership.update(status: "active")
@ -121,21 +134,20 @@ class ChatChannelMembershipsController < ApplicationController
flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list."
else
send_chat_action_message("@#{current_user.username} added @#{@chat_channel_membership.user.username}", current_user, @chat_channel_membership.chat_channel_id, "joined")
NotifyMailer.with(membership: @chat_channel_membership, inviter: @chat_channel_membership.user).channel_invite_email.deliver_later
NotifyMailer.channel_invite_email(@chat_channel_membership, @chat_channel_membership.user).deliver_later
flash[:settings_notice] = "Accepted request of #{@chat_channel_membership.user.username} to join #{channel_name}."
membership = ChatChannelMembership.find_by!(chat_channel_id: @chat_channel_membership.chat_channel.id, user: current_user)
respond_to do |format|
format.html { redirect_to(edit_chat_channel_membership_path(membership)) }
format.json { render json: { status: "success", message: "Accepted Request" } }
end && return
end
else
@chat_channel_membership.update(status: "rejected")
flash[:settings_notice] = "Invitation rejected."
end
redirect_to chat_channel_memberships_path
membership_user = MembershipUserPresenter.new(@chat_channel_membership).as_json
respond_to do |format|
format.html { redirect_to chat_channel_memberships_path }
format.json { render json: { status: "success", message: flash[:settings_notice], success: true, membership: membership_user }, status: :ok }
end
end
def send_chat_action_message(message, user, channel_id, action)
@ -143,4 +155,12 @@ class ChatChannelMembershipsController < ApplicationController
message = Message.create("message_markdown" => message, "user_id" => user.id, "chat_channel_id" => channel_id, "chat_action" => action)
pusher_message_created(false, message, temp_message_id)
end
def user_not_authorized
render json: { success: false, message: "User not authorized" }, status: :unauthorized
end
def record_not_found
render json: { success: false, message: "not found" }, status: :not_found
end
end

View file

@ -1,6 +1,6 @@
class ChatChannelsController < ApplicationController
before_action :authenticate_user!, only: %i[moderate]
before_action :set_channel, only: %i[show update open moderate]
before_action :set_channel, only: %i[show update update_channel open moderate]
after_action :verify_authorized
def index
@ -33,21 +33,36 @@ class ChatChannelsController < ApplicationController
end
def update
if ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update
if !chat_channel_params[:discoverable].to_i.zero?
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: @chat_channel.id, role: "member", status: "active")
chat_channel = ChatChannelUpdateService.perform(@chat_channel, chat_channel_params)
if chat_channel.errors.any?
flash[:error] = chat_channel.errors.full_messages.to_sentence
else
if chat_channel_params[:discoverable].to_i.zero?
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: chat_channel.id, role: "member", status: "active")
else
ChatChannelMembership.find_by(user_id: SiteConfig.mascot_user_id)&.destroy
end
flash[:settings_notice] = "Channel settings updated."
else
default_error_message = "Channel settings updation failed. Try again later."
flash[:error] = @chat_channel.errors_as_sentence.presence || default_error_message
end
current_user_membership = @chat_channel.mod_memberships.find_by!(user: current_user)
redirect_to edit_chat_channel_membership_path(current_user_membership)
end
def update_channel
chat_channel = ChatChannelUpdateService.perform(@chat_channel, chat_channel_params)
if chat_channel.errors.any?
render json: { success: false, errors: chat_channel.errors.full_messages, message: "Channel settings updation failed. Try again later." }, success: :bad_request
else
if chat_channel_params[:discoverable]
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: chat_channel.id, role: "member", status: "active")
else
ChatChannelMembership.find_by(user_id: SiteConfig.mascot_user_id)&.destroy
end
render json: { success: true, message: "Channel settings updated.", data: {} }, success: :ok
end
end
def open
membership = @chat_channel.chat_channel_memberships.where(user_id: current_user.id).first
membership.update(last_opened_at: 1.second.from_now, has_unopened_messages: false)

View file

@ -0,0 +1,44 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import Membership from './Membership';
const ActiveMembershipSection = ({
activeMemberships,
removeMembership,
currentMembershipRole,
}) => {
return (
<div className="p-4 grid gap-2 crayons-card mb-4">
<h3 className="mb-2 active_members">Members</h3>
{activeMemberships && activeMemberships.length > 0
? activeMemberships.map((pendingMembership) => (
<Membership
membership={pendingMembership}
removeMembership={removeMembership}
membershipType="active"
currentMembershipRole={currentMembershipRole}
className="active-member"
/>
))
: null}
</div>
);
};
ActiveMembershipSection.propTypes = {
activeMemberships: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
}),
).isRequired,
removeMembership: PropTypes.func.isRequired,
currentMembershipRole: PropTypes.string.isRequired,
};
export default ActiveMembershipSection;

View file

@ -0,0 +1,28 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const ChannelDescriptionSection = ({
channelName,
channelDescription,
currentMembershipRole
}) => {
return (
<div className="p-4 grid gap-2 crayons-card mb-4 channel_details">
<h1 className="mb-1 channel_title">{channelName}</h1>
<p>{channelDescription}</p>
<p className="fw-bold">
You are a channel
{' '}
{currentMembershipRole}
</p>
</div>
)
}
ChannelDescriptionSection.propTypes = {
channelName: PropTypes.string.isRequired,
currentMembershipRole: PropTypes.string.isRequired,
channelDescription: PropTypes.string.isRequired
}
export default ChannelDescriptionSection;

View file

@ -0,0 +1,69 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import ActiveMembershipSection from './ActiveMembershipsSection';
import PendingMembershipSection from './PendingMembershipSection'
import RequestedMembershipSection from './RequestedMembershipSection';
const ChatChannelMembershipSection = ({
pendingMemberships,
requestedMemberships,
chatChannelAcceptMembership,
activeMemberships,
removeMembership,
currentMembershipRole
}) => {
return (
<div className="membership-list">
<ActiveMembershipSection
activeMemberships={activeMemberships}
removeMembership={removeMembership}
currentMembershipRole={currentMembershipRole}
/>
<PendingMembershipSection
pendingMemberships={pendingMemberships}
removeMembership={removeMembership}
currentMembershipRole={currentMembershipRole}
/>
<RequestedMembershipSection
requestedMemberships={requestedMemberships}
removeMembership={removeMembership}
chatChannelAcceptMembership={chatChannelAcceptMembership}
currentMembershipRole={currentMembershipRole}
/>
</div>
)
}
ChatChannelMembershipSection.propTypes = {
pendingMemberships: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
})).isRequired,
requestedMemberships: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
})).isRequired,
activeMemberships: PropTypes.arrayOf(PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
})).isRequired,
removeMembership: PropTypes.func.isRequired,
chatChannelAcceptMembership: PropTypes.func.isRequired,
currentMembershipRole: PropTypes.string.isRequired
}
export default ChatChannelMembershipSection;

View file

@ -0,0 +1,378 @@
import { h, Component, render } from 'preact';
import PropTypes from 'prop-types';
import {
getChannelDetails,
updatePersonalChatChannelNotificationSettings,
rejectChatChannelJoiningRequest,
acceptChatChannelJoiningRequest,
updateChatChannelDescription,
sendChatChannelInvitation,
leaveChatChannelMembership,
} from '../actions/chat_channel_setting_actions';
import { Snackbar, addSnackbarItem } from '../../Snackbar';
import ModSection from './ModSection';
import PersonalSettings from './PersonalSetting';
import LeaveMembershipSection from './LeaveMembershipSection';
import ModFaqSection from './ModFaqSection';
import ChannelDescriptionSection from './ChannelDescriptionSection';
import ChatChannelMembershipSection from './ChatChannelMembershipSection';
const snackZone = document.getElementById('snack-zone');
render(<Snackbar lifespan="3" />, snackZone, null);
export default class ChatChannelSettings extends Component {
static propTypes = {
activeMembershipId: PropTypes.number.isRequired,
};
constructor(props) {
super(props);
this.state = {
successMessages: null,
errorMessages: null,
activeMemberships: [],
pendingMemberships: [],
requestedMemberships: [],
chatChannel: null,
currentMembership: null,
activeMembershipId: props.activeMembershipId,
channelDescription: null,
channelDiscoverable: null,
invitationUsernames: null,
showGlobalBadgeNotification: null,
};
}
componentDidMount() {
const { activeMembershipId } = this.state;
getChannelDetails(activeMembershipId)
.then((response) => {
if (response.success) {
const { result } = response;
this.setState({
chatChannel: result.chat_channel,
activeMemberships: result.memberships.active,
pendingMemberships: result.memberships.pending,
requestedMemberships: result.memberships.requested,
currentMembership: result.current_membership,
channelDescription: result.chat_channel.description,
channelDiscoverable: result.chat_channel.discoverable,
showGlobalBadgeNotification:
result.current_membership.show_global_badge_notification,
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
});
}
})
.catch((error) => {
this.setState({
successMessages: null,
errorMessages: error.message,
});
});
}
handleDescriptionChange = (e) => {
const description = e.target.value;
this.setState({
channelDescription: description,
});
};
handlePersonChatChennelSetting = (e) => {
const status = e.target.checked;
this.setState({
showGlobalBadgeNotification: status,
});
};
updateCurrentMembershipNotificationSettings = async () => {
const { currentMembership, showGlobalBadgeNotification } = this.state;
const response = await updatePersonalChatChannelNotificationSettings(
currentMembership.id,
showGlobalBadgeNotification,
);
const { message } = response;
if (response.success) {
this.setState((prevState) => {
return {
errorMessages: null,
successMessages: response.message,
currentMembership: {
...prevState.currentMembership,
show_global_badge_notification: showGlobalBadgeNotification,
},
};
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
showGlobalBadgeNotification:
currentMembership.show_global_badge_notification,
});
}
addSnackbarItem({ message });
};
chatChannelRemoveMembership = async (membershipId, membershipStatus) => {
const { chatChannel } = this.state;
const response = await rejectChatChannelJoiningRequest(
chatChannel.id,
membershipId,
membershipStatus,
);
return response;
};
filterMemberships = (memberships, membershipId) => {
const filteredMembership = memberships.filter(
(membership) => membership.membership_id !== Number(membershipId),
);
return filteredMembership;
};
removeMembership = async (e) => {
const { membershipId, membershipStatus } = e.target.dataset;
const response = await this.chatChannelRemoveMembership(
membershipId,
membershipStatus,
);
const { message } = response;
this.updateMemberships(membershipId, response, membershipStatus);
addSnackbarItem({ message });
};
updateMemberships = (membershipId, response, membershipStatus) => {
if (response.success) {
this.setState((prevState) => {
return {
errorMessages: null,
successMessages: response.message,
activeMemberships:
membershipStatus === 'active'
? this.filterMemberships(
prevState.activeMemberships,
membershipId,
)
: prevState.activeMemberships,
pendingMemberships:
membershipStatus === 'pending'
? this.filterMemberships(
prevState.pendingMemberships,
membershipId,
)
: prevState.pendingMemberships,
requestedMemberships:
membershipStatus === 'joining_request'
? this.filterMemberships(
prevState.requestedMemberships,
membershipId,
)
: prevState.requestedMembership,
};
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
});
}
};
chatChannelAcceptMembership = async (e) => {
const { chatChannel } = this.state;
const { membershipId } = e.target.dataset;
const response = await acceptChatChannelJoiningRequest(
chatChannel.id,
membershipId,
);
const { message } = response;
if (response.success) {
this.setState((prevState) => {
const filteredRequestedMemberships = prevState.requestedMemberships.filter(
(requestedMembership) =>
requestedMembership.membership_id !== Number(membershipId),
);
const updatedActiveMembership = [
...prevState.activeMemberships,
response.membership,
];
return {
errorMessages: null,
successMessages: response.message,
requestedMemberships: filteredRequestedMemberships,
activeMemberships: updatedActiveMembership,
};
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
});
}
addSnackbarItem({ message });
};
handleChannelDiscoverableStatus = (e) => {
const status = e.target.checked;
this.setState({
channelDiscoverable: status,
});
};
handleChannelDescriptionChanges = async () => {
const { chatChannel, channelDescription, channelDiscoverable } = this.state;
const { id } = chatChannel;
const response = await updateChatChannelDescription(
id,
channelDescription,
channelDiscoverable,
);
const { message } = response;
if (response.success) {
this.componentDidMount();
this.setState((prevState) => {
return {
errorMessages: null,
successMessages: response.message,
chatChannel: {
...prevState,
description: channelDescription,
discoverable: channelDiscoverable,
},
};
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
channelDiscoverable: chatChannel.discoverable,
});
}
addSnackbarItem({ message });
};
handleInvitationUsernames = (e) => {
const invitationUsernameValue = e.target.value;
this.setState({
invitationUsernames: invitationUsernameValue,
});
};
handleChatChannelInvitations = async () => {
const { invitationUsernames, chatChannel } = this.state;
const { id } = chatChannel;
const response = await sendChatChannelInvitation(id, invitationUsernames);
const { message } = response;
if (response.success) {
this.componentDidMount();
this.setState({
errorMessages: null,
successMessages: response.message,
invitationUsernames: null,
});
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
});
}
addSnackbarItem({ message });
};
handleleaveChatChannelMembership = async () => {
// eslint-disable-next-line no-restricted-globals
const actionStatus = confirm(
'Are you absolutely sure you want to leave this channel? This action is permanent.',
);
const { currentMembership } = this.state;
if (actionStatus) {
const response = await leaveChatChannelMembership(currentMembership.id);
if (response.success) {
this.componentDidMount();
} else {
this.setState({
successMessages: null,
errorMessages: response.message,
});
}
}
};
render() {
const {
chatChannel,
currentMembership,
activeMemberships,
pendingMemberships,
requestedMemberships,
channelDescription,
channelDiscoverable,
invitationUsernames,
showGlobalBadgeNotification,
} = this.state;
if (!chatChannel) {
return null;
}
return (
<div className="activechatchannel__activeArticle channel_settings">
<div className="p-4">
<ChannelDescriptionSection
channelName={chatChannel.name}
channelDescription={chatChannel.description}
currentMembershipRole={currentMembership.role}
/>
<ChatChannelMembershipSection
currentMembershipRole={currentMembership.role}
activeMemberships={activeMemberships}
removeMembership={this.removeMembership}
pendingMemberships={pendingMemberships}
requestedMemberships={requestedMemberships}
chatChannelAcceptMembership={this.chatChannelAcceptMembership}
/>
<ModSection
invitationUsernames={invitationUsernames}
handleInvitationUsernames={this.handleInvitationUsernames}
handleChatChannelInvitations={this.handleChatChannelInvitations}
channelDescription={channelDescription}
handleDescriptionChange={this.handleDescriptionChange}
channelDiscoverable={channelDiscoverable}
handleChannelDiscoverableStatus={
this.handleChannelDiscoverableStatus
}
handleChannelDescriptionChanges={
this.handleChannelDescriptionChanges
}
currentMembershipRole={currentMembership.role}
/>
<PersonalSettings
updateCurrentMembershipNotificationSettings={
this.updateCurrentMembershipNotificationSettings
}
showGlobalBadgeNotification={showGlobalBadgeNotification}
handlePersonChatChennelSetting={this.handlePersonChatChennelSetting}
/>
<LeaveMembershipSection
currentMembershipRole={currentMembership.role}
handleleaveChatChannelMembership={
this.handleleaveChatChannelMembership
}
/>
<ModFaqSection currentMembershipRole={currentMembership.role} />
</div>
</div>
);
}
}

View file

@ -0,0 +1,47 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const InviteForm = ({
handleChatChannelInvitations,
invitationUsernames,
handleInvitationUsernames
}) => {
return (
<div className="crayons-card p-4 grid gap-2 mb-4">
<div className="crayons-field">
<label
className="crayons-field__label invitation_form_title"
htmlFor="chat_channel_membership_Usernames to Invite"
>
Usernames to invite
</label>
<input
placeholder="Comma separated"
className="crayons-textfield"
type="text"
value={invitationUsernames}
name="chat_channel_membership[invitation_usernames]"
id="chat_channel_membership_invitation_usernames"
onChange={handleInvitationUsernames}
/>
</div>
<div>
<button
className="crayons-btn"
type="submit"
onClick={handleChatChannelInvitations}
>
Submit
</button>
</div>
</div>
)
}
InviteForm.propTypes = {
handleInvitationUsernames: PropTypes.func.isRequired,
handleChatChannelInvitations: PropTypes.func.isRequired,
invitationUsernames: PropTypes.func.isRequired
}
export default InviteForm;

View file

@ -0,0 +1,33 @@
import { h } from 'preact';
import PropsType from 'prop-types';
const LeaveMembershipSection = ({
currentMembershipRole,
handleleaveChatChannelMembership,
}) => {
if (currentMembershipRole !== 'member') {
return null;
}
return (
<div className="crayons-card p-4 grid gap-2 mb-4 leave_membership_section">
<h3>Danger Zone</h3>
<div>
<button
className="crayons-btn crayons-btn--danger leave_button"
type="submit"
onClick={handleleaveChatChannelMembership}
>
Leave Channel
</button>
</div>
</div>
);
};
LeaveMembershipSection.propTypes = {
currentMembershipRole: PropsType.string.isRequired,
handleleaveChatChannelMembership: PropsType.func.isRequired,
};
export default LeaveMembershipSection;

View file

@ -0,0 +1,67 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const Membership = ({
membership,
removeMembership,
membershipType,
chatChannelAcceptMembership,
currentMembershipRole,
}) => {
return (
<div className="flex items-center">
<a href={`/${membership.username}`}>
<span className="crayons-avatar crayons-avatar--l mr-3">
<img
className="crayons-avatar__image align-middle"
role="presentation"
src={membership.image}
alt={`${membership.name} profile`}
/>
</span>
<span className="mr-2 user_name">{membership.name}</span>
</a>
{membershipType === 'requested' ? (
<button
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost add-membership"
type="button"
onClick={chatChannelAcceptMembership}
data-membership-id={membership.membership_id}
>
+
</button>
) : null}
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
<button
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost remove-membership"
type="button"
onClick={removeMembership}
data-membership-id={membership.membership_id}
data-membership-status={membership.status}
>
x
</button>
) : null}
</div>
);
};
Membership.propTypes = {
membership: PropTypes.objectOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
}),
).isRequired,
removeMembership: PropTypes.func.isRequired,
membershipType: PropTypes.func.isRequired,
chatChannelAcceptMembership: PropTypes.func.isRequired,
currentMembershipRole: PropTypes.string.isRequired,
};
export default Membership;

View file

@ -0,0 +1,30 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const ModFaqSection = ({ currentMembershipRole }) => {
if (currentMembershipRole !== 'mod') {
return null;
}
return (
<div className="crayons-card grid gap-2 p-4 faq-section">
<p className="contact-details">
Questions about Connect Channel moderation? Contact
<a
href="mailto:yo@dev.to"
target="_blank"
rel="noopener noreferrer"
className="mx-2 url-link"
>
yo@dev.to
</a>
</p>
</div>
);
};
ModFaqSection.propTypes = {
currentMembershipRole: PropTypes.string.isRequired,
};
export default ModFaqSection;

View file

@ -0,0 +1,52 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import InviteForm from './InviateForm';
import SettingsFrom from './SettingsForm';
const ModSection = ({
handleChatChannelInvitations,
invitationUsernames,
handleInvitationUsernames,
channelDescription,
handleDescriptionChange,
channelDiscoverable,
handleChannelDiscoverableStatus,
handleChannelDescriptionChanges,
currentMembershipRole,
}) => {
if (currentMembershipRole !== 'mod') {
return null;
}
return (
<div className="mod-section">
<InviteForm
handleInvitationUsernames={handleInvitationUsernames}
invitationUsernames={invitationUsernames}
handleChatChannelInvitations={handleChatChannelInvitations}
/>
<SettingsFrom
channelDescription={channelDescription}
handleDescriptionChange={handleDescriptionChange}
channelDiscoverable={channelDiscoverable}
handleChannelDiscoverableStatus={handleChannelDiscoverableStatus}
handleChannelDescriptionChanges={handleChannelDescriptionChanges}
/>
</div>
);
};
ModSection.propTypes = {
handleInvitationUsernames: PropTypes.func.isRequired,
handleChatChannelInvitations: PropTypes.func.isRequired,
invitationUsernames: PropTypes.func.isRequired,
channelDescription: PropTypes.string.isRequired,
handleDescriptionChange: PropTypes.func.isRequired,
handleChannelDiscoverableStatus: PropTypes.func.isRequired,
handleChannelDescriptionChanges: PropTypes.func.isRequired,
channelDiscoverable: PropTypes.bool.isRequired,
currentMembershipRole: PropTypes.string.isRequired,
};
export default ModSection;

View file

@ -0,0 +1,48 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import Membership from './Membership';
const PendingMembershipSection = ({
pendingMemberships,
removeMembership,
currentMembershipRole,
}) => {
if (currentMembershipRole !== 'mod') {
return null;
}
return (
<div className="p-4 grid gap-2 crayons-card mb-4 pending_memberships">
<h3 className="mb-2">Pending Invitations</h3>
{pendingMemberships && pendingMemberships.length > 0
? pendingMemberships.map((pendingMembership) => (
<Membership
membership={pendingMembership}
removeMembership={removeMembership}
membershipType="pending"
currentMembershipRole={currentMembershipRole}
className="pending-member"
/>
))
: null}
</div>
);
};
PendingMembershipSection.propTypes = {
pendingMemberships: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
}),
).isRequired,
removeMembership: PropTypes.func.isRequired,
currentMembershipRole: PropTypes.func.isRequired,
};
export default PendingMembershipSection;

View file

@ -0,0 +1,44 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const PersonalSettings = ({
handlePersonChatChennelSetting,
showGlobalBadgeNotification,
updateCurrentMembershipNotificationSettings,
}) => {
return (
<div className="crayons-card p-4 grid gap-2 mb-4 personl-settings">
<h3>Personal Settings</h3>
<h4>Notifications</h4>
<div className="crayons-field crayons-field--checkbox">
<input
type="checkbox"
id="c3"
className="crayons-checkbox"
checked={showGlobalBadgeNotification}
onChange={handlePersonChatChennelSetting}
/>
<label htmlFor="c3" className="crayons-field__label">
Receive Notifications for New Messages
</label>
</div>
<div>
<button
className="crayons-btn"
type="submit"
onClick={updateCurrentMembershipNotificationSettings}
>
Submit
</button>
</div>
</div>
);
};
PersonalSettings.propTypes = {
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
showGlobalBadgeNotification: PropTypes.bool.isRequired,
handlePersonChatChennelSetting: PropTypes.func.isRequired,
};
export default PersonalSettings;

View file

@ -0,0 +1,51 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
import Membership from './Membership';
const RequestedMembershipSection = ({
requestedMemberships,
removeMembership,
chatChannelAcceptMembership,
currentMembershipRole,
}) => {
if (currentMembershipRole !== 'mod') {
return null;
}
return (
<div className="p-4 grid gap-2 crayons-card mb-4">
<h3 className="mb-2 requested_memberships">Joining Request</h3>
{requestedMemberships && requestedMemberships.length > 0
? requestedMemberships.map((pendingMembership) => (
<Membership
membership={pendingMembership}
removeMembership={removeMembership}
chatChannelAcceptMembership={chatChannelAcceptMembership}
membershipType="requested"
currentMembershipRole={currentMembershipRole}
className="requested-member"
/>
))
: null}
</div>
);
};
RequestedMembershipSection.propTypes = {
requestedMemberships: PropTypes.arrayOf(
PropTypes.shape({
name: PropTypes.string.isRequired,
membership_id: PropTypes.number.isRequired,
user_id: PropTypes.number.isRequired,
role: PropTypes.string.isRequired,
image: PropTypes.string.isRequired,
username: PropTypes.string.isRequired,
status: PropTypes.string.isRequired,
}),
).isRequired,
removeMembership: PropTypes.func.isRequired,
chatChannelAcceptMembership: PropTypes.func.isRequired,
currentMembershipRole: PropTypes.func.isRequired,
};
export default RequestedMembershipSection;

View file

@ -0,0 +1,62 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const SettingsFrom = ({
channelDescription,
handleDescriptionChange,
channelDiscoverable,
handleChannelDiscoverableStatus,
handleChannelDescriptionChanges,
}) => {
return (
<div className="crayons-card p-4 grid gap-2 mb-4 settings-section">
<h3>Channel Settings</h3>
<div className="crayons-field">
<label
className="crayons-field__label"
htmlFor="chat_channel_description"
>
Description
</label>
<textarea
className="crayons-textfield"
name="description"
id="chat_channel_description"
value={channelDescription}
onChange={handleDescriptionChange}
/>
</div>
<div className="crayons-field crayons-field--checkbox">
<input
type="checkbox"
id="c2"
className="crayons-checkbox"
checked={channelDiscoverable}
onChange={handleChannelDiscoverableStatus}
/>
<label htmlFor="c2" className="crayons-field__label">
Channel Discoverable
</label>
</div>
<div>
<button
className="crayons-btn"
type="submit"
onClick={handleChannelDescriptionChanges}
>
Submit
</button>
</div>
</div>
);
};
SettingsFrom.propTypes = {
channelDescription: PropTypes.string.isRequired,
handleDescriptionChange: PropTypes.func.isRequired,
handleChannelDiscoverableStatus: PropTypes.func.isRequired,
handleChannelDescriptionChanges: PropTypes.func.isRequired,
channelDiscoverable: PropTypes.bool.isRequired,
};
export default SettingsFrom;

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ActiveMembershipsSection /> should render and test snapshot 1`] = `
<div
class="p-4 grid gap-2 crayons-card mb-4"
>
<h3
class="mb-2 active_members"
>
Members
</h3>
</div>
`;

View file

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChannelDescriptionSection /> should render and test snapshot 1`] = `
<div
class="p-4 grid gap-2 crayons-card mb-4 channel_details"
>
<h1
class="mb-1 channel_title"
>
some name
</h1>
<p>
some description
</p>
<p
class="fw-bold"
>
You are a channel mod
</p>
</div>
`;

View file

@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChatChannelMembershipSection /> should render and test snapshot 1`] = `
<div
class="membership-list"
>
<div
class="p-4 grid gap-2 crayons-card mb-4"
>
<h3
class="mb-2 active_members"
>
Members
</h3>
</div>
</div>
`;

View file

@ -0,0 +1,3 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChatChannelSettings /> should render and test snapshot 1`] = `null`;

View file

@ -0,0 +1,34 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<InviteForm /> should render the test snapshot 1`] = `
<div
class="crayons-card p-4 grid gap-2 mb-4"
>
<div
class="crayons-field"
>
<label
class="crayons-field__label invitation_form_title"
htmlFor="chat_channel_membership_Usernames to Invite"
>
Usernames to invite
</label>
<input
class="crayons-textfield"
id="chat_channel_membership_invitation_usernames"
name="chat_channel_membership[invitation_usernames]"
placeholder="Comma separated"
type="text"
value=""
/>
</div>
<div>
<button
class="crayons-btn"
type="submit"
>
Submit
</button>
</div>
</div>
`;

View file

@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<LeaveMembershipSection /> should render and test snapshot 1`] = `
<div
class="crayons-card p-4 grid gap-2 mb-4 leave_membership_section"
>
<h3>
Danger Zone
</h3>
<div>
<button
class="crayons-btn crayons-btn--danger leave_button"
type="submit"
>
Leave Channel
</button>
</div>
</div>
`;

View file

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Membership /> should render and test snapshot 1`] = `
<div
class="flex items-center"
>
<a
href="/testusername"
>
<span
class="crayons-avatar crayons-avatar--l mr-3"
>
<img
alt="test user profile"
class="crayons-avatar__image align-middle"
role="presentation"
src=""
/>
</span>
<span
class="mr-2 user_name"
>
test user
</span>
</a>
</div>
`;

View file

@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ChannelDescriptionSection /> should render and test snapshot 1`] = `
<div
class="crayons-card grid gap-2 p-4 faq-section"
>
<p
class="contact-details"
>
Questions about Connect Channel moderation? Contact
<a
class="mx-2 url-link"
href="mailto:yo@dev.to"
rel="noopener noreferrer"
target="_blank"
>
yo@dev.to
</a>
</p>
</div>
`;

View file

@ -0,0 +1,82 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<ModSection /> should render and test snapshot 1`] = `
<div
class="mod-section"
>
<div
class="crayons-card p-4 grid gap-2 mb-4"
>
<div
class="crayons-field"
>
<label
class="crayons-field__label invitation_form_title"
htmlFor="chat_channel_membership_Usernames to Invite"
>
Usernames to invite
</label>
<input
class="crayons-textfield"
id="chat_channel_membership_invitation_usernames"
name="chat_channel_membership[invitation_usernames]"
placeholder="Comma separated"
type="text"
/>
</div>
<div>
<button
class="crayons-btn"
type="submit"
>
Submit
</button>
</div>
</div>
<div
class="crayons-card p-4 grid gap-2 mb-4 settings-section"
>
<h3>
Channel Settings
</h3>
<div
class="crayons-field"
>
<label
class="crayons-field__label"
htmlFor="chat_channel_description"
>
Description
</label>
<textarea
class="crayons-textfield"
id="chat_channel_description"
name="description"
/>
</div>
<div
class="crayons-field crayons-field--checkbox"
>
<input
class="crayons-checkbox"
id="c2"
type="checkbox"
/>
<label
class="crayons-field__label"
htmlFor="c2"
>
Channel Discoverable
</label>
</div>
<div>
<button
class="crayons-btn"
type="submit"
>
Submit
</button>
</div>
</div>
</div>
`;

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PendingMembershipSections /> should render and test snapshot 1`] = `
<div
class="p-4 grid gap-2 crayons-card mb-4 pending_memberships"
>
<h3
class="mb-2"
>
Pending Invitations
</h3>
</div>
`;

View file

@ -0,0 +1,38 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<PersonalSettng /> should render and test snapshot 1`] = `
<div
class="crayons-card p-4 grid gap-2 mb-4 personl-settings"
>
<h3>
Personal Settings
</h3>
<h4>
Notifications
</h4>
<div
class="crayons-field crayons-field--checkbox"
>
<input
checked={true}
class="crayons-checkbox"
id="c3"
type="checkbox"
/>
<label
class="crayons-field__label"
htmlFor="c3"
>
Receive Notifications for New Messages
</label>
</div>
<div>
<button
class="crayons-btn"
type="submit"
>
Submit
</button>
</div>
</div>
`;

View file

@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<RequestedMembershipSection /> should render and test snapshot 1`] = `
<div
class="p-4 grid gap-2 crayons-card mb-4"
>
<h3
class="mb-2 requested_memberships"
>
Joining Request
</h3>
</div>
`;

View file

@ -0,0 +1,49 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<SettingsForm /> should render and test snapshot 1`] = `
<div
class="crayons-card p-4 grid gap-2 mb-4 settings-section"
>
<h3>
Channel Settings
</h3>
<div
class="crayons-field"
>
<label
class="crayons-field__label"
htmlFor="chat_channel_description"
>
Description
</label>
<textarea
class="crayons-textfield"
id="chat_channel_description"
name="description"
/>
</div>
<div
class="crayons-field crayons-field--checkbox"
>
<input
class="crayons-checkbox"
id="c2"
type="checkbox"
/>
<label
class="crayons-field__label"
htmlFor="c2"
>
Channel Discoverable
</label>
</div>
<div>
<button
class="crayons-btn"
type="submit"
>
Submit
</button>
</div>
</div>
`;

View file

@ -0,0 +1,63 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ActiveMembershipsSection from '../ChatChannelSettings/ActiveMembershipsSection';
const data = {
activeMemberships: [],
currentMembershipRole: 'mod',
};
const membership = {
activeMemberships: [
{
name: 'test user',
username: 'testusername',
user_id: '1',
membership_id: '2',
role: 'mod',
status: 'active',
image: '',
},
],
membershipType: 'active',
currentMembershipRole: 'mod',
};
const getActiveMembershipsSection = (membershipData) => (
<ActiveMembershipsSection
activeMemberships={membershipData.activeMemberships}
currentMembershipRole={membershipData.currentMembershipRole}
/>
);
describe('<ActiveMembershipsSection />', () => {
it('should render and test snapshot', () => {
const tree = render(getActiveMembershipsSection(data));
expect(tree).toMatchSnapshot();
});
it('should have the elements', () => {
const context = shallow(getActiveMembershipsSection(data));
expect(context.find('.active_members').exists()).toEqual(true);
});
it('should have title', () => {
const context = shallow(getActiveMembershipsSection(data));
expect(context.find('.active_members').text()).toEqual('Members');
});
it('should not render the membership list', () => {
const context = shallow(getActiveMembershipsSection(data));
expect(context.find('.items-center').exists()).toEqual(false);
});
it('should render the membership list', () => {
const context = shallow(getActiveMembershipsSection(membership));
expect(context.find('.active-member').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,39 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ChannelDescriptionSection from '../ChatChannelSettings/ChannelDescriptionSection';
const data = {
channelName: "some name",
channelDescription: "some description",
currentMembershipRole: "mod"
}
const getChannelDescriptionSection = (channelDetails) => {
return (
<ChannelDescriptionSection
channelName={channelDetails.channelName}
channelDescription={channelDetails.channelDescription}
currentMembershipRole={channelDetails.currentMembershipRole}
/>
)
}
describe('<ChannelDescriptionSection />', () => {
it ('should render and test snapshot', () => {
const tree = render(getChannelDescriptionSection(data));
expect(tree).toMatchSnapshot();
})
it ("should render the the component", () => {
const context = shallow(getChannelDescriptionSection(data));
expect(context.find('.channel_details').exists()).toEqual(true);
})
it ("should render the same header", () => {
const context = shallow(getChannelDescriptionSection(data));
expect(context.find('.channel_title').text()).toEqual(data.channelName)
})
})

View file

@ -0,0 +1,34 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ChatChannelMembershipSection from '../ChatChannelSettings/ChatChannelMembershipSection';
const data = {
pendingMemberships: [],
requestedMemberships: [],
activeMemberships: []
}
const getChatChannelMembershipSection = (memberships) => {
return (
<ChatChannelMembershipSection
pendingMemberships={memberships.pendingMemberships}
requestedMemberships={memberships.requestedMemberships}
activeMemberships={memberships.activeMemberships}
/>
)
}
describe('<ChatChannelMembershipSection />', () => {
it ('should render and test snapshot', () => {
const tree = render(getChatChannelMembershipSection(data));
expect(tree).toMatchSnapshot();
})
it ('should have the proper elements, attributes and values', () => {
const context = shallow(getChatChannelMembershipSection(data));
expect(context.find('.membership-list').exists()).toEqual(true)
})
})

View file

@ -0,0 +1,203 @@
import fetch from 'jest-fetch-mock';
import {
getChannelDetails,
updatePersonalChatChannelNotificationSettings,
rejectChatChannelJoiningRequest,
acceptChatChannelJoiningRequest,
updateChatChannelDescription,
sendChatChannelInvitation,
leaveChatChannelMembership,
} from '../actions/chat_channel_setting_actions';
/* global globalThis */
describe('Chat cahnnel API requestes', () => {
const csrfToken = 'this-is-a-csrf-token';
const chanChannelMembershipId = 26; // Just a random chatChannelMembershipId ID.
const channelId = 2;
beforeAll(() => {
globalThis.fetch = fetch;
globalThis.getCsrfToken = async () => csrfToken;
});
afterAll(() => {
delete globalThis.fetch;
delete globalThis.getCsrfToken;
});
afterEach(() => {
jest.restoreAllMocks();
});
describe('get chat channel info', () => {
it('should have success response with channel details', async () => {
const response = {
success: true,
current_membership: {
user_id: 1,
id: 2,
chat_channel_id: 1,
status: 'active',
role: 'mod',
},
chat_channel: {
name: 'dummy channel',
description: 'some dummy description',
discoverable: true,
status: 'active',
},
memberships: {
active_memberships: [],
pending_memberships: [],
requested_memberships: [],
},
};
fetch.mockResponse(JSON.stringify(response));
const chatChannelDetails = await getChannelDetails(
chanChannelMembershipId,
);
expect(chatChannelDetails).toEqual(response);
});
it('not found channel', async () => {
const response = {
success: false,
message: 'not found',
};
fetch.mockResponse(JSON.stringify(response));
const chatChannelDetails = await getChannelDetails(
chanChannelMembershipId,
);
expect(chatChannelDetails).toEqual(response);
});
});
describe('Update chat channel notification', () => {
it('should have success', async () => {
const response = { success: true, message: 'user settings updated' };
fetch.mockResponse(JSON.stringify(response));
const updateProfile = await updatePersonalChatChannelNotificationSettings(
chanChannelMembershipId,
true,
);
expect(updateProfile).toEqual(response);
});
it('should return not found error', async () => {
const response = { success: false, message: 'not found' };
fetch.mockResponse(JSON.stringify(response));
const updateProfile = await updatePersonalChatChannelNotificationSettings(
'',
true,
);
expect(updateProfile).toEqual(response);
});
});
describe('reject chat channel membership', () => {
it('should have success', async () => {
const response = { success: true, message: 'user removed' };
fetch.mockResponse(JSON.stringify(response));
const result = await rejectChatChannelJoiningRequest(
channelId,
chanChannelMembershipId,
'pending',
);
expect(result).toEqual(response);
});
it('should return not found error', async () => {
const response = { success: false, message: 'not found' };
fetch.mockResponse(JSON.stringify(response));
const result = await rejectChatChannelJoiningRequest('', '', 'pending');
expect(result).toEqual(response);
});
});
describe('Accept chat channel membership', () => {
it('should have success', async () => {
const response = { success: true, message: 'added to chat channel' };
fetch.mockResponse(JSON.stringify(response));
const result = await acceptChatChannelJoiningRequest(
channelId,
chanChannelMembershipId,
);
expect(result).toEqual(response);
});
it('should return not found error', async () => {
const response = { success: false, message: 'not found' };
fetch.mockResponse(JSON.stringify(response));
const result = await acceptChatChannelJoiningRequest('', '');
expect(result).toEqual(response);
});
});
describe('update chat channel', () => {
it('should have success', async () => {
const response = { success: true, message: 'channel is updated' };
fetch.mockResponse(JSON.stringify(response));
const result = await updateChatChannelDescription(
channelId,
'some description',
true,
);
expect(result).toEqual(response);
});
});
describe('Send inbvitation for join chat channel', () => {
it('should have success', async () => {
const response = { success: true, message: 'added to chat channel' };
fetch.mockResponse(JSON.stringify(response));
const result = await sendChatChannelInvitation(channelId, 'dummyuser');
expect(result).toEqual(response);
});
it('should not send any invitation', async () => {
const response = { success: true, message: 'no invitation sent' };
fetch.mockResponse(JSON.stringify(response));
const result = await sendChatChannelInvitation(channelId, '');
expect(result).toEqual(response);
});
it('should return not found error', async () => {
const response = { success: false, message: 'not found' };
fetch.mockResponse(JSON.stringify(response));
const result = await sendChatChannelInvitation('', '');
expect(result).toEqual(response);
});
});
describe('Leave chat channel', () => {
it('should have success', async () => {
const response = { success: true, message: 'user left the channel' };
fetch.mockResponse(JSON.stringify(response));
const result = await leaveChatChannelMembership(chanChannelMembershipId);
expect(result).toEqual(response);
});
it('should return not found error', async () => {
const response = { success: false, message: 'not found' };
fetch.mockResponse(JSON.stringify(response));
const result = await leaveChatChannelMembership('');
expect(result).toEqual(response);
});
});
});

View file

@ -0,0 +1,28 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ChatChannelSettings from '../ChatChannelSettings/ChatChannelSettings';
const channelDetails = {
data: {},
activeMembershipId: 12,
};
const getChatChannelSettinsg = (resource) => (
<ChatChannelSettings
resource={resource.data}
activeMembershipId={resource.activeMembershipId}
/>
);
describe('<ChatChannelSettings />', () => {
it('should render and test snapshot', () => {
const tree = render(getChatChannelSettinsg(channelDetails));
expect(tree).toMatchSnapshot();
});
it('should not render if not channel', () => {
const context = shallow(getChatChannelSettinsg(channelDetails));
expect(context.find('.channel_settings').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,27 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import InviteForm from '../ChatChannelSettings/InviateForm';
const data = {
invitationUsernames: ''
}
const getInviteForm = (invitations) => (
<InviteForm
invitationUsernames={invitations.invitationUsernames}
/>
)
describe('<InviteForm />', () => {
it("should render the test snapshot", () => {
const tree = render(getInviteForm(data))
expect(tree).toMatchSnapshot();
})
it("should have the element", () => {
const context = shallow(getInviteForm(data));
expect(context.find('.invitation_form_title').exists()).toEqual(true)
})
})

View file

@ -0,0 +1,40 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import LeaveMembershipSection from '../ChatChannelSettings/LeaveMembershipSection';
const data = {
currentMembershipRole: 'member',
};
const modUser = {
currentMembershipRole: 'mod',
};
const getLeaveMembershipSection = (membershipData) => (
<LeaveMembershipSection
currentMembershipRole={membershipData.currentMembershipRole}
/>
);
describe('<LeaveMembershipSection />', () => {
it('should render and test snapshot', () => {
const tree = render(getLeaveMembershipSection(data));
expect(tree).toMatchSnapshot();
});
it('should have the elements', () => {
const context = shallow(getLeaveMembershipSection(data));
expect(context.find('.leave_membership_section').exists()).toEqual(true);
});
it('should have the leave button', () => {
const context = shallow(getLeaveMembershipSection(data));
expect(context.find('.leave_button').text()).toEqual('Leave Channel');
});
it('should not render', () => {
const context = shallow(getLeaveMembershipSection(modUser));
expect(context.find('.leave_membership_section').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,71 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import Membership from '../ChatChannelSettings/Membership';
const modUser = {
membership: {
name: 'test user',
username: 'testusername',
user_id: '1',
membership_id: '2',
role: 'mod',
status: 'active',
image: '',
},
membershipType: 'active',
currentMembershipRole: 'mod',
};
const memberUser = {
membership: {
name: 'test user',
username: 'testusername',
user_id: '1',
membership_id: '2',
role: 'member',
status: 'active',
image: '',
},
membershipType: 'requested',
currentMembershipRole: 'mod',
};
const getMembership = (membershipData) => (
<Membership
membership={membershipData.membership}
membershipType={membershipData.membershipType}
currentMembershipRole={membershipData.currentMembershipRole}
/>
);
describe('<Membership />', () => {
it('should render and test snapshot', () => {
const tree = render(getMembership(modUser));
expect(tree).toMatchSnapshot();
});
it('should have the element', () => {
const context = shallow(getMembership(modUser));
expect(context.find('.user_name').exists()).toEqual(true);
});
it('should have the the same ame', () => {
const context = shallow(getMembership(modUser));
expect(context.find('.user_name').text()).toEqual(modUser.membership.name);
});
it('should not visible remove and add membership button', () => {
const context = shallow(getMembership(modUser));
expect(context.find('.remove-membership').exists()).toEqual(false);
expect(context.find('.add-membership').exists()).toEqual(false);
});
it('should add button visible', () => {
const context = shallow(getMembership(memberUser));
expect(context.find('.add-membership').exists()).toEqual(true);
expect(context.find('.remove-membership').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,49 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ModFaqSection from '../ChatChannelSettings/ModFaqSection';
const data = {
currentMembershipRole: 'mod',
};
const memberUser = {
currentMembershipRole: 'member',
};
const getModFaqSection = (resource) => {
return (
<ModFaqSection currentMembershipRole={resource.currentMembershipRole} />
);
};
describe('<ChannelDescriptionSection />', () => {
it('should render and test snapshot', () => {
const tree = render(getModFaqSection(data));
expect(tree).toMatchSnapshot();
});
it('should render the the component', () => {
const context = shallow(getModFaqSection(data));
expect(context.find('.faq-section').exists()).toEqual(true);
});
it('should have the contact details', () => {
const context = shallow(getModFaqSection(data));
expect(context.find('.contact-details').exists()).toEqual(true);
});
it('should render the same link text', () => {
const context = shallow(getModFaqSection(data));
expect(context.find('.url-link').text()).toEqual('yo@dev.to');
});
it('should not render the element', () => {
const context = shallow(getModFaqSection(memberUser));
expect(context.find('.faq-section').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,36 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ModSection from '../ChatChannelSettings/ModSection';
const modUser = {
currentMembershipRole: 'mod',
};
const memberUser = {
currentMembershipRole: 'member',
};
const getModSection = (resource) => {
return <ModSection currentMembershipRole={resource.currentMembershipRole} />;
};
describe('<ModSection />', () => {
it('should render and test snapshot', () => {
const tree = render(getModSection(modUser));
expect(tree).toMatchSnapshot();
});
it('should render the the component', () => {
const context = shallow(getModSection(modUser));
expect(context.find('.mod-section').exists()).toEqual(true);
});
it('should not render the the component', () => {
const context = shallow(getModSection(memberUser));
expect(context.find('.mod-section').exists()).toEqual(false);
});
});

View file

@ -0,0 +1,57 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import PendingMembershipSections from '../ChatChannelSettings/PendingMembershipSection';
const data = {
pendingMemberships: [],
currentMembershipRole: 'mod',
};
const membership = {
pendingMemberships: [
{
name: 'test user',
username: 'testusername',
user_id: '1',
membership_id: '2',
role: 'member',
status: 'pending',
image: '',
},
],
membershipType: 'pending',
currentMembershipRole: 'mod',
};
const getPendingMembershipSections = (membershipData) => (
<PendingMembershipSections
pendingMemberships={membershipData.pendingMemberships}
currentMembershipRole={membershipData.currentMembershipRole}
/>
);
describe('<PendingMembershipSections />', () => {
it('should render and test snapshot', () => {
const tree = render(getPendingMembershipSections(data));
expect(tree).toMatchSnapshot();
});
it('should have the elements', () => {
const context = shallow(getPendingMembershipSections(data));
expect(context.find('.pending_memberships').exists()).toEqual(true);
});
it('should not render the membership list', () => {
const context = shallow(getPendingMembershipSections(data));
expect(context.find('.membership-list').exists()).toEqual(false);
});
it('should render the membership list', () => {
const context = shallow(getPendingMembershipSections(membership));
expect(context.find('.pending-member').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,30 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import PersonalSettng from '../ChatChannelSettings/PersonalSetting';
const data = {
showGlobalBadgeNotification: true,
};
const getPersonalSettng = (channelDetails) => {
return (
<PersonalSettng
showGlobalBadgeNotification={channelDetails.showGlobalBadgeNotification}
/>
);
};
describe('<PersonalSettng />', () => {
it('should render and test snapshot', () => {
const tree = render(getPersonalSettng(data));
expect(tree).toMatchSnapshot();
});
it('should render the the component', () => {
const context = shallow(getPersonalSettng(data));
expect(context.find('.personl-settings').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,57 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import RequestedMembershipSection from '../ChatChannelSettings/RequestedMembershipSection';
const data = {
requestedMemberships: [],
currentMembershipRole: 'mod',
};
const membership = {
requestedMemberships: [
{
name: 'test user',
username: 'testusername',
user_id: '1',
membership_id: '2',
role: 'member',
status: 'requested',
image: '',
},
],
membershipType: 'requested',
currentMembershipRole: 'mod',
};
const getRequestedMembershipSection = (membershipData) => (
<RequestedMembershipSection
requestedMemberships={membershipData.requestedMemberships}
currentMembershipRole={membershipData.currentMembershipRole}
/>
);
describe('<RequestedMembershipSection />', () => {
it('should render and test snapshot', () => {
const tree = render(getRequestedMembershipSection(data));
expect(tree).toMatchSnapshot();
});
it('should have the elements', () => {
const context = shallow(getRequestedMembershipSection(data));
expect(context.find('.requested_memberships').exists()).toEqual(true);
});
it('should not render the membership list', () => {
const context = shallow(getRequestedMembershipSection(data));
expect(context.find('.items-center').exists()).toEqual(false);
});
it('should render the membership list', () => {
const context = shallow(getRequestedMembershipSection(membership));
expect(context.find('.requested-member').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,30 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import SettingsForm from '../ChatChannelSettings/SettingsForm';
const data = {
channelDescription: 'some description test',
channelDiscoverable: true,
};
const getSettingsForm = (channelDetails) => {
return (
<SettingsForm
showGlobalBadgeNotification={channelDetails.showGlobalBadgeNotification}
/>
);
};
describe('<SettingsForm />', () => {
it('should render and test snapshot', () => {
const tree = render(getSettingsForm(data));
expect(tree).toMatchSnapshot();
});
it('should render the the component', () => {
const context = shallow(getSettingsForm(data));
expect(context.find('.settings-section').exists()).toEqual(true);
});
});

View file

@ -0,0 +1,151 @@
import { request } from '../../utilities/http';
/**
* This function will get all details of the chat channel accrding to the membership role.
*
* @param {number} chatChannelMembershipId Current User chat channel membership ID
*/
export async function getChannelDetails(chatChannelMembershipId) {
const response = await request(
`/chat_channel_memberships/chat_channel_info/${chatChannelMembershipId}`,
{
credentials: 'same-origin',
},
);
return response.json();
}
/**
* This function is used to update the notification settings.
*
* @param {number} membershipId Current user Chat Channel membership Id.
* @param {boolean} notificationBadge Boolean value for the notification
*/
export async function updatePersonalChatChannelNotificationSettings(
membershipId,
notificationBadge,
) {
const response = await request(
`/chat_channel_memberships/update_membership/${membershipId}`,
{
method: 'PATCH',
body: {
chat_channel_membership: {
show_global_badge_notification: notificationBadge,
},
},
credentials: 'same-origin',
},
);
return response.json();
}
/**
* This function is used to reject chat channel joining request & pending requests.
*
* @param { number } channelId Active Chat Channel ID
* @param { number } membershipId Requested user membership Id
* @param { string } membershipStatus Requested user membership status
*/
export async function rejectChatChannelJoiningRequest(
channelId,
membershipId,
membershipStatus,
) {
const response = await request(
`/chat_channel_memberships/remove_membership`,
{
method: 'POST',
body: {
status: membershipStatus || 'pending',
chat_channel_id: channelId,
membership_id: membershipId,
},
credentials: 'same-origin',
},
);
return response.json();
}
/**
*
* @param {number} channelId Active chat channel Id
* @param {number} membershipId Chat channel joining request membership id
*/
export async function acceptChatChannelJoiningRequest(channelId, membershipId) {
const response = await request(`/chat_channel_memberships/add_membership`, {
method: 'POST',
body: {
chat_channel_id: channelId,
membership_id: membershipId,
chat_channel_membership: {
user_action: 'accept',
},
},
credentials: 'same-origin',
});
return response.json();
}
export async function updateChatChannelDescription(
channelId,
description,
discoverable,
) {
const response = await request(`/chat_channels/update_channel/${channelId}`, {
method: 'PATCH',
body: { chat_channel: { description, discoverable } },
credentials: 'same-origin',
});
return response.json();
}
/**
* Send Active chat channel invitation
*
* @param {numner} channelId Active chat channel
* @param {string} invitationUsernames UserNames coma seprated
*/
export async function sendChatChannelInvitation(
channelId,
invitationUsernames,
) {
const response = await request(
`/chat_channel_memberships/create_membership_request`,
{
method: 'POST',
body: {
chat_channel_membership: {
chat_channel_id: channelId,
invitation_usernames: invitationUsernames,
},
},
credentials: 'same-origin',
},
);
return response.json();
}
/**
* This function is used to leave the chat channel.
*
* @param {number} membershipId Current User Chat channel membership id
*/
export async function leaveChatChannelMembership(membershipId) {
const response = await request(
`/chat_channel_memberships/leave_membership/${membershipId}`,
{
method: 'PATCH',
credentials: 'same-origin',
},
);
return response.json();
}

View file

@ -1,21 +1,18 @@
import { request } from '../../utilities/http';
export function rejectJoiningRequest(
channelId,
membershipId,
successCb,
failureCb,
) {
fetch(`/chat_channel_memberships/remove_membership`, {
request(`/chat_channel_memberships/remove_membership`, {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: {
status: 'pending',
chat_channel_id: channelId,
membership_id: membershipId,
}),
},
credentials: 'same-origin',
})
.then((response) => response.json())
@ -29,20 +26,15 @@ export function acceptJoiningRequest(
successCb,
failureCb,
) {
fetch(`/chat_channel_memberships/add_membership`, {
request(`/chat_channel_memberships/add_membership`, {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: {
chat_channel_id: channelId,
membership_id: membershipId,
chat_channel_membership: {
user_action: 'accept',
},
}),
},
credentials: 'same-origin',
})
.then((response) => response.json())
@ -51,18 +43,13 @@ export function acceptJoiningRequest(
}
export function sendChannelRequest(id, successCb, failureCb) {
fetch(`/join_chat_channel`, {
request(`/join_chat_channel`, {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
body: {
chat_channel_membership: {
chat_channel_id: id,
},
}),
},
credentials: 'same-origin',
})
.then((response) => response.json())

View file

@ -867,6 +867,7 @@ export default class Chat extends Component {
e.stopPropagation();
const { activeChannelId, activeChannel } = this.state;
if (content.startsWith('chat_channels/')) {
this.setActiveContentState(activeChannelId, {
type_of: 'loading-user',
@ -936,9 +937,14 @@ export default class Chat extends Component {
fullscreenContent: mode,
expanded: mode === null || window.innerWidth > 1600,
});
} else if (target.dataset.content === 'chat_channel_setting') {
this.setActiveContent({
data: {},
type_of: 'chat-channel-setting',
activeMembershipId: activeChannel.id
});
}
}
document.getElementById('messageform').focus();
return false;
};
@ -1050,7 +1056,7 @@ export default class Chat extends Component {
<b>must</b>
</em>
{' '}
abide by the
abide by the
{' '}
<a href="/code-of-conduct">code of conduct</a>
.
@ -1062,7 +1068,7 @@ export default class Chat extends Component {
return (
<div className="chatmessage" style={{ color: 'grey' }}>
<div className="chatmessage__body">
You have joined
You have joined
{' '}
{activeChannel.channel_name}
! All interactions
@ -1071,7 +1077,7 @@ export default class Chat extends Component {
<b>must</b>
</em>
{' '}
abide by the
abide by the
{' '}
<a href="/code-of-conduct">code of conduct</a>
.
@ -1717,6 +1723,7 @@ export default class Chat extends Component {
this.toggleSearchShowing();
};
renderChannelHeaderInner = () => {
const { activeChannel } = this.state;
if (activeChannel.channel_type === 'direct') {
@ -1732,9 +1739,9 @@ export default class Chat extends Component {
}
return (
<a
href={`/chat_channel_memberships/${activeChannel.id}/edit`}
href='#/'
onClick={this.triggerActiveContent}
data-content="sidecar-chat_channel_membership"
data-content="chat_channel_setting"
>
{activeChannel.channel_name}
</a>
@ -1753,12 +1760,8 @@ export default class Chat extends Component {
const dataContent =
activeChannel.channel_type === 'direct'
? 'sidecar-user'
: `sidecar-chat_channel_membership`;
: `chat_channel_setting`;
const path =
activeChannel.channel_type === 'direct'
? `/${activeChannel.channel_username}`
: `/chat_channel_memberships/${activeChannel.id}/edit`;
return (
<a
@ -1768,7 +1771,7 @@ export default class Chat extends Component {
if (e.keyCode === 13) this.triggerActiveContent(e);
}}
tabIndex="0"
href={path}
href='#/'
data-content={dataContent}
>
<img

View file

@ -3,13 +3,14 @@ import PropTypes from 'prop-types';
import Article from './article';
import ChannelRequest from './channelRequest';
import RequestManager from './requestManager';
import ChatChannelSettings from './ChatChannelSettings/ChatChannelSettings';
export default class Content extends Component {
static propTypes = {
resource: PropTypes.object,
activeChannelId: PropTypes.number,
pusherKey: PropTypes.string,
fullscreen: PropTypes.bool,
resource : PropTypes.object,
activeChannelId : PropTypes.number,
pusherKey : PropTypes.string,
fullscreen : PropTypes.bool
};
render() {
@ -17,14 +18,9 @@ export default class Content extends Component {
if (!this.props.resource) {
return '';
}
const smartSvgIcon = (content, d) => (
<svg
data-content={content}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<svg data-content={content} xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24">
<path data-content={content} fill="none" d="M0 0h24v24H0z" />
<path data-content={content} d={d} />
</svg>
@ -42,7 +38,7 @@ export default class Content extends Component {
>
{smartSvgIcon(
'exit',
'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z',
'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z'
)}
</button>
<button
@ -50,16 +46,18 @@ export default class Content extends Component {
data-content="fullscreen"
style={{ left: '39px' }}
>
{' '}
{fullscreen
? smartSvgIcon(
'fullscreen',
'M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z',
)
: smartSvgIcon(
'fullscreen',
'M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z',
)}
{' '}
{fullscreen ? (
smartSvgIcon(
'fullscreen',
'M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z'
)
) : (
smartSvgIcon(
'fullscreen',
'M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z'
)
)}
</button>
{display(this.props)}
</div>
@ -69,27 +67,27 @@ export default class Content extends Component {
function display(props) {
const { resource } = props;
if (resource.type_of === 'loading-user') {
return <div className="loading-user" />;
}
if (resource.type_of === 'article') {
return <Article resource={resource} />;
}
if (resource.type_of === 'channel-request') {
return (
<ChannelRequest
resource={resource.data}
handleJoiningRequest={resource.handleJoiningRequest}
/>
);
}
if (resource.type_of === 'channel-request-manager') {
return (
<RequestManager
resource={resource.data}
handleRequestRejection={resource.handleRequestRejection}
handleRequestApproval={resource.handleRequestApproval}
/>
);
switch(resource.type_of) {
case 'loading-user':
return <div className="loading-user" />
case 'article':
return <Article resource={resource} />;
case 'channel-request':
return <ChannelRequest resource={resource.data} handleJoiningRequest={resource.handleJoiningRequest} />;
case 'channel-request-manager':
return (
<RequestManager
resource={resource.data}
handleRequestRejection={resource.handleRequestRejection}
handleRequestApproval={resource.handleRequestApproval}
/>
)
case 'chat-channel-setting':
return (
<ChatChannelSettings resource={resource.data} activeMembershipId={resource.activeMembershipId} />
);
default:
return null;
}
}

View file

@ -10,4 +10,20 @@ class ChatChannelMembershipPolicy < ApplicationPolicy
def destroy?
record.present? && user.id == record.user_id
end
def leave_membership?
record.present? && user.id == record.user_id
end
def update_membership?
record.present? && user.id == record.user_id
end
def invitation?
record.present? && user.id == record.user_id
end
def chat_channel_info?
record.present? && user.id == record.user_id
end
end

View file

@ -35,6 +35,10 @@ class ChatChannelPolicy < ApplicationPolicy
user_part_of_channel && channel_is_direct
end
def update_channel?
user_can_edit_channel
end
private
def user_can_edit_channel

View file

@ -0,0 +1,42 @@
class ChatChannelDetailPresenter
def initialize(chat_channel, current_membership)
@chat_channel = chat_channel
@current_membership = current_membership
end
attr_accessor :chat_channel, :current_membership
def as_json
{
chat_channel: {
name: chat_channel.channel_name,
type: chat_channel.channel_type,
description: chat_channel.description,
discoverable: chat_channel.discoverable,
slug: chat_channel.slug,
status: chat_channel.status,
id: chat_channel.id
},
memberships: {
active: membership_users(chat_channel.active_memberships),
pending: membership_users(chat_channel.pending_memberships),
requested: membership_users(chat_channel.requested_memberships)
},
current_membership: current_membership
}
end
def membership_users(memberships)
memberships.includes(:user).map do |membership|
{
name: membership.user.name,
username: membership.user.username,
user_id: membership.user.id,
membership_id: membership.id,
role: membership.role,
status: membership.status,
image: ProfileImage.new(membership.user).get(width: 90)
}
end
end
end

View file

@ -0,0 +1,19 @@
class MembershipUserPresenter
def initialize(membership)
@membership = membership
end
attr_accessor :membership
def as_json
{
name: membership.user.name,
username: membership.user.username,
user_id: membership.user.id,
membership_id: membership.id,
role: membership.role,
status: membership.status,
image: ProfileImage.new(membership.user).get(width: 90)
}
end
end

View file

@ -0,0 +1,5 @@
class ApplicationService
def self.perform(*args, &block)
new(*args, &block).perform
end
end

View file

@ -1,4 +1,4 @@
class ChatChannelUpdateService
class ChatChannelUpdateService < ApplicationService
attr_accessor :chat_channel, :params
def initialize(chat_channel, params)
@ -6,7 +6,8 @@ class ChatChannelUpdateService
@params = params
end
def update
def perform
chat_channel.update(params)
chat_channel
end
end

View file

@ -30,6 +30,7 @@ module PracticalDeveloper
config.autoload_paths += Dir["#{config.root}/app/labor/"]
config.autoload_paths += Dir["#{config.root}/app/decorators/"]
config.autoload_paths += Dir["#{config.root}/app/services/"]
config.autoload_paths += Dir["#{config.root}/app/presenters/"]
config.autoload_paths += Dir["#{config.root}/app/liquid_tags/"]
config.autoload_paths += Dir["#{config.root}/app/black_box/"]
config.autoload_paths += Dir["#{config.root}/app/sanitizers"]

View file

@ -283,6 +283,15 @@ Rails.application.routes.draw do
post "/pusher/auth" => "pusher#auth"
# Chat channel
patch "/chat_channels/update_channel/:id" => "chat_channels#update_channel"
# Chat Channel Membership json response
get "/chat_channel_memberships/chat_channel_info/:id" => "chat_channel_memberships#chat_channel_info"
post "/chat_channel_memberships/create_membership_request" => "chat_channel_memberships#create_membership_request"
patch "/chat_channel_memberships/leave_membership/:id" => "chat_channel_memberships#leave_membership"
patch "/chat_channel_memberships/update_membership/:id" => "chat_channel_memberships#update_membership"
get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview
get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview
get "/social_previews/organization/:id" => "social_previews#organization", :as => :organization_social_preview

View file

@ -10,178 +10,121 @@ RSpec.describe "ChatChannelMemberships", type: :request do
chat_channel.add_users([user])
end
describe "GET /chat_channel_memberships" do
context "when pending invitations exists" do
describe "GET /chat_channel_info" do
context "when chat channel membership role is mod" do
before do
user.add_role(:super_admin)
post "/chat_channel_memberships", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
}
}
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
get "/chat_channel_memberships/chat_channel_info/#{membership.id}"
end
it "shows chat_channel_memberships list pending invitation" do
sign_in second_user
get "/chat_channel_memberships"
expect(response.body).to include "Pending Invitations"
expect(response.body).to include chat_channel.channel_name.to_s
it "return all details of chat channel" do
expect(response.status).to eq(200)
puts response.body.inspect
expect(JSON.parse(response.body)["result"].keys).to eq(%w[chat_channel memberships current_membership])
end
end
context "when no pending invitation" do
it "shows chat_channel_memberships list pending invitation" do
sign_in second_user
get "/chat_channel_memberships"
expect(response.body).to include "You have no pending invitations"
end
end
end
describe "GET /chat_channel_memberships/find_by_chat_channel_id" do
context "when user is logged in" do
context "when membership role is member" do
before do
sign_in second_user
chat_channel.add_users([second_user])
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id)
get "/chat_channel_memberships/chat_channel_info/#{membership.id}"
end
it "returns chat channel membership details" do
sign_in second_user
get "/chat_channel_memberships/find_by_chat_channel_id", params: { chat_channel_id: chat_channel.id }
expected_keys = %w[id status chat_channel_id last_opened_at channel_text
channel_last_message_at channel_status channel_username
channel_type channel_name channel_image
channel_modified_slug channel_messages_count]
expect(response.parsed_body.keys).to(match_array(expected_keys))
end
end
context "when user is not logged in" do
it "renders not_found" do
expect do
get "/chat_channel_memberships/find_by_chat_channel_id", params: {}
end.to raise_error(ActiveRecord::RecordNotFound)
it "return only channel info and current membership" do
expect(response.status).to eq(200)
expect(JSON.parse(response.body)["result"].keys).to eq(%w[chat_channel memberships current_membership])
expect(JSON.parse(response.body)["result"]["memberships"]["pending"].length).to eq(0)
expect(JSON.parse(response.body)["result"]["memberships"]["requested"].length).to eq(0)
end
end
end
describe "GET /chat_channel_memberships/:id/edit" do
before do
chat_channel.add_users([second_user])
end
let(:chat_channel_membership) { chat_channel.chat_channel_memberships.where(user_id: second_user.id).first }
context "when user is not logged in" do
it "raise Pundit::NotAuthorizedError" do
expect do
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
end.to raise_error(Pundit::NotAuthorizedError)
end
end
context "when user is logged in and channel id is wrong" do
it "raise ActiveRecord::RecordNotFound" do
sign_in second_user
expect do
get "/chat_channel_memberships/ERW/edit"
end.to raise_error(ActiveRecord::RecordNotFound)
end
end
context "when user is channel member" do
it "allows user to view channel members" do
sign_in second_user
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
expect(response.body).to include("Members")
expect(response.body).to include(user.username.to_s)
expect(response.body).to include(second_user.username.to_s)
expect(response.body).not_to include("Pending Invitations")
end
end
context "when user is channel moderator" do
it "allows user to view channel members" do
sign_in second_user
chat_channel_membership.update(role: "mod")
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
expect(response.body).to include("Members")
expect(response.body).to include(user.username.to_s)
expect(response.body).to include(second_user.username.to_s)
expect(response.body).to include("Pending Invitations")
expect(response.body).to include("You are a channel mod")
end
end
end
describe "POST /chat_channel_memberships" do
context "when user is super admin" do
it "creates chat channel invitation" do
describe "POST/ Send Invitation fot chat channel" do
context "when chat channel membership role is mod" do
before do
user.add_role(:super_admin)
expect do
post "/chat_channel_memberships", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
}
}
end.to change { ChatChannelMembership.all.size }.by(1)
expect(ChatChannelMembership.last.status).to eq("pending")
end
end
context "when user is channel moderator, and invited user was a member of channel, and than left channel" do
it "creates chat channel invitation" do
chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod")
ChatChannelMembership.create(chat_channel_id: chat_channel.id, user_id: second_user.id, status: "left_channel")
post "/chat_channel_memberships", params: {
post "/chat_channel_memberships/create_membership_request", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
}
}
expect(ChatChannelMembership.last.status).to eq("pending")
end
it "Send invitation to user" do
expect(response.status).to eq(200)
end
end
context "when user is channel moderator, invited user was not a member of channel" do
it "creates chat channel invitation" do
chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod")
chat_channel_members_count = ChatChannelMembership.all.size
post "/chat_channel_memberships", params: {
context "when chat channel membership role is member" do
before do
sign_in second_user
chat_channel.add_users([second_user])
post "/chat_channel_memberships/create_membership_request", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
invitation_usernames: "test2031",
chat_channel_id: chat_channel.id
}
}
expect(ChatChannelMembership.all.size).to eq(chat_channel_members_count + 1)
expect(ChatChannelMembership.last.status).to eq("pending")
end
it "disallows invitation creation when org private group" do
chat_channel.update_column(:channel_name, "@org private group chat")
chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod")
expect do
post "/chat_channel_memberships", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
}
}
end.to raise_error(Pundit::NotAuthorizedError)
it "user not authorized" do
expect(response.status).to eq(401)
end
end
end
describe "POST/ remove_membership" do
context "when the user is super admin" do
it "remove the user from chat channel" do
allow(Pusher).to receive(:trigger).and_return(true)
user.add_role(:super_admin)
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: membership.id
}
expect(response.status).to eq(200)
expect(membership.reload.status).to eq("removed_from_channel")
end
end
context "when user is not authorized to add channel membership" do
it "raise Pundit::NotAuthorizedError" do
expect do
post "/chat_channel_memberships", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
}
}
end.to raise_error(Pundit::NotAuthorizedError)
context "when user is chat channel membership role is mod" do
it "remove the user from chat channel" do
allow(Pusher).to receive(:trigger).and_return(true)
membership = chat_channel.chat_channel_memberships.find_by(user_id: user.id)
membership.update(role: "mod")
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: membership.id
}
expect(response.status).to eq(200)
expect(membership.reload.status).to eq("removed_from_channel")
end
end
context "when user chat channel membership role is member" do
it "user is not unauthorized" do
sign_in second_user
membership = ChatChannelMembership.last
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: membership.id
}
expect(response.status).to eq(401)
end
end
end
@ -189,7 +132,7 @@ RSpec.describe "ChatChannelMemberships", type: :request do
describe "PUT /chat_channel_memberships/:id" do
before do
user.add_role(:super_admin)
post "/chat_channel_memberships", params: {
post "/chat_channel_memberships/create_membership_request", params: {
chat_channel_membership: {
invitation_usernames: second_user.username.to_s,
chat_channel_id: chat_channel.id
@ -227,97 +170,96 @@ RSpec.describe "ChatChannelMemberships", type: :request do
end
context "when user not logged in" do
it "raise Pundit::NotAuthorizedError" do
it "unauthorized" do
membership = ChatChannelMembership.last
expect do
put "/chat_channel_memberships/#{membership.id}", params: {
chat_channel_membership: { user_action: "accept" }
}
end.to raise_error(Pundit::NotAuthorizedError)
end
end
put "/chat_channel_memberships/#{membership.id}", params: {
chat_channel_membership: { user_action: "accept" }
}
context "when user is unauthorized" do
it "raise Pundit::NotAuthorizedError" do
membership = ChatChannelMembership.last
sign_in user
expect do
put "/chat_channel_memberships/#{membership.id}", params: {
chat_channel_membership: { user_action: "accept" }
}
end.to raise_error(Pundit::NotAuthorizedError)
expect(response.status).to eq(401)
end
end
end
describe "DELETE /chat_channel_memberships/:id" do
describe "PATCH/ update_membership" do
context "when user is logged in" do
it "update the notification status" do
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
membership.update(show_global_badge_notification: false)
patch "/chat_channel_memberships/update_membership/#{membership.id}", params: {
chat_channel_membership: {
show_global_badge_notification: true
}
}
expect(response.status).to eq(200)
expect(membership.reload.show_global_badge_notification).to eq(true)
end
end
context "when user is not logged in" do
it "not found membership" do
patch "/chat_channel_memberships/update_membership/", params: {
chat_channel_membership: {
show_global_badge_notification: true
}
}
expect(response.status).to eq(404)
end
end
end
describe "POST/ /leave_membership/:id" do
context "when user is logged in" do
it "leaves chat channel" do
allow(Pusher).to receive(:trigger).and_return(true)
chat_channel.add_users([second_user])
membership = ChatChannelMembership.last
sign_in second_user
delete "/chat_channel_memberships/#{membership.id}", params: {}
expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel")
expect(response).to(redirect_to(chat_channel_memberships_path))
patch "/chat_channel_memberships/leave_membership/#{membership.id}"
expect(response.status).to eq(200)
expect(membership.reload.status).to eq("left_channel")
end
end
context "when user is not logged in" do
it "raise Pundit::NotAuthorizedError" do
it "unauthorized user" do
chat_channel.add_users([second_user])
membership = ChatChannelMembership.last
expect do
delete "/chat_channel_memberships/#{membership.id}", params: {}
end.to(raise_error(Pundit::NotAuthorizedError))
membership_status = membership.status
patch "/chat_channel_memberships/leave_membership/#{membership.id}"
expect(response.status).to eq(401)
expect(membership.reload.status).to eq(membership_status)
end
end
end
describe "POST /chat_channel_memberships/remove_membership" do
before do
chat_channel.add_users([second_user])
end
describe "GET /chat_channel_memberships/find_by_chat_channel_id" do
context "when user is logged in" do
before do
chat_channel.add_users([second_user])
end
context "when user is super admin" do
it "removes member from channel" do
allow(Pusher).to receive(:trigger).and_return(true)
user.add_role(:super_admin)
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
removed_channel_membership = ChatChannelMembership.last
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: removed_channel_membership.id
}
expect(removed_channel_membership.reload.status).to eq("removed_from_channel")
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
it "returns chat channel membership details" do
sign_in second_user
get "/chat_channel_memberships/find_by_chat_channel_id", params: { chat_channel_id: chat_channel.id }
expected_keys = %w[id status chat_channel_id last_opened_at channel_text
channel_last_message_at channel_status channel_username
channel_type channel_name channel_image
channel_modified_slug channel_messages_count]
expect(response.parsed_body.keys).to(match_array(expected_keys))
end
end
context "when user is moderator of channel" do
it "removes member from channel" do
allow(Pusher).to receive(:trigger).and_return(true)
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
membership.update(role: "mod")
removed_channel_membership = ChatChannelMembership.last
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: removed_channel_membership.id
}
expect(removed_channel_membership.reload.status).to eq("removed_from_channel")
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
end
end
context "when user is member of channel" do
it "raise Pundit::NotAuthorizedError" do
membership = ChatChannelMembership.last
expect do
post "/chat_channel_memberships/remove_membership", params: {
chat_channel_id: chat_channel.id,
membership_id: membership.id
}
end.to(raise_error(Pundit::NotAuthorizedError))
context "when user is not logged in" do
it "renders not_found" do
get "/chat_channel_memberships/find_by_chat_channel_id", params: {}
expect(response.status).to eq(404)
end
end
end
@ -338,24 +280,25 @@ RSpec.describe "ChatChannelMemberships", type: :request do
}
}
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
expect(response).to(redirect_to(chat_channel_memberships_path))
end
end
context "when user is member of channel" do
it "raise Pundit::NotAuthorizedError" do
expect do
channel = ChatChannel.first
membership = ChatChannelMembership.last
membership.update(status: "joining_request")
post "/chat_channel_memberships/add_membership", params: {
membership_id: membership.id,
chat_channel_id: channel.id,
chat_channel_membership: {
user_action: "accept"
}
it "User not authorize" do
channel = ChatChannel.first
membership = ChatChannelMembership.last
membership.update(status: "joining_request")
post "/chat_channel_memberships/add_membership", params: {
membership_id: membership.id,
chat_channel_id: channel.id,
chat_channel_membership: {
user_action: "accept"
}
end.to(raise_error(Pundit::NotAuthorizedError))
}
expect(ChatChannelMembership.find(membership.id).status).to eq("joining_request")
expect(response.status).to eq(401)
end
end
end

View file

@ -176,6 +176,45 @@ RSpec.describe "ChatChannels", type: :request do
end
end
describe "PATCH /chat_channels/update_channel/:id" do
it "updates chat channel for valid user" do
user.add_role(:super_admin)
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
membership.update(role: "mod")
patch "/chat_channels/update_channel/#{chat_channel.id}", params: {
chat_channel: {
channel_name: "Hello Channel",
slug: "hello-channelly",
}
}
expect(response.status).to eq(200)
expect(ChatChannel.last.slug).to eq("hello-channelly")
end
it "un-authorized users" do
expect do
patch "/chat_channels/update_channel/#{chat_channel.id}", params: {
chat_channel: {
channel_name: "Hello Channel",
slug: "hello-channelly"
}
}
end.to raise_error(Pundit::NotAuthorizedError)
end
it "returns errors if channel is invalid" do
# slug should be taken
user.add_role(:super_admin)
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
membership.update(role: "mod")
patch "/chat_channels/update_channel/#{chat_channel.id}", params: {
chat_channel: { channel_name:"Hello Channel", slug: invite_channel.slug }
}
expect(ChatChannel.last.slug).not_to eq("hello-channelly")
end
end
describe "POST /chat_channels/:id/moderate" do
it "raises NotAuthorizedError if user is not logged in" do
expect do