🚀 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:
Sarthak Sharma 2020-12-08 11:40:36 +05:30 committed by GitHub
parent 5c2f27baed
commit 6dee5edd0e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 629 additions and 314 deletions

View file

@ -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?

View file

@ -111,6 +111,7 @@
.chat__channeltypefilter {
width: 100%;
margin-top: 15px;
}
.chat__channeltypefilterbutton {

View file

@ -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?

View file

@ -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

View file

@ -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

View file

@ -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

View 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

View file

@ -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 {

View file

@ -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 () => {

View 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();
});
});

View file

@ -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();
}

View file

@ -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 = ({

View file

@ -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>
);
};

View 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,
};

View 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,
};

View 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;

View file

@ -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;

View 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,
});

View file

@ -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"

View file

@ -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

View 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

View 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

View file

@ -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>

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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"