[deploy] Connect: move settings to sidecar iframe, improve invitations and remove experimental features (#6903)
* 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 * Move channel settings to sidecar * added spec for chat_channels_memberships_controller.rb (#6539) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message Co-authored-by: jitendra <jitendrarajpurohit@skynox.tech> * Refining "Move sidecar to iframe" branch (#6634) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message * 🐞 Bug Fix: Open sidecar of clicking @all * 🐞 Bug Fix: adding space after selecting the username from suggestion box * added remove channel member functionality for moderators * 🔩 Small Tweaks: Hover issue fixed * 🐞 Bug Fix: Remove sidecar toggle on @all tag (for now) * updated manually constructed urls with rails path helpers * mod users cannot remove members of other channel Co-authored-by: jitendra <jitendrarajpurohit@skynox.tech> * 🚀 Finalising the connect group/channel feature. (#6829) * added spec for chat_channels_memberships_controller.rb * simplified record not found error for membership * error message formatting updated * added error key in flash message * 🐞 Bug Fix: Open sidecar of clicking @all * 🐞 Bug Fix: adding space after selecting the username from suggestion box * added remove channel member functionality for moderators * 🔩 Small Tweaks: Hover issue fixed * 🐞 Bug Fix: Remove sidecar toggle on @all tag (for now) * updated manually constructed urls with rails path helpers * mod users cannot remove members of other channel * mod members can now invite user who have left the channel * 🐞 Bug Fix: Ability to open sidecar on mention all properly * Eslint Changes * send email notification on inviting user feature added * 🛠 Email design enhancements for channel invitation * send invitation to mod user added from internal * 🛠 Gif changes for Mod * spec fixed * inviter name issue fixed * 🚀 Open sidecar when automatically if you use link from email Co-authored-by: jitendra <jitendrarajpurohit@skynox.tech> * review changes * added spec for channel_invite_email * Bug🐞: Design problem with sidecar after merge * Bug🐞: Design problem with invitation alert box * replaced instance variables with local variable * Bug🐞: Wrong css classes fixed * Bug🐞: Edit option bug * yarn.lock file updated * code review changes * updated manually constructed urls with rails path helpers in spec * user name url issue fix in invite email * user url corrected Co-authored-by: Sarthak Sharma <7lovesharma7@gmail.com> Co-authored-by: benhalpern <bendhalpern@gmail.com>
This commit is contained in:
parent
05b6220e2d
commit
e2edf960dc
44 changed files with 797 additions and 2046 deletions
|
|
@ -1,5 +1,4 @@
|
|||
@import 'variables';
|
||||
@import 'connect/_channel-details';
|
||||
@import 'mixins';
|
||||
|
||||
// High level classes
|
||||
|
|
@ -645,54 +644,19 @@
|
|||
}
|
||||
}
|
||||
|
||||
.activecontent__githubrepo {
|
||||
}
|
||||
|
||||
.activecontent__githubrepoheader {
|
||||
padding-left: 30px;
|
||||
padding-bottom: 3px;
|
||||
min-height: 30px;
|
||||
margin-top: -9px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.activecontent__githubrepofiles {
|
||||
border-bottom: 1px solid $light-medium-gray;
|
||||
font-size: 0.8em;
|
||||
overflow: auto;
|
||||
|
||||
&.activecontent__githubrepofiles--root {
|
||||
height: 280px;
|
||||
max-height: 40vh;
|
||||
}
|
||||
}
|
||||
|
||||
.activecontent__githubreporeadme {
|
||||
font-size: 0.8em;
|
||||
|
||||
img {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.activecontent__githubrepofilerow {
|
||||
padding: 4px 2px;
|
||||
border-top: 1px solid $light-medium-gray;
|
||||
}
|
||||
|
||||
.chat__channelinvitationsindicator button {
|
||||
background: $green;
|
||||
.chat__channelinvitationsindicator a {
|
||||
background: linear-gradient(10deg, darken($green, 25%), darken($green, 15%));
|
||||
text-align: center;
|
||||
padding: 10px 0px;
|
||||
padding: 30px 0px;
|
||||
display: block;
|
||||
border-radius: 3px;
|
||||
margin: 3px 0px;
|
||||
border: 0px;
|
||||
width: 93%;
|
||||
font-size: 0.9em;
|
||||
width: 94%;
|
||||
color: white;
|
||||
font-size: 1.1em;
|
||||
font-weight: 700;
|
||||
-webkit-appearance: unset;
|
||||
}
|
||||
|
||||
.chatchannels {
|
||||
|
|
@ -1255,6 +1219,9 @@
|
|||
border-left: 3px solid #4e57ef;
|
||||
height: 50px;
|
||||
overflow: hidden;
|
||||
p {
|
||||
margin: 18px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.closeEdit {
|
||||
|
|
|
|||
|
|
@ -104,6 +104,18 @@ pre {
|
|||
}
|
||||
}
|
||||
|
||||
.cta {
|
||||
color: $black;
|
||||
background: $green;
|
||||
font-family: $helvetica-condensed;
|
||||
-webkit-appearance: none;
|
||||
font-stretch: condensed;
|
||||
font-weight: bold;
|
||||
&.cta-danger {
|
||||
background: $red;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,8 @@
|
|||
$input-width: 650px;
|
||||
|
||||
#page-content.users-edit,
|
||||
#page-content.chat_channel_memberships-edit,
|
||||
#page-content.chat_channel_memberships-index,
|
||||
#page-content.users-update,
|
||||
#page-content.users-confirm_destroy,
|
||||
#page-content.organizations-create,
|
||||
|
|
@ -12,18 +14,25 @@ $input-width: 650px;
|
|||
#notice {
|
||||
background: $green;
|
||||
color: black;
|
||||
padding: 40px 0px 40px;
|
||||
padding: 40px 15px 40px;
|
||||
text-align: center;
|
||||
position: relative;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
font-weight: 700;
|
||||
font-size: 1.05em;
|
||||
&.error-notice {
|
||||
background: rgba(255, 80, 80, 0.7);
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
.user-settings-page {
|
||||
h1,
|
||||
h3,
|
||||
p {
|
||||
margin: 10px 0 !important;
|
||||
}
|
||||
width: 900px;
|
||||
max-width: 94%;
|
||||
margin: 0 auto;
|
||||
|
|
@ -340,6 +349,27 @@ $input-width: 650px;
|
|||
resize: none;
|
||||
}
|
||||
}
|
||||
%inline-button-form {
|
||||
display: inline-block;
|
||||
input[type='submit'] {
|
||||
display: inline-block;
|
||||
font-size: 17px;
|
||||
border: 0px;
|
||||
border-radius: 100px;
|
||||
padding: 2px 10px;
|
||||
width: auto;
|
||||
margin: 0px;
|
||||
margin-left: 5px;
|
||||
color: white;
|
||||
background: $black;
|
||||
}
|
||||
}
|
||||
.inline-button-form {
|
||||
@extend %inline-button-form;
|
||||
}
|
||||
.inline-button-form-hidden {
|
||||
display: none;
|
||||
}
|
||||
h2 {
|
||||
font-size: calc(40px + 1vw);
|
||||
font-family: $helvetica-condensed;
|
||||
|
|
@ -676,6 +706,31 @@ $input-width: 650px;
|
|||
}
|
||||
}
|
||||
|
||||
.settings-form-userlist {
|
||||
.settings-form-userlistrow {
|
||||
font-size: 18px;
|
||||
padding: 6px 0px;
|
||||
min-height: 33px;
|
||||
display: block;
|
||||
@include themeable(color, theme-color, $black);
|
||||
a {
|
||||
@include themeable(color, theme-color, $black);
|
||||
}
|
||||
img {
|
||||
height: 1.5em;
|
||||
width: 1.5em;
|
||||
border-radius: 100px;
|
||||
vertical-align: -0.25em;
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
&:hover {
|
||||
.inline-button-form-hidden {
|
||||
@extend %inline-button-form;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.signout_confirm-wrapper {
|
||||
padding: calc(15% + 50px) 2% 30%;
|
||||
text-align: center;
|
||||
|
|
@ -786,3 +841,14 @@ ul.delete__account {
|
|||
.membership-purchase-link {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
#page-content.chat_channel_memberships-edit,
|
||||
#page-content.chat_channel_memberships-index {
|
||||
#page-content-inner {
|
||||
margin-top: 40px;
|
||||
}
|
||||
.notice {
|
||||
margin-top: -40px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,11 @@
|
|||
class ChatChannelMembershipsController < ApplicationController
|
||||
after_action :verify_authorized
|
||||
|
||||
def index
|
||||
skip_authorization
|
||||
@pending_invites = current_user.chat_channel_memberships.includes(:chat_channel).where(status: "pending")
|
||||
end
|
||||
|
||||
def find_by_chat_channel_id
|
||||
@membership = ChatChannelMembership.where(chat_channel_id: params[:chat_channel_id], user_id: current_user.id).first!
|
||||
authorize @membership
|
||||
|
|
@ -11,41 +16,80 @@ class ChatChannelMembershipsController < ApplicationController
|
|||
)
|
||||
end
|
||||
|
||||
def edit
|
||||
@membership = ChatChannelMembership.find(params[:id])
|
||||
@channel = @membership.chat_channel
|
||||
authorize @membership
|
||||
end
|
||||
|
||||
def create
|
||||
@chat_channel = ChatChannel.find(permitted_params[:chat_channel_id])
|
||||
membership_params = params[:chat_channel_membership]
|
||||
@chat_channel = ChatChannel.find(membership_params[:chat_channel_id])
|
||||
authorize @chat_channel, :update?
|
||||
ChatChannelMembership.create(
|
||||
user_id: permitted_params[:user_id],
|
||||
chat_channel_id: @chat_channel.id,
|
||||
status: "pending",
|
||||
)
|
||||
usernames = membership_params[:invitation_usernames].split(",").map { |username| username.strip.delete("@") }
|
||||
users = User.where(username: usernames)
|
||||
invitations_sent = @chat_channel.invite_users(users: users, membership_role: "member", inviter: current_user)
|
||||
flash[:settings_notice] = if invitations_sent.zero?
|
||||
"No invitations sent. Check for username typos."
|
||||
else
|
||||
"#{invitations_sent} #{'invitation'.pluralize(invitations_sent)} sent."
|
||||
end
|
||||
membership = @chat_channel.chat_channel_memberships.find_by!(user: current_user)
|
||||
redirect_to edit_chat_channel_membership_path(membership)
|
||||
end
|
||||
|
||||
def remove_membership
|
||||
@chat_channel = ChatChannel.find(params[:chat_channel_id])
|
||||
authorize @chat_channel, :update?
|
||||
@chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id])
|
||||
if params[:status] == "pending"
|
||||
@chat_channel_membership.destroy
|
||||
flash[:settings_notice] = "Invitation removed."
|
||||
else
|
||||
@chat_channel_membership.update(status: "removed_from_channel")
|
||||
flash[:settings_notice] = "Removed #{@chat_channel_membership.user.name}"
|
||||
end
|
||||
membership = ChatChannelMembership.find_by!(chat_channel_id: params[:chat_channel_id], user: current_user)
|
||||
redirect_to edit_chat_channel_membership_path(membership)
|
||||
end
|
||||
|
||||
def update
|
||||
@chat_channel_membership = ChatChannelMembership.find(params[:id])
|
||||
authorize @chat_channel_membership
|
||||
if permitted_params[:user_action] == "accept"
|
||||
@chat_channel_membership.update(status: "active")
|
||||
if permitted_params[:user_action].present?
|
||||
respond_to_invitation
|
||||
else
|
||||
@chat_channel_membership.update(status: "rejected")
|
||||
@chat_channel_membership.update(permitted_params)
|
||||
flash[:settings_notice] = "Personal settings updated."
|
||||
redirect_to edit_chat_channel_membership_path(@chat_channel_membership.id)
|
||||
end
|
||||
@chat_channels_memberships = current_user.
|
||||
chat_channel_memberships.includes(:chat_channel).
|
||||
where(status: "pending").
|
||||
order("chat_channel_memberships.updated_at DESC")
|
||||
render "chat_channels/index.json"
|
||||
end
|
||||
|
||||
def destroy
|
||||
@chat_channel_membership = ChatChannel.find(params[:id]).
|
||||
chat_channel_memberships.where(user_id: current_user.id).first
|
||||
@chat_channel_membership = ChatChannelMembership.find(params[:id])
|
||||
authorize @chat_channel_membership
|
||||
channel_name = @chat_channel_membership.chat_channel.channel_name
|
||||
@chat_channel_membership.update(status: "left_channel")
|
||||
@chat_channels_memberships = []
|
||||
render json: { result: "left channel" }, status: :created
|
||||
flash[:settings_notice] = "You have left the channel #{channel_name}. It may take a moment to be removed from your list."
|
||||
redirect_to chat_channel_memberships_path
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def permitted_params
|
||||
params.require(:chat_channel_membership).permit(:user_id, :chat_channel_id, :user_action, :id)
|
||||
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
|
||||
end
|
||||
|
||||
def respond_to_invitation
|
||||
if permitted_params[:user_action] == "accept"
|
||||
@chat_channel_membership.update(status: "active")
|
||||
channel_name = @chat_channel_membership.chat_channel.channel_name
|
||||
flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list."
|
||||
else
|
||||
@chat_channel_membership.update(status: "rejected")
|
||||
flash[:settings_notice] = "Invitation rejected."
|
||||
end
|
||||
redirect_to chat_channel_memberships_path
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -30,8 +30,14 @@ class ChatChannelsController < ApplicationController
|
|||
end
|
||||
|
||||
def update
|
||||
ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update
|
||||
render_chat_channel
|
||||
if ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update
|
||||
flash[:settings_notice] = "Channel settings updated."
|
||||
else
|
||||
default_error_message = "Channel settings updation failed. Try again later."
|
||||
flash[:error] = @chat_channel.errors.full_messages.to_sentence.presence || default_error_message
|
||||
end
|
||||
current_user_membership = @chat_channel.mod_memberships.find_by!(user: current_user)
|
||||
redirect_to edit_chat_channel_membership_path(current_user_membership)
|
||||
end
|
||||
|
||||
def open
|
||||
|
|
@ -75,7 +81,7 @@ class ChatChannelsController < ApplicationController
|
|||
valid_listing = ClassifiedListing.where(user_id: params[:user_id], contact_via_connect: true).limit(1)
|
||||
authorize ChatChannel
|
||||
if chat_recipient.inbox_type == "open" || valid_listing.length == 1
|
||||
chat = ChatChannel.create_with_users([current_user, chat_recipient], "direct")
|
||||
chat = ChatChannel.create_with_users(users: [current_user, chat_recipient], channel_type: "direct")
|
||||
message_markdown = params[:message]
|
||||
message = Message.new(
|
||||
chat_channel: chat,
|
||||
|
|
|
|||
|
|
@ -6,13 +6,18 @@ class Internal::ChatChannelsController < Internal::ApplicationController
|
|||
end
|
||||
|
||||
def create
|
||||
ChatChannel.create_with_users(users_by_param, "invite_only", chat_channel_params[:channel_name])
|
||||
ChatChannel.create_with_users(
|
||||
users: users_by_param,
|
||||
channel_type: "invite_only",
|
||||
contrived_name: chat_channel_params[:channel_name],
|
||||
membership_role: "mod",
|
||||
)
|
||||
redirect_back(fallback_location: "/internal/chat_channels")
|
||||
end
|
||||
|
||||
def update
|
||||
@chat_channel = ChatChannel.find(params[:id])
|
||||
@chat_channel.add_users(users_by_param)
|
||||
@chat_channel.invite_users(users: users_by_param, membership_role: "mod")
|
||||
redirect_back(fallback_location: "/internal/chat_channels")
|
||||
end
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<CodeEditor /> should render and test snapshot 1`] = `
|
||||
<div
|
||||
class="chatcodeeditor"
|
||||
id="codeeditor"
|
||||
>
|
||||
<div
|
||||
class="chatcodeeditor__header"
|
||||
>
|
||||
Experimental (WIP)
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,100 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<GithubRepo /> with fake token should render and test snapshot 1`] = `
|
||||
preact-render-spy (1 nodes)
|
||||
-------
|
||||
<div class="activecontent__githubrepo">
|
||||
<div class="activecontent__githubrepoheader">
|
||||
<a href="/Users/benhalpern/dev/dev.to_core/app"></a>
|
||||
invalid json response body at reason: Unexpected end of JSON input
|
||||
</div>
|
||||
<div class="activecontent__githubrepofiles activecontent__githubrepofiles--root">
|
||||
<div class="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href="https://github.com/username/repositoryname/tree/master/Camera"
|
||||
data-api-url="https://api.github.com/repos/username/repositoryname/contents/Camera?ref=master"
|
||||
data-path="Camera"
|
||||
onClick={[Function anonymous]}
|
||||
>
|
||||
<span
|
||||
role="img"
|
||||
aria-label="folder-emoji"
|
||||
>
|
||||
📁
|
||||
</span>
|
||||
Camera
|
||||
</a>
|
||||
</div>
|
||||
<div class="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href="https://github.com/username/repositoryname/tree/master/Environment"
|
||||
data-api-url="https://api.github.com/repos/username/repositoryname/contents/Environment?ref=master"
|
||||
data-path="Environment"
|
||||
onClick={[Function anonymous]}
|
||||
>
|
||||
<span
|
||||
role="img"
|
||||
aria-label="folder-emoji"
|
||||
>
|
||||
📁
|
||||
</span>
|
||||
Environment
|
||||
</a>
|
||||
</div>
|
||||
<div class="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href="https://github.com/username/repositoryname/tree/master/Interactables"
|
||||
data-api-url="https://api.github.com/repos/username/repositoryname/contents/Interactables?ref=master"
|
||||
data-path="Interactables"
|
||||
onClick={[Function anonymous]}
|
||||
>
|
||||
<span
|
||||
role="img"
|
||||
aria-label="folder-emoji"
|
||||
>
|
||||
📁
|
||||
</span>
|
||||
Interactables
|
||||
</a>
|
||||
</div>
|
||||
<div class="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href="https://github.com/username/repositoryname/tree/master/Level%20Design"
|
||||
data-api-url="https://api.github.com/repos/username/repositoryname/contents/Level%20Design?ref=master"
|
||||
data-path="Level Design"
|
||||
onClick={[Function anonymous]}
|
||||
>
|
||||
Level Design
|
||||
</a>
|
||||
</div>
|
||||
<div class="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href="https://github.com/username/repositoryname/tree/master/Player"
|
||||
data-api-url="https://api.github.com/repos/username/repositoryname/contents/Player?ref=master"
|
||||
data-path="Player"
|
||||
onClick={[Function anonymous]}
|
||||
>
|
||||
Player
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
||||
exports[`<GithubRepo /> with no token should render and test snapshot 1`] = `
|
||||
<div
|
||||
class="activecontent__githubrepo"
|
||||
>
|
||||
<div
|
||||
class="activecontent__githubrepoheader"
|
||||
>
|
||||
<em>
|
||||
Authentication required
|
||||
</em>
|
||||
</div>
|
||||
<p>
|
||||
This feature is in internal alpha testing mode.
|
||||
</p>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,460 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<UserDetails /> for user1 should render and test snapshot 1`] = `
|
||||
<div>
|
||||
<img
|
||||
alt="bojackhorseman profile"
|
||||
src="https://media.giphy.com/media/3o7WTHb2WjEXbsmWDS/giphy.gif"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "500px",
|
||||
"display": "block",
|
||||
"height": "210px",
|
||||
"margin": " 15px auto",
|
||||
"width": "210px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h1
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="/bojackhorseman"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Bojack Horseman
|
||||
</a>
|
||||
</h1>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "50px",
|
||||
"margin": "auto",
|
||||
"width": "96%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="https://twitter.com/bojacktwitter"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="twitter logo"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/bojackgithub"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="github logo"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="http://bojackhorseman.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="external link icon"
|
||||
class="external-link-img"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"fontStyle": "italic",
|
||||
}
|
||||
}
|
||||
>
|
||||
I am the Bojack Horseman from Horsing Around and Secreteriat
|
||||
</div>
|
||||
<div
|
||||
class="activechatchannel__activecontentuserdetails"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="key"
|
||||
>
|
||||
location
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
Los Angeles, CA
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="key"
|
||||
>
|
||||
joined
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
January 2, 1964
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="userdetails__blockreport"
|
||||
/>
|
||||
<div
|
||||
id="userdetails__reportabuse"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="userdetails__reportabuse"
|
||||
>
|
||||
<p>
|
||||
Reporting abuse will:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with you
|
||||
</li>
|
||||
<li>
|
||||
give the DEV team your consent to read messages in this chat to understand your report and take appropriate action
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented across DEV yet.
|
||||
</p>
|
||||
<h5>
|
||||
Are you sure?
|
||||
</h5>
|
||||
<a
|
||||
href="/report-abuse"
|
||||
onClick={[Function]}
|
||||
tabIndex="0"
|
||||
>
|
||||
Yes, Report
|
||||
</a>
|
||||
<a
|
||||
class="no"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="userdetails__blockmsg"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="userdetails__blockmsg"
|
||||
>
|
||||
<p>
|
||||
Blocking on connect will:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with you
|
||||
</li>
|
||||
<li>
|
||||
NOT notify the user you will block--this channel will become inaccessible for both users
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented across DEV yet. Consider reporting abuse to the DEV team if this user is spamming or harassing elsewhere on dev.to, so we can take further action.
|
||||
</p>
|
||||
<h5>
|
||||
Are you sure?
|
||||
</h5>
|
||||
<a
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
Yes, Block
|
||||
</a>
|
||||
<a
|
||||
class="no"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<UserDetails /> for user2 should render and test snapshot 1`] = `
|
||||
<div>
|
||||
<img
|
||||
alt="mrpeanutbutter profile"
|
||||
src="https://media.giphy.com/media/xThuW6sWCGbpZMpX7a/giphy.gif"
|
||||
style={
|
||||
Object {
|
||||
"borderRadius": "500px",
|
||||
"display": "block",
|
||||
"height": "210px",
|
||||
"margin": " 15px auto",
|
||||
"width": "210px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
<h1
|
||||
style={
|
||||
Object {
|
||||
"textAlign": "center",
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="/mrpeanutbutter"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
Mr. Peanutbutter
|
||||
</a>
|
||||
</h1>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"height": "50px",
|
||||
"margin": "auto",
|
||||
"width": "96%",
|
||||
}
|
||||
}
|
||||
>
|
||||
<a
|
||||
href="https://twitter.com/mrpbtwitter"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="twitter logo"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="https://github.com/mrpbgithub"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="github logo"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
<a
|
||||
href="http://mrpeanutbutter.com"
|
||||
rel="noopener noreferrer"
|
||||
target="_blank"
|
||||
>
|
||||
<img
|
||||
alt="external link icon"
|
||||
class="external-link-img"
|
||||
src=""
|
||||
style={
|
||||
Object {
|
||||
"margin": "5px 15px 15px 0px",
|
||||
"width": "30px",
|
||||
}
|
||||
}
|
||||
/>
|
||||
</a>
|
||||
</div>
|
||||
<div
|
||||
style={
|
||||
Object {
|
||||
"fontStyle": "italic",
|
||||
}
|
||||
}
|
||||
>
|
||||
Woof Woof *smile*
|
||||
</div>
|
||||
<div
|
||||
class="activechatchannel__activecontentuserdetails"
|
||||
>
|
||||
<div>
|
||||
<div
|
||||
class="key"
|
||||
>
|
||||
location
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
Los Angeles, DO(G)
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="key"
|
||||
>
|
||||
joined
|
||||
</div>
|
||||
<div
|
||||
class="value"
|
||||
>
|
||||
1960s
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="userdetails__blockreport"
|
||||
>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Block User
|
||||
</button>
|
||||
<button
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
Report Abuse
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
id="userdetails__reportabuse"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="userdetails__reportabuse"
|
||||
>
|
||||
<p>
|
||||
Reporting abuse will:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with you
|
||||
</li>
|
||||
<li>
|
||||
give the DEV team your consent to read messages in this chat to understand your report and take appropriate action
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented across DEV yet.
|
||||
</p>
|
||||
<h5>
|
||||
Are you sure?
|
||||
</h5>
|
||||
<a
|
||||
href="/report-abuse"
|
||||
onClick={[Function]}
|
||||
tabIndex="0"
|
||||
>
|
||||
Yes, Report
|
||||
</a>
|
||||
<a
|
||||
class="no"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
id="userdetails__blockmsg"
|
||||
style={
|
||||
Object {
|
||||
"display": "none",
|
||||
}
|
||||
}
|
||||
>
|
||||
<div
|
||||
class="userdetails__blockmsg"
|
||||
>
|
||||
<p>
|
||||
Blocking on connect will:
|
||||
</p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with you
|
||||
</li>
|
||||
<li>
|
||||
NOT notify the user you will block--this channel will become inaccessible for both users
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented across DEV yet. Consider reporting abuse to the DEV team if this user is spamming or harassing elsewhere on dev.to, so we can take further action.
|
||||
</p>
|
||||
<h5>
|
||||
Are you sure?
|
||||
</h5>
|
||||
<a
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
Yes, Block
|
||||
</a>
|
||||
<a
|
||||
class="no"
|
||||
onClick={[Function]}
|
||||
onKeyUp={[Function]}
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -1,78 +0,0 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`<View /> should render and test snapshot (no channel) 1`] = `
|
||||
<div
|
||||
class="chatNonChatView"
|
||||
>
|
||||
<div
|
||||
class="container"
|
||||
>
|
||||
<button
|
||||
class="chatNonChatView_exitbutton"
|
||||
data-content="exit"
|
||||
onClick={[Function]}
|
||||
type="button"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<h1>
|
||||
Channel Invitations
|
||||
<span
|
||||
aria-label="hugging-emoji"
|
||||
role="img"
|
||||
>
|
||||
🤗
|
||||
</span>
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`<View /> should render and test snapshot (with channel) 1`] = `
|
||||
preact-render-spy (1 nodes)
|
||||
-------
|
||||
<div class="chatNonChatView">
|
||||
<div class="container">
|
||||
<button
|
||||
type="button"
|
||||
class="chatNonChatView_exitbutton"
|
||||
onClick={[Function onViewExitFake]}
|
||||
data-content="exit"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
<h1>
|
||||
Channel Invitations
|
||||
<span
|
||||
role="img"
|
||||
aria-label="hugging-emoji"
|
||||
>
|
||||
🤗
|
||||
</span>
|
||||
</h1>
|
||||
<div class="chatNonChatView_contentblock">
|
||||
<h2>name</h2>
|
||||
<div>
|
||||
<em>some description</em>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
class="cta"
|
||||
onClick={[Function handleInvitationAcceptFake]}
|
||||
data-content="12345"
|
||||
>
|
||||
Accept
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cta"
|
||||
onClick={[Function handleInvitationDeclineFake]}
|
||||
data-content="12345"
|
||||
>
|
||||
Decline
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
`;
|
||||
|
|
@ -1,14 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import CodeEditor from '../codeEditor';
|
||||
|
||||
const getCodeEditor = () => (
|
||||
<CodeEditor activeChannelId={12345} pusherKey="ASDFGHJKL" />
|
||||
);
|
||||
|
||||
describe('<CodeEditor />', () => {
|
||||
it('should render and test snapshot', () => {
|
||||
const tree = render(getCodeEditor());
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
|
@ -1,179 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import fetch from 'jest-fetch-mock';
|
||||
import GithubRepo from '../githubRepo';
|
||||
|
||||
global.fetch = fetch;
|
||||
|
||||
function flushPromises() {
|
||||
return new Promise(resolve => setImmediate(resolve));
|
||||
}
|
||||
|
||||
const getGithubRepo = token => (
|
||||
<GithubRepo
|
||||
activeChannelId={12345}
|
||||
pusherKey="ASDFGHJKL"
|
||||
githubToken={token}
|
||||
resource={{ args: 'someargs' }}
|
||||
/>
|
||||
);
|
||||
|
||||
const contents = [
|
||||
{
|
||||
name: 'Camera',
|
||||
path: 'Camera',
|
||||
sha: 'hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp',
|
||||
size: 0,
|
||||
url:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Camera?ref=master',
|
||||
html_url: 'https://github.com/username/repositoryname/tree/master/Camera',
|
||||
git_url:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp',
|
||||
download_url: null,
|
||||
type: 'dir',
|
||||
_links: {
|
||||
self:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Camera?ref=master',
|
||||
git:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/hysst5jI2idHutihWo3JxYlTByoj0lkdXmkmuBEp',
|
||||
html: 'https://github.com/username/repositoryname/tree/master/Camera',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Environment',
|
||||
path: 'Environment',
|
||||
sha: 'dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs',
|
||||
size: 0,
|
||||
url:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Environment?ref=master',
|
||||
html_url:
|
||||
'https://github.com/username/repositoryname/tree/master/Environment',
|
||||
git_url:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs',
|
||||
download_url: null,
|
||||
type: 'dir',
|
||||
_links: {
|
||||
self:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Environment?ref=master',
|
||||
git:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/dwogYlYGQOXj1ru3L9HYfX7HdX3WNQPJgJVeStRs',
|
||||
html:
|
||||
'https://github.com/username/repositoryname/tree/master/Environment',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Interactables',
|
||||
path: 'Interactables',
|
||||
sha: '44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat',
|
||||
size: 0,
|
||||
url:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Interactables?ref=master',
|
||||
html_url:
|
||||
'https://github.com/username/repositoryname/tree/master/Interactables',
|
||||
git_url:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat',
|
||||
download_url: null,
|
||||
type: 'dir',
|
||||
_links: {
|
||||
self:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Interactables?ref=master',
|
||||
git:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/44OLxtYSQjr2DLVKPnwGTj6JuQpo7Te7pEIDULat',
|
||||
html:
|
||||
'https://github.com/username/repositoryname/tree/master/Interactables',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Level Design',
|
||||
path: 'Level Design',
|
||||
sha: 'g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax',
|
||||
size: 583,
|
||||
url:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Level%20Design?ref=master',
|
||||
html_url:
|
||||
'https://github.com/username/repositoryname/tree/master/Level%20Design',
|
||||
git_url:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax',
|
||||
download_url:
|
||||
'https://raw.githubusercontent.com/username/repositoryname/master/Level%20Design',
|
||||
type: 'file',
|
||||
_links: {
|
||||
self:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Level%20Design?ref=master',
|
||||
git:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/g178Oy2xV8gqFBhoaflJkSbHN01dxWEqjJTxw6Ax',
|
||||
html:
|
||||
'https://github.com/username/repositoryname/tree/master/Level%20Design',
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Player',
|
||||
path: 'Player',
|
||||
sha: 'tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp',
|
||||
size: 1000,
|
||||
url:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Player?ref=master',
|
||||
html_url: 'https://github.com/username/repositoryname/tree/master/Player',
|
||||
git_url:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp',
|
||||
download_url:
|
||||
'https://raw.githubusercontent.com/username/repositoryname/master/Player',
|
||||
type: 'file',
|
||||
_links: {
|
||||
self:
|
||||
'https://api.github.com/repos/username/repositoryname/contents/Player?ref=master',
|
||||
git:
|
||||
'https://api.github.com/repos/username/repositoryname/git/trees/tia6ISq19krmOJzbtwpCTIuwRnvpSxZY2g1FMgOp',
|
||||
html: 'https://github.com/username/repositoryname/tree/master/Player',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('<GithubRepo />', () => {
|
||||
describe('with no token', () => {
|
||||
it('should render and test snapshot', () => {
|
||||
const tree = render(getGithubRepo());
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the proper elements, attributes and values', () => {
|
||||
const context = shallow(getGithubRepo());
|
||||
expect(context.find('.activecontent__githubrepo').exists()).toEqual(true);
|
||||
expect(context.find('em').text()).toEqual('Authentication required');
|
||||
});
|
||||
});
|
||||
|
||||
describe('with fake token', () => {
|
||||
it('should render and test snapshot', async () => {
|
||||
await fetch.mockResponseOnce(JSON.stringify(contents));
|
||||
const context = shallow(getGithubRepo('some_token'));
|
||||
await flushPromises();
|
||||
expect(context).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the proper elements, attributes and values with states set', async () => {
|
||||
await fetch.mockResponseOnce(JSON.stringify(contents));
|
||||
const context = shallow(getGithubRepo('some_token'));
|
||||
await flushPromises();
|
||||
|
||||
const len = context.find('.activecontent__githubrepofilerow').length;
|
||||
expect(len > 0).toEqual(true);
|
||||
for (let i = 0; i < len; i += 1) {
|
||||
const ghrow = context.find('.activecontent__githubrepofilerow').at(i);
|
||||
expect(ghrow.exists()).toEqual(true);
|
||||
if (contents[i].type === 'dir') {
|
||||
expect(ghrow.text()).toEqual(`📁 ${contents[i].name}`);
|
||||
} else {
|
||||
expect(ghrow.text()).toEqual(contents[i].name);
|
||||
}
|
||||
expect(ghrow.childAt(0).attr('href')).toEqual(contents[i].html_url);
|
||||
expect(ghrow.childAt(0).attr('data-api-url')).toEqual(contents[i].url);
|
||||
expect(ghrow.childAt(0).attr('data-path')).toEqual(contents[i].path);
|
||||
}
|
||||
expect(context.find('.activecontent__githubrepoheader').text()).toEqual(
|
||||
context.state('path'),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,211 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { JSDOM } from 'jsdom';
|
||||
import { shallow } from 'preact-render-spy';
|
||||
import UserDetails from '../userDetails';
|
||||
|
||||
const doc = new JSDOM('<!doctype html><html><body></body></html>');
|
||||
global.document = doc;
|
||||
global.window = doc.defaultView;
|
||||
global.window.currentUser = { id: '1' };
|
||||
|
||||
const user1 = {
|
||||
id: '1',
|
||||
username: 'bojackhorseman',
|
||||
name: 'Bojack Horseman',
|
||||
summary: 'I am the Bojack Horseman from Horsing Around and Secreteriat',
|
||||
joined_at: 'January 2, 1964',
|
||||
twitter_username: 'bojacktwitter',
|
||||
github_username: 'bojackgithub',
|
||||
website_url: 'http://bojackhorseman.com',
|
||||
location: 'Los Angeles, CA',
|
||||
profile_image: 'https://media.giphy.com/media/3o7WTHb2WjEXbsmWDS/giphy.gif',
|
||||
};
|
||||
|
||||
const user2 = {
|
||||
id: '2',
|
||||
username: 'mrpeanutbutter',
|
||||
name: 'Mr. Peanutbutter',
|
||||
summary: 'Woof Woof *smile*',
|
||||
joined_at: '1960s',
|
||||
twitter_username: 'mrpbtwitter',
|
||||
github_username: 'mrpbgithub',
|
||||
website_url: 'http://mrpeanutbutter.com',
|
||||
location: 'Los Angeles, DO(G)',
|
||||
profile_image: 'https://media.giphy.com/media/xThuW6sWCGbpZMpX7a/giphy.gif',
|
||||
};
|
||||
|
||||
const channel = { channel_type: 'direct', id: 2 };
|
||||
|
||||
const getUserDetails = user => (
|
||||
<UserDetails user={user} activeChannel={channel} activeChannelId={2} />
|
||||
);
|
||||
|
||||
describe('<UserDetails />', () => {
|
||||
describe('for user1', () => {
|
||||
it('should render and test snapshot', () => {
|
||||
const tree = render(getUserDetails(user1));
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the appropriate elements, attributes and values', () => {
|
||||
const context = shallow(getUserDetails(user1));
|
||||
expect(
|
||||
context.find('.activechatchannel__activecontentuserdetails').exists(),
|
||||
).toEqual(true); // only class to check for
|
||||
const parentDiv = context.find('div').at(0);
|
||||
|
||||
expect(parentDiv.childAt(0)).toEqual(context.find('img').at(0));
|
||||
expect(
|
||||
context
|
||||
.find('img')
|
||||
.at(0)
|
||||
.attr('src'),
|
||||
).toEqual(user1.profile_image); // profile pic
|
||||
|
||||
expect(parentDiv.childAt(1)).toEqual(context.find('h1'));
|
||||
expect(context.find('h1').text()).toEqual(user1.name); // user.name
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(0)
|
||||
.attr('href'),
|
||||
).toEqual(`/${user1.username}`); // user.username
|
||||
|
||||
expect(
|
||||
context
|
||||
.find('.userdetails__blockreport')
|
||||
.at(0)
|
||||
.children()[0],
|
||||
).toEqual('');
|
||||
|
||||
// social links
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(1)
|
||||
.attr('href'),
|
||||
).toEqual(`https://twitter.com/${user1.twitter_username}`); // twitter
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(2)
|
||||
.attr('href'),
|
||||
).toEqual(`https://github.com/${user1.github_username}`); // github
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(3)
|
||||
.attr('href'),
|
||||
).toEqual(user1.website_url); // website
|
||||
|
||||
expect(parentDiv.childAt(3)).toEqual(context.find('div').at(2));
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(2)
|
||||
.text(),
|
||||
).toEqual(user1.summary); // user.summary
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(6)
|
||||
.text(),
|
||||
).toEqual(user1.location); // user.location
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(8)
|
||||
.text(),
|
||||
).toEqual(user1.joined_at); // user.joined_at
|
||||
});
|
||||
});
|
||||
|
||||
describe('for user2', () => {
|
||||
it('should render and test snapshot', () => {
|
||||
const tree = render(getUserDetails(user2));
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the appropriate elements, attributes and values', () => {
|
||||
const context = shallow(getUserDetails(user2));
|
||||
expect(
|
||||
context.find('.activechatchannel__activecontentuserdetails').exists(),
|
||||
).toEqual(true); // only class to check for
|
||||
const parentDiv = context.find('div').at(0);
|
||||
|
||||
expect(parentDiv.childAt(0)).toEqual(context.find('img').at(0));
|
||||
expect(
|
||||
context
|
||||
.find('img')
|
||||
.at(0)
|
||||
.attr('src'),
|
||||
).toEqual(user2.profile_image); // profile pic
|
||||
|
||||
expect(parentDiv.childAt(1)).toEqual(context.find('h1'));
|
||||
expect(context.find('h1').text()).toEqual(user2.name); // user.name
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(0)
|
||||
.attr('href'),
|
||||
).toEqual(`/${user2.username}`); // user.username
|
||||
|
||||
expect(
|
||||
parentDiv
|
||||
.find('.userdetails__blockreport')
|
||||
.at(0)
|
||||
.childAt(0)
|
||||
.text(),
|
||||
).toEqual('Block User');
|
||||
|
||||
expect(
|
||||
parentDiv
|
||||
.find('.userdetails__blockreport')
|
||||
.at(0)
|
||||
.childAt(1)
|
||||
.text(),
|
||||
).toEqual('Report Abuse');
|
||||
|
||||
// social links
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(1)
|
||||
.attr('href'),
|
||||
).toEqual(`https://twitter.com/${user2.twitter_username}`); // twitter
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(2)
|
||||
.attr('href'),
|
||||
).toEqual(`https://github.com/${user2.github_username}`); // github
|
||||
expect(
|
||||
context
|
||||
.find('a')
|
||||
.at(3)
|
||||
.attr('href'),
|
||||
).toEqual(user2.website_url); // website
|
||||
|
||||
expect(parentDiv.childAt(3)).toEqual(context.find('div').at(2));
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(2)
|
||||
.text(),
|
||||
).toEqual(user2.summary); // user.summary
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(6)
|
||||
.text(),
|
||||
).toEqual(user2.location); // user.location
|
||||
expect(
|
||||
context
|
||||
.find('div')
|
||||
.at(8)
|
||||
.text(),
|
||||
).toEqual(user2.joined_at); // user.joined_at
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,139 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import render from 'preact-render-to-json';
|
||||
import { deep } from 'preact-render-spy';
|
||||
import View from '../view';
|
||||
|
||||
let exited = false;
|
||||
let accepted = false;
|
||||
let declined = false;
|
||||
|
||||
const onViewExitFake = () => {
|
||||
exited = true;
|
||||
};
|
||||
|
||||
const handleInvitationAcceptFake = () => {
|
||||
accepted = true;
|
||||
};
|
||||
|
||||
const handleInvitationDeclineFake = () => {
|
||||
declined = true;
|
||||
};
|
||||
|
||||
const sampleChannel = [
|
||||
{
|
||||
channel_name: 'name',
|
||||
description: 'some description',
|
||||
membership_id: '12345',
|
||||
},
|
||||
];
|
||||
|
||||
const getView = channel => (
|
||||
<View
|
||||
channels={channel}
|
||||
onViewExit={onViewExitFake}
|
||||
onAcceptInvitation={handleInvitationAcceptFake}
|
||||
onDeclineInvitation={handleInvitationDeclineFake}
|
||||
/>
|
||||
);
|
||||
|
||||
describe('<View />', () => {
|
||||
it('should render and test snapshot (no channel)', () => {
|
||||
const tree = render(getView([]));
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should render and test snapshot (with channel)', () => {
|
||||
const tree = deep(getView(sampleChannel), { depth: 2 });
|
||||
expect(tree).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should have the proper attributes and text values (no channel provided)', () => {
|
||||
const context = deep(getView([]), { depth: 2 });
|
||||
expect(context.find('.chatNonChatView').exists()).toEqual(true);
|
||||
expect(context.find('.container').exists()).toEqual(true);
|
||||
|
||||
expect(context.find('.chatNonChatView_exitbutton').exists()).toEqual(true);
|
||||
expect(context.find('.chatNonChatView_exitbutton').text()).toEqual('×');
|
||||
|
||||
expect(context.find('h1').exists()).toEqual(true);
|
||||
expect(context.find('h1').text()).toEqual('Channel Invitations 🤗');
|
||||
|
||||
expect(context.find('.chatNonChatView_contentblock').exists()).toEqual(
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
it('should have the proper attributes and text values (with channel provided)', () => {
|
||||
const context = deep(getView(sampleChannel), { depth: 2 });
|
||||
expect(context.find('.chatNonChatView_contentblock').exists()).toEqual(
|
||||
true,
|
||||
);
|
||||
|
||||
expect(context.find('h2').exists()).toEqual(true);
|
||||
expect(context.find('h2').text()).toEqual('name');
|
||||
|
||||
expect(context.find('em').exists()).toEqual(true);
|
||||
expect(context.find('em').text()).toEqual('some description');
|
||||
|
||||
expect(context.find('.cta').exists()).toEqual(true);
|
||||
expect(
|
||||
context
|
||||
.find('.cta')
|
||||
.at(0)
|
||||
.attr('data-content'),
|
||||
).toEqual('12345'); // accept button
|
||||
expect(
|
||||
context
|
||||
.find('.cta')
|
||||
.at(0)
|
||||
.text(),
|
||||
).toEqual('Accept'); // accept button
|
||||
expect(
|
||||
context
|
||||
.find('.cta')
|
||||
.at(1)
|
||||
.attr('data-content'),
|
||||
).toEqual('12345'); // decline button
|
||||
expect(
|
||||
context
|
||||
.find('.cta')
|
||||
.at(1)
|
||||
.text(),
|
||||
).toEqual('Decline'); // accept button
|
||||
});
|
||||
|
||||
it('should trigger exit', () => {
|
||||
const context = deep(getView([]), { depth: 2 });
|
||||
context.find('.chatNonChatView_exitbutton').simulate('click');
|
||||
expect(exited).toEqual(true);
|
||||
exited = false;
|
||||
});
|
||||
|
||||
it('should trigger accept', () => {
|
||||
const context = deep(getView(sampleChannel), { depth: 2 });
|
||||
context
|
||||
.find('.cta')
|
||||
.at(0)
|
||||
.simulate('click'); // click accept button
|
||||
|
||||
expect(exited).toEqual(false);
|
||||
expect(accepted).toEqual(true);
|
||||
expect(declined).toEqual(false);
|
||||
|
||||
accepted = false;
|
||||
});
|
||||
|
||||
it('should trigger decline', () => {
|
||||
const context = deep(getView(sampleChannel), { depth: 2 });
|
||||
context
|
||||
.find('.cta')
|
||||
.at(1)
|
||||
.simulate('click'); // click decline button
|
||||
|
||||
expect(exited).toEqual(false);
|
||||
expect(accepted).toEqual(false);
|
||||
expect(declined).toEqual(true);
|
||||
|
||||
declined = false;
|
||||
});
|
||||
});
|
||||
|
|
@ -21,7 +21,6 @@ import Compose from './compose';
|
|||
import Message from './message';
|
||||
import Content from './content';
|
||||
import Video from './video';
|
||||
import View from './view';
|
||||
|
||||
import setupPusher from '../src/utils/pusher';
|
||||
import debounceAction from '../src/utils/debounceAction';
|
||||
|
|
@ -67,7 +66,6 @@ export default class Chat extends Component {
|
|||
activeVideoChannelId: null,
|
||||
incomingVideoCallChannelIds: [],
|
||||
videoCallParticipants: [],
|
||||
nonChatView: null,
|
||||
inviteChannels: [],
|
||||
soundOn: true,
|
||||
videoOn: true,
|
||||
|
|
@ -676,7 +674,12 @@ export default class Chat extends Component {
|
|||
};
|
||||
|
||||
triggerSwitchChannel = (id, slug) => {
|
||||
const { chatChannels, isMobileDevice, unopenedChannelIds } = this.state;
|
||||
const {
|
||||
chatChannels,
|
||||
isMobileDevice,
|
||||
unopenedChannelIds,
|
||||
activeChannelId,
|
||||
} = this.state;
|
||||
const newUnopenedChannelIds = unopenedChannelIds;
|
||||
const index = newUnopenedChannelIds.indexOf(id);
|
||||
if (index > -1) {
|
||||
|
|
@ -692,6 +695,17 @@ export default class Chat extends Component {
|
|||
),
|
||||
});
|
||||
this.setupChannel(id);
|
||||
const params = new URLSearchParams(window.location.search);
|
||||
|
||||
if (params.get('ref') === 'group_invite') {
|
||||
this.setActiveContentState(activeChannelId, {
|
||||
type_of: 'loading-post',
|
||||
});
|
||||
this.setActiveContent({
|
||||
path: '/chat_channel_memberships',
|
||||
type_of: 'article',
|
||||
});
|
||||
}
|
||||
window.history.replaceState(null, null, `/connect/${slug}`);
|
||||
if (!isMobileDevice) {
|
||||
document.getElementById('messageform').focus();
|
||||
|
|
@ -765,7 +779,6 @@ export default class Chat extends Component {
|
|||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const { target } = e;
|
||||
const content =
|
||||
target.dataset.content || target.parentElement.dataset.content;
|
||||
|
|
@ -773,7 +786,7 @@ export default class Chat extends Component {
|
|||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
|
||||
const { activeChannelId } = this.state;
|
||||
const { activeChannelId, activeChannel } = this.state;
|
||||
if (target.dataset.content.startsWith('chat_channels/')) {
|
||||
this.setActiveContentState(activeChannelId, {
|
||||
type_of: 'loading-user',
|
||||
|
|
@ -783,6 +796,14 @@ export default class Chat extends Component {
|
|||
this.setActiveContent,
|
||||
null,
|
||||
);
|
||||
} else if (target.dataset.content === 'sidecar_all') {
|
||||
this.setActiveContentState(activeChannelId, {
|
||||
type_of: 'loading-post',
|
||||
});
|
||||
this.setActiveContent({
|
||||
path: `/chat_channel_memberships/${activeChannel.id}/edit`,
|
||||
type_of: 'article',
|
||||
});
|
||||
} else if (
|
||||
content.startsWith('sidecar') ||
|
||||
content.startsWith('article')
|
||||
|
|
@ -866,14 +887,6 @@ export default class Chat extends Component {
|
|||
getChannels(filterQuery, null, this.props, 0, filters, this.loadChannels);
|
||||
};
|
||||
|
||||
triggerNonChatView = e => {
|
||||
this.setState({ nonChatView: e.target.dataset.content });
|
||||
};
|
||||
|
||||
triggerExitView = () => {
|
||||
this.setState({ nonChatView: null });
|
||||
};
|
||||
|
||||
handleFailure = err => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(err);
|
||||
|
|
@ -1012,13 +1025,17 @@ export default class Chat extends Component {
|
|||
if (state.inviteChannels.length > 0) {
|
||||
invitesButton = (
|
||||
<div className="chat__channelinvitationsindicator">
|
||||
<button
|
||||
onClick={this.triggerNonChatView}
|
||||
data-content="invitations"
|
||||
<a
|
||||
href="/chat_channel_memberships"
|
||||
onClick={this.triggerActiveContent}
|
||||
data-content="sidecar-chat_channel_memberships"
|
||||
type="button"
|
||||
>
|
||||
<span role="img" aria-label="emoji">
|
||||
👋
|
||||
</span>
|
||||
New Invitations!
|
||||
</button>
|
||||
</a>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -1273,9 +1290,9 @@ export default class Chat extends Component {
|
|||
let before = text.substring(0, start);
|
||||
before = text.substring(0, before.lastIndexOf('@') + 1);
|
||||
const after = text.substring(end, text.length);
|
||||
el.value = before + name + after;
|
||||
el.selectionStart = start + name.length;
|
||||
el.selectionEnd = start + name.length;
|
||||
el.value = `${before + name } ${ after}`;
|
||||
el.selectionStart = start + name.length + 1;
|
||||
el.selectionEnd = start + name.length + 1;
|
||||
el.focus();
|
||||
this.setState({ showMemberlist: false });
|
||||
};
|
||||
|
|
@ -1441,7 +1458,7 @@ export default class Chat extends Component {
|
|||
};
|
||||
|
||||
renderChannelHeaderInner = () => {
|
||||
const { activeChannel, activeChannelId } = this.state;
|
||||
const { activeChannel } = this.state;
|
||||
if (activeChannel.channel_type === 'direct') {
|
||||
return (
|
||||
<a
|
||||
|
|
@ -1455,9 +1472,9 @@ export default class Chat extends Component {
|
|||
}
|
||||
return (
|
||||
<a
|
||||
href={`/connect/${activeChannel.channel_modified_slug}`}
|
||||
href={`/chat_channel_memberships/${activeChannel.id}/edit`}
|
||||
onClick={this.triggerActiveContent}
|
||||
data-content={`chat_channels/${activeChannelId}`}
|
||||
data-content="sidecar-chat_channel_membership"
|
||||
>
|
||||
{activeChannel.channel_name}
|
||||
</a>
|
||||
|
|
@ -1466,7 +1483,6 @@ export default class Chat extends Component {
|
|||
|
||||
renderChannelConfigImage = () => {
|
||||
const { activeContent, activeChannel, activeChannelId } = this.state;
|
||||
|
||||
if (
|
||||
activeContent[activeChannelId] &&
|
||||
activeContent[activeChannelId].type_of
|
||||
|
|
@ -1477,7 +1493,12 @@ export default class Chat extends Component {
|
|||
const dataContent =
|
||||
activeChannel.channel_type === 'direct'
|
||||
? 'sidecar-user'
|
||||
: `chat_channels/${activeChannelId}`;
|
||||
: `sidecar-chat_channel_membership`;
|
||||
|
||||
const path =
|
||||
activeChannel.channel_type === 'direct'
|
||||
? `/${activeChannel.channel_username}`
|
||||
: `/chat_channel_memberships/${activeChannel.id}/edit`;
|
||||
|
||||
return (
|
||||
<a
|
||||
|
|
@ -1487,7 +1508,7 @@ export default class Chat extends Component {
|
|||
if (e.keyCode === 13) this.triggerActiveContent(e);
|
||||
}}
|
||||
tabIndex="0"
|
||||
href={`/${activeChannel.channel_username}`}
|
||||
href={path}
|
||||
data-content={dataContent}
|
||||
>
|
||||
<img
|
||||
|
|
@ -1551,17 +1572,6 @@ export default class Chat extends Component {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
let nonChatView = '';
|
||||
if (state.nonChatView) {
|
||||
nonChatView = (
|
||||
<View
|
||||
channels={state.inviteChannels}
|
||||
onViewExit={this.triggerExitView}
|
||||
onAcceptInvitation={this.handleInvitationAccept}
|
||||
onDeclineInvitation={this.handleInvitationDecline}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div
|
||||
className={`chat chat--${
|
||||
|
|
@ -1572,7 +1582,6 @@ export default class Chat extends Component {
|
|||
{this.renderChatChannels()}
|
||||
<div className="chat__activechat">
|
||||
{vid}
|
||||
{nonChatView}
|
||||
{this.renderActiveChatChannel(channelHeader, incomingCall)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,49 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class CodeEditor extends Component {
|
||||
static propTypes = {
|
||||
activeChannelId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
componentDidMount() {
|
||||
const { activeChannelId } = this.props;
|
||||
import('codemirror').then(CodeMirror => {
|
||||
const editor = document.getElementById('codeeditor');
|
||||
const myCodeMirror = CodeMirror(editor, {
|
||||
mode: 'javascript',
|
||||
theme: 'material',
|
||||
autofocus: true,
|
||||
});
|
||||
myCodeMirror.setSize('100%', '100%');
|
||||
// Initial trigger:
|
||||
const channel = window.pusher.channel(
|
||||
`presence-channel-${activeChannelId}`,
|
||||
);
|
||||
channel.trigger('client-livecode', {
|
||||
context: 'initializing-live-code-channel',
|
||||
channel: `presence-channel-${activeChannelId}`,
|
||||
});
|
||||
// Coding trigger:
|
||||
myCodeMirror.on('keyup', cm => {
|
||||
channel.trigger('client-livecode', {
|
||||
keyPressed: true,
|
||||
value: cm.getValue(),
|
||||
cursorPos: cm.getCursor(),
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
shouldComponentUpdate() {
|
||||
return false;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div id="codeeditor" className="chatcodeeditor">
|
||||
<div className="chatcodeeditor__header">Experimental (WIP)</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,8 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import CodeEditor from './codeEditor';
|
||||
import GithubRepo from './githubRepo';
|
||||
import ChannelDetails from './channelDetails';
|
||||
import UserDetails from './userDetails';
|
||||
import Article from './article';
|
||||
|
||||
|
||||
export default class Content extends Component {
|
||||
static propTypes = {
|
||||
resource: PropTypes.object,
|
||||
|
|
@ -50,55 +47,7 @@ function display(props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
if (props.resource.type_of === 'loading-user') {
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
height: '25px',
|
||||
width: '96%',
|
||||
margin: ' 8px auto',
|
||||
display: 'block',
|
||||
backgroundColor: '#f5f6f7',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (props.resource.type_of === 'user') {
|
||||
return (
|
||||
<UserDetails
|
||||
user={props.resource}
|
||||
activeChannelId={props.activeChannelId}
|
||||
activeChannel={props.activeChannel}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (props.resource.type_of === 'article') {
|
||||
return <Article resource={props.resource} />;
|
||||
}
|
||||
if (props.resource.type_of === 'github') {
|
||||
return (
|
||||
<GithubRepo
|
||||
activeChannelId={props.activeChannelId}
|
||||
pusherKey={props.pusherKey}
|
||||
githubToken={props.githubToken}
|
||||
resource={props.resource}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (props.resource.type_of === 'chat_channel') {
|
||||
return (
|
||||
<ChannelDetails
|
||||
channel={props.resource}
|
||||
activeChannelId={props.activeChannelId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (props.resource.type_of === 'code_editor') {
|
||||
return (
|
||||
<CodeEditor
|
||||
activeChannelId={props.activeChannelId}
|
||||
pusherKey={props.pusherKey}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,185 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
import marked from 'marked';
|
||||
import { getJSONContents } from './actions';
|
||||
|
||||
export default class GithubRepo extends Component {
|
||||
static propTypes = {
|
||||
githubToken: PropTypes.string.isRequired,
|
||||
resource: PropTypes.shape({
|
||||
args: PropTypes.string,
|
||||
}).isRequired,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
root: true,
|
||||
directories: [],
|
||||
files: [],
|
||||
readme: null,
|
||||
content: null,
|
||||
token: props.githubToken,
|
||||
path: null,
|
||||
};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
const { token } = this.state;
|
||||
const { resource } = this.props;
|
||||
if (token) {
|
||||
getJSONContents(
|
||||
`https://api.github.com/repos/${resource.args}/contents?access_token=${token}`,
|
||||
this.loadContent,
|
||||
this.loadFailure,
|
||||
);
|
||||
getJSONContents(
|
||||
`https://api.github.com/repos/${resource.args}/readme?access_token=${token}`,
|
||||
this.loadContent,
|
||||
this.loadFailure,
|
||||
);
|
||||
}
|
||||
this.setState({ path: resource.args });
|
||||
}
|
||||
|
||||
handleItemClick = e => {
|
||||
const { token } = this.state;
|
||||
e.preventDefault();
|
||||
getJSONContents(
|
||||
`${e.target.dataset.apiUrl}&access_token=${token}`,
|
||||
this.loadContent,
|
||||
this.loadFailure,
|
||||
);
|
||||
this.setState({
|
||||
root: false,
|
||||
path: e.target.dataset.path,
|
||||
});
|
||||
};
|
||||
|
||||
loadContent = response => {
|
||||
const files = [];
|
||||
const directories = [];
|
||||
if (response.message === 'Not Found') {
|
||||
this.setState({ path: 'Repo not found (misspelled or private?)' });
|
||||
} else if (Array.isArray(response)) {
|
||||
response.forEach(item => {
|
||||
if (item.type === 'file') {
|
||||
files.push(item);
|
||||
} else {
|
||||
directories.push(item);
|
||||
}
|
||||
});
|
||||
this.setState({
|
||||
files,
|
||||
directories,
|
||||
});
|
||||
} else if (response.path === 'README.md') {
|
||||
this.setState({
|
||||
readme: window.atob(response.content),
|
||||
});
|
||||
} else if (response.content) {
|
||||
this.setState({
|
||||
content: window.atob(response.content),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
loadFailure = response => {
|
||||
this.setState({ path: response.message });
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
token,
|
||||
content,
|
||||
path,
|
||||
directories: directoriesFromState,
|
||||
files: filesFromState,
|
||||
readme: readmeFromState,
|
||||
root,
|
||||
} = this.state;
|
||||
if (!token || token.length === 0) {
|
||||
return (
|
||||
<div className="activecontent__githubrepo">
|
||||
<div className="activecontent__githubrepoheader">
|
||||
<em>Authentication required</em>
|
||||
</div>
|
||||
<p>This feature is in internal alpha testing mode.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (content) {
|
||||
return (
|
||||
<div className="activecontent__githubrepo">
|
||||
<div className="activecontent__githubrepoheader">{path}</div>
|
||||
<pre>{content}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const directories = directoriesFromState.map(item => (
|
||||
<div className="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href={item.html_url}
|
||||
data-api-url={item.url}
|
||||
data-path={item.path}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
<span role="img" aria-label="folder-emoji">
|
||||
📁
|
||||
</span>
|
||||
{' '}
|
||||
{item.name}
|
||||
</a>
|
||||
</div>
|
||||
));
|
||||
const files = filesFromState.map(item => (
|
||||
<div className="activecontent__githubrepofilerow">
|
||||
<a
|
||||
href={item.html_url}
|
||||
data-api-url={item.url}
|
||||
data-path={item.path}
|
||||
onClick={this.handleItemClick}
|
||||
>
|
||||
{item.name}
|
||||
</a>
|
||||
</div>
|
||||
));
|
||||
let readme = '';
|
||||
if (readmeFromState) {
|
||||
readme = (
|
||||
<div
|
||||
className="activecontent__githubreporeadme"
|
||||
// eslint-disable-next-line react/no-danger
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: marked(readmeFromState),
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (root) {
|
||||
return (
|
||||
<div className="activecontent__githubrepo">
|
||||
<div className="activecontent__githubrepoheader">
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-has-content */}
|
||||
<a href="/Users/benhalpern/dev/dev.to_core/app" />
|
||||
{path}
|
||||
</div>
|
||||
<div className="activecontent__githubrepofiles activecontent__githubrepofiles--root">
|
||||
{directories}
|
||||
{files}
|
||||
</div>
|
||||
{readme}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="activecontent__githubrepo">
|
||||
<div className="activecontent__githubrepoheader">{path}</div>
|
||||
<div className="activecontent__githubrepofiles">
|
||||
{directories}
|
||||
{files}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,271 +0,0 @@
|
|||
import { h } from 'preact';
|
||||
import { PropTypes } from 'prop-types';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import twitterImage from 'images/twitter-logo.svg';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import githubImage from 'images/github-logo.svg';
|
||||
// eslint-disable-next-line import/no-unresolved
|
||||
import websiteImage from 'images/external-link-logo.svg';
|
||||
|
||||
function blockUser(blockedUserId) {
|
||||
const body = {
|
||||
user_block: {
|
||||
blocked_id: blockedUserId,
|
||||
},
|
||||
};
|
||||
|
||||
getCsrfToken().then(sendFetch('block-user', JSON.stringify(body)));
|
||||
}
|
||||
|
||||
const setUpButton = ({ modalId = '', otherModalId = '', btnName = '' }) => {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const modal = document.getElementById(`${modalId}`);
|
||||
const otherModal = document.getElementById(`${otherModalId}`);
|
||||
otherModal.style.display = 'none';
|
||||
if (modal.style.display === 'none') {
|
||||
modal.style.display = 'block';
|
||||
window.location.href = `#${modalId}`;
|
||||
} else {
|
||||
modal.style.display = 'none';
|
||||
window.location.href = `#`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{btnName}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
setUpButton.propTypes = {
|
||||
modalId: PropTypes.string.isRequired,
|
||||
otherModalId: PropTypes.string.isRequired,
|
||||
btnName: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
const userDetailsConfig = {
|
||||
twitter_username: {
|
||||
hostUrl: 'https://twitter.com/',
|
||||
srcImage: twitterImage,
|
||||
imageAltText: 'twitter logo',
|
||||
},
|
||||
github_username: {
|
||||
hostUrl: 'https://github.com/',
|
||||
srcImage: githubImage,
|
||||
imageAltText: 'github logo',
|
||||
},
|
||||
website_url: {
|
||||
className: 'external-link-img',
|
||||
hostUrl: '',
|
||||
srcImage: websiteImage,
|
||||
imageAltText: 'external link icon',
|
||||
},
|
||||
};
|
||||
|
||||
const UserDetails = ({ user, activeChannelId, activeChannel }) => {
|
||||
const channelId = activeChannelId;
|
||||
const channel = activeChannel || {};
|
||||
const socialIcons = [];
|
||||
const userMeta = ['twitter_username', 'github_username', 'website_url'];
|
||||
userMeta.forEach(metaProp => {
|
||||
if (user[metaProp]) {
|
||||
const { className, hostUrl, srcImage, imageAltText } = userDetailsConfig[
|
||||
metaProp
|
||||
];
|
||||
socialIcons.push(
|
||||
<a
|
||||
href={`${hostUrl}${user[metaProp]}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<img
|
||||
className={className}
|
||||
src={srcImage}
|
||||
style={{ width: '30px', margin: '5px 15px 15px 0px' }}
|
||||
alt={imageAltText}
|
||||
/>
|
||||
</a>,
|
||||
);
|
||||
}
|
||||
});
|
||||
let userLocation = '';
|
||||
if (user.location && user.location.length > 0) {
|
||||
userLocation = (
|
||||
<div>
|
||||
<div className="key">location</div>
|
||||
<div className="value">{user.location}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
let blockButton = '';
|
||||
if (channel.channel_type === 'direct' && window.currentUser.id !== user.id) {
|
||||
blockButton = setUpButton({
|
||||
modalId: 'userdetails__blockmsg',
|
||||
otherModalId: 'userdetails__reportabuse',
|
||||
btnName: 'Block User',
|
||||
});
|
||||
}
|
||||
|
||||
let reportButton = '';
|
||||
if (window.currentUser.id !== user.id) {
|
||||
reportButton = setUpButton({
|
||||
modalId: 'userdetails__reportabuse',
|
||||
otherModalId: 'userdetails__blockmsg',
|
||||
btnName: 'Report Abuse',
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<img
|
||||
src={user.profile_image}
|
||||
alt={`${user.username} profile`}
|
||||
style={{
|
||||
height: '210px',
|
||||
width: '210px',
|
||||
margin: ' 15px auto',
|
||||
display: 'block',
|
||||
borderRadius: '500px',
|
||||
}}
|
||||
/>
|
||||
<h1 style={{ textAlign: 'center' }}>
|
||||
<a href={`/${user.username}`} target="_blank" rel="noopener noreferrer">
|
||||
{user.name}
|
||||
</a>
|
||||
</h1>
|
||||
<div style={{ height: '50px', margin: 'auto', width: '96%' }}>
|
||||
{socialIcons}
|
||||
</div>
|
||||
<div style={{ fontStyle: 'italic' }}>{user.summary}</div>
|
||||
<div className="activechatchannel__activecontentuserdetails">
|
||||
{userLocation}
|
||||
<div className="key">joined</div>
|
||||
<div className="value">{user.joined_at}</div>
|
||||
</div>
|
||||
<div className="userdetails__blockreport">
|
||||
{blockButton}
|
||||
{reportButton}
|
||||
</div>
|
||||
<div id="userdetails__reportabuse" style={{ display: 'none' }}>
|
||||
<div className="userdetails__reportabuse">
|
||||
<p>Reporting abuse will: </p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with
|
||||
you
|
||||
</li>
|
||||
<li>
|
||||
give the DEV team your consent to read messages in this chat to
|
||||
understand your report and take appropriate action
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented
|
||||
across DEV yet.
|
||||
</p>
|
||||
<h5>Are you sure?</h5>
|
||||
<a
|
||||
tabIndex="0"
|
||||
href="/report-abuse"
|
||||
onClick={() => {
|
||||
blockUser(channelId);
|
||||
}}
|
||||
>
|
||||
Yes, Report
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="no"
|
||||
onClick={() => {
|
||||
document.getElementById(
|
||||
'userdetails__reportabuse',
|
||||
).style.display = 'none';
|
||||
window.location.href = `#`;
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) {
|
||||
document.getElementById(
|
||||
'userdetails__reportabuse',
|
||||
).style.display = 'none';
|
||||
window.location.href = `#`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div id="userdetails__blockmsg" style={{ display: 'none' }}>
|
||||
<div className="userdetails__blockmsg">
|
||||
<p>Blocking on connect will: </p>
|
||||
<ul>
|
||||
<li>
|
||||
close this chat and prevent this user from re-opening chat with
|
||||
you
|
||||
</li>
|
||||
<li>
|
||||
NOT notify the user you will block--this channel will become
|
||||
inaccessible for both users
|
||||
</li>
|
||||
</ul>
|
||||
<p>
|
||||
Blocking is only on Connect right now and has not been implemented
|
||||
across DEV yet. Consider reporting abuse to the DEV team if this
|
||||
user is spamming or harassing elsewhere on dev.to, so we can take
|
||||
further action.
|
||||
</p>
|
||||
<h5>Are you sure?</h5>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
onClick={() => {
|
||||
blockUser(user.id);
|
||||
window.location.href = `/connect`;
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) {
|
||||
blockUser(user.id);
|
||||
window.location.href = `/connect`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
Yes, Block
|
||||
</a>
|
||||
{/* eslint-disable-next-line jsx-a11y/anchor-is-valid */}
|
||||
<a
|
||||
role="button"
|
||||
tabIndex="0"
|
||||
className="no"
|
||||
onClick={() => {
|
||||
document.getElementById('userdetails__blockmsg').style.display =
|
||||
'none';
|
||||
window.location.href = `#`;
|
||||
}}
|
||||
onKeyUp={e => {
|
||||
if (e.keyCode === 13) {
|
||||
document.getElementById('userdetails__blockmsg').style.display =
|
||||
'none';
|
||||
window.location.href = `#`;
|
||||
}
|
||||
}}
|
||||
>
|
||||
No
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
UserDetails.propTypes = {
|
||||
user: PropTypes.objectOf().isRequired,
|
||||
activeChannelId: PropTypes.string.isRequired,
|
||||
activeChannel: PropTypes.objectOf().isRequired,
|
||||
};
|
||||
|
||||
export default UserDetails;
|
||||
|
|
@ -1,85 +0,0 @@
|
|||
import { h, Component } from 'preact';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
* TODO: Instead of calling this function in render, use jsx (<SetupButton />).
|
||||
*/
|
||||
function SetupButton({ className, onClickCallback, dataContent, btnLabel }) {
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
className={className}
|
||||
onClick={onClickCallback}
|
||||
data-content={dataContent}
|
||||
>
|
||||
{btnLabel}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
SetupButton.propTypes = {
|
||||
className: PropTypes.string.isRequired,
|
||||
onClickCallback: PropTypes.func.isRequired,
|
||||
dataContent: PropTypes.string.isRequired,
|
||||
btnLabel: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default class View extends Component {
|
||||
static propTypes = {
|
||||
onAcceptInvitation: PropTypes.func.isRequired,
|
||||
onDeclineInvitation: PropTypes.func.isRequired,
|
||||
onViewExit: PropTypes.func.isRequired,
|
||||
channels: PropTypes.arrayOf(PropTypes.object).isRequired,
|
||||
};
|
||||
|
||||
channel = props => {
|
||||
const { onAcceptInvitation, onDeclineInvitation } = this.props;
|
||||
return (
|
||||
<div className="chatNonChatView_contentblock">
|
||||
<h2>{props.channel.channel_name}</h2>
|
||||
<div>
|
||||
<em>{props.channel.description}</em>
|
||||
</div>
|
||||
{SetupButton({
|
||||
className: 'cta',
|
||||
onClickCallback: onAcceptInvitation,
|
||||
dataContent: props.channel.membership_id,
|
||||
btnLabel: 'Accept',
|
||||
})}
|
||||
{SetupButton({
|
||||
className: 'cta',
|
||||
onClickCallback: onDeclineInvitation,
|
||||
dataContent: props.channel.membership_id,
|
||||
btnLabel: 'Decline',
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
render() {
|
||||
const { onViewExit, channels: channelsFromProps } = this.props;
|
||||
const channels = channelsFromProps.map(channel => {
|
||||
return <this.channel channel={channel} />;
|
||||
});
|
||||
return (
|
||||
<div className="chatNonChatView">
|
||||
<div className="container">
|
||||
{SetupButton({
|
||||
className: 'chatNonChatView_exitbutton',
|
||||
onClickCallback: onViewExit,
|
||||
dataContent: 'exit',
|
||||
btnLabel: '×',
|
||||
})}
|
||||
<h1>
|
||||
Channel Invitations
|
||||
{' '}
|
||||
<span role="img" aria-label="hugging-emoji">
|
||||
🤗
|
||||
</span>
|
||||
</h1>
|
||||
{channels}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -26,7 +26,11 @@ module AssignTagModerator
|
|||
if tag.mod_chat_channel_id
|
||||
ChatChannel.find(tag.mod_chat_channel_id).add_users(user) if user.chat_channels.where(id: tag.mod_chat_channel_id).none?
|
||||
else
|
||||
channel = ChatChannel.create_with_users(([user] + User.with_role(:mod_relations_admin)).flatten.uniq, "invite_only", "##{tag.name} mods")
|
||||
channel = ChatChannel.create_with_users(
|
||||
users: ([user] + User.with_role(:mod_relations_admin)).flatten.uniq,
|
||||
channel_type: "invite_only",
|
||||
contrived_name: "##{tag.name} mods",
|
||||
)
|
||||
tag.update_column(:mod_chat_channel_id, channel.id)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -81,6 +81,17 @@ class NotifyMailer < ApplicationMailer
|
|||
mail(to: @user.email, subject: subject)
|
||||
end
|
||||
|
||||
def channel_invite_email(membership, inviter)
|
||||
@membership = membership
|
||||
@inviter = inviter
|
||||
subject = if @membership.role == "mod"
|
||||
"You are invited to Channel #{@membership.chat_channel.channel_name} as moderator."
|
||||
else
|
||||
"You are invited to Channel #{@membership.chat_channel.channel_name} by #{@inviter.name}."
|
||||
end
|
||||
mail(to: @membership.user.email, subject: subject)
|
||||
end
|
||||
|
||||
def account_deleted_email(user)
|
||||
@name = user.name
|
||||
subject = "dev.to - Account Deletion Confirmation"
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ class ChatChannel < ApplicationRecord
|
|||
validates :channel_type, presence: true, inclusion: { in: %w[open invite_only direct] }
|
||||
validates :status, presence: true, inclusion: { in: %w[active inactive blocked] }
|
||||
validates :slug, uniqueness: true, presence: true
|
||||
validates :description, length: { maximum: 200 }, allow_blank: true
|
||||
|
||||
def open?
|
||||
channel_type == "open"
|
||||
|
|
@ -51,34 +52,41 @@ class ChatChannel < ApplicationRecord
|
|||
chat_channel_memberships.where(user_id: user.id).pluck(:last_opened_at).first
|
||||
end
|
||||
|
||||
def self.create_with_users(users, channel_type = "direct", contrived_name = "New Channel")
|
||||
raise "Invalid direct channel" if users.size != 2 && channel_type == "direct"
|
||||
class << self
|
||||
def create_with_users(users:, channel_type: "direct", contrived_name: "New Channel", membership_role: "member")
|
||||
raise "Invalid direct channel" if users.size != 2 && channel_type == "direct"
|
||||
|
||||
if channel_type == "direct"
|
||||
usernames = users.map(&:username).sort # .map as `users` is an array
|
||||
contrived_name = "Direct chat between " + usernames.join(" and ")
|
||||
slug = usernames.join("/")
|
||||
else
|
||||
slug = contrived_name.to_s.parameterize + "-" + rand(100_000).to_s(26)
|
||||
usernames = users.map(&:username).sort
|
||||
slug = channel_type == "direct" ? usernames.join("/") : contrived_name.to_s.parameterize + "-" + rand(100_000).to_s(26)
|
||||
contrived_name = "Direct chat between " + usernames.join(" and ") if channel_type == "direct"
|
||||
channel = find_or_create_chat_channel(channel_type, slug, contrived_name)
|
||||
if channel_type == "direct"
|
||||
channel.add_users(users)
|
||||
else
|
||||
channel.invite_users(users: users, membership_role: membership_role)
|
||||
end
|
||||
channel
|
||||
end
|
||||
|
||||
channel = ChatChannel.find_by(slug: slug)
|
||||
if channel
|
||||
raise "Blocked channel" if channel.status == "blocked"
|
||||
private
|
||||
|
||||
channel.status = "active"
|
||||
channel.save
|
||||
else
|
||||
channel = create(
|
||||
channel_type: channel_type,
|
||||
channel_name: contrived_name,
|
||||
slug: slug,
|
||||
last_message_at: 1.week.ago,
|
||||
status: "active",
|
||||
)
|
||||
channel.add_users(users)
|
||||
def find_or_create_chat_channel(channel_type, slug, contrived_name)
|
||||
channel = ChatChannel.find_by(slug: slug)
|
||||
if channel
|
||||
raise "Blocked channel" if channel.status == "blocked"
|
||||
|
||||
channel.status = "active"
|
||||
channel.save
|
||||
else
|
||||
create(
|
||||
channel_type: channel_type,
|
||||
channel_name: contrived_name,
|
||||
slug: slug,
|
||||
last_message_at: 1.week.ago,
|
||||
status: "active",
|
||||
)
|
||||
end
|
||||
end
|
||||
channel
|
||||
end
|
||||
|
||||
def add_users(users)
|
||||
|
|
@ -87,6 +95,26 @@ class ChatChannel < ApplicationRecord
|
|||
end
|
||||
end
|
||||
|
||||
def invite_users(users:, membership_role: "member", inviter: nil)
|
||||
invitation_sent = 0
|
||||
Array(users).each do |user|
|
||||
existing_membership = ChatChannelMembership.find_by(user_id: user.id, chat_channel_id: id)
|
||||
if existing_membership.present? && %w[active pending].exclude?(existing_membership.status)
|
||||
if existing_membership.update(status: "pending", role: membership_role)
|
||||
NotifyMailer.channel_invite_email(existing_membership, inviter).deliver_later
|
||||
invitation_sent += 1
|
||||
end
|
||||
else
|
||||
membership = ChatChannelMembership.create(user_id: user.id, chat_channel_id: id, role: membership_role, status: "pending")
|
||||
if membership.persisted?
|
||||
NotifyMailer.channel_invite_email(membership, inviter).deliver_later
|
||||
invitation_sent += 1
|
||||
end
|
||||
end
|
||||
end
|
||||
invitation_sent
|
||||
end
|
||||
|
||||
def remove_user(user)
|
||||
chat_channel_memberships.where(user: user).destroy_all
|
||||
end
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
class ChatChannelMembership < ApplicationRecord
|
||||
attr_accessor :invitation_usernames
|
||||
|
||||
include Searchable
|
||||
SEARCH_SERIALIZER = Search::ChatChannelMembershipSerializer
|
||||
SEARCH_CLASS = Search::ChatChannelMembership
|
||||
|
|
@ -8,7 +10,7 @@ class ChatChannelMembership < ApplicationRecord
|
|||
|
||||
validates :user_id, presence: true, uniqueness: { scope: :chat_channel_id }
|
||||
validates :chat_channel_id, presence: true, uniqueness: { scope: :user_id }
|
||||
validates :status, inclusion: { in: %w[active inactive pending rejected left_channel] }
|
||||
validates :status, inclusion: { in: %w[active inactive pending rejected left_channel removed_from_channel] }
|
||||
validates :role, inclusion: { in: %w[member mod] }
|
||||
validate :permission
|
||||
|
||||
|
|
|
|||
|
|
@ -82,7 +82,7 @@ class Message < ApplicationRecord
|
|||
HTML
|
||||
elsif username == "all" && chat_channel.channel_type == "invite_only"
|
||||
<<~HTML
|
||||
<a class='comment-mentioned-user comment-mentioned-all' data-content="chat_channels/#{chat_channel.id}" href='#' target="_blank">@#{username}</a>
|
||||
<a class='comment-mentioned-user comment-mentioned-all' data-content="sidecar_all" href="#">@#{username}</a>
|
||||
HTML
|
||||
else
|
||||
mention
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ class ChatChannelPolicy < ApplicationPolicy
|
|||
end
|
||||
|
||||
def permitted_attributes
|
||||
%i[channel_name slug command]
|
||||
%i[channel_name slug command description]
|
||||
end
|
||||
|
||||
def create_chat?
|
||||
|
|
|
|||
99
app/views/chat_channel_memberships/edit.html.erb
Normal file
99
app/views/chat_channel_memberships/edit.html.erb
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
<% if flash[:settings_notice].present? %>
|
||||
<div class="notice" id="notice">
|
||||
<%= flash[:settings_notice] %>
|
||||
</div>
|
||||
<% elsif flash[:error].present? %>
|
||||
<div class="notice error-notice" id="notice" style="text-align:center;color:red">
|
||||
ERROR
|
||||
<%= flash[:error] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="user-settings-page">
|
||||
<h1><%= @channel.channel_name %></h1>
|
||||
<% if @membership.role == "mod" %>
|
||||
<h3>You are a channel mod</h3>
|
||||
<% end %>
|
||||
<p>
|
||||
<%= @channel.description %>
|
||||
</p>
|
||||
<div class="settings-form">
|
||||
<div class="settings-form-userlist">
|
||||
<h3>Members</h3>
|
||||
<% @channel.active_memberships.includes(:user).each do |active_membership| %>
|
||||
<div class="settings-form-userlistrow" >
|
||||
<a href="<%= active_membership.user.path %>"><img src="<%= ProfileImage.new(active_membership.user).get(width: 90) %>" /><%= active_membership.user.name %></a>
|
||||
<% if @membership.role == "mod" && active_membership.role != "mod" %>
|
||||
<%= form_tag "/chat_channel_memberships/remove_membership", class: "inline-button-form-hidden" do %>
|
||||
<%= hidden_field_tag(:chat_channel_id, @channel.id) %>
|
||||
<%= hidden_field_tag(:membership_id, active_membership.id) %>
|
||||
<%= submit_tag "×" %>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @membership.role == "mod" %>
|
||||
<div class="settings-form-userlist">
|
||||
<h3>Pending Invitations</h3>
|
||||
<% @channel.pending_memberships.includes(:user).each do |pending_membership| %>
|
||||
<div class="settings-form-userlistrow" >
|
||||
<a href="<%= pending_membership.user.path %>"><img src="<%= ProfileImage.new(pending_membership.user).get(width: 90) %>" /><%= pending_membership.user.name %></a>
|
||||
<%= form_tag "/chat_channel_memberships/remove_membership", class: "inline-button-form" do %>
|
||||
<%= hidden_field_tag(:status, "pending") %>
|
||||
<%= hidden_field_tag(:chat_channel_id, @channel.id) %>
|
||||
<%= hidden_field_tag(:membership_id, pending_membership.id) %>
|
||||
<%= submit_tag "×" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
</div>
|
||||
<%= form_for ChatChannelMembership.new do |f| %>
|
||||
<div class="field">
|
||||
<%= f.hidden_field :chat_channel_id, value: @channel.id %>
|
||||
<%= f.label "Usernames to Invite" %>
|
||||
<%= f.text_field :invitation_usernames, placeholder: "Comma separated" %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= f.submit "SUBMIT", class: "cta" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<% if @membership.role == "mod" %>
|
||||
<hr class="settings-hr"/>
|
||||
<h1>Channel Settings</h1>
|
||||
<h3>Notifications</h3>
|
||||
<%= form_for(@channel) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label :description %>
|
||||
<%= f.text_area :description %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= f.submit "SUBMIT", class: "cta" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<hr class="settings-hr"/>
|
||||
<h1>Personal Settings</h1>
|
||||
<h3>Notifications</h3>
|
||||
<%= form_for(@membership) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.label :show_global_badge_notification, "Receive Notifications for New Messages" %>
|
||||
<%= f.check_box :show_global_badge_notification %>
|
||||
</div>
|
||||
<div class="field">
|
||||
<%= f.submit "SUBMIT", class: "cta" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<hr class="settings-hr"/>
|
||||
<% if @membership.role == "mod" %>
|
||||
<p>Questions about Connect Channel moderation? Contact <a href="mailto:yo@dev.to">yo@dev.to</a></p>
|
||||
<% else %>
|
||||
<h3>Danger Zone</h3>
|
||||
<%= form_for(@membership, html: { method: :delete, onsubmit: "return confirm('Are you absolutely sure you want to leave this channel? This action is permanent.');" } ) do |f| %>
|
||||
<div class="field">
|
||||
<%= f.submit "LEAVE CHANNEL", class: "cta cta-danger" %>
|
||||
</div>
|
||||
<% end %>
|
||||
<% end %>
|
||||
</div>
|
||||
</div>
|
||||
30
app/views/chat_channel_memberships/index.html.erb
Normal file
30
app/views/chat_channel_memberships/index.html.erb
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
<% if flash[:settings_notice] %>
|
||||
<div class="notice" id="notice">
|
||||
<%= flash[:settings_notice] %>
|
||||
</div>
|
||||
<% end %>
|
||||
<div class="user-settings-page">
|
||||
<% if @pending_invites.any? %>
|
||||
<h1>Pending Invitations</h1>
|
||||
<h3>You may leave a group channel at any point in the future.</h3>
|
||||
<% @pending_invites.each do |membership| %>
|
||||
<div>
|
||||
<h2><%= membership.chat_channel.channel_name %></h2>
|
||||
<p><%= membership.chat_channel.description %></p>
|
||||
<%= form_for(membership) do |f| %>
|
||||
<%= f.hidden_field :user_action, value: "accept" %>
|
||||
<%= f.submit "Accept", class: "cta" %>
|
||||
<% end %>
|
||||
<%= form_for(membership) do |f| %>
|
||||
<%= f.hidden_field :user_action, value: "decline" %>
|
||||
<%= f.submit "Decline", class: "cta cta-danger" %>
|
||||
<% end %>
|
||||
</div>
|
||||
<hr class="settings-hr"/>
|
||||
<% end %>
|
||||
<% else %>
|
||||
<p>
|
||||
<em>You have no pending invitations</em>
|
||||
</p>
|
||||
<% end %>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
<table style="width:100%;max-width:700px;margin:auto;font-size:18px;">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="padding:4%;">
|
||||
<% if @membership.role == "mod" %>
|
||||
<h2 style="font-size: 25px; text-align: center;">
|
||||
You have been invited to be a Moderator of a connect group on <%= ApplicationConfig["COMMUNITY_NAME"] %>.
|
||||
</h2>
|
||||
<% else %>
|
||||
<h2 style="font-size: 25px; text-align: center;">
|
||||
<%= link_to(@inviter.name, ApplicationController.helpers.user_url(@inviter)) %> sent you an invitation to join a connect group on <%= ApplicationConfig["COMMUNITY_NAME"] %>.
|
||||
</h2>
|
||||
<% end %>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td style="color:#c3c3c3;font-size:14px;padding-bottom:8px;">
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="width: 700px; padding: 20px 10px 30px; border-top-left-radius: 3px; border-top-right-radius: 3px; border-bottom-right-radius: 3px; border-bottom-left-radius: 3px; font-size: 19px; background-color: rgb(244, 244, 244); text-align: center; background-position: initial initial; background-repeat: initial initial;">
|
||||
<div style="text-align: center;">
|
||||
<h1><%= @membership.chat_channel.channel_name %></h1>
|
||||
</div>
|
||||
<br>
|
||||
<% if @membership.role == "mod" %>
|
||||
<img src="https://media.giphy.com/media/VGG8UY1nEl66Y/giphy.gif" style="width: 300px;">
|
||||
<% else %>
|
||||
<img src="https://media.giphy.com/media/kHs1lBhZWaK5rj7lt3/giphy.gif" style="width: 300px;">
|
||||
<% end %>
|
||||
<br>
|
||||
<br>
|
||||
<div>
|
||||
<p style="background:#3c7dc1;color:white;padding:5px 14px;border-radius:3px;text-decoration:none; margin-top:4px;"><%= link_to("Check Invitation", ApplicationController.helpers.app_url("connect?ref=group_invite")) %> </p>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<br>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
You are invited to the <%= ApplicationConfig["COMMUNITY_NAME"] %> Connect channel <%= @membership.chat_channel.channel_name %>
|
||||
|
||||
View Invitation: <%= ApplicationController.helpers.app_url('connect') %>
|
||||
|
|
@ -9,7 +9,7 @@ module Follows
|
|||
find_by(id: follow_id, follower_type: "User", followable_type: "User")
|
||||
return unless follow&.followable&.following?(follow.follower)
|
||||
|
||||
ChatChannel.create_with_users([follow.followable, follow.follower])
|
||||
ChatChannel.create_with_users(users: [follow.followable, follow.follower])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -151,7 +151,7 @@ Rails.application.routes.draw do
|
|||
|
||||
resources :messages, only: [:create]
|
||||
resources :chat_channels, only: %i[index show create update]
|
||||
resources :chat_channel_memberships, only: %i[create update destroy]
|
||||
resources :chat_channel_memberships, only: %i[index create edit update destroy]
|
||||
resources :articles, only: %i[update create destroy]
|
||||
resources :article_mutes, only: %i[update]
|
||||
resources :comments, only: %i[create update destroy] do
|
||||
|
|
@ -251,6 +251,7 @@ Rails.application.routes.draw do
|
|||
get "/chat_channels/:id/channel_info", to: "chat_channels#channel_info", as: :chat_channel_info
|
||||
post "/chat_channels/create_chat" => "chat_channels#create_chat"
|
||||
post "/chat_channels/block_chat" => "chat_channels#block_chat"
|
||||
post "/chat_channel_memberships/remove_membership" => "chat_channel_memberships#remove_membership"
|
||||
delete "/messages/:id" => "messages#destroy"
|
||||
patch "/messages/:id" => "messages#update"
|
||||
get "/live/:username" => "twitch_live_streams#show"
|
||||
|
|
|
|||
|
|
@ -294,7 +294,7 @@ Rails.logger.info "#{counter}. Creating Chat Channels and Messages"
|
|||
)
|
||||
end
|
||||
|
||||
direct_channel = ChatChannel.create_with_users(User.last(2), "direct")
|
||||
direct_channel = ChatChannel.create_with_users(users: User.last(2), channel_type: "direct")
|
||||
Message.create!(
|
||||
chat_channel: direct_channel,
|
||||
user: User.last,
|
||||
|
|
|
|||
|
|
@ -121,7 +121,6 @@
|
|||
"babel-preset-preact": "^2.0.0",
|
||||
"chart.js": "^2.9.3",
|
||||
"clipboard-polyfill": "^2.8.6",
|
||||
"codemirror": "^5.52.2",
|
||||
"core-js": "3",
|
||||
"file-loader": "^6.0.0",
|
||||
"focus-visible": "^5.0.2",
|
||||
|
|
|
|||
|
|
@ -261,7 +261,7 @@ RSpec.describe NotifyMailer, type: :mailer do
|
|||
end
|
||||
|
||||
describe "#new_message_email" do
|
||||
let(:direct_channel) { ChatChannel.create_with_users([user, user2], "direct") }
|
||||
let(:direct_channel) { ChatChannel.create_with_users(users: [user, user2], channel_type: "direct") }
|
||||
let(:direct_message) { create(:message, user: user, chat_channel: direct_channel) }
|
||||
let(:email) { described_class.new_message_email(direct_message) }
|
||||
|
||||
|
|
@ -407,4 +407,28 @@ RSpec.describe NotifyMailer, type: :mailer do
|
|||
expect(email.html_part.body).to include(CGI.escape("utm_campaign=trusted_role_email"))
|
||||
end
|
||||
end
|
||||
|
||||
describe "#channel_invite_email" do
|
||||
let(:moderator_membership) { create(:chat_channel_membership, user_id: user2.id, role: "mod") }
|
||||
let(:regular_membership) { create(:chat_channel_membership, user_id: user2.id, role: "member") }
|
||||
let(:moderator_email) { described_class.channel_invite_email(moderator_membership, nil) }
|
||||
let(:member_email) { described_class.channel_invite_email(regular_membership, user) }
|
||||
|
||||
it "renders proper subject" do
|
||||
expect(moderator_email.subject).to eq("You are invited to Channel #{moderator_membership.chat_channel.channel_name} as moderator.")
|
||||
expect(member_email.subject).to eq("You are invited to Channel #{regular_membership.chat_channel.channel_name} by #{user.name}.")
|
||||
end
|
||||
|
||||
it "renders proper sender" do
|
||||
expect(moderator_email.from).to eq([SiteConfig.default_site_email])
|
||||
expect(moderator_email["from"].value).to eq("DEV Community <#{SiteConfig.default_site_email}>")
|
||||
expect(member_email.from).to eq([SiteConfig.default_site_email])
|
||||
expect(member_email["from"].value).to eq("DEV Community <#{SiteConfig.default_site_email}>")
|
||||
end
|
||||
|
||||
it "renders proper receiver" do
|
||||
expect(moderator_email.to).to eq([user2.email])
|
||||
expect(member_email.to).to eq([user2.email])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -24,14 +24,14 @@ RSpec.describe ChatChannel, type: :model do
|
|||
|
||||
describe "#create_with_users" do
|
||||
it "creates channel with users" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
chat_channel = described_class.create_with_users(users: users)
|
||||
expect(chat_channel.users.size).to eq(users.size)
|
||||
expect(chat_channel.has_member?(users.first)).to be(true)
|
||||
expect(chat_channel.has_member?(users.last)).to be(true)
|
||||
end
|
||||
|
||||
it "lists active memberships" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
chat_channel = described_class.create_with_users(users: users)
|
||||
expect(chat_channel.active_users.size).to eq(users.size)
|
||||
expect(chat_channel.channel_users.size).to eq(users.size)
|
||||
end
|
||||
|
|
@ -39,7 +39,7 @@ RSpec.describe ChatChannel, type: :model do
|
|||
|
||||
describe "#active_users" do
|
||||
it "decreases active users if one leaves" do
|
||||
chat_channel = described_class.create_with_users(users)
|
||||
chat_channel = described_class.create_with_users(users: users)
|
||||
expect(chat_channel.active_users.size).to eq(users.size)
|
||||
expect(chat_channel.channel_users.size).to eq(users.size)
|
||||
ChatChannelMembership.last.update(status: "left_channel")
|
||||
|
|
|
|||
|
|
@ -10,27 +10,166 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
chat_channel.add_users([user])
|
||||
end
|
||||
|
||||
describe "POST /chat_channel_memberships" do
|
||||
it "creates chat channel invitation" do
|
||||
user.add_role(:super_admin)
|
||||
mems_num = ChatChannelMembership.all.size
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
user_id: second_user.id, chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.all.size).to eq(mems_num + 1)
|
||||
expect(ChatChannelMembership.last.status).to eq("pending")
|
||||
end
|
||||
|
||||
it "denies chat channel invitation to non-authorized user" do
|
||||
expect do
|
||||
describe "GET /chat_channel_memberships" do
|
||||
context "when pending invitations exists" do
|
||||
before do
|
||||
user.add_role(:super_admin)
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
user_id: second_user.id, chat_channel_id: chat_channel.id
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
|
||||
it "shows chat_channel_memberships list pending invitation" do
|
||||
sign_in second_user
|
||||
get "/chat_channel_memberships"
|
||||
expect(response.body).to include "Pending Invitations"
|
||||
expect(response.body).to include chat_channel.channel_name.to_s
|
||||
end
|
||||
end
|
||||
|
||||
context "when no pending invitation" do
|
||||
it "shows chat_channel_memberships list pending invitation" do
|
||||
sign_in second_user
|
||||
get "/chat_channel_memberships"
|
||||
expect(response.body).to include "You have no pending invitations"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /chat_channel_memberships/find_by_chat_channel_id" do
|
||||
context "when user is logged in" do
|
||||
before do
|
||||
chat_channel.add_users([second_user])
|
||||
end
|
||||
|
||||
it "returns chat channel membership details" do
|
||||
sign_in second_user
|
||||
get "/chat_channel_memberships/find_by_chat_channel_id", params: { chat_channel_id: chat_channel.id }
|
||||
expected_keys = %w[id status chat_channel_id last_opened_at channel_text
|
||||
channel_last_message_at channel_status channel_username
|
||||
channel_type channel_name channel_image
|
||||
channel_modified_slug channel_messages_count]
|
||||
expect(response.parsed_body.keys).to(match_array(expected_keys))
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is not logged in" do
|
||||
it "renders not_found" do
|
||||
expect do
|
||||
get "/chat_channel_memberships/find_by_chat_channel_id", params: {}
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /chat_channel_memberships/:id/edit" do
|
||||
before do
|
||||
chat_channel.add_users([second_user])
|
||||
end
|
||||
|
||||
let(:chat_channel_membership) { chat_channel.chat_channel_memberships.where(user_id: second_user.id).first }
|
||||
|
||||
context "when user is not logged in" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
expect do
|
||||
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is logged in and channel id is wrong" do
|
||||
it "raise ActiveRecord::RecordNotFound" do
|
||||
sign_in second_user
|
||||
expect do
|
||||
get "/chat_channel_memberships/ERW/edit"
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is channel member" do
|
||||
it "allows user to view channel members" do
|
||||
sign_in second_user
|
||||
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
|
||||
expect(response.body).to include("Members")
|
||||
expect(response.body).to include(user.username.to_s)
|
||||
expect(response.body).to include(second_user.username.to_s)
|
||||
expect(response.body).not_to include("Pending Invitations")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is channel moderator" do
|
||||
it "allows user to view channel members" do
|
||||
sign_in second_user
|
||||
chat_channel_membership.update(role: "mod")
|
||||
get "/chat_channel_memberships/#{chat_channel_membership.id}/edit"
|
||||
expect(response.body).to include("Members")
|
||||
expect(response.body).to include(user.username.to_s)
|
||||
expect(response.body).to include(second_user.username.to_s)
|
||||
expect(response.body).to include("Pending Invitations")
|
||||
expect(response.body).to include("You are a channel mod")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "POST /chat_channel_memberships" do
|
||||
context "when user is super admin" do
|
||||
it "creates chat channel invitation" do
|
||||
user.add_role(:super_admin)
|
||||
expect do
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
end.to change { ChatChannelMembership.all.size }.by(1)
|
||||
expect(ChatChannelMembership.last.status).to eq("pending")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is channel moderator, and invited user was a member of channel, and than left channel" do
|
||||
it "creates chat channel invitation" do
|
||||
chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod")
|
||||
ChatChannelMembership.create(chat_channel_id: chat_channel.id, user_id: second_user.id, status: "left_channel")
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.last.status).to eq("pending")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is channel moderator, invited user was not a member of channel" do
|
||||
it "creates chat channel invitation" do
|
||||
chat_channel.chat_channel_memberships.where(user_id: user.id).update(role: "mod")
|
||||
chat_channel_members_count = ChatChannelMembership.all.size
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.all.size).to eq(chat_channel_members_count + 1)
|
||||
expect(ChatChannelMembership.last.status).to eq("pending")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is not authorized to add channel membership" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
expect do
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: {
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
|
@ -38,62 +177,130 @@ RSpec.describe "ChatChannelMemberships", type: :request do
|
|||
before do
|
||||
user.add_role(:super_admin)
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: { user_id: second_user.id, chat_channel_id: chat_channel.id }
|
||||
}
|
||||
end
|
||||
|
||||
it "accepts chat channel invitation" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: {
|
||||
user_action: "accept"
|
||||
invitation_usernames: second_user.username.to_s,
|
||||
chat_channel_id: chat_channel.id
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
|
||||
end
|
||||
|
||||
it "rejects chat channel invitation" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: { user_action: "reject" }
|
||||
}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("rejected")
|
||||
end
|
||||
|
||||
it "disallows non-logged-user" do
|
||||
membership = ChatChannelMembership.last
|
||||
expect do
|
||||
context "when second user accept invitation" do
|
||||
it "sets chat channl membership status to rejected" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: { user_action: "accept" }
|
||||
chat_channel_membership: {
|
||||
user_action: "accept"
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
expect(response).to(redirect_to(chat_channel_memberships_path))
|
||||
end
|
||||
end
|
||||
|
||||
context "when second user rejects invitation" do
|
||||
it "sets chat channl membership status to rejected" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: {
|
||||
user_action: "reject"
|
||||
}
|
||||
}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("rejected")
|
||||
end
|
||||
end
|
||||
|
||||
context "when user not logged in" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
membership = ChatChannelMembership.last
|
||||
expect do
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: { user_action: "accept" }
|
||||
}
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is unauthorized" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in user
|
||||
expect do
|
||||
put "/chat_channel_memberships/#{membership.id}", params: {
|
||||
chat_channel_membership: { user_action: "accept" }
|
||||
}
|
||||
end.to raise_error(Pundit::NotAuthorizedError)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "DELETE /chat_channel_memberships/:id" do
|
||||
before do
|
||||
user.add_role(:super_admin)
|
||||
post "/chat_channel_memberships", params: {
|
||||
chat_channel_membership: { user_id: second_user.id, chat_channel_id: chat_channel.id }
|
||||
}
|
||||
context "when user is logged in" do
|
||||
it "leaves chat channel" do
|
||||
chat_channel.add_users([second_user])
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
delete "/chat_channel_memberships/#{membership.id}", params: {}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel")
|
||||
expect(response).to(redirect_to(chat_channel_memberships_path))
|
||||
end
|
||||
end
|
||||
|
||||
it "leaves chat channel" do
|
||||
membership = ChatChannelMembership.last
|
||||
sign_in second_user
|
||||
delete "/chat_channel_memberships/#{membership.chat_channel.id}", params: {}
|
||||
expect(ChatChannelMembership.find(membership.id).status).to eq("left_channel")
|
||||
context "when user is not logged in" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
chat_channel.add_users([second_user])
|
||||
membership = ChatChannelMembership.last
|
||||
expect do
|
||||
delete "/chat_channel_memberships/#{membership.id}", params: {}
|
||||
end.to(raise_error(Pundit::NotAuthorizedError))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe "GET /chat_channel_memberships/find_by_chat_channel_id" do
|
||||
it "renders not_found" do
|
||||
expect do
|
||||
get "/chat_channel_memberships/find_by_chat_channel_id", params: {}
|
||||
end.to raise_error(ActiveRecord::RecordNotFound)
|
||||
describe "POST /chat_channel_memberships/remove_membership" do
|
||||
before do
|
||||
chat_channel.add_users([second_user])
|
||||
end
|
||||
|
||||
context "when user is super admin" do
|
||||
it "removes member from channel" do
|
||||
user.add_role(:super_admin)
|
||||
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
|
||||
removed_channel_membership = ChatChannelMembership.last
|
||||
post "/chat_channel_memberships/remove_membership", params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
membership_id: removed_channel_membership.id
|
||||
}
|
||||
expect(removed_channel_membership.reload.status).to eq("removed_from_channel")
|
||||
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is moderator of channel" do
|
||||
it "removes member from channel" do
|
||||
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
|
||||
membership.update(role: "mod")
|
||||
removed_channel_membership = ChatChannelMembership.last
|
||||
post "/chat_channel_memberships/remove_membership", params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
membership_id: removed_channel_membership.id
|
||||
}
|
||||
expect(removed_channel_membership.reload.status).to eq("removed_from_channel")
|
||||
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
|
||||
end
|
||||
end
|
||||
|
||||
context "when user is member of channel" do
|
||||
it "raise Pundit::NotAuthorizedError" do
|
||||
membership = ChatChannelMembership.last
|
||||
expect do
|
||||
post "/chat_channel_memberships/remove_membership", params: {
|
||||
chat_channel_id: chat_channel.id,
|
||||
membership_id: membership.id
|
||||
}
|
||||
end.to(raise_error(Pundit::NotAuthorizedError))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
before do
|
||||
sign_in user
|
||||
chat_channel.add_users([user])
|
||||
chat_channel.chat_channel_memberships.update(status: "active")
|
||||
end
|
||||
|
||||
describe "GET /connect" do
|
||||
|
|
@ -136,10 +137,13 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
describe "PUT /chat_channels/:id" do
|
||||
it "updates channel for valid user" do
|
||||
user.add_role(:super_admin)
|
||||
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
|
||||
membership.update(role: "mod")
|
||||
put "/chat_channels/#{chat_channel.id}",
|
||||
params: { chat_channel: { channel_name: "Hello Channel", slug: "hello-channelly" } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
expect(ChatChannel.last.slug).to eq("hello-channelly")
|
||||
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
|
||||
end
|
||||
|
||||
it "dissallows invalid users" do
|
||||
|
|
@ -153,10 +157,12 @@ RSpec.describe "ChatChannels", type: :request do
|
|||
it "returns errors if channel is invalid" do
|
||||
# slug should be taken
|
||||
user.add_role(:super_admin)
|
||||
membership = chat_channel.chat_channel_memberships.where(user_id: user.id).last
|
||||
membership.update(role: "mod")
|
||||
put "/chat_channels/#{chat_channel.id}",
|
||||
params: { chat_channel: { channel_name: "HEy hey hoho", slug: invite_channel.slug } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
expect(response.body).to include("Slug has already been taken")
|
||||
expect(response).to(redirect_to(edit_chat_channel_membership_path(membership.id)))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
|||
24
spec/requests/internal/chat_channel_spec.rb
Normal file
24
spec/requests/internal/chat_channel_spec.rb
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
require "rails_helper"
|
||||
|
||||
RSpec.describe "/internal/chat_channels", type: :request do
|
||||
include ActiveJob::TestHelper
|
||||
|
||||
let(:user) { create(:user) }
|
||||
let(:chat_channel) { create(:chat_channel) }
|
||||
|
||||
describe "POST /chat_channels" do
|
||||
around { |example| perform_enqueued_jobs(&example) }
|
||||
|
||||
it "creates chat_channel for with users as moderator" do
|
||||
user.add_role(:super_admin)
|
||||
sign_in user
|
||||
expect do
|
||||
post "/internal/chat_channels",
|
||||
params: { chat_channel: { channel_name: "Hello Channel", usernames_string: "#{user.username}" } },
|
||||
headers: { HTTP_ACCEPT: "application/json" }
|
||||
end.to change(ActionMailer::Base.deliveries, :length)
|
||||
expect(ChatChannel.last.channel_name).to eq("Hello Channel")
|
||||
expect(ChatChannel.last.pending_users).to include(user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
@ -48,7 +48,7 @@ RSpec.describe "Internal::Users", type: :request do
|
|||
badge_id: badge.id,
|
||||
rewarding_context_message_markdown: "message",
|
||||
)
|
||||
ChatChannel.create_with_users([user2, user3], "direct")
|
||||
ChatChannel.create_with_users(users: [user2, user3], channel_type: "direct")
|
||||
user2.follow(user3)
|
||||
user.follow(super_admin)
|
||||
user3.follow(user2)
|
||||
|
|
|
|||
|
|
@ -4,10 +4,10 @@ RSpec.describe Users::CleanupChatChannels, type: :service do
|
|||
let(:user) { create(:user) }
|
||||
let(:other_user) { create(:user) }
|
||||
let!(:dm_channel) do
|
||||
ChatChannel.create_with_users([user, other_user])
|
||||
ChatChannel.create_with_users(users: [user, other_user])
|
||||
end
|
||||
let!(:open_channel) do
|
||||
ChatChannel.create_with_users([user, other_user], "open")
|
||||
ChatChannel.create_with_users(users: [user, other_user], channel_type: "open")
|
||||
end
|
||||
|
||||
it "deletes direct chat channels" do
|
||||
|
|
|
|||
|
|
@ -91,13 +91,13 @@ RSpec.describe Users::Delete, type: :service do
|
|||
let_it_be(:other_user) { create(:user) }
|
||||
|
||||
it "deletes the user's private chat channels" do
|
||||
chat_channel = ChatChannel.create_with_users([user, other_user])
|
||||
chat_channel = ChatChannel.create_with_users(users: [user, other_user])
|
||||
described_class.call(user)
|
||||
expect(ChatChannel.find_by(id: chat_channel.id)).to be_nil
|
||||
end
|
||||
|
||||
it "does not delete the user's open channels" do
|
||||
chat_channel = ChatChannel.create_with_users([user, other_user], "open")
|
||||
chat_channel = ChatChannel.create_with_users(users: [user, other_user], channel_type: "open")
|
||||
described_class.call(user)
|
||||
expect(ChatChannel.find_by(id: chat_channel.id)).not_to be_nil
|
||||
end
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ RSpec.describe Moderator::BanishUserWorker, type: :worker do
|
|||
create(:article, user_id: user.id)
|
||||
create(:article, user_id: user.id)
|
||||
create(:classified_listing, user: user)
|
||||
ChatChannel.create_with_users([user, user2])
|
||||
ChatChannel.create_with_users(users: [user, user2])
|
||||
user.follow(user2)
|
||||
described_class.new.perform(admin.id, user.id)
|
||||
user.reload
|
||||
|
|
|
|||
|
|
@ -4023,11 +4023,6 @@ code-point-at@^1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77"
|
||||
integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=
|
||||
|
||||
codemirror@^5.52.2:
|
||||
version "5.52.2"
|
||||
resolved "https://registry.yarnpkg.com/codemirror/-/codemirror-5.52.2.tgz#c29e1f7179f85eb0dd17c0586fa810e4838ff584"
|
||||
integrity sha512-WCGCixNUck2HGvY8/ZNI1jYfxPG5cRHv0VjmWuNzbtCLz8qYA5d+je4QhSSCtCaagyeOwMi/HmmPTjBgiTm2lQ==
|
||||
|
||||
collect-v8-coverage@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/collect-v8-coverage/-/collect-v8-coverage-1.0.0.tgz#150ee634ac3650b71d9c985eb7f608942334feb1"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue