[deploy] 🚀 Feature: Ability to search discoverable channels and send request to join (#7385)

* 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

* added new column discoverable for public channel and changes in elasticsearch

* fix corrections

* changes in serializer file, added channel_discoverable

* added checkbox with discoverable policy

* changes in code for discoverable channels

* accept and reject member invitation by mod

* add joining request to closed groups

* fixed merge error

* changes in joining member to closed groups

* join to closed group

* changes in message action of joining

* changes in chat upopened json

* 🚀Feature : Ability to search Global Channels

* touch updated_at in after commit update

* 🚀Feature : Ability to send request to discoverable channel

* 🚀Feature : Ability to send request to discoverable channel

* created new route for joining closed channel

* 🚀 Success handle on joining request sent

* removed redundant code

* join_channel improvement, removed authorization for join channel

* list of joining requests

* added joining_request status channels on search list

* changes in mailer and query builder optimized

* 🛠 Adding filter for new Query

* added rspec for add membership method in controller

* rspec for sending join channel request

* invite join request channel list rspec

* test case for query builder discoverable

* viewable and discoverable channel list

* refactored logic for search channels

* 🚀 Check if Request already sent

* 🛠 Optimizing code and making channelButton Component

* 🛠 Optimizing codefor SVG problem

* 🛠 Optimized action.js

* changes in search controller query

* hot fix

* removed unwanted code

* 🛠  Fix the Channel Name problem

* 🛠  Optimizing code further for CodeClimate

* added new column discoverable for public channel and changes in elasticsearch

* fix corrections

* changes in serializer file, added channel_discoverable

* added checkbox with discoverable policy

* changes in code for discoverable channels

* accept and reject member invitation by mod

* add joining request to closed groups

* fixed merge error

* changes in joining member to closed groups

* join to closed group

* changes in message action of joining

* changes in chat upopened json

* touch updated_at in after commit update

* 🚀Feature : Ability to search Global Channels

* 🚀Feature : Ability to send request to discoverable channel

* 🚀Feature : Ability to send request to discoverable channel

* created new route for joining closed channel

* 🚀 Success handle on joining request sent

* removed redundant code

* join_channel improvement, removed authorization for join channel

* list of joining requests

* added joining_request status channels on search list

* changes in mailer and query builder optimized

* added rspec for add membership method in controller

* rspec for sending join channel request

* invite join request channel list rspec

* 🛠 Adding filter for new Query

* test case for query builder discoverable

* viewable and discoverable channel list

* refactored logic for search channels

* 🚀 Check if Request already sent

* 🛠 Optimizing code and making channelButton Component

* 🛠 Optimizing codefor SVG problem

* 🛠 Optimized action.js

* changes in search controller query

* hot fix

* 🛠  Fix the Channel Name problem

* 🛠  Fixing merge problem

* 🛠  Optimizing code further for CodeClimate

* updated UI for membership edit

* fixed test casses and fixed UI for chat_channel_edit

* 🛠napshots added

* test cases fixed

* 🛠 Test cases added for new component

* channel settings accesible only by mod

* 🛠 More Test cases added

* 🛠 Svg code optimized

* 🛠 Svg code optimized in videocontent

* optimized code for search query

* fix test case for query builder

* changes in joining closed channel logic

* refactored join channel

* redirect to edit channel after joining request

* changes in test case for redirect

* optimized code

* 🛠 Eslint bugs fixed

* test case fixed

* optimization

* olving channel repetition problem

* optimization of code for query builder

* changes in query builder

* test cases fixed

* 🛠 Handling reduntant data on frontend

* 🛠  Don't show data if no filter query

* 🛠 Optimized code for fixing bugs and code coverage

* 🛠  Optimizing code further for CodeClimate

Co-authored-by: Parasgr-code <paras.gaur@skynox.tech>
This commit is contained in:
Sarthak Sharma 2020-05-11 18:59:15 +05:30 committed by GitHub
parent d89f4e14d7
commit 90d60bf73f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
36 changed files with 997 additions and 348 deletions

View file

@ -1214,3 +1214,54 @@
}
}
}
.joining-message {
display: grid;
margin-top: 30px;
justify-content: center;
h3,
h2 {
width: 90%;
margin-top: 10px;
text-align: center;
justify-self: center;
}
}
.user-picture,
.send-request {
display: flex;
justify-content: center;
margin-top: 10vh;
}
.user-picture {
.chatmessage__profilepic {
.chatmessagebody__profileimage {
width: 50px;
height: 50px;
border: 1px solid var(--base-30);
}
}
}
.send-request {
.cta {
margin: 4px 6px 4px 0;
border-radius: 3px;
font-weight: 600px;
font-family: 'HelveticaNeue-CondensedBold', 'HelveticaNeueBoldCondensed',
'HelveticaNeue-Bold-Condensed', 'Helvetica Neue Bold Condensed',
'HelveticaNeueBold', 'HelveticaNeue-Bold', 'Helvetica Neue Bold',
'HelveticaNeue', 'Helvetica Neue', 'TeXGyreHerosCnBold', 'Helvetica',
'Tahoma', 'Geneva', 'Arial Narrow', 'Arial', sans-serif;
font-size: 18px;
border: 0px;
height: 50px;
padding: 0 30px;
}
}
.loading-user {
height: '210px';
width: '210px';
margin: ' 15px auto';
display: 'block';
border-radius: '500px';
background-color: '#f5f6f7';
}

View file

@ -1,5 +1,5 @@
class ChatChannelMembershipsController < ApplicationController
after_action :verify_authorized
after_action :verify_authorized, except: :join_channel
include MessagesHelper
def index
@ -39,6 +39,23 @@ class ChatChannelMembershipsController < ApplicationController
redirect_to edit_chat_channel_membership_path(membership)
end
def join_channel
membership_params = params[:chat_channel_membership]
chat_channel = ChatChannel.find(membership_params[:chat_channel_id])
existing_membership = ChatChannelMembership.find_by(user_id: current_user.id, chat_channel_id: chat_channel.id)
if existing_membership.present? && %w[active joining_request].exclude?(existing_membership.status)
status = existing_membership.update(status: "joining_request", role: "member")
else
membership = ChatChannelMembership.new(user_id: current_user.id, chat_channel_id: chat_channel.id, role: "member", status: "joining_request")
status = membership.save
end
if status
render json: { status: "success", message: "Request Sent" }
else
render json: { status: 400, message: "Unable to join channel" }, status: :bad_request
end
end
def remove_membership
@chat_channel = ChatChannel.find(params[:chat_channel_id])
authorize @chat_channel, :update?
@ -55,11 +72,18 @@ class ChatChannelMembershipsController < ApplicationController
redirect_to edit_chat_channel_membership_path(membership)
end
def add_membership
@chat_channel = ChatChannel.find(params[:chat_channel_id])
authorize @chat_channel, :update?
@chat_channel_membership = @chat_channel.chat_channel_memberships.find(params[:membership_id])
respond_to_invitation(@chat_channel_membership.status) if permitted_params[:user_action].present? && @chat_channel_membership.status == "joining_request"
end
def update
@chat_channel_membership = ChatChannelMembership.find(params[:id])
authorize @chat_channel_membership
if permitted_params[:user_action].present?
respond_to_invitation
respond_to_invitation(@chat_channel_membership.status)
else
@chat_channel_membership.update(permitted_params)
flash[:settings_notice] = "Personal settings updated."
@ -84,12 +108,20 @@ class ChatChannelMembershipsController < ApplicationController
params.require(:chat_channel_membership).permit(:user_action, :show_global_badge_notification)
end
def respond_to_invitation
def respond_to_invitation(previous_status)
if permitted_params[:user_action] == "accept"
@chat_channel_membership.update(status: "active")
channel_name = @chat_channel_membership.chat_channel.channel_name
send_chat_action_message("@#{current_user.username} joined #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "joined")
flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list."
if previous_status == "pending"
send_chat_action_message("@#{current_user.username} joined #{@chat_channel_membership.channel_name}", current_user, @chat_channel_membership.chat_channel_id, "joined")
flash[:settings_notice] = "Invitation to #{channel_name} accepted. It may take a moment to show up in your list."
else
send_chat_action_message("@#{current_user.username} added @#{@chat_channel_membership.user.username}", current_user, @chat_channel_membership.chat_channel_id, "joined")
NotifyMailer.channel_invite_email(@chat_channel_membership, @chat_channel_membership.user).deliver_later
flash[:settings_notice] = "Accepted request of #{@chat_channel_membership.user.username} to join #{channel_name}."
membership = ChatChannelMembership.find_by!(chat_channel_id: @chat_channel_membership.chat_channel.id, user: current_user)
redirect_to(edit_chat_channel_membership_path(membership)) && return
end
else
@chat_channel_membership.update(status: "rejected")
flash[:settings_notice] = "Invitation rejected."

View file

@ -13,6 +13,9 @@ class ChatChannelsController < ApplicationController
elsif params[:state] == "pending"
authorize ChatChannel
render_pending_json_response
elsif params[:state] == "joining_request"
authorize ChatChannel
render_joining_request_json_response
else
skip_authorization
render_channels_html
@ -31,6 +34,11 @@ class ChatChannelsController < ApplicationController
def update
if ChatChannelUpdateService.new(@chat_channel, chat_channel_params).update
if !chat_channel_params[:discoverable].to_i.zero?
ChatChannelMembership.create(user_id: SiteConfig.mascot_user_id, chat_channel_id: @chat_channel.id, role: "member", status: "active")
else
ChatChannelMembership.find_by(user_id: SiteConfig.mascot_user_id)&.destroy
end
flash[:settings_notice] = "Channel settings updated."
else
default_error_message = "Channel settings updation failed. Try again later."
@ -140,9 +148,9 @@ class ChatChannelsController < ApplicationController
def render_unopened_json_response
@chat_channels_memberships = if session_current_user_id
ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel).
where("has_unopened_messages = ? OR status = ?",
true, "pending").
where(has_unopened_messages: true).
where(show_global_badge_notification: true).
where.not(status: %w[removed_from_channel left_channel]).
order("chat_channel_memberships.updated_at DESC")
else
[]
@ -164,10 +172,16 @@ class ChatChannelsController < ApplicationController
def render_unopened_ids_response
@unopened_ids = ChatChannelMembership.where(user_id: session_current_user_id).includes(:chat_channel).
where(has_unopened_messages: true).pluck(:chat_channel_id)
where(has_unopened_messages: true).where.not(status: %w[removed_from_channel left_channel]).pluck(:chat_channel_id)
render json: { unopened_ids: @unopened_ids }
end
def render_joining_request_json_response
requested_memberships = current_user.chat_channel_memberships.includes(:chat_channel).
where(chat_channels: { discoverable: true }, role: "mod").pluck(:chat_channel_id).map { |membership_id| ChatChannel.find_by(id: membership_id).requested_memberships }
render json: { joining_requests: requested_memberships.flatten }
end
def render_channels_html
return unless current_user && params[:slug]

View file

@ -57,8 +57,9 @@ class SearchController < ApplicationController
end
def chat_channels
search_user_id = chat_channel_params[:user_id].present? ? [current_user.id, SiteConfig.mascot_user_id] : [current_user.id]
ccm_docs = Search::ChatChannelMembership.search_documents(
params: chat_channel_params.merge(user_id: current_user.id).to_h,
params: chat_channel_params.merge(user_id: search_user_id).to_h,
)
render json: { result: ccm_docs }
@ -118,6 +119,7 @@ class SearchController < ApplicationController
channel_type
channel_status
status
user_id
]
params.permit(accessible)

View file

@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Message /> should render and test snapshot 1`] = `
<button
class="chatchanneltabbutton"
data-channel-id={23}
data-channel-name="test"
data-channel-slug="@test34"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={null}
type="button"
>
<span
class="chatchanneltab chatchanneltab--"
data-channel-id={23}
data-channel-name="test"
data-channel-slug="@test34"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid #00FFFF",
"boxShadow": "3px 3px 0px #00FFFF",
}
}
>
<span
class="chatchanneltabindicator chatchanneltabindicator--"
data-channel-id={23}
data-channel-slug="@test34"
>
<img
alt="pic"
class="chatchanneltabindicatordirectimage invert-channel-image"
/>
</span>
test
</span>
</button>
`;

View file

@ -0,0 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<Message /> should render and test snapshot 1`] = `
<div
class="activechatchannel__activeArticle activesendrequest"
>
<div
class="joining-message"
>
<h2>
Hey Sarthak !
</h2>
<h3>
You are not a member of this group yet. Send request to join.
</h3>
</div>
<div
class="user-picture"
>
<div
class="chatmessage__profilepic"
>
<img
alt="undefined profile"
class="chatmessagebody__profileimage"
role="presentation"
/>
<img
alt="IronMan profile"
class="chatmessagebody__profileimage"
role="presentation"
src="/assets/organization.svg"
/>
</div>
</div>
<div
class="send-request"
>
<button
class="cta"
type="button"
>
Join IronMan
</button>
</div>
</div>
`;

View file

@ -9,14 +9,20 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
id="chatchannels__channelslist"
>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="channel name 1"
data-channel-slug="@0"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="channel name 1"
data-channel-slug="@0"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -30,21 +36,27 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
class="chatchanneltabindicatordirectimage invert-channel-image"
/>
</span>
channel name 1
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="group channel 2"
data-channel-slug="@1"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="group channel 2"
data-channel-slug="@1"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -65,14 +77,20 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="group channel 3"
data-channel-slug="@2"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="group channel 3"
data-channel-slug="@2"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -86,7 +104,7 @@ exports[`<Channels /> expanded with chat channels should render and test snapsho
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
class="chatchanneltabindicatordirectimage invert-channel-image"
/>
</span>
group channel 3
@ -186,14 +204,20 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
id="chatchannels__channelslist"
>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="channel name 1"
data-channel-slug="@0"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="channel name 1"
data-channel-slug="@0"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -207,21 +231,27 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
class="chatchanneltabindicatordirectimage invert-channel-image"
/>
</span>
channel name 1
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="group channel 2"
data-channel-slug="@1"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="group channel 2"
data-channel-slug="@1"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -242,14 +272,20 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
class="chatchanneltabbutton"
data-channel-name="group channel 3"
data-channel-slug="@2"
data-channel-status="active"
data-content="sidecar-channel-request"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
data-channel-name="group channel 3"
data-channel-slug="@2"
data-channel-status="active"
data-content="sidecar-channel-request"
style={
Object {
"border": "1px solid undefined",
@ -263,7 +299,7 @@ exports[`<Channels /> not expanded with chat channels should render and test sna
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
class="chatchanneltabindicatordirectimage invert-channel-image"
/>
</span>
group channel 3

View file

@ -68,83 +68,7 @@ exports[`<Chat /> should load chat 1`] = `
<div
class="chatchannels__channelslist"
id="chatchannels__channelslist"
>
<button
class="chatchanneltabbutton crayons-link"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
style={
Object {
"border": "1px solid undefined",
"boxShadow": "3px 3px 0px undefined",
}
}
>
<span
class="chatchanneltabindicator chatchanneltabindicator--old"
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
/>
</span>
channel name 1
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
style={
Object {
"border": "1px solid undefined",
"boxShadow": "3px 3px 0px undefined",
}
}
>
<span
class="chatchanneltabindicator chatchanneltabindicator--old"
>
<img
alt="pic"
class="chatchanneltabindicatordirectimage"
/>
</span>
group channel 2
</span>
</button>
<button
class="chatchanneltabbutton crayons-link"
onClick={[Function]}
type="button"
>
<span
class="chatchanneltab chatchanneltab--inactive chatchanneltab--old"
style={
Object {
"border": "1px solid undefined",
"boxShadow": "3px 3px 0px undefined",
}
}
>
<span
class="chatchanneltabindicator chatchanneltabindicator--old"
>
<img
alt="pic"
class="chatchanneltabindicatorgroupimage invert-channel-image"
/>
</span>
group channel 3
</span>
</button>
</div>
/>
<div
class="chatchannels__config"
>

View file

@ -37,6 +37,7 @@ exports[`<Content /> as loading-user should render and test snapshot 1`] = `
}
}
>
<svg
data-content="fullscreen"
height="24"
@ -56,16 +57,7 @@ exports[`<Content /> as loading-user should render and test snapshot 1`] = `
</svg>
</button>
<div
style={
Object {
"backgroundColor": "#f5f6f7",
"borderRadius": "500px",
"display": "block",
"height": "210px",
"margin": " 15px auto",
"width": "210px",
}
}
class="loading-user"
/>
</div>
`;

View file

@ -0,0 +1,33 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ChannelButton from '../components/channelButton';
const chnl = {
channel_name: 'test',
channel_color: '#00FFFF',
channel_type: 'invite_only',
channel_modified_slug: '@test34',
id: 34,
chat_channel_id: 23,
status: 'active',
};
const getChannel = (channel) => <ChannelButton channel={channel} />;
describe('<Message />', () => {
it('should render and test snapshot', () => {
const tree = render(getChannel(chnl));
expect(tree).toMatchSnapshot();
});
it('should have the proper elements, attributes and values', () => {
const context = shallow(getChannel(chnl));
expect(
context.find('.chatchanneltabbutton').attr('data-channel-slug'),
).toEqual(chnl.channel_modified_slug); // check user
expect(context.find('.chatchanneltabbutton').text()).toEqual(
chnl.channel_name,
);
});
});

View file

@ -0,0 +1,28 @@
import { h } from 'preact';
import render from 'preact-render-to-json';
import { shallow } from 'preact-render-spy';
import ChannelRequest from '../channelRequest';
const data = {
user: {
name: 'Sarthak',
},
channel: {
name: 'IronMan',
},
};
const getChannelRequest = (resource) => <ChannelRequest resource={resource} />;
describe('<Message />', () => {
it('should render and test snapshot', () => {
const tree = render(getChannelRequest(data));
expect(tree).toMatchSnapshot();
});
it('should have the proper elements, attributes and values', () => {
const context = shallow(getChannelRequest(data));
expect(context.find('.joining-message').exists()).toEqual(true);
});
});

View file

@ -25,10 +25,12 @@ const fakeChannels = [
slug: '0',
channel_modified_slug: '@0',
id: 12345,
status: 'active',
messages_count: 124,
},
{
channel_name: 'group channel 2',
status: 'active',
last_opened_at: 'September 12, 2018',
channel_users: [
{
@ -66,6 +68,7 @@ const fakeChannels = [
],
last_message_at: 'September 29, 2018',
channel_type: 'group',
status: 'active',
slug: '2',
channel_modified_slug: '@2',
id: 67890,
@ -103,28 +106,16 @@ describe('<Channels />', () => {
true,
);
expect(
context
.find('.chatchannels__configmenu')
.childAt(0)
.text(),
context.find('.chatchannels__configmenu').childAt(0).text(),
).toEqual('DEV Settings');
expect(
context
.find('.chatchannels__configmenu')
.childAt(0)
.attr('href'),
context.find('.chatchannels__configmenu').childAt(0).attr('href'),
).toEqual('/settings');
expect(
context
.find('.chatchannels__configmenu')
.childAt(1)
.text(),
context.find('.chatchannels__configmenu').childAt(1).text(),
).toEqual('Report Abuse');
expect(
context
.find('.chatchannels__configmenu')
.childAt(1)
.attr('href'),
context.find('.chatchannels__configmenu').childAt(1).attr('href'),
).toEqual('/report-abuse');
// welcome message should not exist because there are channels
@ -158,28 +149,16 @@ describe('<Channels />', () => {
true,
);
expect(
context
.find('.chatchannels__configmenu')
.childAt(0)
.text(),
context.find('.chatchannels__configmenu').childAt(0).text(),
).toEqual('DEV Settings');
expect(
context
.find('.chatchannels__configmenu')
.childAt(0)
.attr('href'),
context.find('.chatchannels__configmenu').childAt(0).attr('href'),
).toEqual('/settings');
expect(
context
.find('.chatchannels__configmenu')
.childAt(1)
.text(),
context.find('.chatchannels__configmenu').childAt(1).text(),
).toEqual('Report Abuse');
expect(
context
.find('.chatchannels__configmenu')
.childAt(1)
.attr('href'),
context.find('.chatchannels__configmenu').childAt(1).attr('href'),
).toEqual('/report-abuse');
});
});
@ -199,30 +178,6 @@ describe('<Channels />', () => {
expect(context.find('.chatchannels__channelslist').exists()).toEqual(
true,
);
expect(
context
.find('.chatchannels__channelslist')
.childAt(2)
.attr('data-channel-slug'),
).toEqual('@1'); // check user
expect(
context
.find('.chatchannels__channelslist')
.childAt(2)
.text(),
).toEqual('group channel 2'); // ensure user has no text
expect(
context
.find('.chatchannels__channelslist')
.childAt(1)
.text(),
).toEqual(fakeChannels[0].channel_name);
expect(
context
.find('.chatchannels__channelslist')
.childAt(3)
.text(),
).toEqual(fakeChannels[2].channel_name);
});
});

View file

@ -6,7 +6,7 @@ export function getAllMessages(channelId, messageOffset, successCb, failureCb) {
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -29,7 +29,7 @@ export function sendMessage(messageObject, successCb, failureCb) {
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -51,7 +51,7 @@ export function editMessage(editedMessage, successCb, failureCb) {
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -67,7 +67,7 @@ export function sendOpen(activeChannelId, successCb, failureCb) {
body: JSON.stringify({}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -92,7 +92,7 @@ export function conductModeration(
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -100,7 +100,7 @@ export function conductModeration(
export function getChannels(
query,
retrievalID,
props,
searchType,
paginationNumber,
additionalFilters,
successCb,
@ -114,14 +114,16 @@ export function getChannels(
dataHash.per_page = 30;
dataHash.page = paginationNumber;
dataHash.channel_text = query;
if (searchType === 'discoverable') {
dataHash.user_id = 'all';
}
const responsePromise = fetchSearch('chat_channels', dataHash);
return responsePromise.then(response => {
return responsePromise.then((response) => {
const channels = response.result;
if (
retrievalID === null ||
channels.filter(e => e.chat_channel_id === retrievalID).length === 1
channels.filter((e) => e.chat_channel_id === retrievalID).length === 1
) {
successCb(channels, query);
} else {
@ -133,8 +135,8 @@ export function getChannels(
credentials: 'same-origin',
},
)
.then(individualResponse => individualResponse.json())
.then(json => {
.then((individualResponse) => individualResponse.json())
.then((json) => {
channels.unshift(json);
successCb(channels, query);
});
@ -146,8 +148,8 @@ export function getUnopenedChannelIds(successCb) {
fetch('/chat_channels?state=unopened_ids', {
credentials: 'same-origin',
})
.then(response => response.json())
.then(json => {
.then((response) => response.json())
.then((json) => {
successCb(json.unopened_ids);
});
}
@ -158,7 +160,7 @@ export function getContent(url, successCb, failureCb) {
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -169,7 +171,7 @@ export function getJSONContents(url, successCb, failureCb) {
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -180,7 +182,7 @@ export function getChannelInvites(successCb, failureCb) {
'Content-Type': 'application/json',
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -200,7 +202,7 @@ export function sendChannelInviteAction(id, action, successCb, failureCb) {
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
@ -220,7 +222,27 @@ export function deleteMessage(messageId, successCb, failureCb) {
}),
credentials: 'same-origin',
})
.then(response => response.json())
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}
export function sendChannelRequest(id, successCb, failureCb) {
fetch(`/join_chat_channel`, {
method: 'POST',
headers: {
Accept: 'application/json',
'X-CSRF-Token': window.csrfToken,
'Content-Type': 'application/json',
},
body: JSON.stringify({
chat_channel_membership: {
chat_channel_id: id,
},
}),
credentials: 'same-origin',
})
.then((response) => response.json())
.then(successCb)
.catch(failureCb);
}

View file

@ -0,0 +1,53 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const ChannelRequest = ({ resource: data, handleJoiningRequest }) => (
<div className="activechatchannel__activeArticle activesendrequest">
<div className="joining-message">
<h2>Hey {data.user.name} !</h2>
<h3>You are not a member of this group yet. Send request to join.</h3>
</div>
<div className="user-picture">
<div className="chatmessage__profilepic">
<img
role="presentation"
className="chatmessagebody__profileimage"
src={data.user.profile_image_90}
alt={`${data.user.username} profile`}
/>
<img
role="presentation"
className="chatmessagebody__profileimage"
src="/assets/organization.svg"
alt={`${data.channel.name} profile`}
/>
</div>
</div>
<div className="send-request">
{data.channel.status !== 'joining_request' ? (
<button
type="button"
className="cta"
onClick={handleJoiningRequest}
data-channel-id={data.channel.id}
>
{' '}
Join {data.channel.name}{' '}
</button>
) : (
<button type="button" className="cta">
{' '}
Requested Already{' '}
</button>
)}
</div>
</div>
);
ChannelRequest.propTypes = {
resource: PropTypes.shape({
data: PropTypes.object,
}).isRequired,
handleJoiningRequest: PropTypes.func.isRequired,
};
export default ChannelRequest;

View file

@ -2,6 +2,8 @@ import { h } from 'preact';
import PropTypes from 'prop-types';
// eslint-disable-next-line import/no-unresolved
import ConfigImage from 'images/three-dots.svg';
import ChannelButton from './components/channelButton';
import { channelSorter } from './util';
const Channels = ({
activeChannelId,
@ -11,53 +13,46 @@ const Channels = ({
expanded,
filterQuery,
channelsLoaded,
currentUserId,
triggerActiveContent,
}) => {
const channels = chatChannels.map(channel => {
const sortedChatChannels = channelSorter(
chatChannels,
currentUserId,
filterQuery,
);
const discoverableChannels = sortedChatChannels.discoverableChannels.map(
(channel) => {
return (
<ChannelButton
channel={channel}
discoverableChannel
triggerActiveContent={triggerActiveContent}
/>
);
},
);
const channels = sortedChatChannels.activeChannels.map((channel) => {
const isActive = parseInt(activeChannelId, 10) === channel.chat_channel_id;
const isUnopened =
!isActive && unopenedChannelIds.includes(channel.chat_channel_id);
let newMessagesIndicator = isUnopened ? 'new' : 'old';
const newMessagesIndicator = isUnopened ? 'new' : 'old';
const otherClassname = isActive
? 'chatchanneltab--active'
: 'chatchanneltab--inactive';
return (
<button
type="button"
key={channel.id}
className="chatchanneltabbutton crayons-link"
onClick={handleSwitchChannel}
data-channel-id={channel.chat_channel_id}
data-channel-slug={channel.channel_modified_slug}
>
<span
className={`chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicator}`}
data-channel-id={channel.chat_channel_id}
data-channel-slug={channel.channel_modified_slug}
style={{
border: `1px solid ${channel.channel_color}`,
boxShadow: `3px 3px 0px ${channel.channel_color}`,
}}
>
<span
data-channel-slug={channel.channel_modified_slug}
className={`chatchanneltabindicator chatchanneltabindicator--${newMessagesIndicator}`}
data-channel-id={channel.chat_channel_id}
>
<img
src={channel.channel_image}
alt="pic"
className={
channel.channel_type === 'direct'
? 'chatchanneltabindicatordirectimage'
: 'chatchanneltabindicatorgroupimage invert-channel-image'
}
/>
</span>
{isUnopened ? <span class="crayons-indicator crayons-indicator--accent crayons-indicator--bullet"></span> : ''}{channel.channel_name}
</span>
</button>
<ChannelButton
channel={channel}
newMessagesIndicator={newMessagesIndicator}
otherClassname={otherClassname}
handleSwitchChannel={handleSwitchChannel}
isUnopened={isUnopened}
/>
);
});
console.log(channels);
let topNotice = '';
if (
expanded &&
@ -103,6 +98,16 @@ const Channels = ({
>
{topNotice}
{channels}
{discoverableChannels.length > 0 ? (
<div>
<span className="crayons-indicator crayons-indicator--">
Global Channel Search
</span>
{discoverableChannels}
</div>
) : (
''
)}
{channelsListFooter}
</div>
{configFooter}
@ -115,9 +120,11 @@ Channels.propTypes = {
chatChannels: PropTypes.arrayOf(PropTypes.objectOf()).isRequired,
unopenedChannelIds: PropTypes.arrayOf().isRequired,
handleSwitchChannel: PropTypes.func.isRequired,
triggerActiveContent: PropTypes.func.isRequired,
expanded: PropTypes.bool.isRequired,
filterQuery: PropTypes.string.isRequired,
channelsLoaded: PropTypes.bool.isRequired,
currentUserId: PropTypes.string.isRequired,
};
export default Channels;

View file

@ -14,8 +14,15 @@ import {
sendChannelInviteAction,
deleteMessage,
editMessage,
sendChannelRequest,
} from './actions';
import { hideMessages, scrollToBottom, setupObserver } from './util';
import {
hideMessages,
scrollToBottom,
setupObserver,
getCurrentUser,
channelSorter,
} from './util';
import Alert from './alert';
import Channels from './channels';
import Compose from './compose';
@ -124,7 +131,7 @@ export default class Chat extends Component {
getChannels(
'',
activeChannelId,
this.props,
'',
channelPaginationNum,
filters,
this.loadChannels,
@ -165,9 +172,11 @@ export default class Chat extends Component {
}
}
filterForActiveChannel = (channels, id) =>
filterForActiveChannel = (channels, id, currentUserId) =>
channels.filter(
(channel) => channel.chat_channel_id === parseInt(id, 10),
(channel) =>
channel.chat_channel_id === parseInt(id, 10) &&
channel.viewable_by === parseInt(currentUserId, 10),
)[0];
subscribePusher = (channelName) => {
@ -271,7 +280,7 @@ export default class Chat extends Component {
};
loadPaginatedChannels = (channels) => {
const state = this.state;
const {state} = this;
const currentChannels = state.chatChannels;
const currentChannelIds = currentChannels.map((channel) => channel.id);
const newChannels = currentChannels;
@ -408,8 +417,10 @@ export default class Chat extends Component {
unopenedChannelIds,
} = this.state;
const receivedChatChannelId = message.chat_channel_id;
const messageList = document.getElementById('messagelist')
const nearBottom = (messageList.scrollTop + messageList.offsetHeight + 400 > messageList.scrollHeight)
const messageList = document.getElementById('messagelist');
const nearBottom =
messageList.scrollTop + messageList.offsetHeight + 400 >
messageList.scrollHeight;
if (nearBottom) {
scrollToBottom();
}
@ -431,7 +442,9 @@ export default class Chat extends Component {
}
}
const newShowAlert =
activeChannelId === receivedChatChannelId ? { showAlert: !nearBottom } : {};
activeChannelId === receivedChatChannelId
? { showAlert: !nearBottom }
: {};
let newMessageChannelIndex = 0;
let newMessageChannel = null;
const newChannelsObj = chatChannels.map((channel, index) => {
@ -506,7 +519,7 @@ export default class Chat extends Component {
getChannels(
filterQuery,
activeChannelId,
this.props,
'',
channelPaginationNum,
filters,
this.loadPaginatedChannels,
@ -546,17 +559,28 @@ export default class Chat extends Component {
e.preventDefault();
}
}
if (leftPressed && activeContent[activeChannelId] && e.target.value === '' && document.getElementById('activecontent-iframe')) {
if (
leftPressed &&
activeContent[activeChannelId] &&
e.target.value === '' &&
document.getElementById('activecontent-iframe')
) {
e.preventDefault();
try {
e.target.value = document.getElementById('activecontent-iframe').contentWindow.location.href
} catch(err){
e.target.value = activeContent[activeChannelId].path
e.target.value = document.getElementById(
'activecontent-iframe',
).contentWindow.location.href;
} catch (err) {
e.target.value = activeContent[activeChannelId].path;
}
}
if (rightPressed && !activeContent[activeChannelId] && e.target.value === '') {
if (
rightPressed &&
!activeContent[activeChannelId] &&
e.target.value === ''
) {
e.preventDefault();
const richLinks = document.querySelectorAll(".chatchannels__richlink");
const richLinks = document.querySelectorAll('.chatchannels__richlink');
if (richLinks.length === 0) {
return;
}
@ -570,9 +594,11 @@ export default class Chat extends Component {
}
if (escPressed && activeContent[activeChannelId]) {
this.setActiveContentState(activeChannelId, null);
this.setState({fullscreenContent: null, expanded: window.innerWidth > 600});
this.setState({
fullscreenContent: null,
expanded: window.innerWidth > 600,
});
}
};
handleKeyDownEdit = (e) => {
@ -620,7 +646,7 @@ export default class Chat extends Component {
} else if (message.startsWith('/play ')) {
const messageObject = {
activeChannelId,
message: message,
message,
mentionedUsersId: this.getMentionedUsers(message),
};
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
@ -637,7 +663,7 @@ export default class Chat extends Component {
type_of: 'loading-post',
});
this.setActiveContent({
path: '/search?q=' + message.replace('/search ', ''),
path: `/search?q=${message.replace('/search ', '')}`,
type_of: 'article',
});
} else if (message.startsWith('/s ')) {
@ -645,7 +671,7 @@ export default class Chat extends Component {
type_of: 'loading-post',
});
this.setActiveContent({
path: '/search?q=' + message.replace('/s ', ''),
path: `/search?q=${message.replace('/s ', '')}`,
type_of: 'article',
});
} else if (message.startsWith('/')) {
@ -672,7 +698,7 @@ export default class Chat extends Component {
message,
mentionedUsersId: this.getMentionedUsers(message),
};
this.setState({scrolled: false, showAlert: false})
this.setState({ scrolled: false, showAlert: false });
sendMessage(messageObject, this.handleSuccess, this.handleFailure);
}
};
@ -695,6 +721,7 @@ export default class Chat extends Component {
isMobileDevice,
unopenedChannelIds,
activeChannelId,
currentUserId,
} = this.state;
const newUnopenedChannelIds = unopenedChannelIds;
const index = newUnopenedChannelIds.indexOf(id);
@ -702,7 +729,11 @@ export default class Chat extends Component {
newUnopenedChannelIds.splice(index, 1);
}
this.setState({
activeChannel: this.filterForActiveChannel(chatChannels, id),
activeChannel: this.filterForActiveChannel(
chatChannels,
id,
currentUserId,
),
activeChannelId: parseInt(id, 10),
scrolled: false,
showAlert: false,
@ -805,16 +836,25 @@ export default class Chat extends Component {
e.stopPropagation();
const { activeChannelId, activeChannel } = this.state;
if (target.dataset.content.startsWith('chat_channels/')) {
if (content.startsWith('chat_channels/')) {
this.setActiveContentState(activeChannelId, {
type_of: 'loading-user',
});
getContent(
`/${target.dataset.content}/channel_info`,
this.setActiveContent,
null,
);
} else if (target.dataset.content === 'sidecar_all') {
getContent(`/${content}/channel_info`, this.setActiveContent, null);
} else if (content === 'sidecar-channel-request') {
this.setActiveContent({
data: {
user: getCurrentUser(),
channel: {
id: target.dataset.channelId,
name: target.dataset.channelName,
status: target.dataset.channelStatus,
},
},
handleJoiningRequest: this.handleJoiningRequest,
type_of: 'channel-request',
});
} else if (content === 'sidecar_all') {
this.setActiveContentState(activeChannelId, {
type_of: 'loading-post',
});
@ -847,11 +887,17 @@ export default class Chat extends Component {
});
} else if (target.dataset.content === 'exit') {
this.setActiveContentState(activeChannelId, null);
this.setState({ fullscreenContent: null, expanded: window.innerWidth > 600 });
this.setState({
fullscreenContent: null,
expanded: window.innerWidth > 600,
});
} else if (target.dataset.content === 'fullscreen') {
const mode =
this.state.fullscreenContent === 'sidecar' ? null : 'sidecar';
this.setState({ fullscreenContent: mode, expanded: (mode === null || window.innerWidth > 1600) });
this.setState({
fullscreenContent: mode,
expanded: mode === null || window.innerWidth > 1600,
});
}
}
document.getElementById('messageform').focus();
@ -919,7 +965,18 @@ export default class Chat extends Component {
fetchingPaginatedChannels: false,
});
const filters = type === 'all' ? {} : { filters: `channel_type:${type}` };
getChannels(filterQuery, null, this.props, 0, filters, this.loadChannels);
if (filterQuery && type !== 'direct') {
getChannels(
filterQuery,
null,
'discoverable',
0,
filters,
this.loadChannels,
);
} else {
getChannels(filterQuery, null, '', 0, filters, this.loadChannels);
}
};
handleFailure = (err) => {
@ -1021,14 +1078,18 @@ export default class Chat extends Component {
channelTypeFilter === 'all'
? {}
: { filters: `channel_type:${channelTypeFilter}` };
getChannels(
e.target.value,
null,
this.props,
0,
filters,
this.loadChannels,
);
if (e.target.value) {
getChannels(
e.target.value,
null,
'discoverable',
0,
filters,
this.loadChannels,
);
} else {
getChannels(e.target.value, null, '', 0, filters, this.loadChannels);
}
};
toggleExpand = () => {
@ -1053,6 +1114,9 @@ export default class Chat extends Component {
setTimeout(function () {
document.getElementById('chatchannelsearchbar').focus();
}, 100);
} else {
getChannels('', null, '', 0, '', this.loadChannels);
this.setState({ filterQuery: '' });
}
this.setState({ searchShowing: !this.state.searchShowing });
};
@ -1156,31 +1220,33 @@ export default class Chat extends Component {
channelsLoaded={state.channelsLoaded}
filterQuery={state.filterQuery}
expanded={state.expanded}
currentUserId={state.currentUserId}
triggerActiveContent={this.triggerActiveContent}
/>
{notificationsState}
</div>
);
}
return (
<div className="chat__channels">
{notificationsButton}
<button
className="chat__channelstogglebutt"
onClick={this.toggleExpand}
style={{ width: '100%' }}
type="button"
>
{'>'}
</button>
<Channels
activeChannelId={state.activeChannelId}
chatChannels={state.chatChannels}
unopenedChannelIds={state.unopenedChannelIds}
handleSwitchChannel={this.handleSwitchChannel}
expanded={state.expanded}
/>
{notificationsState}
</div>
<div className="chat__channels">
{notificationsButton}
<button
className="chat__channelstogglebutt"
onClick={this.toggleExpand}
style={{ width: '100%' }}
type="button"
>
{'>'}
</button>
<Channels
activeChannelId={state.activeChannelId}
chatChannels={state.chatChannels}
unopenedChannelIds={state.unopenedChannelIds}
handleSwitchChannel={this.handleSwitchChannel}
expanded={state.expanded}
/>
{notificationsState}
</div>
);
}
return '';
@ -1320,11 +1386,18 @@ export default class Chat extends Component {
onTriggerVideoContent = (e) => {
if (e.target.dataset.content === 'exit') {
this.setState({ videoPath: null, fullscreenContent: null, expanded: window.innerWidth > 600 });
this.setState({
videoPath: null,
fullscreenContent: null,
expanded: window.innerWidth > 600,
});
} else if (this.state.fullscreenContent === 'video') {
this.setState({ fullscreenContent: null });
} else {
this.setState({ fullscreenContent: 'video', expanded: window.innerWidth > 1600 });
this.setState({
fullscreenContent: 'video',
expanded: window.innerWidth > 1600,
});
}
};
@ -1551,6 +1624,21 @@ export default class Chat extends Component {
this.setState({ showDeleteModal: false });
};
handleJoiningRequest = (e) => {
sendChannelRequest(
e.target.dataset.channelId,
this.handleJoiningRequestSuccess,
null,
);
};
handleJoiningRequestSuccess = () => {
const { activeChannelId } = this.state;
this.setActiveContentState(activeChannelId, null);
this.setState({ fullscreenContent: null });
this.toggleSearchShowing();
};
renderChannelHeaderInner = () => {
const { activeChannel } = this.state;
if (activeChannel.channel_type === 'direct') {

View file

@ -0,0 +1,98 @@
import { h } from 'preact';
import PropTypes from 'prop-types';
const ChannelButton = ({
channel,
handleSwitchChannel,
otherClassname,
newMessagesIndicator,
isUnopened,
discoverableChannel,
triggerActiveContent,
}) => {
return (
<button
type="button"
key={channel.id}
className="chatchanneltabbutton"
onClick={discoverableChannel ? triggerActiveContent : handleSwitchChannel}
data-content="sidecar-channel-request"
data-channel-id={channel.chat_channel_id}
data-channel-slug={channel.channel_modified_slug}
data-channel-status={channel.status}
data-channel-name={channel.channel_name}
>
<span
className={
discoverableChannel
? 'chatchanneltab chatchanneltab--inactive'
: `chatchanneltab ${otherClassname} chatchanneltab--${newMessagesIndicator}`
}
data-channel-id={channel.chat_channel_id}
data-content="sidecar-channel-request"
data-channel-slug={channel.channel_modified_slug}
data-channel-status={channel.status}
data-channel-name={channel.channel_name}
style={{
border: `1px solid ${channel.channel_color}`,
boxShadow: `3px 3px 0px ${channel.channel_color}`,
}}
>
<span
data-channel-slug={channel.channel_modified_slug}
className={
discoverableChannel
? 'chatchanneltabindicator'
: `chatchanneltabindicator chatchanneltabindicator--${newMessagesIndicator}`
}
data-channel-id={channel.chat_channel_id}
>
<img
src={channel.channel_image}
alt="pic"
className={
channel.channel_type === 'direct'
? 'chatchanneltabindicatordirectimage'
: 'chatchanneltabindicatordirectimage invert-channel-image'
}
/>
</span>
{isUnopened ? (
<span className="crayons-indicator crayons-indicator--accent crayons-indicator--bullet" />
) : (
''
)}
{channel.channel_name}
</span>
</button>
);
};
ChannelButton.propTypes = {
channel: PropTypes.shape({
channel_name: PropTypes.string,
channel_color: PropTypes.string,
channel_type: PropTypes.string,
channel_modified_slug: PropTypes.string,
id: PropTypes.number,
chat_channel_id: PropTypes.number,
status: PropTypes.string,
channel_image: PropTypes.string,
}).isRequired,
discoverableChannel: PropTypes.bool,
handleSwitchChannel: PropTypes.func,
triggerActiveContent: PropTypes.func,
newMessagesIndicator: PropTypes.string,
otherClassname: PropTypes.string,
isUnopened: PropTypes.string,
};
ChannelButton.defaultProps = {
otherClassname: '',
isUnopened: '',
newMessagesIndicator: '',
discoverableChannel: false,
handleSwitchChannel: null,
triggerActiveContent: null,
};
export default ChannelButton;

View file

@ -1,8 +1,7 @@
import { h, Component } from 'preact';
import PropTypes from 'prop-types';
import Article from './article';
import ChannelRequest from './channelRequest';
export default class Content extends Component {
static propTypes = {
resource: PropTypes.object,
@ -12,28 +11,53 @@ export default class Content extends Component {
};
render() {
const { onTriggerContent, fullscreen } = this.props;
if (!this.props.resource) {
return '';
}
const fullScreenIcon = this.props.fullscreen ? <svg data-content="fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="fullscreen" fill="none" d="M0 0h24v24H0z"/><path data-content="fullscreen" d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"/></svg> : <svg data-content="fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="fullscreen" fill="none" d="M0 0h24v24H0z"/><path data-content="fullscreen" d="M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"/></svg>;
const smartSvgIcon = (content, d) => (
<svg
data-content={content}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path data-content={content} fill="none" d="M0 0h24v24H0z" />
<path data-content={content} d={d} />
</svg>
);
return (
<div
className="activechatchannel__activecontent activechatchannel__activecontent--sidecar"
id="chat_activecontent"
onClick={this.props.onTriggerContent}
onClick={onTriggerContent}
>
<button
className="activechatchannel__activecontentexitbutton crayons-btn crayons-btn--secondary"
data-content="exit"
>
<svg data-content="exit" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="exit" fill="none" d="M0 0h24v24H0z"/><path data-content="exit" d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
{smartSvgIcon(
'exit',
'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z',
)}
</button>
<button
className="activechatchannel__activecontentexitbutton activechatchannel__activecontentexitbutton--fullscreen crayons-btn crayons-btn--secondary"
data-content="fullscreen"
style={{left: "39px"}}
style={{ left: '39px' }}
>
{fullScreenIcon}
{' '}
{fullscreen
? smartSvgIcon(
'fullscreen',
'M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z',
)
: smartSvgIcon(
'fullscreen',
'M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z',
)}
</button>
{display(this.props)}
</div>
@ -42,21 +66,19 @@ export default class Content extends Component {
}
function display(props) {
if (props.resource.type_of === 'loading-user') {
const { resource } = props;
if (resource.type_of === 'loading-user') {
return <div className="loading-user" />;
}
if (resource.type_of === 'article') {
return <Article resource={resource} />;
}
if (resource.type_of === 'channel-request') {
return (
<div
style={{
height: '210px',
width: '210px',
margin: ' 15px auto',
display: 'block',
borderRadius: '500px',
backgroundColor: '#f5f6f7',
}}
<ChannelRequest
resource={resource.data}
handleJoiningRequest={resource.handleJoiningRequest}
/>
);
}
if (props.resource.type_of === 'article') {
return <Article resource={props.resource} />;
}
}

View file

@ -30,6 +30,11 @@ const getWaitOnUserDataHandler = ({ resolve, reject, waitTime = 20 }) => {
};
};
export const getCurrentUser = () => {
const { user } = document.body.dataset;
return JSON.parse(user);
};
export function getUserDataAndCsrfToken() {
return new Promise((resolve, reject) => {
getWaitOnUserDataHandler({ resolve, reject })();
@ -52,7 +57,7 @@ export function setupObserver(callback) {
export function hideMessages(messages, userId) {
const cleanedMessages = Object.keys(messages).reduce(
(accumulator, channelId) => {
const newMessages = messages[channelId].map(message => {
const newMessages = messages[channelId].map((message) => {
if (message.user_id === userId) {
const messageClone = Object.assign({ type: 'hidden' }, message);
messageClone.message = '<message removed>';
@ -79,3 +84,26 @@ export function adjustTimestamp(timestamp) {
time = new Intl.DateTimeFormat('en-US', options).format(time);
return time;
}
export const channelSorter = (channels, currentUserId, filterQuery) => {
const activeChannels = channels.filter(
(channel) =>
channel.viewable_by === currentUserId && channel.status === 'active',
);
const joiningChannels = channels.filter(
(channel) => channel.status === 'joining_request',
);
const ChannelIds = [
[...new Set(activeChannels.map((x) => x.chat_channel_id))],
[...new Set(joiningChannels.map((x) => x.chat_channel_id))],
];
const discoverableChannels = channels
.filter(
(channel) =>
(channel.status === 'joining_request' && filterQuery) ||
(!ChannelIds[1].includes(channel.chat_channel_id) &&
channel.viewable_by !== currentUserId),
)
.filter((channel) => !ChannelIds[0].includes(channel.chat_channel_id));
return { activeChannels, discoverableChannels };
};

View file

@ -1,12 +1,23 @@
import { h, Component } from 'preact';
export default class VideoContent extends Component {
render() {
if (!this.props.videoPath) {
return ""
return '';
}
const fullScreenIcon = this.props.fullscreen ? <svg data-content="fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="fullscreen" fill="none" d="M0 0h24v24H0z"/><path data-content="fullscreen" d="M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z"/></svg> : <svg data-content="fullscreen" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="fullscreen" fill="none" d="M0 0h24v24H0z"/><path data-content="fullscreen" d="M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z"/></svg>;
const smartSvgIcon = (content, d) => (
<svg
data-content={content}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="24"
height="24"
>
<path data-content={content} fill="none" d="M0 0h24v24H0z" />
<path data-content={content} d={d} />
</svg>
);
return (
<div
className="activechatchannel__activecontent activechatchannel__activecontent--video"
@ -17,14 +28,25 @@ export default class VideoContent extends Component {
className="activechatchannel__activecontentexitbutton crayons-btn crayons-btn--secondary"
data-content="exit"
>
<svg data-content="exit" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24"><path data-content="exit" fill="none" d="M0 0h24v24H0z"/><path data-content="exit" d="M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z"/></svg>
{smartSvgIcon(
'exit',
'M12 10.586l4.95-4.95 1.414 1.414-4.95 4.95 4.95 4.95-1.414 1.414-4.95-4.95-4.95 4.95-1.414-1.414 4.95-4.95-4.95-4.95L7.05 5.636z',
)}
</button>
<button
className="activechatchannel__activecontentexitbutton activechatchannel__activecontentexitbutton--fullscreen crayons-btn crayons-btn--secondary"
data-content="fullscreen"
style={{left: "39px"}}
style={{ left: '39px' }}
>
{fullScreenIcon}
{fullscreen
? smartSvgIcon(
'fullscreen',
'M18 7h4v2h-6V3h2v4zM8 9H2V7h4V3h2v6zm10 8v4h-2v-6h6v2h-4zM8 15v6H6v-4H2v-2h6z',
)
: smartSvgIcon(
'fullscreen',
'M20 3h2v6h-2V5h-4V3h4zM4 3h4v2H4v4H2V3h2zm16 16v-4h2v6h-6v-2h4zM4 19h4v2H2v-6h2v4z',
)}
</button>
<iframe src={this.props.videoPath} />
</div>

View file

@ -3,7 +3,6 @@ import Chat from '../chat/chat';
function loadElement() {
const root = document.getElementById('chat');
if (root) {
render(<Chat {...root.dataset} />, root, root.firstChild);
const placeholder = document.getElementById('chat_placeholder');

View file

@ -9,6 +9,7 @@ class ChatChannel < ApplicationRecord
has_many :pending_memberships, -> { where status: "pending" }, class_name: "ChatChannelMembership", inverse_of: :chat_channel
has_many :rejected_memberships, -> { where status: "rejected" }, class_name: "ChatChannelMembership", inverse_of: :chat_channel
has_many :mod_memberships, -> { where role: "mod" }, class_name: "ChatChannelMembership", inverse_of: :chat_channel
has_many :requested_memberships, -> { where status: "joining_request" }, class_name: "ChatChannelMembership", inverse_of: :chat_channel
has_many :active_users, through: :active_memberships, class_name: "User", source: :user
has_many :pending_users, through: :pending_memberships, class_name: "User", source: :user
has_many :rejected_users, through: :rejected_memberships, class_name: "User", source: :user

View file

@ -10,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 removed_from_channel] }
validates :status, inclusion: { in: %w[active inactive pending rejected left_channel removed_from_channel joining_request] }
validates :role, inclusion: { in: %w[member mod] }
validate :permission
@ -70,6 +70,10 @@ class ChatChannelMembership < ApplicationRecord
user_id
end
def channel_discoverable
chat_channel.discoverable
end
private
def channel_color

View file

@ -24,7 +24,7 @@ class ChatChannelPolicy < ApplicationPolicy
end
def permitted_attributes
%i[channel_name slug command description]
%i[channel_name slug command description discoverable]
end
def create_chat?

View file

@ -5,6 +5,6 @@ module Search
attributes :id, :status, :viewable_by, :chat_channel_id, :last_opened_at,
:channel_text, :channel_last_message_at, :channel_status,
:channel_status, :channel_type, :channel_username, :channel_name,
:channel_image, :channel_modified_slug, :channel_messages_count
:channel_image, :channel_modified_slug, :channel_discoverable, :channel_messages_count
end
end

View file

@ -20,11 +20,11 @@ module Search
def initialize(params:)
@params = params.deep_symbolize_keys
@params[:viewable_by] = params[:user_id]
@params[:viewable_by] = @params[:user_id]
# TODO: @mstruve: When we want to allow people like admins to
# search ALL memberships this will need to change
@params[:status] = "active"
@params[:status] = %w[active joining_request]
build_body
end
@ -41,7 +41,11 @@ module Search
FILTER_KEYS.map do |filter_key|
next if @params[filter_key].blank? || @params[filter_key] == "all"
{ term: { filter_key => @params[filter_key] } }
if %i[viewable_by status].include? filter_key
{ terms: { filter_key => @params[filter_key] } }
else
{ term: { filter_key => @params[filter_key] } }
end
end.compact
end
end

View file

@ -37,7 +37,6 @@
</div>
<% end %>
</div>
<% if @membership.role == "mod" %>
<div class="crayons-card p-4 grid gap-2 mb-4">
<h3>Pending Invitations</h3>
@ -57,30 +56,66 @@
<% end %>
</div>
<% end %>
</div>
<div class="crayons-card p-4 grid gap-2 mb-4">
<h3>Joining Request</h3>
<% @channel.requested_memberships.includes(:user).each do |requested_membership| %>
<div class="flex items-center">
<a href="<%= requested_membership.user.path %>">
<span class="crayons-avatar crayons-avatar--l mr-2">
<img src="<%= ProfileImage.new(requested_membership.user).get(width: 90) %>" class="crayons-avatar__image" />
</span>
<%= requested_membership.user.name %>
</a>
<%= form_tag "/chat_channel_memberships/add_membership", class: "inline-button-form" do %>
<%= hidden_field_tag(:chat_channel_id, @channel.id) %>
<%= hidden_field_tag(:membership_id, requested_membership.id) %>
<%= hidden_field(:chat_channel_membership, :user_action, value: "accept") %>
<button class="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost" type="submit">+</button>
<% end %>
<%= 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, requested_membership.id) %>
<button class="crayons-btn crayons-btn--icon-rounded crayons-btn--ghost" type="submit">x</button>
<% end %>
</div>
<% end %>
</div>
<div class="crayons-card p-4 grid gap-2 mb-4">
<% if @channel.private_org_channel? %>
<p><em>Invite new members by <a href="/settings/organization">inviting them to this organization.</a></em></p>
<% else %>
<%= form_for ChatChannelMembership.new, html: { class: "grid gap-4" } do |f| %>
<div class="crayons-field">
<%= f.hidden_field :chat_channel_id, value: @channel.id %>
<%= f.label "Usernames to Invite", class: "crayons-field__label" %>
<%= f.text_field :invitation_usernames, placeholder: "Comma separated", class: "crayons-textfield" %>
</div>
<div><button type="submit" class="crayons-btn">Submit</button></div>
<% end %>
<%= form_for ChatChannelMembership.new, html: { class: "grid gap-4" } do |f| %>
<div class="crayons-field">
<%= f.hidden_field :chat_channel_id, value: @channel.id %>
<%= f.label "Usernames to Invite", class: "crayons-field__label" %>
<%= f.text_field :invitation_usernames, placeholder: "Comma separated", class: "crayons-textfield" %>
</div>
<div><button type="submit" class="crayons-btn">Submit</button></div>
<% end %>
</div>
<h3>Channel Settings</h3>
<div class="crayons-card p-4 grid gap-2 mb-4">
<h3>Channel Settings</h3>
<%= form_for(@channel) do |f| %>
<div class="crayons-field">
<%= f.label :description, class: "crayons-field__label" %>
<%= f.text_area :description, class: "crayons-textfield" %>
</div>
<div>
<button type="submit" class="crayons-btn">Submit</button>
<div class="crayons-field">
<%= f.label :description, class: "crayons-field__label" %>
<%= f.text_area :description, class: "crayons-textfield" %>
</div>
<div class="crayons-card p-4 grid gap-2 mb-4">
<div class="crayons-field crayons-field--checkbox">
<%= f.check_box :discoverable, class: "crayons-checkbox" %>
<%= f.label :discoverable, "Channel Discoverable", class: "crayons-field__label" %>
</div>
</div>
<div>
<button type="submit" class="crayons-btn">Submit</button>
</div>
<% end %>
<% end %>
</div>
<% end %>

View file

@ -10,6 +10,10 @@
<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 <%= community_name %>.
</h2>
<% elsif @membership.status == "joining_request" %>
<h2 style="font-size: 25px; text-align: center;">
Your request to join <%= community_name %> has been accepted.
</h2>
<% else %>
<h2 style="font-size: 25px; text-align: center;">
You have been invited to a Connect group on <%= community_name %>.

View file

@ -51,6 +51,9 @@
"channel_messages_count": {
"type": "integer"
},
"channel_discoverable": {
"type": "boolean"
},
"last_indexed_at": {
"type": "date"
}

View file

@ -265,6 +265,8 @@ Rails.application.routes.draw do
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"
post "/chat_channel_memberships/add_membership" => "chat_channel_memberships#add_membership"
post "/join_chat_channel" => "chat_channel_memberships#join_channel"
delete "/messages/:id" => "messages#destroy"
patch "/messages/:id" => "messages#update"
get "/live/:username" => "twitch_live_streams#show"

View file

@ -0,0 +1,5 @@
class AddColumnDiscoverableToChatChannels < ActiveRecord::Migration[5.2]
def change
add_column :chat_channels, :discoverable, :boolean, :default => false
end
end

View file

@ -304,6 +304,7 @@ ActiveRecord::Schema.define(version: 2020_05_04_075409) do
t.string "channel_type", null: false
t.datetime "created_at", null: false
t.string "description"
t.boolean "discoverable", default: false
t.datetime "last_message_at", default: "2017-01-01 05:00:00"
t.string "slug"
t.string "status", default: "active"

View file

@ -321,4 +321,79 @@ RSpec.describe "ChatChannelMemberships", type: :request do
end
end
end
describe "POST /chat_channel_memberships/add_membership" do
context "when user is moderator of channel" do
it "adds requested member to join closed channel" do
allow(Pusher).to receive(:trigger).and_return(true)
channel = ChatChannel.first
membership = ChatChannelMembership.last
membership.update(status: "joining_request")
membership.update(role: "mod")
post "/chat_channel_memberships/add_membership", params: {
membership_id: membership.id,
chat_channel_id: channel.id,
chat_channel_membership: {
user_action: "accept"
}
}
expect(ChatChannelMembership.find(membership.id).status).to eq("active")
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
expect do
channel = ChatChannel.first
membership = ChatChannelMembership.last
membership.update(status: "joining_request")
post "/chat_channel_memberships/add_membership", params: {
membership_id: membership.id,
chat_channel_id: channel.id,
chat_channel_membership: {
user_action: "accept"
}
}
end.to(raise_error(Pundit::NotAuthorizedError))
end
end
end
describe "POST /join_chat_channel" do
let(:chat_channel_membership) do
{
chat_channel_id: chat_channel.id
}
end
before do
allow(Pusher).to receive(:trigger).and_return(true)
sign_in second_user
post "/join_chat_channel", params: { chat_channel_membership: chat_channel_membership }
end
context "when user was not member of closed channel" do
it "requested to join closed channel" do
expect(ChatChannelMembership.last.status).to eq("joining_request")
expect(response.status).to eq(200)
end
it "returns in json" do
expect(response.content_type).to eq("application/json")
end
end
context "when user was a member of channel, and than left channel" do
it "requested to join closed channel" do
ChatChannelMembership.create(chat_channel_id: chat_channel.id, user_id: second_user.id, status: "left_channel")
expect(ChatChannelMembership.last.status).to eq("joining_request")
expect(response.status).to eq(200)
end
it "returns in json" do
expect(response.content_type).to eq("application/json")
end
end
end
end

View file

@ -51,6 +51,17 @@ RSpec.describe "ChatChannels", type: :request do
end
end
describe "get /chat_channels?state=joining_request" do
it "returns joining request channels" do
membership = ChatChannelMembership.create(chat_channel_id: invite_channel.id, user_id: user.id, status: "joining_request", role: "mod")
membership.chat_channel.update(discoverable: true)
sign_in user
get "/chat_channels?state=joining_request"
expect(response.body).to include("\"status\":\"joining_request\"")
expect(response.body).to include("joining_requests")
end
end
describe "get /chat_channels?state=unopened_ids" do
it "returns unopened chat channel ids" do
direct_channel.add_users [user]

View file

@ -18,7 +18,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
allow(chat_channel_membership1).to receive(:channel_text).and_return("a name")
allow(chat_channel_membership2).to receive(:channel_text).and_return("another name and slug")
index_documents([chat_channel_membership1, chat_channel_membership2])
name_params = { size: 5, channel_text: "name", user_id: user.id }
name_params = { size: 5, channel_text: "name", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: name_params)
expect(chat_channel_membership_docs.count).to eq(2)
@ -32,7 +32,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
new_user = create(:user)
chat_channel_membership3 = create(:chat_channel_membership, user_id: new_user.id)
index_documents([chat_channel_membership1, chat_channel_membership2, chat_channel_membership3])
params = { size: 5, user_id: new_user.id }
params = { size: 5, user_id: [new_user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -42,7 +42,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
it "searches by channel_status" do
allow(chat_channel_membership1).to receive(:channel_status).and_return("popping")
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { size: 5, channel_status: "popping", user_id: user.id }
params = { size: 5, channel_status: "popping", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -52,7 +52,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
it "searches by channel_type" do
allow(chat_channel_membership2).to receive(:channel_type).and_return("invite_only")
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { size: 5, channel_type: "invite_only", user_id: user.id }
params = { size: 5, channel_type: "invite_only", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -63,7 +63,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
chat_channel_membership1.update(status: "inactive")
chat_channel_membership2.update(status: "active")
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { size: 5, user_id: user.id }
params = { size: 5, user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -78,7 +78,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
chat_channel_membership1.update(status: "active")
chat_channel_membership2.update(status: "inactive")
index_documents([chat_channel_membership1, chat_channel_membership2])
name_params = { size: 5, channel_text: "name", status: "active", user_id: user.id }
name_params = { size: 5, channel_text: "name", status: "active", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: name_params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -91,7 +91,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
allow(chat_channel_membership1).to receive(:channel_type).and_return("not_direct")
allow(chat_channel_membership2).to receive(:channel_type).and_return("direct")
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { size: 5, sort_by: "channel_type", sort_direction: "asc", user_id: user.id }
params = { size: 5, sort_by: "channel_type", sort_direction: "asc", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(2)
@ -103,7 +103,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
allow(chat_channel_membership1).to receive(:channel_last_message_at).and_return(Time.current)
allow(chat_channel_membership2).to receive(:channel_last_message_at).and_return(1.year.ago)
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { size: 5, user_id: user.id }
params = { size: 5, user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(2)
@ -113,7 +113,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
it "will return a set number of docs based on pagination params" do
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { page: 0, per_page: 1, user_id: user.id }
params = { page: 0, per_page: 1, user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs.count).to eq(1)
@ -123,12 +123,12 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
allow(chat_channel_membership1).to receive(:channel_last_message_at).and_return(Time.current)
allow(chat_channel_membership2).to receive(:channel_last_message_at).and_return(1.year.ago)
index_documents([chat_channel_membership1, chat_channel_membership2])
first_page_params = { page: 0, per_page: 1, sort_by: "channel_last_message_at", order: "dsc", user_id: user.id }
first_page_params = { page: 0, per_page: 1, sort_by: "channel_last_message_at", order: "dsc", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: first_page_params)
expect(chat_channel_membership_docs.first["id"]).to eq(chat_channel_membership1.id)
second_page_params = { page: 1, per_page: 1, sort_by: "channel_last_message_at", order: "dsc", user_id: user.id }
second_page_params = { page: 1, per_page: 1, sort_by: "channel_last_message_at", order: "dsc", user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: second_page_params)
expect(chat_channel_membership_docs.first["id"]).to eq(chat_channel_membership2.id)
@ -138,7 +138,7 @@ RSpec.describe Search::ChatChannelMembership, type: :service do
allow(chat_channel_membership1).to receive(:channel_last_message_at).and_return(Time.current)
allow(chat_channel_membership2).to receive(:channel_last_message_at).and_return(1.year.ago)
index_documents([chat_channel_membership1, chat_channel_membership2])
params = { page: 3, per_page: 1, user_id: user.id }
params = { page: 3, per_page: 1, user_id: [user.id] }
chat_channel_membership_docs = described_class.search_documents(params: params)
expect(chat_channel_membership_docs).to eq([])

View file

@ -21,8 +21,8 @@ RSpec.describe Search::QueryBuilders::ChatChannelMembership, type: :service do
expected_filters = [
{ "term" => { "channel_status" => "active" } },
{ "term" => { "channel_type" => "direct" } },
{ "term" => { "status" => "active" } },
{ "term" => { "viewable_by" => 1 } },
{ "terms" => { "status" => %w[active joining_request] } },
{ "terms" => { "viewable_by" => 1 } },
]
expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(expected_filters)
end
@ -46,7 +46,7 @@ RSpec.describe Search::QueryBuilders::ChatChannelMembership, type: :service do
"query" => "a_name*", "fields" => [:channel_text], "lenient" => true, "analyze_wildcard" => true
}
}]
expected_filters = [{ "term" => { "channel_status" => "active" } }, { "term" => { "viewable_by" => 1 } }, { "term" => { "status" => "active" } }]
expected_filters = [{ "term" => { "channel_status" => "active" } }, { "terms" => { "status" => %w[active joining_request] } }, { "terms" => { "viewable_by" => 1 } }]
expect(query.as_hash.dig("query", "bool", "must")).to match_array(expected_query)
expect(query.as_hash.dig("query", "bool", "filter")).to match_array(expected_filters)
end
@ -55,8 +55,18 @@ RSpec.describe Search::QueryBuilders::ChatChannelMembership, type: :service do
params = { user_id: 1 }
filter = described_class.new(params: params)
expected_filters = [
{ "term" => { "status" => "active" } },
{ "term" => { "viewable_by" => 1 } },
{ "terms" => { "status" => %w[active joining_request] } },
{ "terms" => { "viewable_by" => 1 } },
]
expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(expected_filters)
end
it "always applies channel discoverable and status params" do
params = { user_id: SiteConfig.mascot_user_id }
filter = described_class.new(params: params)
expected_filters = [
{ "terms" => { "status" => %w[active joining_request] } },
{ "terms" => { "viewable_by" => 1 } },
]
expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(expected_filters)
end
@ -65,8 +75,8 @@ RSpec.describe Search::QueryBuilders::ChatChannelMembership, type: :service do
params = { not_supported: "direct", user_id: 1 }
filter = described_class.new(params: params)
expected_filters = [
{ "term" => { "status" => "active" } },
{ "term" => { "viewable_by" => 1 } },
{ "terms" => { "status" => %w[active joining_request] } },
{ "terms" => { "viewable_by" => 1 } },
]
expect(filter.as_hash.dig("query", "bool", "filter")).to match_array(expected_filters)
end