From 3addb64326e39dfaaa28da6a5e3b7e4555026ae1 Mon Sep 17 00:00:00 2001 From: Sarthak Sharma <7lovesharma7@gmail.com> Date: Mon, 20 Jul 2020 17:38:31 +0530 Subject: [PATCH] =?UTF-8?q?[deploy]=20=F0=9F=9A=80=20Feature:=20Chat=20cha?= =?UTF-8?q?nnel=20membership=20manager=20component=20(#8945)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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 * Merge conflict resolved * fix video content issue * add UI for membership management * add api to update memberahip role * add methods for manage membership * Add integration test cases * add emoji for admin * Open member profile in sidecar only * ๐Ÿž Problem with direct channel sidecar * ๐Ÿž Few other UI enhancements * fix mod typo * add limit upto 4 for display active memberships * fix UI issues * fix action ui for membership * fix sidebar redirection issue * fix svg buttons * add test cases * fixed broken spec * fix PR suggestions * remove not used code * fix typo * fix PR suggestion * fix typos * add invitation url expiry * fix specs * fix PR suggestions * removed unused gem * remove presenter format * handle invitation link expiry with redis * fix PR suggestions * user can view non-discoverable channel invitation link * fix typos * remove commented code * add spacing * PR suggestions * remove class from button * replace componentDidMount with commen function * remove action button for single membership * add chat message on update role * add spece between lines Co-authored-by: Narender Singh Co-authored-by: Fernando Valverde --- SECURITY.md | 3 +- app/assets/stylesheets/chat.scss | 64 +++++++ .../chat_channel_memberships_controller.rb | 88 ++++++++- app/helpers/chat_channel_membership_helper.rb | 27 +++ .../ActiveMembershipsSection.jsx | 49 ++--- .../ChatChannelMembershipSection.jsx | 44 ++--- .../ChatChannelSettings.jsx | 163 +++++++++++------ .../ChatChannelSettingsSection.jsx | 106 +++++++++++ .../chat/ChatChannelSettings/InviteForm.jsx | 13 +- .../LeaveMembershipSection.jsx | 21 ++- .../chat/ChatChannelSettings/Membership.jsx | 30 ++-- .../InvitationLinkManager.jsx | 121 +++++++++++++ .../ManageActiveMembership.jsx | 119 +++++++++++++ .../MembershipManager/Membership.jsx | 130 ++++++++++++++ .../MembershipManager/MembershipSection.jsx | 52 ++++++ .../ChatChannelSettings/ModFaqSection.jsx | 7 +- .../chat/ChatChannelSettings/ModSection.jsx | 11 +- .../PendingMembershipSection.jsx | 14 +- .../ChatChannelSettings/PersonalSetting.jsx | 12 +- .../RequestedMembershipSection.jsx | 16 +- .../chat/ChatChannelSettings/SettingsForm.jsx | 9 +- .../__tests__/ManageActiveMembership.test.jsx | 41 +++++ .../chat/__tests__/Membership.test.jsx | 46 +++++ .../chatChannelSettingActions.test.js | 23 +++ .../__tests__/invitationLinkManager.test.jsx | 74 ++++++++ .../__tests__/leaveMembershipSection.test.jsx | 4 +- .../chat/__tests__/membership.test.jsx | 167 ------------------ .../actions/chat_channel_setting_actions.js | 26 ++- app/javascript/chat/chat.jsx | 13 +- app/javascript/chat/content.jsx | 2 +- .../common-prop-types/membership-prop-type.js | 11 ++ .../utilities/validateAndroidNative.js | 10 ++ app/policies/chat_channel_policy.rb | 12 ++ .../chat_channel_detail_presenter.rb | 42 ----- app/presenters/membership_user_presenter.rb | 19 -- .../chat_channel_info.json.jbuilder | 20 +++ .../join_channel_invitation.html.erb | 17 ++ config/fastly/snippets/safe_params_list.vcl | 2 +- config/routes.rb | 3 + .../requests/chat_channel_memberships_spec.rb | 132 +++++++++++++- 40 files changed, 1333 insertions(+), 430 deletions(-) create mode 100644 app/helpers/chat_channel_membership_helper.rb create mode 100644 app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx create mode 100644 app/javascript/chat/__tests__/ManageActiveMembership.test.jsx create mode 100644 app/javascript/chat/__tests__/Membership.test.jsx create mode 100644 app/javascript/chat/__tests__/invitationLinkManager.test.jsx delete mode 100644 app/javascript/chat/__tests__/membership.test.jsx create mode 100644 app/javascript/common-prop-types/membership-prop-type.js create mode 100644 app/javascript/utilities/validateAndroidNative.js delete mode 100644 app/presenters/chat_channel_detail_presenter.rb delete mode 100644 app/presenters/membership_user_presenter.rb create mode 100644 app/views/chat_channel_memberships/chat_channel_info.json.jbuilder create mode 100644 app/views/chat_channel_memberships/join_channel_invitation.html.erb diff --git a/SECURITY.md b/SECURITY.md index 4791b3505..10c08f154 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,4 +2,5 @@ ## Reporting a Vulnerability -Please refer to https://dev.to/security if you would like to report a vulnerability. +Please refer to https://dev.to/security if you would like to report a +vulnerability. diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index c3473d2d2..d0173cd05 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -282,6 +282,7 @@ @include themeable(border, theme-border, 1px solid $outline-color); padding: 13px; min-width: 50%; + overflow-x: hidden; @media screen and (min-width: 1440px) { min-width: 45%; } @@ -1164,3 +1165,66 @@ .channel_details { margin-top: 30px; } + +.chat_channel-member-list { + height: 300px; + overflow-y: scroll; +} + +.member-list-item { + justify-content: space-between; + + .admin-emoji { + width: 6%; + } + + .admin-emoji-button { + width: 10%; + } +} + +.membership-section { + position: relative; + + .membership-actions { + position: relative; + + .membership-management__dropdown-memu { + display: none; + position: absolute; + background-color: var(--theme-container-background, #fff); + border-style: solid; + border-color: var(--theme-color, #0a0a0a); + border-radius: 5px; + border-width: 1px; + z-index: 100; + width: 200px; + right: 78px; + top: 20px; + + button { + width: 100%; + text-align: left; + } + + &:hover { + display: block; + } + } + + .membership-management__dropdown-button { + height: 17px; + cursor: pointer; + color: var(--theme-color, #0a0a0a); + + &:hover + .membership-management__dropdown-memu { + display: block; + } + + img { + height: 100%; + opacity: 0.6; + } + } + } +} diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 991534b05..a8150e84f 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -25,15 +25,17 @@ class ChatChannelMembershipsController < ApplicationController @membership = ChatChannelMembership.find(params[:id]) authorize @membership @channel = @membership.chat_channel - data = ChatChannelDetailPresenter.new(@channel, @membership).as_json - - render json: { success: true, result: data, message: "" }, success: :ok + invite_cache_key = "chat-channel-invite-#{@channel.id}" + invitation_slug = Rails.cache.fetch(invite_cache_key, expires_in: 12.hours) do + "invitation-link-#{SecureRandom.hex(3)}" + end + @invitation_link = "/join_channel_invitation/#{@channel.slug}?invitation_slug=#{invitation_slug}" end def create_membership_request - chat_channel = ChatChannel.find_by(id: channel_membership_request_params[:chat_channel_id]) + chat_channel = ChatChannel.find_by(id: channel_membership_params[:chat_channel_id]) authorize chat_channel, :update? - usernames = channel_membership_request_params[:invitation_usernames].split(",").map do |username| + usernames = channel_membership_params[:invitation_usernames].split(",").map do |username| username.strip.delete("@") end users = User.where(username: usernames) @@ -131,14 +133,82 @@ class ChatChannelMembershipsController < ApplicationController end end + def update_membership_role + @chat_channel = ChatChannel.find_by(id: params[:id]) + authorize @chat_channel, :update? + membership = ChatChannelMembership.find_by( + id: channel_membership_params[:membership_id], + chat_channel_id: @chat_channel.id, + ) + + membership.update(role: channel_membership_params[:role]) + if membership.errors.any? + render json: { + success: false, + message: "Failed to update membership", + errors: chat_channel_membership.errors.full_messages + }, status: :bad_request + else + role = membership.reload.role + send_chat_action_message( + "@#{membership.user.username} role is updated as #{role}", + current_user, @chat_channel.id, + "updated" + ) + + render json: { success: true, message: "User Membership is updated" }, status: :ok + end + end + + def join_channel_invitation + @chat_channel = ChatChannel.find_by(slug: params[:channel_slug]) + authorize @chat_channel + invite_cache_key = "chat-channel-invite-#{@chat_channel.id}" + invitation_slug = Rails.cache.read(invite_cache_key) + existing_membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: @chat_channel.id) + redirect_to connect_path(@chat_channel.slug) if existing_membership && existing_membership.status == "active" + @link_expired = true if invitation_slug != params[:invitation_slug] + end + + def joining_invitation_response + chat_channel = ChatChannel.find_by(id: params[:chat_channel_id]) + authorize chat_channel + if params[:user_action] == "accept" + membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: chat_channel.id) + if !membership + membership = ChatChannelMembership.new(user_id: current_user.id, chat_channel_id: chat_channel.id) + membership.save + unless membership&.errors&.any? + send_chat_action_message("@#{membership.user.username} join the channel", current_user, chat_channel.id, + "joined") + end + elsif membership.status != "active" + # This check checks if the user already has the chatChannelMembership with the status pending, joining_request + # Then update it to as active. + membership.update(role: "member", status: "active") + send_chat_action_message("@#{membership.user.username} join the channel", current_user, + membership.chat_channel_id, "joined") + end + + if membership&.errors&.any? + flash[:settings_notice] = membership.errors.full_messages + redirect_to root_path + end + + redirect_to connect_path(chat_channel.slug) + else + redirect_to root_path + end + end + private def permitted_params 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) + def channel_membership_params + params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames, :membership_id, :role) end def respond_to_invitation(previous_status) @@ -176,9 +246,9 @@ class ChatChannelMembershipsController < ApplicationController notice = "Invitation rejected." end - flash[:settings_notice] = notice + membership_user = helpers.format_membership(@chat_channel_membership) - membership_user = MembershipUserPresenter.new(@chat_channel_membership).as_json + flash[:settings_notice] = notice respond_to do |format| format.html { redirect_to chat_channel_memberships_path } diff --git a/app/helpers/chat_channel_membership_helper.rb b/app/helpers/chat_channel_membership_helper.rb new file mode 100644 index 000000000..dc1ffb8b1 --- /dev/null +++ b/app/helpers/chat_channel_membership_helper.rb @@ -0,0 +1,27 @@ +module ChatChannelMembershipHelper + 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 + + def format_membership(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 diff --git a/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx index 65cc0eae7..17d8b03f2 100644 --- a/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx @@ -1,12 +1,18 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; + +import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type'; import Membership from './Membership'; +import { Button } from '@crayons'; const ActiveMembershipSection = ({ activeMemberships, removeMembership, currentMembershipRole, + toggleScreens, }) => { + const activeMembershipList = activeMemberships.slice(0, 4); + return (

Members

- {activeMemberships && activeMemberships.length > 0 - ? activeMemberships.map((pendingMembership) => ( - - )) - : null} + {activeMembershipList.map((activeMembership) => ( + + ))} +
+ +
); }; 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, + activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired, removeMembership: PropTypes.func.isRequired, currentMembershipRole: PropTypes.string.isRequired, + toggleScreens: PropTypes.func.isRequired, }; export default ActiveMembershipSection; diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx index 45524e6d4..8419d1854 100644 --- a/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx @@ -1,8 +1,9 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type'; import ActiveMembershipSection from './ActiveMembershipsSection'; -import PendingMembershipSection from './PendingMembershipSection' +import PendingMembershipSection from './PendingMembershipSection'; import RequestedMembershipSection from './RequestedMembershipSection'; const ChatChannelMembershipSection = ({ @@ -11,7 +12,8 @@ const ChatChannelMembershipSection = ({ chatChannelAcceptMembership, activeMemberships, removeMembership, - currentMembershipRole + currentMembershipRole, + toggleScreens, }) => { return (
@@ -19,6 +21,7 @@ const ChatChannelMembershipSection = ({ activeMemberships={activeMemberships} removeMembership={removeMembership} currentMembershipRole={currentMembershipRole} + toggleScreens={toggleScreens} />
- ) -} + ); +}; 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, + pendingMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired, + requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired, + activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired, removeMembership: PropTypes.func.isRequired, chatChannelAcceptMembership: PropTypes.func.isRequired, - currentMembershipRole: PropTypes.string.isRequired -} + currentMembershipRole: PropTypes.string.isRequired, + toggleScreens: PropTypes.func.isRequired, +}; export default ChatChannelMembershipSection; diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx index 59daf6a02..2f1dbad1b 100644 --- a/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx +++ b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx @@ -9,15 +9,12 @@ import { updateChatChannelDescription, sendChatChannelInvitation, leaveChatChannelMembership, + updateMembershipRole, } from '../actions/chat_channel_setting_actions'; import { 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'; +import ManageActiveMembership from './MembershipManager/ManageActiveMembership'; +import ChatChannelSettingsSection from './ChatChannelSettingsSection'; export default class ChatChannelSettings extends Component { static propTypes = { @@ -35,16 +32,30 @@ export default class ChatChannelSettings extends Component { requestedMemberships: [], chatChannel: null, currentMembership: null, - activeMembershipId: props.activeMembershipId, + activeMembershipId: null, channelDescription: null, channelDiscoverable: null, invitationUsernames: null, showGlobalBadgeNotification: null, + displaySettings: true, + displayMembershipManager: false, + invitationLink: null, }; } componentDidMount() { - const { activeMembershipId } = this.state; + this.updateChannelDetails(); + } + + componentWillReceiveProps() { + const { activeMembershipId } = this.props; + this.setState({ + activeMembershipId, + }); + } + + updateChannelDetails = () => { + const { activeMembershipId } = this.props; getChannelDetails(activeMembershipId) .then((response) => { @@ -60,6 +71,7 @@ export default class ChatChannelSettings extends Component { channelDiscoverable: result.chat_channel.discoverable, showGlobalBadgeNotification: result.current_membership.show_global_badge_notification, + invitationLink: result.invitation_link, }); } else { this.setState({ @@ -74,7 +86,7 @@ export default class ChatChannelSettings extends Component { errorMessages: error.message, }); }); - } + }; handleDescriptionChange = (e) => { const description = e.target.value; @@ -83,7 +95,7 @@ export default class ChatChannelSettings extends Component { }); }; - handlePersonChatChennelSetting = (e) => { + handlePersonChannelSetting = (e) => { const status = e.target.checked; this.setState({ showGlobalBadgeNotification: status, @@ -149,6 +161,7 @@ export default class ChatChannelSettings extends Component { updateMemberships = (membershipId, response, membershipStatus) => { if (response.success) { + this.updateChannelDetails(); this.setState((prevState) => { return { errorMessages: null, @@ -236,7 +249,7 @@ export default class ChatChannelSettings extends Component { const { message } = response; if (response.success) { - this.componentDidMount(); + this.updateChannelDetails(); this.setState((prevState) => { return { errorMessages: null, @@ -265,13 +278,13 @@ export default class ChatChannelSettings extends Component { }); }; - handleChatChannelInvitations = async () => { + handleChannelInvitations = async () => { const { invitationUsernames, chatChannel } = this.state; const { id } = chatChannel; const response = await sendChatChannelInvitation(id, invitationUsernames); const { message } = response; if (response.success) { - this.componentDidMount(); + this.updateChannelDetails(); this.setState({ errorMessages: null, successMessages: response.message, @@ -286,7 +299,7 @@ export default class ChatChannelSettings extends Component { addSnackbarItem({ message }); }; - handleleaveChatChannelMembership = async () => { + handleleaveChannelMembership = 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.', @@ -295,7 +308,7 @@ export default class ChatChannelSettings extends Component { if (actionStatus) { const response = await leaveChatChannelMembership(currentMembership.id); if (response.success) { - this.componentDidMount(); + this.updateChannelDetails(); } else { this.setState({ successMessages: null, @@ -305,6 +318,53 @@ export default class ChatChannelSettings extends Component { } }; + toggleScreens = () => { + const { displaySettings, displayMembershipManager } = this.state; + + this.setState({ + displaySettings: !displaySettings, + displayMembershipManager: !displayMembershipManager, + }); + }; + + handleUpdateMembershipRole = async (e) => { + const { membershipId, role } = e.target.dataset; + const { chatChannel } = this.state; + const response = await updateMembershipRole( + membershipId, + chatChannel.id, + role, + ); + const { message } = response; + if (response.success) { + this.updateChannelDetails(); + this.setState((prevState) => { + const { activeMemberships } = prevState; + const updatedActiveMemberships = activeMemberships.map( + (activeMembership) => { + if (activeMembership.membership_id === Number(membershipId)) { + return { ...activeMembership, role }; + } + return activeMembership; + }, + ); + return { + ...prevState, + activeMemberships: updatedActiveMemberships, + errorMessages: null, + successMessages: response.message, + }; + }); + } else { + this.setState({ + successMessages: null, + errorMessages: response.message, + }); + } + + addSnackbarItem({ message }); + }; + render() { const { chatChannel, @@ -316,6 +376,8 @@ export default class ChatChannelSettings extends Component { channelDiscoverable, invitationUsernames, showGlobalBadgeNotification, + displaySettings, + invitationLink, } = this.state; if (!chatChannel) { @@ -325,53 +387,44 @@ export default class ChatChannelSettings extends Component { return (
- - - {currentMembership.role === 'mod' && ( - - )} - - {currentMembership.role === 'member' && ( - + ) : ( + - )} - {currentMembership.role === 'mod' && ( - )}
diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx new file mode 100644 index 000000000..7f09b9252 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx @@ -0,0 +1,106 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +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 ChatChannelSettingsSection = ({ + channelDiscoverable, + updateCurrentMembershipNotificationSettings, + handleleaveChannelMembership, + handlePersonChannelSetting, + handleChannelDescriptionChanges, + handleChannelDiscoverableStatus, + handleDescriptionChange, + handleChannelInvitations, + handleInvitationUsernames, + toggleScreens, + removeMembership, + chatChannelAcceptMembership, + channelDescription, + chatChannel, + currentMembership, + activeMemberships, + pendingMemberships, + requestedMemberships, + invitationUsernames, + showGlobalBadgeNotification, +}) => ( +
+ + + + + + +
+); + +ChatChannelSettingsSection.propTypes = { + chatChannel: PropTypes.isRequired, + currentMembership: PropTypes.isRequired, + activeMemberships: PropTypes.isRequired, + pendingMemberships: PropTypes.isRequired, + requestedMemberships: PropTypes.isRequired, + invitationUsernames: PropTypes.string.isRequired, + channelDescription: PropTypes.string.isRequired, + channelDiscoverable: PropTypes.bool.isRequired, + showGlobalBadgeNotification: PropTypes.bool.isRequired, + handleleaveChannelMembership: PropTypes.func.isRequired, + chatChannelAcceptMembership: PropTypes.func.isRequired, + removeMembership: PropTypes.func.isRequired, + toggleScreens: PropTypes.func.isRequired, + handleInvitationUsernames: PropTypes.func.isRequired, + handleChannelInvitations: PropTypes.func.isRequired, + handleDescriptionChange: PropTypes.func.isRequired, + handleChannelDiscoverableStatus: PropTypes.func.isRequired, + handleChannelDescriptionChanges: PropTypes.func.isRequired, + handlePersonChannelSetting: PropTypes.func.isRequired, + updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired, +}; + +export default ChatChannelSettingsSection; diff --git a/app/javascript/chat/ChatChannelSettings/InviteForm.jsx b/app/javascript/chat/ChatChannelSettings/InviteForm.jsx index 700551d4c..194b71766 100644 --- a/app/javascript/chat/ChatChannelSettings/InviteForm.jsx +++ b/app/javascript/chat/ChatChannelSettings/InviteForm.jsx @@ -1,8 +1,9 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import { Button } from '@crayons'; const InviteForm = ({ - handleChatChannelInvitations, + handleChannelInvitations, invitationUsernames, handleInvitationUsernames, }) => { @@ -26,13 +27,9 @@ const InviteForm = ({ />
- +
); @@ -40,7 +37,7 @@ const InviteForm = ({ InviteForm.propTypes = { handleInvitationUsernames: PropTypes.func.isRequired, - handleChatChannelInvitations: PropTypes.func.isRequired, + handleChannelInvitations: PropTypes.func.isRequired, invitationUsernames: PropTypes.func.isRequired, }; diff --git a/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx index fe6aa3b9d..9e0eb6909 100644 --- a/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx @@ -1,25 +1,34 @@ import { h } from 'preact'; import PropsType from 'prop-types'; +import { Button } from '@crayons'; + +const LeaveMembershipSection = ({ + handleleaveChannelMembership, + currentMembershipRole, +}) => { + if (currentMembershipRole === 'mod') { + return null; + } -const LeaveMembershipSection = ({ handleleaveChatChannelMembership }) => { return (

Danger Zone

- +
); }; LeaveMembershipSection.propTypes = { - handleleaveChatChannelMembership: PropsType.func.isRequired, + handleleaveChannelMembership: PropsType.func.isRequired, }; export default LeaveMembershipSection; diff --git a/app/javascript/chat/ChatChannelSettings/Membership.jsx b/app/javascript/chat/ChatChannelSettings/Membership.jsx index 6031221fd..4071a94e3 100644 --- a/app/javascript/chat/ChatChannelSettings/Membership.jsx +++ b/app/javascript/chat/ChatChannelSettings/Membership.jsx @@ -1,5 +1,7 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type'; +import { Button } from '@crayons'; const Membership = ({ membership, @@ -10,7 +12,13 @@ const Membership = ({ }) => { return (
- + {membership.name} {membershipType === 'requested' ? ( - + ) : null} {membership.role !== 'mod' && currentMembershipRole === 'mod' ? ( - + ) : null}
); }; 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, + membership: PropTypes.objectOf(defaulMembershipPropType).isRequired, removeMembership: PropTypes.func.isRequired, membershipType: PropTypes.func.isRequired, chatChannelAcceptMembership: PropTypes.func.isRequired, diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx new file mode 100644 index 000000000..0ec9b9a42 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx @@ -0,0 +1,121 @@ +import { h, Component } from 'preact'; +import PropTypes from 'prop-types'; +import { isNativeAndroid } from '../../../utilities/validateAndroidNative'; + +import { Button } from '@crayons'; + +function isClipboardSupported() { + return ( + typeof navigator.clipboard !== 'undefined' && navigator.clipboard !== null + ); +} + +const CopyIcon = () => ( + + Copy Invitation Url + + +); + +export default class InvitationLinkManager extends Component { + static propTypes = { + invitationLink: PropTypes.string.isRequired, + currentMembership: PropTypes.isRequired, + }; + + constructor(props) { + super(props); + + this.state = { + invitationLink: props.invitationLink, + showImageCopiedMessage: false, + currentMembership: props.currentMembership, + }; + } + + copyText = () => { + this.imageMarkdownInput = document.getElementById( + 'chat-channel-unviation-url', + ); + + if (isNativeAndroid()) { + AndroidBridge.copyToClipboard(this.imageMarkdownInput.value); + this.setState({ showImageCopiedMessage: true }); + } else if (isClipboardSupported()) { + navigator.clipboard + .writeText(this.imageMarkdownInput.value) + .then(() => { + this.setState({ showImageCopiedMessage: true }); + }) + .catch((_err) => { + this.execCopyText(); + }); + } else { + this.execCopyText(); + } + }; + + execCopyText() { + this.imageMarkdownInput.setSelectionRange( + 0, + this.imageMarkdownInput.value.length, + ); + document.execCommand('copy'); + this.setState({ showImageCopiedMessage: true }); + } + + render() { + const { + showImageCopiedMessage, + invitationLink, + currentMembership, + } = this.state; + + if (currentMembership.role !== 'mod') { + return null; + } + + return ( +
+

Invitation Link

+ + +
+ ); + } +} diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx new file mode 100644 index 000000000..8f05df5e9 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx @@ -0,0 +1,119 @@ +import { h, Component } from 'preact'; +import PropTypes from 'prop-types'; + +import MembershipSection from './MembershipSection'; +import InvitationLinkManager from './InvitationLinkManager'; + +export default class ManageActiveMembership extends Component { + static 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, + currentMembership: PropTypes.isRequired, + invitationLink: PropTypes.string.isRequired, + removeMembership: PropTypes.func.isRequired, + handleUpdateMembershipRole: PropTypes.func.isRequired, + }; + + constructor(props) { + super(props); + this.state = { + activeMemberships: props.activeMemberships, + searchMembers: null, + listAllMemberShips: props.activeMemberships, + currentMembership: props.currentMembership, + invitationLink: props.invitationLink, + removeMembership: props.removeMembership, + handleUpdateMembershipRole: props.handleUpdateMembershipRole, + }; + } + + componentWillReceiveProps() { + const { + activeMemberships, + currentMembership, + removeMembership, + invitationLink, + } = this.props; + this.setState({ + listAllMemberShips: activeMemberships, + currentMembership, + invitationLink, + removeMembership, + activeMemberships, + }); + } + + searchTheMembershipUser = (e) => { + const query = e.target?.value?.toLowerCase(); + + this.setState((prevState) => { + const filteredActiveMemberships = prevState.activeMemberships.filter( + (activeMembership) => { + const value = activeMembership.name.toLowerCase(); + return value.includes(query); + }, + ); + + return { + searchMembers: query, + listAllMemberShips: filteredActiveMemberships, + }; + }); + }; + + render() { + const { + searchMembers, + listAllMemberShips, + currentMembership, + invitationLink, + removeMembership, + handleUpdateMembershipRole, + } = this.state; + + return ( +
+
+

Chat Channel Membership manager

+
+
+ +
+ +
+
+ +
+ ); + } +} diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx new file mode 100644 index 000000000..6045a036c --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx @@ -0,0 +1,130 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; +import adminEmoji from '../../../../assets/images/emoji/apple-fire.png'; +import { Button } from '@crayons'; + +const Membership = ({ + membership, + currentMembership, + removeMembership, + handleUpdateMembershipRole, + showActionButton, +}) => { + const addAsModButton = + membership.role === 'member' ? ( + + ) : null; + + const addAsMemberButton = + membership.role === 'mod' ? ( + + ) : null; + + const removeMembershipButton = + membership.role === 'member' ? ( + + ) : null; + + const dropdown = + currentMembership.role === 'mod' && showActionButton ? ( + + {removeMembershipButton} + {addAsModButton} + {addAsMemberButton} + + ) : null; + + return ( +
+
+ + + {`${membership.name} + + {membership.name} + + {membership.role === 'mod' ? ( + admin emoji + ) : null} + + +
+
{dropdown}
+
+ ); +}; + +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, + currentMembership: PropTypes.isRequired, + removeMembership: PropTypes.func.isRequired, + handleUpdateMembershipRole: PropTypes.func.isRequired, +}; + +export default Membership; diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx new file mode 100644 index 000000000..3310539c3 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx @@ -0,0 +1,52 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; +import Membership from './Membership'; + +const MembershipSection = ({ + memberships, + currentMembership, + removeMembership, + handleUpdateMembershipRole, +}) => { + if (!memberships || memberships.length === 0) { + return

No membership

; + } + + const membershipCount = memberships.length; + + return ( +
+ {memberships.map((activeMembership) => ( + {}} + className="active-member" + currentMembership={currentMembership} + removeMembership={removeMembership} + handleUpdateMembershipRole={handleUpdateMembershipRole} + showActionButton={membershipCount > 1} + /> + ))} +
+ ); +}; + +MembershipSection.propType = { + memberships: 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, + currentMembership: PropTypes.isRequired, + removeMembership: PropTypes.func.isRequired, + handleUpdateMembershipRole: PropTypes.func.isRequired, +}; + +export default MembershipSection; diff --git a/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx index 44f5d0c0f..17fe92d18 100644 --- a/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx @@ -1,7 +1,11 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; -const ModFaqSection = ({ email }) => { +const ModFaqSection = ({ email, currentMembershipRole }) => { + if (currentMembershipRole === 'member') { + return null; + } + return (

@@ -21,6 +25,7 @@ const ModFaqSection = ({ email }) => { ModFaqSection.propTypes = { email: PropTypes.string.isRequired, + currentMembershipRole: PropTypes.string.isRequired, }; export default ModFaqSection; diff --git a/app/javascript/chat/ChatChannelSettings/ModSection.jsx b/app/javascript/chat/ChatChannelSettings/ModSection.jsx index d313a7f19..a229edbbd 100644 --- a/app/javascript/chat/ChatChannelSettings/ModSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/ModSection.jsx @@ -5,7 +5,7 @@ import InviteForm from './InviteForm'; import SettingsForm from './SettingsForm'; const ModSection = ({ - handleChatChannelInvitations, + handleChannelInvitations, invitationUsernames, handleInvitationUsernames, channelDescription, @@ -13,13 +13,18 @@ const ModSection = ({ channelDiscoverable, handleChannelDiscoverableStatus, handleChannelDescriptionChanges, + currentMembershipRole, }) => { + if (currentMembershipRole === 'member') { + return null; + } + return (

{ @@ -16,20 +17,19 @@ const PersonalSettings = ({ id="c3" className="crayons-checkbox" checked={showGlobalBadgeNotification} - onChange={handlePersonChatChennelSetting} + onChange={handlePersonChannelSetting} />
- +
); @@ -38,7 +38,7 @@ const PersonalSettings = ({ PersonalSettings.propTypes = { updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired, showGlobalBadgeNotification: PropTypes.bool.isRequired, - handlePersonChatChennelSetting: PropTypes.func.isRequired, + handlePersonChannelSetting: PropTypes.func.isRequired, }; export default PersonalSettings; diff --git a/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx index 051f4a629..34521126c 100644 --- a/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx +++ b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx @@ -1,5 +1,7 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type'; + import Membership from './Membership'; const RequestedMembershipSection = ({ @@ -8,7 +10,7 @@ const RequestedMembershipSection = ({ chatChannelAcceptMembership, currentMembershipRole, }) => { - if (currentMembershipRole !== 'mod') { + if (currentMembershipRole === 'member') { return null; } @@ -37,17 +39,7 @@ const RequestedMembershipSection = ({ }; 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, + requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired, removeMembership: PropTypes.func.isRequired, chatChannelAcceptMembership: PropTypes.func.isRequired, currentMembershipRole: PropTypes.func.isRequired, diff --git a/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx index 7564bd0af..937026899 100644 --- a/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx +++ b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx @@ -1,5 +1,6 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; +import { Button } from '@crayons'; const SettingsFrom = ({ channelDescription, @@ -42,13 +43,9 @@ const SettingsFrom = ({
- +
); diff --git a/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx b/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx new file mode 100644 index 000000000..d009f7b19 --- /dev/null +++ b/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx @@ -0,0 +1,41 @@ +import { h } from 'preact'; +import { render } from '@testing-library/preact'; +import { axe } from 'jest-axe'; +import ManageActiveMembership from '../ChatChannelSettings/MembershipManager/ManageActiveMembership'; + +const currentModMembership = { + name: 'dummy user', + username: 'dummyuser', + user_id: 1, + chat_channel_id: 2, + status: 'active', + role: 'mod', +}; + +describe('', () => { + it('should have no a11y violations', async () => { + const { container } = render( + , + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should render', () => { + const { queryByText, queryByPlaceholderText } = render( + , + ); + + expect(queryByText('Chat Channel Membership manager')).toBeDefined(); + expect(queryByPlaceholderText('Search Member...')).toBeDefined(); + }); +}); diff --git a/app/javascript/chat/__tests__/Membership.test.jsx b/app/javascript/chat/__tests__/Membership.test.jsx new file mode 100644 index 000000000..a1249ff12 --- /dev/null +++ b/app/javascript/chat/__tests__/Membership.test.jsx @@ -0,0 +1,46 @@ +import { h } from 'preact'; +import { render } from '@testing-library/preact'; +import { axe } from 'jest-axe'; +import Membership from '../ChatChannelSettings/MembershipManager/Membership'; + +const membershipData = { + name: 'dummy Name', + user_id: 1, + chat_channel_id: 2, + membership_id: 1, + username: 'dummyuser', +}; + +const currentModMembership = { + name: 'dummy user', + username: 'dummyuser', + user_id: 1, + chat_channel_id: 2, + status: 'active', + role: 'mod', +}; + +describe('', () => { + it('should have no a11y violations', async () => { + const { container } = render( + , + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should render', () => { + const { queryByText } = render( + , + ); + + expect(queryByText('dummy user')).toBeDefined(); + }); +}); diff --git a/app/javascript/chat/__tests__/chatChannelSettingActions.test.js b/app/javascript/chat/__tests__/chatChannelSettingActions.test.js index b1a4ff31e..3b15906da 100644 --- a/app/javascript/chat/__tests__/chatChannelSettingActions.test.js +++ b/app/javascript/chat/__tests__/chatChannelSettingActions.test.js @@ -7,6 +7,7 @@ import { updateChatChannelDescription, sendChatChannelInvitation, leaveChatChannelMembership, + updateMembershipRole, } from '../actions/chat_channel_setting_actions'; /* global globalThis */ @@ -200,4 +201,26 @@ describe('Chat cahnnel API requestes', () => { expect(result).toEqual(response); }); }); + + describe('Update the membership role', () => { + it('should have the success response', async () => { + const response = { success: true, message: 'user membership is updated' }; + fetch.mockResponse(JSON.stringify(response)); + + const result = await updateMembershipRole( + chanChannelMembershipId, + channelId, + 'mod', + ); + expect(result).toEqual(response); + }); + + it('should return the not found', async () => { + const response = { success: false, message: 'not found' }; + fetch.mockResponse(JSON.stringify(response)); + + const result = await updateMembershipRole('', '', 'mod'); + expect(result).toEqual(response); + }); + }); }); diff --git a/app/javascript/chat/__tests__/invitationLinkManager.test.jsx b/app/javascript/chat/__tests__/invitationLinkManager.test.jsx new file mode 100644 index 000000000..030eb9dd0 --- /dev/null +++ b/app/javascript/chat/__tests__/invitationLinkManager.test.jsx @@ -0,0 +1,74 @@ +import { h } from 'preact'; +import { render } from '@testing-library/preact'; +import { axe } from 'jest-axe'; +import InviationLinkManager from '../ChatChannelSettings/MembershipManager/InvitationLinkManager'; + +const currentModMembership = { + name: 'dummy user', + username: 'dummyuser', + user_id: 1, + chat_channel_id: 2, + status: 'active', + role: 'mod', +}; + +const currentMemberMembership = { + name: 'dummy member', + username: 'dummymember', + user_id: 1, + chat_channel_id: 2, + status: 'active', + role: 'member', +}; + +const svg = ( + + Copy Invitation Url + + +); + +describe('', () => { + it('should have no a11y violations', async () => { + const { container } = render( + , + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should render', () => { + const { queryByText } = render( + , + ); + + expect(queryByText('https://dummy-invitation.link')).toBeDefined(); + expect(queryByText('Invitation Link')).toBeDefined(); + }); + + it('should not render', () => { + const { rerender } = render( + , + ); + + expect(rerender()).toEqual(undefined); + }); +}); diff --git a/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx b/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx index 6e9f7156c..9b0ef9e49 100644 --- a/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx +++ b/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx @@ -21,9 +21,7 @@ describe('', () => { it('should have user leave channel when leave button is clicked', () => { const leaveHandler = jest.fn(); const { getByText } = render( - , + , ); const leaveButton = getByText('Leave Channel'); diff --git a/app/javascript/chat/__tests__/membership.test.jsx b/app/javascript/chat/__tests__/membership.test.jsx deleted file mode 100644 index 3c0376081..000000000 --- a/app/javascript/chat/__tests__/membership.test.jsx +++ /dev/null @@ -1,167 +0,0 @@ -import { h } from 'preact'; -import { render } from '@testing-library/preact'; -import { axe } from 'jest-axe'; -import Membership from '../ChatChannelSettings/Membership'; - -function getModUser() { - return { - membership: { - name: 'test user', - username: 'testusername', - user_id: '1', - membership_id: '2', - role: 'mod', - status: 'active', - image: '', - }, - membershipType: 'active', - currentMembershipRole: 'mod', - }; -} - -function getMemberUser() { - return { - membership: { - name: 'test user', - username: 'testusername', - user_id: '1', - membership_id: '2', - role: 'member', - status: 'active', - image: '', - }, - membershipType: 'requested', - currentMembershipRole: 'mod', - }; -} - -describe('', () => { - it('should have no a11y violations for a moderator user', async () => { - const { membership, membershipType, currentMembershipRole } = getModUser(); - const { container } = render( - , - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('should have no a11y violations for a member user', async () => { - const { - membership, - membershipType, - currentMembershipRole, - } = getMemberUser(); - const { container } = render( - , - ); - const results = await axe(container); - - expect(results).toHaveNoViolations(); - }); - - it('should render a moderator user', () => { - const { membership, membershipType, currentMembershipRole } = getModUser(); - const { getByAltText, getByTitle } = render( - , - ); - const userProfileLink = getByTitle('test user profile'); - - expect(userProfileLink.getAttribute('href')).toEqual('/testusername'); - - getByAltText('test user profile'); - }); - - it('should render a member user', () => { - const { - membership, - membershipType, - currentMembershipRole, - } = getMemberUser(); - const { getByAltText, getByTitle, getByText } = render( - , - ); - - // users requesting to join channel - const userProfileLink = getByTitle('test user profile'); - - expect(userProfileLink.getAttribute('href')).toEqual('/testusername'); - - getByAltText('test user profile'); - - const addButton = getByText('+', { - selector: 'button', - }); - - expect(addButton.dataset.membershipId).toEqual('2'); - - const deleteButton = getByText('x', { selector: 'button' }); - - expect(deleteButton.dataset.membershipId).toEqual('2'); - expect(deleteButton.dataset.membershipStatus).toEqual('active'); - }); - - it('should not show add/remove buttons for a moderator', () => { - const { membership, membershipType, currentMembershipRole } = getModUser(); - const { queryByText } = render( - , - ); - - // a moderator should not have add or remove buttons - expect( - queryByText('+', { - selector: 'button', - }), - ).toBeNull(); - expect( - queryByText('x', { - selector: 'button', - }), - ).toBeNull(); - }); - - it('should show add/remove buttons for a member', () => { - const { - membership, - membershipType, - currentMembershipRole, - } = getMemberUser(); - const { queryByText } = render( - , - ); - expect( - queryByText('+', { - selector: 'button', - }), - ).toBeDefined(); - expect( - queryByText('x', { - selector: 'button', - }), - ).toBeDefined(); - }); -}); diff --git a/app/javascript/chat/actions/chat_channel_setting_actions.js b/app/javascript/chat/actions/chat_channel_setting_actions.js index 033e56a39..fd41e3f36 100644 --- a/app/javascript/chat/actions/chat_channel_setting_actions.js +++ b/app/javascript/chat/actions/chat_channel_setting_actions.js @@ -142,7 +142,31 @@ export async function leaveChatChannelMembership(membershipId) { `/chat_channel_memberships/leave_membership/${membershipId}`, { method: 'PATCH', - + credentials: 'same-origin', + }, + ); + + return response.json(); +} + +/** + * This function is used to update the membership role + * @param {number} membershipId selected User Chat channel membership id + * @param {number} chatChannelId Current chat chaneel id + * @param {string} role updated role for the membership + */ +export async function updateMembershipRole(membershipId, chatChannelId, role) { + const response = await request( + `/chat_channel_memberships/update_membership_role/${chatChannelId}`, + { + method: 'PATCH', + body: { + chat_channel_membership: { + chat_channel_id: chatChannelId, + membership_id: membershipId, + role, + }, + }, credentials: 'same-origin', }, ); diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index c0b55e7fa..0e0c21dc0 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -1796,6 +1796,11 @@ export default class Chat extends Component { ? 'sidecar-user' : `chat_channel_setting`; + const contentLink = + activeChannel.channel_type === 'direct' + ? `/${activeChannel.channel_username}` + : '#/'; + return ( - + ); diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx index 45030d3f0..603b88399 100644 --- a/app/javascript/chat/content.jsx +++ b/app/javascript/chat/content.jsx @@ -40,13 +40,13 @@ export default class Content extends Component { return ( // TODO: A button (role="button") cannot contain other interactive elements, i.e. buttons. // TODO: These should have key click events as well. - // eslint-disable-next-line jsx-a11y/click-events-have-key-events diff --git a/config/fastly/snippets/safe_params_list.vcl b/config/fastly/snippets/safe_params_list.vcl index 30c3f0c65..3d8f8daf3 100644 --- a/config/fastly/snippets/safe_params_list.vcl +++ b/config/fastly/snippets/safe_params_list.vcl @@ -2,6 +2,6 @@ import querystring; sub vcl_recv { # return this URL with only the parameters that match this regular expression if (req.url !~ "/internal/" && req.url !~ "/search/" && req.url !~ "/bulk_show") { - set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|invitation_token|reset_password_token|ut|verb)$"); + set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|invitation_token|reset_password_token|ut|verb|invitation_slug)$"); } } diff --git a/config/routes.rb b/config/routes.rb index 09534c6a1..d4c280994 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -302,6 +302,9 @@ Rails.application.routes.draw do 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" + patch "/chat_channel_memberships/update_membership_role/:id" => "chat_channel_memberships#update_membership_role" + get "/join_channel_invitation/:channel_slug" => "chat_channel_memberships#join_channel_invitation" + post "/joining_invitation_response" => "chat_channel_memberships#joining_invitation_response" get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb index c0785984e..7895cdecf 100644 --- a/spec/requests/chat_channel_memberships_spec.rb +++ b/spec/requests/chat_channel_memberships_spec.rb @@ -16,12 +16,13 @@ RSpec.describe "ChatChannelMemberships", type: :request do user.add_role(:super_admin) membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id) - get "/chat_channel_memberships/chat_channel_info/#{membership.id}" + get "/chat_channel_memberships/chat_channel_info/#{membership.id}", as: :json end it "return all details of chat channel" do - expect(response).to have_http_status(:ok) - expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership]) + expect(response.status).to eq(200) + expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership + invitation_link]) end end @@ -31,14 +32,15 @@ RSpec.describe "ChatChannelMemberships", type: :request do 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}" + get "/chat_channel_memberships/chat_channel_info/#{membership.id}", as: :json end 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) + expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership + invitation_link]) + expect(response.parsed_body["result"]["memberships"]["pending"].length).to eq(0) + expect(response.parsed_body["result"]["memberships"]["requested"].length).to eq(0) end end end @@ -338,4 +340,120 @@ RSpec.describe "ChatChannelMemberships", type: :request do end end end + + describe "PATCH /update_membership_role" do + before do + user.add_role(:super_admin) + chat_channel.add_users([second_user]) + end + + context "when user role is member" do + it "update the membership role to mod" do + allow(Pusher).to receive(:trigger).and_return(true) + membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id) + + patch "/chat_channel_memberships/update_membership_role/#{chat_channel.id}", params: { + chat_channel_membership: { + membership_id: membership.id, + role: "mod" + } + } + expect(response.status).to eq(200) + expect(membership.reload.role).to eq("mod") + end + end + + context "when user is mod" do + it "update the membership role to member" do + allow(Pusher).to receive(:trigger).and_return(true) + membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id) + + patch "/chat_channel_memberships/update_membership_role/#{chat_channel.id}", params: { + chat_channel_membership: { + membership_id: membership.id, + role: "member" + } + } + + expect(response.status).to eq(200) + expect(membership.reload.role).to eq("member") + end + end + + context "when there is no channel id" do + it "channel not found" do + membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id) + + patch "/chat_channel_memberships/update_membership_role/", params: { + chat_channel_membership: { + membership_id: membership.id, + role: "member" + } + } + + expect(response.status).to eq(404) + expect(membership.reload.role).to eq("member") + end + end + end + + describe "GET /join_channel_invitation" do + context "when user is not member" do + it "render the page" do + allow(Pusher).to receive(:trigger).and_return(true) + sign_in second_user + chat_channel.update(discoverable: true) + + get "/join_channel_invitation/#{chat_channel.slug}" + + expect(response.status).to eq(200) + end + end + + context "when user is not logged-in" do + it "not allowed to create membership" do + sign_out second_user + + get "/join_channel_invitation/#{chat_channel.slug}" + expect(response.status).to eq(401) + end + end + end + + describe "POST /joining_invitation_response" do + context "when user accept the request" do + it "will create membership" do + allow(Pusher).to receive(:trigger).and_return(true) + + sign_in second_user + chat_channel.update(discoverable: true) + + post "/joining_invitation_response", params: { + user_action: "accept", + chat_channel_id: chat_channel.id + } + membership = ChatChannelMembership.last + + expect(response.status).to eq(302) + expect(membership.user_id).to eq(second_user.id) + end + end + + context "when user decline the request" do + it "will not create the membership" do + sign_in second_user + chat_channel.update(discoverable: true) + + post "/joining_invitation_response", params: { + user_action: "decline", + chat_channel_id: chat_channel.id + } + + membership = ChatChannelMembership.last + + expect(response.status).to eq(302) + expect(membership.user_id).not_to eq(second_user.id) + end + end + end end