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`] = `
-
-
-
-`;
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)
--------
-
-
-`;
-
-exports[` with no token should render and test snapshot 1`] = `
-
-
-
- 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`] = `
-
-
-
-
-
- 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`] = `
-
-
-
-
-
- Woof Woof *smile*
-
-
-
-
- location
-
-
- Los Angeles, DO(G)
-
-
-
- joined
-
-
- 1960s
-
-
-
-
- Block User
-
-
- Report Abuse
-
-
-
-
-
- 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
-
-
- Accept
-
-
- Decline
-
-
-
-
-
-`;
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 = (
-
+
+ 👋
+
New Invitations!
-
+
);
}
@@ -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 (
-
- );
- }
-}
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 (
-
- );
- }
- 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 (
- {
- const modal = document.getElementById(`${modalId}`);
- const otherModal = document.getElementById(`${otherModalId}`);
- otherModal.style.display = 'none';
- if (modal.style.display === 'none') {
- modal.style.display = 'block';
- window.location.href = `#${modalId}`;
- } else {
- modal.style.display = 'none';
- window.location.href = `#`;
- }
- }}
- >
- {btnName}
-
- );
-};
-
-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(
-
-
- ,
- );
- }
- });
- 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 (
-
-
-
-
- {socialIcons}
-
-
{user.summary}
-
- {userLocation}
-
joined
-
{user.joined_at}
-
-
- {blockButton}
- {reportButton}
-
-
-
-
- );
-};
-
-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 (
-
- {btnLabel}
-
- );
-}
-
-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
-
+
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 %>
+
+
<%= @channel.channel_name %>
+ <% if @membership.role == "mod" %>
+
You are a channel mod
+ <% end %>
+
+ <%= @channel.description %>
+
+
+
+ <% if @membership.role == "mod" %>
+
+ <%= form_for ChatChannelMembership.new do |f| %>
+
+ <%= f.hidden_field :chat_channel_id, value: @channel.id %>
+ <%= f.label "Usernames to Invite" %>
+ <%= f.text_field :invitation_usernames, placeholder: "Comma separated" %>
+
+
+ <%= f.submit "SUBMIT", class: "cta" %>
+
+ <% end %>
+ <% end %>
+ <% if @membership.role == "mod" %>
+
+
Channel Settings
+
Notifications
+ <%= form_for(@channel) do |f| %>
+
+ <%= f.label :description %>
+ <%= f.text_area :description %>
+
+
+ <%= f.submit "SUBMIT", class: "cta" %>
+
+ <% end %>
+ <% end %>
+
+
Personal Settings
+
Notifications
+ <%= form_for(@membership) do |f| %>
+
+ <%= f.label :show_global_badge_notification, "Receive Notifications for New Messages" %>
+ <%= f.check_box :show_global_badge_notification %>
+
+
+ <%= f.submit "SUBMIT", class: "cta" %>
+
+ <% end %>
+
+ <% if @membership.role == "mod" %>
+
Questions about Connect Channel moderation? Contact yo@dev.to
+ <% else %>
+
Danger Zone
+ <%= form_for(@membership, html: { method: :delete, onsubmit: "return confirm('Are you absolutely sure you want to leave this channel? This action is permanent.');" } ) do |f| %>
+
+ <%= f.submit "LEAVE CHANNEL", class: "cta cta-danger" %>
+
+ <% end %>
+ <% 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 %>
+
+ <% if @pending_invites.any? %>
+
Pending Invitations
+
You may leave a group channel at any point in the future.
+ <% @pending_invites.each do |membership| %>
+
+
<%= membership.chat_channel.channel_name %>
+
<%= membership.chat_channel.description %>
+ <%= form_for(membership) do |f| %>
+ <%= f.hidden_field :user_action, value: "accept" %>
+ <%= f.submit "Accept", class: "cta" %>
+ <% end %>
+ <%= form_for(membership) do |f| %>
+ <%= f.hidden_field :user_action, value: "decline" %>
+ <%= f.submit "Decline", class: "cta cta-danger" %>
+ <% end %>
+
+
+ <% end %>
+ <% else %>
+
+ You have no pending invitations
+
+ <% 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"