✂✂✂ 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:
Michael Kohl 2021-11-18 21:21:00 +07:00 committed by GitHub
parent 83b4a6db81
commit 09828853f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
250 changed files with 176 additions and 15427 deletions

View file

@ -123,13 +123,6 @@ GA_TRACKING_ID="Optional"
# (https://mailchimp.com/developer/) # (https://mailchimp.com/developer/)
MAILCHIMP_API_KEY="Optional-valid" 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) # Google recaptcha (Optional)
# (https://developers.google.com/recaptcha/intro) # (https://developers.google.com/recaptcha/intro)
RECAPTCHA_SECRET= RECAPTCHA_SECRET=

View file

@ -18,6 +18,5 @@ require:
Rails/HasManyOrHasOneDependent: Rails/HasManyOrHasOneDependent:
Exclude: Exclude:
- 'app/models/article.rb' - 'app/models/article.rb'
- 'app/models/chat_channel.rb'
- 'app/models/organization.rb' - 'app/models/organization.rb'
- 'app/models/user.rb' - 'app/models/user.rb'

View file

@ -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 "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 "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 "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 "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 "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 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 "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 "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 "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-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-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 gem "rack-timeout", "~> 0.6" # Rack middleware which aborts requests that have been running for longer than a specified timeout

View file

@ -561,11 +561,6 @@ GEM
activesupport (>= 3.0.0) activesupport (>= 3.0.0)
pundit-matchers (1.7.0) pundit-matchers (1.7.0)
rspec-rails (>= 3.0.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) raabro (1.4.0)
racc (1.6.0) racc (1.6.0)
rack (2.2.3) rack (2.2.3)
@ -946,6 +941,7 @@ DEPENDENCIES
html_truncator (~> 0.4) html_truncator (~> 0.4)
htmlentities (~> 4.3, >= 4.3.4) htmlentities (~> 4.3, >= 4.3.4)
httparty (~> 0.20) httparty (~> 0.20)
httpclient (~> 2.8.3)
hypershield (~> 0.2.2) hypershield (~> 0.2.2)
i18n-js (~> 3.9.0) i18n-js (~> 3.9.0)
i18n-tasks (~> 0.9.35) i18n-tasks (~> 0.9.35)
@ -982,7 +978,6 @@ DEPENDENCIES
puma (~> 5.5.2) puma (~> 5.5.2)
pundit (~> 2.1) pundit (~> 2.1)
pundit-matchers (~> 1.7) pundit-matchers (~> 1.7)
pusher (~> 2.0)
rack-attack (~> 6.5.0) rack-attack (~> 6.5.0)
rack-cors (~> 1.1) rack-cors (~> 1.1)
rack-host-redirect (~> 1.3) rack-host-redirect (~> 1.3)

View file

