🚀 Ability to allow tag moderators to create chat channels (#9741)
* Feature 🚀 : Ability to delete messages in chat channels - Sending message ID to frontend - Deleting Message - Use pusher to delete message realtime * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Refactoring🛠: Message controller refactoring * Test Cases📝 : Specs for Delete message added * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Merge conflict resolved * UI completed for Create Channel * API for create channel & invitations * 🚀 Combining Backend and Frontend * Test cases added * UI completed for Create Channel * API for create channel & invitations * 🚀 Combining Backend and Frontend * Test cases added * fix suggestions * add test cases * replace mod_relations_admin to tag_moderator * fix test cases * refactor and fix code climate issues * Update app/controllers/chat_channel_memberships_controller.rb Co-authored-by: Michael Kohl <me@citizen428.net> * fix suggestions * renaming the methods * Update app/javascript/chat/chat.jsx Co-authored-by: Nick Taylor <nick@iamdeveloper.com> * Update app/javascript/chat/components/createChatModal.jsx Co-authored-by: Nick Taylor <nick@iamdeveloper.com> * add jsDoc and add validation * add PR suggestions * renmae the file name to PascalCase * add styled component * refactor channel button component * fix specs * fix code smell issues * fix file name issue * handle error message * add jsDoc * Update app/controllers/chat_channels_controller.rb Co-authored-by: Michael Kohl <me@citizen428.net> * refactor chat_channel_helpers * update old hash to new hash * Update app/javascript/chat/components/ChannelButton.jsx Co-authored-by: Nick Taylor <nick@iamdeveloper.com> * Update app/views/chat_channels/index.html.erb Co-authored-by: Michael Kohl <me@citizen428.net> * fix syntax error * Update app/views/chat_channels/index.html.erb Co-authored-by: Nick Taylor <nick@iamdeveloper.com> * Policies changed * The Disable Button issue 🐞 * Fixing Disabled button Issue 🐞 * New group doesn't show up 🐞 Co-authored-by: Narender Singh <narender2031@gmail.com> Co-authored-by: Michael Kohl <me@citizen428.net> Co-authored-by: Nick Taylor <nick@iamdeveloper.com>
This commit is contained in:
parent
5c2f27baed
commit
6dee5edd0e
30 changed files with 629 additions and 314 deletions
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
4
.github/PULL_REQUEST_TEMPLATE.md
vendored
|
|
@ -33,7 +33,9 @@ as any relevant images for UI changes._
|
|||
|
||||
### UI accessibility concerns?
|
||||
|
||||
_If your PR includes UI changes, please replace this line with details on how accessibility is impacted and tested. For more info, check out the [Forem Accessibility Docs](https://docs.forem.com/frontend/accessibility)._
|
||||
_If your PR includes UI changes, please replace this line with details on how
|
||||
accessibility is impacted and tested. For more info, check out the
|
||||
[Forem Accessibility Docs](https://docs.forem.com/frontend/accessibility)._
|
||||
|
||||
## Added tests?
|
||||
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@
|
|||
|
||||
.chat__channeltypefilter {
|
||||
width: 100%;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.chat__channeltypefilterbutton {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
after_action :verify_authorized, except: %w[join_channel request_details]
|
||||
|
||||
include MessagesHelper
|
||||
include ChatChannelMembershipsHelper
|
||||
|
||||
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
|
||||
rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
|
||||
|
|
@ -38,16 +39,11 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
def create_membership_request
|
||||
chat_channel = ChatChannel.find_by(id: channel_membership_params[:chat_channel_id])
|
||||
authorize chat_channel, :update?
|
||||
usernames = channel_membership_params[:invitation_usernames].split(",").map do |username|
|
||||
username.strip.delete("@")
|
||||
end
|
||||
users = User.where(username: usernames)
|
||||
invitations_sent = chat_channel.invite_users(users: users, membership_role: "member", inviter: current_user)
|
||||
message = if invitations_sent.zero?
|
||||
"No invitations sent. Check for username typos."
|
||||
else
|
||||
"#{invitations_sent} #{'invitation'.pluralize(invitations_sent)} sent."
|
||||
end
|
||||
message = ChatChannels::SendInvitation.call(
|
||||
channel_membership_params[:invitation_usernames],
|
||||
current_user,
|
||||
chat_channel,
|
||||
)
|
||||
|
||||
render json: { success: true, message: message, data: {} }, status: :ok
|
||||
end
|
||||
|
|
@ -138,7 +134,7 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
user_chat_channels = ChatChannel.includes(:chat_channel_memberships).where(
|
||||
chat_channel_memberships: { user_id: current_user.id, role: "mod", status: "active" },
|
||||
)
|
||||
@memberships = user_chat_channels.map(&:requested_memberships).flatten
|
||||
@memberships = user_chat_channels.flat_map(&:requested_memberships)
|
||||
@user_invitations = ChatChannelMembership.where(
|
||||
user_id: current_user.id,
|
||||
status: %w[pending],
|
||||
|
|
@ -256,7 +252,7 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
notice = "Invitation rejected."
|
||||
end
|
||||
|
||||
membership_user = helpers.format_membership(@chat_channel_membership)
|
||||
membership_user = format_membership(@chat_channel_membership)
|
||||
flash[:settings_notice] = notice
|
||||
|
||||
respond_to do |format|
|
||||
|
|
@ -273,7 +269,7 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
end
|
||||
|
||||
def send_chat_action_message(message, user, channel_id, action)
|
||||
temp_message_id = (0...20).map { ("a".."z").to_a[rand(8)] }.join
|
||||
temp_message_id = SecureRandom.hex(20)
|
||||
message = Message.create("message_markdown" => message, "user_id" => user.id, "chat_channel_id" => channel_id,
|
||||
"chat_action" => action)
|
||||
pusher_message_created(false, message, temp_message_id) unless message.left_channel?
|
||||
|
|
|
|||
|
|
@ -3,6 +3,9 @@ class ChatChannelsController < ApplicationController
|
|||
before_action :set_channel, only: %i[show update update_channel open moderate]
|
||||
after_action :verify_authorized
|
||||
|
||||
include MessagesHelper
|
||||
include ChatChannelsHelper
|
||||
|
||||
CHANNEL_ATTRIBUTES_FOR_SERIALIZATION = %i[id description channel_name].freeze
|
||||
private_constant :CHANNEL_ATTRIBUTES_FOR_SERIALIZATION
|
||||
|
||||
|
|
@ -10,16 +13,20 @@ class ChatChannelsController < ApplicationController
|
|||
case params[:state]
|
||||
when "unopened"
|
||||
authorize ChatChannel
|
||||
render_unopened_json_response
|
||||
@chat_channels_memberships = unopened_json_response
|
||||
render "index.json"
|
||||
when "unopened_ids"
|
||||
authorize ChatChannel
|
||||
render_unopened_ids_response
|
||||
@unopened_ids = unopened_ids_response
|
||||
render json: { unopened_ids: @unopened_ids }
|
||||
when "pending"
|
||||
authorize ChatChannel
|
||||
render_pending_json_response
|
||||
@chat_channels_memberships = pending_json_response
|
||||
render "index.json"
|
||||
when "joining_request"
|
||||
authorize ChatChannel
|
||||
render_joining_request_json_response
|
||||
@chat_channels_memberships = joining_request_json_response
|
||||
render "index.json"
|
||||
else
|
||||
skip_authorization
|
||||
render_channels_html
|
||||
|
|
@ -45,12 +52,12 @@ class ChatChannelsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
@chat_channel.update(chat_channel_params)
|
||||
if @chat_channel.errors.any?
|
||||
flash[:error] = @chat_channel.errors.full_messages.to_sentence
|
||||
chat_channel = ChatChannels::UpdateChannel.call(@chat_channel, chat_channel_params)
|
||||
if chat_channel.errors.any?
|
||||
flash[:error] = chat_channel.errors.full_messages.to_sentence
|
||||
else
|
||||
if chat_channel_params[:discoverable].to_i.zero?
|
||||
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: @chat_channel.id,
|
||||
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: chat_channel.id,
|
||||
role: "member", status: "active")
|
||||
else
|
||||
ChatChannelMembership.find_by(user_id: SiteConfig.mascot_user_id)&.destroy
|
||||
|
|
@ -172,6 +179,41 @@ class ChatChannelsController < ApplicationController
|
|||
render json: { error: "not found", status: 404 }, status: :not_found
|
||||
end
|
||||
|
||||
def create_channel
|
||||
chat_channel_params = params[:chat_channel]
|
||||
chat_channel_name = chat_channel_params[:channel_name].split(" ").join("-")
|
||||
chat_channel = ChatChannel.new(
|
||||
channel_type: "invite_only",
|
||||
channel_name: chat_channel_params[:channel_name],
|
||||
slug: "#{chat_channel_name}-#{SecureRandom.hex(5)}",
|
||||
)
|
||||
authorize chat_channel
|
||||
chat_channel.save
|
||||
membership = chat_channel.chat_channel_memberships.new(user_id: current_user.id, role: "mod")
|
||||
if membership.save
|
||||
message = ChatChannels::SendInvitation.call(
|
||||
chat_channel_params[:invitation_usernames],
|
||||
current_user,
|
||||
chat_channel,
|
||||
)
|
||||
|
||||
send_chat_action_message(
|
||||
"channel is created by #{current_user.username}",
|
||||
current_user, membership.chat_channel_id,
|
||||
"chat channel is created"
|
||||
)
|
||||
render json: {
|
||||
success: true,
|
||||
message: "Channel is created #{message ? "& #{message}" : nil}"
|
||||
}, status: :ok
|
||||
else
|
||||
render json: {
|
||||
success: false,
|
||||
message: membership.errors_as_sentence
|
||||
}, status: 445
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_channel
|
||||
|
|
@ -183,56 +225,6 @@ class ChatChannelsController < ApplicationController
|
|||
params.require(:chat_channel).permit(policy(ChatChannel).permitted_attributes)
|
||||
end
|
||||
|
||||
def render_unopened_json_response
|
||||
@chat_channels_memberships = if session_current_user_id
|
||||
ChatChannelMembership.where(user_id: session_current_user_id)
|
||||
.where(has_unopened_messages: true)
|
||||
.where(show_global_badge_notification: true)
|
||||
.where.not(status: %w[removed_from_channel left_channel])
|
||||
.includes(%i[chat_channel user])
|
||||
.order("chat_channel_memberships.updated_at" => :desc)
|
||||
else
|
||||
[]
|
||||
end
|
||||
render "index.json"
|
||||
end
|
||||
|
||||
def render_pending_json_response
|
||||
@chat_channels_memberships = if current_user
|
||||
current_user
|
||||
.chat_channel_memberships.includes(:chat_channel)
|
||||
.where(status: "pending")
|
||||
.order("chat_channel_memberships.updated_at" => :desc)
|
||||
else
|
||||
[]
|
||||
end
|
||||
render "index.json"
|
||||
end
|
||||
|
||||
def render_unopened_ids_response
|
||||
@unopened_ids = ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel)
|
||||
.where(has_unopened_messages: true).where.not(status: %w[removed_from_channel
|
||||
left_channel]).pluck(:chat_channel_id)
|
||||
render json: { unopened_ids: @unopened_ids }
|
||||
end
|
||||
|
||||
def render_joining_request_json_response
|
||||
requested_memberships_id = current_user
|
||||
.chat_channel_memberships
|
||||
.includes(:chat_channel)
|
||||
.where(chat_channels: { discoverable: true }, role: "mod")
|
||||
.pluck(:chat_channel_id)
|
||||
.map { |membership_id| ChatChannel.find_by(id: membership_id).requested_memberships }
|
||||
.flatten
|
||||
.map(&:id)
|
||||
|
||||
@chat_channels_memberships = ChatChannelMembership
|
||||
.includes(%i[user chat_channel])
|
||||
.where(id: requested_memberships_id)
|
||||
|
||||
render "index.json"
|
||||
end
|
||||
|
||||
def render_channels_html
|
||||
return unless current_user && params[:slug]
|
||||
|
||||
|
|
@ -267,4 +259,11 @@ class ChatChannelsController < ApplicationController
|
|||
payload = { channel_type: @chat_channel.channel_type, adjusted_slug: adjusted_slug }.to_json
|
||||
Pusher.trigger(ChatChannel.pm_notifications_channel(session_current_user_id), "message-opened", payload)
|
||||
end
|
||||
|
||||
def send_chat_action_message(message, user, channel_id, action)
|
||||
temp_message_id = SecureRandom.hex(20)
|
||||
message = Message.create(message_markdown: message, user_id: user.id, chat_channel_id: channel_id,
|
||||
chat_action: action)
|
||||
pusher_message_created(false, message, temp_message_id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
module ChatChannelMembershipHelper
|
||||
def membership_users(memberships)
|
||||
memberships.includes(:user).map do |membership|
|
||||
{
|
||||
name: membership.user.name,
|
||||
username: membership.user.username,
|
||||
user_id: membership.user.id,
|
||||
membership_id: membership.id,
|
||||
role: membership.role,
|
||||
status: membership.status,
|
||||
image: Images::Profile.call(membership.user.profile_image_url, length: 90)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
def format_membership(membership)
|
||||
{
|
||||
name: membership.user.name,
|
||||
username: membership.user.username,
|
||||
user_id: membership.user.id,
|
||||
membership_id: membership.id,
|
||||
role: membership.role,
|
||||
status: membership.status,
|
||||
image: Images::Profile.call(membership.user.profile_image_url, length: 90)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
@ -15,4 +15,22 @@ module ChatChannelMembershipsHelper
|
|||
}
|
||||
end
|
||||
end
|
||||
|
||||
def membership_users(memberships)
|
||||
memberships.includes(:user).map do |membership|
|
||||
format_membership(membership)
|
||||
end
|
||||
end
|
||||
|
||||
def format_membership(membership)
|
||||
{
|
||||
name: membership.user.name,
|
||||
username: membership.user.username,
|
||||
user_id: membership.user.id,
|
||||
membership_id: membership.id,
|
||||
role: membership.role,
|
||||
status: membership.status,
|
||||
image: Images::Profile.call(membership.user.profile_image_url, length: 90)
|
||||
}
|
||||
end
|
||||
end
|
||||
|
|
|
|||
44
app/helpers/chat_channels_helper.rb
Normal file
44
app/helpers/chat_channels_helper.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
module ChatChannelsHelper
|
||||
def unopened_json_response
|
||||
if session_current_user_id
|
||||
ChatChannelMembership.where(user_id: session_current_user_id)
|
||||
.where(has_unopened_messages: true)
|
||||
.where(show_global_badge_notification: true)
|
||||
.where.not(status: %w[removed_from_channel left_channel])
|
||||
.includes(%i[chat_channel user])
|
||||
.order("chat_channel_memberships.updated_at" => :desc)
|
||||
else
|
||||
ChatChannelMembership.none
|
||||
end
|
||||
end
|
||||
|
||||
def pending_json_response
|
||||
if current_user
|
||||
current_user
|
||||
.chat_channel_memberships.includes(:chat_channel)
|
||||
.where(status: "pending")
|
||||
.order("chat_channel_memberships.updated_at" => :desc)
|
||||
else
|
||||
ChatChannelMembership.none
|
||||
end
|
||||
end
|
||||
|
||||
def unopened_ids_response
|
||||
ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel)
|
||||
.where(has_unopened_messages: true).where.not(status: %w[removed_from_channel
|
||||
left_channel]).pluck(:chat_channel_id)
|
||||
end
|
||||
|
||||
def joining_request_json_response
|
||||
requested_memberships_id = current_user
|
||||
.chat_channel_memberships
|
||||
.includes(:chat_channel)
|
||||
.where(chat_channels: { discoverable: true }, role: "mod")
|
||||
.pluck(:chat_channel_id)
|
||||
.flat_map { |membership_id| ChatChannel.find_by(id: membership_id).requested_memberships.ids }
|
||||
|
||||
ChatChannelMembership
|
||||
.includes(%i[user chat_channel])
|
||||
.where(id: requested_memberships_id)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import ChannelButton from '../components/channelButton';
|
||||
import ChannelButton from '../components/ChannelButton';
|
||||
|
||||
const getChannel = () => {
|
||||
return {
|
||||
|
|
|
|||
|
|
@ -78,6 +78,7 @@ function getRootData() {
|
|||
}),
|
||||
githubToken: 'somegithubtoken',
|
||||
pusherKey: 'somepusherkey',
|
||||
tagModerator: JSON.stringify({ isTagModerator: true }),
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -176,16 +177,13 @@ describe('<Chat />', () => {
|
|||
|
||||
it('should render expanded', () => {
|
||||
fetch.mockResponse(getMockResponse());
|
||||
const { getByTestId, getByText, getByLabelText, getByRole } = render(
|
||||
const { getByTestId, getByText, getByRole } = render(
|
||||
<Chat {...getRootData()} />,
|
||||
);
|
||||
const chat = getByTestId('chat');
|
||||
|
||||
expect(chat.getAttribute('aria-expanded')).toEqual('true');
|
||||
|
||||
// renderChatChannels
|
||||
getByLabelText('Toggle channel search');
|
||||
|
||||
// chat filtering
|
||||
getByText('all', { selector: 'button' });
|
||||
getByText('direct', { selector: 'button' });
|
||||
|
|
@ -196,15 +194,15 @@ describe('<Chat />', () => {
|
|||
|
||||
expect(activeChat).not.toBeNull();
|
||||
|
||||
getByText('Scroll to Bottom', { selector: '[role="button"]' });
|
||||
getByText('Scroll to Bottom', { selector: '[type="button"]' });
|
||||
|
||||
// Delete modal should be visible
|
||||
getByRole('dialog', {
|
||||
selector: '[aria-hidden="false"]',
|
||||
});
|
||||
getByText('Are you sure, you want to delete this message?');
|
||||
getByText('Cancel', { selector: '[role="button"]' });
|
||||
getByText('Delete', { selector: '[role="button"]' });
|
||||
getByText('Cancel', { selector: '[type="button"]' });
|
||||
getByText('Delete', { selector: '[type="button"]' });
|
||||
});
|
||||
|
||||
it('should collapse and expand chat channels properly', async () => {
|
||||
|
|
|
|||
46
app/javascript/chat/__tests__/membership.test.jsx
Normal file
46
app/javascript/chat/__tests__/membership.test.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import Membership from '../ChatChannelSettings/MembershipManager/Membership';
|
||||
|
||||
const membershipData = {
|
||||
name: 'dummy Name',
|
||||
user_id: 1,
|
||||
chat_channel_id: 2,
|
||||
membership_id: 1,
|
||||
username: 'dummyuser',
|
||||
};
|
||||
|
||||
const currentModMembership = {
|
||||
name: 'dummy user',
|
||||
username: 'dummyuser',
|
||||
user_id: 1,
|
||||
chat_channel_id: 2,
|
||||
status: 'active',
|
||||
role: 'mod',
|
||||
};
|
||||
|
||||
describe('<Membership />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<Membership
|
||||
currentMembership={currentModMembership}
|
||||
membership={membershipData}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(
|
||||
<Membership
|
||||
currentMembership={currentModMembership}
|
||||
membership={membershipData}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('dummy user')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -8,9 +8,6 @@ import { request } from '../../utilities/http';
|
|||
export async function getChannelDetails(chatChannelMembershipId) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/chat_channel_info/${chatChannelMembershipId}`,
|
||||
{
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
|
|
@ -35,7 +32,6 @@ export async function updatePersonalChatChannelNotificationSettings(
|
|||
show_global_badge_notification: notificationBadge,
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -63,7 +59,6 @@ export async function rejectChatChannelJoiningRequest(
|
|||
chat_channel_id: channelId,
|
||||
membership_id: membershipId,
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -85,7 +80,6 @@ export async function acceptChatChannelJoiningRequest(channelId, membershipId) {
|
|||
user_action: 'accept',
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
return response.json();
|
||||
|
|
@ -125,7 +119,6 @@ export async function sendChatChannelInvitation(
|
|||
invitation_usernames: invitationUsernames,
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -142,7 +135,6 @@ export async function leaveChatChannelMembership(membershipId) {
|
|||
`/chat_channel_memberships/leave_membership/${membershipId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
|
|
@ -167,9 +159,28 @@ export async function updateMembershipRole(membershipId, chatChannelId, role) {
|
|||
role,
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create Chat Channel
|
||||
* @param {string} channelName
|
||||
* @param {string} userNames
|
||||
*/
|
||||
|
||||
export async function createChannel(channelName, userNames) {
|
||||
const response = await request(`/create_channel`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
chat_channel: {
|
||||
channel_name: channelName,
|
||||
invitation_usernames: userNames,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { h } from 'preact';
|
|||
import PropTypes from 'prop-types';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import ConfigImage from 'images/overflow-horizontal.svg';
|
||||
import ChannelButton from './components/channelButton';
|
||||
import ChannelButton from './components/ChannelButton';
|
||||
import { channelSorter } from './util';
|
||||
|
||||
const Channels = ({
|
||||
|
|
|
|||
|
|
@ -22,6 +22,7 @@ import {
|
|||
deleteMessage,
|
||||
editMessage,
|
||||
} from './actions/actions';
|
||||
import CreateChatModal from './components/CreateChatModal';
|
||||
import {
|
||||
sendChannelRequest,
|
||||
rejectJoiningRequest,
|
||||
|
|
@ -44,6 +45,7 @@ import Content from './content';
|
|||
import { VideoContent } from './videoContent';
|
||||
import { DragAndDropZone } from '@utilities/dragAndDrop';
|
||||
import { dragAndUpload } from '@utilities/dragAndUpload';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const NARROW_WIDTH_LIMIT = 767;
|
||||
const WIDE_WIDTH_LIMIT = 1600;
|
||||
|
|
@ -54,12 +56,15 @@ export default class Chat extends Component {
|
|||
chatChannels: PropTypes.string.isRequired,
|
||||
chatOptions: PropTypes.string.isRequired,
|
||||
githubToken: PropTypes.string.isRequired,
|
||||
tagModerator: PropTypes.shape({ isTagModerator: PropTypes.bool.isRequired })
|
||||
.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
const chatChannels = JSON.parse(props.chatChannels);
|
||||
const chatOptions = JSON.parse(props.chatOptions);
|
||||
|
||||
this.debouncedChannelFilter = debounceAction(
|
||||
this.triggerChannelFilter.bind(this),
|
||||
);
|
||||
|
|
@ -102,6 +107,8 @@ export default class Chat extends Component {
|
|||
memberFilterQuery: null,
|
||||
rerenderIfUnchangedCheck: null,
|
||||
userRequestCount: 0,
|
||||
openModal: false,
|
||||
isTagModerator: JSON.parse(props.tagModerator).isTagModerator,
|
||||
};
|
||||
if (chatOptions.activeChannelId) {
|
||||
getAllMessages(chatOptions.activeChannelId, 0, this.receiveAllMessages);
|
||||
|
|
@ -1221,16 +1228,15 @@ export default class Chat extends Component {
|
|||
};
|
||||
|
||||
renderChannelFilterButton = (type, name, active) => (
|
||||
<button
|
||||
<Button
|
||||
data-channel-type={type}
|
||||
onClick={this.triggerChannelTypeFilter}
|
||||
className={`chat__channeltypefilterbutton crayons-indicator crayons-indicator--${
|
||||
type === active ? 'accent' : ''
|
||||
}`}
|
||||
type="button"
|
||||
>
|
||||
{name}
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
|
||||
toggleSearchShowing = () => {
|
||||
|
|
@ -1276,35 +1282,17 @@ export default class Chat extends Component {
|
|||
return (
|
||||
<div className="chat__channels">
|
||||
{notificationsButton}
|
||||
{state.searchShowing ? (
|
||||
<input
|
||||
placeholder="Search Channels"
|
||||
onKeyUp={this.debouncedChannelFilter}
|
||||
id="chatchannelsearchbar"
|
||||
className="crayons-textfield"
|
||||
aria-label="Search Channels"
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
|
||||
<input
|
||||
placeholder="Search Channels"
|
||||
onKeyUp={this.debouncedChannelFilter}
|
||||
id="chatchannelsearchbar"
|
||||
className="crayons-textfield"
|
||||
aria-label="Search Channels"
|
||||
/>
|
||||
{invitesButton}
|
||||
{joiningRequestButton}
|
||||
<div className="chat__channeltypefilter">
|
||||
<button
|
||||
className="chat__channelssearchtoggle"
|
||||
onClick={this.toggleSearchShowing}
|
||||
aria-label="Toggle channel search"
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
width="17"
|
||||
height="17"
|
||||
>
|
||||
<path fill="none" d="M0 0h24v24H0z" />
|
||||
<path d="M18.031 16.617l4.283 4.282-1.415 1.415-4.282-4.283A8.96 8.96 0 0 1 11 20c-4.968 0-9-4.032-9-9s4.032-9 9-9 9 4.032 9 9a8.96 8.96 0 0 1-1.969 5.617zm-2.006-.742A6.977 6.977 0 0 0 18 11c0-3.868-3.133-7-7-7-3.868 0-7 3.132-7 7 0 3.867 3.132 7 7 7a6.977 6.977 0 0 0 4.875-1.975l.15-.15z" />
|
||||
</svg>
|
||||
</button>
|
||||
{this.renderChannelFilterButton(
|
||||
'all',
|
||||
'all',
|
||||
|
|
@ -1320,33 +1308,59 @@ export default class Chat extends Component {
|
|||
'group',
|
||||
state.channelTypeFilter,
|
||||
)}
|
||||
<button
|
||||
className="chat__channelssearchtoggle "
|
||||
<Button
|
||||
className="chat__channelssearchtoggle crayons-btn--ghost-dimmed p-2"
|
||||
aria-label="Toggle request manager"
|
||||
onClick={this.triggerActiveContent}
|
||||
data-content="sidecar-joining-request-manager"
|
||||
>
|
||||
<span
|
||||
onClick={this.triggerActiveContent}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 477.869 477.869"
|
||||
width="18"
|
||||
height="18"
|
||||
>
|
||||
<path d="M387.415 233.496c48.976-44.03 52.987-119.424 8.958-168.4C355.99 20.177 288.4 12.546 239.02 47.332c-53.83-38-128.264-25.15-166.254 28.68-34.86 49.393-27.26 117.054 17.69 157.483C34.606 262.935-.25 320.976.002 384.108v51.2a17.07 17.07 0 0 0 17.067 17.067h443.733a17.07 17.07 0 0 0 17.067-17.067v-51.2c.252-63.132-34.605-121.173-90.454-150.612zM307.2 59.842c47.062-.052 85.256 38.057 85.31 85.12.037 33.564-19.63 64.023-50.237 77.8-1.314.597-2.628 1.143-3.96 1.707a83.66 83.66 0 0 1-12.988 4.045c-.853.188-1.707.3-2.577.46-4.952.95-9.977 1.457-15.02 1.52-2.27 0-4.557-.17-6.827-.375-.853 0-1.707 0-2.56-.17a86.22 86.22 0 0 1-27.904-8.226c-.324-.154-.7-.137-1.024-.273-1.707-.82-3.413-1.536-4.932-2.458.137-.17.222-.358.358-.53a119.72 119.72 0 0 0 18.278-33.297l.53-1.434a120.38 120.38 0 0 0 4.523-17.562c.154-.87.273-1.707.4-2.645.987-6.067 1.506-12.2 1.553-18.347a120.04 120.04 0 0 0-1.553-18.313l-.4-2.645c-1.064-5.96-2.576-11.83-4.523-17.562l-.53-1.434c-4.282-12-10.453-23.24-18.278-33.297-.137-.17-.222-.358-.358-.53C277.45 63.83 292.2 59.843 307.2 59.842zM85.335 145.176c-.12-47.006 37.886-85.2 84.892-85.33a85.11 85.11 0 0 1 59.134 23.686l2.918 2.9a87.75 87.75 0 0 1 8.09 9.813c.75 1.058 1.434 2.185 2.133 3.277a83.95 83.95 0 0 1 6.263 11.52c.427.973.75 1.963 1.126 2.935a83.42 83.42 0 0 1 4.233 13.653c.12.512.154 1.024.256 1.553a80.34 80.34 0 0 1 0 32.119c-.102.53-.137 1.04-.256 1.553a83.23 83.23 0 0 1-4.233 13.653c-.375.973-.7 1.963-1.126 2.935a84.25 84.25 0 0 1-6.263 11.503c-.7 1.092-1.382 2.22-2.133 3.277a87.55 87.55 0 0 1-8.09 9.813 117.37 117.37 0 0 1-2.918 2.901c-6.9 6.585-14.877 11.962-23.57 15.906a49.35 49.35 0 0 1-4.198 1.707 85.84 85.84 0 0 1-12.663 3.925c-1.075.24-2.185.375-3.277.563a84.67 84.67 0 0 1-14.046 1.417h-1.877c-4.713-.08-9.412-.554-14.046-1.417-1.092-.188-2.202-.324-3.277-.563a85.8 85.8 0 0 1-12.663-3.925l-4.198-1.707c-30.534-13.786-50.173-44.166-50.212-77.667zM307.2 418.242H34.135V384.11c-.25-57.833 36.188-109.468 90.76-128.614 29.296 12.197 62.25 12.197 91.546 0a137.14 137.14 0 0 1 16.623 7.356c3.55 1.826 6.827 3.908 10.24 6.007 2.22 1.382 4.47 2.73 6.605 4.25 3.294 2.338 6.4 4.88 9.455 7.492l5.75 5.12c2.816 2.662 5.46 5.478 8.004 8.363 1.826 2.082 3.6 4.198 5.29 6.383 2.236 2.867 4.37 5.803 6.35 8.823 1.707 2.56 3.226 5.222 4.727 7.885 1.707 2.935 3.277 5.87 4.7 8.926s2.697 6.4 3.925 9.66c1.075 2.833 2.22 5.65 3.106 8.533 1.195 3.96 2.03 8.055 2.867 12.15.512 2.423 1.178 4.796 1.553 7.253 1.01 6.757 1.53 13.58 1.553 20.412v34.133zm136.534 0h-102.4V384.11c0-5.342-.307-10.633-.785-15.872-.137-1.536-.375-3.055-.546-4.59-.46-3.772-1-7.51-1.707-11.213l-.973-4.762c-.82-3.8-1.77-7.566-2.85-11.298l-1.058-3.686c-4.78-15.277-11.704-29.797-20.565-43.127l-.666-.973a168.96 168.96 0 0 0-9.404-12.646l-.12-.154c-3.413-4.232-7.117-8.346-11.008-12.237h.7a120.8 120.8 0 0 0 14.524 1.024h.94c4.496-.04 8.985-.33 13.45-.87 1.4-.17 2.782-.427 4.18-.65a117.43 117.43 0 0 0 10.752-2.167l3.055-.785a116.21 116.21 0 0 0 13.653-4.642c54.612 19.127 91.083 70.785 90.83 128.65v34.132z" />
|
||||
</svg>
|
||||
{this.state.userRequestCount > 0 ? (
|
||||
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet requests-badge">
|
||||
{this.state.userRequestCount}
|
||||
</span>
|
||||
) : null}
|
||||
</Button>
|
||||
{this.state.isTagModerator ? (
|
||||
<Button
|
||||
className="chat__channelssearchtoggle crayons-btn--ghost-dimmed p-2"
|
||||
aria-label="Toggle request manager"
|
||||
onClick={this.toggleModalCreateChannel}
|
||||
data-content="sidecar-joining-request-manager"
|
||||
role="button"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg
|
||||
version="1.1"
|
||||
id="Capa_1"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 477.869 477.869"
|
||||
x="0px"
|
||||
y="0px"
|
||||
width="18"
|
||||
height="18"
|
||||
viewBox="0 0 512 512"
|
||||
style="enable-background:new 0 0 512 512;"
|
||||
>
|
||||
<path d="M387.415 233.496c48.976-44.03 52.987-119.424 8.958-168.4C355.99 20.177 288.4 12.546 239.02 47.332c-53.83-38-128.264-25.15-166.254 28.68-34.86 49.393-27.26 117.054 17.69 157.483C34.606 262.935-.25 320.976.002 384.108v51.2a17.07 17.07 0 0 0 17.067 17.067h443.733a17.07 17.07 0 0 0 17.067-17.067v-51.2c.252-63.132-34.605-121.173-90.454-150.612zM307.2 59.842c47.062-.052 85.256 38.057 85.31 85.12.037 33.564-19.63 64.023-50.237 77.8-1.314.597-2.628 1.143-3.96 1.707a83.66 83.66 0 0 1-12.988 4.045c-.853.188-1.707.3-2.577.46-4.952.95-9.977 1.457-15.02 1.52-2.27 0-4.557-.17-6.827-.375-.853 0-1.707 0-2.56-.17a86.22 86.22 0 0 1-27.904-8.226c-.324-.154-.7-.137-1.024-.273-1.707-.82-3.413-1.536-4.932-2.458.137-.17.222-.358.358-.53a119.72 119.72 0 0 0 18.278-33.297l.53-1.434a120.38 120.38 0 0 0 4.523-17.562c.154-.87.273-1.707.4-2.645.987-6.067 1.506-12.2 1.553-18.347a120.04 120.04 0 0 0-1.553-18.313l-.4-2.645c-1.064-5.96-2.576-11.83-4.523-17.562l-.53-1.434c-4.282-12-10.453-23.24-18.278-33.297-.137-.17-.222-.358-.358-.53C277.45 63.83 292.2 59.843 307.2 59.842zM85.335 145.176c-.12-47.006 37.886-85.2 84.892-85.33a85.11 85.11 0 0 1 59.134 23.686l2.918 2.9a87.75 87.75 0 0 1 8.09 9.813c.75 1.058 1.434 2.185 2.133 3.277a83.95 83.95 0 0 1 6.263 11.52c.427.973.75 1.963 1.126 2.935a83.42 83.42 0 0 1 4.233 13.653c.12.512.154 1.024.256 1.553a80.34 80.34 0 0 1 0 32.119c-.102.53-.137 1.04-.256 1.553a83.23 83.23 0 0 1-4.233 13.653c-.375.973-.7 1.963-1.126 2.935a84.25 84.25 0 0 1-6.263 11.503c-.7 1.092-1.382 2.22-2.133 3.277a87.55 87.55 0 0 1-8.09 9.813 117.37 117.37 0 0 1-2.918 2.901c-6.9 6.585-14.877 11.962-23.57 15.906a49.35 49.35 0 0 1-4.198 1.707 85.84 85.84 0 0 1-12.663 3.925c-1.075.24-2.185.375-3.277.563a84.67 84.67 0 0 1-14.046 1.417h-1.877c-4.713-.08-9.412-.554-14.046-1.417-1.092-.188-2.202-.324-3.277-.563a85.8 85.8 0 0 1-12.663-3.925l-4.198-1.707c-30.534-13.786-50.173-44.166-50.212-77.667zM307.2 418.242H34.135V384.11c-.25-57.833 36.188-109.468 90.76-128.614 29.296 12.197 62.25 12.197 91.546 0a137.14 137.14 0 0 1 16.623 7.356c3.55 1.826 6.827 3.908 10.24 6.007 2.22 1.382 4.47 2.73 6.605 4.25 3.294 2.338 6.4 4.88 9.455 7.492l5.75 5.12c2.816 2.662 5.46 5.478 8.004 8.363 1.826 2.082 3.6 4.198 5.29 6.383 2.236 2.867 4.37 5.803 6.35 8.823 1.707 2.56 3.226 5.222 4.727 7.885 1.707 2.935 3.277 5.87 4.7 8.926s2.697 6.4 3.925 9.66c1.075 2.833 2.22 5.65 3.106 8.533 1.195 3.96 2.03 8.055 2.867 12.15.512 2.423 1.178 4.796 1.553 7.253 1.01 6.757 1.53 13.58 1.553 20.412v34.133zm136.534 0h-102.4V384.11c0-5.342-.307-10.633-.785-15.872-.137-1.536-.375-3.055-.546-4.59-.46-3.772-1-7.51-1.707-11.213l-.973-4.762c-.82-3.8-1.77-7.566-2.85-11.298l-1.058-3.686c-4.78-15.277-11.704-29.797-20.565-43.127l-.666-.973a168.96 168.96 0 0 0-9.404-12.646l-.12-.154c-3.413-4.232-7.117-8.346-11.008-12.237h.7a120.8 120.8 0 0 0 14.524 1.024h.94c4.496-.04 8.985-.33 13.45-.87 1.4-.17 2.782-.427 4.18-.65a117.43 117.43 0 0 0 10.752-2.167l3.055-.785a116.21 116.21 0 0 0 13.653-4.642c54.612 19.127 91.083 70.785 90.83 128.65v34.132z" />
|
||||
<path
|
||||
d="M492,236H276V20c0-11.046-8.954-20-20-20c-11.046,0-20,8.954-20,20v216H20c-11.046,0-20,8.954-20,20s8.954,20,20,20h216
|
||||
v216c0,11.046,8.954,20,20,20s20-8.954,20-20V276h216c11.046,0,20-8.954,20-20C512,244.954,503.046,236,492,236z"
|
||||
/>
|
||||
</svg>
|
||||
{this.state.userRequestCount > 0 ? (
|
||||
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet requests-badge">
|
||||
{this.state.userRequestCount}
|
||||
</span>
|
||||
) : null}
|
||||
</span>
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
{this.state.openModal ? (
|
||||
<CreateChatModal
|
||||
toggleModalCreateChannel={this.toggleModalCreateChannel}
|
||||
handleCreateChannelSuccess={this.handleCreateChannelSuccess}
|
||||
/>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
</div>
|
||||
<Channels
|
||||
activeChannelId={state.activeChannelId}
|
||||
|
|
@ -1367,6 +1381,11 @@ export default class Chat extends Component {
|
|||
return '';
|
||||
};
|
||||
|
||||
toggleModalCreateChannel = () => {
|
||||
const { openModal } = this.state;
|
||||
this.setState({ openModal: !openModal });
|
||||
};
|
||||
|
||||
navigateToChannelsList = () => {
|
||||
const chatContainer = document.getElementsByClassName(
|
||||
'chat__activechat',
|
||||
|
|
@ -1375,6 +1394,17 @@ export default class Chat extends Component {
|
|||
chatContainer.classList.add('chat__activechat--hidden');
|
||||
};
|
||||
|
||||
handleCreateChannelSuccess = () => {
|
||||
this.toggleModalCreateChannel();
|
||||
const searchParams = {
|
||||
query: '',
|
||||
retrievalID: null,
|
||||
searchType: '',
|
||||
paginationNumber: 0,
|
||||
};
|
||||
getChannels(searchParams, {}, this.loadChannels);
|
||||
};
|
||||
|
||||
handleMessageScroll = () => {
|
||||
const {
|
||||
allMessagesLoaded,
|
||||
|
|
@ -1513,9 +1543,8 @@ export default class Chat extends Component {
|
|||
className="chatchanneljumpback chatchanneljumpback__hide"
|
||||
id="jumpback_button"
|
||||
>
|
||||
<div
|
||||
role="button"
|
||||
className="chatchanneljumpback__messages"
|
||||
<Button
|
||||
className="chatchanneljumpback__messages crayons-btn--outlined"
|
||||
onClick={this.jumpBacktoBottom}
|
||||
tabIndex="0"
|
||||
onKeyUp={(e) => {
|
||||
|
|
@ -1523,7 +1552,7 @@ export default class Chat extends Component {
|
|||
}}
|
||||
>
|
||||
Scroll to Bottom
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
{this.renderDeleteModal()}
|
||||
<div className="activechatchannel__alerts">
|
||||
|
|
@ -1788,8 +1817,7 @@ export default class Chat extends Component {
|
|||
<div className="crayons-modal__box__body">
|
||||
<h3>Are you sure, you want to delete this message?</h3>
|
||||
<div className="delete-actions__container">
|
||||
<div
|
||||
role="button"
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--danger message__delete__button"
|
||||
onClick={this.handleMessageDelete}
|
||||
tabIndex="0"
|
||||
|
|
@ -1797,11 +1825,9 @@ export default class Chat extends Component {
|
|||
if (e.keyCode === 13) this.handleMessageDelete();
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
Delete
|
||||
</div>
|
||||
<div
|
||||
role="button"
|
||||
</Button>
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--secondary message__cancel__button"
|
||||
onClick={this.handleCloseDeleteModal}
|
||||
tabIndex="0"
|
||||
|
|
@ -1809,9 +1835,8 @@ export default class Chat extends Component {
|
|||
if (e.keyCode === 13) this.handleCloseDeleteModal();
|
||||
}}
|
||||
>
|
||||
{' '}
|
||||
Cancel
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -1847,7 +1872,7 @@ export default class Chat extends Component {
|
|||
|
||||
renderChannelBackNav = () => {
|
||||
return (
|
||||
<button
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost active-channel__back-btn"
|
||||
onClick={this.navigateToChannelsList}
|
||||
onKeyUp={(e) => {
|
||||
|
|
@ -1864,7 +1889,7 @@ export default class Chat extends Component {
|
|||
>
|
||||
<path d="M10.828 12l4.95 4.95-1.414 1.414L8 12l6.364-6.364 1.414 1.414z" />
|
||||
</svg>
|
||||
</button>
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
|
|||
91
app/javascript/chat/components/ChannelButton.jsx
Normal file
91
app/javascript/chat/components/ChannelButton.jsx
Normal file
|
|
@ -0,0 +1,91 @@
|
|||
import { h, createRef } from 'preact';
|
||||
import { useEffect } from 'preact/hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaultChannelPropTypes } from '../../common-prop-types/channel-list-prop-type';
|
||||
import ChannelImage from './ChannelImage';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
/**
|
||||
* Render the channel button
|
||||
* @param {object} props
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <ChannelButton
|
||||
* channel={channel}
|
||||
newMessagesIndicator={newMessagesIndicator}
|
||||
otherClassname={otherClassname}
|
||||
handleSwitchChannel={handleSwitchChannel}
|
||||
isUnopened={isUnopened}
|
||||
* />
|
||||
*
|
||||
*/
|
||||
export default function ChannelButton(props) {
|
||||
const buttonRef = createRef();
|
||||
const { isActiveChannel = false } = props;
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveChannel) {
|
||||
buttonRef.current.click();
|
||||
}
|
||||
}, [isActiveChannel, buttonRef]);
|
||||
|
||||
const {
|
||||
channel,
|
||||
handleSwitchChannel,
|
||||
otherClassname,
|
||||
newMessagesIndicator,
|
||||
isUnopened,
|
||||
discoverableChannel,
|
||||
triggerActiveContent,
|
||||
} = props;
|
||||
|
||||
return (
|
||||
<Button
|
||||
ref={buttonRef}
|
||||
key={channel.id}
|
||||
className={
|
||||
discoverableChannel
|
||||
? 'chatchanneltab chatchanneltab--inactive crayons-btn--ghost'
|
||||
: `chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicator} crayons-btn--ghost`
|
||||
}
|
||||
onClick={discoverableChannel ? triggerActiveContent : handleSwitchChannel}
|
||||
data-content="sidecar-channel-request"
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
data-channel-slug={channel.channel_modified_slug}
|
||||
data-channel-status={channel.status}
|
||||
data-channel-name={channel.channel_name}
|
||||
>
|
||||
<ChannelImage
|
||||
channel={channel}
|
||||
newMessagesIndicator={newMessagesIndicator}
|
||||
discoverableChannel={discoverableChannel}
|
||||
/>
|
||||
{isUnopened ? (
|
||||
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet" />
|
||||
) : null}
|
||||
{channel.channel_name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelButton.propTypes = {
|
||||
channel: defaultChannelPropTypes,
|
||||
discoverableChannel: PropTypes.bool,
|
||||
handleSwitchChannel: PropTypes.func,
|
||||
triggerActiveContent: PropTypes.func,
|
||||
newMessagesIndicator: PropTypes.string,
|
||||
otherClassname: PropTypes.string,
|
||||
isUnopened: PropTypes.string,
|
||||
};
|
||||
|
||||
ChannelButton.defaultProps = {
|
||||
otherClassname: '',
|
||||
isUnopened: '',
|
||||
newMessagesIndicator: '',
|
||||
discoverableChannel: false,
|
||||
handleSwitchChannel: null,
|
||||
triggerActiveContent: null,
|
||||
};
|
||||
36
app/javascript/chat/components/ChannelImage.jsx
Normal file
36
app/javascript/chat/components/ChannelImage.jsx
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaultChannelPropTypes } from '../../common-prop-types/channel-list-prop-type';
|
||||
|
||||
export default function ChannelImage(props) {
|
||||
const { channel, newMessagesIndicator, discoverableChannel } = props;
|
||||
|
||||
return (
|
||||
<span
|
||||
data-channel-slug={channel.channel_modified_slug}
|
||||
className={
|
||||
discoverableChannel
|
||||
? 'chatchanneltabindicator'
|
||||
: `chatchanneltabindicator chatchanneltabindicator--${newMessagesIndicator}`
|
||||
}
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
>
|
||||
<img
|
||||
src={channel.channel_image}
|
||||
alt="pic"
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
className={
|
||||
channel.channel_type === 'direct'
|
||||
? 'chatchanneltabindicatordirectimage'
|
||||
: 'chatchanneltabindicatordirectimage invert-channel-image'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelImage.propTypes = {
|
||||
channel: defaultChannelPropTypes,
|
||||
discoverableChannel: PropTypes.bool,
|
||||
newMessagesIndicator: PropTypes.string,
|
||||
};
|
||||
89
app/javascript/chat/components/CreateChatModal.jsx
Normal file
89
app/javascript/chat/components/CreateChatModal.jsx
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import PropTypes from 'prop-types';
|
||||
import { createChannel } from '../actions/chat_channel_setting_actions';
|
||||
import { addSnackbarItem } from '../../Snackbar';
|
||||
import { Modal, Button } from '@crayons';
|
||||
|
||||
/**
|
||||
*
|
||||
* This component is used to create a chat channel. At the moment only support for tag_moderator user types.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {function} props.toggleModalCreateChannel
|
||||
* @param {function} props.handleCreateChannelSuccess
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <CreateChatModal
|
||||
* toggleModalCreateChannel={toggleModalCreateChannel}
|
||||
* handleCreateChannelSuccess={handleCreateChannelSuccess}
|
||||
* />
|
||||
*
|
||||
*/
|
||||
|
||||
function CreateChatModal({
|
||||
toggleModalCreateChannel,
|
||||
handleCreateChannelSuccess,
|
||||
}) {
|
||||
const [channelName, setchannelName] = useState(undefined);
|
||||
const [userNames, setUserNames] = useState(undefined);
|
||||
|
||||
const handleCreateChannel = async (e) => {
|
||||
e.preventDefault();
|
||||
const result = await createChannel(channelName, userNames);
|
||||
if (result.success) {
|
||||
handleCreateChannelSuccess();
|
||||
addSnackbarItem({ message: result.message });
|
||||
} else {
|
||||
addSnackbarItem({ message: result.message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal title="Create A Channel" size="s" onClose={toggleModalCreateChannel}>
|
||||
<div className="crayons-field">
|
||||
<label htmlFor="t1" className="crayons-field__label">
|
||||
Channel Name
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="t1"
|
||||
className="crayons-textfield"
|
||||
placeholder="Enter name here..."
|
||||
value={channelName}
|
||||
onInput={(e) => setchannelName(e.target.value)}
|
||||
/>
|
||||
<label htmlFor="t2" className="crayons-field__label">
|
||||
Invite Users
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
id="t2"
|
||||
className="crayons-textfield"
|
||||
placeholder="Separate username with comma"
|
||||
value={userNames}
|
||||
onInput={(e) => setUserNames(e.target.value)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
className="crayons-btn"
|
||||
onClick={handleCreateChannel}
|
||||
style="margin-top:20px"
|
||||
disabled={!channelName}
|
||||
>
|
||||
Create
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
CreateChatModal.propTypes = {
|
||||
toggleModalCreateChannel: PropTypes.func.isRequired,
|
||||
handleCreateChannelSuccess: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default CreateChatModal;
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { h, Component, createRef } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
class ChannelButton extends Component {
|
||||
buttonRef = createRef();
|
||||
|
||||
componentDidMount() {
|
||||
const { isActiveChannel = false } = this.props;
|
||||
|
||||
if (isActiveChannel) {
|
||||
this.buttonRef.current.click();
|
||||
}
|
||||
}
|
||||
|
||||
renderChannelImage = () => {
|
||||
const { channel, newMessagesIndicator, discoverableChannel } = this.props;
|
||||
|
||||
return (
|
||||
<span
|
||||
data-channel-slug={channel.channel_modified_slug}
|
||||
className={
|
||||
discoverableChannel
|
||||
? 'chatchanneltabindicator'
|
||||
: `chatchanneltabindicator chatchanneltabindicator--${newMessagesIndicator}`
|
||||
}
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
>
|
||||
<img
|
||||
src={channel.channel_image}
|
||||
alt="pic"
|
||||
className={
|
||||
channel.channel_type === 'direct'
|
||||
? 'chatchanneltabindicatordirectimage'
|
||||
: 'chatchanneltabindicatordirectimage invert-channel-image'
|
||||
}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
channel,
|
||||
handleSwitchChannel,
|
||||
otherClassname,
|
||||
newMessagesIndicator,
|
||||
isUnopened,
|
||||
discoverableChannel,
|
||||
triggerActiveContent,
|
||||
} = this.props;
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
ref={this.buttonRef}
|
||||
key={channel.id}
|
||||
className="chatchanneltabbutton"
|
||||
onClick={
|
||||
discoverableChannel ? triggerActiveContent : handleSwitchChannel
|
||||
}
|
||||
data-content="sidecar-channel-request"
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
data-channel-slug={channel.channel_modified_slug}
|
||||
data-channel-status={channel.status}
|
||||
data-channel-name={channel.channel_name}
|
||||
>
|
||||
<span
|
||||
className={
|
||||
discoverableChannel
|
||||
? 'chatchanneltab chatchanneltab--inactive'
|
||||
: `chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicator}`
|
||||
}
|
||||
data-channel-id={channel.chat_channel_id}
|
||||
data-content="sidecar-channel-request"
|
||||
data-channel-slug={channel.channel_modified_slug}
|
||||
data-channel-status={channel.status}
|
||||
data-channel-name={channel.channel_name}
|
||||
style={{
|
||||
border: `1px solid ${channel.channel_color}`,
|
||||
boxShadow: `3px 3px 0px ${channel.channel_color}`,
|
||||
}}
|
||||
>
|
||||
{this.renderChannelImage()}
|
||||
{isUnopened ? (
|
||||
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet" />
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{channel.channel_name}
|
||||
</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
ChannelButton.propTypes = {
|
||||
channel: PropTypes.shape({
|
||||
channel_name: PropTypes.string,
|
||||
channel_color: PropTypes.string,
|
||||
channel_type: PropTypes.string,
|
||||
channel_modified_slug: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
chat_channel_id: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
channel_image: PropTypes.string,
|
||||
}).isRequired,
|
||||
discoverableChannel: PropTypes.bool,
|
||||
handleSwitchChannel: PropTypes.func,
|
||||
triggerActiveContent: PropTypes.func,
|
||||
newMessagesIndicator: PropTypes.string,
|
||||
otherClassname: PropTypes.string,
|
||||
isUnopened: PropTypes.string,
|
||||
};
|
||||
|
||||
ChannelButton.defaultProps = {
|
||||
otherClassname: '',
|
||||
isUnopened: '',
|
||||
newMessagesIndicator: '',
|
||||
discoverableChannel: false,
|
||||
handleSwitchChannel: null,
|
||||
triggerActiveContent: null,
|
||||
};
|
||||
export default ChannelButton;
|
||||
14
app/javascript/common-prop-types/channel-list-prop-type.js
Normal file
14
app/javascript/common-prop-types/channel-list-prop-type.js
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
import PropTypes from 'prop-types';
|
||||
|
||||
export const defaultChannelPropTypes = PropTypes.shape({
|
||||
channel: PropTypes.shape({
|
||||
channel_name: PropTypes.string,
|
||||
channel_color: PropTypes.string,
|
||||
channel_type: PropTypes.string,
|
||||
channel_modified_slug: PropTypes.string,
|
||||
id: PropTypes.number,
|
||||
chat_channel_id: PropTypes.number,
|
||||
status: PropTypes.string,
|
||||
channel_image: PropTypes.string,
|
||||
}).isRequired,
|
||||
});
|
||||
|
|
@ -35,6 +35,7 @@ class ChatChannel < ApplicationRecord
|
|||
validates :status, presence: true, inclusion: { in: STATUSES }
|
||||
validates :slug, uniqueness: true, presence: true
|
||||
validates :description, length: { maximum: 200 }, allow_blank: true
|
||||
validates :channel_name, presence: true
|
||||
|
||||
def open?
|
||||
channel_type == "open"
|
||||
|
|
|
|||
|
|
@ -51,6 +51,10 @@ class ChatChannelPolicy < ApplicationPolicy
|
|||
record.present?
|
||||
end
|
||||
|
||||
def create_channel?
|
||||
record.present? && user.tag_moderator?
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def user_can_edit_channel
|
||||
|
|
|
|||
31
app/services/chat_channels/send_invitation.rb
Normal file
31
app/services/chat_channels/send_invitation.rb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
module ChatChannels
|
||||
class SendInvitation
|
||||
attr_accessor :invitation_usernames, :current_user, :chat_channel
|
||||
|
||||
def initialize(invitation_usernames, current_user, chat_channel)
|
||||
@invitation_usernames = invitation_usernames
|
||||
@current_user = current_user
|
||||
@chat_channel = chat_channel
|
||||
end
|
||||
|
||||
def self.call(*args)
|
||||
new(*args).call
|
||||
end
|
||||
|
||||
def call
|
||||
if invitation_usernames.present?
|
||||
usernames = invitation_usernames.split(",").map do |username|
|
||||
username.strip.delete("@")
|
||||
end
|
||||
users = User.where(username: usernames)
|
||||
invitations_sent = chat_channel.invite_users(users: users, membership_role: "member", inviter: current_user)
|
||||
message = if invitations_sent.zero?
|
||||
"No invitations sent. Check for username typos."
|
||||
else
|
||||
"#{invitations_sent} #{'invitation'.pluralize(invitations_sent)} sent."
|
||||
end
|
||||
end
|
||||
message
|
||||
end
|
||||
end
|
||||
end
|
||||
19
app/services/chat_channels/update_channel.rb
Normal file
19
app/services/chat_channels/update_channel.rb
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
module ChatChannels
|
||||
class UpdateChannel
|
||||
attr_accessor :chat_channel, :params
|
||||
|
||||
def initialize(chat_channel, params)
|
||||
@chat_channel = chat_channel
|
||||
@params = params
|
||||
end
|
||||
|
||||
def self.call(*args)
|
||||
new(*args).call
|
||||
end
|
||||
|
||||
def call
|
||||
chat_channel.update(params)
|
||||
chat_channel
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -14,7 +14,8 @@
|
|||
data-pusher-key="<%= ApplicationConfig["PUSHER_KEY"] %>"
|
||||
data-github-token="<%= @github_token %>"
|
||||
data-chat-channels="<%= [] %>"
|
||||
data-chat-options="<%= { showChannelsList: true, showTimestamp: true, activeChannelId: @active_channel&.id, currentUserId: current_user.id }.to_json %>">
|
||||
data-chat-options="<%= { showChannelsList: true, showTimestamp: true, activeChannelId: @active_channel&.id, currentUserId: current_user.id }.to_json %>"
|
||||
data-tag-moderator="<%= { isTagModerator: current_user.tag_moderator? }.to_json %>">
|
||||
<div class="chat" id="chat_placeholder">
|
||||
<div class="chat__channels chat__channels--placeholder">
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -355,6 +355,7 @@ Rails.application.routes.draw do
|
|||
|
||||
# Chat channel
|
||||
patch "/chat_channels/update_channel/:id" => "chat_channels#update_channel"
|
||||
post "/create_channel" => "chat_channels#create_channel"
|
||||
|
||||
# Chat Channel Membership json response
|
||||
get "/chat_channel_memberships/chat_channel_info/:id" => "chat_channel_memberships#chat_channel_info"
|
||||
|
|
|
|||
|
|
@ -115,7 +115,11 @@ score.
|
|||
|
||||
### Consider accessibility in UI changes
|
||||
|
||||
If the change you're proposing touches a user interface, include accessibility in your approach. This includes things like color contrast, keyboard accessibility, screen reader labels, and other common requirements. For more information, check out the [Forem Accessibility docs page](https://docs.forem.com/frontend/accessibility).
|
||||
If the change you're proposing touches a user interface, include accessibility
|
||||
in your approach. This includes things like color contrast, keyboard
|
||||
accessibility, screen reader labels, and other common requirements. For more
|
||||
information, check out the
|
||||
[Forem Accessibility docs page](https://docs.forem.com/frontend/accessibility).
|
||||
|
||||
### Please use inclusive language
|
||||
|
||||
|
|
|
|||
|
|
@ -1,17 +1,18 @@
|
|||
---
|
||||
title: Frontend Guide
|
||||
items:
|
||||
- plain-js.md
|
||||
- webpacker.md
|
||||
- preact.md
|
||||
- instant-click.md
|
||||
- dynamic-imports.md
|
||||
- styles.md
|
||||
- linting-formatting.md
|
||||
- liquid-tags.md
|
||||
- debugging.md
|
||||
- tracking.md
|
||||
- accessibility.md
|
||||
- tips.md
|
||||
|
||||
title: Frontend Guide items:
|
||||
|
||||
- plain-js.md
|
||||
- webpacker.md
|
||||
- preact.md
|
||||
- instant-click.md
|
||||
- dynamic-imports.md
|
||||
- styles.md
|
||||
- linting-formatting.md
|
||||
- liquid-tags.md
|
||||
- debugging.md
|
||||
- tracking.md
|
||||
- accessibility.md
|
||||
- tips.md
|
||||
|
||||
# Frontend Guide
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ module DataUpdateScripts
|
|||
n = index + 300 # + 300 gives the server time to boot up
|
||||
BustCachePathWorker.set(queue: :high_priority).perform_in(n.seconds, "#{article.path}?i=i")
|
||||
end
|
||||
|
||||
relation.offset(1500).limit(3000).each_with_index do |article, index|
|
||||
|
||||
n = (index * 3) + 450
|
||||
BustCachePathWorker.set(queue: :medium_priority).perform_in(n.seconds, "#{article.path}?i=i")
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
FactoryBot.define do
|
||||
factory :chat_channel do
|
||||
channel_type { "open" }
|
||||
channel_name { Faker::Name.name }
|
||||
sequence(:slug) { |n| "slug-#{n}" }
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -433,4 +433,29 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /create_channel" do
|
||||
it "create channel by mod users only" do
|
||||
user.add_role(:tag_moderator)
|
||||
post "/create_channel", params: {
|
||||
chat_channel: {
|
||||
channel_name: "dummy test",
|
||||
invitation_usernames: ""
|
||||
}
|
||||
}
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["success"]).to eq(true)
|
||||
end
|
||||
|
||||
it "when non mod user logged in" do
|
||||
expect do
|
||||
post "/create_channel", params: {
|
||||
chat_channel: {
|
||||
channel_name: "dummy test",
|
||||
invitation_usernames: ""
|
||||
}
|
||||
}
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12528,6 +12528,11 @@ nan@^2.12.1, nan@^2.13.2, nan@^2.14.0:
|
|||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nan@^2.14.0:
|
||||
version "2.14.2"
|
||||
resolved "https://registry.yarnpkg.com/nan/-/nan-2.14.2.tgz#f5376400695168f4cc694ac9393d0c9585eeea19"
|
||||
integrity sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==
|
||||
|
||||
nanoid@2.1.11:
|
||||
version "2.1.11"
|
||||
resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue