diff --git a/SECURITY.md b/SECURITY.md
index 4791b3505..10c08f154 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -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.
diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss
index c3473d2d2..d0173cd05 100644
--- a/app/assets/stylesheets/chat.scss
+++ b/app/assets/stylesheets/chat.scss
@@ -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;
+ }
+ }
+ }
+}
diff --git a/app/controllers/chat_channel_memberships_controller.rb b/app/controllers/chat_channel_memberships_controller.rb
index 991534b05..a8150e84f 100644
--- a/app/controllers/chat_channel_memberships_controller.rb
+++ b/app/controllers/chat_channel_memberships_controller.rb
@@ -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 }
diff --git a/app/helpers/chat_channel_membership_helper.rb b/app/helpers/chat_channel_membership_helper.rb
new file mode 100644
index 000000000..dc1ffb8b1
--- /dev/null
+++ b/app/helpers/chat_channel_membership_helper.rb
@@ -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
diff --git a/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx
index 65cc0eae7..17d8b03f2 100644
--- a/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/ActiveMembershipsSection.jsx
@@ -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 (
Members
- {activeMemberships && activeMemberships.length > 0
- ? activeMemberships.map((pendingMembership) => (
-
- ))
- : null}
+ {activeMembershipList.map((activeMembership) => (
+
+ ))}
+
+
+
);
};
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;
diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx
index 45524e6d4..8419d1854 100644
--- a/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/ChatChannelMembershipSection.jsx
@@ -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 (
@@ -19,6 +21,7 @@ const ChatChannelMembershipSection = ({
activeMemberships={activeMemberships}
removeMembership={removeMembership}
currentMembershipRole={currentMembershipRole}
+ toggleScreens={toggleScreens}
/>
- )
-}
+ );
+};
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;
diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx
index 59daf6a02..2f1dbad1b 100644
--- a/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx
+++ b/app/javascript/chat/ChatChannelSettings/ChatChannelSettings.jsx
@@ -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 (
-
-
- {currentMembership.role === 'mod' && (
-
- )}
-
- {currentMembership.role === 'member' && (
-
+ ) : (
+
- )}
- {currentMembership.role === 'mod' && (
-
)}
diff --git a/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx b/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx
new file mode 100644
index 000000000..7f09b9252
--- /dev/null
+++ b/app/javascript/chat/ChatChannelSettings/ChatChannelSettingsSection.jsx
@@ -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,
+}) => (
+
+);
+
+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;
diff --git a/app/javascript/chat/ChatChannelSettings/InviteForm.jsx b/app/javascript/chat/ChatChannelSettings/InviteForm.jsx
index 700551d4c..194b71766 100644
--- a/app/javascript/chat/ChatChannelSettings/InviteForm.jsx
+++ b/app/javascript/chat/ChatChannelSettings/InviteForm.jsx
@@ -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 = ({
/>
-
);
@@ -40,7 +37,7 @@ const InviteForm = ({
InviteForm.propTypes = {
handleInvitationUsernames: PropTypes.func.isRequired,
- handleChatChannelInvitations: PropTypes.func.isRequired,
+ handleChannelInvitations: PropTypes.func.isRequired,
invitationUsernames: PropTypes.func.isRequired,
};
diff --git a/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx
index fe6aa3b9d..9e0eb6909 100644
--- a/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/LeaveMembershipSection.jsx
@@ -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 (
Danger Zone
-
Leave Channel
-
+
);
};
LeaveMembershipSection.propTypes = {
- handleleaveChatChannelMembership: PropsType.func.isRequired,
+ handleleaveChannelMembership: PropsType.func.isRequired,
};
export default LeaveMembershipSection;
diff --git a/app/javascript/chat/ChatChannelSettings/Membership.jsx b/app/javascript/chat/ChatChannelSettings/Membership.jsx
index 6031221fd..4071a94e3 100644
--- a/app/javascript/chat/ChatChannelSettings/Membership.jsx
+++ b/app/javascript/chat/ChatChannelSettings/Membership.jsx
@@ -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 (
-
+
{membership.name}
{membershipType === 'requested' ? (
-
+
-
+
) : null}
{membership.role !== 'mod' && currentMembershipRole === 'mod' ? (
-
x
-
+
) : null}
);
};
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,
diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx
new file mode 100644
index 000000000..0ec9b9a42
--- /dev/null
+++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/InvitationLinkManager.jsx
@@ -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 = () => (
+
+);
+
+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 (
+
+
Invitation Link
+
+
+
+
+ Copied!
+
+
+
+ );
+ }
+}
diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx
new file mode 100644
index 000000000..8f05df5e9
--- /dev/null
+++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/ManageActiveMembership.jsx
@@ -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 (
+
+
+
Chat Channel Membership manager
+
+
+
+
+ );
+ }
+}
diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx
new file mode 100644
index 000000000..6045a036c
--- /dev/null
+++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/Membership.jsx
@@ -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' ? (
+
+ Promote to Mod
+
+ ) : null;
+
+ const addAsMemberButton =
+ membership.role === 'mod' ? (
+
+ Remove Mod
+
+ ) : null;
+
+ const removeMembershipButton =
+ membership.role === 'member' ? (
+
+
+
+ ) : null;
+
+ const dropdown =
+ currentMembership.role === 'mod' && showActionButton ? (
+
+ {removeMembershipButton}
+ {addAsModButton}
+ {addAsMemberButton}
+
+ ) : null;
+
+ return (
+
+ );
+};
+
+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;
diff --git a/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx
new file mode 100644
index 000000000..3310539c3
--- /dev/null
+++ b/app/javascript/chat/ChatChannelSettings/MembershipManager/MembershipSection.jsx
@@ -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 No membership
;
+ }
+
+ const membershipCount = memberships.length;
+
+ return (
+
+ {memberships.map((activeMembership) => (
+ {}}
+ className="active-member"
+ currentMembership={currentMembership}
+ removeMembership={removeMembership}
+ handleUpdateMembershipRole={handleUpdateMembershipRole}
+ showActionButton={membershipCount > 1}
+ />
+ ))}
+
+ );
+};
+
+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;
diff --git a/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx
index 44f5d0c0f..17fe92d18 100644
--- a/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/ModFaqSection.jsx
@@ -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 (
@@ -21,6 +25,7 @@ const ModFaqSection = ({ email }) => {
ModFaqSection.propTypes = {
email: PropTypes.string.isRequired,
+ currentMembershipRole: PropTypes.string.isRequired,
};
export default ModFaqSection;
diff --git a/app/javascript/chat/ChatChannelSettings/ModSection.jsx b/app/javascript/chat/ChatChannelSettings/ModSection.jsx
index d313a7f19..a229edbbd 100644
--- a/app/javascript/chat/ChatChannelSettings/ModSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/ModSection.jsx
@@ -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 (
{
@@ -16,20 +17,19 @@ const PersonalSettings = ({
id="c3"
className="crayons-checkbox"
checked={showGlobalBadgeNotification}
- onChange={handlePersonChatChennelSetting}
+ onChange={handlePersonChannelSetting}
/>
-
Submit
-
+
);
@@ -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;
diff --git a/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx
index 051f4a629..34521126c 100644
--- a/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx
+++ b/app/javascript/chat/ChatChannelSettings/RequestedMembershipSection.jsx
@@ -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,
diff --git a/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx
index 7564bd0af..937026899 100644
--- a/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx
+++ b/app/javascript/chat/ChatChannelSettings/SettingsForm.jsx
@@ -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 = ({
-
+
Submit
-
+
);
diff --git a/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx b/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx
new file mode 100644
index 000000000..d009f7b19
--- /dev/null
+++ b/app/javascript/chat/__tests__/ManageActiveMembership.test.jsx
@@ -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('', () => {
+ it('should have no a11y violations', async () => {
+ const { container } = render(
+ ,
+ );
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+ });
+
+ it('should render', () => {
+ const { queryByText, queryByPlaceholderText } = render(
+ ,
+ );
+
+ expect(queryByText('Chat Channel Membership manager')).toBeDefined();
+ expect(queryByPlaceholderText('Search Member...')).toBeDefined();
+ });
+});
diff --git a/app/javascript/chat/__tests__/Membership.test.jsx b/app/javascript/chat/__tests__/Membership.test.jsx
new file mode 100644
index 000000000..a1249ff12
--- /dev/null
+++ b/app/javascript/chat/__tests__/Membership.test.jsx
@@ -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('', () => {
+ it('should have no a11y violations', async () => {
+ const { container } = render(
+ ,
+ );
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+ });
+
+ it('should render', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('dummy user')).toBeDefined();
+ });
+});
diff --git a/app/javascript/chat/__tests__/chatChannelSettingActions.test.js b/app/javascript/chat/__tests__/chatChannelSettingActions.test.js
index b1a4ff31e..3b15906da 100644
--- a/app/javascript/chat/__tests__/chatChannelSettingActions.test.js
+++ b/app/javascript/chat/__tests__/chatChannelSettingActions.test.js
@@ -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);
+ });
+ });
});
diff --git a/app/javascript/chat/__tests__/invitationLinkManager.test.jsx b/app/javascript/chat/__tests__/invitationLinkManager.test.jsx
new file mode 100644
index 000000000..030eb9dd0
--- /dev/null
+++ b/app/javascript/chat/__tests__/invitationLinkManager.test.jsx
@@ -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 = (
+
+);
+
+describe('', () => {
+ it('should have no a11y violations', async () => {
+ const { container } = render(
+ ,
+ );
+ const results = await axe(container);
+
+ expect(results).toHaveNoViolations();
+ });
+
+ it('should render', () => {
+ const { queryByText } = render(
+ ,
+ );
+
+ expect(queryByText('https://dummy-invitation.link')).toBeDefined();
+ expect(queryByText('Invitation Link')).toBeDefined();
+ });
+
+ it('should not render', () => {
+ const { rerender } = render(
+ ,
+ );
+
+ expect(rerender()).toEqual(undefined);
+ });
+});
diff --git a/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx b/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx
index 6e9f7156c..9b0ef9e49 100644
--- a/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx
+++ b/app/javascript/chat/__tests__/leaveMembershipSection.test.jsx
@@ -21,9 +21,7 @@ describe('', () => {
it('should have user leave channel when leave button is clicked', () => {
const leaveHandler = jest.fn();
const { getByText } = render(
- ,
+ ,
);
const leaveButton = getByText('Leave Channel');
diff --git a/app/javascript/chat/__tests__/membership.test.jsx b/app/javascript/chat/__tests__/membership.test.jsx
deleted file mode 100644
index 3c0376081..000000000
--- a/app/javascript/chat/__tests__/membership.test.jsx
+++ /dev/null
@@ -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('', () => {
- it('should have no a11y violations for a moderator user', async () => {
- const { membership, membershipType, currentMembershipRole } = getModUser();
- const { container } = render(
- ,
- );
- 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(
- ,
- );
- const results = await axe(container);
-
- expect(results).toHaveNoViolations();
- });
-
- it('should render a moderator user', () => {
- const { membership, membershipType, currentMembershipRole } = getModUser();
- const { getByAltText, getByTitle } = render(
- ,
- );
- 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(
- ,
- );
-
- // 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(
- ,
- );
-
- // 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(
- ,
- );
- expect(
- queryByText('+', {
- selector: 'button',
- }),
- ).toBeDefined();
- expect(
- queryByText('x', {
- selector: 'button',
- }),
- ).toBeDefined();
- });
-});
diff --git a/app/javascript/chat/actions/chat_channel_setting_actions.js b/app/javascript/chat/actions/chat_channel_setting_actions.js
index 033e56a39..fd41e3f36 100644
--- a/app/javascript/chat/actions/chat_channel_setting_actions.js
+++ b/app/javascript/chat/actions/chat_channel_setting_actions.js
@@ -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',
},
);
diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx
index c0b55e7fa..0e0c21dc0 100644
--- a/app/javascript/chat/chat.jsx
+++ b/app/javascript/chat/chat.jsx
@@ -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 (
);
diff --git a/app/javascript/chat/content.jsx b/app/javascript/chat/content.jsx
index 45030d3f0..603b88399 100644
--- a/app/javascript/chat/content.jsx
+++ b/app/javascript/chat/content.jsx
@@ -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
+ <% if @link_expired %>
+ This Invitation link is expired. Please contact the channel mod for new Invitation link
+ <% else %>
+ Would you like to join the <%= @chat_channel.channel_name %> channel
+ <%= 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 %>
+
diff --git a/config/fastly/snippets/safe_params_list.vcl b/config/fastly/snippets/safe_params_list.vcl
index 30c3f0c65..3d8f8daf3 100644
--- a/config/fastly/snippets/safe_params_list.vcl
+++ b/config/fastly/snippets/safe_params_list.vcl
@@ -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)$");
}
}
diff --git a/config/routes.rb b/config/routes.rb
index 09534c6a1..d4c280994 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -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
diff --git a/spec/requests/chat_channel_memberships_spec.rb b/spec/requests/chat_channel_memberships_spec.rb
index c0785984e..7895cdecf 100644
--- a/spec/requests/chat_channel_memberships_spec.rb
+++ b/spec/requests/chat_channel_memberships_spec.rb
@@ -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