@ -1,6 +1,6 @@
/* /*
global initializeLocalStorageRender, initializeBodyData, global initializeLocalStorageRender, initializeBodyData,
initializeAllChatButtons, initializeAllTagEditButtons, initializeUserFollowButts, initializeAllTagEditButtons, initializeUserFollowButts,
initializeBaseTracking, initializeCommentsPage, initializeBaseTracking, initializeCommentsPage,
initializeArticleDate, initializeArticleReactions, initNotifications, initializeArticleDate, initializeArticleReactions, initNotifications,
initializeCommentDate, initializeSettings, initializeCommentDate, initializeSettings,
@ -50,7 +50,6 @@ function initializePage() {
clearInterval(waitingForDataLoad); clearInterval(waitingForDataLoad);
if (document.body.getAttribute('data-user-status') === 'logged-in') { if (document.body.getAttribute('data-user-status') === 'logged-in') {
initializeBaseUserData(); initializeBaseUserData();
initializeAllChatButtons();
initializeAllTagEditButtons(); initializeAllTagEditButtons();
} }
initializeBroadcast(); initializeBroadcast();

View file

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

View file

@ -46,12 +46,6 @@ function sendFetch(switchStatement, body) {
addTokenToBody: true, addTokenToBody: true,
body, body,
}); });
case 'chat-creation':
return fetchCallback({
url: '/chat_channels/create_chat',
addTokenToBody: true,
body,
});
case 'block-user': case 'block-user':
return fetchCallback({ return fetchCallback({
url: '/user_blocks', url: '/user_blocks',

File diff suppressed because it is too large Load diff

View file

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

View file

@ -19,8 +19,6 @@
@import 'ltags/LiquidTags'; @import 'ltags/LiquidTags';
@import 'chat';
@import 'email_subscriptions'; @import 'email_subscriptions';
@import 'runtime'; @import 'runtime';

View file

@ -46,7 +46,6 @@ body.dark-theme {
.image-upload-button button, .image-upload-button button,
.icon-image, .icon-image,
.dev-badge, .dev-badge,
.chatchannels__config img,
.message__actions img { .message__actions img {
-webkit-filter: invert(95%); -webkit-filter: invert(95%);
filter: invert(95%); filter: invert(95%);

View file

@ -8,7 +8,6 @@ module Admin
articles: "https://admin.forem.com/docs/forem-basics/posts", articles: "https://admin.forem.com/docs/forem-basics/posts",
badges: "https://admin.forem.com/docs/forem-basics/badges", badges: "https://admin.forem.com/docs/forem-basics/badges",
badge_achievements: "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", display_ads: "https://admin.forem.com/docs/advanced-customization/display-ads",
feedback_messages: "https://admin.forem.com/docs/advanced-customization/reports", feedback_messages: "https://admin.forem.com/docs/advanced-customization/reports",
html_variants: "https://admin.forem.com/docs/advanced-customization/html-variants", html_variants: "https://admin.forem.com/docs/advanced-customization/html-variants",

View file

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

View file

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

View file

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

View file

@ -80,7 +80,7 @@ module ListingsToolkit
ALLOWED_PARAMS = %i[ ALLOWED_PARAMS = %i[
title body_markdown listing_category_id tag_list title body_markdown listing_category_id tag_list
expires_at contact_via_connect location organization_id action expires_at location organization_id action
].freeze ].freeze
# Filter for a set of known safe params # Filter for a set of known safe params

View file

@ -19,7 +19,6 @@ class EmailSubscriptionsController < ApplicationController
email_comment_notifications: "comment notifications", email_comment_notifications: "comment notifications",
email_follower_notifications: "follower notifications", email_follower_notifications: "follower notifications",
email_mention_notifications: "mention notifications", email_mention_notifications: "mention notifications",
email_connect_messages: "#{Settings::Community.community_name} connect messages",
email_unread_notifications: "unread notifications", email_unread_notifications: "unread notifications",
email_badge_notifications: "badge notifications" email_badge_notifications: "badge notifications"
}.freeze }.freeze

View file

@ -3,7 +3,7 @@ class ListingsController < ApplicationController
INDEX_JSON_OPTIONS = { INDEX_JSON_OPTIONS = {
only: %i[ 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 originally_published_at
], ],
methods: %i[category], methods: %i[category],

View file

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

View file

@ -22,7 +22,6 @@ class ModerationsController < ApplicationController
@articles = articles.includes(:user).to_json(JSON_OPTIONS) @articles = articles.includes(:user).to_json(JSON_OPTIONS)
@tag = Tag.find_by(name: params[:tag]) || not_found if params[:tag].present? @tag = Tag.find_by(name: params[:tag]) || not_found if params[:tag].present?
@current_user_tags = current_user.moderator_for_tags @current_user_tags = current_user.moderator_for_tags
@community_mod_channel = current_user.chat_channels.find_by("channel_name LIKE ?", "Community Mods: Team%")
end end
def article def article

View file

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

View file

@ -1,17 +1,8 @@
class SearchController < ApplicationController 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 :format_integer_params
before_action :sanitize_params, only: %i[listings reactions feed_content] 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 = [ LISTINGS_PARAMS = [
:category, :category,
:listing_search, :listing_search,
@ -65,23 +56,6 @@ class SearchController < ApplicationController
render json: { result: result } render json: { result: result }
end 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 def listings
result = Search::Listing.search_documents( result = Search::Listing.search_documents(
category: listing_params[:category], category: listing_params[:category],
@ -193,10 +167,6 @@ class SearchController < ApplicationController
) )
end end
def chat_channel_params
params.permit(CHAT_CHANNEL_PARAMS)
end
def listing_params def listing_params
params.permit(LISTINGS_PARAMS) params.permit(LISTINGS_PARAMS)
end end

View file

@ -19,7 +19,6 @@ class UserBlocksController < ApplicationController
@user_block.config = "default" @user_block.config = "default"
if @user_block.save if @user_block.save
UserBlocks::ChannelHandler.new(@user_block).block_chat_channel
current_user.stop_following(@user_block.blocked) current_user.stop_following(@user_block.blocked)
@user_block.blocked.stop_following(current_user) @user_block.blocked.stop_following(current_user)
render json: { result: "blocked" } render json: { result: "blocked" }
@ -39,7 +38,6 @@ class UserBlocksController < ApplicationController
authorize @user_block authorize @user_block
if @user_block.destroy if @user_block.destroy
UserBlocks::ChannelHandler.new(@user_block).unblock_chat_channel
render json: { result: "unblocked" } render json: { result: "unblocked" }
else else
render json: { error: @user_block.errors_as_sentence, status: 422 }, status: :unprocessable_entity render json: { error: @user_block.errors_as_sentence, status: 422 }, status: :unprocessable_entity

View file

@ -7,7 +7,6 @@ module Users
ALLOWED_PARAMS = %i[email_badge_notifications ALLOWED_PARAMS = %i[email_badge_notifications
email_comment_notifications email_comment_notifications
email_community_mod_newsletter email_community_mod_newsletter
email_connect_messages
email_digest_periodic email_digest_periodic
email_follower_notifications email_follower_notifications
email_membership_newsletter email_membership_newsletter

View file

@ -1,5 +1,5 @@
module ApplicationHelper module ApplicationHelper
# rubocop:disable Performance/OpenStruct # rubocop:disable Style/OpenStructUse, Performance/OpenStruct
USER_COLORS = ["#19063A", "#dce9f3"].freeze USER_COLORS = ["#19063A", "#dce9f3"].freeze
DELETED_USER = OpenStruct.new( DELETED_USER = OpenStruct.new(
@ -11,7 +11,7 @@ module ApplicationHelper
twitter_username: nil, twitter_username: nil,
github_username: nil, github_username: nil,
) )
# rubocop:enable Performance/OpenStruct # rubocop:enable Style/OpenStructUse, Performance/OpenStruct
LARGE_USERBASE_THRESHOLD = 1000 LARGE_USERBASE_THRESHOLD = 1000

View file

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

View file

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

View file

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

View file

@ -23,6 +23,6 @@ module NotificationsHelper
) )
else else
I18n.t(action, user: key_to_link.call("user")) I18n.t(action, user: key_to_link.call("user"))
end.html_safe end.html_safe # rubocop:disable Rails/OutputSafety
end end
end end

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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"] *',
});
});
});

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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