From 90d60bf73fee5ab9866a195261abf7d911e17e0d Mon Sep 17 00:00:00 2001 From: Sarthak Sharma <7lovesharma7@gmail.com> Date: Mon, 11 May 2020 18:59:15 +0530 Subject: [PATCH] =?UTF-8?q?[deploy]=20=F0=9F=9A=80=20Feature:=20Ability=20?= =?UTF-8?q?to=20search=20discoverable=20channels=20and=20send=20request=20?= =?UTF-8?q?to=20join=20(#7385)?= 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 * added new column discoverable for public channel and changes in elasticsearch * fix corrections * changes in serializer file, added channel_discoverable * added checkbox with discoverable policy * changes in code for discoverable channels * accept and reject member invitation by mod * add joining request to closed groups * fixed merge error * changes in joining member to closed groups * join to closed group * changes in message action of joining * changes in chat upopened json * ๐Ÿš€Feature : Ability to search Global Channels * touch updated_at in after commit update * ๐Ÿš€Feature : Ability to send request to discoverable channel * ๐Ÿš€Feature : Ability to send request to discoverable channel * created new route for joining closed channel * ๐Ÿš€ Success handle on joining request sent * removed redundant code * join_channel improvement, removed authorization for join channel * list of joining requests * added joining_request status channels on search list * changes in mailer and query builder optimized * ๐Ÿ›  Adding filter for new Query * added rspec for add membership method in controller * rspec for sending join channel request * invite join request channel list rspec * test case for query builder discoverable * viewable and discoverable channel list * refactored logic for search channels * ๐Ÿš€ Check if Request already sent * ๐Ÿ›  Optimizing code and making channelButton Component * ๐Ÿ›  Optimizing codefor SVG problem * ๐Ÿ›  Optimized action.js * changes in search controller query * hot fix * removed unwanted code * ๐Ÿ›  Fix the Channel Name problem * ๐Ÿ›  Optimizing code further for CodeClimate * added new column discoverable for public channel and changes in elasticsearch * fix corrections * changes in serializer file, added channel_discoverable * added checkbox with discoverable policy * changes in code for discoverable channels * accept and reject member invitation by mod * add joining request to closed groups * fixed merge error * changes in joining member to closed groups * join to closed group * changes in message action of joining * changes in chat upopened json * touch updated_at in after commit update * ๐Ÿš€Feature : Ability to search Global Channels * ๐Ÿš€Feature : Ability to send request to discoverable channel * ๐Ÿš€Feature : Ability to send request to discoverable channel * created new route for joining closed channel * ๐Ÿš€ Success handle on joining request sent * removed redundant code * join_channel improvement, removed authorization for join channel * list of joining requests * added joining_request status channels on search list * changes in mailer and query builder optimized * added rspec for add membership method in controller * rspec for sending join channel request * invite join request channel list rspec * ๐Ÿ›  Adding filter for new Query * test case for query builder discoverable * viewable and discoverable channel list * refactored logic for search channels * ๐Ÿš€ Check if Request already sent * ๐Ÿ›  Optimizing code and making channelButton Component * ๐Ÿ›  Optimizing codefor SVG problem * ๐Ÿ›  Optimized action.js * changes in search controller query * hot fix * ๐Ÿ›  Fix the Channel Name problem * ๐Ÿ›  Fixing merge problem * ๐Ÿ›  Optimizing code further for CodeClimate * updated UI for membership edit * fixed test casses and fixed UI for chat_channel_edit * ๐Ÿ› napshots added * test cases fixed * ๐Ÿ›  Test cases added for new component * channel settings accesible only by mod * ๐Ÿ›  More Test cases added * ๐Ÿ›  Svg code optimized * ๐Ÿ›  Svg code optimized in videocontent * optimized code for search query * fix test case for query builder * changes in joining closed channel logic * refactored join channel * redirect to edit channel after joining request * changes in test case for redirect * optimized code * ๐Ÿ›  Eslint bugs fixed * test case fixed * optimization * olving channel repetition problem * optimization of code for query builder * changes in query builder * test cases fixed * ๐Ÿ›  Handling reduntant data on frontend * ๐Ÿ›  Don't show data if no filter query * ๐Ÿ›  Optimized code for fixing bugs and code coverage * ๐Ÿ›  Optimizing code further for CodeClimate Co-authored-by: Parasgr-code --- app/assets/stylesheets/chat.scss | 51 +++++ .../chat_channel_memberships_controller.rb | 42 +++- app/controllers/chat_channels_controller.rb | 20 +- app/controllers/search_controller.rb | 4 +- .../__snapshots__/channelButton.test.jsx.snap | 41 ++++ .../channelRequest.test.jsx.snap | 47 ++++ .../__snapshots__/channels.test.jsx.snap | 56 ++++- .../__snapshots__/chat.test.jsx.snap | 78 +------ .../__snapshots__/content.test.jsx.snap | 12 +- .../chat/__tests__/channelButton.test.jsx | 33 +++ .../chat/__tests__/channelRequest.test.jsx | 28 +++ .../chat/__tests__/channels.test.jsx | 67 +----- app/javascript/chat/actions.js | 58 +++-- app/javascript/chat/channelRequest.jsx | 53 +++++ app/javascript/chat/channels.jsx | 81 ++++--- app/javascript/chat/chat.jsx | 210 +++++++++++++----- .../chat/components/channelButton.jsx | 98 ++++++++ app/javascript/chat/content.jsx | 62 ++++-- app/javascript/chat/util.js | 30 ++- app/javascript/chat/videoContent.jsx | 34 ++- app/javascript/packs/Chat.jsx | 1 - app/models/chat_channel.rb | 1 + app/models/chat_channel_membership.rb | 6 +- app/policies/chat_channel_policy.rb | 2 +- .../chat_channel_membership_serializer.rb | 2 +- .../query_builders/chat_channel_membership.rb | 10 +- .../chat_channel_memberships/edit.html.erb | 69 ++++-- .../channel_invite_email.html.erb | 4 + .../mappings/chat_channel_memberships.json | 3 + config/routes.rb | 2 + ...dd_column_discoverable_to_chat_channels.rb | 5 + db/schema.rb | 1 + .../requests/chat_channel_memberships_spec.rb | 75 +++++++ spec/requests/chat_channels_spec.rb | 11 + .../search/chat_channel_membership_spec.rb | 24 +- .../chat_channel_membership_spec.rb | 24 +- 36 files changed, 997 insertions(+), 348 deletions(-) create mode 100644 app/javascript/chat/__tests__/__snapshots__/channelButton.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/__snapshots__/channelRequest.test.jsx.snap create mode 100644 app/javascript/chat/__tests__/channelButton.test.jsx create mode 100644 app/javascript/chat/__tests__/channelRequest.test.jsx create mode 100644 app/javascript/chat/channelRequest.jsx create mode 100644 app/javascript/chat/components/channelButton.jsx create mode 100644 db/migrate/20200411085952_add_column_discoverable_to_chat_channels.rb diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index 20f93e461..719df0d4b 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -1214,3 +1214,54 @@ } } } +.joining-message { + display: grid; + margin-top: 30px; + justify-content: center; + h3, + h2 { + width: 90%; + margin-top: 10px; + text-align: center; + justify-self: center; + } +} +.user-picture, +.send-request { + display: flex; + justify-content: center; + margin-top: 10vh; +} +.user-picture { + .chatmessage__profilepic { + .chatmessagebody__profileimage { + width: 50px; + height: 50px; + border: 1px solid var(--base-30); + } + } +} +.send-request { + .cta { + margin: 4px 6px 4px 0; + border-radius: 3px; + font-weight: 600px; + font-family: 'HelveticaNeue-CondensedBold', 'HelveticaNeueBoldCondensed', + 'HelveticaNeue-Bold-Condensed', 'Helvetica Neue Bold Condensed', + 'HelveticaNeueBold', 'HelveticaNeue-Bold', 'Helvetica Neue Bold', + 'HelveticaNeue', 'Helvetica Neue', 'TeXGyreHerosCnBold', 'Helvetica', + 'Tahoma', 'Geneva', 'Arial Narrow', 'Arial', sans-serif; + font-size: 18px; + border: 0px; + height: 50px; + padding: 0 30px; + } +} +.loading-user { + height: '210px'; + width: '210px'; + margin: ' 15px auto'; + display: 'block'; + border-radius: '500px'; + background-color: '#f5f6f7'; +} diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 6f80921c8..66c6647e6 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -1,5 +1,5 @@ class ChatChannelMembershipsController < ApplicationController - after_action :verify_authorized + after_action :verify_authorized, except: :join_channel include MessagesHelper def index @@ -39,6 +39,23 @@ class ChatChannelMembershipsController < ApplicationController redirect_to edit_chat_channel_membership_path(membership) end + def join_channel + membership_params = params[:chat_channel_membership] + chat_channel = ChatChannel.find(membership_params[:chat_channel_id]) + existing_membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: chat_channel.id) + if existing_membership.present? && %w[active joining_request].exclude?(existing_membership.status) + status = existing_membership.update(status: "joining_request", role: "member") + else + membership = ChatChannelMembership.new(user_id: current_user.id, chat_channel_id: chat_channel.id, role: "member", status: "joining_request") + status = membership.save + end + if status + render json: { status: "success", message: "Request Sent" } + else + render json: { status: 400, message: "Unable to join channel" }, status: :bad_request + end + end + def remove_membership @chat_channel = ChatChannel.find(params[:chat_channel_id]) authorize @chat_channel, :update? @@ -55,11 +72,18 @@ class ChatChannelMembershipsController < ApplicationController redirect_to edit_chat_channel_membership_path(membership) end + def add_membership + @chat_channel = ChatChannel.find(params[:chat_channel_id]) + authorize @chat_channel, :update? + @chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id]) + respond_to_invitation(@chat_channel_membership.status) if permitted_params[:user_action].present? && @chat_channel_membership.status == "joining_request" + end + def update @chat_channel_membership = ChatChannelMembership.find(params[:id]) authorize @chat_channel_membership if permitted_params[:user_action].present? - respond_to_invitation + respond_to_invitation(@chat_channel_membership.status) else @chat_channel_membership.update(permitted_params) flash[:settings_notice] = "Personal settings updated." @@ -84,12 +108,20 @@ class ChatChannelMembershipsController < ApplicationController params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification) end - def respond_to_invitation + def respond_to_invitation(previous_status) if permitted_params[:user_action] == "accept" @chat_channel_membership.update(status: "active") channel_name = @chat_channel_membership.chat_channel.channel_name - send_chat_action_message("@#{current_user.username} joined #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "joined") - flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list." + if previous_status == "pending" + send_chat_action_message("@#{current_user.username} joined #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "joined") + 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.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) + redirect_to(edit_chat_channel_membership_path(membership)) && return + end else @chat_channel_membership.update(status: "rejected") flash[:settings_notice] = "Invitation rejected." diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index 638284a40..1780212bf 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -13,6 +13,9 @@ class ChatChannelsController < ApplicationController elsif params[:state] == "pending" authorize ChatChannel render_pending_json_response + elsif params[:state] == "joining_request" + authorize ChatChannel + render_joining_request_json_response else skip_authorization render_channels_html @@ -31,6 +34,11 @@ class ChatChannelsController < ApplicationController 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") + 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." @@ -140,9 +148,9 @@ class ChatChannelsController < ApplicationController def render_unopened_json_response @chat_channels_memberships = if session_current_user_id ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel). - where("has_unopened_messages = ? OR status = ?", - true, "pending"). + where(has_unopened_messages: true). where(show_global_badge_notification: true). + where.not(status: %w[removed_from_channel left_channel]). order("chat_channel_memberships.updated_at DESC") else [] @@ -164,10 +172,16 @@ class ChatChannelsController < ApplicationController def render_unopened_ids_response @unopened_ids = ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel). - where(has_unopened_messages: true).pluck(:chat_channel_id) + where(has_unopened_messages: true).where.not(status: %w[removed_from_channel left_channel]).pluck(:chat_channel_id) render json: { unopened_ids: @unopened_ids } end + def render_joining_request_json_response + requested_memberships = current_user.chat_channel_memberships.includes(:chat_channel). + where(chat_channels: { discoverable: true }, role: "mod").pluck(:chat_channel_id).map { |membership_id| ChatChannel.find_by(id: membership_id).requested_memberships } + render json: { joining_requests: requested_memberships.flatten } + end + def render_channels_html return unless current_user && params[:slug] diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index c96a6fd82..eb30c0b00 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -57,8 +57,9 @@ class SearchController < ApplicationController end def chat_channels + search_user_id = chat_channel_params[:user_id].present? ? [current_user.id, SiteConfig.mascot_user_id] : [current_user.id] ccm_docs = Search::ChatChannelMembership.search_documents( - params: chat_channel_params.merge(user_id: current_user.id).to_h, + params: chat_channel_params.merge(user_id: search_user_id).to_h, ) render json: { result: ccm_docs } @@ -118,6 +119,7 @@ class SearchController < ApplicationController channel_type channel_status status + user_id ] params.permit(accessible) diff --git a/app/javascript/chat/__tests__/__snapshots__/channelButton.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/channelButton.test.jsx.snap new file mode 100644 index 000000000..15795f518 --- /dev/null +++ b/app/javascript/chat/__tests__/__snapshots__/channelButton.test.jsx.snap @@ -0,0 +1,41 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and test snapshot 1`] = ` + +`; diff --git a/app/javascript/chat/__tests__/__snapshots__/channelRequest.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/channelRequest.test.jsx.snap new file mode 100644 index 000000000..da10f9660 --- /dev/null +++ b/app/javascript/chat/__tests__/__snapshots__/channelRequest.test.jsx.snap @@ -0,0 +1,47 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[` should render and test snapshot 1`] = ` +
+
+

+ Hey Sarthak ! +

+

+ You are not a member of this group yet. Send request to join. +

+
+
+
+ undefined profile + IronMan profile +
+
+
+ +
+
+`; diff --git a/app/javascript/chat/__tests__/__snapshots__/channels.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/channels.test.jsx.snap index 3b4f3d8b4..053dd1a86 100644 --- a/app/javascript/chat/__tests__/__snapshots__/channels.test.jsx.snap +++ b/app/javascript/chat/__tests__/__snapshots__/channels.test.jsx.snap @@ -9,14 +9,20 @@ exports[` expanded with chat channels should render and test snapsho id="chatchannels__channelslist" > - - - + />
diff --git a/app/javascript/chat/__tests__/__snapshots__/content.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/content.test.jsx.snap index 54868c7f0..84bea0c2a 100644 --- a/app/javascript/chat/__tests__/__snapshots__/content.test.jsx.snap +++ b/app/javascript/chat/__tests__/__snapshots__/content.test.jsx.snap @@ -37,6 +37,7 @@ exports[` as loading-user should render and test snapshot 1`] = ` } } > + as loading-user should render and test snapshot 1`] = `
`; diff --git a/app/javascript/chat/__tests__/channelButton.test.jsx b/app/javascript/chat/__tests__/channelButton.test.jsx new file mode 100644 index 000000000..3b1ab8012 --- /dev/null +++ b/app/javascript/chat/__tests__/channelButton.test.jsx @@ -0,0 +1,33 @@ +import { h } from 'preact'; +import render from 'preact-render-to-json'; +import { shallow } from 'preact-render-spy'; +import ChannelButton from '../components/channelButton'; + +const chnl = { + channel_name: 'test', + channel_color: '#00FFFF', + channel_type: 'invite_only', + channel_modified_slug: '@test34', + id: 34, + chat_channel_id: 23, + status: 'active', +}; + +const getChannel = (channel) => ; + +describe('', () => { + it('should render and test snapshot', () => { + const tree = render(getChannel(chnl)); + expect(tree).toMatchSnapshot(); + }); + + it('should have the proper elements, attributes and values', () => { + const context = shallow(getChannel(chnl)); + expect( + context.find('.chatchanneltabbutton').attr('data-channel-slug'), + ).toEqual(chnl.channel_modified_slug); // check user + expect(context.find('.chatchanneltabbutton').text()).toEqual( + chnl.channel_name, + ); + }); +}); diff --git a/app/javascript/chat/__tests__/channelRequest.test.jsx b/app/javascript/chat/__tests__/channelRequest.test.jsx new file mode 100644 index 000000000..26ec4de45 --- /dev/null +++ b/app/javascript/chat/__tests__/channelRequest.test.jsx @@ -0,0 +1,28 @@ +import { h } from 'preact'; +import render from 'preact-render-to-json'; +import { shallow } from 'preact-render-spy'; +import ChannelRequest from '../channelRequest'; + +const data = { + user: { + name: 'Sarthak', + }, + channel: { + name: 'IronMan', + }, +}; + +const getChannelRequest = (resource) => ; + +describe('', () => { + it('should render and test snapshot', () => { + const tree = render(getChannelRequest(data)); + expect(tree).toMatchSnapshot(); + }); + + it('should have the proper elements, attributes and values', () => { + const context = shallow(getChannelRequest(data)); + + expect(context.find('.joining-message').exists()).toEqual(true); + }); +}); diff --git a/app/javascript/chat/__tests__/channels.test.jsx b/app/javascript/chat/__tests__/channels.test.jsx index 79c38bb7e..2a9455c80 100644 --- a/app/javascript/chat/__tests__/channels.test.jsx +++ b/app/javascript/chat/__tests__/channels.test.jsx @@ -25,10 +25,12 @@ const fakeChannels = [ slug: '0', channel_modified_slug: '@0', id: 12345, + status: 'active', messages_count: 124, }, { channel_name: 'group channel 2', + status: 'active', last_opened_at: 'September 12, 2018', channel_users: [ { @@ -66,6 +68,7 @@ const fakeChannels = [ ], last_message_at: 'September 29, 2018', channel_type: 'group', + status: 'active', slug: '2', channel_modified_slug: '@2', id: 67890, @@ -103,28 +106,16 @@ describe('', () => { true, ); expect( - context - .find('.chatchannels__configmenu') - .childAt(0) - .text(), + context.find('.chatchannels__configmenu').childAt(0).text(), ).toEqual('DEV Settings'); expect( - context - .find('.chatchannels__configmenu') - .childAt(0) - .attr('href'), + context.find('.chatchannels__configmenu').childAt(0).attr('href'), ).toEqual('/settings'); expect( - context - .find('.chatchannels__configmenu') - .childAt(1) - .text(), + context.find('.chatchannels__configmenu').childAt(1).text(), ).toEqual('Report Abuse'); expect( - context - .find('.chatchannels__configmenu') - .childAt(1) - .attr('href'), + context.find('.chatchannels__configmenu').childAt(1).attr('href'), ).toEqual('/report-abuse'); // welcome message should not exist because there are channels @@ -158,28 +149,16 @@ describe('', () => { true, ); expect( - context - .find('.chatchannels__configmenu') - .childAt(0) - .text(), + context.find('.chatchannels__configmenu').childAt(0).text(), ).toEqual('DEV Settings'); expect( - context - .find('.chatchannels__configmenu') - .childAt(0) - .attr('href'), + context.find('.chatchannels__configmenu').childAt(0).attr('href'), ).toEqual('/settings'); expect( - context - .find('.chatchannels__configmenu') - .childAt(1) - .text(), + context.find('.chatchannels__configmenu').childAt(1).text(), ).toEqual('Report Abuse'); expect( - context - .find('.chatchannels__configmenu') - .childAt(1) - .attr('href'), + context.find('.chatchannels__configmenu').childAt(1).attr('href'), ).toEqual('/report-abuse'); }); }); @@ -199,30 +178,6 @@ describe('', () => { expect(context.find('.chatchannels__channelslist').exists()).toEqual( true, ); - expect( - context - .find('.chatchannels__channelslist') - .childAt(2) - .attr('data-channel-slug'), - ).toEqual('@1'); // check user - expect( - context - .find('.chatchannels__channelslist') - .childAt(2) - .text(), - ).toEqual('group channel 2'); // ensure user has no text - expect( - context - .find('.chatchannels__channelslist') - .childAt(1) - .text(), - ).toEqual(fakeChannels[0].channel_name); - expect( - context - .find('.chatchannels__channelslist') - .childAt(3) - .text(), - ).toEqual(fakeChannels[2].channel_name); }); }); diff --git a/app/javascript/chat/actions.js b/app/javascript/chat/actions.js index d60b2493e..903706109 100644 --- a/app/javascript/chat/actions.js +++ b/app/javascript/chat/actions.js @@ -6,7 +6,7 @@ export function getAllMessages(channelId, messageOffset, successCb, failureCb) { 'Content-Type': 'application/json', credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -29,7 +29,7 @@ export function sendMessage(messageObject, successCb, failureCb) { }), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -51,7 +51,7 @@ export function editMessage(editedMessage, successCb, failureCb) { }), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -67,7 +67,7 @@ export function sendOpen(activeChannelId, successCb, failureCb) { body: JSON.stringify({}), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -92,7 +92,7 @@ export function conductModeration( }), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -100,7 +100,7 @@ export function conductModeration( export function getChannels( query, retrievalID, - props, + searchType, paginationNumber, additionalFilters, successCb, @@ -114,14 +114,16 @@ export function getChannels( dataHash.per_page = 30; dataHash.page = paginationNumber; dataHash.channel_text = query; - + if (searchType === 'discoverable') { + dataHash.user_id = 'all'; + } const responsePromise = fetchSearch('chat_channels', dataHash); - return responsePromise.then(response => { + return responsePromise.then((response) => { const channels = response.result; if ( retrievalID === null || - channels.filter(e => e.chat_channel_id === retrievalID).length === 1 + channels.filter((e) => e.chat_channel_id === retrievalID).length === 1 ) { successCb(channels, query); } else { @@ -133,8 +135,8 @@ export function getChannels( credentials: 'same-origin', }, ) - .then(individualResponse => individualResponse.json()) - .then(json => { + .then((individualResponse) => individualResponse.json()) + .then((json) => { channels.unshift(json); successCb(channels, query); }); @@ -146,8 +148,8 @@ export function getUnopenedChannelIds(successCb) { fetch('/chat_channels?state=unopened_ids', { credentials: 'same-origin', }) - .then(response => response.json()) - .then(json => { + .then((response) => response.json()) + .then((json) => { successCb(json.unopened_ids); }); } @@ -158,7 +160,7 @@ export function getContent(url, successCb, failureCb) { 'Content-Type': 'application/json', credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -169,7 +171,7 @@ export function getJSONContents(url, successCb, failureCb) { 'Content-Type': 'application/json', credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -180,7 +182,7 @@ export function getChannelInvites(successCb, failureCb) { 'Content-Type': 'application/json', credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -200,7 +202,7 @@ export function sendChannelInviteAction(id, action, successCb, failureCb) { }), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } @@ -220,7 +222,27 @@ export function deleteMessage(messageId, successCb, failureCb) { }), credentials: 'same-origin', }) - .then(response => response.json()) + .then((response) => response.json()) + .then(successCb) + .catch(failureCb); +} + +export function sendChannelRequest(id, successCb, failureCb) { + fetch(`/join_chat_channel`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'X-CSRF-Token': window.csrfToken, + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + chat_channel_membership: { + chat_channel_id: id, + }, + }), + credentials: 'same-origin', + }) + .then((response) => response.json()) .then(successCb) .catch(failureCb); } diff --git a/app/javascript/chat/channelRequest.jsx b/app/javascript/chat/channelRequest.jsx new file mode 100644 index 000000000..c92d24e2f --- /dev/null +++ b/app/javascript/chat/channelRequest.jsx @@ -0,0 +1,53 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const ChannelRequest = ({ resource: data, handleJoiningRequest }) => ( +
+
+

Hey {data.user.name} !

+

You are not a member of this group yet. Send request to join.

+
+
+
+ {`${data.user.username} + {`${data.channel.name} +
+
+
+ {data.channel.status !== 'joining_request' ? ( + + ) : ( + + )} +
+
+); + +ChannelRequest.propTypes = { + resource: PropTypes.shape({ + data: PropTypes.object, + }).isRequired, + handleJoiningRequest: PropTypes.func.isRequired, +}; +export default ChannelRequest; diff --git a/app/javascript/chat/channels.jsx b/app/javascript/chat/channels.jsx index 5e10c9b4a..19f4bc878 100644 --- a/app/javascript/chat/channels.jsx +++ b/app/javascript/chat/channels.jsx @@ -2,6 +2,8 @@ import { h } from 'preact'; import PropTypes from 'prop-types'; // eslint-disable-next-line import/no-unresolved import ConfigImage from 'images/three-dots.svg'; +import ChannelButton from './components/channelButton'; +import { channelSorter } from './util'; const Channels = ({ activeChannelId, @@ -11,53 +13,46 @@ const Channels = ({ expanded, filterQuery, channelsLoaded, + currentUserId, + triggerActiveContent, }) => { - const channels = chatChannels.map(channel => { + const sortedChatChannels = channelSorter( + chatChannels, + currentUserId, + filterQuery, + ); + const discoverableChannels = sortedChatChannels.discoverableChannels.map( + (channel) => { + return ( + + ); + }, + ); + + const channels = sortedChatChannels.activeChannels.map((channel) => { const isActive = parseInt(activeChannelId, 10) === channel.chat_channel_id; const isUnopened = !isActive && unopenedChannelIds.includes(channel.chat_channel_id); - let newMessagesIndicator = isUnopened ? 'new' : 'old'; + const newMessagesIndicator = isUnopened ? 'new' : 'old'; const otherClassname = isActive ? 'chatchanneltab--active' : 'chatchanneltab--inactive'; + return ( - + ); }); + console.log(channels); let topNotice = ''; if ( expanded && @@ -103,6 +98,16 @@ const Channels = ({ > {topNotice} {channels} + {discoverableChannels.length > 0 ? ( +
+ + Global Channel Search + + {discoverableChannels} +
+ ) : ( + '' + )} {channelsListFooter}
{configFooter} @@ -115,9 +120,11 @@ Channels.propTypes = { chatChannels: PropTypes.arrayOf(PropTypes.objectOf()).isRequired, unopenedChannelIds: PropTypes.arrayOf().isRequired, handleSwitchChannel: PropTypes.func.isRequired, + triggerActiveContent: PropTypes.func.isRequired, expanded: PropTypes.bool.isRequired, filterQuery: PropTypes.string.isRequired, channelsLoaded: PropTypes.bool.isRequired, + currentUserId: PropTypes.string.isRequired, }; export default Channels; diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index 0f1c86cc3..fa21ae1a8 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -14,8 +14,15 @@ import { sendChannelInviteAction, deleteMessage, editMessage, + sendChannelRequest, } from './actions'; -import { hideMessages, scrollToBottom, setupObserver } from './util'; +import { + hideMessages, + scrollToBottom, + setupObserver, + getCurrentUser, + channelSorter, +} from './util'; import Alert from './alert'; import Channels from './channels'; import Compose from './compose'; @@ -124,7 +131,7 @@ export default class Chat extends Component { getChannels( '', activeChannelId, - this.props, + '', channelPaginationNum, filters, this.loadChannels, @@ -165,9 +172,11 @@ export default class Chat extends Component { } } - filterForActiveChannel = (channels, id) => + filterForActiveChannel = (channels, id, currentUserId) => channels.filter( - (channel) => channel.chat_channel_id === parseInt(id, 10), + (channel) => + channel.chat_channel_id === parseInt(id, 10) && + channel.viewable_by === parseInt(currentUserId, 10), )[0]; subscribePusher = (channelName) => { @@ -271,7 +280,7 @@ export default class Chat extends Component { }; loadPaginatedChannels = (channels) => { - const state = this.state; + const {state} = this; const currentChannels = state.chatChannels; const currentChannelIds = currentChannels.map((channel) => channel.id); const newChannels = currentChannels; @@ -408,8 +417,10 @@ export default class Chat extends Component { unopenedChannelIds, } = this.state; const receivedChatChannelId = message.chat_channel_id; - const messageList = document.getElementById('messagelist') - const nearBottom = (messageList.scrollTop + messageList.offsetHeight + 400 > messageList.scrollHeight) + const messageList = document.getElementById('messagelist'); + const nearBottom = + messageList.scrollTop + messageList.offsetHeight + 400 > + messageList.scrollHeight; if (nearBottom) { scrollToBottom(); } @@ -431,7 +442,9 @@ export default class Chat extends Component { } } const newShowAlert = - activeChannelId === receivedChatChannelId ? { showAlert: !nearBottom } : {}; + activeChannelId === receivedChatChannelId + ? { showAlert: !nearBottom } + : {}; let newMessageChannelIndex = 0; let newMessageChannel = null; const newChannelsObj = chatChannels.map((channel, index) => { @@ -506,7 +519,7 @@ export default class Chat extends Component { getChannels( filterQuery, activeChannelId, - this.props, + '', channelPaginationNum, filters, this.loadPaginatedChannels, @@ -546,17 +559,28 @@ export default class Chat extends Component { e.preventDefault(); } } - if (leftPressed && activeContent[activeChannelId] && e.target.value === '' && document.getElementById('activecontent-iframe')) { + if ( + leftPressed && + activeContent[activeChannelId] && + e.target.value === '' && + document.getElementById('activecontent-iframe') + ) { e.preventDefault(); try { - e.target.value = document.getElementById('activecontent-iframe').contentWindow.location.href - } catch(err){ - e.target.value = activeContent[activeChannelId].path + e.target.value = document.getElementById( + 'activecontent-iframe', + ).contentWindow.location.href; + } catch (err) { + e.target.value = activeContent[activeChannelId].path; } } - if (rightPressed && !activeContent[activeChannelId] && e.target.value === '') { + if ( + rightPressed && + !activeContent[activeChannelId] && + e.target.value === '' + ) { e.preventDefault(); - const richLinks = document.querySelectorAll(".chatchannels__richlink"); + const richLinks = document.querySelectorAll('.chatchannels__richlink'); if (richLinks.length === 0) { return; } @@ -570,9 +594,11 @@ export default class Chat extends Component { } if (escPressed && activeContent[activeChannelId]) { this.setActiveContentState(activeChannelId, null); - this.setState({fullscreenContent: null, expanded: window.innerWidth > 600}); + this.setState({ + fullscreenContent: null, + expanded: window.innerWidth > 600, + }); } - }; handleKeyDownEdit = (e) => { @@ -620,7 +646,7 @@ export default class Chat extends Component { } else if (message.startsWith('/play ')) { const messageObject = { activeChannelId, - message: message, + message, mentionedUsersId: this.getMentionedUsers(message), }; sendMessage(messageObject, this.handleSuccess, this.handleFailure); @@ -637,7 +663,7 @@ export default class Chat extends Component { type_of: 'loading-post', }); this.setActiveContent({ - path: '/search?q=' + message.replace('/search ', ''), + path: `/search?q=${message.replace('/search ', '')}`, type_of: 'article', }); } else if (message.startsWith('/s ')) { @@ -645,7 +671,7 @@ export default class Chat extends Component { type_of: 'loading-post', }); this.setActiveContent({ - path: '/search?q=' + message.replace('/s ', ''), + path: `/search?q=${message.replace('/s ', '')}`, type_of: 'article', }); } else if (message.startsWith('/')) { @@ -672,7 +698,7 @@ export default class Chat extends Component { message, mentionedUsersId: this.getMentionedUsers(message), }; - this.setState({scrolled: false, showAlert: false}) + this.setState({ scrolled: false, showAlert: false }); sendMessage(messageObject, this.handleSuccess, this.handleFailure); } }; @@ -695,6 +721,7 @@ export default class Chat extends Component { isMobileDevice, unopenedChannelIds, activeChannelId, + currentUserId, } = this.state; const newUnopenedChannelIds = unopenedChannelIds; const index = newUnopenedChannelIds.indexOf(id); @@ -702,7 +729,11 @@ export default class Chat extends Component { newUnopenedChannelIds.splice(index, 1); } this.setState({ - activeChannel: this.filterForActiveChannel(chatChannels, id), + activeChannel: this.filterForActiveChannel( + chatChannels, + id, + currentUserId, + ), activeChannelId: parseInt(id, 10), scrolled: false, showAlert: false, @@ -805,16 +836,25 @@ export default class Chat extends Component { e.stopPropagation(); const { activeChannelId, activeChannel } = this.state; - if (target.dataset.content.startsWith('chat_channels/')) { + if (content.startsWith('chat_channels/')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-user', }); - getContent( - `/${target.dataset.content}/channel_info`, - this.setActiveContent, - null, - ); - } else if (target.dataset.content === 'sidecar_all') { + getContent(`/${content}/channel_info`, this.setActiveContent, null); + } else if (content === 'sidecar-channel-request') { + this.setActiveContent({ + data: { + user: getCurrentUser(), + channel: { + id: target.dataset.channelId, + name: target.dataset.channelName, + status: target.dataset.channelStatus, + }, + }, + handleJoiningRequest: this.handleJoiningRequest, + type_of: 'channel-request', + }); + } else if (content === 'sidecar_all') { this.setActiveContentState(activeChannelId, { type_of: 'loading-post', }); @@ -847,11 +887,17 @@ export default class Chat extends Component { }); } else if (target.dataset.content === 'exit') { this.setActiveContentState(activeChannelId, null); - this.setState({ fullscreenContent: null, expanded: window.innerWidth > 600 }); + this.setState({ + fullscreenContent: null, + expanded: window.innerWidth > 600, + }); } else if (target.dataset.content === 'fullscreen') { const mode = this.state.fullscreenContent === 'sidecar' ? null : 'sidecar'; - this.setState({ fullscreenContent: mode, expanded: (mode === null || window.innerWidth > 1600) }); + this.setState({ + fullscreenContent: mode, + expanded: mode === null || window.innerWidth > 1600, + }); } } document.getElementById('messageform').focus(); @@ -919,7 +965,18 @@ export default class Chat extends Component { fetchingPaginatedChannels: false, }); const filters = type === 'all' ? {} : { filters: `channel_type:${type}` }; - getChannels(filterQuery, null, this.props, 0, filters, this.loadChannels); + if (filterQuery && type !== 'direct') { + getChannels( + filterQuery, + null, + 'discoverable', + 0, + filters, + this.loadChannels, + ); + } else { + getChannels(filterQuery, null, '', 0, filters, this.loadChannels); + } }; handleFailure = (err) => { @@ -1021,14 +1078,18 @@ export default class Chat extends Component { channelTypeFilter === 'all' ? {} : { filters: `channel_type:${channelTypeFilter}` }; - getChannels( - e.target.value, - null, - this.props, - 0, - filters, - this.loadChannels, - ); + if (e.target.value) { + getChannels( + e.target.value, + null, + 'discoverable', + 0, + filters, + this.loadChannels, + ); + } else { + getChannels(e.target.value, null, '', 0, filters, this.loadChannels); + } }; toggleExpand = () => { @@ -1053,6 +1114,9 @@ export default class Chat extends Component { setTimeout(function () { document.getElementById('chatchannelsearchbar').focus(); }, 100); + } else { + getChannels('', null, '', 0, '', this.loadChannels); + this.setState({ filterQuery: '' }); } this.setState({ searchShowing: !this.state.searchShowing }); }; @@ -1156,31 +1220,33 @@ export default class Chat extends Component { channelsLoaded={state.channelsLoaded} filterQuery={state.filterQuery} expanded={state.expanded} + currentUserId={state.currentUserId} + triggerActiveContent={this.triggerActiveContent} /> {notificationsState} ); } return ( -
- {notificationsButton} - - - {notificationsState} -
+
+ {notificationsButton} + + + {notificationsState} +
); } return ''; @@ -1320,11 +1386,18 @@ export default class Chat extends Component { onTriggerVideoContent = (e) => { if (e.target.dataset.content === 'exit') { - this.setState({ videoPath: null, fullscreenContent: null, expanded: window.innerWidth > 600 }); + this.setState({ + videoPath: null, + fullscreenContent: null, + expanded: window.innerWidth > 600, + }); } else if (this.state.fullscreenContent === 'video') { this.setState({ fullscreenContent: null }); } else { - this.setState({ fullscreenContent: 'video', expanded: window.innerWidth > 1600 }); + this.setState({ + fullscreenContent: 'video', + expanded: window.innerWidth > 1600, + }); } }; @@ -1551,6 +1624,21 @@ export default class Chat extends Component { this.setState({ showDeleteModal: false }); }; + handleJoiningRequest = (e) => { + sendChannelRequest( + e.target.dataset.channelId, + this.handleJoiningRequestSuccess, + null, + ); + }; + + handleJoiningRequestSuccess = () => { + const { activeChannelId } = this.state; + this.setActiveContentState(activeChannelId, null); + this.setState({ fullscreenContent: null }); + this.toggleSearchShowing(); + }; + renderChannelHeaderInner = () => { const { activeChannel } = this.state; if (activeChannel.channel_type === 'direct') { diff --git a/app/javascript/chat/components/channelButton.jsx b/app/javascript/chat/components/channelButton.jsx new file mode 100644 index 000000000..fa2096b6c --- /dev/null +++ b/app/javascript/chat/components/channelButton.jsx @@ -0,0 +1,98 @@ +import { h } from 'preact'; +import PropTypes from 'prop-types'; + +const ChannelButton = ({ + channel, + handleSwitchChannel, + otherClassname, + newMessagesIndicator, + isUnopened, + discoverableChannel, + triggerActiveContent, +}) => { + return ( + + ); +}; + +ChannelButton.propTypes = { + channel: PropTypes.shape({ + channel_name: PropTypes.string, + channel_color: PropTypes.string, + channel_type: PropTypes.string, + channel_modified_slug: PropTypes.string, + id: PropTypes.number, + chat_channel_id: PropTypes.number, + status: PropTypes.string, + channel_image: PropTypes.string, + }).isRequired, + discoverableChannel: PropTypes.bool, + handleSwitchChannel: PropTypes.func, + triggerActiveContent: PropTypes.func, + newMessagesIndicator: PropTypes.string, + otherClassname: PropTypes.string, + isUnopened: PropTypes.string, +}; + +ChannelButton.defaultProps = { + otherClassname: '', + isUnopened: '', + newMessagesIndicator: '', + discoverableChannel: false, + handleSwitchChannel: null, + triggerActiveContent: null, +}; +export default ChannelButton; diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx index f16ce0c8e..aa5ffb62d 100644 --- a/app/javascript/chat/content.jsx +++ b/app/javascript/chat/content.jsx @@ -1,8 +1,7 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; import Article from './article'; - - +import ChannelRequest from './channelRequest'; export default class Content extends Component { static propTypes = { resource: PropTypes.object, @@ -12,28 +11,53 @@ export default class Content extends Component { }; render() { + const { onTriggerContent, fullscreen } = this.props; if (!this.props.resource) { return ''; } - const fullScreenIcon = this.props.fullscreen ? : ; + const smartSvgIcon = (content, d) => ( + + + + + ); + return (
{display(this.props)}
@@ -42,21 +66,19 @@ export default class Content extends Component { } function display(props) { - if (props.resource.type_of === 'loading-user') { + const { resource } = props; + if (resource.type_of === 'loading-user') { + return
; + } + if (resource.type_of === 'article') { + return
; + } + if (resource.type_of === 'channel-request') { return ( -
); } - if (props.resource.type_of === 'article') { - return
; - } } diff --git a/app/javascript/chat/util.js b/app/javascript/chat/util.js index a832aa6ce..90785f83a 100644 --- a/app/javascript/chat/util.js +++ b/app/javascript/chat/util.js @@ -30,6 +30,11 @@ const getWaitOnUserDataHandler = ({ resolve, reject, waitTime = 20 }) => { }; }; +export const getCurrentUser = () => { + const { user } = document.body.dataset; + return JSON.parse(user); +}; + export function getUserDataAndCsrfToken() { return new Promise((resolve, reject) => { getWaitOnUserDataHandler({ resolve, reject })(); @@ -52,7 +57,7 @@ export function setupObserver(callback) { export function hideMessages(messages, userId) { const cleanedMessages = Object.keys(messages).reduce( (accumulator, channelId) => { - const newMessages = messages[channelId].map(message => { + const newMessages = messages[channelId].map((message) => { if (message.user_id === userId) { const messageClone = Object.assign({ type: 'hidden' }, message); messageClone.message = ''; @@ -79,3 +84,26 @@ export function adjustTimestamp(timestamp) { time = new Intl.DateTimeFormat('en-US', options).format(time); return time; } + +export const channelSorter = (channels, currentUserId, filterQuery) => { + const activeChannels = channels.filter( + (channel) => + channel.viewable_by === currentUserId && channel.status === 'active', + ); + const joiningChannels = channels.filter( + (channel) => channel.status === 'joining_request', + ); + const ChannelIds = [ + [...new Set(activeChannels.map((x) => x.chat_channel_id))], + [...new Set(joiningChannels.map((x) => x.chat_channel_id))], + ]; + const discoverableChannels = channels + .filter( + (channel) => + (channel.status === 'joining_request' && filterQuery) || + (!ChannelIds[1].includes(channel.chat_channel_id) && + channel.viewable_by !== currentUserId), + ) + .filter((channel) => !ChannelIds[0].includes(channel.chat_channel_id)); + return { activeChannels, discoverableChannels }; +}; diff --git a/app/javascript/chat/videoContent.jsx b/app/javascript/chat/videoContent.jsx index 80744124a..5d8794231 100644 --- a/app/javascript/chat/videoContent.jsx +++ b/app/javascript/chat/videoContent.jsx @@ -1,12 +1,23 @@ import { h, Component } from 'preact'; export default class VideoContent extends Component { - render() { if (!this.props.videoPath) { - return "" + return ''; } - const fullScreenIcon = this.props.fullscreen ? : ; + + const smartSvgIcon = (content, d) => ( + + + + + ); return (
- + {smartSvgIcon( + 'exit', + 'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z', + )}