From e78a0078a73a5a9727fa1c5916b04592a1359b0b Mon Sep 17 00:00:00 2001 From: narender2031 Date: Thu, 11 Jun 2020 02:28:37 +0530 Subject: [PATCH] =?UTF-8?q?[deploy]=20Refactor=20=F0=9F=9A=80=20:=20Replac?= =?UTF-8?q?ed=20Chat=20Channel=20Setting=20page=20with=20Preact=20componen?= =?UTF-8?q?t.=20=20(#8271)?= 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 * ๐Ÿž 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 --- app/assets/stylesheets/chat.scss | 4 + .../chat_channel_memberships_controller.rb | 114 +++--- app/controllers/chat_channels_controller.rb | 29 +- .../ActiveMembershipsSection.jsx | 44 ++ .../ChannelDescriptionSection.jsx | 28 ++ .../ChatChannelMembershipSection.jsx | 69 ++++ .../ChatChannelSettings.jsx | 378 ++++++++++++++++++ .../chat/ChatChannelSettings/InviateForm.jsx | 47 +++ .../LeaveMembershipSection.jsx | 33 ++ .../chat/ChatChannelSettings/Membership.jsx | 67 ++++ .../ChatChannelSettings/ModFaqSection.jsx | 30 ++ .../chat/ChatChannelSettings/ModSection.jsx | 52 +++ .../PendingMembershipSection.jsx | 48 +++ .../ChatChannelSettings/PersonalSetting.jsx | 44 ++ .../RequestedMembershipSection.jsx | 51 +++ .../chat/ChatChannelSettings/SettingsForm.jsx | 62 +++ .../activeMembershipsSection.test.jsx.snap | 13 + .../chatChannelDescription.test.jsx.snap | 21 + .../chatChannelMembersection.test.jsx.snap | 17 + .../chatChannelSettings.test.jsx.snap | 3 + .../__snapshots__/inviteForm.test.jsx.snap | 34 ++ .../leaveMembershipSection.test.jsx.snap | 19 + .../__snapshots__/membership.test.jsx.snap | 27 ++ .../__snapshots__/modFaqSection.test.jsx.snap | 21 + .../__snapshots__/modSection.test.jsx.snap | 82 ++++ .../pendingMembershipsSection.test.jsx.snap | 13 + .../personalSetting.test.jsx.snap | 38 ++ .../requestedMembershipSection.test.jsx.snap | 13 + .../__snapshots__/settingsForm.test.jsx.snap | 49 +++ .../activeMembershipsSection.test.jsx | 63 +++ .../__tests__/chatChannelDescription.test.jsx | 39 ++ .../chatChannelMembersection.test.jsx | 34 ++ .../chatChannelSettingActions.test.js | 203 ++++++++++ .../__tests__/chatChannelSettings.test.jsx | 28 ++ .../chat/__tests__/inviteForm.test.jsx | 27 ++ .../__tests__/leaveMembershipSection.test.jsx | 40 ++ .../chat/__tests__/membership.test.jsx | 71 ++++ .../chat/__tests__/modFaqSection.test.jsx | 49 +++ .../chat/__tests__/modSection.test.jsx | 36 ++ .../pendingMembershipsSection.test.jsx | 57 +++ .../chat/__tests__/personalSetting.test.jsx | 30 ++ .../requestedMembershipSection.test.jsx | 57 +++ .../chat/__tests__/settingsForm.test.jsx | 30 ++ .../actions/chat_channel_setting_actions.js | 151 +++++++ app/javascript/chat/actions/requestActions.js | 35 +- app/javascript/chat/chat.jsx | 27 +- app/javascript/chat/content.jsx | 86 ++-- .../chat_channel_membership_policy.rb | 16 + app/policies/chat_channel_policy.rb | 4 + .../chat_channel_detail_presenter.rb | 42 ++ app/presenters/membership_user_presenter.rb | 19 + app/services/application_service.rb | 5 + app/services/chat_channel_update_service.rb | 5 +- config/application.rb | 1 + config/routes.rb | 9 + .../requests/chat_channel_memberships_spec.rb | 377 ++++++++--------- spec/requests/chat_channels_spec.rb | 39 ++ 57 files changed, 2677 insertions(+), 353 deletions(-) create mode 100644 app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/ChannelDescriptionSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/InviateForm.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/Membership.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/ModSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/PendingMembershipSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/PersonalSetting.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx create mode 100644 app/javascript/chat/ChatChannelSettings/SettingsForm.jsx create mode 100644 app/javascript/chat/__tests__/__snapshots__/activeMembershipsSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/chatChannelDescription.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/chatChannelMembersection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/chatChannelSettings.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/inviteForm.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/leaveMembershipSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/membership.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/modFaqSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/modSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/pendingMembershipsSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/personalSetting.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/requestedMembershipSection.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/settingsForm.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/activeMembershipsSection.test.jsx create mode 100644 app/javascript/chat/__tests__/chatChannelDescription.test.jsx create mode 100644 app/javascript/chat/__tests__/chatChannelMembersection.test.jsx create mode 100644 app/javascript/chat/__tests__/chatChannelSettingActions.test.js create mode 100644 app/javascript/chat/__tests__/chatChannelSettings.test.jsx create mode 100644 app/javascript/chat/__tests__/inviteForm.test.jsx create mode 100644 app/javascript/chat/__tests__/leaveMembershipSection.test.jsx create mode 100644 app/javascript/chat/__tests__/membership.test.jsx create mode 100644 app/javascript/chat/__tests__/modFaqSection.test.jsx create mode 100644 app/javascript/chat/__tests__/modSection.test.jsx create mode 100644 app/javascript/chat/__tests__/pendingMembershipsSection.test.jsx create mode 100644 app/javascript/chat/__tests__/personalSetting.test.jsx create mode 100644 app/javascript/chat/__tests__/requestedMembershipSection.test.jsx create mode 100644 app/javascript/chat/__tests__/settingsForm.test.jsx create mode 100644 app/javascript/chat/actions/chat_channel_setting_actions.js create mode 100644 app/presenters/chat_channel_detail_presenter.rb create mode 100644 app/presenters/membership_user_presenter.rb create mode 100644 app/services/application_service.rb diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index b103b6a84..38c5e03c3 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -1274,3 +1274,7 @@ .action button:nth-child(2n) { margin-left: 10px; } + +.channel_details { + margin-top: 30px; +} diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 7d5359ce2..f56627d58 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -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 diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 66e545f77..5fcddf69a 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -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) diff --git a/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx new file mode 100644 index 000000000..cb16fb577 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx @@ -0,0 +1,44 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; +import Membership from './Membership'; + +const ActiveMembershipSection = ({ + activeMemberships, + removeMembership, + currentMembershipRole, +}) => { + return ( +
+

Members

+ {activeMemberships && activeMemberships.length > 0 + ? activeMemberships.map((pendingMembership) => ( + + )) + : null} +
+ ); +}; + +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; diff --git a/app/javascript/chat/ChatChannelSettings/ChannelDescriptionSection.jsx b/app/javascript/chat/ChatChannelSettings/ChannelDescriptionSection.jsx new file mode 100644 index 000000000..92684c303 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ChannelDescriptionSection.jsx @@ -0,0 +1,28 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const ChannelDescriptionSection = ({ + channelName, + channelDescription, + currentMembershipRole +}) => { + return ( +
+

{channelName}

+

{channelDescription}

+

+ You are a channel + {' '} + {currentMembershipRole} +

+
+ ) +} + +ChannelDescriptionSection.propTypes = { + channelName: PropTypes.string.isRequired, + currentMembershipRole: PropTypes.string.isRequired, + channelDescription: PropTypes.string.isRequired +} + +export default ChannelDescriptionSection; diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx new file mode 100644 index 000000000..45524e6d4 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx @@ -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 ( +
+ + + +
+ ) +} + +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; diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx new file mode 100644 index 000000000..75106cc2f --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx @@ -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(, 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 ( +
+
+ + + + + + +
+
+ ); + } +} diff --git a/app/javascript/chat/ChatChannelSettings/InviateForm.jsx b/app/javascript/chat/ChatChannelSettings/InviateForm.jsx new file mode 100644 index 000000000..a7d746a37 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/InviateForm.jsx @@ -0,0 +1,47 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const InviteForm = ({ + handleChatChannelInvitations, + invitationUsernames, + handleInvitationUsernames +}) => { + return ( +
+
+ + +
+
+ +
+
+ ) +} + +InviteForm.propTypes = { + handleInvitationUsernames: PropTypes.func.isRequired, + handleChatChannelInvitations: PropTypes.func.isRequired, + invitationUsernames: PropTypes.func.isRequired +} + +export default InviteForm; diff --git a/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx new file mode 100644 index 000000000..f72e22fae --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx @@ -0,0 +1,33 @@ +import { h } from 'preact'; +import PropsType from 'prop-types'; + +const LeaveMembershipSection = ({ + currentMembershipRole, + handleleaveChatChannelMembership, +}) => { + if (currentMembershipRole !== 'member') { + return null; + } + + return ( +
+

Danger Zone

+
+ +
+
+ ); +}; + +LeaveMembershipSection.propTypes = { + currentMembershipRole: PropsType.string.isRequired, + handleleaveChatChannelMembership: PropsType.func.isRequired, +}; + +export default LeaveMembershipSection; diff --git a/app/javascript/chat/ChatChannelSettings/Membership.jsx b/app/javascript/chat/ChatChannelSettings/Membership.jsx new file mode 100644 index 000000000..6d321e7bb --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/Membership.jsx @@ -0,0 +1,67 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const Membership = ({ + membership, + removeMembership, + membershipType, + chatChannelAcceptMembership, + currentMembershipRole, +}) => { + return ( +
+ + + {`${membership.name} + + {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, + removeMembership: PropTypes.func.isRequired, + membershipType: PropTypes.func.isRequired, + chatChannelAcceptMembership: PropTypes.func.isRequired, + currentMembershipRole: PropTypes.string.isRequired, +}; + +export default Membership; diff --git a/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx new file mode 100644 index 000000000..2c2eface8 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx @@ -0,0 +1,30 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const ModFaqSection = ({ currentMembershipRole }) => { + if (currentMembershipRole !== 'mod') { + return null; + } + + return ( +
+

+ Questions about Connect Channel moderation? Contact + + yo@dev.to + +

+
+ ); +}; + +ModFaqSection.propTypes = { + currentMembershipRole: PropTypes.string.isRequired, +}; + +export default ModFaqSection; diff --git a/app/javascript/chat/ChatChannelSettings/ModSection.jsx b/app/javascript/chat/ChatChannelSettings/ModSection.jsx new file mode 100644 index 000000000..a45c06a60 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/ModSection.jsx @@ -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 ( +
+ + +
+ ); +}; + +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; diff --git a/app/javascript/chat/ChatChannelSettings/PendingMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/PendingMembershipSection.jsx new file mode 100644 index 000000000..d3aa8b1af --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/PendingMembershipSection.jsx @@ -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 ( +
+

Pending Invitations

+ {pendingMemberships && pendingMemberships.length > 0 + ? pendingMemberships.map((pendingMembership) => ( + + )) + : null} +
+ ); +}; + +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; diff --git a/app/javascript/chat/ChatChannelSettings/PersonalSetting.jsx b/app/javascript/chat/ChatChannelSettings/PersonalSetting.jsx new file mode 100644 index 000000000..946d51c0c --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/PersonalSetting.jsx @@ -0,0 +1,44 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const PersonalSettings = ({ + handlePersonChatChennelSetting, + showGlobalBadgeNotification, + updateCurrentMembershipNotificationSettings, +}) => { + return ( +
+

Personal Settings

+

Notifications

+
+ + +
+
+ +
+
+ ); +}; + +PersonalSettings.propTypes = { + updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired, + showGlobalBadgeNotification: PropTypes.bool.isRequired, + handlePersonChatChennelSetting: PropTypes.func.isRequired, +}; + +export default PersonalSettings; diff --git a/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx new file mode 100644 index 000000000..f060b0a63 --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx @@ -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 ( +
+

Joining Request

+ {requestedMemberships && requestedMemberships.length > 0 + ? requestedMemberships.map((pendingMembership) => ( + + )) + : null} +
+ ); +}; + +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; diff --git a/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx new file mode 100644 index 000000000..37400ae9d --- /dev/null +++ b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx @@ -0,0 +1,62 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const SettingsFrom = ({ + channelDescription, + handleDescriptionChange, + channelDiscoverable, + handleChannelDiscoverableStatus, + handleChannelDescriptionChanges, +}) => { + return ( +
+

Channel Settings

+
+ +