From e2edf960dca7ff445ef31ad47a6e4a2b8773acb3 Mon Sep 17 00:00:00 2001 From: jituraj98 <61928156+jituraj98@users.noreply.github.com> Date: Wed, 1 Apr 2020 21:43:26 +0530 Subject: [PATCH] [deploy] Connect: move settings to sidecar iframe, improve invitations and remove experimental features (#6903) 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 * Move channel settings to sidecar * added spec for chat_channels_memberships_controller.rb (#6539) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message Co-authored-by: jitendra * Refining "Move sidecar to iframe" branch (#6634) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message * 🐞 Bug Fix: Open sidecar of clicking @all * 🐞 Bug Fix: adding space after selecting the username from suggestion box * added remove channel member functionality for moderators * πŸ”© Small Tweaks: Hover issue fixed * 🐞 Bug Fix: Remove sidecar toggle on @all tag (for now) * updated manually constructed urls with rails path helpers * mod users cannot remove members of other channel Co-authored-by: jitendra * πŸš€ Finalising the connect group/channel feature. (#6829) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message * 🐞 Bug Fix: Open sidecar of clicking @all * 🐞 Bug Fix: adding space after selecting the username from suggestion box * added remove channel member functionality for moderators * πŸ”© Small Tweaks: Hover issue fixed * 🐞 Bug Fix: Remove sidecar toggle on @all tag (for now) * updated manually constructed urls with rails path helpers * mod users cannot remove members of other channel * mod members can now invite user who have left the channel * 🐞 Bug Fix: Ability to open sidecar on mention all properly * Eslint Changes * send email notification on inviting user feature added * πŸ›  Email design enhancements for channel invitation * send invitation to mod user added from internal * πŸ›  Gif changes for Mod * spec fixed * inviter name issue fixed * πŸš€ Open sidecar when automatically if you use link from email Co-authored-by: jitendra * review changes * added spec for channel_invite_email * Bug🐞: Design problem with sidecar after merge * Bug🐞: Design problem with invitation alert box * replaced instance variables with local variable * Bug🐞: Wrong css classes fixed * Bug🐞: Edit option bug * yarn.lock file updated * code review changes * updated manually constructed urls with rails path helpers in spec * user name url issue fix in invite email * user url corrected Co-authored-by: Sarthak Sharma <7lovesharma7@gmail.com> Co-authored-by: benhalpern --- app/assets/stylesheets/chat.scss | 55 +-- app/assets/stylesheets/scaffolds.scss | 12 + app/assets/stylesheets/settings.scss | 68 ++- .../chat_channel_memberships_controller.rb | 80 ++- app/controllers/chat_channels_controller.rb | 12 +- .../internal/chat_channels_controller.rb | 9 +- .../__snapshots__/codeEditor.test.jsx.snap | 14 - .../__snapshots__/githubRepo.test.jsx.snap | 100 ---- .../__snapshots__/userDetails.test.jsx.snap | 460 ------------------ .../__snapshots__/view.test.jsx.snap | 78 --- .../chat/__tests__/codeEditor.test.jsx | 14 - .../chat/__tests__/githubRepo.test.jsx | 179 ------- .../chat/__tests__/userDetails.test.jsx | 211 -------- app/javascript/chat/__tests__/view.test.jsx | 139 ------ app/javascript/chat/chat.jsx | 85 ++-- app/javascript/chat/codeEditor.jsx | 49 -- app/javascript/chat/content.jsx | 53 +- app/javascript/chat/githubRepo.jsx | 185 ------- app/javascript/chat/userDetails.jsx | 271 ----------- app/javascript/chat/view.jsx | 85 ---- app/labor/assign_tag_moderator.rb | 6 +- app/mailers/notify_mailer.rb | 11 + app/models/chat_channel.rb | 74 ++- app/models/chat_channel_membership.rb | 4 +- app/models/message.rb | 2 +- app/policies/chat_channel_policy.rb | 2 +- .../chat_channel_memberships/edit.html.erb | 99 ++++ .../chat_channel_memberships/index.html.erb | 30 ++ .../channel_invite_email.html.erb | 45 ++ .../channel_invite_email.text.erb | 3 + .../follows/create_chat_channel_worker.rb | 2 +- config/routes.rb | 3 +- db/seeds.rb | 2 +- package.json | 1 - spec/mailers/notify_mailer_spec.rb | 26 +- spec/models/chat_channel_spec.rb | 6 +- .../requests/chat_channel_memberships_spec.rb | 319 +++++++++--- spec/requests/chat_channels_spec.rb | 8 +- spec/requests/internal/chat_channel_spec.rb | 24 + spec/requests/internal/users_manage_spec.rb | 2 +- .../users/cleanup_chat_channels_spec.rb | 4 +- spec/services/users/delete_spec.rb | 4 +- .../moderator/banish_user_worker_spec.rb | 2 +- yarn.lock | 5 - 44 files changed, 797 insertions(+), 2046 deletions(-) delete mode 100644 app/javascript/chat/__tests__/__snapshots__/codeEditor.test.jsx.snap delete mode 100644 app/javascript/chat/__tests__/__snapshots__/githubRepo.test.jsx.snap delete mode 100644 app/javascript/chat/__tests__/__snapshots__/userDetails.test.jsx.snap delete mode 100644 app/javascript/chat/__tests__/__snapshots__/view.test.jsx.snap delete mode 100644 app/javascript/chat/__tests__/codeEditor.test.jsx delete mode 100644 app/javascript/chat/__tests__/githubRepo.test.jsx delete mode 100644 app/javascript/chat/__tests__/userDetails.test.jsx delete mode 100644 app/javascript/chat/__tests__/view.test.jsx delete mode 100644 app/javascript/chat/codeEditor.jsx delete mode 100644 app/javascript/chat/githubRepo.jsx delete mode 100644 app/javascript/chat/userDetails.jsx delete mode 100644 app/javascript/chat/view.jsx create mode 100644 app/views/chat_channel_memberships/edit.html.erb create mode 100644 app/views/chat_channel_memberships/index.html.erb create mode 100644 app/views/mailers/notify_mailer/channel_invite_email.html.erb create mode 100644 app/views/mailers/notify_mailer/channel_invite_email.text.erb create mode 100644 spec/requests/internal/chat_channel_spec.rb diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index 20df42ae3..a4ee8caba 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -1,5 +1,4 @@ @import 'variables'; -@import 'connect/_channel-details'; @import 'mixins'; // High level classes @@ -645,54 +644,19 @@ } } -.activecontent__githubrepo { -} - -.activecontent__githubrepoheader { - padding-left: 30px; - padding-bottom: 3px; - min-height: 30px; - margin-top: -9px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - font-size: 0.9em; -} - -.activecontent__githubrepofiles { - border-bottom: 1px solid $light-medium-gray; - font-size: 0.8em; - overflow: auto; - - &.activecontent__githubrepofiles--root { - height: 280px; - max-height: 40vh; - } -} - -.activecontent__githubreporeadme { - font-size: 0.8em; - - img { - max-width: 100%; - } -} - -.activecontent__githubrepofilerow { - padding: 4px 2px; - border-top: 1px solid $light-medium-gray; -} - -.chat__channelinvitationsindicator button { - background: $green; +.chat__channelinvitationsindicator a { + background: linear-gradient(10deg, darken($green, 25%), darken($green, 15%)); text-align: center; - padding: 10px 0px; + padding: 30px 0px; display: block; border-radius: 3px; margin: 3px 0px; border: 0px; - width: 93%; - font-size: 0.9em; + width: 94%; + color: white; + font-size: 1.1em; + font-weight: 700; + -webkit-appearance: unset; } .chatchannels { @@ -1255,6 +1219,9 @@ border-left: 3px solid #4e57ef; height: 50px; overflow: hidden; + p { + margin: 18px 0; + } } .closeEdit { diff --git a/app/assets/stylesheets/scaffolds.scss b/app/assets/stylesheets/scaffolds.scss index 44bcd79d0..c4f5ba7b1 100644 --- a/app/assets/stylesheets/scaffolds.scss +++ b/app/assets/stylesheets/scaffolds.scss @@ -104,6 +104,18 @@ pre { } } +.cta { + color: $black; + background: $green; + font-family: $helvetica-condensed; + -webkit-appearance: none; + font-stretch: condensed; + font-weight: bold; + &.cta-danger { + background: $red; + } +} + button { cursor: pointer; } diff --git a/app/assets/stylesheets/settings.scss b/app/assets/stylesheets/settings.scss index faaad7da5..13d7d4032 100755 --- a/app/assets/stylesheets/settings.scss +++ b/app/assets/stylesheets/settings.scss @@ -4,6 +4,8 @@ $input-width: 650px; #page-content.users-edit, +#page-content.chat_channel_memberships-edit, +#page-content.chat_channel_memberships-index, #page-content.users-update, #page-content.users-confirm_destroy, #page-content.organizations-create, @@ -12,18 +14,25 @@ $input-width: 650px; #notice { background: $green; color: black; - padding: 40px 0px 40px; + padding: 40px 15px 40px; text-align: center; position: relative; top: 0; left: 0; right: 0; + font-weight: 700; + font-size: 1.05em; &.error-notice { background: rgba(255, 80, 80, 0.7); color: white; } } .user-settings-page { + h1, + h3, + p { + margin: 10px 0 !important; + } width: 900px; max-width: 94%; margin: 0 auto; @@ -340,6 +349,27 @@ $input-width: 650px; resize: none; } } + %inline-button-form { + display: inline-block; + input[type='submit'] { + display: inline-block; + font-size: 17px; + border: 0px; + border-radius: 100px; + padding: 2px 10px; + width: auto; + margin: 0px; + margin-left: 5px; + color: white; + background: $black; + } + } + .inline-button-form { + @extend %inline-button-form; + } + .inline-button-form-hidden { + display: none; + } h2 { font-size: calc(40px + 1vw); font-family: $helvetica-condensed; @@ -676,6 +706,31 @@ $input-width: 650px; } } +.settings-form-userlist { + .settings-form-userlistrow { + font-size: 18px; + padding: 6px 0px; + min-height: 33px; + display: block; + @include themeable(color, theme-color, $black); + a { + @include themeable(color, theme-color, $black); + } + img { + height: 1.5em; + width: 1.5em; + border-radius: 100px; + vertical-align: -0.25em; + margin-right: 0.25em; + } + &:hover { + .inline-button-form-hidden { + @extend %inline-button-form; + } + } + } +} + .signout_confirm-wrapper { padding: calc(15% + 50px) 2% 30%; text-align: center; @@ -786,3 +841,14 @@ ul.delete__account { .membership-purchase-link { font-size: 2rem; } + +#page-content.chat_channel_memberships-edit, +#page-content.chat_channel_memberships-index { + #page-content-inner { + margin-top: 40px; + } + .notice { + margin-top: -40px; + margin-bottom: 20px; + } +} diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb index 0ceeced98..fcb664783 100644 --- a/app/controllers/chat_channel_memberships_controller.rb +++ b/app/controllers/chat_channel_memberships_controller.rb @@ -1,6 +1,11 @@ class ChatChannelMembershipsController < ApplicationController after_action :verify_authorized + def index + skip_authorization + @pending_invites = current_user.chat_channel_memberships.includes(:chat_channel).where(status: "pending") + end + def find_by_chat_channel_id @membership = ChatChannelMembership.where(chat_channel_id: params[:chat_channel_id], user_id: current_user.id).first! authorize @membership @@ -11,41 +16,80 @@ class ChatChannelMembershipsController < ApplicationController ) end + def edit + @membership = ChatChannelMembership.find(params[:id]) + @channel = @membership.chat_channel + authorize @membership + end + def create - @chat_channel = ChatChannel.find(permitted_params[:chat_channel_id]) + membership_params = params[:chat_channel_membership] + @chat_channel = ChatChannel.find(membership_params[:chat_channel_id]) authorize @chat_channel, :update? - ChatChannelMembership.create( - user_id: permitted_params[:user_id], - chat_channel_id: @chat_channel.id, - status: "pending", - ) + usernames = membership_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) + end + + def remove_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]) + if params[:status] == "pending" + @chat_channel_membership.destroy + flash[:settings_notice] = "Invitation removed." + else + @chat_channel_membership.update(status: "removed_from_channel") + flash[:settings_notice] = "Removed #{@chat_channel_membership.user.name}" + end + membership = ChatChannelMembership.find_by!(chat_channel_id: params[:chat_channel_id], user: current_user) + redirect_to edit_chat_channel_membership_path(membership) end def update @chat_channel_membership = ChatChannelMembership.find(params[:id]) authorize @chat_channel_membership - if permitted_params[:user_action] == "accept" - @chat_channel_membership.update(status: "active") + if permitted_params[:user_action].present? + respond_to_invitation else - @chat_channel_membership.update(status: "rejected") + @chat_channel_membership.update(permitted_params) + flash[:settings_notice] = "Personal settings updated." + redirect_to edit_chat_channel_membership_path(@chat_channel_membership.id) end - @chat_channels_memberships = current_user. - chat_channel_memberships.includes(:chat_channel). - where(status: "pending"). - order("chat_channel_memberships.updated_at DESC") - render "chat_channels/index.json" end def destroy - @chat_channel_membership = ChatChannel.find(params[:id]). - chat_channel_memberships.where(user_id: current_user.id).first + @chat_channel_membership = ChatChannelMembership.find(params[:id]) authorize @chat_channel_membership + channel_name = @chat_channel_membership.chat_channel.channel_name @chat_channel_membership.update(status: "left_channel") @chat_channels_memberships = [] - render json: { result: "left channel" }, status: :created + 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 end + private + def permitted_params - params.require(:chat_channel_membership).permit(:user_id, :chat_channel_id, :user_action, :id) + params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification) + end + + def respond_to_invitation + if permitted_params[:user_action] == "accept" + @chat_channel_membership.update(status: "active") + channel_name = @chat_channel_membership.chat_channel.channel_name + flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list." + else + @chat_channel_membership.update(status: "rejected") + flash[:settings_notice] = "Invitation rejected." + end + redirect_to chat_channel_memberships_path end end diff --git a/app/controllers/chat_channels_controller.rb b/app/controllers/chat_channels_controller.rb index fbab3c968..1c4ccc313 100644 --- a/app/controllers/chat_channels_controller.rb +++ b/app/controllers/chat_channels_controller.rb @@ -30,8 +30,14 @@ class ChatChannelsController < ApplicationController end def update - ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update - render_chat_channel + if ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update + flash[:settings_notice] = "Channel settings updated." + else + default_error_message = "Channel settings updation failed. Try again later." + flash[:error] = @chat_channel.errors.full_messages.to_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 open @@ -75,7 +81,7 @@ class ChatChannelsController < ApplicationController valid_listing = ClassifiedListing.where(user_id: params[:user_id], contact_via_connect: true).limit(1) authorize ChatChannel if chat_recipient.inbox_type == "open" || valid_listing.length == 1 - chat = ChatChannel.create_with_users([current_user, chat_recipient], "direct") + chat = ChatChannel.create_with_users(users: [current_user, chat_recipient], channel_type: "direct") message_markdown = params[:message] message = Message.new( chat_channel: chat, diff --git a/app/controllers/internal/chat_channels_controller.rb b/app/controllers/internal/chat_channels_controller.rb index 923cb84a3..20ffa1c2a 100644 --- a/app/controllers/internal/chat_channels_controller.rb +++ b/app/controllers/internal/chat_channels_controller.rb @@ -6,13 +6,18 @@ class Internal::ChatChannelsController < Internal::ApplicationController end def create - ChatChannel.create_with_users(users_by_param, "invite_only", chat_channel_params[:channel_name]) + ChatChannel.create_with_users( + users: users_by_param, + channel_type: "invite_only", + contrived_name: chat_channel_params[:channel_name], + membership_role: "mod", + ) redirect_back(fallback_location: "/internal/chat_channels") end def update @chat_channel = ChatChannel.find(params[:id]) - @chat_channel.add_users(users_by_param) + @chat_channel.invite_users(users: users_by_param, membership_role: "mod") redirect_back(fallback_location: "/internal/chat_channels") end diff --git a/app/javascript/chat/__tests__/__snapshots__/codeEditor.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/codeEditor.test.jsx.snap deleted file mode 100644 index 2172d92b2..000000000 --- a/app/javascript/chat/__tests__/__snapshots__/codeEditor.test.jsx.snap +++ /dev/null @@ -1,14 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render and test snapshot 1`] = ` -
-
- Experimental (WIP) -
-
-`; diff --git a/app/javascript/chat/__tests__/__snapshots__/githubRepo.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/githubRepo.test.jsx.snap deleted file mode 100644 index f279cfcc7..000000000 --- a/app/javascript/chat/__tests__/__snapshots__/githubRepo.test.jsx.snap +++ /dev/null @@ -1,100 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` with fake token should render and test snapshot 1`] = ` -preact-render-spy (1 nodes) -------- -
-
- - invalid json response body at reason: Unexpected end of JSON input -
- -
- -`; - -exports[` with no token should render and test snapshot 1`] = ` -
-
- - Authentication required - -
-

- This feature is in internal alpha testing mode. -

-
-`; diff --git a/app/javascript/chat/__tests__/__snapshots__/userDetails.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/userDetails.test.jsx.snap deleted file mode 100644 index b04faa386..000000000 --- a/app/javascript/chat/__tests__/__snapshots__/userDetails.test.jsx.snap +++ /dev/null @@ -1,460 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` for user1 should render and test snapshot 1`] = ` -
- bojackhorseman profile -

- - Bojack Horseman - -

- -
- I am the Bojack Horseman from Horsing Around and Secreteriat -
-
-
-
- location -
-
- Los Angeles, CA -
-
-
- joined -
-
- January 2, 1964 -
-
-
-
-
-

- Reporting abuse will: -

-
    -
  • - close this chat and prevent this user from re-opening chat with you -
  • -
  • - give the DEV team your consent to read messages in this chat to understand your report and take appropriate action -
  • -
-

- Blocking is only on Connect right now and has not been implemented across DEV yet. -

-
- Are you sure? -
- - Yes, Report - - - No - -
-
-
-
-

- Blocking on connect will: -

-
    -
  • - close this chat and prevent this user from re-opening chat with you -
  • -
  • - NOT notify the user you will block--this channel will become inaccessible for both users -
  • -
-

- Blocking is only on Connect right now and has not been implemented across DEV yet. Consider reporting abuse to the DEV team if this user is spamming or harassing elsewhere on dev.to, so we can take further action. -

-
- Are you sure? -
- - Yes, Block - - - No - -
-
-
-`; - -exports[` for user2 should render and test snapshot 1`] = ` -
- mrpeanutbutter profile -

- - Mr. Peanutbutter - -

- -
- Woof Woof *smile* -
-
-
-
- location -
-
- Los Angeles, DO(G) -
-
-
- joined -
-
- 1960s -
-
-
- - -
-
-
-

- Reporting abuse will: -

-
    -
  • - close this chat and prevent this user from re-opening chat with you -
  • -
  • - give the DEV team your consent to read messages in this chat to understand your report and take appropriate action -
  • -
-

- Blocking is only on Connect right now and has not been implemented across DEV yet. -

-
- Are you sure? -
- - Yes, Report - - - No - -
-
-
-
-

- Blocking on connect will: -

-
    -
  • - close this chat and prevent this user from re-opening chat with you -
  • -
  • - NOT notify the user you will block--this channel will become inaccessible for both users -
  • -
-

- Blocking is only on Connect right now and has not been implemented across DEV yet. Consider reporting abuse to the DEV team if this user is spamming or harassing elsewhere on dev.to, so we can take further action. -

-
- Are you sure? -
- - Yes, Block - - - No - -
-
-
-`; diff --git a/app/javascript/chat/__tests__/__snapshots__/view.test.jsx.snap b/app/javascript/chat/__tests__/__snapshots__/view.test.jsx.snap deleted file mode 100644 index ddf10ddba..000000000 --- a/app/javascript/chat/__tests__/__snapshots__/view.test.jsx.snap +++ /dev/null @@ -1,78 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[` should render and test snapshot (no channel) 1`] = ` -
-
- -

- Channel Invitations - - πŸ€— - -

-
-
-`; - -exports[` should render and test snapshot (with channel) 1`] = ` -preact-render-spy (1 nodes) -------- -
-
- -

- Channel Invitations - - πŸ€— - -

-
-

name

-
- some description -
- - -
-
-
- -`; diff --git a/app/javascript/chat/__tests__/codeEditor.test.jsx b/app/javascript/chat/__tests__/codeEditor.test.jsx deleted file mode 100644 index d32df7d8b..000000000 --- a/app/javascript/chat/__tests__/codeEditor.test.jsx +++ /dev/null @@ -1,14 +0,0 @@ -import { h } from 'preact'; -import render from 'preact-render-to-json'; -import CodeEditor from '../codeEditor'; - -const getCodeEditor = () => ( - -); - -describe('', () => { - it('should render and test snapshot', () => { - const tree = render(getCodeEditor()); - expect(tree).toMatchSnapshot(); - }); -}); diff --git a/app/javascript/chat/__tests__/githubRepo.test.jsx b/app/javascript/chat/__tests__/githubRepo.test.jsx deleted file mode 100644 index ee511b432..000000000 --- a/app/javascript/chat/__tests__/githubRepo.test.jsx +++ /dev/null @@ -1,179 +0,0 @@ -import { h } from 'preact'; -import render from 'preact-render-to-json'; -import { shallow } from 'preact-render-spy'; -import fetch from 'jest-fetch-mock'; -import GithubRepo from '../githubRepo'; - -global.fetch = fetch; - -function flushPromises() { - return new Promise(resolve => setImmediate(resolve)); -} - -const getGithubRepo = token => ( - -); - -const contents = [ - { - name: 'Camera', - path: 'Camera', - sha: 'hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp', - size: 0, - url: - 'https://api.github.com/repos/username/repositoryname/contents/Camera?ref=master', - html_url: 'https://github.com/username/repositoryname/tree/master/Camera', - git_url: - 'https://api.github.com/repos/username/repositoryname/git/trees/hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp', - download_url: null, - type: 'dir', - _links: { - self: - 'https://api.github.com/repos/username/repositoryname/contents/Camera?ref=master', - git: - 'https://api.github.com/repos/username/repositoryname/git/trees/hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp', - html: 'https://github.com/username/repositoryname/tree/master/Camera', - }, - }, - { - name: 'Environment', - path: 'Environment', - sha: 'dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs', - size: 0, - url: - 'https://api.github.com/repos/username/repositoryname/contents/Environment?ref=master', - html_url: - 'https://github.com/username/repositoryname/tree/master/Environment', - git_url: - 'https://api.github.com/repos/username/repositoryname/git/trees/dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs', - download_url: null, - type: 'dir', - _links: { - self: - 'https://api.github.com/repos/username/repositoryname/contents/Environment?ref=master', - git: - 'https://api.github.com/repos/username/repositoryname/git/trees/dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs', - html: - 'https://github.com/username/repositoryname/tree/master/Environment', - }, - }, - { - name: 'Interactables', - path: 'Interactables', - sha: '44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat', - size: 0, - url: - 'https://api.github.com/repos/username/repositoryname/contents/Interactables?ref=master', - html_url: - 'https://github.com/username/repositoryname/tree/master/Interactables', - git_url: - 'https://api.github.com/repos/username/repositoryname/git/trees/44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat', - download_url: null, - type: 'dir', - _links: { - self: - 'https://api.github.com/repos/username/repositoryname/contents/Interactables?ref=master', - git: - 'https://api.github.com/repos/username/repositoryname/git/trees/44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat', - html: - 'https://github.com/username/repositoryname/tree/master/Interactables', - }, - }, - { - name: 'Level Design', - path: 'Level Design', - sha: 'g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax', - size: 583, - url: - 'https://api.github.com/repos/username/repositoryname/contents/Level%20Design?ref=master', - html_url: - 'https://github.com/username/repositoryname/tree/master/Level%20Design', - git_url: - 'https://api.github.com/repos/username/repositoryname/git/trees/g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax', - download_url: - 'https://raw.githubusercontent.com/username/repositoryname/master/Level%20Design', - type: 'file', - _links: { - self: - 'https://api.github.com/repos/username/repositoryname/contents/Level%20Design?ref=master', - git: - 'https://api.github.com/repos/username/repositoryname/git/trees/g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax', - html: - 'https://github.com/username/repositoryname/tree/master/Level%20Design', - }, - }, - { - name: 'Player', - path: 'Player', - sha: 'tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp', - size: 1000, - url: - 'https://api.github.com/repos/username/repositoryname/contents/Player?ref=master', - html_url: 'https://github.com/username/repositoryname/tree/master/Player', - git_url: - 'https://api.github.com/repos/username/repositoryname/git/trees/tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp', - download_url: - 'https://raw.githubusercontent.com/username/repositoryname/master/Player', - type: 'file', - _links: { - self: - 'https://api.github.com/repos/username/repositoryname/contents/Player?ref=master', - git: - 'https://api.github.com/repos/username/repositoryname/git/trees/tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp', - html: 'https://github.com/username/repositoryname/tree/master/Player', - }, - }, -]; - -describe('', () => { - describe('with no token', () => { - it('should render and test snapshot', () => { - const tree = render(getGithubRepo()); - expect(tree).toMatchSnapshot(); - }); - - it('should have the proper elements, attributes and values', () => { - const context = shallow(getGithubRepo()); - expect(context.find('.activecontent__githubrepo').exists()).toEqual(true); - expect(context.find('em').text()).toEqual('Authentication required'); - }); - }); - - describe('with fake token', () => { - it('should render and test snapshot', async () => { - await fetch.mockResponseOnce(JSON.stringify(contents)); - const context = shallow(getGithubRepo('some_token')); - await flushPromises(); - expect(context).toMatchSnapshot(); - }); - - it('should have the proper elements, attributes and values with states set', async () => { - await fetch.mockResponseOnce(JSON.stringify(contents)); - const context = shallow(getGithubRepo('some_token')); - await flushPromises(); - - const len = context.find('.activecontent__githubrepofilerow').length; - expect(len > 0).toEqual(true); - for (let i = 0; i < len; i += 1) { - const ghrow = context.find('.activecontent__githubrepofilerow').at(i); - expect(ghrow.exists()).toEqual(true); - if (contents[i].type === 'dir') { - expect(ghrow.text()).toEqual(`πŸ“ ${contents[i].name}`); - } else { - expect(ghrow.text()).toEqual(contents[i].name); - } - expect(ghrow.childAt(0).attr('href')).toEqual(contents[i].html_url); - expect(ghrow.childAt(0).attr('data-api-url')).toEqual(contents[i].url); - expect(ghrow.childAt(0).attr('data-path')).toEqual(contents[i].path); - } - expect(context.find('.activecontent__githubrepoheader').text()).toEqual( - context.state('path'), - ); - }); - }); -}); diff --git a/app/javascript/chat/__tests__/userDetails.test.jsx b/app/javascript/chat/__tests__/userDetails.test.jsx deleted file mode 100644 index 9b34c09b3..000000000 --- a/app/javascript/chat/__tests__/userDetails.test.jsx +++ /dev/null @@ -1,211 +0,0 @@ -import { h } from 'preact'; -import render from 'preact-render-to-json'; -import { JSDOM } from 'jsdom'; -import { shallow } from 'preact-render-spy'; -import UserDetails from '../userDetails'; - -const doc = new JSDOM(''); -global.document = doc; -global.window = doc.defaultView; -global.window.currentUser = { id: '1' }; - -const user1 = { - id: '1', - username: 'bojackhorseman', - name: 'Bojack Horseman', - summary: 'I am the Bojack Horseman from Horsing Around and Secreteriat', - joined_at: 'January 2, 1964', - twitter_username: 'bojacktwitter', - github_username: 'bojackgithub', - website_url: 'http://bojackhorseman.com', - location: 'Los Angeles, CA', - profile_image: 'https://media.giphy.com/media/3o7WTHb2WjEXbsmWDS/giphy.gif', -}; - -const user2 = { - id: '2', - username: 'mrpeanutbutter', - name: 'Mr. Peanutbutter', - summary: 'Woof Woof *smile*', - joined_at: '1960s', - twitter_username: 'mrpbtwitter', - github_username: 'mrpbgithub', - website_url: 'http://mrpeanutbutter.com', - location: 'Los Angeles, DO(G)', - profile_image: 'https://media.giphy.com/media/xThuW6sWCGbpZMpX7a/giphy.gif', -}; - -const channel = { channel_type: 'direct', id: 2 }; - -const getUserDetails = user => ( - -); - -describe('', () => { - describe('for user1', () => { - it('should render and test snapshot', () => { - const tree = render(getUserDetails(user1)); - expect(tree).toMatchSnapshot(); - }); - - it('should have the appropriate elements, attributes and values', () => { - const context = shallow(getUserDetails(user1)); - expect( - context.find('.activechatchannel__activecontentuserdetails').exists(), - ).toEqual(true); // only class to check for - const parentDiv = context.find('div').at(0); - - expect(parentDiv.childAt(0)).toEqual(context.find('img').at(0)); - expect( - context - .find('img') - .at(0) - .attr('src'), - ).toEqual(user1.profile_image); // profile pic - - expect(parentDiv.childAt(1)).toEqual(context.find('h1')); - expect(context.find('h1').text()).toEqual(user1.name); // user.name - expect( - context - .find('a') - .at(0) - .attr('href'), - ).toEqual(`/${user1.username}`); // user.username - - expect( - context - .find('.userdetails__blockreport') - .at(0) - .children()[0], - ).toEqual(''); - - // social links - expect( - context - .find('a') - .at(1) - .attr('href'), - ).toEqual(`https://twitter.com/${user1.twitter_username}`); // twitter - expect( - context - .find('a') - .at(2) - .attr('href'), - ).toEqual(`https://github.com/${user1.github_username}`); // github - expect( - context - .find('a') - .at(3) - .attr('href'), - ).toEqual(user1.website_url); // website - - expect(parentDiv.childAt(3)).toEqual(context.find('div').at(2)); - expect( - context - .find('div') - .at(2) - .text(), - ).toEqual(user1.summary); // user.summary - expect( - context - .find('div') - .at(6) - .text(), - ).toEqual(user1.location); // user.location - expect( - context - .find('div') - .at(8) - .text(), - ).toEqual(user1.joined_at); // user.joined_at - }); - }); - - describe('for user2', () => { - it('should render and test snapshot', () => { - const tree = render(getUserDetails(user2)); - expect(tree).toMatchSnapshot(); - }); - - it('should have the appropriate elements, attributes and values', () => { - const context = shallow(getUserDetails(user2)); - expect( - context.find('.activechatchannel__activecontentuserdetails').exists(), - ).toEqual(true); // only class to check for - const parentDiv = context.find('div').at(0); - - expect(parentDiv.childAt(0)).toEqual(context.find('img').at(0)); - expect( - context - .find('img') - .at(0) - .attr('src'), - ).toEqual(user2.profile_image); // profile pic - - expect(parentDiv.childAt(1)).toEqual(context.find('h1')); - expect(context.find('h1').text()).toEqual(user2.name); // user.name - expect( - context - .find('a') - .at(0) - .attr('href'), - ).toEqual(`/${user2.username}`); // user.username - - expect( - parentDiv - .find('.userdetails__blockreport') - .at(0) - .childAt(0) - .text(), - ).toEqual('Block User'); - - expect( - parentDiv - .find('.userdetails__blockreport') - .at(0) - .childAt(1) - .text(), - ).toEqual('Report Abuse'); - - // social links - expect( - context - .find('a') - .at(1) - .attr('href'), - ).toEqual(`https://twitter.com/${user2.twitter_username}`); // twitter - expect( - context - .find('a') - .at(2) - .attr('href'), - ).toEqual(`https://github.com/${user2.github_username}`); // github - expect( - context - .find('a') - .at(3) - .attr('href'), - ).toEqual(user2.website_url); // website - - expect(parentDiv.childAt(3)).toEqual(context.find('div').at(2)); - expect( - context - .find('div') - .at(2) - .text(), - ).toEqual(user2.summary); // user.summary - expect( - context - .find('div') - .at(6) - .text(), - ).toEqual(user2.location); // user.location - expect( - context - .find('div') - .at(8) - .text(), - ).toEqual(user2.joined_at); // user.joined_at - }); - }); -}); diff --git a/app/javascript/chat/__tests__/view.test.jsx b/app/javascript/chat/__tests__/view.test.jsx deleted file mode 100644 index fd4cb6f89..000000000 --- a/app/javascript/chat/__tests__/view.test.jsx +++ /dev/null @@ -1,139 +0,0 @@ -import { h } from 'preact'; -import render from 'preact-render-to-json'; -import { deep } from 'preact-render-spy'; -import View from '../view'; - -let exited = false; -let accepted = false; -let declined = false; - -const onViewExitFake = () => { - exited = true; -}; - -const handleInvitationAcceptFake = () => { - accepted = true; -}; - -const handleInvitationDeclineFake = () => { - declined = true; -}; - -const sampleChannel = [ - { - channel_name: 'name', - description: 'some description', - membership_id: '12345', - }, -]; - -const getView = channel => ( - -); - -describe('', () => { - it('should render and test snapshot (no channel)', () => { - const tree = render(getView([])); - expect(tree).toMatchSnapshot(); - }); - - it('should render and test snapshot (with channel)', () => { - const tree = deep(getView(sampleChannel), { depth: 2 }); - expect(tree).toMatchSnapshot(); - }); - - it('should have the proper attributes and text values (no channel provided)', () => { - const context = deep(getView([]), { depth: 2 }); - expect(context.find('.chatNonChatView').exists()).toEqual(true); - expect(context.find('.container').exists()).toEqual(true); - - expect(context.find('.chatNonChatView_exitbutton').exists()).toEqual(true); - expect(context.find('.chatNonChatView_exitbutton').text()).toEqual('Γ—'); - - expect(context.find('h1').exists()).toEqual(true); - expect(context.find('h1').text()).toEqual('Channel Invitations πŸ€—'); - - expect(context.find('.chatNonChatView_contentblock').exists()).toEqual( - false, - ); - }); - - it('should have the proper attributes and text values (with channel provided)', () => { - const context = deep(getView(sampleChannel), { depth: 2 }); - expect(context.find('.chatNonChatView_contentblock').exists()).toEqual( - true, - ); - - expect(context.find('h2').exists()).toEqual(true); - expect(context.find('h2').text()).toEqual('name'); - - expect(context.find('em').exists()).toEqual(true); - expect(context.find('em').text()).toEqual('some description'); - - expect(context.find('.cta').exists()).toEqual(true); - expect( - context - .find('.cta') - .at(0) - .attr('data-content'), - ).toEqual('12345'); // accept button - expect( - context - .find('.cta') - .at(0) - .text(), - ).toEqual('Accept'); // accept button - expect( - context - .find('.cta') - .at(1) - .attr('data-content'), - ).toEqual('12345'); // decline button - expect( - context - .find('.cta') - .at(1) - .text(), - ).toEqual('Decline'); // accept button - }); - - it('should trigger exit', () => { - const context = deep(getView([]), { depth: 2 }); - context.find('.chatNonChatView_exitbutton').simulate('click'); - expect(exited).toEqual(true); - exited = false; - }); - - it('should trigger accept', () => { - const context = deep(getView(sampleChannel), { depth: 2 }); - context - .find('.cta') - .at(0) - .simulate('click'); // click accept button - - expect(exited).toEqual(false); - expect(accepted).toEqual(true); - expect(declined).toEqual(false); - - accepted = false; - }); - - it('should trigger decline', () => { - const context = deep(getView(sampleChannel), { depth: 2 }); - context - .find('.cta') - .at(1) - .simulate('click'); // click decline button - - expect(exited).toEqual(false); - expect(accepted).toEqual(false); - expect(declined).toEqual(true); - - declined = false; - }); -}); diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index d9cb8dc4f..7fc9f0e74 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -21,7 +21,6 @@ import Compose from './compose'; import Message from './message'; import Content from './content'; import Video from './video'; -import View from './view'; import setupPusher from '../src/utils/pusher'; import debounceAction from '../src/utils/debounceAction'; @@ -67,7 +66,6 @@ export default class Chat extends Component { activeVideoChannelId: null, incomingVideoCallChannelIds: [], videoCallParticipants: [], - nonChatView: null, inviteChannels: [], soundOn: true, videoOn: true, @@ -676,7 +674,12 @@ export default class Chat extends Component { }; triggerSwitchChannel = (id, slug) => { - const { chatChannels, isMobileDevice, unopenedChannelIds } = this.state; + const { + chatChannels, + isMobileDevice, + unopenedChannelIds, + activeChannelId, + } = this.state; const newUnopenedChannelIds = unopenedChannelIds; const index = newUnopenedChannelIds.indexOf(id); if (index > -1) { @@ -692,6 +695,17 @@ export default class Chat extends Component { ), }); this.setupChannel(id); + const params = new URLSearchParams(window.location.search); + + if (params.get('ref') === 'group_invite') { + this.setActiveContentState(activeChannelId, { + type_of: 'loading-post', + }); + this.setActiveContent({ + path: '/chat_channel_memberships', + type_of: 'article', + }); + } window.history.replaceState(null, null, `/connect/${slug}`); if (!isMobileDevice) { document.getElementById('messageform').focus(); @@ -765,7 +779,6 @@ export default class Chat extends Component { ) { return false; } - const { target } = e; const content = target.dataset.content || target.parentElement.dataset.content; @@ -773,7 +786,7 @@ export default class Chat extends Component { e.preventDefault(); e.stopPropagation(); - const { activeChannelId } = this.state; + const { activeChannelId, activeChannel } = this.state; if (target.dataset.content.startsWith('chat_channels/')) { this.setActiveContentState(activeChannelId, { type_of: 'loading-user', @@ -783,6 +796,14 @@ export default class Chat extends Component { this.setActiveContent, null, ); + } else if (target.dataset.content === 'sidecar_all') { + this.setActiveContentState(activeChannelId, { + type_of: 'loading-post', + }); + this.setActiveContent({ + path: `/chat_channel_memberships/${activeChannel.id}/edit`, + type_of: 'article', + }); } else if ( content.startsWith('sidecar') || content.startsWith('article') @@ -866,14 +887,6 @@ export default class Chat extends Component { getChannels(filterQuery, null, this.props, 0, filters, this.loadChannels); }; - triggerNonChatView = e => { - this.setState({ nonChatView: e.target.dataset.content }); - }; - - triggerExitView = () => { - this.setState({ nonChatView: null }); - }; - handleFailure = err => { // eslint-disable-next-line no-console console.error(err); @@ -1012,13 +1025,17 @@ export default class Chat extends Component { if (state.inviteChannels.length > 0) { invitesButton = (
- +
); } @@ -1273,9 +1290,9 @@ export default class Chat extends Component { let before = text.substring(0, start); before = text.substring(0, before.lastIndexOf('@') + 1); const after = text.substring(end, text.length); - el.value = before + name + after; - el.selectionStart = start + name.length; - el.selectionEnd = start + name.length; + el.value = `${before + name } ${ after}`; + el.selectionStart = start + name.length + 1; + el.selectionEnd = start + name.length + 1; el.focus(); this.setState({ showMemberlist: false }); }; @@ -1441,7 +1458,7 @@ export default class Chat extends Component { }; renderChannelHeaderInner = () => { - const { activeChannel, activeChannelId } = this.state; + const { activeChannel } = this.state; if (activeChannel.channel_type === 'direct') { return ( {activeChannel.channel_name} @@ -1466,7 +1483,6 @@ export default class Chat extends Component { renderChannelConfigImage = () => { const { activeContent, activeChannel, activeChannelId } = this.state; - if ( activeContent[activeChannelId] && activeContent[activeChannelId].type_of @@ -1477,7 +1493,12 @@ export default class Chat extends Component { const dataContent = activeChannel.channel_type === 'direct' ? 'sidecar-user' - : `chat_channels/${activeChannelId}`; + : `sidecar-chat_channel_membership`; + + const path = + activeChannel.channel_type === 'direct' + ? `/${activeChannel.channel_username}` + : `/chat_channel_memberships/${activeChannel.id}/edit`; return ( ); } - let nonChatView = ''; - if (state.nonChatView) { - nonChatView = ( - - ); - } return (
{vid} - {nonChatView} {this.renderActiveChatChannel(channelHeader, incomingCall)}
diff --git a/app/javascript/chat/codeEditor.jsx b/app/javascript/chat/codeEditor.jsx deleted file mode 100644 index 30bba683f..000000000 --- a/app/javascript/chat/codeEditor.jsx +++ /dev/null @@ -1,49 +0,0 @@ -import { h, Component } from 'preact'; -import PropTypes from 'prop-types'; - -export default class CodeEditor extends Component { - static propTypes = { - activeChannelId: PropTypes.string.isRequired, - }; - - componentDidMount() { - const { activeChannelId } = this.props; - import('codemirror').then(CodeMirror => { - const editor = document.getElementById('codeeditor'); - const myCodeMirror = CodeMirror(editor, { - mode: 'javascript', - theme: 'material', - autofocus: true, - }); - myCodeMirror.setSize('100%', '100%'); - // Initial trigger: - const channel = window.pusher.channel( - `presence-channel-${activeChannelId}`, - ); - channel.trigger('client-livecode', { - context: 'initializing-live-code-channel', - channel: `presence-channel-${activeChannelId}`, - }); - // Coding trigger: - myCodeMirror.on('keyup', cm => { - channel.trigger('client-livecode', { - keyPressed: true, - value: cm.getValue(), - cursorPos: cm.getCursor(), - }); - }); - }); - } - - shouldComponentUpdate() { - return false; - } - - render() { - return ( -
-
Experimental (WIP)
-
- ); - } -} diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx index 4c39b67f4..8cdb2794b 100644 --- a/app/javascript/chat/content.jsx +++ b/app/javascript/chat/content.jsx @@ -1,11 +1,8 @@ import { h, Component } from 'preact'; import PropTypes from 'prop-types'; -import CodeEditor from './codeEditor'; -import GithubRepo from './githubRepo'; -import ChannelDetails from './channelDetails'; -import UserDetails from './userDetails'; import Article from './article'; + export default class Content extends Component { static propTypes = { resource: PropTypes.object, @@ -50,55 +47,7 @@ function display(props) { /> ); } - if (props.resource.type_of === 'loading-user') { - return ( -
- ); - } - if (props.resource.type_of === 'user') { - return ( - - ); - } if (props.resource.type_of === 'article') { return
; } - if (props.resource.type_of === 'github') { - return ( - - ); - } - if (props.resource.type_of === 'chat_channel') { - return ( - - ); - } - if (props.resource.type_of === 'code_editor') { - return ( - - ); - } } diff --git a/app/javascript/chat/githubRepo.jsx b/app/javascript/chat/githubRepo.jsx deleted file mode 100644 index 2d2881ff6..000000000 --- a/app/javascript/chat/githubRepo.jsx +++ /dev/null @@ -1,185 +0,0 @@ -import { h, Component } from 'preact'; -import PropTypes from 'prop-types'; -import marked from 'marked'; -import { getJSONContents } from './actions'; - -export default class GithubRepo extends Component { - static propTypes = { - githubToken: PropTypes.string.isRequired, - resource: PropTypes.shape({ - args: PropTypes.string, - }).isRequired, - }; - - constructor(props) { - super(props); - this.state = { - root: true, - directories: [], - files: [], - readme: null, - content: null, - token: props.githubToken, - path: null, - }; - } - - componentDidMount() { - const { token } = this.state; - const { resource } = this.props; - if (token) { - getJSONContents( - `https://api.github.com/repos/${resource.args}/contents?access_token=${token}`, - this.loadContent, - this.loadFailure, - ); - getJSONContents( - `https://api.github.com/repos/${resource.args}/readme?access_token=${token}`, - this.loadContent, - this.loadFailure, - ); - } - this.setState({ path: resource.args }); - } - - handleItemClick = e => { - const { token } = this.state; - e.preventDefault(); - getJSONContents( - `${e.target.dataset.apiUrl}&access_token=${token}`, - this.loadContent, - this.loadFailure, - ); - this.setState({ - root: false, - path: e.target.dataset.path, - }); - }; - - loadContent = response => { - const files = []; - const directories = []; - if (response.message === 'Not Found') { - this.setState({ path: 'Repo not found (misspelled or private?)' }); - } else if (Array.isArray(response)) { - response.forEach(item => { - if (item.type === 'file') { - files.push(item); - } else { - directories.push(item); - } - }); - this.setState({ - files, - directories, - }); - } else if (response.path === 'README.md') { - this.setState({ - readme: window.atob(response.content), - }); - } else if (response.content) { - this.setState({ - content: window.atob(response.content), - }); - } - }; - - loadFailure = response => { - this.setState({ path: response.message }); - }; - - render() { - const { - token, - content, - path, - directories: directoriesFromState, - files: filesFromState, - readme: readmeFromState, - root, - } = this.state; - if (!token || token.length === 0) { - return ( -
-
- Authentication required -
-

This feature is in internal alpha testing mode.

-
- ); - } - if (content) { - return ( -
-
{path}
-
{content}
-
- ); - } - const directories = directoriesFromState.map(item => ( -
- )); - const files = filesFromState.map(item => ( - - )); - let readme = ''; - if (readmeFromState) { - readme = ( -
- ); - } - if (root) { - return ( -
-
- {/* eslint-disable-next-line jsx-a11y/anchor-has-content */} - - {path} -
-
- {directories} - {files} -
- {readme} -
- ); - } - return ( -
-
{path}
-
- {directories} - {files} -
-
- ); - } -} diff --git a/app/javascript/chat/userDetails.jsx b/app/javascript/chat/userDetails.jsx deleted file mode 100644 index d34b85045..000000000 --- a/app/javascript/chat/userDetails.jsx +++ /dev/null @@ -1,271 +0,0 @@ -import { h } from 'preact'; -import { PropTypes } from 'prop-types'; -// eslint-disable-next-line import/no-unresolved -import twitterImage from 'images/twitter-logo.svg'; -// eslint-disable-next-line import/no-unresolved -import githubImage from 'images/github-logo.svg'; -// eslint-disable-next-line import/no-unresolved -import websiteImage from 'images/external-link-logo.svg'; - -function blockUser(blockedUserId) { - const body = { - user_block: { - blocked_id: blockedUserId, - }, - }; - - getCsrfToken().then(sendFetch('block-user', JSON.stringify(body))); -} - -const setUpButton = ({ modalId = '', otherModalId = '', btnName = '' }) => { - return ( - - ); -}; - -setUpButton.propTypes = { - modalId: PropTypes.string.isRequired, - otherModalId: PropTypes.string.isRequired, - btnName: PropTypes.string.isRequired, -}; - -const userDetailsConfig = { - twitter_username: { - hostUrl: 'https://twitter.com/', - srcImage: twitterImage, - imageAltText: 'twitter logo', - }, - github_username: { - hostUrl: 'https://github.com/', - srcImage: githubImage, - imageAltText: 'github logo', - }, - website_url: { - className: 'external-link-img', - hostUrl: '', - srcImage: websiteImage, - imageAltText: 'external link icon', - }, -}; - -const UserDetails = ({ user, activeChannelId, activeChannel }) => { - const channelId = activeChannelId; - const channel = activeChannel || {}; - const socialIcons = []; - const userMeta = ['twitter_username', 'github_username', 'website_url']; - userMeta.forEach(metaProp => { - if (user[metaProp]) { - const { className, hostUrl, srcImage, imageAltText } = userDetailsConfig[ - metaProp - ]; - socialIcons.push( -
- {imageAltText} - , - ); - } - }); - let userLocation = ''; - if (user.location && user.location.length > 0) { - userLocation = ( -
-
location
-
{user.location}
-
- ); - } - let blockButton = ''; - if (channel.channel_type === 'direct' && window.currentUser.id !== user.id) { - blockButton = setUpButton({ - modalId: 'userdetails__blockmsg', - otherModalId: 'userdetails__reportabuse', - btnName: 'Block User', - }); - } - - let reportButton = ''; - if (window.currentUser.id !== user.id) { - reportButton = setUpButton({ - modalId: 'userdetails__reportabuse', - otherModalId: 'userdetails__blockmsg', - btnName: 'Report Abuse', - }); - } - - return ( -
- {`${user.username} -

- - {user.name} - -

-
- {socialIcons} -
-
{user.summary}
-
- {userLocation} -
joined
-
{user.joined_at}
-
-
- {blockButton} - {reportButton} -
-
-
-

Reporting abuse will:

-
    -
  • - close this chat and prevent this user from re-opening chat with - you -
  • -
  • - give the DEV team your consent to read messages in this chat to - understand your report and take appropriate action -
  • -
-

- Blocking is only on Connect right now and has not been implemented - across DEV yet. -

-
Are you sure?
- { - blockUser(channelId); - }} - > - Yes, Report - - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - document.getElementById( - 'userdetails__reportabuse', - ).style.display = 'none'; - window.location.href = `#`; - }} - onKeyUp={e => { - if (e.keyCode === 13) { - document.getElementById( - 'userdetails__reportabuse', - ).style.display = 'none'; - window.location.href = `#`; - } - }} - > - No - -
-
-
-
-

Blocking on connect will:

-
    -
  • - close this chat and prevent this user from re-opening chat with - you -
  • -
  • - NOT notify the user you will block--this channel will become - inaccessible for both users -
  • -
-

- Blocking is only on Connect right now and has not been implemented - across DEV yet. Consider reporting abuse to the DEV team if this - user is spamming or harassing elsewhere on dev.to, so we can take - further action. -

-
Are you sure?
- {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - blockUser(user.id); - window.location.href = `/connect`; - }} - onKeyUp={e => { - if (e.keyCode === 13) { - blockUser(user.id); - window.location.href = `/connect`; - } - }} - > - Yes, Block - - {/* eslint-disable-next-line jsx-a11y/anchor-is-valid */} - { - document.getElementById('userdetails__blockmsg').style.display = - 'none'; - window.location.href = `#`; - }} - onKeyUp={e => { - if (e.keyCode === 13) { - document.getElementById('userdetails__blockmsg').style.display = - 'none'; - window.location.href = `#`; - } - }} - > - No - -
-
-
- ); -}; - -UserDetails.propTypes = { - user: PropTypes.objectOf().isRequired, - activeChannelId: PropTypes.string.isRequired, - activeChannel: PropTypes.objectOf().isRequired, -}; - -export default UserDetails; diff --git a/app/javascript/chat/view.jsx b/app/javascript/chat/view.jsx deleted file mode 100644 index e282a9a63..000000000 --- a/app/javascript/chat/view.jsx +++ /dev/null @@ -1,85 +0,0 @@ -import { h, Component } from 'preact'; -import PropTypes from 'prop-types'; - -/** - * TODO: Instead of calling this function in render, use jsx (). - */ -function SetupButton({ className, onClickCallback, dataContent, btnLabel }) { - return ( - - ); -} - -SetupButton.propTypes = { - className: PropTypes.string.isRequired, - onClickCallback: PropTypes.func.isRequired, - dataContent: PropTypes.string.isRequired, - btnLabel: PropTypes.string.isRequired, -}; - -export default class View extends Component { - static propTypes = { - onAcceptInvitation: PropTypes.func.isRequired, - onDeclineInvitation: PropTypes.func.isRequired, - onViewExit: PropTypes.func.isRequired, - channels: PropTypes.arrayOf(PropTypes.object).isRequired, - }; - - channel = props => { - const { onAcceptInvitation, onDeclineInvitation } = this.props; - return ( -
-

{props.channel.channel_name}

-
- {props.channel.description} -
- {SetupButton({ - className: 'cta', - onClickCallback: onAcceptInvitation, - dataContent: props.channel.membership_id, - btnLabel: 'Accept', - })} - {SetupButton({ - className: 'cta', - onClickCallback: onDeclineInvitation, - dataContent: props.channel.membership_id, - btnLabel: 'Decline', - })} -
- ); - }; - - render() { - const { onViewExit, channels: channelsFromProps } = this.props; - const channels = channelsFromProps.map(channel => { - return ; - }); - return ( -
-
- {SetupButton({ - className: 'chatNonChatView_exitbutton', - onClickCallback: onViewExit, - dataContent: 'exit', - btnLabel: 'Γ—', - })} -

- Channel Invitations - {' '} - - πŸ€— - -

- {channels} -
-
- ); - } -} diff --git a/app/labor/assign_tag_moderator.rb b/app/labor/assign_tag_moderator.rb index 7f2e13927..832bf34ee 100644 --- a/app/labor/assign_tag_moderator.rb +++ b/app/labor/assign_tag_moderator.rb @@ -26,7 +26,11 @@ module AssignTagModerator if tag.mod_chat_channel_id ChatChannel.find(tag.mod_chat_channel_id).add_users(user) if user.chat_channels.where(id: tag.mod_chat_channel_id).none? else - channel = ChatChannel.create_with_users(([user] + User.with_role(:mod_relations_admin)).flatten.uniq, "invite_only", "##{tag.name} mods") + channel = ChatChannel.create_with_users( + users: ([user] + User.with_role(:mod_relations_admin)).flatten.uniq, + channel_type: "invite_only", + contrived_name: "##{tag.name} mods", + ) tag.update_column(:mod_chat_channel_id, channel.id) end end diff --git a/app/mailers/notify_mailer.rb b/app/mailers/notify_mailer.rb index ce1b820a8..ba6940951 100644 --- a/app/mailers/notify_mailer.rb +++ b/app/mailers/notify_mailer.rb @@ -81,6 +81,17 @@ class NotifyMailer < ApplicationMailer mail(to: @user.email, subject: subject) end + def channel_invite_email(membership, inviter) + @membership = membership + @inviter = inviter + subject = if @membership.role == "mod" + "You are invited to Channel #{@membership.chat_channel.channel_name} as moderator." + else + "You are invited to Channel #{@membership.chat_channel.channel_name} by #{@inviter.name}." + end + mail(to: @membership.user.email, subject: subject) + end + def account_deleted_email(user) @name = user.name subject = "dev.to - Account Deletion Confirmation" diff --git a/app/models/chat_channel.rb b/app/models/chat_channel.rb index b0120d134..a8391b19f 100644 --- a/app/models/chat_channel.rb +++ b/app/models/chat_channel.rb @@ -17,6 +17,7 @@ class ChatChannel < ApplicationRecord validates :channel_type, presence: true, inclusion: { in: %w[open invite_only direct] } validates :status, presence: true, inclusion: { in: %w[active inactive blocked] } validates :slug, uniqueness: true, presence: true + validates :description, length: { maximum: 200 }, allow_blank: true def open? channel_type == "open" @@ -51,34 +52,41 @@ class ChatChannel < ApplicationRecord chat_channel_memberships.where(user_id: user.id).pluck(:last_opened_at).first end - def self.create_with_users(users, channel_type = "direct", contrived_name = "New Channel") - raise "Invalid direct channel" if users.size != 2 && channel_type == "direct" + class << self + def create_with_users(users:, channel_type: "direct", contrived_name: "New Channel", membership_role: "member") + raise "Invalid direct channel" if users.size != 2 && channel_type == "direct" - if channel_type == "direct" - usernames = users.map(&:username).sort # .map as `users` is an array - contrived_name = "Direct chat between " + usernames.join(" and ") - slug = usernames.join("/") - else - slug = contrived_name.to_s.parameterize + "-" + rand(100_000).to_s(26) + usernames = users.map(&:username).sort + slug = channel_type == "direct" ? usernames.join("/") : contrived_name.to_s.parameterize + "-" + rand(100_000).to_s(26) + contrived_name = "Direct chat between " + usernames.join(" and ") if channel_type == "direct" + channel = find_or_create_chat_channel(channel_type, slug, contrived_name) + if channel_type == "direct" + channel.add_users(users) + else + channel.invite_users(users: users, membership_role: membership_role) + end + channel end - channel = ChatChannel.find_by(slug: slug) - if channel - raise "Blocked channel" if channel.status == "blocked" + private - channel.status = "active" - channel.save - else - channel = create( - channel_type: channel_type, - channel_name: contrived_name, - slug: slug, - last_message_at: 1.week.ago, - status: "active", - ) - channel.add_users(users) + def find_or_create_chat_channel(channel_type, slug, contrived_name) + channel = ChatChannel.find_by(slug: slug) + if channel + raise "Blocked channel" if channel.status == "blocked" + + channel.status = "active" + channel.save + else + create( + channel_type: channel_type, + channel_name: contrived_name, + slug: slug, + last_message_at: 1.week.ago, + status: "active", + ) + end end - channel end def add_users(users) @@ -87,6 +95,26 @@ class ChatChannel < ApplicationRecord end end + def invite_users(users:, membership_role: "member", inviter: nil) + invitation_sent = 0 + Array(users).each do |user| + existing_membership = ChatChannelMembership.find_by(user_id: user.id, chat_channel_id: id) + if existing_membership.present? && %w[active pending].exclude?(existing_membership.status) + if existing_membership.update(status: "pending", role: membership_role) + NotifyMailer.channel_invite_email(existing_membership, inviter).deliver_later + invitation_sent += 1 + end + else + membership = ChatChannelMembership.create(user_id: user.id, chat_channel_id: id, role: membership_role, status: "pending") + if membership.persisted? + NotifyMailer.channel_invite_email(membership, inviter).deliver_later + invitation_sent += 1 + end + end + end + invitation_sent + end + def remove_user(user) chat_channel_memberships.where(user: user).destroy_all end diff --git a/app/models/chat_channel_membership.rb b/app/models/chat_channel_membership.rb index 5efe16e11..56e4c4cd7 100644 --- a/app/models/chat_channel_membership.rb +++ b/app/models/chat_channel_membership.rb @@ -1,4 +1,6 @@ class ChatChannelMembership < ApplicationRecord + attr_accessor :invitation_usernames + include Searchable SEARCH_SERIALIZER = Search::ChatChannelMembershipSerializer SEARCH_CLASS = Search::ChatChannelMembership @@ -8,7 +10,7 @@ class ChatChannelMembership < ApplicationRecord validates :user_id, presence: true, uniqueness: { scope: :chat_channel_id } validates :chat_channel_id, presence: true, uniqueness: { scope: :user_id } - validates :status, inclusion: { in: %w[active inactive pending rejected left_channel] } + validates :status, inclusion: { in: %w[active inactive pending rejected left_channel removed_from_channel] } validates :role, inclusion: { in: %w[member mod] } validate :permission diff --git a/app/models/message.rb b/app/models/message.rb index 4aeb6f1f9..5f1dcf597 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -82,7 +82,7 @@ class Message < ApplicationRecord HTML elsif username == "all" && chat_channel.channel_type == "invite_only" <<~HTML - @#{username} + @#{username} HTML else mention diff --git a/app/policies/chat_channel_policy.rb b/app/policies/chat_channel_policy.rb index 25d8fde14..f2b9c0e30 100644 --- a/app/policies/chat_channel_policy.rb +++ b/app/policies/chat_channel_policy.rb @@ -24,7 +24,7 @@ class ChatChannelPolicy < ApplicationPolicy end def permitted_attributes - %i[channel_name slug command] + %i[channel_name slug command description] end def create_chat? diff --git a/app/views/chat_channel_memberships/edit.html.erb b/app/views/chat_channel_memberships/edit.html.erb new file mode 100644 index 000000000..ae2fb7be6 --- /dev/null +++ b/app/views/chat_channel_memberships/edit.html.erb @@ -0,0 +1,99 @@ +<% if flash[:settings_notice].present? %> +
+ <%= flash[:settings_notice] %> +
+<% elsif flash[:error].present? %> +
+ ERROR + <%= flash[:error] %> +
+<% end %> + diff --git a/app/views/chat_channel_memberships/index.html.erb b/app/views/chat_channel_memberships/index.html.erb new file mode 100644 index 000000000..89641393b --- /dev/null +++ b/app/views/chat_channel_memberships/index.html.erb @@ -0,0 +1,30 @@ +<% if flash[:settings_notice] %> +
+ <%= flash[:settings_notice] %> +
+<% end %> + \ No newline at end of file diff --git a/app/views/mailers/notify_mailer/channel_invite_email.html.erb b/app/views/mailers/notify_mailer/channel_invite_email.html.erb new file mode 100644 index 000000000..d9f0785f4 --- /dev/null +++ b/app/views/mailers/notify_mailer/channel_invite_email.html.erb @@ -0,0 +1,45 @@ + + + + + + +
+ <% if @membership.role == "mod" %> +

+ You have been invited to be a Moderator of a connect group on <%= ApplicationConfig["COMMUNITY_NAME"] %>. +

+ <% else %> +

+ <%= link_to(@inviter.name, ApplicationController.helpers.user_url(@inviter)) %> sent you an invitation to join a connect group on <%= ApplicationConfig["COMMUNITY_NAME"] %>. +

+ <% end %> + + + + + + + + + +
+
+
+
+

<%= @membership.chat_channel.channel_name %>

+
+
+ <% if @membership.role == "mod" %> + + <% else %> + + <% end %> +
+
+
+

<%= link_to("Check Invitation", ApplicationController.helpers.app_url("connect?ref=group_invite")) %>

+
+
+
+
diff --git a/app/views/mailers/notify_mailer/channel_invite_email.text.erb b/app/views/mailers/notify_mailer/channel_invite_email.text.erb new file mode 100644 index 000000000..50fc5c0b0 --- /dev/null +++ b/app/views/mailers/notify_mailer/channel_invite_email.text.erb @@ -0,0 +1,3 @@ +You are invited to the <%= ApplicationConfig["COMMUNITY_NAME"] %> Connect channel <%= @membership.chat_channel.channel_name %> + +View Invitation: <%= ApplicationController.helpers.app_url('connect') %> diff --git a/app/workers/follows/create_chat_channel_worker.rb b/app/workers/follows/create_chat_channel_worker.rb index d596e9e86..4623cf37f 100644 --- a/app/workers/follows/create_chat_channel_worker.rb +++ b/app/workers/follows/create_chat_channel_worker.rb @@ -9,7 +9,7 @@ module Follows find_by(id: follow_id, follower_type: "User", followable_type: "User") return unless follow&.followable&.following?(follow.follower) - ChatChannel.create_with_users([follow.followable, follow.follower]) + ChatChannel.create_with_users(users: [follow.followable, follow.follower]) end end end diff --git a/config/routes.rb b/config/routes.rb index ce5a50f53..25a8d34cc 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -151,7 +151,7 @@ Rails.application.routes.draw do resources :messages, only: [:create] resources :chat_channels, only: %i[index show create update] - resources :chat_channel_memberships, only: %i[create update destroy] + resources :chat_channel_memberships, only: %i[index create edit update destroy] resources :articles, only: %i[update create destroy] resources :article_mutes, only: %i[update] resources :comments, only: %i[create update destroy] do @@ -251,6 +251,7 @@ Rails.application.routes.draw do get "/chat_channels/:id/channel_info", to: "chat_channels#channel_info", as: :chat_channel_info post "/chat_channels/create_chat" => "chat_channels#create_chat" post "/chat_channels/block_chat" => "chat_channels#block_chat" + post "/chat_channel_memberships/remove_membership" => "chat_channel_memberships#remove_membership" delete "/messages/:id" => "messages#destroy" patch "/messages/:id" => "messages#update" get "/live/:username" => "twitch_live_streams#show" diff --git a/db/seeds.rb b/db/seeds.rb index 5b87a757c..9bc6f840c 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -294,7 +294,7 @@ Rails.logger.info "#{counter}. Creating Chat Channels and Messages" ) end -direct_channel = ChatChannel.create_with_users(User.last(2), "direct") +direct_channel = ChatChannel.create_with_users(users: User.last(2), channel_type: "direct") Message.create!( chat_channel: direct_channel, user: User.last, diff --git a/package.json b/package.json index 6826450a3..5f711eaab 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,6 @@ "babel-preset-preact": "^2.0.0", "chart.js": "^2.9.3", "clipboard-polyfill": "^2.8.6", - "codemirror": "^5.52.2", "core-js": "3", "file-loader": "^6.0.0", "focus-visible": "^5.0.2", diff --git a/spec/mailers/notify_mailer_spec.rb b/spec/mailers/notify_mailer_spec.rb index b0ffd61bd..a8c81d03c 100644 --- a/spec/mailers/notify_mailer_spec.rb +++ b/spec/mailers/notify_mailer_spec.rb @@ -261,7 +261,7 @@ RSpec.describe NotifyMailer, type: :mailer do end describe "#new_message_email" do - let(:direct_channel) { ChatChannel.create_with_users([user, user2], "direct") } + let(:direct_channel) { ChatChannel.create_with_users(users: [user, user2], channel_type: "direct") } let(:direct_message) { create(:message, user: user, chat_channel: direct_channel) } let(:email) { described_class.new_message_email(direct_message) } @@ -407,4 +407,28 @@ RSpec.describe NotifyMailer, type: :mailer do expect(email.html_part.body).to include(CGI.escape("utm_campaign=trusted_role_email")) end end + + describe "#channel_invite_email" do + let(:moderator_membership) { create(:chat_channel_membership, user_id: user2.id, role: "mod") } + let(:regular_membership) { create(:chat_channel_membership, user_id: user2.id, role: "member") } + let(:moderator_email) { described_class.channel_invite_email(moderator_membership, nil) } + let(:member_email) { described_class.channel_invite_email(regular_membership, user) } + + it "renders proper subject" do + expect(moderator_email.subject).to eq("You are invited to Channel #{moderator_membership.chat_channel.channel_name} as moderator.") + expect(member_email.subject).to eq("You are invited to Channel #{regular_membership.chat_channel.channel_name} by #{user.name}.") + end + + it "renders proper sender" do + expect(moderator_email.from).to eq([SiteConfig.default_site_email]) + expect(moderator_email["from"].value).to eq("DEV Community <#{SiteConfig.default_site_email}>") + expect(member_email.from).to eq([SiteConfig.default_site_email]) + expect(member_email["from"].value).to eq("DEV Community <#{SiteConfig.default_site_email}>") + end + + it "renders proper receiver" do + expect(moderator_email.to).to eq([user2.email]) + expect(member_email.to).to eq([user2.email]) + end + end end diff --git a/spec/models/chat_channel_spec.rb b/spec/models/chat_channel_spec.rb index ccc02e705..8e97e7925 100644 --- a/spec/models/chat_channel_spec.rb +++ b/spec/models/chat_channel_spec.rb @@ -24,14 +24,14 @@ RSpec.describe ChatChannel, type: :model do describe "#create_with_users" do it "creates channel with users" do - chat_channel = described_class.create_with_users(users) + chat_channel = described_class.create_with_users(users: users) expect(chat_channel.users.size).to eq(users.size) expect(chat_channel.has_member?(users.first)).to be(true) expect(chat_channel.has_member?(users.last)).to be(true) end it "lists active memberships" do - chat_channel = described_class.create_with_users(users) + chat_channel = described_class.create_with_users(users: users) expect(chat_channel.active_users.size).to eq(users.size) expect(chat_channel.channel_users.size).to eq(users.size) end @@ -39,7 +39,7 @@ RSpec.describe ChatChannel, type: :model do describe "#active_users" do it "decreases active users if one leaves" do - chat_channel = described_class.create_with_users(users) + chat_channel = described_class.create_with_users(users: users) expect(chat_channel.active_users.size).to eq(users.size) expect(chat_channel.channel_users.size).to eq(users.size) ChatChannelMembership.last.update(status: "left_channel") diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb index 093c71b3b..0cb75cb8b 100644 --- a/spec/requests/chat_channel_memberships_spec.rb +++ b/spec/requests/chat_channel_memberships_spec.rb @@ -10,27 +10,166 @@ RSpec.describe "ChatChannelMemberships", type: :request do chat_channel.add_users([user]) end - describe "POST /chat_channel_memberships" do - it "creates chat channel invitation" do - user.add_role(:super_admin) - mems_num = ChatChannelMembership.all.size - post "/chat_channel_memberships", params: { - chat_channel_membership: { - user_id: second_user.id, chat_channel_id: chat_channel.id - } - } - expect(ChatChannelMembership.all.size).to eq(mems_num + 1) - expect(ChatChannelMembership.last.status).to eq("pending") - end - - it "denies chat channel invitation to non-authorized user" do - expect do + describe "GET /chat_channel_memberships" do + context "when pending invitations exists" do + before do + user.add_role(:super_admin) post "/chat_channel_memberships", params: { chat_channel_membership: { - user_id: second_user.id, chat_channel_id: chat_channel.id + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id } } - end.to raise_error(Pundit::NotAuthorizedError) + end + + it "shows chat_channel_memberships list pending invitation" do + sign_in second_user + get "/chat_channel_memberships" + expect(response.body).to include "Pending Invitations" + expect(response.body).to include chat_channel.channel_name.to_s + end + end + + context "when no pending invitation" do + it "shows chat_channel_memberships list pending invitation" do + sign_in second_user + get "/chat_channel_memberships" + expect(response.body).to include "You have no pending invitations" + end + end + end + + describe "GET /chat_channel_memberships/find_by_chat_channel_id" do + context "when user is logged in" do + before do + chat_channel.add_users([second_user]) + end + + it "returns chat channel membership details" do + sign_in second_user + get "/chat_channel_memberships/find_by_chat_channel_id", params: { chat_channel_id: chat_channel.id } + expected_keys = %w[id status chat_channel_id last_opened_at channel_text + channel_last_message_at channel_status channel_username + channel_type channel_name channel_image + channel_modified_slug channel_messages_count] + expect(response.parsed_body.keys).to(match_array(expected_keys)) + end + end + + context "when user is not logged in" do + it "renders not_found" do + expect do + get "/chat_channel_memberships/find_by_chat_channel_id", params: {} + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + end + + describe "GET /chat_channel_memberships/:id/edit" do + before do + chat_channel.add_users([second_user]) + end + + let(:chat_channel_membership) { chat_channel.chat_channel_memberships.where(user_id: second_user.id).first } + + context "when user is not logged in" do + it "raise Pundit::NotAuthorizedError" do + expect do + get "/chat_channel_memberships/#{chat_channel_membership.id}/edit" + end.to raise_error(Pundit::NotAuthorizedError) + end + end + + context "when user is logged in and channel id is wrong" do + it "raise ActiveRecord::RecordNotFound" do + sign_in second_user + expect do + get "/chat_channel_memberships/ERW/edit" + end.to raise_error(ActiveRecord::RecordNotFound) + end + end + + context "when user is channel member" do + it "allows user to view channel members" do + sign_in second_user + get "/chat_channel_memberships/#{chat_channel_membership.id}/edit" + expect(response.body).to include("Members") + expect(response.body).to include(user.username.to_s) + expect(response.body).to include(second_user.username.to_s) + expect(response.body).not_to include("Pending Invitations") + end + end + + context "when user is channel moderator" do + it "allows user to view channel members" do + sign_in second_user + chat_channel_membership.update(role: "mod") + get "/chat_channel_memberships/#{chat_channel_membership.id}/edit" + expect(response.body).to include("Members") + expect(response.body).to include(user.username.to_s) + expect(response.body).to include(second_user.username.to_s) + expect(response.body).to include("Pending Invitations") + expect(response.body).to include("You are a channel mod") + end + end + end + + describe "POST /chat_channel_memberships" do + context "when user is super admin" do + it "creates chat channel invitation" do + user.add_role(:super_admin) + expect do + post "/chat_channel_memberships", params: { + chat_channel_membership: { + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id + } + } + end.to change { ChatChannelMembership.all.size }.by(1) + expect(ChatChannelMembership.last.status).to eq("pending") + end + end + + context "when user is channel moderator, and invited user was a member of channel, and than left channel" do + it "creates chat channel invitation" do + chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod") + ChatChannelMembership.create(chat_channel_id: chat_channel.id, user_id: second_user.id, status: "left_channel") + post "/chat_channel_memberships", params: { + chat_channel_membership: { + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id + } + } + expect(ChatChannelMembership.last.status).to eq("pending") + end + end + + context "when user is channel moderator, invited user was not a member of channel" do + it "creates chat channel invitation" do + chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod") + chat_channel_members_count = ChatChannelMembership.all.size + post "/chat_channel_memberships", params: { + chat_channel_membership: { + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id + } + } + expect(ChatChannelMembership.all.size).to eq(chat_channel_members_count + 1) + expect(ChatChannelMembership.last.status).to eq("pending") + end + end + + context "when user is not authorized to add channel membership" do + it "raise Pundit::NotAuthorizedError" do + expect do + post "/chat_channel_memberships", params: { + chat_channel_membership: { + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id + } + } + end.to raise_error(Pundit::NotAuthorizedError) + end end end @@ -38,62 +177,130 @@ RSpec.describe "ChatChannelMemberships", type: :request do before do user.add_role(:super_admin) post "/chat_channel_memberships", params: { - chat_channel_membership: { user_id: second_user.id, chat_channel_id: chat_channel.id } - } - end - - it "accepts chat channel invitation" do - membership = ChatChannelMembership.last - sign_in second_user - put "/chat_channel_memberships/#{membership.id}", params: { chat_channel_membership: { - user_action: "accept" + invitation_usernames: second_user.username.to_s, + chat_channel_id: chat_channel.id } } - expect(ChatChannelMembership.find(membership.id).status).to eq("active") end - it "rejects chat channel invitation" do - membership = ChatChannelMembership.last - sign_in second_user - put "/chat_channel_memberships/#{membership.id}", params: { - chat_channel_membership: { user_action: "reject" } - } - expect(ChatChannelMembership.find(membership.id).status).to eq("rejected") - end - - it "disallows non-logged-user" do - membership = ChatChannelMembership.last - expect do + context "when second user accept invitation" do + it "sets chat channl membership status to rejected" do + membership = ChatChannelMembership.last + sign_in second_user put "/chat_channel_memberships/#{membership.id}", params: { - chat_channel_membership: { user_action: "accept" } + chat_channel_membership: { + user_action: "accept" + } } expect(ChatChannelMembership.find(membership.id).status).to eq("active") - end.to raise_error(Pundit::NotAuthorizedError) + expect(response).to(redirect_to(chat_channel_memberships_path)) + end + end + + context "when second user rejects invitation" do + it "sets chat channl membership status to rejected" do + membership = ChatChannelMembership.last + sign_in second_user + put "/chat_channel_memberships/#{membership.id}", params: { + chat_channel_membership: { + user_action: "reject" + } + } + expect(ChatChannelMembership.find(membership.id).status).to eq("rejected") + end + end + + context "when user not logged in" do + it "raise Pundit::NotAuthorizedError" do + membership = ChatChannelMembership.last + expect do + put "/chat_channel_memberships/#{membership.id}", params: { + chat_channel_membership: { user_action: "accept" } + } + end.to raise_error(Pundit::NotAuthorizedError) + end + end + + context "when user is unauthorized" do + it "raise Pundit::NotAuthorizedError" do + membership = ChatChannelMembership.last + sign_in user + expect do + put "/chat_channel_memberships/#{membership.id}", params: { + chat_channel_membership: { user_action: "accept" } + } + end.to raise_error(Pundit::NotAuthorizedError) + end end end describe "DELETE /chat_channel_memberships/:id" do - before do - user.add_role(:super_admin) - post "/chat_channel_memberships", params: { - chat_channel_membership: { user_id: second_user.id, chat_channel_id: chat_channel.id } - } + context "when user is logged in" do + it "leaves chat channel" do + chat_channel.add_users([second_user]) + membership = ChatChannelMembership.last + sign_in second_user + delete "/chat_channel_memberships/#{membership.id}", params: {} + expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel") + expect(response).to(redirect_to(chat_channel_memberships_path)) + end end - it "leaves chat channel" do - membership = ChatChannelMembership.last - sign_in second_user - delete "/chat_channel_memberships/#{membership.chat_channel.id}", params: {} - expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel") + context "when user is not logged in" do + it "raise Pundit::NotAuthorizedError" do + chat_channel.add_users([second_user]) + membership = ChatChannelMembership.last + expect do + delete "/chat_channel_memberships/#{membership.id}", params: {} + end.to(raise_error(Pundit::NotAuthorizedError)) + end end end - describe "GET /chat_channel_memberships/find_by_chat_channel_id" do - it "renders not_found" do - expect do - get "/chat_channel_memberships/find_by_chat_channel_id", params: {} - end.to raise_error(ActiveRecord::RecordNotFound) + describe "POST /chat_channel_memberships/remove_membership" do + before do + chat_channel.add_users([second_user]) + end + + context "when user is super admin" do + it "removes member from channel" do + user.add_role(:super_admin) + membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last + removed_channel_membership = ChatChannelMembership.last + post "/chat_channel_memberships/remove_membership", params: { + chat_channel_id: chat_channel.id, + membership_id: removed_channel_membership.id + } + expect(removed_channel_membership.reload.status).to eq("removed_from_channel") + expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id))) + end + end + + context "when user is moderator of channel" do + it "removes member from channel" do + membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last + membership.update(role: "mod") + removed_channel_membership = ChatChannelMembership.last + post "/chat_channel_memberships/remove_membership", params: { + chat_channel_id: chat_channel.id, + membership_id: removed_channel_membership.id + } + expect(removed_channel_membership.reload.status).to eq("removed_from_channel") + expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id))) + end + end + + context "when user is member of channel" do + it "raise Pundit::NotAuthorizedError" do + membership = ChatChannelMembership.last + expect do + post "/chat_channel_memberships/remove_membership", params: { + chat_channel_id: chat_channel.id, + membership_id: membership.id + } + end.to(raise_error(Pundit::NotAuthorizedError)) + end end end end diff --git a/spec/requests/chat_channels_spec.rb b/spec/requests/chat_channels_spec.rb index 7a92f2c66..9018268dd 100644 --- a/spec/requests/chat_channels_spec.rb +++ b/spec/requests/chat_channels_spec.rb @@ -14,6 +14,7 @@ RSpec.describe "ChatChannels", type: :request do before do sign_in user chat_channel.add_users([user]) + chat_channel.chat_channel_memberships.update(status: "active") end describe "GET /connect" do @@ -136,10 +137,13 @@ RSpec.describe "ChatChannels", type: :request do describe "PUT /chat_channels/:id" do it "updates channel for valid user" do user.add_role(:super_admin) + membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last + membership.update(role: "mod") put "/chat_channels/#{chat_channel.id}", params: { chat_channel: { channel_name: "Hello Channel", slug: "hello-channelly" } }, headers: { HTTP_ACCEPT: "application/json" } expect(ChatChannel.last.slug).to eq("hello-channelly") + expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id))) end it "dissallows invalid users" do @@ -153,10 +157,12 @@ RSpec.describe "ChatChannels", type: :request do it "returns errors if channel is invalid" do # slug should be taken user.add_role(:super_admin) + membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last + membership.update(role: "mod") put "/chat_channels/#{chat_channel.id}", params: { chat_channel: { channel_name: "HEy hey hoho", slug: invite_channel.slug } }, headers: { HTTP_ACCEPT: "application/json" } - expect(response.body).to include("Slug has already been taken") + expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id))) end end diff --git a/spec/requests/internal/chat_channel_spec.rb b/spec/requests/internal/chat_channel_spec.rb new file mode 100644 index 000000000..1d8103fed --- /dev/null +++ b/spec/requests/internal/chat_channel_spec.rb @@ -0,0 +1,24 @@ +require "rails_helper" + +RSpec.describe "/internal/chat_channels", type: :request do + include ActiveJob::TestHelper + + let(:user) { create(:user) } + let(:chat_channel) { create(:chat_channel) } + + describe "POST /chat_channels" do + around { |example| perform_enqueued_jobs(&example) } + + it "creates chat_channel for with users as moderator" do + user.add_role(:super_admin) + sign_in user + expect do + post "/internal/chat_channels", + params: { chat_channel: { channel_name: "Hello Channel", usernames_string: "#{user.username}" } }, + headers: { HTTP_ACCEPT: "application/json" } + end.to change(ActionMailer::Base.deliveries, :length) + expect(ChatChannel.last.channel_name).to eq("Hello Channel") + expect(ChatChannel.last.pending_users).to include(user) + end + end +end diff --git a/spec/requests/internal/users_manage_spec.rb b/spec/requests/internal/users_manage_spec.rb index 12ec31ae9..efb765de6 100644 --- a/spec/requests/internal/users_manage_spec.rb +++ b/spec/requests/internal/users_manage_spec.rb @@ -48,7 +48,7 @@ RSpec.describe "Internal::Users", type: :request do badge_id: badge.id, rewarding_context_message_markdown: "message", ) - ChatChannel.create_with_users([user2, user3], "direct") + ChatChannel.create_with_users(users: [user2, user3], channel_type: "direct") user2.follow(user3) user.follow(super_admin) user3.follow(user2) diff --git a/spec/services/users/cleanup_chat_channels_spec.rb b/spec/services/users/cleanup_chat_channels_spec.rb index b053eb7b9..64e650867 100644 --- a/spec/services/users/cleanup_chat_channels_spec.rb +++ b/spec/services/users/cleanup_chat_channels_spec.rb @@ -4,10 +4,10 @@ RSpec.describe Users::CleanupChatChannels, type: :service do let(:user) { create(:user) } let(:other_user) { create(:user) } let!(:dm_channel) do - ChatChannel.create_with_users([user, other_user]) + ChatChannel.create_with_users(users: [user, other_user]) end let!(:open_channel) do - ChatChannel.create_with_users([user, other_user], "open") + ChatChannel.create_with_users(users: [user, other_user], channel_type: "open") end it "deletes direct chat channels" do diff --git a/spec/services/users/delete_spec.rb b/spec/services/users/delete_spec.rb index 2f8010c86..91c927d72 100644 --- a/spec/services/users/delete_spec.rb +++ b/spec/services/users/delete_spec.rb @@ -91,13 +91,13 @@ RSpec.describe Users::Delete, type: :service do let_it_be(:other_user) { create(:user) } it "deletes the user's private chat channels" do - chat_channel = ChatChannel.create_with_users([user, other_user]) + chat_channel = ChatChannel.create_with_users(users: [user, other_user]) described_class.call(user) expect(ChatChannel.find_by(id: chat_channel.id)).to be_nil end it "does not delete the user's open channels" do - chat_channel = ChatChannel.create_with_users([user, other_user], "open") + chat_channel = ChatChannel.create_with_users(users: [user, other_user], channel_type: "open") described_class.call(user) expect(ChatChannel.find_by(id: chat_channel.id)).not_to be_nil end diff --git a/spec/workers/moderator/banish_user_worker_spec.rb b/spec/workers/moderator/banish_user_worker_spec.rb index 1756624d4..22b2a8abf 100644 --- a/spec/workers/moderator/banish_user_worker_spec.rb +++ b/spec/workers/moderator/banish_user_worker_spec.rb @@ -12,7 +12,7 @@ RSpec.describe Moderator::BanishUserWorker, type: :worker do create(:article, user_id: user.id) create(:article, user_id: user.id) create(:classified_listing, user: user) - ChatChannel.create_with_users([user, user2]) + ChatChannel.create_with_users(users: [user, user2]) user.follow(user2) described_class.new.perform(admin.id, user.id) user.reload diff --git a/yarn.lock b/yarn.lock index fef7d6d21..d48316788 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4023,11 +4023,6 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemirror@^5.52.2: - version "5.52.2" - resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.52.2.tgz#c29e1f7179f85eb0dd17c0586fa810e4838ff584" - integrity sha512-WCGCixNUck2HGvY8/ZNI1jYfxPG5cRHv0VjmWuNzbtCLz8qYA5d+je4QhSSCtCaagyeOwMi/HmmPTjBgiTm2lQ== - collect-v8-coverage@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz#150ee634ac3650b71d9c985eb7f608942334feb1"