[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
|
||||
|
||||
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);
|
||||
padding: 13px;
|
||||
min-width: 50%;
|
||||
overflow-x: hidden;
|
||||
@media screen and (min-width: 1440px) {
|
||||
min-width: 45%;
|
||||
}
|
||||
|
|
@ -1164,3 +1165,66 @@
|
|||
.channel_details {
|
||||
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])
|
||||
authorize @membership
|
||||
@channel = @membership.chat_channel
|
||||
data = ChatChannelDetailPresenter.new(@channel, @membership).as_json
|
||||
|
||||
render json: { success: true, result: data, message: "" }, success: :ok
|
||||
invite_cache_key = "chat-channel-invite-#{@channel.id}"
|
||||
invitation_slug = Rails.cache.fetch(invite_cache_key, expires_in: 12.hours) do
|
||||
"invitation-link-#{SecureRandom.hex(3)}"
|
||||
end
|
||||
@invitation_link = "/join_channel_invitation/#{@channel.slug}?invitation_slug=#{invitation_slug}"
|
||||
end
|
||||
|
||||
def create_membership_request
|
||||
chat_channel = ChatChannel.find_by(id: channel_membership_request_params[:chat_channel_id])
|
||||
chat_channel = ChatChannel.find_by(id: channel_membership_params[:chat_channel_id])
|
||||
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("@")
|
||||
end
|
||||
users = User.where(username: usernames)
|
||||
|
|
@ -131,14 +133,82 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
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
|
||||
|
||||
def permitted_params
|
||||
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
|
||||
end
|
||||
|
||||
def channel_membership_request_params
|
||||
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames)
|
||||
def channel_membership_params
|
||||
params.require(:chat_channel_membership).permit(:chat_channel_id, :invitation_usernames, :membership_id, :role)
|
||||
end
|
||||
|
||||
def respond_to_invitation(previous_status)
|
||||
|
|
@ -176,9 +246,9 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
notice = "Invitation rejected."
|
||||
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|
|
||||
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 PropTypes from 'prop-types';
|
||||
|
||||
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import Membership from './Membership';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const ActiveMembershipSection = ({
|
||||
activeMemberships,
|
||||
removeMembership,
|
||||
currentMembershipRole,
|
||||
toggleScreens,
|
||||
}) => {
|
||||
const activeMembershipList = activeMemberships.slice(0, 4);
|
||||
|
||||
return (
|
||||
<div
|
||||
data-testid="active-memberships"
|
||||
|
|
@ -14,35 +20,34 @@ const ActiveMembershipSection = ({
|
|||
data-active-count={activeMemberships ? activeMemberships.length : 0}
|
||||
>
|
||||
<h3 className="mb-2 active_members">Members</h3>
|
||||
{activeMemberships && activeMemberships.length > 0
|
||||
? activeMemberships.map((pendingMembership) => (
|
||||
{activeMembershipList.map((activeMembership) => (
|
||||
<Membership
|
||||
membership={pendingMembership}
|
||||
membership={activeMembership}
|
||||
removeMembership={removeMembership}
|
||||
membershipType="active"
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
className="active-member"
|
||||
/>
|
||||
))
|
||||
: null}
|
||||
))}
|
||||
<div className="row align-center">
|
||||
<Button
|
||||
className="align-center view-all-memberships"
|
||||
size="s"
|
||||
onClick={toggleScreens}
|
||||
type="button"
|
||||
>
|
||||
View All
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ActiveMembershipSection.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,
|
||||
activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
toggleScreens: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ActiveMembershipSection;
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import ActiveMembershipSection from './ActiveMembershipsSection';
|
||||
import PendingMembershipSection from './PendingMembershipSection'
|
||||
import PendingMembershipSection from './PendingMembershipSection';
|
||||
import RequestedMembershipSection from './RequestedMembershipSection';
|
||||
|
||||
const ChatChannelMembershipSection = ({
|
||||
|
|
@ -11,7 +12,8 @@ const ChatChannelMembershipSection = ({
|
|||
chatChannelAcceptMembership,
|
||||
activeMemberships,
|
||||
removeMembership,
|
||||
currentMembershipRole
|
||||
currentMembershipRole,
|
||||
toggleScreens,
|
||||
}) => {
|
||||
return (
|
||||
<div className="membership-list">
|
||||
|
|
@ -19,6 +21,7 @@ const ChatChannelMembershipSection = ({
|
|||
activeMemberships={activeMemberships}
|
||||
removeMembership={removeMembership}
|
||||
currentMembershipRole={currentMembershipRole}
|
||||
toggleScreens={toggleScreens}
|
||||
/>
|
||||
<PendingMembershipSection
|
||||
pendingMemberships={pendingMemberships}
|
||||
|
|
@ -32,38 +35,17 @@ const ChatChannelMembershipSection = ({
|
|||
currentMembershipRole={currentMembershipRole}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
ChatChannelMembershipSection.propTypes = {
|
||||
pendingMemberships: 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,
|
||||
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,
|
||||
pendingMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
activeMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired
|
||||
}
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
toggleScreens: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default ChatChannelMembershipSection;
|
||||
|
|
|
|||
|
|
@ -9,15 +9,12 @@ import {
|
|||
updateChatChannelDescription,
|
||||
sendChatChannelInvitation,
|
||||
leaveChatChannelMembership,
|
||||
updateMembershipRole,
|
||||
} from '../actions/chat_channel_setting_actions';
|
||||
|
||||
import { addSnackbarItem } from '../../Snackbar';
|
||||
import ModSection from './ModSection';
|
||||
import PersonalSettings from './PersonalSetting';
|
||||
import LeaveMembershipSection from './LeaveMembershipSection';
|
||||
import ModFaqSection from './ModFaqSection';
|
||||
import ChannelDescriptionSection from './ChannelDescriptionSection';
|
||||
import ChatChannelMembershipSection from './ChatChannelMembershipSection';
|
||||
import ManageActiveMembership from './MembershipManager/ManageActiveMembership';
|
||||
import ChatChannelSettingsSection from './ChatChannelSettingsSection';
|
||||
|
||||
export default class ChatChannelSettings extends Component {
|
||||
static propTypes = {
|
||||
|
|
@ -35,16 +32,30 @@ export default class ChatChannelSettings extends Component {
|
|||
requestedMemberships: [],
|
||||
chatChannel: null,
|
||||
currentMembership: null,
|
||||
activeMembershipId: props.activeMembershipId,
|
||||
activeMembershipId: null,
|
||||
channelDescription: null,
|
||||
channelDiscoverable: null,
|
||||
invitationUsernames: null,
|
||||
showGlobalBadgeNotification: null,
|
||||
displaySettings: true,
|
||||
displayMembershipManager: false,
|
||||
invitationLink: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { activeMembershipId } = this.state;
|
||||
this.updateChannelDetails();
|
||||
}
|
||||
|
||||
componentWillReceiveProps() {
|
||||
const { activeMembershipId } = this.props;
|
||||
this.setState({
|
||||
activeMembershipId,
|
||||
});
|
||||
}
|
||||
|
||||
updateChannelDetails = () => {
|
||||
const { activeMembershipId } = this.props;
|
||||
|
||||
getChannelDetails(activeMembershipId)
|
||||
.then((response) => {
|
||||
|
|
@ -60,6 +71,7 @@ export default class ChatChannelSettings extends Component {
|
|||
channelDiscoverable: result.chat_channel.discoverable,
|
||||
showGlobalBadgeNotification:
|
||||
result.current_membership.show_global_badge_notification,
|
||||
invitationLink: result.invitation_link,
|
||||
});
|
||||
} else {
|
||||
this.setState({
|
||||
|
|
@ -74,7 +86,7 @@ export default class ChatChannelSettings extends Component {
|
|||
errorMessages: error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
handleDescriptionChange = (e) => {
|
||||
const description = e.target.value;
|
||||
|
|
@ -83,7 +95,7 @@ export default class ChatChannelSettings extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
handlePersonChatChennelSetting = (e) => {
|
||||
handlePersonChannelSetting = (e) => {
|
||||
const status = e.target.checked;
|
||||
this.setState({
|
||||
showGlobalBadgeNotification: status,
|
||||
|
|
@ -149,6 +161,7 @@ export default class ChatChannelSettings extends Component {
|
|||
|
||||
updateMemberships = (membershipId, response, membershipStatus) => {
|
||||
if (response.success) {
|
||||
this.updateChannelDetails();
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
errorMessages: null,
|
||||
|
|
@ -236,7 +249,7 @@ export default class ChatChannelSettings extends Component {
|
|||
const { message } = response;
|
||||
|
||||
if (response.success) {
|
||||
this.componentDidMount();
|
||||
this.updateChannelDetails();
|
||||
this.setState((prevState) => {
|
||||
return {
|
||||
errorMessages: null,
|
||||
|
|
@ -265,13 +278,13 @@ export default class ChatChannelSettings extends Component {
|
|||
});
|
||||
};
|
||||
|
||||
handleChatChannelInvitations = async () => {
|
||||
handleChannelInvitations = async () => {
|
||||
const { invitationUsernames, chatChannel } = this.state;
|
||||
const { id } = chatChannel;
|
||||
const response = await sendChatChannelInvitation(id, invitationUsernames);
|
||||
const { message } = response;
|
||||
if (response.success) {
|
||||
this.componentDidMount();
|
||||
this.updateChannelDetails();
|
||||
this.setState({
|
||||
errorMessages: null,
|
||||
successMessages: response.message,
|
||||
|
|
@ -286,7 +299,7 @@ export default class ChatChannelSettings extends Component {
|
|||
addSnackbarItem({ message });
|
||||
};
|
||||
|
||||
handleleaveChatChannelMembership = async () => {
|
||||
handleleaveChannelMembership = async () => {
|
||||
// eslint-disable-next-line no-restricted-globals
|
||||
const actionStatus = confirm(
|
||||
'Are you absolutely sure you want to leave this channel? This action is permanent.',
|
||||
|
|
@ -295,7 +308,7 @@ export default class ChatChannelSettings extends Component {
|
|||
if (actionStatus) {
|
||||
const response = await leaveChatChannelMembership(currentMembership.id);
|
||||
if (response.success) {
|
||||
this.componentDidMount();
|
||||
this.updateChannelDetails();
|
||||
} else {
|
||||
this.setState({
|
||||
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() {
|
||||
const {
|
||||
chatChannel,
|
||||
|
|
@ -316,6 +376,8 @@ export default class ChatChannelSettings extends Component {
|
|||
channelDiscoverable,
|
||||
invitationUsernames,
|
||||
showGlobalBadgeNotification,
|
||||
displaySettings,
|
||||
invitationLink,
|
||||
} = this.state;
|
||||
|
||||
if (!chatChannel) {
|
||||
|
|
@ -325,53 +387,44 @@ export default class ChatChannelSettings extends Component {
|
|||
return (
|
||||
<div className="activechatchannel__activeArticle channel_settings">
|
||||
<div className="p-4">
|
||||
<ChannelDescriptionSection
|
||||
channelName={chatChannel.name}
|
||||
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}
|
||||
{displaySettings ? (
|
||||
<ChatChannelSettingsSection
|
||||
channelDiscoverable={channelDiscoverable}
|
||||
handleChannelDiscoverableStatus={
|
||||
this.handleChannelDiscoverableStatus
|
||||
}
|
||||
handleChannelDescriptionChanges={
|
||||
this.handleChannelDescriptionChanges
|
||||
}
|
||||
currentMembershipRole={currentMembership.role}
|
||||
/>
|
||||
)}
|
||||
<PersonalSettings
|
||||
updateCurrentMembershipNotificationSettings={
|
||||
this.updateCurrentMembershipNotificationSettings
|
||||
}
|
||||
showGlobalBadgeNotification={showGlobalBadgeNotification}
|
||||
handlePersonChatChennelSetting={this.handlePersonChatChennelSetting}
|
||||
/>
|
||||
{currentMembership.role === 'member' && (
|
||||
<LeaveMembershipSection
|
||||
currentMembershipRole={currentMembership.role}
|
||||
handleleaveChatChannelMembership={
|
||||
this.handleleaveChatChannelMembership
|
||||
handleleaveChannelMembership={this.handleleaveChannelMembership}
|
||||
handlePersonChannelSetting={this.handlePersonChannelSetting}
|
||||
handleChannelDescriptionChanges={
|
||||
this.handleChannelDescriptionChanges
|
||||
}
|
||||
handleChannelDiscoverableStatus={
|
||||
this.handleChannelDiscoverableStatus
|
||||
}
|
||||
handleDescriptionChange={this.handleDescriptionChange}
|
||||
handleChannelInvitations={this.handleChannelInvitations}
|
||||
handleInvitationUsernames={this.handleInvitationUsernames}
|
||||
toggleScreens={this.toggleScreens}
|
||||
removeMembership={this.removeMembership}
|
||||
chatChannelAcceptMembership={this.chatChannelAcceptMembership}
|
||||
channelDescription={channelDescription}
|
||||
chatChannel={chatChannel}
|
||||
currentMembership={currentMembership}
|
||||
activeMemberships={activeMemberships}
|
||||
pendingMemberships={pendingMemberships}
|
||||
requestedMemberships={requestedMemberships}
|
||||
invitationUsernames={invitationUsernames}
|
||||
showGlobalBadgeNotification={showGlobalBadgeNotification}
|
||||
/>
|
||||
) : (
|
||||
<ManageActiveMembership
|
||||
activeMemberships={activeMemberships}
|
||||
currentMembership={currentMembership}
|
||||
chatChannel={chatChannel}
|
||||
invitationLink={invitationLink}
|
||||
removeMembership={this.removeMembership}
|
||||
handleUpdateMembershipRole={this.handleUpdateMembershipRole}
|
||||
/>
|
||||
)}
|
||||
{currentMembership.role === 'mod' && (
|
||||
<ModFaqSection email="yo@dev.to" />
|
||||
)}
|
||||
</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 PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const InviteForm = ({
|
||||
handleChatChannelInvitations,
|
||||
handleChannelInvitations,
|
||||
invitationUsernames,
|
||||
handleInvitationUsernames,
|
||||
}) => {
|
||||
|
|
@ -26,13 +27,9 @@ const InviteForm = ({
|
|||
/>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="crayons-btn"
|
||||
type="submit"
|
||||
onClick={handleChatChannelInvitations}
|
||||
>
|
||||
<Button type="submit" onClick={handleChannelInvitations}>
|
||||
Submit
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -40,7 +37,7 @@ const InviteForm = ({
|
|||
|
||||
InviteForm.propTypes = {
|
||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||
handleChatChannelInvitations: PropTypes.func.isRequired,
|
||||
handleChannelInvitations: PropTypes.func.isRequired,
|
||||
invitationUsernames: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,25 +1,34 @@
|
|||
import { h } from 'preact';
|
||||
import PropsType from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const LeaveMembershipSection = ({
|
||||
handleleaveChannelMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'mod') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const LeaveMembershipSection = ({ handleleaveChatChannelMembership }) => {
|
||||
return (
|
||||
<div className="crayons-card p-4 grid gap-2 mb-4 leave_membership_section">
|
||||
<h3>Danger Zone</h3>
|
||||
<div>
|
||||
<button
|
||||
className="crayons-btn crayons-btn--danger leave_button"
|
||||
<Button
|
||||
className="leave_button"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={handleleaveChatChannelMembership}
|
||||
onClick={handleleaveChannelMembership}
|
||||
>
|
||||
Leave Channel
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
LeaveMembershipSection.propTypes = {
|
||||
handleleaveChatChannelMembership: PropsType.func.isRequired,
|
||||
handleleaveChannelMembership: PropsType.func.isRequired,
|
||||
};
|
||||
|
||||
export default LeaveMembershipSection;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const Membership = ({
|
||||
membership,
|
||||
|
|
@ -10,7 +12,13 @@ const Membership = ({
|
|||
}) => {
|
||||
return (
|
||||
<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">
|
||||
<img
|
||||
className="crayons-avatar__image align-middle"
|
||||
|
|
@ -21,17 +29,17 @@ const Membership = ({
|
|||
<span className="mr-2 user_name">{membership.name}</span>
|
||||
</a>
|
||||
{membershipType === 'requested' ? (
|
||||
<button
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost add-membership"
|
||||
type="button"
|
||||
onClick={chatChannelAcceptMembership}
|
||||
data-membership-id={membership.membership_id}
|
||||
>
|
||||
+
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
|
||||
<button
|
||||
<Button
|
||||
className="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost remove-membership"
|
||||
type="button"
|
||||
onClick={removeMembership}
|
||||
|
|
@ -39,24 +47,14 @@ const Membership = ({
|
|||
data-membership-status={membership.status}
|
||||
>
|
||||
x
|
||||
</button>
|
||||
</Button>
|
||||
) : null}
|
||||
</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,
|
||||
membership: PropTypes.objectOf(defaulMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
membershipType: 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 PropTypes from 'prop-types';
|
||||
|
||||
const ModFaqSection = ({ email }) => {
|
||||
const ModFaqSection = ({ email, currentMembershipRole }) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="crayons-card grid gap-2 p-4 faq-section">
|
||||
<p className="contact-details">
|
||||
|
|
@ -21,6 +25,7 @@ const ModFaqSection = ({ email }) => {
|
|||
|
||||
ModFaqSection.propTypes = {
|
||||
email: PropTypes.string.isRequired,
|
||||
currentMembershipRole: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ModFaqSection;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import InviteForm from './InviteForm';
|
|||
import SettingsForm from './SettingsForm';
|
||||
|
||||
const ModSection = ({
|
||||
handleChatChannelInvitations,
|
||||
handleChannelInvitations,
|
||||
invitationUsernames,
|
||||
handleInvitationUsernames,
|
||||
channelDescription,
|
||||
|
|
@ -13,13 +13,18 @@ const ModSection = ({
|
|||
channelDiscoverable,
|
||||
handleChannelDiscoverableStatus,
|
||||
handleChannelDescriptionChanges,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mod-section">
|
||||
<InviteForm
|
||||
handleInvitationUsernames={handleInvitationUsernames}
|
||||
invitationUsernames={invitationUsernames}
|
||||
handleChatChannelInvitations={handleChatChannelInvitations}
|
||||
handleChannelInvitations={handleChannelInvitations}
|
||||
/>
|
||||
<SettingsForm
|
||||
channelDescription={channelDescription}
|
||||
|
|
@ -34,7 +39,7 @@ const ModSection = ({
|
|||
|
||||
ModSection.propTypes = {
|
||||
handleInvitationUsernames: PropTypes.func.isRequired,
|
||||
handleChatChannelInvitations: PropTypes.func.isRequired,
|
||||
handleChannelInvitations: PropTypes.func.isRequired,
|
||||
invitationUsernames: PropTypes.func.isRequired,
|
||||
channelDescription: PropTypes.string.isRequired,
|
||||
handleDescriptionChange: PropTypes.func.isRequired,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
import Membership from './Membership';
|
||||
|
||||
const PendingMembershipSection = ({
|
||||
|
|
@ -34,17 +36,7 @@ const PendingMembershipSection = ({
|
|||
};
|
||||
|
||||
PendingMembershipSection.propTypes = {
|
||||
pendingMemberships: 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,
|
||||
pendingMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.func.isRequired,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const PersonalSettings = ({
|
||||
handlePersonChatChennelSetting,
|
||||
handlePersonChannelSetting,
|
||||
showGlobalBadgeNotification,
|
||||
updateCurrentMembershipNotificationSettings,
|
||||
}) => {
|
||||
|
|
@ -16,20 +17,19 @@ const PersonalSettings = ({
|
|||
id="c3"
|
||||
className="crayons-checkbox"
|
||||
checked={showGlobalBadgeNotification}
|
||||
onChange={handlePersonChatChennelSetting}
|
||||
onChange={handlePersonChannelSetting}
|
||||
/>
|
||||
<label htmlFor="c3" className="crayons-field__label">
|
||||
Receive Notifications for New Messages
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="crayons-btn"
|
||||
<Button
|
||||
type="submit"
|
||||
onClick={updateCurrentMembershipNotificationSettings}
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -38,7 +38,7 @@ const PersonalSettings = ({
|
|||
PersonalSettings.propTypes = {
|
||||
updateCurrentMembershipNotificationSettings: PropTypes.func.isRequired,
|
||||
showGlobalBadgeNotification: PropTypes.bool.isRequired,
|
||||
handlePersonChatChennelSetting: PropTypes.func.isRequired,
|
||||
handlePersonChannelSetting: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default PersonalSettings;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { defaulMembershipPropType } from '../../common-prop-types/membership-prop-type';
|
||||
|
||||
import Membership from './Membership';
|
||||
|
||||
const RequestedMembershipSection = ({
|
||||
|
|
@ -8,7 +10,7 @@ const RequestedMembershipSection = ({
|
|||
chatChannelAcceptMembership,
|
||||
currentMembershipRole,
|
||||
}) => {
|
||||
if (currentMembershipRole !== 'mod') {
|
||||
if (currentMembershipRole === 'member') {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
@ -37,17 +39,7 @@ const RequestedMembershipSection = ({
|
|||
};
|
||||
|
||||
RequestedMembershipSection.propTypes = {
|
||||
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,
|
||||
status: PropTypes.string.isRequired,
|
||||
}),
|
||||
).isRequired,
|
||||
requestedMemberships: PropTypes.arrayOf(defaulMembershipPropType).isRequired,
|
||||
removeMembership: PropTypes.func.isRequired,
|
||||
chatChannelAcceptMembership: PropTypes.func.isRequired,
|
||||
currentMembershipRole: PropTypes.func.isRequired,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { h } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@crayons';
|
||||
|
||||
const SettingsFrom = ({
|
||||
channelDescription,
|
||||
|
|
@ -42,13 +43,9 @@ const SettingsFrom = ({
|
|||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<button
|
||||
className="crayons-btn"
|
||||
type="submit"
|
||||
onClick={handleChannelDescriptionChanges}
|
||||
>
|
||||
<Button type="submit" onClick={handleChannelDescriptionChanges}>
|
||||
Submit
|
||||
</button>
|
||||
</Button>
|
||||
</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,
|
||||
sendChatChannelInvitation,
|
||||
leaveChatChannelMembership,
|
||||
updateMembershipRole,
|
||||
} from '../actions/chat_channel_setting_actions';
|
||||
|
||||
/* global globalThis */
|
||||
|
|
@ -200,4 +201,26 @@ describe('Chat cahnnel API requestes', () => {
|
|||
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', () => {
|
||||
const leaveHandler = jest.fn();
|
||||
const { getByText } = render(
|
||||
<LeaveMembershipSection
|
||||
handleleaveChatChannelMembership={leaveHandler}
|
||||
/>,
|
||||
<LeaveMembershipSection handleleaveChannelMembership={leaveHandler} />,
|
||||
);
|
||||
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}`,
|
||||
{
|
||||
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',
|
||||
},
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1796,6 +1796,11 @@ export default class Chat extends Component {
|
|||
? 'sidecar-user'
|
||||
: `chat_channel_setting`;
|
||||
|
||||
const contentLink =
|
||||
activeChannel.channel_type === 'direct'
|
||||
? `/${activeChannel.channel_username}`
|
||||
: '#/';
|
||||
|
||||
return (
|
||||
<a
|
||||
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);
|
||||
}}
|
||||
tabIndex="0"
|
||||
href={path}
|
||||
href={contentLink}
|
||||
data-content={dataContent}
|
||||
>
|
||||
<svg
|
||||
|
|
@ -1813,8 +1818,12 @@ export default class Chat extends Component {
|
|||
width="24"
|
||||
height="24"
|
||||
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>
|
||||
</a>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -40,13 +40,13 @@ export default class Content extends Component {
|
|||
return (
|
||||
// TODO: A button (role="button") cannot contain other interactive elements, i.e. buttons.
|
||||
// TODO: These should have key click events as well.
|
||||
// eslint-disable-next-line jsx-a11y/click-events-have-key-events
|
||||
<div
|
||||
className="activechatchannel__activecontent activechatchannel__activecontent--sidecar"
|
||||
id="chat_activecontent"
|
||||
onClick={onTriggerContent}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<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
|
||||
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
|
||||
|
||||
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 {
|
||||
# return this URL with only the parameters that match this regular expression
|
||||
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"
|
||||
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_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/user/:id" => "social_previews#user", :as => :user_social_preview
|
||||
|
|
|
|||
|
|
@ -16,12 +16,13 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
user.add_role(:super_admin)
|
||||
|
||||
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
|
||||
|
||||
it "return all details of chat channel" do
|
||||
expect(response).to have_http_status(:ok)
|
||||
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership])
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership
|
||||
invitation_link])
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -31,14 +32,15 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
chat_channel.add_users([second_user])
|
||||
|
||||
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
|
||||
|
||||
it "return only channel info and current membership" do
|
||||
expect(response.status).to eq(200)
|
||||
expect(JSON.parse(response.body)["result"].keys).to eq(%w[chat_channel memberships current_membership])
|
||||
expect(JSON.parse(response.body)["result"]["memberships"]["pending"].length).to eq(0)
|
||||
expect(JSON.parse(response.body)["result"]["memberships"]["requested"].length).to eq(0)
|
||||
expect(response.parsed_body["result"].keys).to eq(%w[chat_channel memberships current_membership
|
||||
invitation_link])
|
||||
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
|
||||
|
|
@ -338,4 +340,120 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue