[deploy] 🚀 Feature: Chat channel membership manager component (#8945)
* Feature 🚀 : Ability to delete messages in chat channels - Sending message ID to frontend - Deleting Message - Use pusher to delete message realtime * Minor Bug 🐞: Show message action only for current user - User can delete or edit their own messages * Test cases added * Bug 🐞: Update message id for receiver Message id was not sent to receiver by pusher * Refactoring🛠: Message controller refactoring * Test Cases📝 : Specs for Delete message added * Feature 🚀 : Ability to edit messages * Test Cases📝 : Specs for Edit message added * Merge conflict resolved * fix video content issue * add UI for membership management * add api to update memberahip role * add methods for manage membership * Add integration test cases * add emoji for admin * Open member profile in sidecar only * 🐞 Problem with direct channel sidecar * 🐞 Few other UI enhancements * fix mod typo * add limit upto 4 for display active memberships * fix UI issues * fix action ui for membership * fix sidebar redirection issue * fix svg buttons * add test cases * fixed broken spec * fix PR suggestions * remove not used code * fix typo * fix PR suggestion * fix typos * add invitation url expiry * fix specs * fix PR suggestions * removed unused gem * remove presenter format * handle invitation link expiry with redis * fix PR suggestions * user can view non-discoverable channel invitation link * fix typos * remove commented code * add spacing * PR suggestions * remove class from button * replace componentDidMount with commen function * remove action button for single membership * add chat message on update role * add spece between lines Co-authored-by: Narender Singh <narender2031@gmail.com> Co-authored-by: Fernando Valverde <fdov88@gmail.com>
This commit is contained in:
parent
803d482f90
commit
3addb64326
40 changed files with 1333 additions and 430 deletions
|
|
@ -2,4 +2,5 @@
|
||||||
|
|
||||||
## Reporting a Vulnerability
|
## Reporting a Vulnerability
|
||||||
|
|
||||||
Please refer to https://dev.to/security if you would like to report a vulnerability.
|
Please refer to https://dev.to/security if you would like to report a
|
||||||
|
vulnerability.
|
||||||
|
|
|
||||||
|
|
@ -282,6 +282,7 @@
|
||||||
@include themeable(border, theme-border, 1px solid $outline-color);
|
@include themeable(border, theme-border, 1px solid $outline-color);
|
||||||
padding: 13px;
|
padding: 13px;
|
||||||
min-width: 50%;
|
min-width: 50%;
|
||||||
|
overflow-x: hidden;
|
||||||
@media screen and (min-width: 1440px) {
|
@media screen and (min-width: 1440px) {
|
||||||
min-width: 45%;
|
min-width: 45%;
|
||||||
}
|
}
|
||||||
|
|
@ -1164,3 +1165,66 @@
|
||||||
.channel_details {
|
.channel_details {
|
||||||
margin-top: 30px;
|
margin-top: 30px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.chat_channel-member-list {
|
||||||
|
height: 300px;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-list-item {
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.admin-emoji {
|
||||||
|
width: 6%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.admin-emoji-button {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-section {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.membership-actions {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.membership-management__dropdown-memu {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
background-color: var(--theme-container-background, #fff);
|
||||||
|
border-style: solid;
|
||||||
|
border-color: var(--theme-color, #0a0a0a);
|
||||||
|
border-radius: 5px;
|
||||||
|
border-width: 1px;
|
||||||
|
z-index: 100;
|
||||||
|
width: 200px;
|
||||||
|
right: 78px;
|
||||||
|
top: 20px;
|
||||||
|
|
||||||
|
button {
|
||||||
|
width: 100%;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.membership-management__dropdown-button {
|
||||||
|
height: 17px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--theme-color, #0a0a0a);
|
||||||
|
|
||||||
|
&:hover + .membership-management__dropdown-memu {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,15 +25,17 @@ class ChatChannelMembershipsController < ApplicationController
|
||||||
@membership = ChatChannelMembership.find(params[:id])
|
@membership = ChatChannelMembership.find(params[:id])
|
||||||
authorize @membership
|
authorize @membership
|
||||||
@channel = @membership.chat_channel
|
@channel = @membership.chat_channel
|
||||||
data = ChatChannelDetailPresenter.new(@channel, @membership).as_json
|
invite_cache_key = "chat-channel-invite-#{@channel.id}"
|
||||||
|
invitation_slug = Rails.cache.fetch(invite_cache_key, expires_in: 12.hours) do
|
||||||
render json: { success: true, result: data, message: "" }, success: :ok
|
"invitation-link-#{SecureRandom.hex(3)}"
|
||||||
|
end
|
||||||
|
@invitation_link = "/join_channel_invitation/#{@channel.slug}?invitation_slug=#{invitation_slug}"
|
||||||
end
|
end
|
||||||
|
|
||||||
def create_membership_request
|
def create_membership_request
|
||||||
chat_channel = ChatChannel.find_by(id: channel_membership_request_params[:chat_channel_id])
|
chat_channel = ChatChannel.find_by(id: channel_membership_params[:chat_channel_id])
|
||||||
authorize chat_channel, :update?
|
authorize chat_channel, :update?
|
||||||
usernames = channel_membership_request_params[:invitation_usernames].split(",").map do |username|
|
usernames = channel_membership_params[:invitation_usernames].split(",").map do |username|
|
||||||
username.strip.delete("@")
|
username.strip.delete("@")
|
||||||
end
|
end
|
||||||
users = User.where(username: usernames)
|
users = User.where(username: usernames)
|
||||||
|
|
@ -131,14 +133,82 @@ class ChatChannelMembershipsController < ApplicationController
|
||||||
end
|
end
|
||||||
end
|
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 connect_path(@chat_channel.slug) if existing_membership && 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.new(user_id: current_user.id, chat_channel_id: chat_channel.id)
|
||||||
|
membership.save
|
||||||
|
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
|
private
|
||||||
|
|
||||||
def permitted_params
|
def permitted_params
|
||||||
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
|
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
|
||||||
end
|
end
|
||||||
|
|
||||||
def channel_membership_request_params
|
def channel_membership_params
|
||||||
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames)
|
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames, :membership_id, :role)
|
||||||
end
|
end
|
||||||
|
|
||||||
def respond_to_invitation(previous_status)
|
def respond_to_invitation(previous_status)
|
||||||
|
|
@ -176,9 +246,9 @@ class ChatChannelMembershipsController < ApplicationController
|
||||||
notice = "Invitation rejected."
|
notice = "Invitation rejected."
|
||||||
end
|
end
|
||||||
|
|
||||||
flash[:settings_notice] = notice
|
membership_user = helpers.format_membership(@chat_channel_membership)
|
||||||
|
|
||||||
membership_user = MembershipUserPresenter.new(@chat_channel_membership).as_json
|
flash[:settings_notice] = notice
|
||||||
|
|
||||||
respond_to do |format|
|
respond_to do |format|
|
||||||
format.html { redirect_to chat_channel_memberships_path }
|
format.html { redirect_to chat_channel_memberships_path }
|
||||||
|
|
|
||||||
27
app/helpers/chat_channel_membership_helper.rb
Normal file
27
app/helpers/chat_channel_membership_helper.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
module ChatChannelMembershipHelper
|
||||||
|
def membership_users(memberships)
|
||||||
|
memberships.includes(:user).map do |membership|
|
||||||
|
{
|
||||||
|
name: membership.user.name,
|
||||||
|
username: membership.user.username,
|
||||||
|
user_id: membership.user.id,
|
||||||
|
membership_id: membership.id,
|
||||||
|
role: membership.role,
|
||||||
|
status: membership.status,
|
||||||
|
image: ProfileImage.new(membership.user).get(width: 90)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def format_membership(membership)
|
||||||
|
{
|
||||||
|
name: membership.user.name,
|
||||||
|
username: membership.user.username,
|
||||||
|
user_id: membership.user.id,
|
||||||
|
membership_id: membership.id,
|
||||||
|
role: membership.role,
|
||||||
|
status: membership.status,
|
||||||
|
image: ProfileImage.new(membership.user).get(width: 90)
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,12 +1,18 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||||
import Membership from './Membership';
|
import Membership from './Membership';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
const ActiveMembershipSection = ({
|
const ActiveMembershipSection = ({
|
||||||
activeMemberships,
|
activeMemberships,
|
||||||
removeMembership,
|
removeMembership,
|
||||||
currentMembershipRole,
|
currentMembershipRole,
|
||||||
|
toggleScreens,
|
||||||
}) => {
|
}) => {
|
||||||
|
const activeMembershipList = activeMemberships.slice(0, 4);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
data-testid="active-memberships"
|
data-testid="active-memberships"
|
||||||
|
|
@ -14,35 +20,34 @@ const ActiveMembershipSection = ({
|
||||||
data-active-count={activeMemberships ? activeMemberships.length : 0}
|
data-active-count={activeMemberships ? activeMemberships.length : 0}
|
||||||
>
|
>
|
||||||
<h3 className="mb-2 active_members">Members</h3>
|
<h3 className="mb-2 active_members">Members</h3>
|
||||||
{activeMemberships && activeMemberships.length > 0
|
{activeMembershipList.map((activeMembership) => (
|
||||||
? activeMemberships.map((pendingMembership) => (
|
<Membership
|
||||||
<Membership
|
membership={activeMembership}
|
||||||
membership={pendingMembership}
|
removeMembership={removeMembership}
|
||||||
removeMembership={removeMembership}
|
membershipType="active"
|
||||||
membershipType="active"
|
currentMembershipRole={currentMembershipRole}
|
||||||
currentMembershipRole={currentMembershipRole}
|
className="active-member"
|
||||||
className="active-member"
|
/>
|
||||||
/>
|
))}
|
||||||
))
|
<div className="row align-center">
|
||||||
: null}
|
<Button
|
||||||
|
className="align-center view-all-memberships"
|
||||||
|
size="s"
|
||||||
|
onClick={toggleScreens}
|
||||||
|
type="button"
|
||||||
|
>
|
||||||
|
View All
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
ActiveMembershipSection.propTypes = {
|
ActiveMembershipSection.propTypes = {
|
||||||
activeMemberships: PropTypes.arrayOf(
|
activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
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,
|
|
||||||
removeMembership: PropTypes.func.isRequired,
|
removeMembership: PropTypes.func.isRequired,
|
||||||
currentMembershipRole: PropTypes.string.isRequired,
|
currentMembershipRole: PropTypes.string.isRequired,
|
||||||
|
toggleScreens: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ActiveMembershipSection;
|
export default ActiveMembershipSection;
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||||
import ActiveMembershipSection from './ActiveMembershipsSection';
|
import ActiveMembershipSection from './ActiveMembershipsSection';
|
||||||
import PendingMembershipSection from './PendingMembershipSection'
|
import PendingMembershipSection from './PendingMembershipSection';
|
||||||
import RequestedMembershipSection from './RequestedMembershipSection';
|
import RequestedMembershipSection from './RequestedMembershipSection';
|
||||||
|
|
||||||
const ChatChannelMembershipSection = ({
|
const ChatChannelMembershipSection = ({
|
||||||
|
|
@ -11,7 +12,8 @@ const ChatChannelMembershipSection = ({
|
||||||
chatChannelAcceptMembership,
|
chatChannelAcceptMembership,
|
||||||
activeMemberships,
|
activeMemberships,
|
||||||
removeMembership,
|
removeMembership,
|
||||||
currentMembershipRole
|
currentMembershipRole,
|
||||||
|
toggleScreens,
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="membership-list">
|
<div className="membership-list">
|
||||||
|
|
@ -19,6 +21,7 @@ const ChatChannelMembershipSection = ({
|
||||||
activeMemberships={activeMemberships}
|
activeMemberships={activeMemberships}
|
||||||
removeMembership={removeMembership}
|
removeMembership={removeMembership}
|
||||||
currentMembershipRole={currentMembershipRole}
|
currentMembershipRole={currentMembershipRole}
|
||||||
|
toggleScreens={toggleScreens}
|
||||||
/>
|
/>
|
||||||
<PendingMembershipSection
|
<PendingMembershipSection
|
||||||
pendingMemberships={pendingMemberships}
|
pendingMemberships={pendingMemberships}
|
||||||
|
|
@ -32,38 +35,17 @@ const ChatChannelMembershipSection = ({
|
||||||
currentMembershipRole={currentMembershipRole}
|
currentMembershipRole={currentMembershipRole}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
ChatChannelMembershipSection.propTypes = {
|
ChatChannelMembershipSection.propTypes = {
|
||||||
pendingMemberships: PropTypes.arrayOf(PropTypes.shape({
|
pendingMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
name: PropTypes.string.isRequired,
|
requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
membership_id: PropTypes.number.isRequired,
|
activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
user_id: PropTypes.number.isRequired,
|
|
||||||
role: PropTypes.string.isRequired,
|
|
||||||
image: PropTypes.string.isRequired,
|
|
||||||
username: PropTypes.string.isRequired,
|
|
||||||
})).isRequired,
|
|
||||||
requestedMemberships: 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,
|
|
||||||
})).isRequired,
|
|
||||||
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,
|
|
||||||
removeMembership: PropTypes.func.isRequired,
|
removeMembership: PropTypes.func.isRequired,
|
||||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||||
currentMembershipRole: PropTypes.string.isRequired
|
currentMembershipRole: PropTypes.string.isRequired,
|
||||||
}
|
toggleScreens: PropTypes.func.isRequired,
|
||||||
|
};
|
||||||
|
|
||||||
export default ChatChannelMembershipSection;
|
export default ChatChannelMembershipSection;
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,12 @@ import {
|
||||||
updateChatChannelDescription,
|
updateChatChannelDescription,
|
||||||
sendChatChannelInvitation,
|
sendChatChannelInvitation,
|
||||||
leaveChatChannelMembership,
|
leaveChatChannelMembership,
|
||||||
|
updateMembershipRole,
|
||||||
} from '../actions/chat_channel_setting_actions';
|
} from '../actions/chat_channel_setting_actions';
|
||||||
|
|
||||||
import { addSnackbarItem } from '../../Snackbar';
|
import { addSnackbarItem } from '../../Snackbar';
|
||||||
import ModSection from './ModSection';
|
import ManageActiveMembership from './MembershipManager/ManageActiveMembership';
|
||||||
import PersonalSettings from './PersonalSetting';
|
import ChatChannelSettingsSection from './ChatChannelSettingsSection';
|
||||||
import LeaveMembershipSection from './LeaveMembershipSection';
|
|
||||||
import ModFaqSection from './ModFaqSection';
|
|
||||||
import ChannelDescriptionSection from './ChannelDescriptionSection';
|
|
||||||
import ChatChannelMembershipSection from './ChatChannelMembershipSection';
|
|
||||||
|
|
||||||
export default class ChatChannelSettings extends Component {
|
export default class ChatChannelSettings extends Component {
|
||||||
static propTypes = {
|
static propTypes = {
|
||||||
|
|
@ -35,16 +32,30 @@ export default class ChatChannelSettings extends Component {
|
||||||
requestedMemberships: [],
|
requestedMemberships: [],
|
||||||
chatChannel: null,
|
chatChannel: null,
|
||||||
currentMembership: null,
|
currentMembership: null,
|
||||||
activeMembershipId: props.activeMembershipId,
|
activeMembershipId: null,
|
||||||
channelDescription: null,
|
channelDescription: null,
|
||||||
channelDiscoverable: null,
|
channelDiscoverable: null,
|
||||||
invitationUsernames: null,
|
invitationUsernames: null,
|
||||||
showGlobalBadgeNotification: null,
|
showGlobalBadgeNotification: null,
|
||||||
|
displaySettings: true,
|
||||||
|
displayMembershipManager: false,
|
||||||
|
invitationLink: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
const { activeMembershipId } = this.state;
|
this.updateChannelDetails();
|
||||||
|
}
|
||||||
|
|
||||||
|
componentWillReceiveProps() {
|
||||||
|
const { activeMembershipId } = this.props;
|
||||||
|
this.setState({
|
||||||
|
activeMembershipId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
updateChannelDetails = () => {
|
||||||
|
const { activeMembershipId } = this.props;
|
||||||
|
|
||||||
getChannelDetails(activeMembershipId)
|
getChannelDetails(activeMembershipId)
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
|
|
@ -60,6 +71,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
channelDiscoverable: result.chat_channel.discoverable,
|
channelDiscoverable: result.chat_channel.discoverable,
|
||||||
showGlobalBadgeNotification:
|
showGlobalBadgeNotification:
|
||||||
result.current_membership.show_global_badge_notification,
|
result.current_membership.show_global_badge_notification,
|
||||||
|
invitationLink: result.invitation_link,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
|
|
@ -74,7 +86,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
errorMessages: error.message,
|
errorMessages: error.message,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
};
|
||||||
|
|
||||||
handleDescriptionChange = (e) => {
|
handleDescriptionChange = (e) => {
|
||||||
const description = e.target.value;
|
const description = e.target.value;
|
||||||
|
|
@ -83,7 +95,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handlePersonChatChennelSetting = (e) => {
|
handlePersonChannelSetting = (e) => {
|
||||||
const status = e.target.checked;
|
const status = e.target.checked;
|
||||||
this.setState({
|
this.setState({
|
||||||
showGlobalBadgeNotification: status,
|
showGlobalBadgeNotification: status,
|
||||||
|
|
@ -149,6 +161,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
|
|
||||||
updateMemberships = (membershipId, response, membershipStatus) => {
|
updateMemberships = (membershipId, response, membershipStatus) => {
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
|
this.updateChannelDetails();
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
return {
|
return {
|
||||||
errorMessages: null,
|
errorMessages: null,
|
||||||
|
|
@ -236,7 +249,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
const { message } = response;
|
const { message } = response;
|
||||||
|
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.componentDidMount();
|
this.updateChannelDetails();
|
||||||
this.setState((prevState) => {
|
this.setState((prevState) => {
|
||||||
return {
|
return {
|
||||||
errorMessages: null,
|
errorMessages: null,
|
||||||
|
|
@ -265,13 +278,13 @@ export default class ChatChannelSettings extends Component {
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleChatChannelInvitations = async () => {
|
handleChannelInvitations = async () => {
|
||||||
const { invitationUsernames, chatChannel } = this.state;
|
const { invitationUsernames, chatChannel } = this.state;
|
||||||
const { id } = chatChannel;
|
const { id } = chatChannel;
|
||||||
const response = await sendChatChannelInvitation(id, invitationUsernames);
|
const response = await sendChatChannelInvitation(id, invitationUsernames);
|
||||||
const { message } = response;
|
const { message } = response;
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.componentDidMount();
|
this.updateChannelDetails();
|
||||||
this.setState({
|
this.setState({
|
||||||
errorMessages: null,
|
errorMessages: null,
|
||||||
successMessages: response.message,
|
successMessages: response.message,
|
||||||
|
|
@ -286,7 +299,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
addSnackbarItem({ message });
|
addSnackbarItem({ message });
|
||||||
};
|
};
|
||||||
|
|
||||||
handleleaveChatChannelMembership = async () => {
|
handleleaveChannelMembership = async () => {
|
||||||
// eslint-disable-next-line no-restricted-globals
|
// eslint-disable-next-line no-restricted-globals
|
||||||
const actionStatus = confirm(
|
const actionStatus = confirm(
|
||||||
'Are you absolutely sure you want to leave this channel? This action is permanent.',
|
'Are you absolutely sure you want to leave this channel? This action is permanent.',
|
||||||
|
|
@ -295,7 +308,7 @@ export default class ChatChannelSettings extends Component {
|
||||||
if (actionStatus) {
|
if (actionStatus) {
|
||||||
const response = await leaveChatChannelMembership(currentMembership.id);
|
const response = await leaveChatChannelMembership(currentMembership.id);
|
||||||
if (response.success) {
|
if (response.success) {
|
||||||
this.componentDidMount();
|
this.updateChannelDetails();
|
||||||
} else {
|
} else {
|
||||||
this.setState({
|
this.setState({
|
||||||
successMessages: null,
|
successMessages: null,
|
||||||
|
|
@ -305,6 +318,53 @@ export default class ChatChannelSettings extends Component {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
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() {
|
render() {
|
||||||
const {
|
const {
|
||||||
chatChannel,
|
chatChannel,
|
||||||
|
|
@ -316,6 +376,8 @@ export default class ChatChannelSettings extends Component {
|
||||||
channelDiscoverable,
|
channelDiscoverable,
|
||||||
invitationUsernames,
|
invitationUsernames,
|
||||||
showGlobalBadgeNotification,
|
showGlobalBadgeNotification,
|
||||||
|
displaySettings,
|
||||||
|
invitationLink,
|
||||||
} = this.state;
|
} = this.state;
|
||||||
|
|
||||||
if (!chatChannel) {
|
if (!chatChannel) {
|
||||||
|
|
@ -325,53 +387,44 @@ export default class ChatChannelSettings extends Component {
|
||||||
return (
|
return (
|
||||||
<div className="activechatchannel__activeArticle channel_settings">
|
<div className="activechatchannel__activeArticle channel_settings">
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<ChannelDescriptionSection
|
{displaySettings ? (
|
||||||
channelName={chatChannel.name}
|
<ChatChannelSettingsSection
|
||||||
channelDescription={chatChannel.description}
|
|
||||||
currentMembershipRole={currentMembership.role}
|
|
||||||
/>
|
|
||||||
<ChatChannelMembershipSection
|
|
||||||
currentMembershipRole={currentMembership.role}
|
|
||||||
activeMemberships={activeMemberships}
|
|
||||||
removeMembership={this.removeMembership}
|
|
||||||
pendingMemberships={pendingMemberships}
|
|
||||||
requestedMemberships={requestedMemberships}
|
|
||||||
chatChannelAcceptMembership={this.chatChannelAcceptMembership}
|
|
||||||
/>
|
|
||||||
{currentMembership.role === 'mod' && (
|
|
||||||
<ModSection
|
|
||||||
invitationUsernames={invitationUsernames}
|
|
||||||
handleInvitationUsernames={this.handleInvitationUsernames}
|
|
||||||
handleChatChannelInvitations={this.handleChatChannelInvitations}
|
|
||||||
channelDescription={channelDescription}
|
|
||||||
handleDescriptionChange={this.handleDescriptionChange}
|
|
||||||
channelDiscoverable={channelDiscoverable}
|
channelDiscoverable={channelDiscoverable}
|
||||||
handleChannelDiscoverableStatus={
|
updateCurrentMembershipNotificationSettings={
|
||||||
this.handleChannelDiscoverableStatus
|
this.updateCurrentMembershipNotificationSettings
|
||||||
}
|
}
|
||||||
|
handleleaveChannelMembership={this.handleleaveChannelMembership}
|
||||||
|
handlePersonChannelSetting={this.handlePersonChannelSetting}
|
||||||
handleChannelDescriptionChanges={
|
handleChannelDescriptionChanges={
|
||||||
this.handleChannelDescriptionChanges
|
this.handleChannelDescriptionChanges
|
||||||
}
|
}
|
||||||
currentMembershipRole={currentMembership.role}
|
handleChannelDiscoverableStatus={
|
||||||
/>
|
this.handleChannelDiscoverableStatus
|
||||||
)}
|
|
||||||
<PersonalSettings
|
|
||||||
updateCurrentMembershipNotificationSettings={
|
|
||||||
this.updateCurrentMembershipNotificationSettings
|
|
||||||
}
|
|
||||||
showGlobalBadgeNotification={showGlobalBadgeNotification}
|
|
||||||
handlePersonChatChennelSetting={this.handlePersonChatChennelSetting}
|
|
||||||
/>
|
|
||||||
{currentMembership.role === 'member' && (
|
|
||||||
<LeaveMembershipSection
|
|
||||||
currentMembershipRole={currentMembership.role}
|
|
||||||
handleleaveChatChannelMembership={
|
|
||||||
this.handleleaveChatChannelMembership
|
|
||||||
}
|
}
|
||||||
|
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}
|
||||||
/>
|
/>
|
||||||
)}
|
|
||||||
{currentMembership.role === 'mod' && (
|
|
||||||
<ModFaqSection email="yo@dev.to" />
|
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,106 @@
|
||||||
|
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';
|
||||||
|
|
||||||
|
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}
|
||||||
|
email="yo@dev.to"
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatChannelSettingsSection;
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
const InviteForm = ({
|
const InviteForm = ({
|
||||||
handleChatChannelInvitations,
|
handleChannelInvitations,
|
||||||
invitationUsernames,
|
invitationUsernames,
|
||||||
handleInvitationUsernames,
|
handleInvitationUsernames,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -26,13 +27,9 @@ const InviteForm = ({
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button type="submit" onClick={handleChannelInvitations}>
|
||||||
className="crayons-btn"
|
|
||||||
type="submit"
|
|
||||||
onClick={handleChatChannelInvitations}
|
|
||||||
>
|
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -40,7 +37,7 @@ const InviteForm = ({
|
||||||
|
|
||||||
InviteForm.propTypes = {
|
InviteForm.propTypes = {
|
||||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||||
handleChatChannelInvitations: PropTypes.func.isRequired,
|
handleChannelInvitations: PropTypes.func.isRequired,
|
||||||
invitationUsernames: PropTypes.func.isRequired,
|
invitationUsernames: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,34 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropsType from 'prop-types';
|
import PropsType from 'prop-types';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
|
const LeaveMembershipSection = ({
|
||||||
|
handleleaveChannelMembership,
|
||||||
|
currentMembershipRole,
|
||||||
|
}) => {
|
||||||
|
if (currentMembershipRole === 'mod') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const LeaveMembershipSection = ({ handleleaveChatChannelMembership }) => {
|
|
||||||
return (
|
return (
|
||||||
<div className="crayons-card p-4 grid gap-2 mb-4 leave_membership_section">
|
<div className="crayons-card p-4 grid gap-2 mb-4 leave_membership_section">
|
||||||
<h3>Danger Zone</h3>
|
<h3>Danger Zone</h3>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button
|
||||||
className="crayons-btn crayons-btn--danger leave_button"
|
className="leave_button"
|
||||||
|
variant="danger"
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={handleleaveChatChannelMembership}
|
onClick={handleleaveChannelMembership}
|
||||||
>
|
>
|
||||||
Leave Channel
|
Leave Channel
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
LeaveMembershipSection.propTypes = {
|
LeaveMembershipSection.propTypes = {
|
||||||
handleleaveChatChannelMembership: PropsType.func.isRequired,
|
handleleaveChannelMembership: PropsType.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default LeaveMembershipSection;
|
export default LeaveMembershipSection;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
const Membership = ({
|
const Membership = ({
|
||||||
membership,
|
membership,
|
||||||
|
|
@ -10,7 +12,13 @@ const Membership = ({
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<a href={`/${membership.username}`} title={`${membership.name} profile`}>
|
<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">
|
<span className="crayons-avatar crayons-avatar--l mr-3">
|
||||||
<img
|
<img
|
||||||
className="crayons-avatar__image align-middle"
|
className="crayons-avatar__image align-middle"
|
||||||
|
|
@ -21,17 +29,17 @@ const Membership = ({
|
||||||
<span className="mr-2 user_name">{membership.name}</span>
|
<span className="mr-2 user_name">{membership.name}</span>
|
||||||
</a>
|
</a>
|
||||||
{membershipType === 'requested' ? (
|
{membershipType === 'requested' ? (
|
||||||
<button
|
<Button
|
||||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost add-membership"
|
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost add-membership"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={chatChannelAcceptMembership}
|
onClick={chatChannelAcceptMembership}
|
||||||
data-membership-id={membership.membership_id}
|
data-membership-id={membership.membership_id}
|
||||||
>
|
>
|
||||||
+
|
+
|
||||||
</button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
|
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
|
||||||
<button
|
<Button
|
||||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost remove-membership"
|
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost remove-membership"
|
||||||
type="button"
|
type="button"
|
||||||
onClick={removeMembership}
|
onClick={removeMembership}
|
||||||
|
|
@ -39,24 +47,14 @@ const Membership = ({
|
||||||
data-membership-status={membership.status}
|
data-membership-status={membership.status}
|
||||||
>
|
>
|
||||||
x
|
x
|
||||||
</button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
Membership.propTypes = {
|
Membership.propTypes = {
|
||||||
membership: PropTypes.objectOf(
|
membership: PropTypes.objectOf(defaulMembershipPropType).isRequired,
|
||||||
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,
|
|
||||||
removeMembership: PropTypes.func.isRequired,
|
removeMembership: PropTypes.func.isRequired,
|
||||||
membershipType: PropTypes.func.isRequired,
|
membershipType: PropTypes.func.isRequired,
|
||||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,121 @@
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import { isNativeAndroid } from '../../../utilities/validateAndroidNative';
|
||||||
|
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
|
function isClipboardSupported() {
|
||||||
|
return (
|
||||||
|
typeof navigator.clipboard !== 'undefined' && navigator.clipboard !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 default 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',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNativeAndroid()) {
|
||||||
|
AndroidBridge.copyToClipboard(this.imageMarkdownInput.value);
|
||||||
|
this.setState({ showImageCopiedMessage: true });
|
||||||
|
} else if (isClipboardSupported()) {
|
||||||
|
navigator.clipboard
|
||||||
|
.writeText(this.imageMarkdownInput.value)
|
||||||
|
.then(() => {
|
||||||
|
this.setState({ showImageCopiedMessage: true });
|
||||||
|
})
|
||||||
|
.catch((_err) => {
|
||||||
|
this.execCopyText();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.execCopyText();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
execCopyText() {
|
||||||
|
this.imageMarkdownInput.setSelectionRange(
|
||||||
|
0,
|
||||||
|
this.imageMarkdownInput.value.length,
|
||||||
|
);
|
||||||
|
document.execCommand('copy');
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,119 @@
|
||||||
|
import { h, Component } from 'preact';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import MembershipSection from './MembershipSection';
|
||||||
|
import InvitationLinkManager from './InvitationLinkManager';
|
||||||
|
|
||||||
|
export default 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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
} = this.state;
|
||||||
|
|
||||||
|
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}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<InvitationLinkManager
|
||||||
|
invitationLink={invitationLink}
|
||||||
|
currentMembership={currentMembership}
|
||||||
|
className="inviation-link-section"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,130 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import adminEmoji from '../../../../assets/images/emoji/apple-fire.png';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Membership;
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
import Membership from './Membership';
|
||||||
|
|
||||||
|
const MembershipSection = ({
|
||||||
|
memberships,
|
||||||
|
currentMembership,
|
||||||
|
removeMembership,
|
||||||
|
handleUpdateMembershipRole,
|
||||||
|
}) => {
|
||||||
|
if (!memberships || memberships.length === 0) {
|
||||||
|
return <p className="lh-base">No membership</p>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const membershipCount = memberships.length;
|
||||||
|
|
||||||
|
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,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MembershipSection;
|
||||||
|
|
@ -1,7 +1,11 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
const ModFaqSection = ({ email }) => {
|
const ModFaqSection = ({ email, currentMembershipRole }) => {
|
||||||
|
if (currentMembershipRole === 'member') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="crayons-card grid gap-2 p-4 faq-section">
|
<div className="crayons-card grid gap-2 p-4 faq-section">
|
||||||
<p className="contact-details">
|
<p className="contact-details">
|
||||||
|
|
@ -21,6 +25,7 @@ const ModFaqSection = ({ email }) => {
|
||||||
|
|
||||||
ModFaqSection.propTypes = {
|
ModFaqSection.propTypes = {
|
||||||
email: PropTypes.string.isRequired,
|
email: PropTypes.string.isRequired,
|
||||||
|
currentMembershipRole: PropTypes.string.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default ModFaqSection;
|
export default ModFaqSection;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ import InviteForm from './InviteForm';
|
||||||
import SettingsForm from './SettingsForm';
|
import SettingsForm from './SettingsForm';
|
||||||
|
|
||||||
const ModSection = ({
|
const ModSection = ({
|
||||||
handleChatChannelInvitations,
|
handleChannelInvitations,
|
||||||
invitationUsernames,
|
invitationUsernames,
|
||||||
handleInvitationUsernames,
|
handleInvitationUsernames,
|
||||||
channelDescription,
|
channelDescription,
|
||||||
|
|
@ -13,13 +13,18 @@ const ModSection = ({
|
||||||
channelDiscoverable,
|
channelDiscoverable,
|
||||||
handleChannelDiscoverableStatus,
|
handleChannelDiscoverableStatus,
|
||||||
handleChannelDescriptionChanges,
|
handleChannelDescriptionChanges,
|
||||||
|
currentMembershipRole,
|
||||||
}) => {
|
}) => {
|
||||||
|
if (currentMembershipRole === 'member') {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mod-section">
|
<div className="mod-section">
|
||||||
<InviteForm
|
<InviteForm
|
||||||
handleInvitationUsernames={handleInvitationUsernames}
|
handleInvitationUsernames={handleInvitationUsernames}
|
||||||
invitationUsernames={invitationUsernames}
|
invitationUsernames={invitationUsernames}
|
||||||
handleChatChannelInvitations={handleChatChannelInvitations}
|
handleChannelInvitations={handleChannelInvitations}
|
||||||
/>
|
/>
|
||||||
<SettingsForm
|
<SettingsForm
|
||||||
channelDescription={channelDescription}
|
channelDescription={channelDescription}
|
||||||
|
|
@ -34,7 +39,7 @@ const ModSection = ({
|
||||||
|
|
||||||
ModSection.propTypes = {
|
ModSection.propTypes = {
|
||||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||||
handleChatChannelInvitations: PropTypes.func.isRequired,
|
handleChannelInvitations: PropTypes.func.isRequired,
|
||||||
invitationUsernames: PropTypes.func.isRequired,
|
invitationUsernames: PropTypes.func.isRequired,
|
||||||
channelDescription: PropTypes.string.isRequired,
|
channelDescription: PropTypes.string.isRequired,
|
||||||
handleDescriptionChange: PropTypes.func.isRequired,
|
handleDescriptionChange: PropTypes.func.isRequired,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||||
import Membership from './Membership';
|
import Membership from './Membership';
|
||||||
|
|
||||||
const PendingMembershipSection = ({
|
const PendingMembershipSection = ({
|
||||||
|
|
@ -34,17 +36,7 @@ const PendingMembershipSection = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
PendingMembershipSection.propTypes = {
|
PendingMembershipSection.propTypes = {
|
||||||
pendingMemberships: PropTypes.arrayOf(
|
pendingMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
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,
|
|
||||||
removeMembership: PropTypes.func.isRequired,
|
removeMembership: PropTypes.func.isRequired,
|
||||||
currentMembershipRole: PropTypes.func.isRequired,
|
currentMembershipRole: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
const PersonalSettings = ({
|
const PersonalSettings = ({
|
||||||
handlePersonChatChennelSetting,
|
handlePersonChannelSetting,
|
||||||
showGlobalBadgeNotification,
|
showGlobalBadgeNotification,
|
||||||
updateCurrentMembershipNotificationSettings,
|
updateCurrentMembershipNotificationSettings,
|
||||||
}) => {
|
}) => {
|
||||||
|
|
@ -16,20 +17,19 @@ const PersonalSettings = ({
|
||||||
id="c3"
|
id="c3"
|
||||||
className="crayons-checkbox"
|
className="crayons-checkbox"
|
||||||
checked={showGlobalBadgeNotification}
|
checked={showGlobalBadgeNotification}
|
||||||
onChange={handlePersonChatChennelSetting}
|
onChange={handlePersonChannelSetting}
|
||||||
/>
|
/>
|
||||||
<label htmlFor="c3" className="crayons-field__label">
|
<label htmlFor="c3" className="crayons-field__label">
|
||||||
Receive Notifications for New Messages
|
Receive Notifications for New Messages
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button
|
||||||
className="crayons-btn"
|
|
||||||
type="submit"
|
type="submit"
|
||||||
onClick={updateCurrentMembershipNotificationSettings}
|
onClick={updateCurrentMembershipNotificationSettings}
|
||||||
>
|
>
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
@ -38,7 +38,7 @@ const PersonalSettings = ({
|
||||||
PersonalSettings.propTypes = {
|
PersonalSettings.propTypes = {
|
||||||
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
|
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
|
||||||
showGlobalBadgeNotification: PropTypes.bool.isRequired,
|
showGlobalBadgeNotification: PropTypes.bool.isRequired,
|
||||||
handlePersonChatChennelSetting: PropTypes.func.isRequired,
|
handlePersonChannelSetting: PropTypes.func.isRequired,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default PersonalSettings;
|
export default PersonalSettings;
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||||
|
|
||||||
import Membership from './Membership';
|
import Membership from './Membership';
|
||||||
|
|
||||||
const RequestedMembershipSection = ({
|
const RequestedMembershipSection = ({
|
||||||
|
|
@ -8,7 +10,7 @@ const RequestedMembershipSection = ({
|
||||||
chatChannelAcceptMembership,
|
chatChannelAcceptMembership,
|
||||||
currentMembershipRole,
|
currentMembershipRole,
|
||||||
}) => {
|
}) => {
|
||||||
if (currentMembershipRole !== 'mod') {
|
if (currentMembershipRole === 'member') {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -37,17 +39,7 @@ const RequestedMembershipSection = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
RequestedMembershipSection.propTypes = {
|
RequestedMembershipSection.propTypes = {
|
||||||
requestedMemberships: PropTypes.arrayOf(
|
requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||||
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,
|
|
||||||
removeMembership: PropTypes.func.isRequired,
|
removeMembership: PropTypes.func.isRequired,
|
||||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||||
currentMembershipRole: PropTypes.func.isRequired,
|
currentMembershipRole: PropTypes.func.isRequired,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,6 @@
|
||||||
import { h } from 'preact';
|
import { h } from 'preact';
|
||||||
import PropTypes from 'prop-types';
|
import PropTypes from 'prop-types';
|
||||||
|
import { Button } from '@crayons';
|
||||||
|
|
||||||
const SettingsFrom = ({
|
const SettingsFrom = ({
|
||||||
channelDescription,
|
channelDescription,
|
||||||
|
|
@ -42,13 +43,9 @@ const SettingsFrom = ({
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<button
|
<Button type="submit" onClick={handleChannelDescriptionChanges}>
|
||||||
className="crayons-btn"
|
|
||||||
type="submit"
|
|
||||||
onClick={handleChannelDescriptionChanges}
|
|
||||||
>
|
|
||||||
Submit
|
Submit
|
||||||
</button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
});
|
||||||
46
app/javascript/chat/__tests__/Membership.test.jsx
Normal file
46
app/javascript/chat/__tests__/Membership.test.jsx
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
import { h } from 'preact';
|
||||||
|
import { render } from '@testing-library/preact';
|
||||||
|
import { axe } from 'jest-axe';
|
||||||
|
import Membership from '../ChatChannelSettings/MembershipManager/Membership';
|
||||||
|
|
||||||
|
const membershipData = {
|
||||||
|
name: 'dummy Name',
|
||||||
|
user_id: 1,
|
||||||
|
chat_channel_id: 2,
|
||||||
|
membership_id: 1,
|
||||||
|
username: 'dummyuser',
|
||||||
|
};
|
||||||
|
|
||||||
|
const currentModMembership = {
|
||||||
|
name: 'dummy user',
|
||||||
|
username: 'dummyuser',
|
||||||
|
user_id: 1,
|
||||||
|
chat_channel_id: 2,
|
||||||
|
status: 'active',
|
||||||
|
role: 'mod',
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('<Membership />', () => {
|
||||||
|
it('should have no a11y violations', async () => {
|
||||||
|
const { container } = render(
|
||||||
|
<Membership
|
||||||
|
currentMembership={currentModMembership}
|
||||||
|
membership={membershipData}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
const results = await axe(container);
|
||||||
|
|
||||||
|
expect(results).toHaveNoViolations();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should render', () => {
|
||||||
|
const { queryByText } = render(
|
||||||
|
<Membership
|
||||||
|
currentMembership={currentModMembership}
|
||||||
|
membership={membershipData}
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(queryByText('dummy user')).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -7,6 +7,7 @@ import {
|
||||||
updateChatChannelDescription,
|
updateChatChannelDescription,
|
||||||
sendChatChannelInvitation,
|
sendChatChannelInvitation,
|
||||||
leaveChatChannelMembership,
|
leaveChatChannelMembership,
|
||||||
|
updateMembershipRole,
|
||||||
} from '../actions/chat_channel_setting_actions';
|
} from '../actions/chat_channel_setting_actions';
|
||||||
|
|
||||||
/* global globalThis */
|
/* global globalThis */
|
||||||
|
|
@ -200,4 +201,26 @@ describe('Chat cahnnel API requestes', () => {
|
||||||
expect(result).toEqual(response);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
74
app/javascript/chat/__tests__/invitationLinkManager.test.jsx
Normal file
74
app/javascript/chat/__tests__/invitationLinkManager.test.jsx
Normal file
|
|
@ -0,0 +1,74 @@
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -21,9 +21,7 @@ describe('<LeaveMembershipSection />', () => {
|
||||||
it('should have user leave channel when leave button is clicked', () => {
|
it('should have user leave channel when leave button is clicked', () => {
|
||||||
const leaveHandler = jest.fn();
|
const leaveHandler = jest.fn();
|
||||||
const { getByText } = render(
|
const { getByText } = render(
|
||||||
<LeaveMembershipSection
|
<LeaveMembershipSection handleleaveChannelMembership={leaveHandler} />,
|
||||||
handleleaveChatChannelMembership={leaveHandler}
|
|
||||||
/>,
|
|
||||||
);
|
);
|
||||||
const leaveButton = getByText('Leave Channel');
|
const leaveButton = getByText('Leave Channel');
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,167 +0,0 @@
|
||||||
import { h } from 'preact';
|
|
||||||
import { render } from '@testing-library/preact';
|
|
||||||
import { axe } from 'jest-axe';
|
|
||||||
import Membership from '../ChatChannelSettings/Membership';
|
|
||||||
|
|
||||||
function getModUser() {
|
|
||||||
return {
|
|
||||||
membership: {
|
|
||||||
name: 'test user',
|
|
||||||
username: 'testusername',
|
|
||||||
user_id: '1',
|
|
||||||
membership_id: '2',
|
|
||||||
role: 'mod',
|
|
||||||
status: 'active',
|
|
||||||
image: '',
|
|
||||||
},
|
|
||||||
membershipType: 'active',
|
|
||||||
currentMembershipRole: 'mod',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMemberUser() {
|
|
||||||
return {
|
|
||||||
membership: {
|
|
||||||
name: 'test user',
|
|
||||||
username: 'testusername',
|
|
||||||
user_id: '1',
|
|
||||||
membership_id: '2',
|
|
||||||
role: 'member',
|
|
||||||
status: 'active',
|
|
||||||
image: '',
|
|
||||||
},
|
|
||||||
membershipType: 'requested',
|
|
||||||
currentMembershipRole: 'mod',
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('<Membership />', () => {
|
|
||||||
it('should have no a11y violations for a moderator user', async () => {
|
|
||||||
const { membership, membershipType, currentMembershipRole } = getModUser();
|
|
||||||
const { container } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const results = await axe(container);
|
|
||||||
|
|
||||||
expect(results).toHaveNoViolations();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should have no a11y violations for a member user', async () => {
|
|
||||||
const {
|
|
||||||
membership,
|
|
||||||
membershipType,
|
|
||||||
currentMembershipRole,
|
|
||||||
} = getMemberUser();
|
|
||||||
const { container } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const results = await axe(container);
|
|
||||||
|
|
||||||
expect(results).toHaveNoViolations();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a moderator user', () => {
|
|
||||||
const { membership, membershipType, currentMembershipRole } = getModUser();
|
|
||||||
const { getByAltText, getByTitle } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
const userProfileLink = getByTitle('test user profile');
|
|
||||||
|
|
||||||
expect(userProfileLink.getAttribute('href')).toEqual('/testusername');
|
|
||||||
|
|
||||||
getByAltText('test user profile');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should render a member user', () => {
|
|
||||||
const {
|
|
||||||
membership,
|
|
||||||
membershipType,
|
|
||||||
currentMembershipRole,
|
|
||||||
} = getMemberUser();
|
|
||||||
const { getByAltText, getByTitle, getByText } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// users requesting to join channel
|
|
||||||
const userProfileLink = getByTitle('test user profile');
|
|
||||||
|
|
||||||
expect(userProfileLink.getAttribute('href')).toEqual('/testusername');
|
|
||||||
|
|
||||||
getByAltText('test user profile');
|
|
||||||
|
|
||||||
const addButton = getByText('+', {
|
|
||||||
selector: 'button',
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(addButton.dataset.membershipId).toEqual('2');
|
|
||||||
|
|
||||||
const deleteButton = getByText('x', { selector: 'button' });
|
|
||||||
|
|
||||||
expect(deleteButton.dataset.membershipId).toEqual('2');
|
|
||||||
expect(deleteButton.dataset.membershipStatus).toEqual('active');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not show add/remove buttons for a moderator', () => {
|
|
||||||
const { membership, membershipType, currentMembershipRole } = getModUser();
|
|
||||||
const { queryByText } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
|
|
||||||
// a moderator should not have add or remove buttons
|
|
||||||
expect(
|
|
||||||
queryByText('+', {
|
|
||||||
selector: 'button',
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
expect(
|
|
||||||
queryByText('x', {
|
|
||||||
selector: 'button',
|
|
||||||
}),
|
|
||||||
).toBeNull();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show add/remove buttons for a member', () => {
|
|
||||||
const {
|
|
||||||
membership,
|
|
||||||
membershipType,
|
|
||||||
currentMembershipRole,
|
|
||||||
} = getMemberUser();
|
|
||||||
const { queryByText } = render(
|
|
||||||
<Membership
|
|
||||||
membership={membership}
|
|
||||||
membershipType={membershipType}
|
|
||||||
currentMembershipRole={currentMembershipRole}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
expect(
|
|
||||||
queryByText('+', {
|
|
||||||
selector: 'button',
|
|
||||||
}),
|
|
||||||
).toBeDefined();
|
|
||||||
expect(
|
|
||||||
queryByText('x', {
|
|
||||||
selector: 'button',
|
|
||||||
}),
|
|
||||||
).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -142,7 +142,31 @@ export async function leaveChatChannelMembership(membershipId) {
|
||||||
`/chat_channel_memberships/leave_membership/${membershipId}`,
|
`/chat_channel_memberships/leave_membership/${membershipId}`,
|
||||||
{
|
{
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
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,
|
||||||
|
},
|
||||||
|
},
|
||||||
credentials: 'same-origin',
|
credentials: 'same-origin',
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1796,6 +1796,11 @@ export default class Chat extends Component {
|
||||||
? 'sidecar-user'
|
? 'sidecar-user'
|
||||||
: `chat_channel_setting`;
|
: `chat_channel_setting`;
|
||||||
|
|
||||||
|
const contentLink =
|
||||||
|
activeChannel.channel_type === 'direct'
|
||||||
|
? `/${activeChannel.channel_username}`
|
||||||
|
: '#/';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<a
|
<a
|
||||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost"
|
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost"
|
||||||
|
|
@ -1804,7 +1809,7 @@ export default class Chat extends Component {
|
||||||
if (e.keyCode === 13) this.triggerActiveContent(e);
|
if (e.keyCode === 13) this.triggerActiveContent(e);
|
||||||
}}
|
}}
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
href={path}
|
href={contentLink}
|
||||||
data-content={dataContent}
|
data-content={dataContent}
|
||||||
>
|
>
|
||||||
<svg
|
<svg
|
||||||
|
|
@ -1813,8 +1818,12 @@ export default class Chat extends Component {
|
||||||
width="24"
|
width="24"
|
||||||
height="24"
|
height="24"
|
||||||
className="crayons-icon"
|
className="crayons-icon"
|
||||||
|
data-content={dataContent}
|
||||||
>
|
>
|
||||||
<path d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z" />
|
<path
|
||||||
|
data-content={dataContent}
|
||||||
|
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zM11 7h2v2h-2V7zm0 4h2v6h-2v-6z"
|
||||||
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</a>
|
</a>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,13 @@ export default class Content extends Component {
|
||||||
return (
|
return (
|
||||||
// TODO: A button (role="button") cannot contain other interactive elements, i.e. buttons.
|
// TODO: A button (role="button") cannot contain other interactive elements, i.e. buttons.
|
||||||
// TODO: These should have key click events as well.
|
// TODO: These should have key click events as well.
|
||||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
|
||||||
<div
|
<div
|
||||||
className="activechatchannel__activecontent activechatchannel__activecontent--sidecar"
|
className="activechatchannel__activecontent activechatchannel__activecontent--sidecar"
|
||||||
id="chat_activecontent"
|
id="chat_activecontent"
|
||||||
onClick={onTriggerContent}
|
onClick={onTriggerContent}
|
||||||
role="button"
|
role="button"
|
||||||
tabIndex="0"
|
tabIndex="0"
|
||||||
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|
|
||||||
11
app/javascript/common-prop-types/membership-prop-type.js
Normal file
11
app/javascript/common-prop-types/membership-prop-type.js
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const defaulMembershipPropType = 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,
|
||||||
|
});
|
||||||
10
app/javascript/utilities/validateAndroidNative.js
Normal file
10
app/javascript/utilities/validateAndroidNative.js
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
/**
|
||||||
|
* This function is used to check that is Device is android native or not
|
||||||
|
*/
|
||||||
|
export function isNativeAndroid() {
|
||||||
|
return (
|
||||||
|
navigator.userAgent === 'DEV-Native-android' &&
|
||||||
|
typeof AndroidBridge !== 'undefined' &&
|
||||||
|
AndroidBridge !== null
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -39,6 +39,18 @@ class ChatChannelPolicy < ApplicationPolicy
|
||||||
user_can_edit_channel
|
user_can_edit_channel
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def join_channel_invitation?
|
||||||
|
record.present? && user.id
|
||||||
|
end
|
||||||
|
|
||||||
|
def set_channel?
|
||||||
|
user_can_edit_channel
|
||||||
|
end
|
||||||
|
|
||||||
|
def joining_invitation_response?
|
||||||
|
record.present?
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def user_can_edit_channel
|
def user_can_edit_channel
|
||||||
|
|
|
||||||
|
|
@ -1,42 +0,0 @@
|
||||||
class ChatChannelDetailPresenter
|
|
||||||
def initialize(chat_channel, current_membership)
|
|
||||||
@chat_channel = chat_channel
|
|
||||||
@current_membership = current_membership
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_accessor :chat_channel, :current_membership
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{
|
|
||||||
chat_channel: {
|
|
||||||
name: chat_channel.channel_name,
|
|
||||||
type: chat_channel.channel_type,
|
|
||||||
description: chat_channel.description,
|
|
||||||
discoverable: chat_channel.discoverable,
|
|
||||||
slug: chat_channel.slug,
|
|
||||||
status: chat_channel.status,
|
|
||||||
id: chat_channel.id
|
|
||||||
},
|
|
||||||
memberships: {
|
|
||||||
active: membership_users(chat_channel.active_memberships),
|
|
||||||
pending: membership_users(chat_channel.pending_memberships),
|
|
||||||
requested: membership_users(chat_channel.requested_memberships)
|
|
||||||
},
|
|
||||||
current_membership: current_membership
|
|
||||||
}
|
|
||||||
end
|
|
||||||
|
|
||||||
def membership_users(memberships)
|
|
||||||
memberships.includes(:user).map do |membership|
|
|
||||||
{
|
|
||||||
name: membership.user.name,
|
|
||||||
username: membership.user.username,
|
|
||||||
user_id: membership.user.id,
|
|
||||||
membership_id: membership.id,
|
|
||||||
role: membership.role,
|
|
||||||
status: membership.status,
|
|
||||||
image: ProfileImage.new(membership.user).get(width: 90)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
class MembershipUserPresenter
|
|
||||||
def initialize(membership)
|
|
||||||
@membership = membership
|
|
||||||
end
|
|
||||||
|
|
||||||
attr_accessor :membership
|
|
||||||
|
|
||||||
def as_json
|
|
||||||
{
|
|
||||||
name: membership.user.name,
|
|
||||||
username: membership.user.username,
|
|
||||||
user_id: membership.user.id,
|
|
||||||
membership_id: membership.id,
|
|
||||||
role: membership.role,
|
|
||||||
status: membership.status,
|
|
||||||
image: ProfileImage.new(membership.user).get(width: 90)
|
|
||||||
}
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
json.success true
|
||||||
|
json.result do
|
||||||
|
json.chat_channel do
|
||||||
|
json.name @channel.channel_name
|
||||||
|
json.type @channel.channel_type
|
||||||
|
json.description @channel.description
|
||||||
|
json.discoverable @channel.discoverable
|
||||||
|
json.slug @channel.slug
|
||||||
|
json.status @channel.status
|
||||||
|
json.id @channel.id
|
||||||
|
end
|
||||||
|
|
||||||
|
json.memberships do
|
||||||
|
json.active membership_users(@channel.active_memberships)
|
||||||
|
json.pending membership_users(@channel.pending_memberships)
|
||||||
|
json.requested membership_users(@channel.requested_memberships)
|
||||||
|
end
|
||||||
|
json.current_membership @membership
|
||||||
|
json.invitation_link URL.url(@invitation_link)
|
||||||
|
end
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
<div class="grid gap-6 p-6 justify-center">
|
||||||
|
<% if @link_expired %>
|
||||||
|
<h1>This Invitation link is expired. Please contact the channel mod for new Invitation link</h1>
|
||||||
|
<% else %>
|
||||||
|
<h1 class="mt-5">Would you like to join the <%= @chat_channel.channel_name %> channel</h1>
|
||||||
|
<%= form_with(url: joining_invitation_response_url, html: { class: "align-center" }) do |f| %>
|
||||||
|
<%= f.hidden_field :user_action, value: "accept" %>
|
||||||
|
<%= f.hidden_field :chat_channel_id, value: @chat_channel.id %>
|
||||||
|
<%= f.submit "Accept", class: "crayons-btn crayons-btn--s" %>
|
||||||
|
<% end %>
|
||||||
|
<%= form_with(url: joining_invitation_response_url, html: { class: "align-center" }) do |f| %>
|
||||||
|
<%= f.hidden_field :user_action, value: "decline" %>
|
||||||
|
<%= f.hidden_field :chat_channel_id, value: @chat_channel.id %>
|
||||||
|
<%= f.submit "Decline", class: "crayons-btn crayons-btn--danger mb-5 crayons-btn--s" %>
|
||||||
|
<% end %>
|
||||||
|
<% end %>
|
||||||
|
</div>
|
||||||
|
|
@ -2,6 +2,6 @@ import querystring;
|
||||||
sub vcl_recv {
|
sub vcl_recv {
|
||||||
# return this URL with only the parameters that match this regular expression
|
# return this URL with only the parameters that match this regular expression
|
||||||
if (req.url !~ "/internal/" && req.url !~ "/search/" && req.url !~ "/bulk_show") {
|
if (req.url !~ "/internal/" && req.url !~ "/search/" && req.url !~ "/bulk_show") {
|
||||||
set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|invitation_token|reset_password_token|ut|verb)$");
|
set req.url = querystring.regfilter_except(req.url, "^(a_id|args|article_id|article_ids|articles|asc|callback_url|category|chat_channel_id|client_id|code|collection_id|commentable_id|commentable_type|confirmation_token|created_at|end|filter|followable_id|followable_type|fork_id|i|key|message_offset|name|oauth_token|oauth_verifier|offset|org_id|organization_id|p|page|per_page|prefill|preview|purchaser|reactable_ids|redirect_uri|reported_url|reporter_username|response_type|scope|search|signature|sort|start|state|status|tag|tag_list|top|type_of|url|username|invitation_token|reset_password_token|ut|verb|invitation_slug)$");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -302,6 +302,9 @@ Rails.application.routes.draw do
|
||||||
post "/chat_channel_memberships/create_membership_request" => "chat_channel_memberships#create_membership_request"
|
post "/chat_channel_memberships/create_membership_request" => "chat_channel_memberships#create_membership_request"
|
||||||
patch "/chat_channel_memberships/leave_membership/:id" => "chat_channel_memberships#leave_membership"
|
patch "/chat_channel_memberships/leave_membership/:id" => "chat_channel_memberships#leave_membership"
|
||||||
patch "/chat_channel_memberships/update_membership/:id" => "chat_channel_memberships#update_membership"
|
patch "/chat_channel_memberships/update_membership/:id" => "chat_channel_memberships#update_membership"
|
||||||
|
patch "/chat_channel_memberships/update_membership_role/:id" => "chat_channel_memberships#update_membership_role"
|
||||||
|
get "/join_channel_invitation/:channel_slug" => "chat_channel_memberships#join_channel_invitation"
|
||||||
|
post "/joining_invitation_response" => "chat_channel_memberships#joining_invitation_response"
|
||||||
|
|
||||||
get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview
|
get "/social_previews/article/:id" => "social_previews#article", :as => :article_social_preview
|
||||||
get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview
|
get "/social_previews/user/:id" => "social_previews#user", :as => :user_social_preview
|
||||||
|
|
|
||||||
|
|
@ -16,12 +16,13 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
||||||
user.add_role(:super_admin)
|
user.add_role(:super_admin)
|
||||||
|
|
||||||
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
|
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
|
||||||
get "/chat_channel_memberships/chat_channel_info/#{membership.id}"
|
get "/chat_channel_memberships/chat_channel_info/#{membership.id}", as: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
it "return all details of chat channel" do
|
it "return all details of chat channel" do
|
||||||
expect(response).to have_http_status(:ok)
|
expect(response.status).to eq(200)
|
||||||
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership])
|
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership
|
||||||
|
invitation_link])
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -31,14 +32,15 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
||||||
chat_channel.add_users([second_user])
|
chat_channel.add_users([second_user])
|
||||||
|
|
||||||
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id)
|
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id)
|
||||||
get "/chat_channel_memberships/chat_channel_info/#{membership.id}"
|
get "/chat_channel_memberships/chat_channel_info/#{membership.id}", as: :json
|
||||||
end
|
end
|
||||||
|
|
||||||
it "return only channel info and current membership" do
|
it "return only channel info and current membership" do
|
||||||
expect(response.status).to eq(200)
|
expect(response.status).to eq(200)
|
||||||
expect(JSON.parse(response.body)["result"].keys).to eq(%w[chat_channel memberships current_membership])
|
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership
|
||||||
expect(JSON.parse(response.body)["result"]["memberships"]["pending"].length).to eq(0)
|
invitation_link])
|
||||||
expect(JSON.parse(response.body)["result"]["memberships"]["requested"].length).to eq(0)
|
expect(response.parsed_body["result"]["memberships"]["pending"].length).to eq(0)
|
||||||
|
expect(response.parsed_body["result"]["memberships"]["requested"].length).to eq(0)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
@ -338,4 +340,120 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "PATCH /update_membership_role" do
|
||||||
|
before do
|
||||||
|
user.add_role(:super_admin)
|
||||||
|
chat_channel.add_users([second_user])
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user role is member" do
|
||||||
|
it "update the membership role to mod" do
|
||||||
|
allow(Pusher).to receive(:trigger).and_return(true)
|
||||||
|
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id)
|
||||||
|
|
||||||
|
patch "/chat_channel_memberships/update_membership_role/#{chat_channel.id}", params: {
|
||||||
|
chat_channel_membership: {
|
||||||
|
membership_id: membership.id,
|
||||||
|
role: "mod"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(membership.reload.role).to eq("mod")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is mod" do
|
||||||
|
it "update the membership role to member" do
|
||||||
|
allow(Pusher).to receive(:trigger).and_return(true)
|
||||||
|
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: user.id)
|
||||||
|
|
||||||
|
patch "/chat_channel_memberships/update_membership_role/#{chat_channel.id}", params: {
|
||||||
|
chat_channel_membership: {
|
||||||
|
membership_id: membership.id,
|
||||||
|
role: "member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
expect(membership.reload.role).to eq("member")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when there is no channel id" do
|
||||||
|
it "channel not found" do
|
||||||
|
membership = ChatChannelMembership.find_by(chat_channel_id: chat_channel.id, user_id: second_user.id)
|
||||||
|
|
||||||
|
patch "/chat_channel_memberships/update_membership_role/", params: {
|
||||||
|
chat_channel_membership: {
|
||||||
|
membership_id: membership.id,
|
||||||
|
role: "member"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(response.status).to eq(404)
|
||||||
|
expect(membership.reload.role).to eq("member")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "GET /join_channel_invitation" do
|
||||||
|
context "when user is not member" do
|
||||||
|
it "render the page" do
|
||||||
|
allow(Pusher).to receive(:trigger).and_return(true)
|
||||||
|
sign_in second_user
|
||||||
|
chat_channel.update(discoverable: true)
|
||||||
|
|
||||||
|
get "/join_channel_invitation/#{chat_channel.slug}"
|
||||||
|
|
||||||
|
expect(response.status).to eq(200)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user is not logged-in" do
|
||||||
|
it "not allowed to create membership" do
|
||||||
|
sign_out second_user
|
||||||
|
|
||||||
|
get "/join_channel_invitation/#{chat_channel.slug}"
|
||||||
|
expect(response.status).to eq(401)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "POST /joining_invitation_response" do
|
||||||
|
context "when user accept the request" do
|
||||||
|
it "will create membership" do
|
||||||
|
allow(Pusher).to receive(:trigger).and_return(true)
|
||||||
|
|
||||||
|
sign_in second_user
|
||||||
|
chat_channel.update(discoverable: true)
|
||||||
|
|
||||||
|
post "/joining_invitation_response", params: {
|
||||||
|
user_action: "accept",
|
||||||
|
chat_channel_id: chat_channel.id
|
||||||
|
}
|
||||||
|
membership = ChatChannelMembership.last
|
||||||
|
|
||||||
|
expect(response.status).to eq(302)
|
||||||
|
expect(membership.user_id).to eq(second_user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when user decline the request" do
|
||||||
|
it "will not create the membership" do
|
||||||
|
sign_in second_user
|
||||||
|
chat_channel.update(discoverable: true)
|
||||||
|
|
||||||
|
post "/joining_invitation_response", params: {
|
||||||
|
user_action: "decline",
|
||||||
|
chat_channel_id: chat_channel.id
|
||||||
|
}
|
||||||
|
|
||||||
|
membership = ChatChannelMembership.last
|
||||||
|
|
||||||
|
expect(response.status).to eq(302)
|
||||||
|
expect(membership.user_id).not_to eq(second_user.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue