✂✂✂ Remove Connect (#14734)
* Remove Connect
* Remove more Connect specs
* Remove a lot more Connect code
* 🚮
* It all has to go
* Explicitly add httpclient
* Update application layout
* Remove messages association from User
* Start fixing specs
* reintroduce util function and refactor references
* Remove Connect Cypress test
* Fix more specs
* Remove Connect from listings
* Ignore contact_via_connect column on listings
* Remove contact_via_connect usages
* Ignore mod_chat_channel_id on tags
* Drop Connect tables
* Remove email_connect_messages from user notification settings
* Re-add httpclient 2.8.3
This was mistakenly removed as a merge conflict
* Don't need to exclude removed chat channel file
* Remove unneeded style for chat channels
* Remove unneeded channel list prop type
* Remove chat channels index/connect-link from getPageEntries
* Re-add comment from httpclient in Gemfile
* Remove connect references from mailers
Tag Moderators no longer have a chat channel
No longer will users be notified about new messages (there won't be
any)
No longer will users be notified about channel invites (you can't
invite anyone anymore)
* Don't configure Pusher and remove PUSHER_* from .env_sample
since it's removed from gemfile, the Pusher constant will not resolve, if this is
configured in the environment variables we'll fail to boot.
Co-authored-by: Suzanne Aitchison <suzanne@forem.com>
Co-authored-by: Dan Uber <dan@forem.com>
This commit is contained in:
parent
83b4a6db81
commit
09828853f6
250 changed files with 176 additions and 15427 deletions
|
|
@ -123,13 +123,6 @@ GA_TRACKING_ID="Optional"
|
|||
# (https://mailchimp.com/developer/)
|
||||
MAILCHIMP_API_KEY="Optional-valid"
|
||||
|
||||
# Pusher for DEV Connect/notfications
|
||||
# (https://pusher.com/docs)
|
||||
PUSHER_APP_ID=
|
||||
PUSHER_CLUSTER=
|
||||
PUSHER_KEY=
|
||||
PUSHER_SECRET=
|
||||
|
||||
# Google recaptcha (Optional)
|
||||
# (https://developers.google.com/recaptcha/intro)
|
||||
RECAPTCHA_SECRET=
|
||||
|
|
|
|||
|
|
@ -18,6 +18,5 @@ require:
|
|||
Rails/HasManyOrHasOneDependent:
|
||||
Exclude:
|
||||
- 'app/models/article.rb'
|
||||
- 'app/models/chat_channel.rb'
|
||||
- 'app/models/organization.rb'
|
||||
- 'app/models/user.rb'
|
||||
|
|
|
|||
2
Gemfile
2
Gemfile
|
|
@ -48,6 +48,7 @@ gem "honeycomb-beeline", "~> 2.7.1" # Monitoring and Observability gem
|
|||
gem "html_truncator", "~> 0.4" # Truncate an HTML string properly
|
||||
gem "htmlentities", "~> 4.3", ">= 4.3.4" # A module for encoding and decoding (X)HTML entities
|
||||
gem "httparty", "~> 0.20" # Makes http fun! Also, makes consuming restful web services dead easy
|
||||
gem "httpclient", "~> 2.8.3" # Gives something like the functionality of libwww-perl (LWP) in Ruby
|
||||
gem "i18n-js", "~> 3.9.0" # Helps with internationalization in Rails.
|
||||
gem "imgproxy", "~> 2.0" # A gem that easily generates imgproxy URLs for your images
|
||||
gem "inline_svg", "~> 1.7" # Embed SVG documents in your Rails views and style them with CSS
|
||||
|
|
@ -74,7 +75,6 @@ gem "pg_search", "~> 2.3.5" # PgSearch builds Active Record named scopes that ta
|
|||
gem "pghero", "~> 2.8" # Dashboard for Postgres
|
||||
gem "puma", "~> 5.5.2" # Puma is a simple, fast, threaded, and highly concurrent HTTP 1.1 server
|
||||
gem "pundit", "~> 2.1" # Object oriented authorization for Rails applications
|
||||
gem "pusher", "~> 2.0" # Ruby library for Pusher Channels HTTP API
|
||||
gem "rack-attack", "~> 6.5.0" # Used to throttle requests to prevent brute force attacks
|
||||
gem "rack-cors", "~> 1.1" # Middleware that will make Rack-based apps CORS compatible
|
||||
gem "rack-timeout", "~> 0.6" # Rack middleware which aborts requests that have been running for longer than a specified timeout
|
||||
|
|
|
|||
|
|
@ -561,11 +561,6 @@ GEM
|
|||
activesupport (>= 3.0.0)
|
||||
pundit-matchers (1.7.0)
|
||||
rspec-rails (>= 3.0.0)
|
||||
pusher (2.0.2)
|
||||
httpclient (~> 2.8)
|
||||
multi_json (~> 1.15)
|
||||
pusher-signature (~> 0.1.8)
|
||||
pusher-signature (0.1.8)
|
||||
raabro (1.4.0)
|
||||
racc (1.6.0)
|
||||
rack (2.2.3)
|
||||
|
|
@ -946,6 +941,7 @@ DEPENDENCIES
|
|||
html_truncator (~> 0.4)
|
||||
htmlentities (~> 4.3, >= 4.3.4)
|
||||
httparty (~> 0.20)
|
||||
httpclient (~> 2.8.3)
|
||||
hypershield (~> 0.2.2)
|
||||
i18n-js (~> 3.9.0)
|
||||
i18n-tasks (~> 0.9.35)
|
||||
|
|
@ -982,7 +978,6 @@ DEPENDENCIES
|
|||
puma (~> 5.5.2)
|
||||
pundit (~> 2.1)
|
||||
pundit-matchers (~> 1.7)
|
||||
pusher (~> 2.0)
|
||||
rack-attack (~> 6.5.0)
|
||||
rack-cors (~> 1.1)
|
||||
rack-host-redirect (~> 1.3)
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
/*
|
||||
global initializeLocalStorageRender, initializeBodyData,
|
||||
initializeAllChatButtons, initializeAllTagEditButtons, initializeUserFollowButts,
|
||||
initializeAllTagEditButtons, initializeUserFollowButts,
|
||||
initializeBaseTracking, initializeCommentsPage,
|
||||
initializeArticleDate, initializeArticleReactions, initNotifications,
|
||||
initializeCommentDate, initializeSettings,
|
||||
|
|
@ -50,7 +50,6 @@ function initializePage() {
|
|||
clearInterval(waitingForDataLoad);
|
||||
if (document.body.getAttribute('data-user-status') === 'logged-in') {
|
||||
initializeBaseUserData();
|
||||
initializeAllChatButtons();
|
||||
initializeAllTagEditButtons();
|
||||
}
|
||||
initializeBroadcast();
|
||||
|
|
|
|||
|
|
@ -1,119 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
function showChatModal(modal) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
modal.style.display = 'block';
|
||||
document.getElementById('new-message').focus();
|
||||
}
|
||||
|
||||
function hideChatModal(modal) {
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
|
||||
function toggleModal() {
|
||||
var modal = document.getElementsByClassName('crayons-modal')[0];
|
||||
modal.classList.toggle('hidden');
|
||||
}
|
||||
|
||||
function initModal() {
|
||||
var modal = document.getElementsByClassName('crayons-modal')[0];
|
||||
modal
|
||||
.getElementsByClassName('close-modal')[0]
|
||||
.addEventListener('click', toggleModal);
|
||||
modal
|
||||
.getElementsByClassName('crayons-modal__overlay')[0]
|
||||
.addEventListener('click', toggleModal);
|
||||
}
|
||||
|
||||
function handleChatButtonPress(form) {
|
||||
var message = document.getElementById('new-message').value;
|
||||
var formDataInfo = JSON.parse(form.dataset.info);
|
||||
var formData = new FormData();
|
||||
|
||||
if (message.replace(/\s/g, '').length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
formData.append('user_id', formDataInfo.id);
|
||||
formData.append('message', message);
|
||||
formData.append('controller', 'chat_channels');
|
||||
|
||||
getCsrfToken()
|
||||
.then(sendFetch('chat-creation', formData))
|
||||
.then(() => {
|
||||
window.location.href = `/connect/@${formDataInfo.username}`;
|
||||
});
|
||||
}
|
||||
|
||||
function addButtonClickHandle(response, button, modalInfo) {
|
||||
var linkWrap = document.getElementById('user-connect-redirect');
|
||||
var form = document.getElementById('new-message-form');
|
||||
button.classList.add('showing');
|
||||
if (modalInfo.showChat === 'open' && response !== 'mutual') {
|
||||
linkWrap.removeAttribute('href'); // remove link
|
||||
button.addEventListener('click', toggleModal);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
button.classList.remove('hidden'); // show button
|
||||
linkWrap.classList.remove('hidden'); // show button
|
||||
form.onsubmit = () => {
|
||||
handleChatButtonPress(form);
|
||||
return false;
|
||||
};
|
||||
} else if (response === 'mutual') {
|
||||
button.removeEventListener('click', toggleModal);
|
||||
// eslint-disable-next-line no-param-reassign
|
||||
button.classList.remove('hidden'); // show button
|
||||
linkWrap.classList.remove('hidden'); // show button
|
||||
}
|
||||
}
|
||||
|
||||
function fetchButton(button, modalInfo) {
|
||||
var dataRequester;
|
||||
// button.dataset.fetched = 'fetched'; // telling initialize that this button has been fetched
|
||||
if (window.XMLHttpRequest) {
|
||||
dataRequester = new XMLHttpRequest();
|
||||
} else {
|
||||
dataRequester = new ActiveXObject('Microsoft.XMLHTTP');
|
||||
}
|
||||
dataRequester.onreadystatechange = () => {
|
||||
if (
|
||||
dataRequester.readyState === XMLHttpRequest.DONE &&
|
||||
dataRequester.status === 200
|
||||
) {
|
||||
addButtonClickHandle(dataRequester.response, button, modalInfo);
|
||||
}
|
||||
};
|
||||
dataRequester.open(
|
||||
'GET',
|
||||
'/follows/' + modalInfo.id + '?followable_type=' + modalInfo.className,
|
||||
);
|
||||
dataRequester.send();
|
||||
}
|
||||
|
||||
function initializeChatButton(button, modalInfo) {
|
||||
// if user logged out, do nothing
|
||||
var user = userData();
|
||||
if (user === null || user.id === modalInfo.id) {
|
||||
return;
|
||||
}
|
||||
fetchButton(button, modalInfo);
|
||||
}
|
||||
|
||||
// finds all elements with chat action button class
|
||||
function initializeAllChatButtons() {
|
||||
var buttons = document.getElementsByClassName('chat-action-button');
|
||||
var modal = document.getElementById('new-message-form');
|
||||
var i;
|
||||
|
||||
if (!modal) {
|
||||
return;
|
||||
}
|
||||
|
||||
var modalInfo = JSON.parse(modal.dataset.info);
|
||||
initModal();
|
||||
|
||||
for (i = 0; i < buttons.length; i += 1) {
|
||||
initializeChatButton(buttons[i], modalInfo);
|
||||
}
|
||||
}
|
||||
|
|
@ -46,12 +46,6 @@ function sendFetch(switchStatement, body) {
|
|||
addTokenToBody: true,
|
||||
body,
|
||||
});
|
||||
case 'chat-creation':
|
||||
return fetchCallback({
|
||||
url: '/chat_channels/create_chat',
|
||||
addTokenToBody: true,
|
||||
body,
|
||||
});
|
||||
case 'block-user':
|
||||
return fetchCallback({
|
||||
url: '/user_blocks',
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -1,134 +0,0 @@
|
|||
@import '../variables';
|
||||
|
||||
.channeldetails {
|
||||
.channeldetails__name {
|
||||
font-family: var(--ff-sans-serif);
|
||||
font-size: 48px;
|
||||
}
|
||||
|
||||
.channeldetails__description {
|
||||
font-family: var(--ff-sans-serif);
|
||||
color: $dark-gray;
|
||||
}
|
||||
|
||||
// channel users
|
||||
.channeldetails__user {
|
||||
a {
|
||||
padding: 0 4px !important;
|
||||
border-radius: 3px;
|
||||
background: white;
|
||||
}
|
||||
padding: 2px 0;
|
||||
margin-bottom: 10px;
|
||||
.channeldetails__userprofileimage {
|
||||
height: 25px;
|
||||
width: 25px;
|
||||
margin-right: 6px;
|
||||
border-radius: 20px;
|
||||
vertical-align: -6px;
|
||||
}
|
||||
a {
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
}
|
||||
|
||||
.channeldetails__inviteusers {
|
||||
h2 {
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
|
||||
// find users
|
||||
input {
|
||||
border-radius: 3px;
|
||||
border: 1px solid #dbdbdb;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
margin-left: 2px;
|
||||
font-size: 18px;
|
||||
-webkit-appearance: none;
|
||||
}
|
||||
|
||||
.channeldetails__searchresults {
|
||||
padding: 5px 0;
|
||||
|
||||
.channeldetails__searchedusers {
|
||||
padding: 1px 0;
|
||||
|
||||
// profile pic
|
||||
img {
|
||||
height: 22px;
|
||||
width: 22px;
|
||||
margin-right: 6px;
|
||||
border-radius: 20px;
|
||||
vertical-align: -6px;
|
||||
}
|
||||
|
||||
a {
|
||||
font-size: 14px;
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
|
||||
// invite button
|
||||
button {
|
||||
width: 45px;
|
||||
height: 24px;
|
||||
background-color: $purple;
|
||||
border-radius: 3px;
|
||||
color: $white;
|
||||
}
|
||||
|
||||
// in channel message
|
||||
span {
|
||||
color: $dark-gray;
|
||||
font-size: 14px;
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channeldetails__pendingusers {
|
||||
a {
|
||||
font-size: 18px;
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.channeldetails__leavechannel {
|
||||
// Danger Zone
|
||||
h2 {
|
||||
color: $red;
|
||||
font-family: $helvetica-condensed;
|
||||
font-size: 2.5em;
|
||||
}
|
||||
// leave channel
|
||||
button {
|
||||
width: 180px;
|
||||
padding: 3px 0px;
|
||||
border-radius: 3px;
|
||||
background-color: $red;
|
||||
color: $white;
|
||||
font-size: 1.2em;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
.channeldetails__leftchannel {
|
||||
h2 {
|
||||
font-family: var(--ff-sans-serif);
|
||||
font-size: 32px;
|
||||
}
|
||||
h3 {
|
||||
font-family: var(--ff-sans-serif);
|
||||
font-size: 18px;
|
||||
}
|
||||
h4 {
|
||||
font-family: var(--ff-sans-serif);
|
||||
font-size: 18px;
|
||||
}
|
||||
p {
|
||||
font-family: var(--ff-sans-serif);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -19,8 +19,6 @@
|
|||
|
||||
@import 'ltags/LiquidTags';
|
||||
|
||||
@import 'chat';
|
||||
|
||||
@import 'email_subscriptions';
|
||||
|
||||
@import 'runtime';
|
||||
|
|
|
|||
|
|
@ -46,7 +46,6 @@ body.dark-theme {
|
|||
.image-upload-button button,
|
||||
.icon-image,
|
||||
.dev-badge,
|
||||
.chatchannels__config img,
|
||||
.message__actions img {
|
||||
-webkit-filter: invert(95%);
|
||||
filter: invert(95%);
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ module Admin
|
|||
articles: "https://admin.forem.com/docs/forem-basics/posts",
|
||||
badges: "https://admin.forem.com/docs/forem-basics/badges",
|
||||
badge_achievements: "https://admin.forem.com/docs/forem-basics/badges",
|
||||
chat_channels: "https://admin.forem.com/docs/advanced-customization/chat-channels",
|
||||
display_ads: "https://admin.forem.com/docs/advanced-customization/display-ads",
|
||||
feedback_messages: "https://admin.forem.com/docs/advanced-customization/reports",
|
||||
html_variants: "https://admin.forem.com/docs/advanced-customization/html-variants",
|
||||
|
|
|
|||
|
|
@ -1,59 +0,0 @@
|
|||
module Admin
|
||||
class ChatChannelsController < Admin::ApplicationController
|
||||
layout "admin"
|
||||
|
||||
CHAT_ALLOWED_PARAMS = %i[usernames_string channel_name username_string].freeze
|
||||
|
||||
def index
|
||||
@q = ChatChannel.where(channel_type: "invite_only").includes(:users).ransack(params[:q])
|
||||
@group_chat_channels = @q.result.page(params[:page]).per(50)
|
||||
end
|
||||
|
||||
def create
|
||||
ChatChannels::CreateWithUsers.call(
|
||||
users: users_by_param,
|
||||
channel_type: "invite_only",
|
||||
contrived_name: chat_channel_params[:channel_name],
|
||||
membership_role: "mod",
|
||||
)
|
||||
redirect_back(fallback_location: admin_chat_channels_path)
|
||||
end
|
||||
|
||||
def update
|
||||
@chat_channel = ChatChannel.find(params[:id])
|
||||
@chat_channel.invite_users(users: users_by_param)
|
||||
redirect_back(fallback_location: admin_chat_channels_path)
|
||||
end
|
||||
|
||||
def remove_user
|
||||
@chat_channel = ChatChannel.find(params[:id])
|
||||
@chat_channel.remove_user(user_by_param)
|
||||
redirect_back(fallback_location: admin_chat_channels_path)
|
||||
end
|
||||
|
||||
def destroy
|
||||
@chat_channel = ChatChannel.find(params[:id])
|
||||
if @chat_channel.users.count.zero?
|
||||
@chat_channel.destroy
|
||||
flash[:success] = "Channel was successfully deleted."
|
||||
else
|
||||
flash[:alert] = "Channel NOT deleted, because it still has users."
|
||||
end
|
||||
redirect_back(fallback_location: admin_chat_channels_path)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def users_by_param
|
||||
User.where(username: chat_channel_params[:usernames_string].downcase.delete(" ").split(","))
|
||||
end
|
||||
|
||||
def user_by_param
|
||||
User.find_by(username: chat_channel_params[:username_string])
|
||||
end
|
||||
|
||||
def chat_channel_params
|
||||
params.require(:chat_channel).permit(CHAT_ALLOWED_PARAMS)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -1,285 +0,0 @@
|
|||
class ChatChannelMembershipsController < ApplicationController
|
||||
before_action :authenticate_user!
|
||||
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
|
||||
|
||||
def index
|
||||
skip_authorization
|
||||
memberships = current_user.chat_channel_memberships.includes(:chat_channel)
|
||||
@pending_invites = memberships.filter_by_status("pending")
|
||||
end
|
||||
|
||||
def find_by_chat_channel_id
|
||||
@membership = ChatChannelMembership.where(chat_channel_id: params[:chat_channel_id],
|
||||
user_id: current_user.id).first!
|
||||
authorize @membership
|
||||
render json: @membership.to_json(
|
||||
only: %i[id status viewable_by chat_channel_id last_opened_at],
|
||||
methods: %i[channel_text channel_last_message_at channel_status channel_username
|
||||
channel_type channel_text channel_name channel_image channel_modified_slug channel_messages_count],
|
||||
)
|
||||
end
|
||||
|
||||
def chat_channel_info
|
||||
@membership = ChatChannelMembership.find(params[:id])
|
||||
authorize @membership
|
||||
@channel = @membership.chat_channel
|
||||
invite_cache_key = "chat-channel-invite-#{@channel.id}"
|
||||
invitation_slug = Rails.cache.fetch(invite_cache_key, expires_in: 80.hours) do
|
||||
"invitation-link-#{SecureRandom.hex(3)}"
|
||||
end
|
||||
@invitation_link = "/join_channel_invitation/#{@channel.slug}?invitation_slug=#{invitation_slug}"
|
||||
end
|
||||
|
||||
def create_membership_request
|
||||
chat_channel = ChatChannel.find_by(id: channel_membership_params[:chat_channel_id])
|
||||
authorize chat_channel, :update?
|
||||
message = ChatChannels::SendInvitation.call(
|
||||
channel_membership_params[:invitation_usernames],
|
||||
current_user,
|
||||
chat_channel,
|
||||
)
|
||||
|
||||
render json: { success: true, message: message, data: {} }, status: :ok
|
||||
end
|
||||
|
||||
def join_channel
|
||||
membership_params = params[:chat_channel_membership]
|
||||
chat_channel = ChatChannel.find(membership_params[:chat_channel_id])
|
||||
existing_membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: chat_channel.id)
|
||||
if existing_membership.present? && %w[active joining_request].exclude?(existing_membership.status)
|
||||
status = existing_membership.update(status: "joining_request", role: "member")
|
||||
else
|
||||
membership = ChatChannelMembership.new(user_id: current_user.id, chat_channel_id: chat_channel.id,
|
||||
role: "member", status: "joining_request")
|
||||
status = membership.save
|
||||
end
|
||||
if status
|
||||
render json: { status: "success", message: "Request Sent" }, status: :ok
|
||||
else
|
||||
render json: { status: 400, message: "Unable to join channel" }, status: :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
def remove_membership
|
||||
@chat_channel = ChatChannel.find(params[:chat_channel_id])
|
||||
authorize @chat_channel, :update?
|
||||
@chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id])
|
||||
if params[:status] == "pending"
|
||||
@chat_channel_membership.destroy
|
||||
message = "Invitation removed."
|
||||
else
|
||||
membership = @chat_channel_membership
|
||||
message = "@#{current_user.username} removed @#{membership.user.username} from #{membership.channel_name}"
|
||||
send_chat_action_message(
|
||||
message, current_user, @chat_channel_membership.chat_channel_id, "removed_from_channel"
|
||||
)
|
||||
@chat_channel_membership.update(status: "removed_from_channel")
|
||||
message = "Removed #{@chat_channel_membership.user.name}"
|
||||
end
|
||||
|
||||
render json: { status: "success", message: message, success: true }, status: :ok
|
||||
end
|
||||
|
||||
def add_membership
|
||||
@chat_channel = ChatChannel.find(params[:chat_channel_id])
|
||||
authorize @chat_channel, :update?
|
||||
@chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id])
|
||||
|
||||
return unless permitted_params[:user_action].present? && @chat_channel_membership.status == "joining_request"
|
||||
|
||||
respond_to_invitation(@chat_channel_membership.status)
|
||||
end
|
||||
|
||||
def update
|
||||
@chat_channel_membership = ChatChannelMembership.find(params[:id])
|
||||
authorize @chat_channel_membership
|
||||
respond_to_invitation(@chat_channel_membership.status)
|
||||
end
|
||||
|
||||
def update_membership
|
||||
@chat_channel_membership = ChatChannelMembership.find(params[:id])
|
||||
authorize @chat_channel_membership
|
||||
@chat_channel_membership.update(permitted_params)
|
||||
if @chat_channel_membership.errors.any?
|
||||
render json: { success: false, errors: @chat_channel_membership.errors.full_messages,
|
||||
message: "Failed to update settings." }, status: :bad_request
|
||||
else
|
||||
render json: { success: true, message: "Personal settings updated." }, status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def leave_membership
|
||||
chat_channel_membership = ChatChannelMembership.find_by(id: params[:id])
|
||||
authorize chat_channel_membership
|
||||
channel_name = chat_channel_membership.chat_channel.channel_name
|
||||
send_chat_action_message("@#{current_user.username} left #{chat_channel_membership.channel_name}", current_user,
|
||||
chat_channel_membership.chat_channel_id, "left_channel")
|
||||
chat_channel_membership.update(status: "left_channel")
|
||||
message = "You have left the channel #{channel_name}. It may take a moment to be removed from your list."
|
||||
if chat_channel_membership.errors.any?
|
||||
render json: { success: false, message: "Failed to update membership",
|
||||
errors: chat_channel_membership.errors.full_messages }, status: :bad_request
|
||||
else
|
||||
render json: { success: true, message: message }, status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def request_details
|
||||
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.flat_map(&:requested_memberships)
|
||||
@user_invitations = ChatChannelMembership.where(
|
||||
user_id: current_user.id,
|
||||
status: %w[pending],
|
||||
).order("created_at DESC")
|
||||
end
|
||||
|
||||
def update_membership_role
|
||||
@chat_channel = ChatChannel.find_by(id: params[:id])
|
||||
authorize @chat_channel, :update?
|
||||
membership = ChatChannelMembership.find_by(
|
||||
id: channel_membership_params[:membership_id],
|
||||
chat_channel_id: @chat_channel.id,
|
||||
)
|
||||
|
||||
membership.update(role: channel_membership_params[:role])
|
||||
if membership.errors.any?
|
||||
render json: {
|
||||
success: false,
|
||||
message: "Failed to update membership",
|
||||
errors: chat_channel_membership.errors.full_messages
|
||||
}, status: :bad_request
|
||||
else
|
||||
role = membership.reload.role
|
||||
send_chat_action_message(
|
||||
"@#{membership.user.username} role is updated as #{role}",
|
||||
current_user, @chat_channel.id,
|
||||
"updated"
|
||||
)
|
||||
|
||||
render json: { success: true, message: "User Membership is updated" }, status: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def join_channel_invitation
|
||||
@chat_channel = ChatChannel.find_by(slug: params[:channel_slug])
|
||||
authorize @chat_channel
|
||||
invite_cache_key = "chat-channel-invite-#{@chat_channel.id}"
|
||||
invitation_slug = Rails.cache.read(invite_cache_key)
|
||||
existing_membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: @chat_channel.id)
|
||||
redirect_to URI.parse("/connect/#{@chat_channel.slug}").path if existing_membership&.status == "active"
|
||||
@link_expired = true if invitation_slug != params[:invitation_slug]
|
||||
end
|
||||
|
||||
def joining_invitation_response
|
||||
chat_channel = ChatChannel.find_by(id: params[:chat_channel_id])
|
||||
authorize chat_channel
|
||||
if params[:user_action] == "accept"
|
||||
membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: chat_channel.id)
|
||||
if !membership
|
||||
membership = ChatChannelMembership.create(user_id: current_user.id, chat_channel_id: chat_channel.id)
|
||||
unless membership&.errors&.any?
|
||||
send_chat_action_message("@#{membership.user.username} join the channel", current_user, chat_channel.id,
|
||||
"joined")
|
||||
end
|
||||
elsif membership.status != "active"
|
||||
# This check checks if the user already has the chatChannelMembership with the status pending, joining_request
|
||||
# Then update it to as active.
|
||||
membership.update(role: "member", status: "active")
|
||||
send_chat_action_message("@#{membership.user.username} join the channel", current_user,
|
||||
membership.chat_channel_id, "joined")
|
||||
end
|
||||
|
||||
if membership&.errors&.any?
|
||||
flash[:settings_notice] = membership.errors.full_messages
|
||||
redirect_to root_path
|
||||
end
|
||||
|
||||
redirect_to connect_path(chat_channel.slug)
|
||||
else
|
||||
redirect_to root_path
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
|
||||
end
|
||||
|
||||
def channel_membership_params
|
||||
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames, :membership_id, :role)
|
||||
end
|
||||
|
||||
def respond_to_invitation(previous_status)
|
||||
if permitted_params[:user_action] == "accept"
|
||||
@chat_channel_membership.update(status: "active")
|
||||
channel_name = @chat_channel_membership.chat_channel.channel_name
|
||||
|
||||
if previous_status == "pending"
|
||||
send_chat_action_message(
|
||||
"@#{current_user.username} joined #{@chat_channel_membership.channel_name}",
|
||||
current_user,
|
||||
@chat_channel_membership.chat_channel_id,
|
||||
"joined",
|
||||
)
|
||||
|
||||
notice = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list."
|
||||
else
|
||||
send_chat_action_message(
|
||||
"@#{current_user.username} added @#{@chat_channel_membership.user.username}",
|
||||
current_user,
|
||||
@chat_channel_membership.chat_channel_id,
|
||||
"joined",
|
||||
)
|
||||
|
||||
NotifyMailer
|
||||
.with(membership: @chat_channel_membership, inviter: @chat_channel_membership.user)
|
||||
.channel_invite_email
|
||||
.deliver_later
|
||||
|
||||
notice = "Accepted request of #{@chat_channel_membership.user.username} to join #{channel_name}."
|
||||
end
|
||||
else
|
||||
@chat_channel_membership.update(status: "rejected")
|
||||
notice = "Invitation rejected."
|
||||
end
|
||||
|
||||
membership_user = format_membership(@chat_channel_membership)
|
||||
flash[:settings_notice] = notice
|
||||
|
||||
respond_to do |format|
|
||||
format.html { redirect_to chat_channel_memberships_path }
|
||||
format.json do
|
||||
render json: {
|
||||
status: "success",
|
||||
message: flash[:settings_notice],
|
||||
success: true,
|
||||
membership: membership_user
|
||||
}, status: :ok
|
||||
end
|
||||
end
|
||||
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) unless message.left_channel?
|
||||
end
|
||||
|
||||
def user_not_authorized
|
||||
render json: { success: false, message: "User not authorized" }, status: :unauthorized
|
||||
end
|
||||
|
||||
def record_not_found
|
||||
render json: { success: false, message: "not found" }, status: :not_found
|
||||
end
|
||||
end
|
||||
|
|
@ -1,268 +0,0 @@
|
|||
class ChatChannelsController < ApplicationController
|
||||
before_action :authenticate_user!, only: %i[moderate]
|
||||
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
|
||||
|
||||
def index
|
||||
case params[:state]
|
||||
when "unopened"
|
||||
authorize ChatChannel
|
||||
@chat_channels_memberships = unopened_json_response
|
||||
render "index", formats: :json
|
||||
when "unopened_ids"
|
||||
authorize ChatChannel
|
||||
@unopened_ids = unopened_ids_response
|
||||
render json: { unopened_ids: @unopened_ids }
|
||||
when "pending"
|
||||
authorize ChatChannel
|
||||
@chat_channels_memberships = pending_json_response
|
||||
render "index", formats: :json
|
||||
when "joining_request"
|
||||
authorize ChatChannel
|
||||
@chat_channels_memberships = joining_request_json_response
|
||||
render "index", formats: :json
|
||||
else
|
||||
skip_authorization
|
||||
render_channels_html
|
||||
end
|
||||
end
|
||||
|
||||
def show
|
||||
@chat_messages = @chat_channel.messages
|
||||
.includes(:user)
|
||||
.order(created_at: :desc)
|
||||
.offset(params[:message_offset])
|
||||
.limit(50)
|
||||
end
|
||||
|
||||
def create
|
||||
authorize ChatChannel
|
||||
@chat_channel = current_user.chat_channels.create(
|
||||
channel_type: "invite_only",
|
||||
channel_name: chat_channel_params[:channel_name],
|
||||
slug: chat_channel_params[:slug],
|
||||
)
|
||||
render_chat_channel
|
||||
end
|
||||
|
||||
def update
|
||||
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: Settings::General.mascot_user_id, chat_channel_id: chat_channel.id,
|
||||
role: "member", status: "active")
|
||||
else
|
||||
ChatChannelMembership.find_by(user_id: Settings::General.mascot_user_id)&.destroy
|
||||
end
|
||||
flash[:settings_notice] = "Channel settings updated."
|
||||
end
|
||||
current_user_membership = @chat_channel.mod_memberships.find_by!(user: current_user)
|
||||
|
||||
redirect_to edit_chat_channel_membership_path(current_user_membership)
|
||||
end
|
||||
|
||||
def update_channel
|
||||
@chat_channel.update(chat_channel_params)
|
||||
if @chat_channel.errors.any?
|
||||
render json: { success: false, errors: @chat_channel.errors.full_messages,
|
||||
message: "Channel settings updation failed. Try again later." }, success: :bad_request
|
||||
else
|
||||
if chat_channel_params[:discoverable]
|
||||
ChatChannelMembership.create(user_id: Settings::General.mascot_user_id, chat_channel_id: @chat_channel.id,
|
||||
role: "member", status: "active")
|
||||
else
|
||||
ChatChannelMembership.find_by(user_id: Settings::General.mascot_user_id)&.destroy
|
||||
end
|
||||
render json: { success: true, message: "Channel settings updated.", data: {} }, success: :ok
|
||||
end
|
||||
end
|
||||
|
||||
def open
|
||||
membership = @chat_channel.chat_channel_memberships.where(user_id: current_user.id).first
|
||||
membership.update(last_opened_at: 1.second.from_now, has_unopened_messages: false)
|
||||
send_open_notification
|
||||
render json: { status: "success", channel: params[:id] }, status: :ok
|
||||
end
|
||||
|
||||
def moderate
|
||||
chat_channel = ChatChannel.find_by(id: params[:id])
|
||||
authorize chat_channel
|
||||
command, username = chat_channel_params[:command].split
|
||||
case command
|
||||
when "/ban"
|
||||
user = User.find_by(username: username)
|
||||
membership = user&.chat_channel_memberships&.find_by(chat_channel: chat_channel)
|
||||
if user && membership
|
||||
user.add_role(:suspended)
|
||||
user.messages.where(chat_channel: chat_channel).delete_all
|
||||
membership.update(status: "removed_from_channel")
|
||||
Pusher.trigger(chat_channel.pusher_channels, "user-banned", { userId: user.id }.to_json)
|
||||
render json: { status: "moderation-success", message: "#{username} was suspended.", userId: user.id,
|
||||
chatChannelId: chat_channel.id }, status: :ok
|
||||
else
|
||||
render json: {
|
||||
status: "error",
|
||||
message: "Suspend failed. user with username '#{username}' not found in this channel."
|
||||
}, status: :bad_request
|
||||
end
|
||||
when "/unban"
|
||||
user = User.find_by(username: username)
|
||||
if user
|
||||
user.remove_role(:suspended)
|
||||
render json: { status: "moderation-success", message: "#{username} was unsuspended." }, status: :ok
|
||||
else
|
||||
render json: {
|
||||
status: "error",
|
||||
message: "Unsuspend failed. User with username '#{username}' not found in this channel."
|
||||
}, status: :bad_request
|
||||
end
|
||||
when "/clearchannel"
|
||||
@chat_channel.clear_channel
|
||||
render json: { status: "success", message: "cleared!" }, status: :ok
|
||||
else
|
||||
render json: { status: "error", message: "invalid command" }, status: :bad_request
|
||||
end
|
||||
end
|
||||
|
||||
def create_chat
|
||||
chat_recipient = User.find(params[:user_id])
|
||||
valid_listing = Listing.where(user_id: params[:user_id], contact_via_connect: true).limit(1)
|
||||
authorize ChatChannel
|
||||
|
||||
if chat_recipient.setting.inbox_type == "open" || valid_listing.length == 1
|
||||
chat = ChatChannels::CreateWithUsers.call(users: [current_user, chat_recipient], channel_type: "direct")
|
||||
message_markdown = params[:message]
|
||||
message = Message.new(
|
||||
chat_channel: chat,
|
||||
message_markdown: message_markdown,
|
||||
user: current_user,
|
||||
)
|
||||
chat.messages.append(message)
|
||||
render json: { status: "success", message: "chat channel created!" }, status: :ok
|
||||
else
|
||||
render json: { status: "error", message: "not allowed!" }, status: :bad_request
|
||||
end
|
||||
rescue StandardError => e
|
||||
render json: { status: "error", message: e.message }, status: :bad_request
|
||||
end
|
||||
|
||||
def block_chat
|
||||
chat_channel = ChatChannel.find(params[:chat_id])
|
||||
authorize chat_channel
|
||||
chat_channel.status = "blocked"
|
||||
chat_channel.save
|
||||
render json: { status: "success", message: "chat channel blocked" }, status: :ok
|
||||
end
|
||||
|
||||
# NOTE: this is part of an effort of moving some things from the external to
|
||||
# the internal API. No behavior was changes as part of this refactoring, so
|
||||
# this action is a bit unusual.
|
||||
def channel_info
|
||||
skip_authorization
|
||||
|
||||
@chat_channel =
|
||||
ChatChannel
|
||||
.select(CHANNEL_ATTRIBUTES_FOR_SERIALIZATION)
|
||||
.find_by(id: params[:id])
|
||||
|
||||
return if @chat_channel&.has_member?(current_user)
|
||||
|
||||
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
|
||||
@chat_channel = ChatChannel.find_by(id: params[:id]) || not_found
|
||||
authorize @chat_channel
|
||||
end
|
||||
|
||||
def chat_channel_params
|
||||
params.require(:chat_channel).permit(policy(ChatChannel).permitted_attributes)
|
||||
end
|
||||
|
||||
def render_channels_html
|
||||
return unless current_user && params[:slug]
|
||||
|
||||
slug = if params[:slug]&.start_with?("@")
|
||||
[current_user.username, params[:slug].delete("@")].sort.join("/")
|
||||
else
|
||||
params[:slug]
|
||||
end
|
||||
chat_channel = ChatChannel.find_by(slug: slug)
|
||||
return unless chat_channel
|
||||
|
||||
membership = chat_channel.chat_channel_memberships.find_by(user_id: current_user.id)
|
||||
@active_channel = membership&.status == "active" ? chat_channel : nil
|
||||
end
|
||||
|
||||
def render_chat_channel
|
||||
if @chat_channel.valid?
|
||||
render json: { status: "success",
|
||||
chat_channel: @chat_channel.to_json(only: %i[channel_name slug]) },
|
||||
status: :ok
|
||||
else
|
||||
render json: { errors: @chat_channel.errors.full_messages }
|
||||
end
|
||||
end
|
||||
|
||||
def send_open_notification
|
||||
adjusted_slug = if @chat_channel.group?
|
||||
@chat_channel.adjusted_slug
|
||||
else
|
||||
@chat_channel.adjusted_slug(current_user)
|
||||
end
|
||||
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
|
||||
|
|
@ -80,7 +80,7 @@ module ListingsToolkit
|
|||
|
||||
ALLOWED_PARAMS = %i[
|
||||
title body_markdown listing_category_id tag_list
|
||||
expires_at contact_via_connect location organization_id action
|
||||
expires_at location organization_id action
|
||||
].freeze
|
||||
|
||||
# Filter for a set of known safe params
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class EmailSubscriptionsController < ApplicationController
|
|||
email_comment_notifications: "comment notifications",
|
||||
email_follower_notifications: "follower notifications",
|
||||
email_mention_notifications: "mention notifications",
|
||||
email_connect_messages: "#{Settings::Community.community_name} connect messages",
|
||||
email_unread_notifications: "unread notifications",
|
||||
email_badge_notifications: "badge notifications"
|
||||
}.freeze
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ class ListingsController < ApplicationController
|
|||
|
||||
INDEX_JSON_OPTIONS = {
|
||||
only: %i[
|
||||
title processed_html tag_list category id user_id slug contact_via_connect location bumped_at
|
||||
title processed_html tag_list category id user_id slug location bumped_at
|
||||
originally_published_at
|
||||
],
|
||||
methods: %i[category],
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
class MessagesController < ApplicationController
|
||||
before_action :set_message, only: %i[destroy update]
|
||||
before_action :authenticate_user!, only: %i[create]
|
||||
include MessagesHelper
|
||||
|
||||
def create
|
||||
@message = Message.new(message_params)
|
||||
@message.user_id = session_current_user_id
|
||||
@temp_message_id = (0...20).map { ("a".."z").to_a[rand(8)] }.join
|
||||
authorize @message
|
||||
|
||||
# sending temp message only to sender
|
||||
pusher_message_created(true, @message, @temp_message_id)
|
||||
if @message.save
|
||||
pusher_message_created(false, @message, @temp_message_id)
|
||||
notify_mentioned_users(@mentioned_users_id)
|
||||
render json: { status: "success", message: { temp_id: @temp_message_id, id: @message.id } }, status: :created
|
||||
else
|
||||
render json: {
|
||||
status: "error",
|
||||
message: {
|
||||
chat_channel_id: @message.chat_channel_id,
|
||||
message: @message.errors.full_messages,
|
||||
type: "error"
|
||||
}
|
||||
}, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
authorize @message
|
||||
|
||||
if @message.valid?
|
||||
begin
|
||||
Pusher.trigger(@message.chat_channel.pusher_channels, "message-deleted", @message.to_json)
|
||||
rescue Pusher::Error => e
|
||||
Honeybadger.notify(e)
|
||||
end
|
||||
end
|
||||
|
||||
if @message.destroy
|
||||
render json: { status: "success", message: "Message was deleted" }
|
||||
else
|
||||
render json: {
|
||||
status: "error",
|
||||
message: {
|
||||
chat_channel_id: @message.chat_channel_id,
|
||||
message: @message.errors.full_messages,
|
||||
type: "error"
|
||||
}
|
||||
}, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
def update
|
||||
authorize @message
|
||||
|
||||
if @message.update(permitted_attributes(@message).merge(edited_at: Time.zone.now))
|
||||
if @message.valid?
|
||||
begin
|
||||
message_json = create_pusher_payload(@message, "")
|
||||
Pusher.trigger(@message.chat_channel.pusher_channels, "message-edited", message_json)
|
||||
rescue Pusher::Error => e
|
||||
Honeybadger.notify(e)
|
||||
end
|
||||
end
|
||||
render json: { status: "success", message: "Message was edited" }
|
||||
else
|
||||
render json: {
|
||||
status: "error",
|
||||
message: {
|
||||
chat_channel_id: @message.chat_channel_id,
|
||||
message: @message.errors.full_messages,
|
||||
type: "error"
|
||||
}
|
||||
}, status: :unauthorized
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def message_params
|
||||
@mentioned_users_id = params[:message][:mentioned_users_id]
|
||||
params.require(:message).permit(:message_markdown, :user_id, :chat_channel_id)
|
||||
end
|
||||
|
||||
def set_message
|
||||
@message = Message.find(params[:id])
|
||||
end
|
||||
|
||||
def user_not_authorized
|
||||
respond_to do |format|
|
||||
format.json do
|
||||
render json: {
|
||||
status: "error",
|
||||
message: {
|
||||
chat_channel_id: message_params[:chat_channel_id],
|
||||
message: "You can not do that because you are suspended",
|
||||
type: "error"
|
||||
}
|
||||
}, status: :unauthorized
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notify_mentioned_users(user_ids)
|
||||
# If @all is mentioned then we get an array of all of the channel's users IDs from the channel
|
||||
# https://github.com/forem/forem/blob/9bdef4d4ae0b41612001a62c2409121b654bf71f/app/javascript/chat/chat.jsx#L1562
|
||||
return unless user_ids
|
||||
|
||||
message_json = create_pusher_payload(@message, @temp_message_id)
|
||||
|
||||
user_ids.each do |id|
|
||||
Pusher.trigger(ChatChannel.pm_notifications_channel(id), "mentioned", message_json)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -22,7 +22,6 @@ class ModerationsController < ApplicationController
|
|||
@articles = articles.includes(:user).to_json(JSON_OPTIONS)
|
||||
@tag = Tag.find_by(name: params[:tag]) || not_found if params[:tag].present?
|
||||
@current_user_tags = current_user.moderator_for_tags
|
||||
@community_mod_channel = current_user.chat_channels.find_by("channel_name LIKE ?", "Community Mods: Team%")
|
||||
end
|
||||
|
||||
def article
|
||||
|
|
|
|||
|
|
@ -1,27 +0,0 @@
|
|||
class PusherController < ApplicationController
|
||||
def auth
|
||||
if valid_channel
|
||||
response = Pusher.authenticate(params[:channel_name], params[:socket_id],
|
||||
user_id: current_user.id) # => required
|
||||
render json: response
|
||||
else
|
||||
render json: { text: "Forbidden", status: "403" }
|
||||
end
|
||||
end
|
||||
|
||||
def valid_channel
|
||||
valid_private_dm_channel || valid_private_group_channel
|
||||
end
|
||||
|
||||
def valid_private_dm_channel
|
||||
current_user && params[:channel_name] == ChatChannel.pm_notifications_channel(current_user.id)
|
||||
end
|
||||
|
||||
def valid_private_group_channel
|
||||
return false unless params[:channel_name].include?("private-channel-")
|
||||
|
||||
id = params[:channel_name].split("-").last
|
||||
channel = ChatChannel.find(id)
|
||||
channel.has_member?(current_user)
|
||||
end
|
||||
end
|
||||
|
|
@ -1,17 +1,8 @@
|
|||
class SearchController < ApplicationController
|
||||
before_action :authenticate_user!, only: %i[tags chat_channels reactions usernames]
|
||||
before_action :authenticate_user!, only: %i[tags reactions usernames]
|
||||
before_action :format_integer_params
|
||||
before_action :sanitize_params, only: %i[listings reactions feed_content]
|
||||
|
||||
CHAT_CHANNEL_PARAMS = %i[
|
||||
channel_status
|
||||
channel_type
|
||||
page
|
||||
per_page
|
||||
status
|
||||
user_id
|
||||
].freeze
|
||||
|
||||
LISTINGS_PARAMS = [
|
||||
:category,
|
||||
:listing_search,
|
||||
|
|
@ -65,23 +56,6 @@ class SearchController < ApplicationController
|
|||
render json: { result: result }
|
||||
end
|
||||
|
||||
def chat_channels
|
||||
user_ids =
|
||||
if chat_channel_params[:user_id].present?
|
||||
[current_user.id, Settings::General.mascot_user_id, chat_channel_params[:user_id]].reject(&:blank?)
|
||||
else
|
||||
[current_user.id]
|
||||
end
|
||||
|
||||
result = Search::ChatChannelMembership.search_documents(
|
||||
user_ids: user_ids,
|
||||
page: chat_channel_params[:page],
|
||||
per_page: chat_channel_params[:per_page],
|
||||
)
|
||||
|
||||
render json: { result: result }
|
||||
end
|
||||
|
||||
def listings
|
||||
result = Search::Listing.search_documents(
|
||||
category: listing_params[:category],
|
||||
|
|
@ -193,10 +167,6 @@ class SearchController < ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def chat_channel_params
|
||||
params.permit(CHAT_CHANNEL_PARAMS)
|
||||
end
|
||||
|
||||
def listing_params
|
||||
params.permit(LISTINGS_PARAMS)
|
||||
end
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ class UserBlocksController < ApplicationController
|
|||
@user_block.config = "default"
|
||||
|
||||
if @user_block.save
|
||||
UserBlocks::ChannelHandler.new(@user_block).block_chat_channel
|
||||
current_user.stop_following(@user_block.blocked)
|
||||
@user_block.blocked.stop_following(current_user)
|
||||
render json: { result: "blocked" }
|
||||
|
|
@ -39,7 +38,6 @@ class UserBlocksController < ApplicationController
|
|||
authorize @user_block
|
||||
|
||||
if @user_block.destroy
|
||||
UserBlocks::ChannelHandler.new(@user_block).unblock_chat_channel
|
||||
render json: { result: "unblocked" }
|
||||
else
|
||||
render json: { error: @user_block.errors_as_sentence, status: 422 }, status: :unprocessable_entity
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ module Users
|
|||
ALLOWED_PARAMS = %i[email_badge_notifications
|
||||
email_comment_notifications
|
||||
email_community_mod_newsletter
|
||||
email_connect_messages
|
||||
email_digest_periodic
|
||||
email_follower_notifications
|
||||
email_membership_newsletter
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
module ApplicationHelper
|
||||
# rubocop:disable Performance/OpenStruct
|
||||
# rubocop:disable Style/OpenStructUse, Performance/OpenStruct
|
||||
USER_COLORS = ["#19063A", "#dce9f3"].freeze
|
||||
|
||||
DELETED_USER = OpenStruct.new(
|
||||
|
|
@ -11,7 +11,7 @@ module ApplicationHelper
|
|||
twitter_username: nil,
|
||||
github_username: nil,
|
||||
)
|
||||
# rubocop:enable Performance/OpenStruct
|
||||
# rubocop:enable Style/OpenStructUse, Performance/OpenStruct
|
||||
|
||||
LARGE_USERBASE_THRESHOLD = 1000
|
||||
|
||||
|
|
|
|||
|
|
@ -1,36 +0,0 @@
|
|||
module ChatChannelMembershipsHelper
|
||||
def formatted_membership_user(memberships)
|
||||
memberships.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),
|
||||
chat_channel_name: membership.chat_channel.channel_name,
|
||||
chat_channel_id: membership.chat_channel.id,
|
||||
slug: membership.chat_channel.slug
|
||||
}
|
||||
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
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
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,41 +0,0 @@
|
|||
module MessagesHelper
|
||||
def create_pusher_payload(new_message, temp_id)
|
||||
payload = {
|
||||
temp_id: temp_id,
|
||||
id: new_message.id,
|
||||
user_id: new_message.user.id,
|
||||
chat_channel_id: new_message.chat_channel.id,
|
||||
chat_channel_adjusted_slug: new_message.chat_channel.adjusted_slug(current_user, "sender"),
|
||||
channel_type: new_message.chat_channel.channel_type,
|
||||
username: new_message.user.username,
|
||||
profile_image_url: Images::Profile.call(new_message.user.profile_image_url, length: 90),
|
||||
message: new_message.message_html,
|
||||
markdown: new_message.message_markdown,
|
||||
edited_at: new_message.edited_at,
|
||||
timestamp: Time.current,
|
||||
color: new_message.preferred_user_color,
|
||||
reception_method: "pushed",
|
||||
action: new_message.chat_action
|
||||
}
|
||||
|
||||
if new_message.chat_channel.group?
|
||||
payload[:chat_channel_adjusted_slug] = new_message.chat_channel.adjusted_slug
|
||||
end
|
||||
payload.to_json
|
||||
end
|
||||
|
||||
def pusher_message_created(is_single, message, temp_message_id)
|
||||
return unless message.valid?
|
||||
|
||||
begin
|
||||
message_json = create_pusher_payload(message, temp_message_id)
|
||||
if is_single
|
||||
Pusher.trigger(ChatChannel.pm_notifications_channel(message.user_id), "message-created", message_json)
|
||||
else
|
||||
Pusher.trigger(message.chat_channel.pusher_channels, "message-created", message_json)
|
||||
end
|
||||
rescue Pusher::Error => e
|
||||
Honeybadger.notify(e)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -23,6 +23,6 @@ module NotificationsHelper
|
|||
)
|
||||
else
|
||||
I18n.t(action, user: key_to_link.call("user"))
|
||||
end.html_safe
|
||||
end.html_safe # rubocop:disable Rails/OutputSafety
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defaultMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import { Membership } from './Membership';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const ActiveMembershipsSection = ({
|
||||
activeMemberships,
|
||||
removeMembership,
|
||||
currentMembershipRole,
|
||||
toggleScreens,
|
||||
}) => {
|
||||
const activeMembershipList = activeMemberships.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="active-memberships"
|
||||
className="p-4 grid gap-2 crayons-card mb-4"
|
||||
data-active-count={activeMemberships ? activeMemberships.length : 0}
|
||||
>
|
||||
<h3 className="mb-2 active_members">Members</h3>
|
||||
{activeMembershipList.map((activeMembership) => (
|
||||
<Membership
|
||||
membership={activeMembership}
|
||||
removeMembership={removeMembership}
|
||||
membershipType="active"
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
className="active-member"
|
||||
/>
|
||||
))}
|
||||
<div className="row align-center">
|
||||
<Button
|
||||
className="align-center view-all-memberships"
|
||||
size="s"
|
||||
onClick={toggleScreens}
|
||||
type="button"
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActiveMembershipsSection.propTypes = {
|
||||
activeMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
toggleScreens: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ChannelDescriptionSection = ({
|
||||
channelName,
|
||||
channelDescription,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
return (
|
||||
<div className="p-4 grid gap-2 crayons-card mb-4 channel_details">
|
||||
<h2 className="mb-1 channel_title">{channelName}</h2>
|
||||
<p>{channelDescription}</p>
|
||||
<p className="fw-bold">You are a channel {currentMembershipRole}</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ChannelDescriptionSection.propTypes = {
|
||||
channelName: PropTypes.string.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
channelDescription: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defaultMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import { ActiveMembershipsSection } from './ActiveMembershipsSection';
|
||||
import { PendingMembershipSection } from './PendingMembershipSection';
|
||||
import { RequestedMembershipSection } from './RequestedMembershipSection';
|
||||
|
||||
export const ChatChannelMembershipSection = ({
|
||||
pendingMemberships,
|
||||
requestedMemberships,
|
||||
chatChannelAcceptMembership,
|
||||
activeMemberships,
|
||||
removeMembership,
|
||||
currentMembershipRole,
|
||||
toggleScreens,
|
||||
}) => {
|
||||
return (
|
||||
<div className="membership-list">
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
removeMembership={removeMembership}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
toggleScreens={toggleScreens}
|
||||
/>
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
removeMembership={removeMembership}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>
|
||||
<RequestedMembershipSection
|
||||
requestedMemberships={requestedMemberships}
|
||||
removeMembership={removeMembership}
|
||||
chatChannelAcceptMembership={chatChannelAcceptMembership}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ChatChannelMembershipSection.propTypes = {
|
||||
pendingMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
requestedMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
activeMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
toggleScreens: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,440 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {
|
||||
getChannelDetails,
|
||||
updatePersonalChatChannelNotificationSettings,
|
||||
rejectChatChannelJoiningRequest,
|
||||
acceptChatChannelJoiningRequest,
|
||||
updateChatChannelDescription,
|
||||
sendChatChannelInvitation,
|
||||
leaveChatChannelMembership,
|
||||
updateMembershipRole,
|
||||
} from '../actions/chat_channel_setting_actions';
|
||||
|
||||
import { addSnackbarItem } from '../../Snackbar';
|
||||
import { ManageActiveMembership } from './MembershipManager/ManageActiveMembership';
|
||||
import { ChatChannelSettingsSection } from './ChatChannelSettingsSection';
|
||||
|
||||
export class ChatChannelSettings extends Component {
|
||||
static propTypes = {
|
||||
handleLeavingChannel: PropTypes.func.isRequired,
|
||||
activeMembershipId: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
successMessages: null,
|
||||
errorMessages: null,
|
||||
activeMemberships: [],
|
||||
pendingMemberships: [],
|
||||
requestedMemberships: [],
|
||||
chatChannel: null,
|
||||
currentMembership: null,
|
||||
activeMembershipId: null,
|
||||
channelDescription: null,
|
||||
channelDiscoverable: null,
|
||||
invitationUsernames: null,
|
||||
showGlobalBadgeNotification: null,
|
||||
displaySettings: true,
|
||||
displayMembershipManager: false,
|
||||
invitationLink: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateChannelDetails();
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
const { activeMembershipId } = this.props;
|
||||
this.setState({
|
||||
activeMembershipId,
|
||||
});
|
||||
}
|
||||
|
||||
updateChannelDetails = () => {
|
||||
const { activeMembershipId } = this.props;
|
||||
|
||||
getChannelDetails(activeMembershipId)
|
||||
.then((response) => {
|
||||
if (response.success) {
|
||||
const { result } = response;
|
||||
this.setState({
|
||||
chatChannel: result.chat_channel,
|
||||
activeMemberships: result.memberships.active,
|
||||
pendingMemberships: result.memberships.pending,
|
||||
requestedMemberships: result.memberships.requested,
|
||||
currentMembership: result.current_membership,
|
||||
channelDescription: result.chat_channel.description,
|
||||
channelDiscoverable: result.chat_channel.discoverable,
|
||||
showGlobalBadgeNotification:
|
||||
result.current_membership.show_global_badge_notification,
|
||||
invitationLink: result.invitation_link,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: error.message,
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
handleDescriptionChange = (e) => {
|
||||
const description = e.target.value;
|
||||
this.setState({
|
||||
channelDescription: description,
|
||||
});
|
||||
};
|
||||
|
||||
handlePersonChannelSetting = (e) => {
|
||||
const status = e.target.checked;
|
||||
this.setState({
|
||||
showGlobalBadgeNotification: status,
|
||||
});
|
||||
};
|
||||
|
||||
updateCurrentMembershipNotificationSettings = async () => {
|
||||
const { currentMembership, showGlobalBadgeNotification } = this.state;
|
||||
const response = await updatePersonalChatChannelNotificationSettings(
|
||||
currentMembership.id,
|
||||
showGlobalBadgeNotification,
|
||||
);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
currentMembership: {
|
||||
...prevState.currentMembership,
|
||||
show_global_badge_notification: showGlobalBadgeNotification,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
showGlobalBadgeNotification:
|
||||
currentMembership.show_global_badge_notification,
|
||||
});
|
||||
}
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
chatChannelRemoveMembership = async (membershipId, membershipStatus) => {
|
||||
const { chatChannel } = this.state;
|
||||
const response = await rejectChatChannelJoiningRequest(
|
||||
chatChannel.id,
|
||||
membershipId,
|
||||
membershipStatus,
|
||||
);
|
||||
return response;
|
||||
};
|
||||
|
||||
filterMemberships = (memberships, membershipId) => {
|
||||
const filteredMembership = memberships.filter(
|
||||
(membership) => membership.membership_id !== Number(membershipId),
|
||||
);
|
||||
return filteredMembership;
|
||||
};
|
||||
|
||||
removeMembership = async (e) => {
|
||||
const { membershipId, membershipStatus } = e.target.dataset;
|
||||
const response = await this.chatChannelRemoveMembership(
|
||||
membershipId,
|
||||
membershipStatus,
|
||||
);
|
||||
const { message } = response;
|
||||
this.updateMemberships(membershipId, response, membershipStatus);
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
updateMemberships = (membershipId, response, membershipStatus) => {
|
||||
if (response.success) {
|
||||
this.updateChannelDetails();
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
activeMemberships:
|
||||
membershipStatus === 'active'
|
||||
? this.filterMemberships(
|
||||
prevState.activeMemberships,
|
||||
membershipId,
|
||||
)
|
||||
: prevState.activeMemberships,
|
||||
pendingMemberships:
|
||||
membershipStatus === 'pending'
|
||||
? this.filterMemberships(
|
||||
prevState.pendingMemberships,
|
||||
membershipId,
|
||||
)
|
||||
: prevState.pendingMemberships,
|
||||
requestedMemberships:
|
||||
membershipStatus === 'joining_request'
|
||||
? this.filterMemberships(
|
||||
prevState.requestedMemberships,
|
||||
membershipId,
|
||||
)
|
||||
: prevState.requestedMembership,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
chatChannelAcceptMembership = async (e) => {
|
||||
const { chatChannel } = this.state;
|
||||
const { membershipId } = e.target.dataset;
|
||||
const response = await acceptChatChannelJoiningRequest(
|
||||
chatChannel.id,
|
||||
membershipId,
|
||||
);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.setState((prevState) => {
|
||||
const filteredRequestedMemberships = prevState.requestedMemberships.filter(
|
||||
(requestedMembership) =>
|
||||
requestedMembership.membership_id !== Number(membershipId),
|
||||
);
|
||||
const updatedActiveMembership = [
|
||||
...prevState.activeMemberships,
|
||||
response.membership,
|
||||
];
|
||||
return {
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
requestedMemberships: filteredRequestedMemberships,
|
||||
activeMemberships: updatedActiveMembership,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
handleChannelDiscoverableStatus = (e) => {
|
||||
const status = e.target.checked;
|
||||
this.setState({
|
||||
channelDiscoverable: status,
|
||||
});
|
||||
};
|
||||
|
||||
handleChannelDescriptionChanges = async () => {
|
||||
const { chatChannel, channelDescription, channelDiscoverable } = this.state;
|
||||
const { id } = chatChannel;
|
||||
const response = await updateChatChannelDescription(
|
||||
id,
|
||||
channelDescription,
|
||||
channelDiscoverable,
|
||||
);
|
||||
const { message } = response;
|
||||
|
||||
if (response.success) {
|
||||
this.updateChannelDetails();
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
chatChannel: {
|
||||
...prevState,
|
||||
description: channelDescription,
|
||||
discoverable: channelDiscoverable,
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
channelDiscoverable: chatChannel.discoverable,
|
||||
});
|
||||
}
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
handleInvitationUsernames = (e) => {
|
||||
const invitationUsernameValue = e.target.value;
|
||||
this.setState({
|
||||
invitationUsernames: invitationUsernameValue,
|
||||
});
|
||||
};
|
||||
|
||||
handleChannelInvitations = async () => {
|
||||
const { invitationUsernames, chatChannel } = this.state;
|
||||
const { id } = chatChannel;
|
||||
const response = await sendChatChannelInvitation(id, invitationUsernames);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.updateChannelDetails();
|
||||
this.setState({
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
invitationUsernames: null,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
handleleaveChannelMembership = async () => {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const actionStatus = confirm(
|
||||
'Are you absolutely sure you want to leave this channel? This action is permanent.',
|
||||
);
|
||||
const { currentMembership } = this.state;
|
||||
if (actionStatus) {
|
||||
const response = await leaveChatChannelMembership(currentMembership.id);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.setState({
|
||||
successMessages: message,
|
||||
errorMessages: null,
|
||||
});
|
||||
this.props.handleLeavingChannel(currentMembership.id);
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
addSnackbarItem({ message });
|
||||
}
|
||||
};
|
||||
|
||||
toggleScreens = () => {
|
||||
const { displaySettings, displayMembershipManager } = this.state;
|
||||
|
||||
this.setState({
|
||||
displaySettings: !displaySettings,
|
||||
displayMembershipManager: !displayMembershipManager,
|
||||
});
|
||||
};
|
||||
|
||||
handleUpdateMembershipRole = async (e) => {
|
||||
const { membershipId, role } = e.target.dataset;
|
||||
const { chatChannel } = this.state;
|
||||
const response = await updateMembershipRole(
|
||||
membershipId,
|
||||
chatChannel.id,
|
||||
role,
|
||||
);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.updateChannelDetails();
|
||||
this.setState((prevState) => {
|
||||
const { activeMemberships } = prevState;
|
||||
const updatedActiveMemberships = activeMemberships.map(
|
||||
(activeMembership) => {
|
||||
if (activeMembership.membership_id === Number(membershipId)) {
|
||||
return { ...activeMembership, role };
|
||||
}
|
||||
return activeMembership;
|
||||
},
|
||||
);
|
||||
return {
|
||||
...prevState,
|
||||
activeMemberships: updatedActiveMemberships,
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
};
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
successMessages: null,
|
||||
errorMessages: response.message,
|
||||
});
|
||||
}
|
||||
|
||||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
chatChannel,
|
||||
currentMembership,
|
||||
activeMemberships,
|
||||
pendingMemberships,
|
||||
requestedMemberships,
|
||||
channelDescription,
|
||||
channelDiscoverable,
|
||||
invitationUsernames,
|
||||
showGlobalBadgeNotification,
|
||||
displaySettings,
|
||||
invitationLink,
|
||||
} = this.state;
|
||||
|
||||
if (!chatChannel) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="activechatchannel__activeArticle channel_settings">
|
||||
<div className="p-4">
|
||||
{displaySettings ? (
|
||||
<ChatChannelSettingsSection
|
||||
channelDiscoverable={channelDiscoverable}
|
||||
updateCurrentMembershipNotificationSettings={
|
||||
this.updateCurrentMembershipNotificationSettings
|
||||
}
|
||||
handleleaveChannelMembership={this.handleleaveChannelMembership}
|
||||
handlePersonChannelSetting={this.handlePersonChannelSetting}
|
||||
handleChannelDescriptionChanges={
|
||||
this.handleChannelDescriptionChanges
|
||||
}
|
||||
handleChannelDiscoverableStatus={
|
||||
this.handleChannelDiscoverableStatus
|
||||
}
|
||||
handleDescriptionChange={this.handleDescriptionChange}
|
||||
handleChannelInvitations={this.handleChannelInvitations}
|
||||
handleInvitationUsernames={this.handleInvitationUsernames}
|
||||
toggleScreens={this.toggleScreens}
|
||||
removeMembership={this.removeMembership}
|
||||
chatChannelAcceptMembership={this.chatChannelAcceptMembership}
|
||||
channelDescription={channelDescription}
|
||||
chatChannel={chatChannel}
|
||||
currentMembership={currentMembership}
|
||||
activeMemberships={activeMemberships}
|
||||
pendingMemberships={pendingMemberships}
|
||||
requestedMemberships={requestedMemberships}
|
||||
invitationUsernames={invitationUsernames}
|
||||
showGlobalBadgeNotification={showGlobalBadgeNotification}
|
||||
/>
|
||||
) : (
|
||||
<ManageActiveMembership
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembership={currentMembership}
|
||||
chatChannel={chatChannel}
|
||||
invitationLink={invitationLink}
|
||||
removeMembership={this.removeMembership}
|
||||
handleUpdateMembershipRole={this.handleUpdateMembershipRole}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,103 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ModSection } from './ModSection';
|
||||
import { PersonalSettings } from './PersonalSetting';
|
||||
import { LeaveMembershipSection } from './LeaveMembershipSection';
|
||||
import { ModFaqSection } from './ModFaqSection';
|
||||
import { ChannelDescriptionSection } from './ChannelDescriptionSection';
|
||||
import { ChatChannelMembershipSection } from './ChatChannelMembershipSection';
|
||||
|
||||
export const ChatChannelSettingsSection = ({
|
||||
channelDiscoverable,
|
||||
updateCurrentMembershipNotificationSettings,
|
||||
handleleaveChannelMembership,
|
||||
handlePersonChannelSetting,
|
||||
handleChannelDescriptionChanges,
|
||||
handleChannelDiscoverableStatus,
|
||||
handleDescriptionChange,
|
||||
handleChannelInvitations,
|
||||
handleInvitationUsernames,
|
||||
toggleScreens,
|
||||
removeMembership,
|
||||
chatChannelAcceptMembership,
|
||||
channelDescription,
|
||||
chatChannel,
|
||||
currentMembership,
|
||||
activeMemberships,
|
||||
pendingMemberships,
|
||||
requestedMemberships,
|
||||
invitationUsernames,
|
||||
showGlobalBadgeNotification,
|
||||
}) => (
|
||||
<div>
|
||||
<ChannelDescriptionSection
|
||||
channelName={chatChannel.name}
|
||||
channelDescription={chatChannel.description}
|
||||
currentMembershipRole={currentMembership.role}
|
||||
className="channel-description-section"
|
||||
/>
|
||||
<ChatChannelMembershipSection
|
||||
currentMembershipRole={currentMembership.role}
|
||||
activeMemberships={activeMemberships}
|
||||
removeMembership={removeMembership}
|
||||
pendingMemberships={pendingMemberships}
|
||||
requestedMemberships={requestedMemberships}
|
||||
chatChannelAcceptMembership={chatChannelAcceptMembership}
|
||||
toggleScreens={toggleScreens}
|
||||
className="channel-membership-sections"
|
||||
/>
|
||||
<ModSection
|
||||
invitationUsernames={invitationUsernames}
|
||||
handleInvitationUsernames={handleInvitationUsernames}
|
||||
handleChannelInvitations={handleChannelInvitations}
|
||||
channelDescription={channelDescription}
|
||||
handleDescriptionChange={handleDescriptionChange}
|
||||
channelDiscoverable={channelDiscoverable}
|
||||
handleChannelDiscoverableStatus={handleChannelDiscoverableStatus}
|
||||
handleChannelDescriptionChanges={handleChannelDescriptionChanges}
|
||||
currentMembershipRole={currentMembership.role}
|
||||
className="channel-mod-section"
|
||||
/>
|
||||
<PersonalSettings
|
||||
updateCurrentMembershipNotificationSettings={
|
||||
updateCurrentMembershipNotificationSettings
|
||||
}
|
||||
showGlobalBadgeNotification={showGlobalBadgeNotification}
|
||||
handlePersonChannelSetting={handlePersonChannelSetting}
|
||||
className="channel-personal-seeting"
|
||||
/>
|
||||
<LeaveMembershipSection
|
||||
currentMembershipRole={currentMembership.role}
|
||||
handleleaveChannelMembership={handleleaveChannelMembership}
|
||||
className="channel-leave-membership-section"
|
||||
/>
|
||||
<ModFaqSection
|
||||
currentMembershipRole={currentMembership.role}
|
||||
className="channel-mod-faq"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
ChatChannelSettingsSection.propTypes = {
|
||||
chatChannel: PropTypes.isRequired,
|
||||
currentMembership: PropTypes.isRequired,
|
||||
activeMemberships: PropTypes.isRequired,
|
||||
pendingMemberships: PropTypes.isRequired,
|
||||
requestedMemberships: PropTypes.isRequired,
|
||||
invitationUsernames: PropTypes.string.isRequired,
|
||||
channelDescription: PropTypes.string.isRequired,
|
||||
channelDiscoverable: PropTypes.bool.isRequired,
|
||||
showGlobalBadgeNotification: PropTypes.bool.isRequired,
|
||||
handleleaveChannelMembership: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
toggleScreens: PropTypes.func.isRequired,
|
||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||
handleChannelInvitations: PropTypes.func.isRequired,
|
||||
handleDescriptionChange: PropTypes.func.isRequired,
|
||||
handleChannelDiscoverableStatus: PropTypes.func.isRequired,
|
||||
handleChannelDescriptionChanges: PropTypes.func.isRequired,
|
||||
handlePersonChannelSetting: PropTypes.func.isRequired,
|
||||
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const InviteForm = ({
|
||||
handleChannelInvitations,
|
||||
invitationUsernames,
|
||||
handleInvitationUsernames,
|
||||
}) => {
|
||||
return (
|
||||
<div data-testid="invite-form" className="crayons-card p-4 grid gap-2 mb-4">
|
||||
<div className="crayons-field">
|
||||
<label
|
||||
className="crayons-field__label invitation_form_title"
|
||||
htmlFor="chat_channel_membership_invitation_usernames"
|
||||
>
|
||||
Usernames to invite
|
||||
</label>
|
||||
<input
|
||||
placeholder="Comma separated"
|
||||
className="crayons-textfield"
|
||||
type="text"
|
||||
value={invitationUsernames}
|
||||
name="chat_channel_membership[invitation_usernames]"
|
||||
id="chat_channel_membership_invitation_usernames"
|
||||
onChange={handleInvitationUsernames}
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" onClick={handleChannelInvitations}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
InviteForm.propTypes = {
|
||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||
handleChannelInvitations: PropTypes.func.isRequired,
|
||||
invitationUsernames: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropsType from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const LeaveMembershipSection = ({
|
||||
handleleaveChannelMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'mod') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="crayons-card p-4 grid gap-2 mb-4 leave_membership_section">
|
||||
<h3>Danger Zone</h3>
|
||||
<div>
|
||||
<Button
|
||||
className="leave_button"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={handleleaveChannelMembership}
|
||||
>
|
||||
Leave Channel
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LeaveMembershipSection.propTypes = {
|
||||
handleleaveChannelMembership: PropsType.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,62 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaultMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const Membership = ({
|
||||
membership,
|
||||
removeMembership,
|
||||
membershipType,
|
||||
chatChannelAcceptMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
return (
|
||||
<div className="flex items-center">
|
||||
<a
|
||||
href={`/${membership.username}`}
|
||||
className="chatmessagebody__username--link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-content="sidecar-user"
|
||||
>
|
||||
<span className="crayons-avatar crayons-avatar--l mr-3">
|
||||
<img
|
||||
className="crayons-avatar__image align-middle"
|
||||
src={membership.image}
|
||||
alt={`${membership.name} profile`}
|
||||
/>
|
||||
</span>
|
||||
<span className="mr-2 user_name">{membership.name}</span>
|
||||
</a>
|
||||
{membershipType === 'requested' ? (
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost add-membership"
|
||||
type="button"
|
||||
onClick={chatChannelAcceptMembership}
|
||||
data-membership-id={membership.membership_id}
|
||||
>
|
||||
+
|
||||
</Button>
|
||||
) : null}
|
||||
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost remove-membership"
|
||||
type="button"
|
||||
onClick={removeMembership}
|
||||
data-membership-id={membership.membership_id}
|
||||
data-membership-status={membership.status}
|
||||
>
|
||||
x
|
||||
</Button>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Membership.propTypes = {
|
||||
membership: PropTypes.objectOf(defaultMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
membershipType: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
/* global Runtime */
|
||||
|
||||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const CopyIcon = () => (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon copy-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="fc5f15add1e114844f5e"
|
||||
>
|
||||
<title id="fc5f15add1e114844f5e">Copy Invitation Url</title>
|
||||
<path d="M7 6V3a1 1 0 011-1h12a1 1 0 011 1v14a1 1 0 01-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 013 21l.003-14c0-.552.45-1 1.007-1H7zm2 0h8v10h2V4H9v2zm-2 5v2h6v-2H7zm0 4v2h6v-2H7z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
export class InvitationLinkManager extends Component {
|
||||
static propTypes = {
|
||||
invitationLink: PropTypes.string.isRequired,
|
||||
currentMembership: PropTypes.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
invitationLink: props.invitationLink,
|
||||
showImageCopiedMessage: false,
|
||||
currentMembership: props.currentMembership,
|
||||
};
|
||||
}
|
||||
|
||||
copyText = () => {
|
||||
this.imageMarkdownInput = document.getElementById(
|
||||
'chat-channel-unviation-url',
|
||||
);
|
||||
|
||||
Runtime.copyToClipboard(this.imageMarkdownInput.value).then(() => {
|
||||
this.setState({ showImageCopiedMessage: true });
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
showImageCopiedMessage,
|
||||
invitationLink,
|
||||
currentMembership,
|
||||
} = this.state;
|
||||
|
||||
if (currentMembership.role !== 'mod') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="p-4 grid gap-2 crayons-card my-4 invitation-section">
|
||||
<h3 className="text-center title">Invitation Link</h3>
|
||||
<clipboard-copy
|
||||
onClick={this.copyText}
|
||||
for="chat-channel-unviation-url"
|
||||
aria-live="polite"
|
||||
className="flex items-center flex-1"
|
||||
aria-controls="image-markdown-copy-link-announcer"
|
||||
>
|
||||
<input
|
||||
type="text"
|
||||
className="crayons-textfield mr-2"
|
||||
id="chat-channel-unviation-url"
|
||||
readOnly="true"
|
||||
value={invitationLink}
|
||||
aria-label="invitation-link"
|
||||
/>
|
||||
<Button
|
||||
className="spec__image-markdown-copy"
|
||||
variant="ghost"
|
||||
contentType="icon"
|
||||
icon={CopyIcon}
|
||||
/>
|
||||
<span
|
||||
id="image-markdown-copy-link-announcer"
|
||||
role="alert"
|
||||
className={`fs-s ${showImageCopiedMessage ? '' : 'opacity-0'}`}
|
||||
>
|
||||
Copied!
|
||||
</span>
|
||||
</clipboard-copy>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { MembershipSection } from './MembershipSection';
|
||||
import { InvitationLinkManager } from './InvitationLinkManager';
|
||||
|
||||
export class ManageActiveMembership extends Component {
|
||||
static propTypes = {
|
||||
activeMemberships: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
currentMembership: PropTypes.isRequired,
|
||||
invitationLink: PropTypes.string.isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
handleUpdateMembershipRole: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
activeMemberships: props.activeMemberships,
|
||||
searchMembers: null,
|
||||
listAllMemberShips: props.activeMemberships,
|
||||
currentMembership: props.currentMembership,
|
||||
invitationLink: props.invitationLink,
|
||||
removeMembership: props.removeMembership,
|
||||
handleUpdateMembershipRole: props.handleUpdateMembershipRole,
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
const {
|
||||
activeMemberships,
|
||||
currentMembership,
|
||||
removeMembership,
|
||||
invitationLink,
|
||||
} = this.props;
|
||||
this.setState({
|
||||
listAllMemberShips: activeMemberships,
|
||||
currentMembership,
|
||||
invitationLink,
|
||||
removeMembership,
|
||||
activeMemberships,
|
||||
searchMembers: null,
|
||||
});
|
||||
}
|
||||
|
||||
searchTheMembershipUser = (e) => {
|
||||
const query = e.target?.value?.toLowerCase();
|
||||
|
||||
this.setState((prevState) => {
|
||||
const filteredActiveMemberships = prevState.activeMemberships.filter(
|
||||
(activeMembership) => {
|
||||
const value = activeMembership.name.toLowerCase();
|
||||
return value.includes(query);
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
searchMembers: query,
|
||||
listAllMemberShips: filteredActiveMemberships,
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
searchMembers,
|
||||
listAllMemberShips,
|
||||
currentMembership,
|
||||
invitationLink,
|
||||
removeMembership,
|
||||
handleUpdateMembershipRole,
|
||||
activeMemberships,
|
||||
} = this.state;
|
||||
|
||||
const membershipCount = activeMemberships.length;
|
||||
return (
|
||||
<div className="pt-3">
|
||||
<div className="p-4 grid gap-2 crayons-card my-4 mx-auto membership-manager">
|
||||
<h2 className="text-title">Chat Channel Membership manager</h2>
|
||||
</div>
|
||||
<div className="p-4 grid gap-2 crayons-card my-4">
|
||||
<input
|
||||
type="text"
|
||||
className="crayons-textfield"
|
||||
placeholder="Search Member..."
|
||||
value={searchMembers}
|
||||
name="search-members"
|
||||
onKeyUp={this.searchTheMembershipUser.bind(this)}
|
||||
aria-label="search memberships"
|
||||
/>
|
||||
<div
|
||||
data-spy="scroll"
|
||||
data-offset="5"
|
||||
className="chat_channel-member-list"
|
||||
>
|
||||
<MembershipSection
|
||||
memberships={listAllMemberShips}
|
||||
currentMembership={currentMembership}
|
||||
removeMembership={removeMembership}
|
||||
handleUpdateMembershipRole={handleUpdateMembershipRole}
|
||||
membershipCount={membershipCount}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<InvitationLinkManager
|
||||
invitationLink={invitationLink}
|
||||
currentMembership={currentMembership}
|
||||
className="inviation-link-section"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import adminEmoji from '../../../../assets/images/twemoji/fire.svg';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const Membership = ({
|
||||
membership,
|
||||
currentMembership,
|
||||
removeMembership,
|
||||
handleUpdateMembershipRole,
|
||||
showActionButton,
|
||||
}) => {
|
||||
const addAsModButton =
|
||||
membership.role === 'member' ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="s"
|
||||
onClick={handleUpdateMembershipRole}
|
||||
data-membership-id={membership.membership_id}
|
||||
data-role="mod"
|
||||
>
|
||||
Promote to Mod
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const addAsMemberButton =
|
||||
membership.role === 'mod' ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="s"
|
||||
onClick={handleUpdateMembershipRole}
|
||||
data-membership-id={membership.membership_id}
|
||||
data-role="member"
|
||||
>
|
||||
Remove Mod
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const removeMembershipButton =
|
||||
membership.role === 'member' ? (
|
||||
<Button
|
||||
type="button"
|
||||
size="s"
|
||||
variant="ghost-danger"
|
||||
onClick={removeMembership}
|
||||
data-membership-id={membership.membership_id}
|
||||
data-membership-status={membership.status}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 32.526 32.526"
|
||||
width="18"
|
||||
height="18"
|
||||
className="crayons-icon"
|
||||
data-membership-id={membership.membership_id}
|
||||
data-membership-status={membership.status}
|
||||
>
|
||||
<path
|
||||
fill="#4f5458"
|
||||
data-membership-id={membership.membership_id}
|
||||
data-membership-status={membership.status}
|
||||
d="M32.526 2.828L29.698 0 16.263 13.435 2.828 0 0 2.828l13.435 13.435L0 29.698l2.828 2.828 13.435-13.435 13.435 13.435 2.828-2.828-13.435-13.435z"
|
||||
/>
|
||||
</svg>
|
||||
</Button>
|
||||
) : null;
|
||||
|
||||
const dropdown =
|
||||
currentMembership.role === 'mod' && showActionButton ? (
|
||||
<span className="membership-actions">
|
||||
{removeMembershipButton}
|
||||
{addAsModButton}
|
||||
{addAsMemberButton}
|
||||
</span>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<div className="flex items-center my-3 member-list-item justify-content-between">
|
||||
<div className="">
|
||||
<a
|
||||
href={`/${membership.username}`}
|
||||
className="chatmessagebody__username--link"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-content="sidecar-user"
|
||||
>
|
||||
<span className="crayons-avatar crayons-avatar--l mr-3">
|
||||
<img
|
||||
className="crayons-avatar__image align-middle"
|
||||
src={membership.image}
|
||||
alt={`${membership.name} profile`}
|
||||
/>
|
||||
</span>
|
||||
<span className="mr-2 user_name">{membership.name}</span>
|
||||
<span>
|
||||
{membership.role === 'mod' ? (
|
||||
<img
|
||||
src={adminEmoji}
|
||||
alt="admin emoji"
|
||||
data-content="admin emoji"
|
||||
className="admin-emoji"
|
||||
title="MOD"
|
||||
/>
|
||||
) : null}
|
||||
</span>
|
||||
</a>
|
||||
</div>
|
||||
<div className="">{dropdown}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Membership.propTypes = {
|
||||
membership: PropTypes.objectOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
currentMembership: PropTypes.isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
handleUpdateMembershipRole: PropTypes.func.isRequired,
|
||||
showActionButton: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
|
@ -1,50 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Membership } from './Membership';
|
||||
|
||||
export const MembershipSection = ({
|
||||
memberships,
|
||||
currentMembership,
|
||||
removeMembership,
|
||||
handleUpdateMembershipRole,
|
||||
membershipCount,
|
||||
}) => {
|
||||
if (!memberships || memberships.length === 0) {
|
||||
return <p className="lh-base">No membership</p>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="membership-section">
|
||||
{memberships.map((activeMembership) => (
|
||||
<Membership
|
||||
membership={activeMembership}
|
||||
membershipType="active"
|
||||
currentMembershipRole={() => {}}
|
||||
className="active-member"
|
||||
currentMembership={currentMembership}
|
||||
removeMembership={removeMembership}
|
||||
handleUpdateMembershipRole={handleUpdateMembershipRole}
|
||||
showActionButton={membershipCount > 1}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
MembershipSection.propType = {
|
||||
memberships: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
currentMembership: PropTypes.isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
handleUpdateMembershipRole: PropTypes.func.isRequired,
|
||||
membershipCount: PropTypes.number.isRequired,
|
||||
};
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ModFaqSection = ({ currentMembershipRole }) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="crayons-card grid gap-2 p-4 faq-section">
|
||||
<p className="contact-details">
|
||||
Questions about Connect moderation?
|
||||
<a
|
||||
href="/contact"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mx-2 url-link"
|
||||
>
|
||||
Contact site admins
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModFaqSection.propTypes = {
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
};
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { InviteForm } from './InviteForm';
|
||||
import { SettingsForm } from './SettingsForm';
|
||||
|
||||
export const ModSection = ({
|
||||
handleChannelInvitations,
|
||||
invitationUsernames,
|
||||
handleInvitationUsernames,
|
||||
channelDescription,
|
||||
handleDescriptionChange,
|
||||
channelDiscoverable,
|
||||
handleChannelDiscoverableStatus,
|
||||
handleChannelDescriptionChanges,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mod-section">
|
||||
<InviteForm
|
||||
handleInvitationUsernames={handleInvitationUsernames}
|
||||
invitationUsernames={invitationUsernames}
|
||||
handleChannelInvitations={handleChannelInvitations}
|
||||
/>
|
||||
<SettingsForm
|
||||
channelDescription={channelDescription}
|
||||
handleDescriptionChange={handleDescriptionChange}
|
||||
channelDiscoverable={channelDiscoverable}
|
||||
handleChannelDiscoverableStatus={handleChannelDiscoverableStatus}
|
||||
handleChannelDescriptionChanges={handleChannelDescriptionChanges}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ModSection.propTypes = {
|
||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||
handleChannelInvitations: PropTypes.func.isRequired,
|
||||
invitationUsernames: PropTypes.func.isRequired,
|
||||
channelDescription: PropTypes.string.isRequired,
|
||||
handleDescriptionChange: PropTypes.func.isRequired,
|
||||
handleChannelDiscoverableStatus: PropTypes.func.isRequired,
|
||||
handleChannelDescriptionChanges: PropTypes.func.isRequired,
|
||||
channelDiscoverable: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defaultMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import { Membership } from './Membership';
|
||||
|
||||
export const PendingMembershipSection = ({
|
||||
pendingMemberships,
|
||||
removeMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="pending-memberships"
|
||||
className="p-4 grid gap-2 crayons-card mb-4 pending_memberships"
|
||||
data-pending-count={pendingMemberships ? pendingMemberships.length : 0}
|
||||
>
|
||||
<h3 className="mb-2">Pending Invitations</h3>
|
||||
{pendingMemberships && pendingMemberships.length > 0
|
||||
? pendingMemberships.map((pendingMembership) => (
|
||||
<Membership
|
||||
membership={pendingMembership}
|
||||
removeMembership={removeMembership}
|
||||
membershipType="pending"
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
className="pending-member"
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PendingMembershipSection.propTypes = {
|
||||
pendingMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const PersonalSettings = ({
|
||||
handlePersonChannelSetting,
|
||||
showGlobalBadgeNotification,
|
||||
updateCurrentMembershipNotificationSettings,
|
||||
}) => {
|
||||
return (
|
||||
<div className="crayons-card p-4 grid gap-2 mb-4 personl-settings">
|
||||
<h3>Personal Settings</h3>
|
||||
<h4>Notifications</h4>
|
||||
<div className="crayons-field crayons-field--checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="c3"
|
||||
className="crayons-checkbox"
|
||||
checked={showGlobalBadgeNotification}
|
||||
onChange={handlePersonChannelSetting}
|
||||
/>
|
||||
<label htmlFor="c3" className="crayons-field__label">
|
||||
Receive Notifications for New Messages
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={updateCurrentMembershipNotificationSettings}
|
||||
>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PersonalSettings.propTypes = {
|
||||
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
|
||||
showGlobalBadgeNotification: PropTypes.bool.isRequired,
|
||||
handlePersonChannelSetting: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaultMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
|
||||
import { Membership } from './Membership';
|
||||
|
||||
export const RequestedMembershipSection = ({
|
||||
requestedMemberships,
|
||||
removeMembership,
|
||||
chatChannelAcceptMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="requested-memberships"
|
||||
className="p-4 grid gap-2 crayons-card mb-4"
|
||||
data-requested-count={
|
||||
requestedMemberships ? requestedMemberships.length : 0
|
||||
}
|
||||
>
|
||||
<h3 className="mb-2 requested_memberships">Joining Request</h3>
|
||||
{requestedMemberships && requestedMemberships.length > 0
|
||||
? requestedMemberships.map((pendingMembership) => (
|
||||
<Membership
|
||||
membership={pendingMembership}
|
||||
removeMembership={removeMembership}
|
||||
chatChannelAcceptMembership={chatChannelAcceptMembership}
|
||||
membershipType="requested"
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
RequestedMembershipSection.propTypes = {
|
||||
requestedMemberships: PropTypes.arrayOf(defaultMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const SettingsForm = ({
|
||||
channelDescription,
|
||||
handleDescriptionChange,
|
||||
channelDiscoverable,
|
||||
handleChannelDiscoverableStatus,
|
||||
handleChannelDescriptionChanges,
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
data-testid="settings-form"
|
||||
className="crayons-card p-4 grid gap-2 mb-4 settings-section"
|
||||
>
|
||||
<h3>Channel Settings</h3>
|
||||
<div className="crayons-field">
|
||||
<label
|
||||
className="crayons-field__label"
|
||||
htmlFor="chat_channel_description"
|
||||
>
|
||||
Description
|
||||
</label>
|
||||
<textarea
|
||||
className="crayons-textfield"
|
||||
name="description"
|
||||
id="chat_channel_description"
|
||||
value={channelDescription}
|
||||
onChange={handleDescriptionChange}
|
||||
/>
|
||||
</div>
|
||||
<div className="crayons-field crayons-field--checkbox">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="c2"
|
||||
className="crayons-checkbox"
|
||||
checked={channelDiscoverable}
|
||||
onChange={handleChannelDiscoverableStatus}
|
||||
/>
|
||||
<label htmlFor="c2" className="crayons-field__label">
|
||||
Channel Discoverable
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<Button type="submit" onClick={handleChannelDescriptionChanges}>
|
||||
Submit
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
SettingsForm.propTypes = {
|
||||
channelDescription: PropTypes.string.isRequired,
|
||||
handleDescriptionChange: PropTypes.func.isRequired,
|
||||
handleChannelDiscoverableStatus: PropTypes.func.isRequired,
|
||||
handleChannelDescriptionChanges: PropTypes.func.isRequired,
|
||||
channelDiscoverable: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
|
@ -1,147 +0,0 @@
|
|||
import { h, Fragment } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { reportAbuse, blockUser } from '../actions/requestActions';
|
||||
import { addSnackbarItem } from '../../Snackbar';
|
||||
import { Button, FormField, RadioButton } from '@crayons';
|
||||
|
||||
/**
|
||||
* This component render the report abuse
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {object} props.data
|
||||
* @param {function} props.closeReportAbuseForm
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <ReportAbuse
|
||||
* data={data}
|
||||
* closeReportAbuseForm={closeReportAbuseForm}
|
||||
* />
|
||||
*
|
||||
*/
|
||||
export function ReportAbuse({ data, closeReportAbuseForm }) {
|
||||
const [category, setCategory] = useState(null);
|
||||
|
||||
const handleChange = (e) => {
|
||||
setCategory(e.target.value);
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
const response = await reportAbuse(
|
||||
data.message,
|
||||
'connect',
|
||||
category,
|
||||
data.user_id,
|
||||
);
|
||||
const { success, message } = response;
|
||||
if (success) {
|
||||
const confirmBlock = window.confirm(
|
||||
`The message will be reported.\n\nWould you like to block this person as well?\n\nThis will:
|
||||
- prevent them from commenting on your posts
|
||||
- block all notifications from them
|
||||
- prevent them from messaging you via chat`,
|
||||
);
|
||||
|
||||
if (confirmBlock) {
|
||||
const response = await blockUser(data.user_id);
|
||||
if (response.result === 'blocked') {
|
||||
addSnackbarItem({
|
||||
message:
|
||||
'Your report has been submitted and the user has been blocked',
|
||||
});
|
||||
}
|
||||
} else {
|
||||
addSnackbarItem({ message: 'Your report has been submitted.' });
|
||||
}
|
||||
closeReportAbuseForm();
|
||||
} else {
|
||||
addSnackbarItem({ message });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Fragment>
|
||||
<section className="mt-7 p-4 grid gap-2 crayons-card mb-4">
|
||||
<h1 className="lh-tight mb-4 mt-0">Report Abuse</h1>
|
||||
<p>
|
||||
Thank you for reporting any abuse that violates our{' '}
|
||||
<a href="/code-of-conduct">code of conduct</a> or{' '}
|
||||
<a href="/terms">terms and conditions</a>. We continue to try to make
|
||||
this environment a great one for everybody.
|
||||
</p>
|
||||
<fieldset className="report__abuse-options p-4 justify-between">
|
||||
<legend>Why is this content inappropriate?</legend>
|
||||
<FormField variant="radio">
|
||||
<RadioButton
|
||||
id="rude_or_vulgar"
|
||||
name="rude_or_vulgar"
|
||||
value="rude or vulgar"
|
||||
checked={category === 'rude or vulgar'}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
<label htmlFor="rude_or_vulgar" className="crayons-field__label">
|
||||
Rude or vulgar
|
||||
</label>
|
||||
</FormField>
|
||||
<FormField variant="radio">
|
||||
<RadioButton
|
||||
id="harassment"
|
||||
name="harassment"
|
||||
value="harassment"
|
||||
checked={category === 'harassment'}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
<label htmlFor="harassment" className="crayons-field__label">
|
||||
Harassment or hate speech
|
||||
</label>
|
||||
</FormField>
|
||||
<FormField variant="radio">
|
||||
<RadioButton
|
||||
id="spam"
|
||||
name="spam"
|
||||
value="spam"
|
||||
checked={category === 'spam'}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
<label htmlFor="spam" className="crayons-field__label">
|
||||
Spam or copyright issue
|
||||
</label>
|
||||
</FormField>
|
||||
<FormField variant="radio">
|
||||
<RadioButton
|
||||
id="listings"
|
||||
name="listings"
|
||||
value="listings"
|
||||
checked={category === 'listings'}
|
||||
onClick={handleChange}
|
||||
/>
|
||||
<label htmlFor="listings" className="crayons-field__label">
|
||||
Inappropriate listings message/category
|
||||
</label>
|
||||
</FormField>
|
||||
<h2>Message to Report</h2>
|
||||
<div
|
||||
className="reported__message p-2 mt-2 mb-3"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: data.message }}
|
||||
/>
|
||||
<Button disabled={category === null} size="s" onClick={handleSubmit}>
|
||||
Report Message
|
||||
</Button>
|
||||
</fieldset>
|
||||
</section>
|
||||
</Fragment>
|
||||
);
|
||||
}
|
||||
|
||||
ReportAbuse.propTypes = {
|
||||
resource: PropTypes.shape({
|
||||
data: PropTypes.shape({
|
||||
user_id: PropTypes.number.isRequired,
|
||||
message: PropTypes.element.isRequired,
|
||||
}),
|
||||
}).isRequired,
|
||||
};
|
||||
|
|
@ -1,48 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { RequestListItem } from './RequestListItem';
|
||||
|
||||
export const ChannelRequestSection = ({
|
||||
channelRequests,
|
||||
handleRequestApproval,
|
||||
handleRequestRejection,
|
||||
}) => {
|
||||
if (channelRequests.length < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="chat-channel-joining-request"
|
||||
data-active-count={channelRequests ? channelRequests.length : 0}
|
||||
>
|
||||
{channelRequests &&
|
||||
channelRequests.map((channelPendingRequest) => {
|
||||
return (
|
||||
<RequestListItem
|
||||
request={channelPendingRequest}
|
||||
handleRequestApproval={handleRequestApproval}
|
||||
handleRequestRejection={handleRequestRejection}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ChannelRequestSection.propTypes = {
|
||||
channelRequests: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
channel_name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
handleRequestApproval: PropTypes.func.isRequired,
|
||||
handleRequestRejection: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,12 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
|
||||
export const HeaderSection = ({}) => (
|
||||
<div className="request_manager_header crayons-card mb-6 grid grid-flow-row gap-6 p-6">
|
||||
<h1>
|
||||
Request Center{' '}
|
||||
<span role="img" aria-label="handshake">
|
||||
🤝
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const PendingInvitationListItem = ({ request, updateMembership }) => (
|
||||
<div className="crayons-card mb-6">
|
||||
<div className="crayons-card__body channel-request-card">
|
||||
<div className="request-message d-flex flex-wrap">
|
||||
You got invitation to join <b>{request.chat_channel_name}</b>.
|
||||
</div>
|
||||
<div className="request-actions">
|
||||
<Button
|
||||
className="m-2"
|
||||
size="s"
|
||||
variant="danger"
|
||||
onClick={updateMembership}
|
||||
data-channel-id={request.chat_channel_id}
|
||||
data-membership-id={request.membership_id}
|
||||
data-channel-slug={request.slug}
|
||||
data-user-action="reject"
|
||||
>
|
||||
{' '}
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
className="m-2"
|
||||
size="s"
|
||||
onClick={updateMembership}
|
||||
data-channel-id={request.chat_channel_id}
|
||||
data-membership-id={request.membership_id}
|
||||
data-channel-slug={request.slug}
|
||||
data-user-action="accept"
|
||||
>
|
||||
{' '}
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
PendingInvitationListItem.propTypes = {
|
||||
request: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
chat_channel_name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
updateMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PendingInvitationListItem } from './PersonalInvitationListItem';
|
||||
|
||||
export const PersonalInvitationSection = ({
|
||||
userInvitations,
|
||||
updateMembership,
|
||||
}) => {
|
||||
if (!userInvitations || userInvitations?.length < 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="user-invitations"
|
||||
data-active-count={userInvitations ? userInvitations.length : 0}
|
||||
>
|
||||
{userInvitations &&
|
||||
userInvitations.map((userInvitation) => {
|
||||
return (
|
||||
<PendingInvitationListItem
|
||||
request={userInvitation}
|
||||
updateMembership={updateMembership}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PersonalInvitationSection.propTypes = {
|
||||
userInvitations: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
channel_name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
updateMembership: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const RequestListItem = ({
|
||||
request,
|
||||
handleRequestRejection,
|
||||
handleRequestApproval,
|
||||
}) => (
|
||||
<div className="crayons-card mb-6">
|
||||
<div className="crayons-card__body channel-request-card">
|
||||
<div className="request-message d-flex flex-wrap">
|
||||
<b>{request.name}</b> requested to join{' '}
|
||||
<b>{request.chat_channel_name}</b>
|
||||
</div>
|
||||
<div className="request-actions">
|
||||
<Button
|
||||
className="m-2"
|
||||
variant="danger"
|
||||
size="s"
|
||||
onClick={handleRequestRejection}
|
||||
data-channel-id={request.chat_channel_id}
|
||||
data-membership-id={request.membership_id}
|
||||
>
|
||||
Reject
|
||||
</Button>
|
||||
<Button
|
||||
className="m-2"
|
||||
size="s"
|
||||
onClick={handleRequestApproval}
|
||||
data-channel-id={request.chat_channel_id}
|
||||
data-membership-id={request.membership_id}
|
||||
>
|
||||
Accept
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
RequestListItem.propTypes = {
|
||||
request: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
name: PropTypes.string.isRequired,
|
||||
membership_id: PropTypes.number.isRequired,
|
||||
user_id: PropTypes.number.isRequired,
|
||||
role: PropTypes.string.isRequired,
|
||||
image: PropTypes.string.isRequired,
|
||||
username: PropTypes.string.isRequired,
|
||||
status: PropTypes.string.isRequired,
|
||||
chat_channel_name: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
handleRequestRejection: PropTypes.func.isRequired,
|
||||
handleRequestApproval: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,152 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { addSnackbarItem } from '../../Snackbar';
|
||||
import {
|
||||
getChannelRequestInfo,
|
||||
updateMembership,
|
||||
acceptJoiningRequest,
|
||||
rejectJoiningRequest,
|
||||
} from '../actions/requestActions';
|
||||
import { HeaderSection } from './HeaderSection';
|
||||
import { ChannelRequestSection } from './ChannelRequestSection';
|
||||
import { PersonalInvitationSection } from './PersonalInvitationSection';
|
||||
|
||||
export class RequestManager extends Component {
|
||||
static propTypes = {
|
||||
resource: PropTypes.shape({
|
||||
data: PropTypes.object,
|
||||
}).isRequired,
|
||||
updateRequestCount: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
updateRequestCount: props.updateRequestCount,
|
||||
channelJoiningRequests: [],
|
||||
userInvitations: [],
|
||||
};
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
this.setState({
|
||||
updateRequestCount: this.props.updateRequestCount,
|
||||
});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
getChannelRequestInfo().then((response) => {
|
||||
const { result } = response;
|
||||
const { user_joining_requests, channel_joining_memberships } = result;
|
||||
this.setState({
|
||||
channelJoiningRequests: channel_joining_memberships,
|
||||
userInvitations: user_joining_requests,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
handleIUpdateMembership = async (e) => {
|
||||
const {
|
||||
membershipId,
|
||||
userAction,
|
||||
channelSlug,
|
||||
channelId,
|
||||
} = e.target.dataset;
|
||||
const response = await updateMembership(membershipId, userAction);
|
||||
const { success, membership, message } = response;
|
||||
const { updateRequestCount } = this.state;
|
||||
if (success) {
|
||||
this.setState((prevState) => {
|
||||
const filteredUserInvitations = prevState.userInvitations.filter(
|
||||
(userInvitation) =>
|
||||
userInvitation.membership_id !== membership.membership_id,
|
||||
);
|
||||
|
||||
return {
|
||||
userInvitations: filteredUserInvitations,
|
||||
};
|
||||
});
|
||||
if (userAction === 'accept') {
|
||||
updateRequestCount(true, { channelSlug, channelId });
|
||||
}
|
||||
updateRequestCount();
|
||||
addSnackbarItem({ message });
|
||||
} else {
|
||||
addSnackbarItem({ message });
|
||||
}
|
||||
};
|
||||
|
||||
handleAcceptJoingRequest = async (e) => {
|
||||
const { membershipId, channelId } = e.target.dataset;
|
||||
const response = await acceptJoiningRequest(channelId, membershipId);
|
||||
const { success, message, membership } = response;
|
||||
const { updateRequestCount } = this.state;
|
||||
|
||||
if (success) {
|
||||
this.setState((prevState) => {
|
||||
const formattedChannelJoiningRequests = prevState.channelJoiningRequests.filter(
|
||||
(channelJoiningRequest) =>
|
||||
channelJoiningRequest.membership_id !== membership.membership_id,
|
||||
);
|
||||
return {
|
||||
channelJoiningRequests: formattedChannelJoiningRequests,
|
||||
};
|
||||
});
|
||||
|
||||
updateRequestCount();
|
||||
addSnackbarItem({ message });
|
||||
} else {
|
||||
addSnackbarItem({ message });
|
||||
}
|
||||
};
|
||||
|
||||
handleRejectJoingRequest = async (e) => {
|
||||
const { membershipId, channelId } = e.target.dataset;
|
||||
const response = await rejectJoiningRequest(channelId, membershipId);
|
||||
const { success, message } = response;
|
||||
const { updateRequestCount } = this.state;
|
||||
|
||||
if (success) {
|
||||
this.setState((prevState) => {
|
||||
const formattedChannelJoiningRequests = prevState.channelJoiningRequests.filter(
|
||||
(channelJoiningRequest) =>
|
||||
channelJoiningRequest.membership_id !== Number(membershipId),
|
||||
);
|
||||
return {
|
||||
channelJoiningRequests: formattedChannelJoiningRequests,
|
||||
};
|
||||
});
|
||||
|
||||
updateRequestCount();
|
||||
addSnackbarItem({ message });
|
||||
} else {
|
||||
addSnackbarItem({ message });
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { channelJoiningRequests, userInvitations } = this.state;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="p-4">
|
||||
<HeaderSection />
|
||||
{channelJoiningRequests.length <= 0 && userInvitations.length <= 0 ? (
|
||||
<p>You have no pending invitations.</p>
|
||||
) : null}
|
||||
<ChannelRequestSection
|
||||
channelRequests={channelJoiningRequests}
|
||||
handleRequestRejection={this.handleRejectJoingRequest}
|
||||
handleRequestApproval={this.handleAcceptJoingRequest}
|
||||
/>
|
||||
<PersonalInvitationSection
|
||||
userInvitations={userInvitations}
|
||||
updateMembership={this.handleIUpdateMembership}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ManageActiveMembership } from '../ChatChannelSettings/MembershipManager/ManageActiveMembership';
|
||||
|
||||
const currentModMembership = {
|
||||
name: 'dummy user',
|
||||
username: 'dummyuser',
|
||||
user_id: 1,
|
||||
chat_channel_id: 2,
|
||||
status: 'active',
|
||||
role: 'mod',
|
||||
};
|
||||
|
||||
describe('<ManageActiveMembership />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ManageActiveMembership
|
||||
invitationLink="https://dummy-invitation.link"
|
||||
currentMembership={currentModMembership}
|
||||
activeMemberships={[]}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText, queryByPlaceholderText } = render(
|
||||
<ManageActiveMembership
|
||||
invitationLink="https://dummy-invitation.link"
|
||||
currentMembership={currentModMembership}
|
||||
activeMemberships={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('Chat Channel Membership manager')).toBeDefined();
|
||||
expect(queryByPlaceholderText('Search Member...')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,46 +0,0 @@
|
|||
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();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { PersonalInvitationSection } from '../RequestManager/PersonalInvitationSection';
|
||||
|
||||
const data = {
|
||||
request: {
|
||||
name: 'Demo name',
|
||||
membership_id: 11,
|
||||
user_id: 10,
|
||||
role: 'member',
|
||||
image: '/image',
|
||||
username: 'demousername',
|
||||
status: 'pending',
|
||||
chat_channel_name: 'demo channel',
|
||||
},
|
||||
};
|
||||
|
||||
describe('<PersonalInvitationSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<PersonalInvitationSection request={data.request} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { queryByText } = render(
|
||||
<PersonalInvitationSection request={data.request} />,
|
||||
);
|
||||
|
||||
expect(
|
||||
queryByText(
|
||||
`${data.request.name} wants to join ${data.request.chat_channel_name}`,
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
expect(queryByText('Reject', { selector: 'button' })).toBeDefined();
|
||||
|
||||
expect(queryByText('Accept', { selector: 'button' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { PersonalInvitationSection } from '../RequestManager/PersonalInvitationSection';
|
||||
|
||||
const data = {
|
||||
userInvitations: [
|
||||
{
|
||||
name: 'Demo name',
|
||||
membership_id: 11,
|
||||
user_id: 10,
|
||||
role: 'member',
|
||||
image: '/image',
|
||||
username: 'demousername',
|
||||
status: 'joining_request',
|
||||
chat_channel_name: 'demo channel',
|
||||
},
|
||||
],
|
||||
noUserInvitations: [],
|
||||
};
|
||||
|
||||
describe('<PersonalInvitationSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<PersonalInvitationSection userInvitations={data.userInvitations} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { getByTestId } = render(
|
||||
<PersonalInvitationSection userInvitations={data.userInvitations} />,
|
||||
);
|
||||
|
||||
const userInvitationsWrapper = getByTestId('user-invitations');
|
||||
expect(
|
||||
Number(userInvitationsWrapper.dataset.activeCount),
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { RequestListItem } from '../RequestManager/RequestListItem';
|
||||
|
||||
const data = {
|
||||
request: {
|
||||
name: 'Demo name',
|
||||
membership_id: 11,
|
||||
user_id: 10,
|
||||
role: 'member',
|
||||
image: '/image',
|
||||
username: 'demousername',
|
||||
status: 'joining_request',
|
||||
chat_channel_name: 'demo channel',
|
||||
},
|
||||
};
|
||||
|
||||
describe('<RequestListItem />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<RequestListItem request={data.request} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { queryByText } = render(<RequestListItem request={data.request} />);
|
||||
|
||||
expect(
|
||||
queryByText(
|
||||
`${data.request.name} wants to join ${data.request.chat_channel_name}`,
|
||||
),
|
||||
).toBeDefined();
|
||||
|
||||
expect(queryByText('Reject', { selector: 'button' })).toBeDefined();
|
||||
|
||||
expect(queryByText('Accept', { selector: 'button' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,111 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ActiveMembershipsSection } from '../ChatChannelSettings/ActiveMembershipsSection';
|
||||
|
||||
function getEmptyMembershipData() {
|
||||
return {
|
||||
activeMemberships: [],
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
function getMembershipData() {
|
||||
return {
|
||||
activeMemberships: [
|
||||
{
|
||||
name: 'test user',
|
||||
username: 'testusername',
|
||||
user_id: '1',
|
||||
membership_id: '2',
|
||||
role: 'mod',
|
||||
status: 'active',
|
||||
image: '',
|
||||
},
|
||||
],
|
||||
membershipType: 'active',
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
describe('<ActiveMembershipsSection />', () => {
|
||||
it('should have no a11y violations when there are no members', async () => {
|
||||
const {
|
||||
activeMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipData();
|
||||
const { container } = render(
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have no a11y violations when there are members', async () => {
|
||||
const { activeMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { container } = render(
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have a title', () => {
|
||||
const {
|
||||
activeMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipData();
|
||||
const { queryByText } = render(
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not render the membership list', () => {
|
||||
const {
|
||||
activeMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipData();
|
||||
const { getByTestId } = render(
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
// no users to be found
|
||||
const activeMembershipsWrapper = getByTestId('active-memberships');
|
||||
|
||||
expect(Number(activeMembershipsWrapper.dataset.activeCount)).toEqual(0);
|
||||
});
|
||||
|
||||
it('should render the membership list', () => {
|
||||
const { activeMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { getByTestId } = render(
|
||||
<ActiveMembershipsSection
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
// the other fields aren't necessary to test as this is handled in the
|
||||
// <Membership /> tests.
|
||||
const activeMembershipsWrapper = getByTestId('active-memberships');
|
||||
|
||||
expect(
|
||||
Number(activeMembershipsWrapper.dataset.activeCount),
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,27 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Alert } from '../alert';
|
||||
|
||||
describe('<Alert />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<Alert showAlert />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render an alert', () => {
|
||||
const { queryByRole } = render(<Alert showAlert />);
|
||||
|
||||
expect(queryByRole('alert')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not render an alert', () => {
|
||||
const { queryByRole } = render(<Alert />);
|
||||
|
||||
const alert = queryByRole('alert');
|
||||
|
||||
expect(alert).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,30 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Article } from '../article';
|
||||
|
||||
global.fetch = fetch;
|
||||
|
||||
const getArticle = () => {
|
||||
return {
|
||||
title: 'Your approval means nothing to me',
|
||||
type_of: 'article',
|
||||
path: '/princesscarolyn/your-approval-means-nothing-to-me-42640',
|
||||
};
|
||||
};
|
||||
|
||||
describe('<Article />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<Article resource={getArticle()} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', async () => {
|
||||
const { queryByTitle } = render(<Article resource={getArticle()} />);
|
||||
|
||||
expect(queryByTitle('Your approval means nothing to me')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,36 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChannelButton } from '../components/ChannelButton';
|
||||
|
||||
const getChannel = () => {
|
||||
return {
|
||||
channel_name: 'test',
|
||||
channel_color: '#00FFFF',
|
||||
channel_type: 'invite_only',
|
||||
channel_modified_slug: '@test34',
|
||||
id: 34,
|
||||
chat_channel_id: 23,
|
||||
status: 'active',
|
||||
};
|
||||
};
|
||||
|
||||
describe('<ChannelButton />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<ChannelButton channel={getChannel()} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { getByText } = render(<ChannelButton channel={getChannel()} />);
|
||||
const button = getByText('test');
|
||||
|
||||
expect(button.dataset.channelSlug).toEqual('@test34');
|
||||
expect(button.dataset.channelId).toEqual('23');
|
||||
expect(button.dataset.channelStatus).toEqual('active');
|
||||
expect(button.dataset.channelName).toEqual('test');
|
||||
expect(button.dataset.content).toEqual('sidecar-channel-request');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChannelRequest } from '../channelRequest';
|
||||
|
||||
const getResource = () => {
|
||||
return {
|
||||
user: {
|
||||
name: 'Sarthak',
|
||||
username: 'sarthak9',
|
||||
},
|
||||
channel: {
|
||||
name: 'IronMan',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
describe('<ChannelRequest />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<ChannelRequest resource={getResource()} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText, queryByAltText } = render(
|
||||
<ChannelRequest resource={getResource()} />,
|
||||
);
|
||||
|
||||
expect(queryByText('Hey Sarthak !')).toBeDefined();
|
||||
expect(
|
||||
queryByText(
|
||||
'You are not a member of this group yet. Send a request to join.',
|
||||
),
|
||||
).toBeDefined();
|
||||
expect(queryByAltText('sarthak9 profile')).toBeDefined();
|
||||
expect(queryByAltText('IronMan profile')).toBeDefined();
|
||||
expect(queryByText('Join IronMan', { selector: 'button' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChannelRequestSection } from '../RequestManager/ChannelRequestSection';
|
||||
|
||||
const data = {
|
||||
channelRequests: [
|
||||
{
|
||||
name: 'Demo name',
|
||||
membership_id: 11,
|
||||
user_id: 10,
|
||||
role: 'member',
|
||||
image: '/image',
|
||||
username: 'demousername',
|
||||
status: 'joining_request',
|
||||
chat_channel_name: 'demo channel',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
describe('<ChannelRequestSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ChannelRequestSection channelRequests={data.channelRequests} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { getByTestId } = render(
|
||||
<ChannelRequestSection channelRequests={data.channelRequests} />,
|
||||
);
|
||||
|
||||
const channelRequestsWrapper = getByTestId('chat-channel-joining-request');
|
||||
expect(
|
||||
Number(channelRequestsWrapper.dataset.activeCount),
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,171 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render, fireEvent } from '@testing-library/preact';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Channels } from '../channels';
|
||||
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
global.window.currentUser = { id: 'fake_username' };
|
||||
|
||||
let channelSwitched = false;
|
||||
|
||||
const fakeSwitchChannel = () => {
|
||||
channelSwitched = !channelSwitched;
|
||||
};
|
||||
|
||||
const fakeChannels = [
|
||||
{
|
||||
channel_name: 'channel name 1',
|
||||
last_opened_at: 'September 2, 2018',
|
||||
channel_users: [],
|
||||
last_message_at: 'September 21, 2018',
|
||||
channel_type: 'group',
|
||||
slug: '0',
|
||||
channel_modified_slug: '@0',
|
||||
id: 12345,
|
||||
status: 'active',
|
||||
messages_count: 124,
|
||||
},
|
||||
{
|
||||
channel_name: 'group channel 2',
|
||||
status: 'active',
|
||||
last_opened_at: 'September 12, 2018',
|
||||
channel_users: [
|
||||
{
|
||||
profile_image: 'fake_profile_image',
|
||||
darker_color: '#111111',
|
||||
last_opened_at: 'some last open date',
|
||||
},
|
||||
{
|
||||
profile_image: 'fake_profile_pic',
|
||||
darker_color: '#222222',
|
||||
last_opened_at: 'some other last open date',
|
||||
},
|
||||
],
|
||||
last_message_at: 'September 14, 2018',
|
||||
channel_type: 'direct',
|
||||
slug: '1',
|
||||
channel_modified_slug: '@1',
|
||||
id: 12345,
|
||||
messages_count: 83,
|
||||
},
|
||||
{
|
||||
channel_name: 'group channel 3',
|
||||
last_opened_at: 'September 30, 2018',
|
||||
channel_users: [
|
||||
{
|
||||
profile_image: 'fake_profile_image',
|
||||
darker_color: '#111111',
|
||||
last_opened_at: 'some last open date',
|
||||
},
|
||||
{
|
||||
profile_image: 'fake_profile_pic',
|
||||
darker_color: '#222222',
|
||||
last_opened_at: 'some other last open date',
|
||||
},
|
||||
],
|
||||
last_message_at: 'September 29, 2018',
|
||||
channel_type: 'group',
|
||||
status: 'active',
|
||||
slug: '2',
|
||||
channel_modified_slug: '@2',
|
||||
id: 67890,
|
||||
messages_count: 56,
|
||||
},
|
||||
];
|
||||
|
||||
const getChannels = (mod, chatChannels) => (
|
||||
<Channels
|
||||
incomingVideoCallChannelIds={[]} // no incoming calls
|
||||
activeChannelId={12345}
|
||||
chatChannels={chatChannels}
|
||||
unopenedChannelIds={[]}
|
||||
handleSwitchChannel={fakeSwitchChannel}
|
||||
channelsLoaded
|
||||
filterQuery=""
|
||||
expanded={mod}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('<Channels />', () => {
|
||||
describe('expanded', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getChannels(true, fakeChannels));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render with chat channels', () => {
|
||||
const { getByText, getByRole, queryByRole } = render(
|
||||
getChannels(true, fakeChannels),
|
||||
);
|
||||
|
||||
// welcome message should not exist because there are channels
|
||||
expect(queryByRole('alert')).toBeNull();
|
||||
|
||||
// configFooter should exist
|
||||
fireEvent.click(
|
||||
getByRole('button', { name: /configuration navigation menu/i }),
|
||||
);
|
||||
const settings = getByText('Settings');
|
||||
expect(settings.getAttribute('href')).toEqual('/settings');
|
||||
|
||||
const reportAbuse = getByText('Report Abuse');
|
||||
expect(reportAbuse.getAttribute('href')).toEqual('/report-abuse');
|
||||
});
|
||||
|
||||
it('should render without chat channels', () => {
|
||||
const { getByText, getByRole } = render(getChannels(true, []));
|
||||
|
||||
// should show "Welcome to Connect message....."
|
||||
getByRole('alert');
|
||||
|
||||
fireEvent.click(
|
||||
getByRole('button', { name: /configuration navigation menu/i }),
|
||||
);
|
||||
const settings = getByText('Settings');
|
||||
expect(settings.getAttribute('href')).toEqual('/settings');
|
||||
|
||||
const reportAbuse = getByText('Report Abuse');
|
||||
expect(reportAbuse.getAttribute('href')).toEqual('/report-abuse');
|
||||
});
|
||||
});
|
||||
|
||||
describe('not expanded', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getChannels(false, fakeChannels));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have the proper elements, attributes, and content', () => {
|
||||
const { queryByTestId } = render(getChannels(false, fakeChannels));
|
||||
|
||||
// should have group names but no user names
|
||||
// TODO: I don't understand the comment above. To revisit.
|
||||
expect(queryByTestId('chat-channels-list')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('without chat channels', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getChannels(false, []));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render without chat channels', () => {
|
||||
const { getByTestId } = render(getChannels(false, []));
|
||||
|
||||
// should have nothing but empty str
|
||||
const chatChannelList = getByTestId('chat-channels-list');
|
||||
|
||||
expect(chatChannelList.textContent).toEqual('');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,244 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { fireEvent, render } from '@testing-library/preact';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Chat } from '../chat';
|
||||
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
|
||||
// mock observer and user ID
|
||||
window.IntersectionObserver = jest.fn(function intersectionObserverMock() {
|
||||
this.observe = jest.fn();
|
||||
});
|
||||
global.window.currentUser = { id: 'some_id' };
|
||||
|
||||
function getRootData() {
|
||||
return {
|
||||
chatChannels: JSON.stringify([
|
||||
{
|
||||
channel_name: 'channel name 1',
|
||||
last_opened_at: 'September 2, 2018',
|
||||
channel_users: [],
|
||||
last_message_at: 'September 21, 2018',
|
||||
channel_type: 'group',
|
||||
slug: '0',
|
||||
id: 12345,
|
||||
messages_count: 124,
|
||||
},
|
||||
{
|
||||
channel_name: 'group channel 2',
|
||||
last_opened_at: 'September 12, 2018',
|
||||
channel_users: [
|
||||
{
|
||||
profile_image: 'fake_profile_image',
|
||||
darker_color: '#111111',
|
||||
last_opened_at: 'some last open date',
|
||||
},
|
||||
{
|
||||
profile_image: 'fake_profile_pic',
|
||||
darker_color: '#222222',
|
||||
last_opened_at: 'some other last open date',
|
||||
},
|
||||
],
|
||||
last_message_at: 'September 14, 2018',
|
||||
channel_type: 'direct',
|
||||
slug: '1',
|
||||
id: 34561,
|
||||
messages_count: 83,
|
||||
},
|
||||
{
|
||||
channel_name: 'group channel 3',
|
||||
last_opened_at: 'September 30, 2018',
|
||||
channel_users: [
|
||||
{
|
||||
profile_image: 'fake_profile_image',
|
||||
darker_color: '#111111',
|
||||
last_opened_at: 'some last open date',
|
||||
},
|
||||
{
|
||||
profile_image: 'fake_profile_pic',
|
||||
darker_color: '#222222',
|
||||
last_opened_at: 'some other last open date',
|
||||
},
|
||||
],
|
||||
last_message_at: 'September 29, 2018',
|
||||
channel_type: 'group',
|
||||
slug: '2',
|
||||
id: 67890,
|
||||
messages_count: 56,
|
||||
},
|
||||
]),
|
||||
chatOptions: JSON.stringify({
|
||||
showChannelsList: true,
|
||||
showTimestamp: true,
|
||||
activeChannelId: 34561,
|
||||
}),
|
||||
githubToken: 'somegithubtoken',
|
||||
pusherKey: 'somepusherkey',
|
||||
tagModerator: JSON.stringify({ isTagModerator: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function getMockResponse() {
|
||||
return JSON.stringify({
|
||||
result: [
|
||||
{
|
||||
id: 117921,
|
||||
status: 'active',
|
||||
viewable_by: 9597,
|
||||
chat_channel_id: 51923,
|
||||
last_opened_at: '2020-06-07T15:51:12.033Z',
|
||||
channel_text:
|
||||
'Tag Moderators tag-moderators Nick Taylor (he/him) Ben Greenberg Jess Lee (she/her) Katie Nelson ☞ Desigan Chinniah ☜',
|
||||
channel_last_message_at: '2020-06-07T02:13:01.230Z',
|
||||
channel_status: 'active',
|
||||
channel_type: 'invite_only',
|
||||
channel_username: null,
|
||||
channel_name: 'Tag Moderators',
|
||||
channel_image:
|
||||
'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/organization-d83b2b577749cb1cb5c615003fc0d379f0bacc3be6cc1f541ac5655a9d770855.svg',
|
||||
channel_modified_slug: 'tag-moderators',
|
||||
channel_discoverable: false,
|
||||
channel_messages_count: 3287,
|
||||
last_indexed_at: '2020-06-07T15:51:11.100Z',
|
||||
},
|
||||
{
|
||||
id: 209670,
|
||||
status: 'active',
|
||||
viewable_by: 9597,
|
||||
chat_channel_id: 100437,
|
||||
last_opened_at: '2020-06-07T16:05:41.636Z',
|
||||
channel_text:
|
||||
'#react mods react-mods-5en7 Nick Taylor (he/him) Michael Tharrington (he/him) Chris Achard',
|
||||
channel_last_message_at: '2020-06-05T17:03:19.872Z',
|
||||
channel_status: 'active',
|
||||
channel_type: 'invite_only',
|
||||
channel_username: null,
|
||||
channel_name: '#react mods',
|
||||
channel_image:
|
||||
'https://practicaldev-herokuapp-com.freetls.fastly.net/assets/organization-d83b2b577749cb1cb5c615003fc0d379f0bacc3be6cc1f541ac5655a9d770855.svg',
|
||||
channel_modified_slug: 'react-mods-5en7',
|
||||
channel_discoverable: false,
|
||||
channel_messages_count: 7,
|
||||
last_indexed_at: '2020-06-07T16:05:40.723Z',
|
||||
},
|
||||
{
|
||||
id: 204169,
|
||||
status: 'active',
|
||||
viewable_by: 9597,
|
||||
chat_channel_id: 101735,
|
||||
last_opened_at: '2017-01-01T05:00:00.000Z',
|
||||
channel_text:
|
||||
'Direct carolskelly nickytonline carolskelly/nickytonline Nick Taylor (he/him) Carol Skelly',
|
||||
channel_last_message_at: '2020-03-28T00:57:52.210Z',
|
||||
channel_status: 'active',
|
||||
channel_type: 'direct',
|
||||
channel_username: 'carolskelly',
|
||||
channel_name: '@carolskelly',
|
||||
channel_image:
|
||||
'https://res.cloudinary.com/practicaldev/image/fetch/s--lQ_RERs9--/c_fill,f_auto,fl_progressive,h_90,q_auto,w_90/https://dev-to-uploads.s3.amazonaws.com/uploads/user/profile_image/11343/jDcwvKh7.jpg',
|
||||
channel_modified_slug: '@carolskelly',
|
||||
channel_messages_count: 0,
|
||||
last_indexed_at: '2020-04-04T00:57:52.331Z',
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
describe('<Chat />', () => {
|
||||
const csrfToken = 'this-is-a-csrf-token';
|
||||
|
||||
beforeAll(() => {
|
||||
global.Pusher = jest.fn(() => ({
|
||||
subscribe: jest.fn(() => ({
|
||||
bind: jest.fn(),
|
||||
})),
|
||||
}));
|
||||
global.fetch = fetch;
|
||||
global.getCsrfToken = async () => csrfToken;
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
delete global.Pusher;
|
||||
delete global.fetch;
|
||||
delete global.getCsrfToken;
|
||||
});
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
fetch.mockResponse(getMockResponse());
|
||||
const { container } = render(<Chat {...getRootData()} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render expanded', () => {
|
||||
fetch.mockResponse(getMockResponse());
|
||||
const { getByTestId, getByText, getByRole } = render(
|
||||
<Chat {...getRootData()} />,
|
||||
);
|
||||
const chat = getByTestId('chat');
|
||||
|
||||
expect(chat.getAttribute('aria-expanded')).toEqual('true');
|
||||
|
||||
// chat filtering
|
||||
getByText('all', { selector: 'button' });
|
||||
getByText('direct', { selector: 'button' });
|
||||
getByText('group', { selector: 'button' });
|
||||
|
||||
// renderActiveChatChannel
|
||||
const activeChat = getByTestId('active-chat');
|
||||
|
||||
expect(activeChat).not.toBeNull();
|
||||
|
||||
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: '[type="button"]' });
|
||||
getByText('Delete', { selector: '[type="button"]' });
|
||||
});
|
||||
|
||||
it('should collapse and expand chat channels properly', async () => {
|
||||
fetch.mockResponse(getMockResponse());
|
||||
const { queryByText } = render(<Chat {...getRootData()} />);
|
||||
|
||||
// // chat channels
|
||||
expect(
|
||||
queryByText('all', {
|
||||
selector: 'button',
|
||||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
queryByText('direct', {
|
||||
selector: 'button',
|
||||
}),
|
||||
).toBeDefined();
|
||||
expect(
|
||||
queryByText('group', {
|
||||
selector: 'button',
|
||||
}),
|
||||
).toBeDefined();
|
||||
});
|
||||
|
||||
it('should show mention pop-up on keyUp @', async () => {
|
||||
const { getByLabelText, getByTestId } = render(<Chat {...getRootData()} />);
|
||||
|
||||
const inputField = getByLabelText('Compose a message');
|
||||
|
||||
expect(inputField).toBeDefined();
|
||||
fireEvent.change(inputField, { target: { value: '@' } });
|
||||
|
||||
const mentionModal = getByTestId('mentionList');
|
||||
const allButton = getByTestId('all');
|
||||
|
||||
expect(mentionModal).toBeDefined();
|
||||
expect(allButton).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChannelDescriptionSection } from '../ChatChannelSettings/ChannelDescriptionSection';
|
||||
|
||||
describe('<ChannelDescriptionSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ChannelDescriptionSection
|
||||
channelName="some name"
|
||||
channelDescription="some description"
|
||||
currentMembershipRole="mod"
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(
|
||||
<ChannelDescriptionSection
|
||||
channelName="some name"
|
||||
channelDescription="some description"
|
||||
currentMembershipRole="member"
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('some name')).toBeDefined();
|
||||
expect(queryByText('some description')).toBeDefined();
|
||||
expect(queryByText('You are a channel member')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,80 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChatChannelMembershipSection } from '../ChatChannelSettings/ChatChannelMembershipSection';
|
||||
|
||||
describe('<ChatChannelMembershipSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ChatChannelMembershipSection
|
||||
pendingMemberships={[]}
|
||||
requestedMemberships={[]}
|
||||
activeMemberships={[]}
|
||||
currentMembershipRole="mod"
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render when no memberships', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<ChatChannelMembershipSection
|
||||
pendingMemberships={[]}
|
||||
requestedMemberships={[]}
|
||||
activeMemberships={[]}
|
||||
/>,
|
||||
);
|
||||
|
||||
getByTestId('active-memberships');
|
||||
const activeMemberships = getByTestId('active-memberships');
|
||||
|
||||
expect(Number(activeMemberships.dataset.activeCount)).toEqual(0);
|
||||
|
||||
getByText('Members');
|
||||
});
|
||||
|
||||
it('should render with memberships', () => {
|
||||
const { getByText, getByTestId } = render(
|
||||
<ChatChannelMembershipSection
|
||||
pendingMemberships={[{ name: 'Ben', username: 'ben' }]}
|
||||
requestedMemberships={[
|
||||
{ name: 'Peter', username: 'peter' },
|
||||
{ name: 'Jess', username: 'jess' },
|
||||
{ name: 'Xenox', username: 'xenox1' },
|
||||
]}
|
||||
activeMemberships={[
|
||||
{ name: 'Bobby', username: 'bobby' },
|
||||
{ name: 'Sarah', username: 'sarah' },
|
||||
]}
|
||||
currentMembershipRole="mod"
|
||||
/>,
|
||||
);
|
||||
|
||||
getByTestId('active-memberships');
|
||||
const activeMemberships = getByTestId('active-memberships');
|
||||
|
||||
expect(Number(activeMemberships.dataset.activeCount)).toEqual(2);
|
||||
|
||||
getByText('Members', {
|
||||
selector: '[data-testid="active-memberships"] *',
|
||||
});
|
||||
|
||||
const pendingMemberships = getByTestId('pending-memberships');
|
||||
|
||||
expect(Number(pendingMemberships.dataset.pendingCount)).toEqual(1);
|
||||
|
||||
getByText('Pending Invitations', {
|
||||
selector: '[data-testid="pending-memberships"] *',
|
||||
});
|
||||
|
||||
const requestedMemberships = getByTestId('requested-memberships');
|
||||
|
||||
expect(Number(requestedMemberships.dataset.requestedCount)).toEqual(3);
|
||||
|
||||
getByText('Joining Request', {
|
||||
selector: '[data-testid="requested-memberships"] *',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,226 +0,0 @@
|
|||
import fetch from 'jest-fetch-mock';
|
||||
import {
|
||||
getChannelDetails,
|
||||
updatePersonalChatChannelNotificationSettings,
|
||||
rejectChatChannelJoiningRequest,
|
||||
acceptChatChannelJoiningRequest,
|
||||
updateChatChannelDescription,
|
||||
sendChatChannelInvitation,
|
||||
leaveChatChannelMembership,
|
||||
updateMembershipRole,
|
||||
} from '../actions/chat_channel_setting_actions';
|
||||
|
||||
/* global globalThis */
|
||||
|
||||
describe('Chat channel API requests', () => {
|
||||
const csrfToken = 'this-is-a-csrf-token';
|
||||
const chanChannelMembershipId = 26; // Just a random chatChannelMembershipId ID.
|
||||
const channelId = 2;
|
||||
|
||||
beforeAll(() => {
|
||||
globalThis.fetch = fetch;
|
||||
globalThis.getCsrfToken = async () => csrfToken;
|
||||
});
|
||||
afterAll(() => {
|
||||
delete globalThis.fetch;
|
||||
delete globalThis.getCsrfToken;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('get chat channel info', () => {
|
||||
it('should have success response with channel details', async () => {
|
||||
const response = {
|
||||
success: true,
|
||||
current_membership: {
|
||||
user_id: 1,
|
||||
id: 2,
|
||||
chat_channel_id: 1,
|
||||
status: 'active',
|
||||
role: 'mod',
|
||||
},
|
||||
chat_channel: {
|
||||
name: 'dummy channel',
|
||||
description: 'some dummy description',
|
||||
discoverable: true,
|
||||
status: 'active',
|
||||
},
|
||||
memberships: {
|
||||
active_memberships: [],
|
||||
pending_memberships: [],
|
||||
requested_memberships: [],
|
||||
},
|
||||
};
|
||||
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const chatChannelDetails = await getChannelDetails(
|
||||
chanChannelMembershipId,
|
||||
);
|
||||
expect(chatChannelDetails).toEqual(response);
|
||||
});
|
||||
|
||||
it('not found channel', async () => {
|
||||
const response = {
|
||||
success: false,
|
||||
message: 'not found',
|
||||
};
|
||||
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const chatChannelDetails = await getChannelDetails(
|
||||
chanChannelMembershipId,
|
||||
);
|
||||
expect(chatChannelDetails).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update chat channel notification', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'user settings updated' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const updateProfile = await updatePersonalChatChannelNotificationSettings(
|
||||
chanChannelMembershipId,
|
||||
true,
|
||||
);
|
||||
expect(updateProfile).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return not found error', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const updateProfile = await updatePersonalChatChannelNotificationSettings(
|
||||
'',
|
||||
true,
|
||||
);
|
||||
expect(updateProfile).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('reject chat channel membership', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'user removed' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await rejectChatChannelJoiningRequest(
|
||||
channelId,
|
||||
chanChannelMembershipId,
|
||||
'pending',
|
||||
);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return not found error', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await rejectChatChannelJoiningRequest('', '', 'pending');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Accept chat channel membership', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'added to chat channel' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await acceptChatChannelJoiningRequest(
|
||||
channelId,
|
||||
chanChannelMembershipId,
|
||||
);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return not found error', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await acceptChatChannelJoiningRequest('', '');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('update chat channel', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'channel is updated' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await updateChatChannelDescription(
|
||||
channelId,
|
||||
'some description',
|
||||
true,
|
||||
);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Send inbvitation for join chat channel', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'added to chat channel' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await sendChatChannelInvitation(channelId, 'dummyuser');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should not send any invitation', async () => {
|
||||
const response = { success: true, message: 'no invitation sent' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await sendChatChannelInvitation(channelId, '');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return not found error', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await sendChatChannelInvitation('', '');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Leave chat channel', () => {
|
||||
it('should have success', async () => {
|
||||
const response = { success: true, message: 'user left the channel' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await leaveChatChannelMembership(chanChannelMembershipId);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return not found error', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await leaveChatChannelMembership('');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Update the membership role', () => {
|
||||
it('should have the success response', async () => {
|
||||
const response = { success: true, message: 'user membership is updated' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await updateMembershipRole(
|
||||
chanChannelMembershipId,
|
||||
channelId,
|
||||
'mod',
|
||||
);
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
|
||||
it('should return the not found', async () => {
|
||||
const response = { success: false, message: 'not found' };
|
||||
fetch.mockResponse(JSON.stringify(response));
|
||||
|
||||
const result = await updateMembershipRole('', '', 'mod');
|
||||
expect(result).toEqual(response);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ChatChannelSettings } from '../ChatChannelSettings/ChatChannelSettings';
|
||||
|
||||
// TODO: These tests are imcomplete, but currently
|
||||
// this is simply a migration to preact-testing-library.
|
||||
// More tests should be added here.
|
||||
describe('<ChatChannelSettings />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ChatChannelSettings activeMembershipId={12} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render if there are no channels', () => {
|
||||
const { container } = render(
|
||||
<ChatChannelSettings activeMembershipId={12} />,
|
||||
);
|
||||
|
||||
expect(container.firstElementChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render, fireEvent, createEvent } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Compose } from '../compose';
|
||||
|
||||
let submitNoMessage = false;
|
||||
let submitWithMessage = false;
|
||||
let textfieldIsEmpty = true;
|
||||
|
||||
const handleSubmitEmpty = () => {
|
||||
submitNoMessage = true;
|
||||
submitWithMessage = false;
|
||||
textfieldIsEmpty = true;
|
||||
};
|
||||
|
||||
const handleSubmitFake = () => {
|
||||
submitNoMessage = false;
|
||||
submitWithMessage = true;
|
||||
textfieldIsEmpty = true;
|
||||
};
|
||||
|
||||
const handleKeyDownFake = (e) => {
|
||||
const enterPressed = e.keyCode === 13;
|
||||
const shiftPressed = e.shiftKey;
|
||||
if (!enterPressed) {
|
||||
textfieldIsEmpty = false;
|
||||
} else if (textfieldIsEmpty && !shiftPressed) {
|
||||
handleSubmitEmpty();
|
||||
} else if (textfieldIsEmpty && shiftPressed) {
|
||||
textfieldIsEmpty = false;
|
||||
} else {
|
||||
handleSubmitFake();
|
||||
}
|
||||
};
|
||||
|
||||
const getCompose = (tf, props = {}) => {
|
||||
// true -> not empty, false -> empty
|
||||
if (tf) {
|
||||
return (
|
||||
<Compose
|
||||
handleSubmitOnClick={handleSubmitFake}
|
||||
handleKeyDown={handleKeyDownFake}
|
||||
activeChannelId={12345}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Compose
|
||||
handleSubmitOnClick={handleSubmitEmpty}
|
||||
handleKeyDown={handleKeyDownFake}
|
||||
activeChannelId={12345}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('<Compose />', () => {
|
||||
afterEach(() => {
|
||||
submitNoMessage = false;
|
||||
submitWithMessage = false;
|
||||
textfieldIsEmpty = true;
|
||||
});
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getCompose(false));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
describe('behavior with no message', () => {
|
||||
it('should click submit', () => {
|
||||
const { getByText } = render(getCompose(false));
|
||||
const button = getByText(/Send/i);
|
||||
|
||||
button.click();
|
||||
expect(submitNoMessage).toEqual(true);
|
||||
expect(submitWithMessage).toEqual(false);
|
||||
expect(textfieldIsEmpty).toEqual(true);
|
||||
});
|
||||
|
||||
it('should press enter', () => {
|
||||
const { getByLabelText } = render(getCompose(false));
|
||||
const input = getByLabelText('Compose a message');
|
||||
|
||||
fireEvent.keyDown(input, { keyCode: 13 });
|
||||
|
||||
expect(submitNoMessage).toEqual(true);
|
||||
expect(submitWithMessage).toEqual(false);
|
||||
expect(textfieldIsEmpty).toEqual(true);
|
||||
});
|
||||
|
||||
it('should pressed enter and shift', () => {
|
||||
const { getByLabelText } = render(getCompose(false));
|
||||
const input = getByLabelText('Compose a message');
|
||||
|
||||
fireEvent.keyDown(input, { keyCode: 13 });
|
||||
fireEvent.keyDown(input, { keyCode: 16 });
|
||||
|
||||
expect(textfieldIsEmpty).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior with message', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getCompose(true));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have proper elements, attributes and values', () => {
|
||||
const { getByLabelText, getByText } = render(getCompose(true));
|
||||
|
||||
const input = getByLabelText('Compose a message');
|
||||
|
||||
expect(input.textContent).toEqual('');
|
||||
expect(input.getAttribute('maxLength')).toEqual('1000');
|
||||
expect(input.getAttribute('placeholder')).toContain('Write message to');
|
||||
|
||||
// Ensure send button is pressent
|
||||
getByText(/send/i);
|
||||
});
|
||||
|
||||
it('should click submit and check for empty textarea', () => {
|
||||
const { getByLabelText, getByText } = render(getCompose(true));
|
||||
const input = getByLabelText('Compose a message');
|
||||
const sendButton = getByText(/send/i);
|
||||
|
||||
fireEvent.keyDown(input, { keyCode: 69 });
|
||||
expect(textfieldIsEmpty).toEqual(false);
|
||||
|
||||
sendButton.click();
|
||||
|
||||
expect(submitNoMessage).toEqual(false);
|
||||
expect(submitWithMessage).toEqual(true);
|
||||
expect(textfieldIsEmpty).toEqual(true);
|
||||
});
|
||||
|
||||
it('should press enter and check for empty textarea', () => {
|
||||
const { getByLabelText } = render(getCompose(true));
|
||||
const input = getByLabelText('Compose a message');
|
||||
|
||||
fireEvent.keyDown(input, { keyCode: 69 });
|
||||
fireEvent.keyDown(input, { keyCode: 13 });
|
||||
|
||||
expect(submitNoMessage).toEqual(false);
|
||||
expect(submitWithMessage).toEqual(true);
|
||||
expect(textfieldIsEmpty).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
// Check for the actual input value after pressing enter
|
||||
it('should press enter and check for empty input', () => {
|
||||
const compose = getCompose(true);
|
||||
const { getByTestId, rerender } = render(compose);
|
||||
|
||||
const input = getByTestId('messageform');
|
||||
|
||||
fireEvent(input, createEvent('input', input, { target: { value: 'T' } }));
|
||||
|
||||
expect(input.value).toBe('T');
|
||||
|
||||
fireEvent.keyDown(input, { keyCode: 13 });
|
||||
|
||||
rerender(compose);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
// Check for the actual input value after clicking send
|
||||
it('should click send and check for empty input', () => {
|
||||
const compose = getCompose(true);
|
||||
const { getByTestId, getByText, rerender } = render(compose);
|
||||
|
||||
const input = getByTestId('messageform');
|
||||
const sendButton = getByText(/send/i);
|
||||
|
||||
fireEvent(input, createEvent('input', input, { target: { value: 'T' } }));
|
||||
|
||||
expect(input.value).toBe('T');
|
||||
|
||||
sendButton.click();
|
||||
|
||||
rerender(compose);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
// Check for the actual input value after saving an edit
|
||||
it('should click send edit and check for empty input', () => {
|
||||
const compose = getCompose(true, {
|
||||
markdownEdited: false,
|
||||
startEditing: true,
|
||||
editMessageMarkdown: 'Test',
|
||||
handleSubmitOnClickEdit: () => null,
|
||||
});
|
||||
const { getByTestId, getByText, rerender } = render(compose);
|
||||
|
||||
const input = getByTestId('messageform');
|
||||
const saveButton = getByText(/save/i);
|
||||
|
||||
expect(input.value).toBe('Test');
|
||||
|
||||
saveButton.click();
|
||||
|
||||
rerender(compose);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
|
||||
// Check for the actual input value after canceling an edit
|
||||
it('should click close edit and check for empty input', () => {
|
||||
const compose = getCompose(true, {
|
||||
markdownEdited: false,
|
||||
startEditing: true,
|
||||
editMessageMarkdown: 'Test',
|
||||
handleEditMessageClose: () => null,
|
||||
});
|
||||
const { getByTestId, getByText, rerender } = render(compose);
|
||||
|
||||
const input = getByTestId('messageform');
|
||||
const closeButton = getByText(/close/i);
|
||||
|
||||
expect(input.value).toBe('Test');
|
||||
|
||||
closeButton.click();
|
||||
|
||||
rerender(compose);
|
||||
|
||||
expect(input.value).toBe('');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,92 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Content } from '../content';
|
||||
|
||||
const getChannelRequestData = () => ({
|
||||
onTriggerContent: jest.fn(),
|
||||
type_of: 'channel-request',
|
||||
activeChannelId: 12345,
|
||||
pusherKey: 'ASDFGHJKL',
|
||||
githubToken: '',
|
||||
data: {
|
||||
channel: {
|
||||
name: 'bobby',
|
||||
},
|
||||
user: {
|
||||
username: 'spongebob',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const getLoadingUserData = () => ({
|
||||
onTriggerContent: jest.fn(),
|
||||
type_of: 'loading-user',
|
||||
activeChannelId: 1235,
|
||||
pusherKey: 'ASDFGHJKL',
|
||||
githubToken: '',
|
||||
data: {
|
||||
user: {
|
||||
username: 'spongebob',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
describe('<Content />', () => {
|
||||
describe('as loading-user', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const channelRequestResource = getChannelRequestData();
|
||||
const { container } = render(
|
||||
<Content resource={channelRequestResource} />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const channelRequestResource = getChannelRequestData();
|
||||
const { queryByText, queryByTitle } = render(
|
||||
<Content resource={channelRequestResource} />,
|
||||
);
|
||||
|
||||
// Ensure the two buttons render
|
||||
expect(queryByTitle('exit')).toBeDefined();
|
||||
expect(queryByTitle('fullscreen')).toBeDefined();
|
||||
|
||||
// Simple check if the component to request joining a channel appears.
|
||||
// The component itself is tested it in it's own test suite.
|
||||
expect(
|
||||
queryByText(
|
||||
'You are not a member of this group yet. Send a request to join.',
|
||||
),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('as channel-request', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const loadinUserResource = getLoadingUserData();
|
||||
const { container } = render(<Content resource={loadinUserResource} />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const loadinUserResource = getLoadingUserData();
|
||||
const { queryByTitle } = render(
|
||||
<Content resource={loadinUserResource} />,
|
||||
);
|
||||
|
||||
// Ensure the two buttons render
|
||||
expect(queryByTitle('exit')).toBeDefined();
|
||||
expect(queryByTitle('fullscreen')).toBeDefined();
|
||||
expect(queryByTitle('Loading user')).toBeDefined();
|
||||
});
|
||||
});
|
||||
/*
|
||||
testing only as loading user since components that Content uses
|
||||
are independently tested
|
||||
*/
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Draw } from '../draw';
|
||||
|
||||
describe('<Draw />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<Draw />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(<Draw />);
|
||||
|
||||
expect(queryByText('Connect Draw')).not.toBeNull();
|
||||
expect(queryByText('Clear')).not.toBeNull();
|
||||
expect(queryByText('Send')).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,74 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { InviationLinkManager } from '../ChatChannelSettings/MembershipManager/InvitationLinkManager';
|
||||
|
||||
const currentModMembership = {
|
||||
name: 'dummy user',
|
||||
username: 'dummyuser',
|
||||
user_id: 1,
|
||||
chat_channel_id: 2,
|
||||
status: 'active',
|
||||
role: 'mod',
|
||||
};
|
||||
|
||||
const currentMemberMembership = {
|
||||
name: 'dummy member',
|
||||
username: 'dummymember',
|
||||
user_id: 1,
|
||||
chat_channel_id: 2,
|
||||
status: 'active',
|
||||
role: 'member',
|
||||
};
|
||||
|
||||
const svg = (
|
||||
<svg
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
className="crayons-icon copy-icon"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
role="img"
|
||||
aria-labelledby="fc5f15add1e114844f5e"
|
||||
>
|
||||
<title id="fc5f15add1e114844f5e">Copy Invitation Url</title>
|
||||
<path d="M7 6V3a1 1 0 011-1h12a1 1 0 011 1v14a1 1 0 01-1 1h-3v3c0 .552-.45 1-1.007 1H4.007A1 1 0 013 21l.003-14c0-.552.45-1 1.007-1H7zm2 0h8v10h2V4H9v2zm-2 5v2h6v-2H7zm0 4v2h6v-2H7z" />
|
||||
</svg>
|
||||
);
|
||||
|
||||
describe('<InviationLinkManager />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<InviationLinkManager
|
||||
invitationLink="https://dummy-invitation.link"
|
||||
currentMembership={currentModMembership}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(
|
||||
<InviationLinkManager
|
||||
invitationLink="https://dummy-invitation.link"
|
||||
currentMembership={currentModMembership}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('https://dummy-invitation.link')).toBeDefined();
|
||||
expect(queryByText('Invitation Link')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should not render', () => {
|
||||
const { rerender } = render(
|
||||
<InviationLinkManager
|
||||
invitationLink="https://dummy-invitation.link"
|
||||
currentMembership={currentMemberMembership}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(rerender()).toEqual(undefined);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { InviteForm } from '../ChatChannelSettings/InviteForm';
|
||||
|
||||
describe('<InviteForm />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<InviteForm invitationUsernames="" />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render with no usernames', () => {
|
||||
const { queryByLabelText, queryByText } = render(
|
||||
<InviteForm invitationUsernames="" />,
|
||||
);
|
||||
|
||||
expect(queryByLabelText('Usernames to invite')).toBeDefined();
|
||||
expect(queryByText('Submit')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should render with usernames to invite', () => {
|
||||
const { getByLabelText } = render(
|
||||
<InviteForm invitationUsernames="@bobbytables, @xss, @owasp" />,
|
||||
);
|
||||
|
||||
const input = getByLabelText('Usernames to invite');
|
||||
|
||||
expect(input.value).toEqual('@bobbytables, @xss, @owasp');
|
||||
});
|
||||
});
|
||||
|
|
@ -1,32 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { LeaveMembershipSection } from '../ChatChannelSettings/LeaveMembershipSection';
|
||||
|
||||
describe('<LeaveMembershipSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<LeaveMembershipSection />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(<LeaveMembershipSection />);
|
||||
|
||||
expect(queryByText('Danger Zone')).toBeDefined();
|
||||
expect(queryByText('Leave Channel')).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have user leave channel when leave button is clicked', () => {
|
||||
const leaveHandler = jest.fn();
|
||||
const { getByText } = render(
|
||||
<LeaveMembershipSection handleleaveChannelMembership={leaveHandler} />,
|
||||
);
|
||||
const leaveButton = getByText('Leave Channel');
|
||||
|
||||
leaveButton.click();
|
||||
|
||||
expect(leaveHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,60 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import '@testing-library/jest-dom';
|
||||
import { axe } from 'jest-axe';
|
||||
import { Message } from '../message';
|
||||
|
||||
const msg = {
|
||||
username: 'asdf',
|
||||
used_id: 12345,
|
||||
message: 'WE BUILT THIS CITY',
|
||||
color: '#00FFFF',
|
||||
};
|
||||
|
||||
const getMessage = (message, props) => (
|
||||
<Message
|
||||
user={message.username}
|
||||
userID={message.user_id}
|
||||
message={message.message}
|
||||
color={message.color}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('<Message />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getMessage(msg));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { getByText, getByAltText } = render(getMessage(msg));
|
||||
|
||||
getByAltText('asdf profile');
|
||||
getByText(msg.message);
|
||||
|
||||
const profileLink = getByText(msg.username);
|
||||
|
||||
expect(profileLink.parentElement).toHaveStyle({ color: msg.color });
|
||||
});
|
||||
|
||||
it('should highlight @mentions to the logged in user', () => {
|
||||
const testMessage = {
|
||||
username: 'testUser',
|
||||
user_id: 456,
|
||||
message: "<p>hello <a href='testUser'>@testUser</a></p>",
|
||||
};
|
||||
|
||||
const { getByText } = render(
|
||||
getMessage(testMessage, { currentUserId: testMessage.user_id }),
|
||||
);
|
||||
|
||||
const profileLink = getByText(`@${testMessage.username}`);
|
||||
|
||||
expect(profileLink.parentElement.innerHTML).toMatch(
|
||||
new RegExp(`<mark>@${testMessage.username}</mark>`, 'i'),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ModFaqSection } from '../ChatChannelSettings/ModFaqSection';
|
||||
|
||||
describe('<ChannelDescriptionSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<ModFaqSection />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render', () => {
|
||||
const { queryByText } = render(<ModFaqSection />);
|
||||
|
||||
expect(
|
||||
queryByText(/^Questions about Connect Channel moderation\? Contact/),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,23 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ModSection } from '../ChatChannelSettings/ModSection';
|
||||
|
||||
describe('<ModSection />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(<ModSection currentMembershipRole="mod" />);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render if the membership role is a moderator', () => {
|
||||
const { queryByTestId } = render(
|
||||
<ModSection currentMembershipRole="mod" />,
|
||||
);
|
||||
|
||||
// the <InviteForm /> and <SettingsForm /> have their own tests.
|
||||
expect(queryByTestId('invite-form')).toBeDefined();
|
||||
expect(queryByTestId('settings-form')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,95 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { PendingMembershipSection } from '../ChatChannelSettings/PendingMembershipSection';
|
||||
|
||||
function getEmptyMembershipData() {
|
||||
return {
|
||||
activeMemberships: [],
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
function getMembershipData() {
|
||||
return {
|
||||
pendingMemberships: [
|
||||
{
|
||||
name: 'test user',
|
||||
username: 'testusername',
|
||||
user_id: '1',
|
||||
membership_id: '2',
|
||||
role: 'mod',
|
||||
status: 'active',
|
||||
image: '',
|
||||
},
|
||||
],
|
||||
membershipType: 'active',
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
describe('<PendingMembershipSection />', () => {
|
||||
it('should have no a11y violations when there are no members', async () => {
|
||||
const {
|
||||
pendingMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipData();
|
||||
const { container } = render(
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have no a11y violations when there are members', async () => {
|
||||
const { pendingMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { container } = render(
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should not render the membership list', () => {
|
||||
const {
|
||||
pendingMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipData();
|
||||
const { getByTestId } = render(
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
// no users to be found
|
||||
const pendingMembershipsWrapper = getByTestId('pending-memberships');
|
||||
|
||||
expect(Number(pendingMembershipsWrapper.dataset.pendingCount)).toEqual(0);
|
||||
});
|
||||
|
||||
it('should render the membership list', () => {
|
||||
const { pendingMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { getByTestId } = render(
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
// no users to be found
|
||||
const pendingMembershipsWrapper = getByTestId('pending-memberships');
|
||||
|
||||
expect(
|
||||
Number(pendingMembershipsWrapper.dataset.pendingCount),
|
||||
).toBeGreaterThanOrEqual(1);
|
||||
});
|
||||
});
|
||||
|
|
@ -1,33 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { PersonalSettng } from '../ChatChannelSettings/PersonalSetting';
|
||||
|
||||
describe('<PersonalSettng />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<PersonalSettng showGlobalBadgeNotification />,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { queryByText, queryByLabelText } = render(
|
||||
<PersonalSettng showGlobalBadgeNotification />,
|
||||
);
|
||||
|
||||
// get the section header
|
||||
expect(queryByText('Personal Settings')).toBeDefined();
|
||||
|
||||
// get the subsection header
|
||||
expect(queryByText('Notifications')).toBeDefined();
|
||||
|
||||
// form fields
|
||||
expect(
|
||||
queryByLabelText('Receive Notifications for New Messages'),
|
||||
).toBeDefined();
|
||||
expect(queryByText('Submit', { selector: 'button' })).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,44 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { ReportAbuse } from '../ReportAbuse';
|
||||
|
||||
describe('<ReportAbuse />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<ReportAbuse
|
||||
data={{
|
||||
message: 'HI',
|
||||
user_id: 1,
|
||||
}}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the component', () => {
|
||||
const { getByText, getByLabelText } = render(
|
||||
<ReportAbuse data={{ message: 'HI', user_id: 1 }} />,
|
||||
);
|
||||
|
||||
expect(getByText('Report Abuse')).toBeDefined();
|
||||
|
||||
const vulgarInput = getByLabelText('Rude or vulgar');
|
||||
expect(vulgarInput.value).toEqual('rude or vulgar');
|
||||
|
||||
const harassmentInput = getByLabelText('Harassment or hate speech');
|
||||
expect(harassmentInput.value).toEqual('harassment');
|
||||
|
||||
const listingsInput = getByLabelText(
|
||||
'Inappropriate listings message/category',
|
||||
);
|
||||
expect(listingsInput.value).toEqual('listings');
|
||||
|
||||
const spamInput = getByLabelText('Spam or copyright issue');
|
||||
expect(spamInput.value).toEqual('spam');
|
||||
|
||||
expect(getByText('Report Message')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import { axe } from 'jest-axe';
|
||||
import { beforeEach } from '@jest/globals';
|
||||
import { RequestManager } from '../RequestManager/RequestManager';
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
function getData() {
|
||||
const data = [
|
||||
{
|
||||
resource: {},
|
||||
},
|
||||
];
|
||||
|
||||
return data;
|
||||
}
|
||||
|
||||
// TODO: There needs to be some better tests in here in regards to different data: empty, some data etc.
|
||||
describe('<RequestManager />', () => {
|
||||
beforeEach(() => {
|
||||
const csrfToken = 'this-is-a-csrf-token';
|
||||
|
||||
window.fetch = fetch;
|
||||
window.getCsrfToken = async () => csrfToken;
|
||||
|
||||
fetch.mockResponse(
|
||||
JSON.stringify({
|
||||
result: { user_joining_requests: [], channel_joining_memberships: [] },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(
|
||||
<RequestManager resource={getData()} updateRequestCount={jest.fn()} />,
|
||||
);
|
||||
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have the proper elements', () => {
|
||||
const { getByText } = render(
|
||||
<RequestManager resource={getData()} updateRequestCount={jest.fn()} />,
|
||||
);
|
||||
|
||||
expect(getByText('You have no pending invitations.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,94 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { RequestedMembershipSection } from '../ChatChannelSettings/RequestedMembershipSection';
|
||||
|
||||
function getEmptyMembershipRequestsData() {
|
||||
return {
|
||||
requestedMemberships: [],
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
function getMembershipData() {
|
||||
return {
|
||||
requestedMemberships: [
|
||||
{
|
||||
name: 'test user',
|
||||
username: 'testusername',
|
||||
user_id: '1',
|
||||
membership_id: '2',
|
||||
role: 'member',
|
||||
status: 'requested',
|
||||
image: '',
|
||||
},
|
||||
],
|
||||
membershipType: 'requested',
|
||||
currentMembershipRole: 'mod',
|
||||
};
|
||||
}
|
||||
|
||||
describe('<RequestedMembershipSection />', () => {
|
||||
it('should have no a11y violations when there are no requested memberships', async () => {
|
||||
const {
|
||||
requestedMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipRequestsData();
|
||||
const { container } = render(
|
||||
<RequestedMembershipSection
|
||||
requestedMemberships={requestedMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should have no a11y violations when there are requested memberships', async () => {
|
||||
const { requestedMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { container } = render(
|
||||
<RequestedMembershipSection
|
||||
requestedMemberships={requestedMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should not render the membership list', () => {
|
||||
const {
|
||||
requestedMemberships,
|
||||
currentMembershipRole,
|
||||
} = getEmptyMembershipRequestsData();
|
||||
const { getByText, queryByText } = render(
|
||||
<RequestedMembershipSection
|
||||
requestedMemberships={requestedMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
getByText('Joining Request');
|
||||
|
||||
expect(
|
||||
queryByText('+', { selector: 'button[data-membership-id]' }),
|
||||
).toBeNull();
|
||||
});
|
||||
|
||||
it('should render the membership list', () => {
|
||||
const { requestedMemberships, currentMembershipRole } = getMembershipData();
|
||||
const { queryByText } = render(
|
||||
<RequestedMembershipSection
|
||||
requestedMemberships={requestedMemberships}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(queryByText('Joining Request')).toBeDefined();
|
||||
expect(
|
||||
queryByText('+', { selector: 'button[data-membership-id]' }),
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,42 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { render } from '@testing-library/preact';
|
||||
import { axe } from 'jest-axe';
|
||||
import { SettingsForm } from '../ChatChannelSettings/SettingsForm';
|
||||
|
||||
const data = {
|
||||
channelDescription: 'some description test',
|
||||
channelDiscoverable: true,
|
||||
};
|
||||
|
||||
const getSettingsForm = (channelDetails) => {
|
||||
return (
|
||||
<SettingsForm
|
||||
showGlobalBadgeNotification={channelDetails.showGlobalBadgeNotification}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
describe('<SettingsForm />', () => {
|
||||
it('should have no a11y violations', async () => {
|
||||
const { container } = render(getSettingsForm(data));
|
||||
const results = await axe(container);
|
||||
|
||||
expect(results).toHaveNoViolations();
|
||||
});
|
||||
|
||||
it('should render the the component', () => {
|
||||
const { queryByText, queryByLabelText } = render(getSettingsForm(data));
|
||||
|
||||
// title
|
||||
expect(queryByText('Channel Settings')).toBeDefined();
|
||||
|
||||
// description of channel
|
||||
expect(queryByLabelText('Description')).toBeDefined();
|
||||
|
||||
// whether or not the channel is discoverable
|
||||
expect(queryByLabelText('Channel Discoverable')).toBeDefined();
|
||||
|
||||
// submit buttton
|
||||
expect(queryByText('Submit')).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
import { getUserDataAndCsrfToken } from '../util';
|
||||
|
||||
const ERROR_MESSAGE = "Couldn't find user data on page.";
|
||||
|
||||
describe('Chat utilities', () => {
|
||||
describe('getUserDataAndCsrfToken', () => {
|
||||
afterEach(() => {
|
||||
document.head.innerHTML = '';
|
||||
document.body.removeAttribute('data-user');
|
||||
});
|
||||
|
||||
test('should reject if no user or csrf token found.', async () => {
|
||||
await expect(getUserDataAndCsrfToken(document)).rejects.toThrow(
|
||||
ERROR_MESSAGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject if csrf token found but no user.', async () => {
|
||||
document.head.innerHTML =
|
||||
'<meta name="csrf-token" content="some-csrf-token" />';
|
||||
await expect(getUserDataAndCsrfToken(document)).rejects.toThrow(
|
||||
ERROR_MESSAGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('should reject if user found but no csrf token found.', async () => {
|
||||
document.body.setAttribute('data-user', '{}');
|
||||
await expect(getUserDataAndCsrfToken(document)).rejects.toThrow(
|
||||
ERROR_MESSAGE,
|
||||
);
|
||||
});
|
||||
|
||||
test('should resolve if user and csrf token found.', async () => {
|
||||
const csrfToken = 'some-csrf-token';
|
||||
const currentUser = {
|
||||
id: 41,
|
||||
name: 'Guy Fieri',
|
||||
username: 'guyfieri',
|
||||
profile_image_90:
|
||||
'/uploads/user/profile_image/41/0841dbe2-208c-4daa-b498-b2f01f3d37b2.png',
|
||||
followed_tag_names: [],
|
||||
followed_tags: '[]',
|
||||
reading_list_ids: [48, 49, 34, 51, 64, 56],
|
||||
saw_onboarding: true,
|
||||
checked_code_of_conduct: false,
|
||||
display_sponsors: true,
|
||||
trusted: false,
|
||||
};
|
||||
document.head.innerHTML = `<meta name="csrf-token" content="${csrfToken}" />`;
|
||||
document.body.setAttribute('data-user', JSON.stringify(currentUser));
|
||||
|
||||
expect(await getUserDataAndCsrfToken(document)).toEqual({
|
||||
currentUser,
|
||||
csrfToken,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,88 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { adjustTimestamp } from './util';
|
||||
|
||||
export const ActionMessage = ({
|
||||
user,
|
||||
message,
|
||||
color,
|
||||
timestamp,
|
||||
profileImageUrl,
|
||||
onContentTrigger,
|
||||
}) => {
|
||||
const spanStyle = { color };
|
||||
|
||||
const messageArea = (
|
||||
<span
|
||||
className="chatmessagebody__message"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{ __html: message }}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="chatmessage chatmessage__action ">
|
||||
<div className="chatmessage__profilepic">
|
||||
<a
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-content="sidecar-user"
|
||||
onClick={onContentTrigger}
|
||||
>
|
||||
<img
|
||||
role="presentation"
|
||||
className="chatmessagebody__profileimage"
|
||||
src={profileImageUrl}
|
||||
alt={`${user} profile`}
|
||||
data-content="sidecar-user"
|
||||
onClick={onContentTrigger}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
role="presentation"
|
||||
className="chatmessage__body"
|
||||
onClick={onContentTrigger}
|
||||
>
|
||||
<div className="message__info__actions">
|
||||
<div className="message__info">
|
||||
<span
|
||||
className="chatmessagebody__username not-dark-theme-text-compatible"
|
||||
style={spanStyle}
|
||||
>
|
||||
<a
|
||||
className="chatmessagebody__username--link"
|
||||
href={`/${user}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
data-content="sidecar-user"
|
||||
onClick={onContentTrigger}
|
||||
>
|
||||
{user}
|
||||
</a>
|
||||
</span>
|
||||
<span className="chatmessage__timestamp">
|
||||
{`${adjustTimestamp(timestamp)}`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="chatmessage__bodytext">{messageArea}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActionMessage.propTypes = {
|
||||
user: PropTypes.string.isRequired,
|
||||
color: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
timestamp: PropTypes.string,
|
||||
profileImageUrl: PropTypes.string,
|
||||
onContentTrigger: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
ActionMessage.defaultProps = {
|
||||
profileImageUrl: '',
|
||||
timestamp: null,
|
||||
};
|
||||
|
|
@ -1,182 +0,0 @@
|
|||
import { createDataHash } from '../util';
|
||||
|
||||
export function getAllMessages(channelId, messageOffset, successCb, failureCb) {
|
||||
fetch(`/chat_channels/${channelId}?message_offset=${messageOffset}`, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function sendMessage(messageObject, successCb, failureCb) {
|
||||
fetch('/messages', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
message_markdown: messageObject.message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: messageObject.activeChannelId,
|
||||
mentioned_users_id: messageObject.mentionedUsersId,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function editMessage(editedMessage, successCb, failureCb) {
|
||||
fetch(`/messages/${editedMessage.id}`, {
|
||||
method: 'PATCH',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
message_markdown: editedMessage.message,
|
||||
user_id: window.currentUser.id,
|
||||
chat_channel_id: editedMessage.activeChannelId,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function sendOpen(activeChannelId, successCb, failureCb) {
|
||||
fetch(`/chat_channels/${activeChannelId}/open`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function conductModeration(
|
||||
activeChannelId,
|
||||
message,
|
||||
successCb,
|
||||
failureCb,
|
||||
) {
|
||||
fetch(`/chat_channels/${activeChannelId}/moderate`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chat_channel: {
|
||||
command: message,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function getChannels(
|
||||
searchParams,
|
||||
additionalFilters,
|
||||
successCb,
|
||||
_failureCb,
|
||||
) {
|
||||
return createDataHash(additionalFilters, searchParams).then((response) => {
|
||||
if (
|
||||
searchParams.retrievalID === null ||
|
||||
response.result.filter(
|
||||
(e) => e.chat_channel_id === searchParams.retrievalID,
|
||||
).length === 1
|
||||
) {
|
||||
successCb(response.result, searchParams.query);
|
||||
} else {
|
||||
fetch(
|
||||
`/chat_channel_memberships/find_by_chat_channel_id?chat_channel_id=${searchParams.retrievalID}`,
|
||||
{
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
)
|
||||
.then((individualResponse) => individualResponse.json())
|
||||
.then((json) => {
|
||||
response.result.unshift(json);
|
||||
successCb(response.result, searchParams.query);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function getUnopenedChannelIds(successCb) {
|
||||
fetch('/chat_channels?state=unopened_ids', {
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then((json) => {
|
||||
successCb(json.unopened_ids);
|
||||
});
|
||||
}
|
||||
|
||||
export function getContent(url, successCb, failureCb) {
|
||||
fetch(url, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function getJSONContents(url, successCb, failureCb) {
|
||||
fetch(url, {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
export function deleteMessage(messageId, successCb, failureCb) {
|
||||
fetch(`/messages/${messageId}`, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Accept: 'application/json',
|
||||
'X-CSRF-Token': window.csrfToken,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: {
|
||||
user_id: window.currentUser.id,
|
||||
},
|
||||
}),
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
|
@ -1,186 +0,0 @@
|
|||
import { request } from '../../utilities/http';
|
||||
|
||||
/**
|
||||
* This function will get all details of the chat channel accrding to the membership role.
|
||||
*
|
||||
* @param {number} chatChannelMembershipId Current User chat channel membership ID
|
||||
*/
|
||||
export async function getChannelDetails(chatChannelMembershipId) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/chat_channel_info/${chatChannelMembershipId}`,
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to update the notification settings.
|
||||
*
|
||||
* @param {number} membershipId Current user Chat Channel membership Id.
|
||||
* @param {boolean} notificationBadge Boolean value for the notification
|
||||
*/
|
||||
export async function updatePersonalChatChannelNotificationSettings(
|
||||
membershipId,
|
||||
notificationBadge,
|
||||
) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/update_membership/${membershipId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
chat_channel_membership: {
|
||||
show_global_badge_notification: notificationBadge,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to reject chat channel joining request & pending requests.
|
||||
*
|
||||
* @param { number } channelId Active Chat Channel ID
|
||||
* @param { number } membershipId Requested user membership Id
|
||||
* @param { string } membershipStatus Requested user membership status
|
||||
*/
|
||||
export async function rejectChatChannelJoiningRequest(
|
||||
channelId,
|
||||
membershipId,
|
||||
membershipStatus,
|
||||
) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/remove_membership`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
status: membershipStatus || 'pending',
|
||||
chat_channel_id: channelId,
|
||||
membership_id: membershipId,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {number} channelId Active chat channel Id
|
||||
* @param {number} membershipId Chat channel joining request membership id
|
||||
*/
|
||||
export async function acceptChatChannelJoiningRequest(channelId, membershipId) {
|
||||
const response = await request(`/chat_channel_memberships/add_membership`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
chat_channel_id: channelId,
|
||||
membership_id: membershipId,
|
||||
chat_channel_membership: {
|
||||
user_action: 'accept',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export async function updateChatChannelDescription(
|
||||
channelId,
|
||||
description,
|
||||
discoverable,
|
||||
) {
|
||||
const response = await request(`/chat_channels/update_channel/${channelId}`, {
|
||||
method: 'PATCH',
|
||||
body: { chat_channel: { description, discoverable } },
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Active chat channel invitation
|
||||
*
|
||||
* @param {numner} channelId Active chat channel
|
||||
* @param {string} invitationUsernames UserNames coma seprated
|
||||
*/
|
||||
export async function sendChatChannelInvitation(
|
||||
channelId,
|
||||
invitationUsernames,
|
||||
) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/create_membership_request`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
chat_channel_membership: {
|
||||
chat_channel_id: channelId,
|
||||
invitation_usernames: invitationUsernames,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to leave the chat channel.
|
||||
*
|
||||
* @param {number} membershipId Current User Chat channel membership id
|
||||
*/
|
||||
export async function leaveChatChannelMembership(membershipId) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/leave_membership/${membershipId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to update the membership role
|
||||
* @param {number} membershipId selected User Chat channel membership id
|
||||
* @param {number} chatChannelId Current chat chaneel id
|
||||
* @param {string} role updated role for the membership
|
||||
*/
|
||||
export async function updateMembershipRole(membershipId, chatChannelId, role) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/update_membership_role/${chatChannelId}`,
|
||||
{
|
||||
method: 'PATCH',
|
||||
body: {
|
||||
chat_channel_membership: {
|
||||
chat_channel_id: chatChannelId,
|
||||
membership_id: membershipId,
|
||||
role,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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();
|
||||
}
|
||||
|
|
@ -1,143 +0,0 @@
|
|||
import { request } from '../../utilities/http';
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {channelId} channelId
|
||||
* @param {membershipId} membershipId
|
||||
*/
|
||||
export async function rejectJoiningRequest(channelId, membershipId) {
|
||||
const response = await request(
|
||||
`/chat_channel_memberships/remove_membership`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: {
|
||||
status: 'pending',
|
||||
chat_channel_id: channelId,
|
||||
membership_id: membershipId,
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
},
|
||||
);
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is responsible for the Accept joining request for channel
|
||||
* @param {number} channelId
|
||||
* @param {number} membershipId
|
||||
*/
|
||||
|
||||
export async function acceptJoiningRequest(channelId, membershipId) {
|
||||
const response = await request(`/chat_channel_memberships/add_membership`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
chat_channel_id: channelId,
|
||||
membership_id: membershipId,
|
||||
chat_channel_membership: {
|
||||
user_action: 'accept',
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
export function sendChannelRequest(id, successCb, failureCb) {
|
||||
request(`/join_chat_channel`, {
|
||||
method: 'POST',
|
||||
body: {
|
||||
chat_channel_membership: {
|
||||
chat_channel_id: id,
|
||||
},
|
||||
},
|
||||
credentials: 'same-origin',
|
||||
})
|
||||
.then((response) => response.json())
|
||||
.then(successCb)
|
||||
.catch(failureCb);
|
||||
}
|
||||
|
||||
/**
|
||||
* This function will get all the request realted to user and channel
|
||||
*/
|
||||
|
||||
export async function getChannelRequestInfo() {
|
||||
const response = await request(`/channel_request_info/`, {
|
||||
method: 'GET',
|
||||
credentials: 'same-origin',
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* This function handle user action on chat channel invitations
|
||||
*
|
||||
* @param {number} membershipId
|
||||
* @param {string} userAction
|
||||
*/
|
||||
|
||||
export async function updateMembership(membershipId, userAction) {
|
||||
const response = await request(`/chat_channel_memberships/${membershipId}`, {
|
||||
method: 'PUT',
|
||||
credentials: 'same-origin',
|
||||
body: {
|
||||
chat_channel_membership: {
|
||||
user_action: userAction,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {string} feedback_message
|
||||
* @param {string} type_of_feedback
|
||||
* @param {string} category
|
||||
* @param {string} reported_url
|
||||
*/
|
||||
export async function reportAbuse(
|
||||
feedback_message,
|
||||
feedback_type,
|
||||
category,
|
||||
offender_id,
|
||||
) {
|
||||
const response = await request('/feedback_messages', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
feedback_message: {
|
||||
message: feedback_message,
|
||||
feedback_type,
|
||||
category,
|
||||
offender_id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Blocks a user with the given ID from using Connect
|
||||
*
|
||||
* @param {number} userId
|
||||
*
|
||||
*
|
||||
*/
|
||||
|
||||
export async function blockUser(userId) {
|
||||
const response = await request('/user_blocks', {
|
||||
method: 'POST',
|
||||
body: {
|
||||
user_block: {
|
||||
blocked_id: userId,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Alert = ({ showAlert }) => {
|
||||
const otherClassname = showAlert ? '' : 'chatalert__default--hidden';
|
||||
|
||||
return (
|
||||
<div
|
||||
role="alert"
|
||||
aria-hidden={!showAlert}
|
||||
className={`chatalert__default ${otherClassname}`}
|
||||
>
|
||||
More new messages below
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Alert.propTypes = {
|
||||
showAlert: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
|
@ -1,18 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const Article = ({ resource: article }) => (
|
||||
<div className="activechatchannel__activeArticle">
|
||||
<iframe
|
||||
id="activecontent-iframe"
|
||||
src={article.path}
|
||||
title={article.title}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
|
||||
Article.propTypes = {
|
||||
resource: PropTypes.shape({
|
||||
id: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
|
@ -1,47 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
export const ChannelRequest = ({ resource: data, handleJoiningRequest }) => (
|
||||
<div>
|
||||
<div className="joining-message">
|
||||
<h2>Hey {data.user.name} !</h2>
|
||||
<h3>You are not a member of this group yet. Send a request to join.</h3>
|
||||
</div>
|
||||
<div className="user-picture">
|
||||
<div className="chatmessage__profilepic">
|
||||
<img
|
||||
className="chatmessagebody__profileimage"
|
||||
src={data.user.profile_image_90}
|
||||
alt={`${data.user.username} profile`}
|
||||
/>
|
||||
<img
|
||||
className="chatmessagebody__profileimage"
|
||||
src="/assets/organization.svg"
|
||||
alt={`${data.channel.name} profile`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="send-request">
|
||||
{data.channel.status !== 'joining_request' ? (
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={handleJoiningRequest}
|
||||
data-channel-id={data.channel.id}
|
||||
>
|
||||
{' '}
|
||||
Join {data.channel.name}{' '}
|
||||
</Button>
|
||||
) : (
|
||||
<Button variant="secondary"> Requested Already </Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
ChannelRequest.propTypes = {
|
||||
resource: PropTypes.shape({
|
||||
data: PropTypes.object,
|
||||
}).isRequired,
|
||||
handleJoiningRequest: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,119 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ChannelButton } from './components/ChannelButton';
|
||||
import { ConfigMenu } from './configMenu';
|
||||
import { channelSorter } from './util';
|
||||
|
||||
export const Channels = ({
|
||||
activeChannelId,
|
||||
chatChannels,
|
||||
unopenedChannelIds,
|
||||
handleSwitchChannel,
|
||||
expanded,
|
||||
filterQuery = '',
|
||||
channelsLoaded,
|
||||
currentUserId,
|
||||
triggerActiveContent,
|
||||
}) => {
|
||||
const sortedChatChannels = channelSorter(
|
||||
chatChannels,
|
||||
currentUserId,
|
||||
filterQuery,
|
||||
);
|
||||
const discoverableChannels = sortedChatChannels.discoverableChannels.map(
|
||||
(channel) => {
|
||||
return (
|
||||
<ChannelButton
|
||||
channel={channel}
|
||||
key={channel.chat_channel_id}
|
||||
discoverableChannel
|
||||
triggerActiveContent={triggerActiveContent}
|
||||
isActiveChannel={
|
||||
parseInt(activeChannelId, 10) === channel.chat_channel_id
|
||||
}
|
||||
/>
|
||||
);
|
||||
},
|
||||
);
|
||||
const channels = sortedChatChannels.activeChannels.map((channel) => {
|
||||
const isActive = parseInt(activeChannelId, 10) === channel.chat_channel_id;
|
||||
const isUnopened =
|
||||
!isActive && unopenedChannelIds.includes(channel.chat_channel_id);
|
||||
const newMessagesIndicator = isUnopened ? 'new' : 'old';
|
||||
const otherClassname = isActive
|
||||
? 'chatchanneltab--active'
|
||||
: 'chatchanneltab--inactive';
|
||||
|
||||
return (
|
||||
<ChannelButton
|
||||
channel={channel}
|
||||
key={channel.chat_channel_id}
|
||||
newMessagesIndicator={newMessagesIndicator}
|
||||
otherClassname={otherClassname}
|
||||
handleSwitchChannel={handleSwitchChannel}
|
||||
isUnopened={isUnopened}
|
||||
/>
|
||||
);
|
||||
});
|
||||
let topNotice = '';
|
||||
if (
|
||||
expanded &&
|
||||
filterQuery.length === 0 &&
|
||||
channelsLoaded &&
|
||||
(channels.length === 0 || channels[0].messages_count === 0)
|
||||
) {
|
||||
topNotice = (
|
||||
<div className="chatchannels__channelslistheader" role="alert">
|
||||
<span role="img" aria-label="emoji">
|
||||
👋
|
||||
</span>{' '}
|
||||
Welcome to
|
||||
<b> Connect</b>! You may message anyone you mutually follow.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let channelsListFooter = '';
|
||||
if (channels.length === 30) {
|
||||
channelsListFooter = (
|
||||
<div className="chatchannels__channelslistfooter">...</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="chatchannels">
|
||||
<div
|
||||
className="chatchannels__channelslist"
|
||||
id="chatchannels__channelslist"
|
||||
data-testid="chat-channels-list"
|
||||
>
|
||||
{topNotice}
|
||||
{channels}
|
||||
{discoverableChannels.length > 0 && filterQuery.length > 0 ? (
|
||||
<div>
|
||||
<span className="crayons-indicator crayons-indicator--">
|
||||
Global Channel Search
|
||||
</span>
|
||||
{discoverableChannels}
|
||||
</div>
|
||||
) : (
|
||||
''
|
||||
)}
|
||||
{channelsListFooter}
|
||||
</div>
|
||||
|
||||
<ConfigMenu />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Channels.propTypes = {
|
||||
activeChannelId: PropTypes.number.isRequired,
|
||||
chatChannels: PropTypes.arrayOf(PropTypes.objectOf()).isRequired,
|
||||
unopenedChannelIds: PropTypes.arrayOf().isRequired,
|
||||
handleSwitchChannel: PropTypes.func.isRequired,
|
||||
triggerActiveContent: PropTypes.func.isRequired,
|
||||
expanded: PropTypes.bool.isRequired,
|
||||
filterQuery: PropTypes.string.isRequired,
|
||||
channelsLoaded: PropTypes.bool.isRequired,
|
||||
currentUserId: PropTypes.string.isRequired,
|
||||
};
|
||||
File diff suppressed because it is too large
Load diff
|
|
@ -1,93 +0,0 @@
|
|||
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 a button to switch focus to a channel
|
||||
*
|
||||
* @param {object} props
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @example
|
||||
*
|
||||
* <ChannelButton
|
||||
* channel={channel}
|
||||
newMessagesIndicator={newMessagesIndicator}
|
||||
otherClassname={otherClassname}
|
||||
handleSwitchChannel={handleSwitchChannel}
|
||||
isUnopened={isUnopened}
|
||||
* />
|
||||
*
|
||||
*/
|
||||
|
||||
export 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 align-left'
|
||||
: `chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicator} crayons-btn--ghost align-left`
|
||||
}
|
||||
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,
|
||||
};
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
/**
|
||||
* This component renders a button that is used for filtering chat channels.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string} props.type - The type of chat channel selected
|
||||
* @param {string} props.name - Used for testing and is displayed to the user on the button
|
||||
* @param {boolean} props.active - Should the button have the `active` CSS class`?
|
||||
* @param {function} props.onClick - Fired with the onClick trigger
|
||||
*
|
||||
* @component
|
||||
*
|
||||
* @example
|
||||
* <ChannelFilterButton
|
||||
* type="all"
|
||||
* name="all"
|
||||
* active={state.channelTypeFilter === 'all'}
|
||||
* onClick={this.triggerChannelTypeFilter}
|
||||
* />
|
||||
*/
|
||||
|
||||
export function ChannelFilterButton({ type, name, active, onClick }) {
|
||||
return (
|
||||
<Button
|
||||
data-channel-type={type}
|
||||
data-testid={name}
|
||||
onClick={onClick}
|
||||
className={`chat__channeltypefilterbutton crayons-indicator crayons-indicator--${
|
||||
active ? 'accent' : ''
|
||||
}`}
|
||||
>
|
||||
{name}
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
ChannelFilterButton.propTypes = {
|
||||
type: PropTypes.string.isRequired,
|
||||
name: PropTypes.string.isRequired,
|
||||
active: PropTypes.bool.isRequired,
|
||||
onClick: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaultChannelPropTypes } from '../../common-prop-types/channel-list-prop-type';
|
||||
|
||||
/**
|
||||
* Returns an image to help users identify chat channels
|
||||
*
|
||||
* @param {object} channel - Contains information about the channel this image represents
|
||||
* @param {string} newMessagesIndicator - Used to construct a CSS classname
|
||||
* @param {boolean} discoverableChannel - Used to determine which CSS class should be applied
|
||||
*
|
||||
* @example
|
||||
* <ChannelImage
|
||||
channel={{channel_name: "Example Channel",
|
||||
channel_type: "direct",
|
||||
channel_modified_slug: "someslug-f7ff2c5a6a",
|
||||
id: 1,
|
||||
chat_channel_id: 20,
|
||||
status: "active",
|
||||
channel_image: "/some/path/to/image"}}
|
||||
newMessagesIndicator="old"
|
||||
discoverableChannel={false}
|
||||
/>
|
||||
*/
|
||||
|
||||
export function ChannelImage({
|
||||
channel,
|
||||
newMessagesIndicator,
|
||||
discoverableChannel,
|
||||
}) {
|
||||
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,
|
||||
newMessagesIndicator: PropTypes.string,
|
||||
discoverableChannel: PropTypes.bool,
|
||||
};
|
||||
|
|
@ -1,21 +0,0 @@
|
|||
import { h, createContext } from 'preact';
|
||||
import { useReducer } from 'preact/hooks';
|
||||
|
||||
const store = createContext();
|
||||
const { Provider } = store;
|
||||
|
||||
/**
|
||||
* Provides state management for the Connect feature of Forem.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {JSX.Element} props.children Child React elements
|
||||
* @param {object} props.initialState The initial value to store as state
|
||||
* @param {Function} props.reducer Given the current state and an action, returns the new state
|
||||
*/
|
||||
function ConnectStateProvider({ children, initialState, reducer }) {
|
||||
const [state, dispatch] = useReducer(reducer, initialState);
|
||||
|
||||
return <Provider value={{ state, dispatch }}>{children}</Provider>;
|
||||
}
|
||||
|
||||
export { store, ConnectStateProvider };
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue