mirror of
https://github.com/discourse/discourse.git
synced 2025-02-17 09:12:45 +08:00
FEATURE: my threads page (#24771)
This commit adds a new "My threads" link in sidebar and drawer. This link will open the "/chat/threads" page which contains all threads where the current user is a member. It's ordered by activity (unread and then last message created). Moreover, the threads list of a channel page is now showing every threads of a channel, and not just the ones where you are a member.
This commit is contained in:
parent
4949d85c15
commit
09277bc543
|
@ -0,0 +1,24 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class Chat::Api::CurrentUserThreadsController < Chat::ApiController
|
||||
def index
|
||||
with_service(::Chat::LookupUserThreads) do
|
||||
on_success do
|
||||
render_serialized(
|
||||
::Chat::ThreadsView.new(
|
||||
user: guardian.user,
|
||||
threads: result.threads,
|
||||
channel: result.channel,
|
||||
tracking: result.tracking,
|
||||
memberships: result.memberships,
|
||||
load_more_url: result.load_more_url,
|
||||
threads_participants: result.participants,
|
||||
),
|
||||
::Chat::ThreadListSerializer,
|
||||
root: false,
|
||||
)
|
||||
end
|
||||
on_model_not_found(:threads) { render json: success_json.merge(threads: []) }
|
||||
end
|
||||
end
|
||||
end
|
|
@ -11,6 +11,7 @@ module Chat
|
|||
scope: scope,
|
||||
membership: object.memberships.find { |m| m.thread_id == thread.id },
|
||||
include_thread_preview: true,
|
||||
include_channel: true,
|
||||
include_thread_original_message: true,
|
||||
participants: object.threads_participants[thread.id],
|
||||
root: nil,
|
||||
|
@ -23,7 +24,9 @@ module Chat
|
|||
end
|
||||
|
||||
def meta
|
||||
{ channel_id: object.channel.id, load_more_url: object.load_more_url }
|
||||
meta = { load_more_url: object.load_more_url }
|
||||
meta[:channel_id] = object.channel.id if object.channel
|
||||
meta
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -8,6 +8,7 @@ module Chat
|
|||
:title,
|
||||
:status,
|
||||
:channel_id,
|
||||
:channel,
|
||||
:meta,
|
||||
:reply_count,
|
||||
:current_user_membership,
|
||||
|
@ -22,6 +23,14 @@ module Chat
|
|||
@current_user_membership = opts[:membership]
|
||||
end
|
||||
|
||||
def include_channel?
|
||||
@options[:include_channel].presence || false
|
||||
end
|
||||
|
||||
def channel
|
||||
::Chat::ChannelSerializer.new(object.channel, scope: scope, root: false)
|
||||
end
|
||||
|
||||
def include_original_message?
|
||||
@opts[:include_thread_original_message].presence || true
|
||||
end
|
||||
|
|
|
@ -69,7 +69,6 @@ module Chat
|
|||
|
||||
def fetch_threads(guardian:, channel:, **)
|
||||
::Chat::Thread
|
||||
.strict_loading
|
||||
.includes(
|
||||
:channel,
|
||||
:user_chat_thread_memberships,
|
||||
|
@ -93,28 +92,26 @@ module Chat
|
|||
user: :user_status,
|
||||
],
|
||||
)
|
||||
.joins(:user_chat_thread_memberships, :original_message)
|
||||
.joins(
|
||||
"LEFT JOIN chat_messages AS last_message ON last_message.id = chat_threads.last_message_id",
|
||||
"LEFT JOIN user_chat_thread_memberships ON chat_threads.id = user_chat_thread_memberships.thread_id AND user_chat_thread_memberships.user_id = #{guardian.user.id} AND user_chat_thread_memberships.notification_level NOT IN (#{::Chat::UserChatThreadMembership.notification_levels[:muted]})",
|
||||
)
|
||||
.where("user_chat_thread_memberships.user_id = ?", guardian.user.id)
|
||||
.where(
|
||||
"user_chat_thread_memberships.notification_level IN (?)",
|
||||
[
|
||||
::Chat::UserChatThreadMembership.notification_levels[:normal],
|
||||
::Chat::UserChatThreadMembership.notification_levels[:tracking],
|
||||
],
|
||||
.joins(
|
||||
"LEFT JOIN chat_messages AS last_message ON chat_threads.last_message_id = last_message.id",
|
||||
)
|
||||
.where("chat_threads.channel_id = ?", channel.id)
|
||||
.joins(
|
||||
"INNER JOIN chat_messages AS original_message ON chat_threads.original_message_id = original_message.id",
|
||||
)
|
||||
.where(channel_id: channel.id)
|
||||
.where("original_message.chat_channel_id = chat_threads.channel_id")
|
||||
.where("original_message.deleted_at IS NULL")
|
||||
.where("last_message.chat_channel_id = chat_threads.channel_id")
|
||||
.where("last_message.deleted_at IS NULL")
|
||||
.where("chat_threads.replies_count > 0")
|
||||
.order(
|
||||
"CASE WHEN user_chat_thread_memberships.last_read_message_id IS NULL OR user_chat_thread_memberships.last_read_message_id < chat_threads.last_message_id THEN true ELSE false END DESC, last_message.created_at DESC",
|
||||
)
|
||||
.limit(context.limit)
|
||||
.offset(context.offset)
|
||||
.order(
|
||||
"CASE WHEN (
|
||||
chat_threads.last_message_id > user_chat_thread_memberships.last_read_message_id OR
|
||||
user_chat_thread_memberships.last_read_message_id IS NULL
|
||||
) THEN 0 ELSE 1 END, last_message.created_at DESC",
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_tracking(guardian:, threads:, **)
|
||||
|
|
145
plugins/chat/app/services/chat/lookup_user_threads.rb
Normal file
145
plugins/chat/app/services/chat/lookup_user_threads.rb
Normal file
|
@ -0,0 +1,145 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module Chat
|
||||
# Gets a list of threads for a user.
|
||||
#
|
||||
# Only threads that the user is a member of with a notification level
|
||||
# of normal or tracking will be returned.
|
||||
#
|
||||
# @example
|
||||
# Chat::LookupUserThreads.call(guardian: guardian, limit: 5, offset: 2)
|
||||
#
|
||||
class LookupUserThreads
|
||||
include Service::Base
|
||||
|
||||
THREADS_LIMIT = 10
|
||||
|
||||
# @!method call(guardian:, limit: nil, offset: nil)
|
||||
# @param [Guardian] guardian
|
||||
# @param [Integer] limit
|
||||
# @param [Integer] offset
|
||||
# @return [Service::Base::Context]
|
||||
|
||||
contract
|
||||
step :set_limit
|
||||
step :set_offset
|
||||
model :threads
|
||||
step :fetch_tracking
|
||||
step :fetch_memberships
|
||||
step :fetch_participants
|
||||
step :build_load_more_url
|
||||
|
||||
# @!visibility private
|
||||
class Contract
|
||||
attribute :limit, :integer
|
||||
attribute :offset, :integer
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def set_limit(contract:, **)
|
||||
context.limit = (contract.limit || THREADS_LIMIT).to_i.clamp(1, THREADS_LIMIT)
|
||||
end
|
||||
|
||||
def set_offset(contract:, **)
|
||||
context.offset = [contract.offset || 0, 0].max
|
||||
end
|
||||
|
||||
def fetch_threads(guardian:, **)
|
||||
::Chat::Thread
|
||||
.includes(
|
||||
:channel,
|
||||
:user_chat_thread_memberships,
|
||||
original_message_user: :user_status,
|
||||
last_message: [
|
||||
:uploads,
|
||||
:chat_webhook_event,
|
||||
:chat_channel,
|
||||
chat_mentions: {
|
||||
user: :user_status,
|
||||
},
|
||||
user: :user_status,
|
||||
],
|
||||
original_message: [
|
||||
:uploads,
|
||||
:chat_webhook_event,
|
||||
:chat_channel,
|
||||
chat_mentions: {
|
||||
user: :user_status,
|
||||
},
|
||||
user: :user_status,
|
||||
],
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN user_chat_thread_memberships ON chat_threads.id = user_chat_thread_memberships.thread_id",
|
||||
)
|
||||
.joins(
|
||||
"LEFT JOIN chat_messages AS last_message ON chat_threads.last_message_id = last_message.id",
|
||||
)
|
||||
.joins(
|
||||
"INNER JOIN chat_messages AS original_message ON chat_threads.original_message_id = original_message.id",
|
||||
)
|
||||
.where(
|
||||
channel_id:
|
||||
::Chat::Channel
|
||||
.joins(:user_chat_channel_memberships)
|
||||
.where(user_chat_channel_memberships: { user_id: guardian.user.id, following: true })
|
||||
.where.not("user_chat_channel_memberships.muted")
|
||||
.where(
|
||||
{
|
||||
chatable_type: "Category",
|
||||
threading_enabled: true,
|
||||
status: ::Chat::Channel.statuses[:open],
|
||||
},
|
||||
)
|
||||
.select(:id),
|
||||
)
|
||||
.where("original_message.chat_channel_id = chat_threads.channel_id")
|
||||
.where("original_message.deleted_at IS NULL")
|
||||
.where("last_message.chat_channel_id = chat_threads.channel_id")
|
||||
.where("last_message.deleted_at IS NULL")
|
||||
.where("chat_threads.replies_count > 0")
|
||||
.where("user_chat_thread_memberships.user_id = ?", guardian.user.id)
|
||||
.where(
|
||||
"user_chat_thread_memberships.notification_level IN (?)",
|
||||
[
|
||||
::Chat::UserChatThreadMembership.notification_levels[:normal],
|
||||
::Chat::UserChatThreadMembership.notification_levels[:tracking],
|
||||
],
|
||||
)
|
||||
.order(
|
||||
"CASE WHEN user_chat_thread_memberships.last_read_message_id IS NULL OR user_chat_thread_memberships.last_read_message_id < chat_threads.last_message_id THEN true ELSE false END DESC, last_message.created_at DESC",
|
||||
)
|
||||
.limit(context.limit)
|
||||
.offset(context.offset)
|
||||
end
|
||||
|
||||
def fetch_tracking(guardian:, threads:, **)
|
||||
context.tracking =
|
||||
::Chat::TrackingStateReportQuery.call(
|
||||
guardian: guardian,
|
||||
thread_ids: threads.map(&:id),
|
||||
include_threads: true,
|
||||
).thread_tracking
|
||||
end
|
||||
|
||||
def fetch_memberships(guardian:, threads:, **)
|
||||
context.memberships =
|
||||
::Chat::UserChatThreadMembership.where(
|
||||
thread_id: threads.map(&:id),
|
||||
user_id: guardian.user.id,
|
||||
)
|
||||
end
|
||||
|
||||
def fetch_participants(threads:, **)
|
||||
context.participants = ::Chat::ThreadParticipantQuery.call(thread_ids: threads.map(&:id))
|
||||
end
|
||||
|
||||
def build_load_more_url(contract:, **)
|
||||
load_more_params = { limit: context.limit, offset: context.offset + context.limit }.to_query
|
||||
|
||||
context.load_more_url =
|
||||
::URI::HTTP.build(path: "/chat/api/me/threads", query: load_more_params).request_uri
|
||||
end
|
||||
end
|
||||
end
|
|
@ -8,6 +8,8 @@ export default function () {
|
|||
});
|
||||
});
|
||||
|
||||
this.route("threads", { path: "/threads" });
|
||||
|
||||
this.route(
|
||||
"channel.info",
|
||||
{ path: "/c/:channelTitle/:channelId/info" },
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { get, hash } from "@ember/helper";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import UserStatusMessage from "discourse/components/user-status-message";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
|
||||
|
||||
export default class ChatChannelTitle extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get firstUser() {
|
||||
return this.args.channel.chatable.users[0];
|
||||
}
|
||||
|
||||
get users() {
|
||||
return this.args.channel.chatable.users;
|
||||
}
|
||||
|
||||
get groupDirectMessage() {
|
||||
return (
|
||||
this.args.channel.isDirectMessageChannel &&
|
||||
this.args.channel.chatable.group
|
||||
);
|
||||
}
|
||||
|
||||
get groupsDirectMessageTitle() {
|
||||
return this.args.channel.title || this.usernames;
|
||||
}
|
||||
|
||||
get usernames() {
|
||||
return this.users.mapBy("username").join(", ");
|
||||
}
|
||||
|
||||
get channelColorStyle() {
|
||||
return htmlSafe(`color: #${this.args.channel.chatable.color}`);
|
||||
}
|
||||
|
||||
get showUserStatus() {
|
||||
return !!(this.users.length === 1 && this.users[0].status);
|
||||
}
|
||||
|
||||
<template>
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__users-count">
|
||||
{{@channel.membershipsCount}}
|
||||
</span>
|
||||
{{else}}
|
||||
<div class="chat-channel-title__avatar">
|
||||
<ChatUserAvatar @user={{this.firstUser}} @interactive={{false}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.groupsDirectMessageTitle}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.firstUser.username}}
|
||||
</span>
|
||||
{{#if this.showUserStatus}}
|
||||
<UserStatusMessage
|
||||
@class="chat-channel-title__user-status-message"
|
||||
@status={{get this.users "0.status"}}
|
||||
@showDescription={{if this.site.mobileView "true"}}
|
||||
/>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="after-chat-channel-username"
|
||||
@outletArgs={{hash user=@user}}
|
||||
@tagName=""
|
||||
@connectorTagName=""
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @channel.isCategoryChannel}}
|
||||
<div class="chat-channel-title is-category">
|
||||
<span
|
||||
class="chat-channel-title__category-badge"
|
||||
style={{this.channelColorStyle}}
|
||||
>
|
||||
{{icon "d-chat"}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{icon "lock" class="chat-channel-title__restricted-category-icon"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="chat-channel-title__name">
|
||||
{{replaceEmoji @channel.title}}
|
||||
</span>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -100,6 +100,12 @@ export default class ChannelsList extends Component {
|
|||
}`;
|
||||
}
|
||||
|
||||
get hasUnreadThreads() {
|
||||
return this.chatChannelsManager.publicMessageChannels.some(
|
||||
(channel) => channel.unreadThreadsCount > 0
|
||||
);
|
||||
}
|
||||
|
||||
@action
|
||||
toggleChannelSection(section) {
|
||||
this.args.toggleSection(section);
|
||||
|
@ -160,6 +166,24 @@ export default class ChannelsList extends Component {
|
|||
{{didInsert this.computeHasScrollbar}}
|
||||
{{onResize this.computeResizedEntries}}
|
||||
>
|
||||
<div class="channels-list-container user-threads-section">
|
||||
<LinkTo @route="chat.threads" class="chat__user-threads-row-container">
|
||||
<div class="chat__user-threads-row">
|
||||
|
||||
<span class="chat__user-threads-row__title">
|
||||
{{dIcon "discourse-threads" class="chat__user-threads-row__icon"}}
|
||||
{{i18n "chat.my_threads.title"}}
|
||||
</span>
|
||||
|
||||
{{#if this.hasUnreadThreads}}
|
||||
<div class="chat__unread-indicator">
|
||||
<div class="chat__unread-indicator__number"> </div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</div>
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
{{#if this.displayPublicChannels}}
|
||||
<div class="chat-channel-divider public-channels-section">
|
||||
{{#if this.inSidebar}}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<div class="select-kit-header-wrapper">
|
||||
{{#if this.selectedContent}}
|
||||
<ChatChannelTitle @channel={{this.selectedContent}} />
|
||||
<ChannelTitle @channel={{this.selectedContent}} />
|
||||
{{else}}
|
||||
{{i18n "chat.incoming_webhooks.channel_placeholder"}}
|
||||
{{/if}}
|
||||
|
|
|
@ -1 +1 @@
|
|||
<ChatChannelTitle @channel={{this.item}} />
|
||||
<ChannelTitle @channel={{this.item}} />
|
|
@ -5,9 +5,9 @@ import { inject as service } from "@ember/service";
|
|||
import DButton from "discourse/components/d-button";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
|
||||
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
|
||||
export default class ChatChannelMessageEmojiPicker extends Component {
|
||||
@service chatChannelInfoRouteOriginManager;
|
||||
|
@ -64,7 +64,7 @@ export default class ChatChannelMessageEmojiPicker extends Component {
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
|
||||
{{#if this.canEditChannel}}
|
||||
<DButton
|
||||
|
|
|
@ -5,7 +5,7 @@ import { inject as service } from "@ember/service";
|
|||
import { isEmpty } from "@ember/utils";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import ChatChannelTitle from "./chat-channel-title";
|
||||
import ChannelTitle from "./channel-title";
|
||||
import ToggleChannelMembershipButton from "./toggle-channel-membership-button";
|
||||
|
||||
export default class ChatChannelPreviewCard extends Component {
|
||||
|
@ -27,7 +27,7 @@ export default class ChatChannelPreviewCard extends Component {
|
|||
(unless this.showJoinButton "-no-button")
|
||||
}}
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
{{#if this.hasDescription}}
|
||||
<p class="chat-channel-preview-card__description">
|
||||
{{@channel.description}}
|
||||
|
|
|
@ -15,8 +15,8 @@ import { bind } from "discourse-common/utils/decorators";
|
|||
import I18n from "discourse-i18n";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import eq from "truth-helpers/helpers/eq";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
import ChatChannelMetadata from "discourse/plugins/chat/discourse/components/chat-channel-metadata";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
import ToggleChannelMembershipButton from "discourse/plugins/chat/discourse/components/toggle-channel-membership-button";
|
||||
|
||||
const FADEOUT_CLASS = "-fade-out";
|
||||
|
@ -184,7 +184,7 @@ export default class ChatChannelRow extends Component {
|
|||
{{(if this.shouldReset (modifier this.onReset))}}
|
||||
style={{this.rowStyle}}
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
<ChatChannelMetadata @channel={{@channel}} @unreadIndicator={{true}} />
|
||||
|
||||
{{#if
|
||||
|
|
|
@ -1,111 +1,8 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { get, hash } from "@ember/helper";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import PluginOutlet from "discourse/components/plugin-outlet";
|
||||
import UserStatusMessage from "discourse/components/user-status-message";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-user-avatar";
|
||||
|
||||
export default class ChatChannelTitle extends Component {
|
||||
@service currentUser;
|
||||
|
||||
get firstUser() {
|
||||
return this.args.channel.chatable.users[0];
|
||||
}
|
||||
|
||||
get users() {
|
||||
return this.args.channel.chatable.users;
|
||||
}
|
||||
|
||||
get groupDirectMessage() {
|
||||
return (
|
||||
this.args.channel.isDirectMessageChannel &&
|
||||
this.args.channel.chatable.group
|
||||
);
|
||||
}
|
||||
|
||||
get groupsDirectMessageTitle() {
|
||||
return this.args.channel.title || this.usernames;
|
||||
}
|
||||
|
||||
get usernames() {
|
||||
return this.users.mapBy("username").join(", ");
|
||||
}
|
||||
|
||||
get channelColorStyle() {
|
||||
return htmlSafe(`color: #${this.args.channel.chatable.color}`);
|
||||
}
|
||||
|
||||
get showUserStatus() {
|
||||
return !!(this.users.length === 1 && this.users[0].status);
|
||||
}
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
|
||||
export default class OldChatChannelTitle extends Component {
|
||||
<template>
|
||||
{{#if @channel.isDirectMessageChannel}}
|
||||
<div class="chat-channel-title is-dm">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__users-count">
|
||||
{{@channel.membershipsCount}}
|
||||
</span>
|
||||
{{else}}
|
||||
<div class="chat-channel-title__avatar">
|
||||
<ChatUserAvatar @user={{this.firstUser}} @interactive={{false}} />
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="chat-channel-title__user-info">
|
||||
<div class="chat-channel-title__usernames">
|
||||
{{#if this.groupDirectMessage}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.groupsDirectMessageTitle}}
|
||||
</span>
|
||||
{{else}}
|
||||
<span class="chat-channel-title__name">
|
||||
{{this.firstUser.username}}
|
||||
</span>
|
||||
{{#if this.showUserStatus}}
|
||||
<UserStatusMessage
|
||||
@class="chat-channel-title__user-status-message"
|
||||
@status={{get this.users "0.status"}}
|
||||
@showDescription={{if this.site.mobileView "true"}}
|
||||
/>
|
||||
{{/if}}
|
||||
<PluginOutlet
|
||||
@name="after-chat-channel-username"
|
||||
@outletArgs={{hash user=@user}}
|
||||
@tagName=""
|
||||
@connectorTagName=""
|
||||
/>
|
||||
{{/if}}
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{else if @channel.isCategoryChannel}}
|
||||
<div class="chat-channel-title is-category">
|
||||
<span
|
||||
class="chat-channel-title__category-badge"
|
||||
style={{this.channelColorStyle}}
|
||||
>
|
||||
{{icon "d-chat"}}
|
||||
{{#if @channel.chatable.read_restricted}}
|
||||
{{icon "lock" class="chat-channel-title__restricted-category-icon"}}
|
||||
{{/if}}
|
||||
</span>
|
||||
<span class="chat-channel-title__name">
|
||||
{{replaceEmoji @channel.title}}
|
||||
</span>
|
||||
|
||||
{{#if (has-block)}}
|
||||
{{yield}}
|
||||
{{/if}}
|
||||
</div>
|
||||
{{/if}}
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
</template>
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import ChatDrawerHeaderRightActions from "discourse/plugins/chat/discourse/compo
|
|||
import ChatDrawerHeaderTitle from "discourse/plugins/chat/discourse/components/chat-drawer/header/title";
|
||||
import ChatThreadList from "discourse/plugins/chat/discourse/components/chat-thread-list";
|
||||
|
||||
export default class ChatDrawerThreads extends Component {
|
||||
export default class ChatDrawerChannelThreads extends Component {
|
||||
@service appEvents;
|
||||
@service chat;
|
||||
@service chatStateManager;
|
|
@ -2,7 +2,7 @@ import Component from "@glimmer/component";
|
|||
import { on } from "@ember/modifier";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
import ChatChannelTitle from "../../chat-channel-title";
|
||||
import ChannelTitle from "../../channel-title";
|
||||
|
||||
export default class ChatDrawerChannelHeaderTitle extends Component {
|
||||
@service chatStateManager;
|
||||
|
@ -20,7 +20,7 @@ export default class ChatDrawerChannelHeaderTitle extends Component {
|
|||
class="chat-drawer-header__title"
|
||||
>
|
||||
<div class="chat-drawer-header__top-line">
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
</div>
|
||||
</LinkTo>
|
||||
{{else}}
|
||||
|
@ -30,13 +30,13 @@ export default class ChatDrawerChannelHeaderTitle extends Component {
|
|||
class="chat-drawer-header__title"
|
||||
>
|
||||
<div class="chat-drawer-header__top-line">
|
||||
<ChatChannelTitle @channel={{@channel}}>
|
||||
<ChannelTitle @channel={{@channel}}>
|
||||
{{#if @channel.tracking.unreadCount}}
|
||||
<span class="chat-unread-count">
|
||||
{{@channel.tracking.unreadCount}}
|
||||
</span>
|
||||
{{/if}}
|
||||
</ChatChannelTitle>
|
||||
</ChannelTitle>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
|
|
@ -27,6 +27,10 @@ export default class ChatDrawerThread extends Component {
|
|||
if (this.chatHistory.previousRoute?.name === "chat.channel.threads") {
|
||||
link.title = I18n.t("chat.return_to_threads_list");
|
||||
link.route = "chat.channel.threads";
|
||||
} else if (this.chatHistory.previousRoute?.name === "chat.threads") {
|
||||
link.title = I18n.t("chat.my_threads.title");
|
||||
link.route = "chat.threads";
|
||||
link.models = [];
|
||||
} else {
|
||||
link.title = I18n.t("chat.return_to_channel");
|
||||
link.route = "chat.channel";
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { inject as service } from "@ember/service";
|
||||
import I18n from "discourse-i18n";
|
||||
import ChatDrawerHeader from "discourse/plugins/chat/discourse/components/chat-drawer/header";
|
||||
import ChatDrawerHeaderBackLink from "discourse/plugins/chat/discourse/components/chat-drawer/header/back-link";
|
||||
import ChatDrawerHeaderRightActions from "discourse/plugins/chat/discourse/components/chat-drawer/header/right-actions";
|
||||
import ChatDrawerHeaderTitle from "discourse/plugins/chat/discourse/components/chat-drawer/header/title";
|
||||
import UserThreads from "discourse/plugins/chat/discourse/components/user-threads";
|
||||
|
||||
export default class ChatDrawerThreads extends Component {
|
||||
@service appEvents;
|
||||
@service chat;
|
||||
@service chatStateManager;
|
||||
@service chatChannelsManager;
|
||||
|
||||
backLinkTitle = I18n.t("chat.return_to_list");
|
||||
|
||||
<template>
|
||||
<ChatDrawerHeader @toggleExpand={{@drawerActions.toggleExpand}}>
|
||||
|
||||
{{#if this.chatStateManager.isDrawerExpanded}}
|
||||
<div class="chat-drawer-header__left-actions">
|
||||
<div class="chat-drawer-header__top-line">
|
||||
<ChatDrawerHeaderBackLink
|
||||
@route="chat"
|
||||
@title={{this.backLink.title}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<ChatDrawerHeaderTitle
|
||||
@title="chat.threads.list"
|
||||
@icon="discourse-threads"
|
||||
@channelName={{this.chat.activeChannel.title}}
|
||||
/>
|
||||
|
||||
<ChatDrawerHeaderRightActions @drawerActions={{@drawerActions}} />
|
||||
</ChatDrawerHeader>
|
||||
|
||||
{{#if this.chatStateManager.isDrawerExpanded}}
|
||||
<div class="chat-drawer-content">
|
||||
<UserThreads />
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { on } from "@ember/modifier";
|
||||
import { action } from "@ember/object";
|
||||
import { LinkTo } from "@ember/routing";
|
||||
import { inject as service } from "@ember/service";
|
||||
|
@ -7,10 +8,10 @@ import concatClass from "discourse/helpers/concat-class";
|
|||
import icon from "discourse-common/helpers/d-icon";
|
||||
import and from "truth-helpers/helpers/and";
|
||||
import or from "truth-helpers/helpers/or";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
|
||||
import ThreadsListButton from "discourse/plugins/chat/discourse/components/chat/thread/threads-list-button";
|
||||
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
|
||||
export default class ChatFullPageHeader extends Component {
|
||||
@service chatStateManager;
|
||||
|
@ -38,13 +39,20 @@ export default class ChatFullPageHeader extends Component {
|
|||
});
|
||||
}
|
||||
|
||||
@action
|
||||
trapMouse(event) {
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
<template>
|
||||
{{! template-lint-disable no-invalid-interactive }}
|
||||
{{#if (and this.chatStateManager.isFullPageActive this.displayed)}}
|
||||
<div
|
||||
class={{concatClass
|
||||
"chat-full-page-header"
|
||||
(unless @channel.isFollowing "-not-following")
|
||||
}}
|
||||
{{on "mousemove" this.trapMouse}}
|
||||
>
|
||||
<div class="chat-channel-header-details">
|
||||
{{#if this.site.mobileView}}
|
||||
|
@ -63,7 +71,7 @@ export default class ChatFullPageHeader extends Component {
|
|||
@models={{@channel.routeModels}}
|
||||
class="chat-channel-title-wrapper"
|
||||
>
|
||||
<ChatChannelTitle @channel={{@channel}} />
|
||||
<ChannelTitle @channel={{@channel}} />
|
||||
</LinkTo>
|
||||
|
||||
{{#if (or @channel.threadingEnabled this.site.desktopView)}}
|
||||
|
|
|
@ -9,6 +9,7 @@ import formatDate from "discourse/helpers/format-date";
|
|||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import htmlSafe from "discourse-common/helpers/html-safe";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import getURL from "discourse-common/lib/get-url";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import ChatThreadParticipants from "./chat-thread-participants";
|
||||
import ChatUserAvatar from "./chat-user-avatar";
|
||||
|
@ -22,6 +23,10 @@ export default class ChatMessageThreadIndicator extends Component {
|
|||
|
||||
@tracked isActive = false;
|
||||
|
||||
get interactiveUser() {
|
||||
return this.args.interactiveUser ?? true;
|
||||
}
|
||||
|
||||
@action
|
||||
setup(element) {
|
||||
this.element = element;
|
||||
|
@ -37,7 +42,11 @@ export default class ChatMessageThreadIndicator extends Component {
|
|||
this.element.addEventListener("touchCancel", this.cancelTouch);
|
||||
}
|
||||
|
||||
this.element.addEventListener("click", this.openThread, {
|
||||
this.element.addEventListener("mousedown", this.openThread, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.element.addEventListener("keydown", this.openThread, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
@ -55,7 +64,11 @@ export default class ChatMessageThreadIndicator extends Component {
|
|||
this.element.removeEventListener("touchCancel", this.cancelTouch);
|
||||
}
|
||||
|
||||
this.element.removeEventListener("click", this.openThread, {
|
||||
this.element.removeEventListener("mousedown", this.openThread, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
this.element.removeEventListener("keydown", this.openThread, {
|
||||
passive: true,
|
||||
});
|
||||
}
|
||||
|
@ -84,7 +97,25 @@ export default class ChatMessageThreadIndicator extends Component {
|
|||
}
|
||||
|
||||
@bind
|
||||
openThread() {
|
||||
openThread(event) {
|
||||
if (event.type === "keydown" && event.key !== "Enter") {
|
||||
return;
|
||||
}
|
||||
|
||||
// handle middle mouse
|
||||
if (event.type === "mousedown" && (event.which === 2 || event.shiftKey)) {
|
||||
window.open(
|
||||
getURL(
|
||||
this.router.urlFor(
|
||||
"chat.channel.thread",
|
||||
...this.args.message.thread.routeModels
|
||||
)
|
||||
),
|
||||
"_blank"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.chat.activeMessage = null;
|
||||
|
||||
this.router.transitionTo(
|
||||
|
@ -103,12 +134,14 @@ export default class ChatMessageThreadIndicator extends Component {
|
|||
{{willDestroy this.teardown}}
|
||||
role="button"
|
||||
title={{i18n "chat.threads.open"}}
|
||||
tabindex="0"
|
||||
>
|
||||
|
||||
<div class="chat-message-thread-indicator__last-reply-avatar">
|
||||
<ChatUserAvatar
|
||||
@user={{@message.thread.preview.lastReplyUser}}
|
||||
@avatarSize="small"
|
||||
@interactive={{this.interactiveUser}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -76,12 +76,10 @@ export default class ChatThreadList extends Component {
|
|||
// NOTE: This replicates sort logic from the server. We need this because
|
||||
// the thread unread count + last reply date + time update when new messages
|
||||
// are sent to the thread, and we want the list to react in realtime to this.
|
||||
@cached
|
||||
get sortedThreads() {
|
||||
return this.threadsManager.threads
|
||||
.filter(
|
||||
(thread) =>
|
||||
thread.currentUserMembership && !thread.originalMessage.deletedAt
|
||||
)
|
||||
.filter((thread) => !thread.originalMessage.deletedAt)
|
||||
.sort((threadA, threadB) => {
|
||||
// If both are unread we just want to sort by last reply date + time descending.
|
||||
if (threadA.tracking.unreadCount && threadB.tracking.unreadCount) {
|
||||
|
@ -186,6 +184,7 @@ export default class ChatThreadList extends Component {
|
|||
{{/if}}
|
||||
|
||||
<div class="chat-thread-list__items" {{this.fill}}>
|
||||
|
||||
{{#each this.sortedThreads key="id" as |thread|}}
|
||||
<ChatThreadListItem
|
||||
@thread={{thread}}
|
||||
|
|
|
@ -4,6 +4,10 @@ import ChatUserAvatar from "discourse/plugins/chat/discourse/components/chat-use
|
|||
|
||||
export default class ChatThreadParticipants extends Component {
|
||||
get showParticipants() {
|
||||
if (!this.args.thread) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.includeOriginalMessageUser) {
|
||||
return this.participantsUsers.length > 1;
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@ import Component from "@glimmer/component";
|
|||
import { inject as service } from "@ember/service";
|
||||
import concatClass from "discourse/helpers/concat-class";
|
||||
import gt from "truth-helpers/helpers/gt";
|
||||
import ChatChannelTitle from "discourse/plugins/chat/discourse/components/chat-channel-title";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
|
||||
export default class Channel extends Component {
|
||||
@service currentUser;
|
||||
|
@ -17,7 +17,7 @@ export default class Channel extends Component {
|
|||
|
||||
<template>
|
||||
<div class="chat-message-creator__chatable -category-channel">
|
||||
<ChatChannelTitle @channel={{@item.model}} />
|
||||
<ChannelTitle @channel={{@item.model}} />
|
||||
|
||||
{{#if (gt @item.tracking.unreadCount 0)}}
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ import formatDate from "discourse/helpers/format-date";
|
|||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import gt from "truth-helpers/helpers/gt";
|
||||
import ThreadUnreadIndicator from "discourse/plugins/chat/discourse/components/thread-unread-indicator";
|
||||
import ChatThreadParticipants from "../../chat-thread-participants";
|
||||
import ChatUserAvatar from "../../chat-user-avatar";
|
||||
import UnreadIndicator from "./item/unread-indicator";
|
||||
|
||||
export default class ChatThreadListItem extends Component {
|
||||
@service router;
|
||||
|
@ -45,7 +45,7 @@ export default class ChatThreadListItem extends Component {
|
|||
{{/if}}
|
||||
</div>
|
||||
<div class="chat-thread-list-item__unread-indicator">
|
||||
<UnreadIndicator @thread={{@thread}} />
|
||||
<ThreadUnreadIndicator @thread={{@thread}} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -30,24 +30,27 @@ export default class ChatThreadHeader extends Component {
|
|||
|
||||
get backLink() {
|
||||
const prevPage = this.chatHistory.previousRoute?.name;
|
||||
let route, title;
|
||||
let route, title, models;
|
||||
|
||||
if (prevPage === "chat.channel.threads") {
|
||||
route = "chat.channel.threads";
|
||||
title = I18n.t("chat.return_to_threads_list");
|
||||
models = this.args.channel.routeModels;
|
||||
} else if (prevPage === "chat.channel.index" && !this.site.mobileView) {
|
||||
route = "chat.channel.threads";
|
||||
title = I18n.t("chat.return_to_threads_list");
|
||||
models = this.args.channel.routeModels;
|
||||
} else if (prevPage === "chat.threads") {
|
||||
route = "chat.threads";
|
||||
title = I18n.t("chat.my_threads.title");
|
||||
models = [];
|
||||
} else {
|
||||
route = "chat.channel.index";
|
||||
title = I18n.t("chat.return_to_channel");
|
||||
models = this.args.channel.routeModels;
|
||||
}
|
||||
|
||||
return {
|
||||
route,
|
||||
models: this.args.channel.routeModels,
|
||||
title,
|
||||
};
|
||||
return { route, models, title };
|
||||
}
|
||||
|
||||
get canChangeThreadSettings() {
|
||||
|
|
|
@ -0,0 +1,20 @@
|
|||
import Component from "@glimmer/component";
|
||||
import icon from "discourse-common/helpers/d-icon";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import Navbar from "discourse/plugins/chat/discourse/components/navbar";
|
||||
import UserThreads from "discourse/plugins/chat/discourse/components/user-threads";
|
||||
|
||||
export default class ChatThreads extends Component {
|
||||
<template>
|
||||
<div class="chat-threads">
|
||||
<Navbar>
|
||||
<:current>
|
||||
{{icon "discourse-threads"}}
|
||||
{{i18n "chat.my_threads.title"}}
|
||||
</:current>
|
||||
</Navbar>
|
||||
|
||||
<UserThreads />
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import DButton from "discourse/components/d-button";
|
||||
import DiscourseURL from "discourse/lib/url";
|
||||
|
||||
export default class ChatNavbar extends Component {
|
||||
@service chatStateManager;
|
||||
|
||||
@action
|
||||
async closeFullScreen() {
|
||||
this.chatStateManager.prefersDrawer();
|
||||
|
||||
try {
|
||||
await DiscourseURL.routeTo(this.chatStateManager.lastKnownAppURL);
|
||||
await DiscourseURL.routeTo(this.chatStateManager.lastKnownChatURL);
|
||||
} catch (error) {
|
||||
await DiscourseURL.routeTo("/");
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat-navbar-container">
|
||||
<nav class="chat-navbar">
|
||||
{{#if (has-block "current")}}
|
||||
<span class="chat-navbar__current">
|
||||
{{yield to="current"}}
|
||||
</span>
|
||||
{{/if}}
|
||||
|
||||
<ul class="chat-navbar__right-actions">
|
||||
<li class="chat-navbar__right-action">
|
||||
<DButton
|
||||
@icon="discourse-compress"
|
||||
@title="chat.close_full_page"
|
||||
class="open-drawer-btn btn-flat"
|
||||
@action={{this.closeFullScreen}}
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -8,15 +8,15 @@ import ReviewablePostHeader from "discourse/components/reviewable-post-header";
|
|||
import htmlSafe from "discourse-common/helpers/html-safe";
|
||||
import i18n from "discourse-common/helpers/i18n";
|
||||
import or from "truth-helpers/helpers/or";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import ChatChannelTitle from "./chat-channel-title";
|
||||
|
||||
export default class ReviewableChatMessage extends Component {
|
||||
@service store;
|
||||
@service chatChannelsManager;
|
||||
|
||||
@cached
|
||||
get chatChannel() {
|
||||
get channel() {
|
||||
return ChatChannel.create(this.args.reviewable.chat_channel);
|
||||
}
|
||||
|
||||
|
@ -25,12 +25,12 @@ export default class ReviewableChatMessage extends Component {
|
|||
<LinkTo
|
||||
@route="chat.channel.near-message"
|
||||
@models={{array
|
||||
this.chatChannel.slugifiedTitle
|
||||
this.chatChannel.id
|
||||
this.channel.slugifiedTitle
|
||||
this.channel.id
|
||||
@reviewable.target_id
|
||||
}}
|
||||
>
|
||||
<ChatChannelTitle @channel={{this.chatChannel}} />
|
||||
<ChannelTitle @channel={{this.channel}} />
|
||||
</LinkTo>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { htmlSafe } from "@ember/template";
|
||||
import replaceEmoji from "discourse/helpers/replace-emoji";
|
||||
import { escapeExpression } from "discourse/lib/utilities";
|
||||
import ThreadUnreadIndicator from "discourse/plugins/chat/discourse/components/thread-unread-indicator";
|
||||
|
||||
export default class ChatThreadTitle extends Component {
|
||||
get title() {
|
||||
if (this.args.thread.title) {
|
||||
return replaceEmoji(htmlSafe(escapeExpression(this.args.thread.title)));
|
||||
} else {
|
||||
return replaceEmoji(htmlSafe(this.args.thread.originalMessage.excerpt));
|
||||
}
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat__thread-title-container">
|
||||
<div class="chat__thread-title">
|
||||
<span class="chat__thread-title__name">
|
||||
{{this.title}}
|
||||
</span>
|
||||
|
||||
<ThreadUnreadIndicator @thread={{@thread}} />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
import Component from "@glimmer/component";
|
||||
|
||||
export default class ChatThreadListItemUnreadIndicator extends Component {
|
||||
export default class ChatThreadUnreadIndicator extends Component {
|
||||
get unreadCount() {
|
||||
return this.args.thread.tracking.unreadCount;
|
||||
}
|
|
@ -0,0 +1,107 @@
|
|||
import Component from "@glimmer/component";
|
||||
import { cached } from "@glimmer/tracking";
|
||||
import { action } from "@ember/object";
|
||||
import { inject as service } from "@ember/service";
|
||||
import { modifier } from "ember-modifier";
|
||||
import ConditionalLoadingSpinner from "discourse/components/conditional-loading-spinner";
|
||||
import isElementInViewport from "discourse/lib/is-element-in-viewport";
|
||||
import { INPUT_DELAY } from "discourse-common/config/environment";
|
||||
import discourseDebounce from "discourse-common/lib/debounce";
|
||||
import { bind } from "discourse-common/utils/decorators";
|
||||
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
|
||||
import ThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
|
||||
import ThreadTitle from "discourse/plugins/chat/discourse/components/thread-title";
|
||||
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
|
||||
|
||||
export default class UserThreads extends Component {
|
||||
@service chat;
|
||||
@service chatApi;
|
||||
@service router;
|
||||
|
||||
loadMore = modifier((element) => {
|
||||
this.intersectionObserver = new IntersectionObserver(this.loadThreads);
|
||||
this.intersectionObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
this.intersectionObserver.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
fill = modifier((element) => {
|
||||
this.resizeObserver = new ResizeObserver(() => {
|
||||
if (isElementInViewport(element)) {
|
||||
this.loadThreads();
|
||||
}
|
||||
});
|
||||
|
||||
this.resizeObserver.observe(element);
|
||||
|
||||
return () => {
|
||||
this.resizeObserver.disconnect();
|
||||
};
|
||||
});
|
||||
|
||||
@cached
|
||||
get threadsCollection() {
|
||||
return this.chatApi.userThreads(this.handleLoadedThreads);
|
||||
}
|
||||
|
||||
@action
|
||||
loadThreads() {
|
||||
discourseDebounce(this, this.debouncedLoadThreads, INPUT_DELAY);
|
||||
}
|
||||
|
||||
async debouncedLoadThreads() {
|
||||
await this.threadsCollection.load({ limit: 10 });
|
||||
}
|
||||
|
||||
@bind
|
||||
handleLoadedThreads(result) {
|
||||
return result.threads.map((threadObject) => {
|
||||
const channel = ChatChannel.create(threadObject.channel);
|
||||
const thread = ChatThread.create(channel, threadObject);
|
||||
const tracking = result.tracking[thread.id];
|
||||
if (tracking) {
|
||||
thread.tracking.mentionCount = tracking.mention_count;
|
||||
thread.tracking.unreadCount = tracking.unread_count;
|
||||
}
|
||||
return thread;
|
||||
});
|
||||
}
|
||||
|
||||
<template>
|
||||
<div class="chat__user-threads-container">
|
||||
<div class="chat__user-threads" {{this.fill}}>
|
||||
{{#each this.threadsCollection.items as |thread|}}
|
||||
<div
|
||||
class="chat__user-threads__thread-container"
|
||||
data-id={{thread.id}}
|
||||
>
|
||||
<div class="chat__user-threads__thread">
|
||||
<div class="chat__user-threads__title">
|
||||
<ThreadTitle @thread={{thread}} />
|
||||
<ChannelTitle @channel={{thread.channel}} />
|
||||
</div>
|
||||
|
||||
<div class="chat__user-threads__thread-indicator">
|
||||
<ThreadIndicator
|
||||
@message={{thread.originalMessage}}
|
||||
@interactiveUser={{false}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{/each}}
|
||||
|
||||
<div {{this.loadMore}}>
|
||||
<br />
|
||||
</div>
|
||||
|
||||
<ConditionalLoadingSpinner
|
||||
@condition={{this.threadsCollection.loading}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
}
|
|
@ -42,6 +42,59 @@ export default {
|
|||
});
|
||||
|
||||
withPluginApi("1.3.0", (api) => {
|
||||
api.addSidebarSection(
|
||||
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||
const SidebarChatMyThreadsSectionLink = class extends BaseCustomSidebarSectionLink {
|
||||
route = "chat.threads";
|
||||
text = I18n.t("chat.my_threads.title");
|
||||
title = I18n.t("chat.my_threads.title");
|
||||
name = "user-threads";
|
||||
prefixType = "icon";
|
||||
prefixValue = "discourse-threads";
|
||||
suffixType = "icon";
|
||||
suffixCSSClass = "unread";
|
||||
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
|
||||
if (container.isDestroyed) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.chatChannelsManager = container.lookup(
|
||||
"service:chat-channels-manager"
|
||||
);
|
||||
}
|
||||
|
||||
get suffixValue() {
|
||||
return this.chatChannelsManager.publicMessageChannels.some(
|
||||
(channel) => channel.unreadThreadsCount > 0
|
||||
)
|
||||
? "circle"
|
||||
: "";
|
||||
}
|
||||
};
|
||||
|
||||
const SidebarChatMyThreadsSection = class extends BaseCustomSidebarSection {
|
||||
// we only show `My Threads` link
|
||||
hideSectionHeader = true;
|
||||
|
||||
name = "user-threads";
|
||||
|
||||
// sidebar API doesn’t let you have undefined values
|
||||
// even if you don't show the section’s header
|
||||
title = "";
|
||||
|
||||
get links() {
|
||||
return [new SidebarChatMyThreadsSectionLink()];
|
||||
}
|
||||
};
|
||||
|
||||
return SidebarChatMyThreadsSection;
|
||||
},
|
||||
CHAT_PANEL
|
||||
);
|
||||
|
||||
if (this.siteSettings.enable_public_channels) {
|
||||
api.addSidebarSection(
|
||||
(BaseCustomSidebarSection, BaseCustomSidebarSectionLink) => {
|
||||
|
|
|
@ -110,6 +110,10 @@ export default class ChatChannel {
|
|||
).length;
|
||||
}
|
||||
|
||||
get unreadThreadsCount() {
|
||||
return Array.from(this.threadsManager.unreadThreadOverview.values()).length;
|
||||
}
|
||||
|
||||
updateLastViewedAt() {
|
||||
this.currentUserMembership.lastViewedAt = new Date();
|
||||
}
|
||||
|
|
|
@ -45,6 +45,10 @@ export default class ChatThread {
|
|||
? ChatMessage.create(channel, args.original_message)
|
||||
: null;
|
||||
|
||||
if (this.originalMessage) {
|
||||
this.originalMessage.thread = this;
|
||||
}
|
||||
|
||||
this.title = args.title;
|
||||
|
||||
if (args.current_user_membership) {
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import { inject as service } from "@ember/service";
|
||||
import DiscourseRoute from "discourse/routes/discourse";
|
||||
|
||||
export default class ChatChannelThreads extends DiscourseRoute {
|
||||
@service chat;
|
||||
|
||||
activate() {
|
||||
this.chat.activeChannel = null;
|
||||
}
|
||||
}
|
|
@ -28,6 +28,7 @@ export default class ChatRoute extends DiscourseRoute {
|
|||
|
||||
const INTERCEPTABLE_ROUTES = [
|
||||
"chat.channel",
|
||||
"chat.threads",
|
||||
"chat.channel.thread",
|
||||
"chat.channel.thread.index",
|
||||
"chat.channel.thread.near-message",
|
||||
|
|
|
@ -273,7 +273,7 @@ export default class ChatApi extends Service {
|
|||
* @returns {Promise}
|
||||
*/
|
||||
listCurrentUserChannels() {
|
||||
return this.#getRequest("/channels/me");
|
||||
return this.#getRequest("/me/channels");
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -308,6 +308,15 @@ export default class ChatApi extends Service {
|
|||
return this.#deleteRequest(`/channels/${channelId}/memberships/me`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the list of tracked threads for the current user.
|
||||
*
|
||||
* @returns {Promise}
|
||||
*/
|
||||
userThreads(handler) {
|
||||
return new Collection(`${this.#basePath}/me/threads`, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notifications settings of current user for a channel.
|
||||
* @param {number} channelId - The ID of the channel.
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import { tracked } from "@glimmer/tracking";
|
||||
import Service, { inject as service } from "@ember/service";
|
||||
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel";
|
||||
import ChatDrawerChannelThreads from "discourse/plugins/chat/discourse/components/chat-drawer/channel-threads";
|
||||
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index";
|
||||
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread";
|
||||
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads";
|
||||
|
@ -36,13 +37,16 @@ const ROUTES = {
|
|||
},
|
||||
},
|
||||
"chat.channel.threads": {
|
||||
name: ChatDrawerThreads,
|
||||
name: ChatDrawerChannelThreads,
|
||||
extractParams: (route) => {
|
||||
return {
|
||||
channelId: route.parent.params.channelId,
|
||||
};
|
||||
},
|
||||
},
|
||||
"chat.threads": {
|
||||
name: ChatDrawerThreads,
|
||||
},
|
||||
chat: { name: ChatDrawerIndex },
|
||||
"chat.channel.near-message": {
|
||||
name: ChatDrawerChannel,
|
||||
|
|
|
@ -168,7 +168,7 @@
|
|||
{{/if}}
|
||||
</div>
|
||||
|
||||
<div><ChatChannelTitle @channel={{webhook.chat_channel}} /></div>
|
||||
<div><ChannelTitle @channel={{webhook.chat_channel}} /></div>
|
||||
<div>{{webhook.description}}</div>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<Chat::Threads />
|
|
@ -198,7 +198,6 @@ body.has-full-page-chat {
|
|||
background: var(--d-content-background);
|
||||
|
||||
.chat-full-page-header {
|
||||
border-top: 1px solid var(--primary-low);
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
background: var(--secondary);
|
||||
z-index: 3;
|
||||
|
@ -363,7 +362,6 @@ html.has-full-page-chat {
|
|||
|
||||
.main-chat-outlet {
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -42,6 +42,10 @@
|
|||
}
|
||||
}
|
||||
|
||||
.d-icon {
|
||||
position: unset;
|
||||
}
|
||||
|
||||
.d-icon-lock {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
|
|
@ -260,9 +260,12 @@ a.chat-drawer-header__title {
|
|||
}
|
||||
|
||||
.chat-drawer-content {
|
||||
@include chat-scrollbar();
|
||||
box-sizing: border-box;
|
||||
height: 100%;
|
||||
min-height: 1px;
|
||||
padding-bottom: 0.25em;
|
||||
position: relative;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
|
23
plugins/chat/assets/stylesheets/common/chat-navbar.scss
Normal file
23
plugins/chat/assets/stylesheets/common/chat-navbar.scss
Normal file
|
@ -0,0 +1,23 @@
|
|||
.chat-navbar {
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
|
||||
&-container {
|
||||
padding-inline: 1rem;
|
||||
position: sticky;
|
||||
top: var(--header-offset);
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
background: var(--secondary);
|
||||
height: 50px;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
z-index: z("header") - 1;
|
||||
}
|
||||
}
|
||||
|
||||
.chat-navbar__right-actions {
|
||||
list-style: none;
|
||||
margin-left: auto;
|
||||
}
|
|
@ -120,11 +120,6 @@
|
|||
|
||||
&__unread-indicator {
|
||||
flex: 0 0 auto;
|
||||
|
||||
.chat-thread-list-item-unread-indicator__number {
|
||||
color: var(--primary);
|
||||
font-size: var(--font-up-1);
|
||||
}
|
||||
}
|
||||
|
||||
&__open-button {
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
.chat__thread-title {
|
||||
display: flex;
|
||||
|
||||
.chat-thread-list-item-unread-indicator {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
}
|
|
@ -9,4 +9,5 @@
|
|||
padding: 0.21em 0.42em;
|
||||
border-radius: 1em;
|
||||
min-width: 0.6em;
|
||||
align-self: center;
|
||||
}
|
||||
|
|
|
@ -21,3 +21,7 @@
|
|||
line-height: var(--line-height-small);
|
||||
}
|
||||
}
|
||||
|
||||
.chat__unread-indicator {
|
||||
@include chat-unread-indicator;
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
}
|
||||
|
||||
&__container {
|
||||
position: relative;
|
||||
padding: 1px; // for is-online box-shadow effect, preventing cutoff
|
||||
|
||||
.avatar {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
.chat__user-threads-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
|
||||
.chat__unread-indicator {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
&-container {
|
||||
height: 3.6em;
|
||||
padding: 0 0.5rem;
|
||||
margin: 0.5rem 0rem 0 0.5rem;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-low);
|
||||
}
|
||||
|
||||
.chat__user-threads-row__icon {
|
||||
color: var(--primary);
|
||||
}
|
||||
.chat__user-threads-row__title {
|
||||
color: var(--primary);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
.chat__user-threads__thread-indicator {
|
||||
padding-top: 1rem;
|
||||
|
||||
.chat-message-thread-indicator {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.chat__user-threads__title {
|
||||
.chat-channel-title__name {
|
||||
font-size: var(--font-down-1);
|
||||
color: var(--primary-medium);
|
||||
}
|
||||
}
|
||||
|
||||
.chat__user-threads__thread {
|
||||
padding-bottom: 1rem;
|
||||
border-bottom: 1px solid var(--primary-low);
|
||||
|
||||
&-container {
|
||||
padding: 1rem 1rem 0 1rem;
|
||||
}
|
||||
}
|
|
@ -65,3 +65,7 @@
|
|||
@import "chat-channel-row";
|
||||
@import "chat-channel-members";
|
||||
@import "chat-channel-settings";
|
||||
@import "chat-user-threads";
|
||||
@import "chat-navbar";
|
||||
@import "chat-user-threads-row";
|
||||
@import "chat-thread-title";
|
||||
|
|
|
@ -35,6 +35,12 @@
|
|||
}
|
||||
}
|
||||
|
||||
.sidebar-section-wrapper.sidebar-section[data-section-name="my-threads"] {
|
||||
.sidebar-section-content {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.sidebar-container {
|
||||
.channels-list {
|
||||
color: var(--primary);
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
"avatar info info participants"
|
||||
"excerpt excerpt excerpt replies";
|
||||
&__replies-count {
|
||||
align-self: flex-start;
|
||||
align-self: center;
|
||||
grid-area: replies;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
|
|
@ -491,6 +491,9 @@ en:
|
|||
create_export: "Create export"
|
||||
export_has_started: "The export has started. You'll receive a PM when it's ready."
|
||||
|
||||
my_threads:
|
||||
title: My threads
|
||||
|
||||
direct_messages:
|
||||
title: "Personal chat"
|
||||
new: "Create a personal chat"
|
||||
|
|
|
@ -4,7 +4,8 @@ Chat::Engine.routes.draw do
|
|||
namespace :api, defaults: { format: :json } do
|
||||
get "/chatables" => "chatables#index"
|
||||
get "/channels" => "channels#index"
|
||||
get "/channels/me" => "current_user_channels#index"
|
||||
get "/me/channels" => "current_user_channels#index"
|
||||
get "/me/threads" => "current_user_threads#index"
|
||||
post "/channels" => "channels#create"
|
||||
put "/channels/read/" => "reads#update_all"
|
||||
put "/channels/:channel_id/read/:message_id" => "reads#update"
|
||||
|
@ -72,6 +73,7 @@ Chat::Engine.routes.draw do
|
|||
|
||||
# chat_controller routes
|
||||
get "/" => "chat#respond"
|
||||
get "/threads" => "chat#respond"
|
||||
get "/browse" => "chat#respond"
|
||||
get "/browse/all" => "chat#respond"
|
||||
get "/browse/closed" => "chat#respond"
|
||||
|
|
|
@ -0,0 +1,22 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
class AddUserChatThreadMembershipsOnThreadIdUserIdIndex < ActiveRecord::Migration[7.0]
|
||||
disable_ddl_transaction!
|
||||
|
||||
def up
|
||||
execute <<~SQL
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_user_chat_thread_memberships_on_thread_id_user_id
|
||||
SQL
|
||||
|
||||
execute <<~SQL
|
||||
CREATE INDEX CONCURRENTLY idx_user_chat_thread_memberships_on_thread_id_user_id
|
||||
ON user_chat_thread_memberships (thread_id, user_id);
|
||||
SQL
|
||||
end
|
||||
|
||||
def down
|
||||
execute <<~SQL
|
||||
DROP INDEX CONCURRENTLY IF EXISTS idx_user_chat_thread_memberships_on_thread_id_user_id
|
||||
SQL
|
||||
end
|
||||
end
|
|
@ -220,12 +220,16 @@ Fabricator(:chat_thread, class_name: "Chat::Thread") do
|
|||
thread.add(thread.original_message_user)
|
||||
|
||||
if transients[:with_replies]
|
||||
Fabricate.times(
|
||||
transients[:with_replies],
|
||||
:chat_message,
|
||||
thread: thread,
|
||||
use_service: transients[:use_service],
|
||||
)
|
||||
Fabricate
|
||||
.times(
|
||||
transients[:with_replies],
|
||||
:chat_message,
|
||||
thread: thread,
|
||||
use_service: transients[:use_service],
|
||||
)
|
||||
.each { |message| thread.add(message.user) }
|
||||
|
||||
thread.update!(replies_count: transients[:with_replies])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -84,9 +84,9 @@ RSpec.describe Chat::Api::ChannelThreadsController do
|
|||
end
|
||||
|
||||
describe "index" do
|
||||
fab!(:thread_1) { Fabricate(:chat_thread, channel: public_channel) }
|
||||
fab!(:thread_2) { Fabricate(:chat_thread, channel: public_channel) }
|
||||
fab!(:thread_3) { Fabricate(:chat_thread, channel: public_channel) }
|
||||
fab!(:thread_1) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
|
||||
fab!(:thread_2) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
|
||||
fab!(:thread_3) { Fabricate(:chat_thread, channel: public_channel, with_replies: 1) }
|
||||
fab!(:message_1) do
|
||||
Fabricate(
|
||||
:chat_message,
|
||||
|
@ -111,11 +111,11 @@ RSpec.describe Chat::Api::ChannelThreadsController do
|
|||
thread_3.add(current_user)
|
||||
end
|
||||
|
||||
it "returns the threads the user has sent messages in for the channel" do
|
||||
it "returns the threads of the channel" do
|
||||
get "/chat/api/channels/#{public_channel.id}/threads"
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["threads"].map { |thread| thread["id"] }).to eq(
|
||||
[thread_3.id, thread_1.id],
|
||||
[thread_3.id, thread_2.id, thread_1.id],
|
||||
)
|
||||
end
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
describe "#index" do
|
||||
context "as anonymous user" do
|
||||
it "returns an error" do
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
end
|
||||
|
@ -25,7 +25,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
end
|
||||
|
||||
it "returns an error" do
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.status).to eq(403)
|
||||
end
|
||||
|
@ -39,7 +39,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
it "returns public channels with memberships" do
|
||||
channel = Fabricate(:category_channel)
|
||||
channel.add(current_user)
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(channel.id)
|
||||
end
|
||||
|
@ -49,7 +49,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
channel = Fabricate(:private_category_channel, group: group)
|
||||
group.add(current_user)
|
||||
channel.add(current_user)
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.parsed_body["public_channels"][0]["id"]).to eq(channel.id)
|
||||
end
|
||||
|
@ -58,21 +58,21 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
group = Fabricate(:group)
|
||||
channel = Fabricate(:private_category_channel, group: group)
|
||||
channel.add(current_user) # TODO: we should error here
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.parsed_body["public_channels"]).to be_blank
|
||||
end
|
||||
|
||||
it "returns dm channels you are part of" do
|
||||
dm_channel = Fabricate(:direct_message_channel, users: [current_user])
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.parsed_body["direct_message_channels"][0]["id"]).to eq(dm_channel.id)
|
||||
end
|
||||
|
||||
it "doesn’t return dm channels from other users" do
|
||||
Fabricate(:direct_message_channel)
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.parsed_body["direct_message_channels"]).to be_blank
|
||||
end
|
||||
|
@ -81,7 +81,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
Fabricate(:direct_message_channel, users: [current_user])
|
||||
channel = Fabricate(:category_channel)
|
||||
channel.add(current_user)
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
|
||||
|
@ -112,7 +112,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
channel = Fabricate(:category_channel)
|
||||
channel.add(current_user)
|
||||
channel.chatable.destroy!
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["public_channels"]).to be_blank
|
||||
|
@ -123,7 +123,7 @@ describe Chat::Api::CurrentUserChannelsController do
|
|||
it "doesn’t return the channel" do
|
||||
channel = Fabricate(:direct_message_channel, users: [current_user])
|
||||
channel.chatable.destroy!
|
||||
get "/chat/api/channels/me"
|
||||
get "/chat/api/me/channels"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["direct_message_channels"]).to be_blank
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require "rails_helper"
|
||||
|
||||
describe Chat::Api::CurrentUserThreadsController do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
|
||||
before do
|
||||
SiteSetting.chat_enabled = true
|
||||
SiteSetting.chat_allowed_groups = Group::AUTO_GROUPS[:everyone]
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
describe "#index" do
|
||||
describe "success" do
|
||||
it "works" do
|
||||
get "/chat/api/me/threads"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
end
|
||||
end
|
||||
|
||||
context "when threads are not found" do
|
||||
it "returns a 200 with empty threads" do
|
||||
get "/chat/api/me/threads"
|
||||
|
||||
expect(response.status).to eq(200)
|
||||
expect(response.parsed_body["threads"]).to eq([])
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -113,11 +113,14 @@ RSpec.describe ::Chat::LookupChannelThreads do
|
|||
before do
|
||||
[thread_1, thread_2, thread_3].each.with_index do |t, index|
|
||||
t.original_message.update!(created_at: (index + 1).weeks.ago)
|
||||
t.update!(replies_count: 2)
|
||||
t.add(current_user)
|
||||
end
|
||||
end
|
||||
|
||||
describe "model - threads" do
|
||||
before { channel_1.add(current_user) }
|
||||
|
||||
it { is_expected.to be_a_success }
|
||||
|
||||
it "orders threads by the last reply created_at timestamp" do
|
||||
|
@ -153,9 +156,13 @@ RSpec.describe ::Chat::LookupChannelThreads do
|
|||
|
||||
it "sorts very old unreads to top over recency, and sorts both unreads and other threads by recency" do
|
||||
thread_4 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_4.update!(replies_count: 2)
|
||||
thread_5 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_5.update!(replies_count: 2)
|
||||
thread_6 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_6.update!(replies_count: 2)
|
||||
thread_7 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_7.update!(replies_count: 2)
|
||||
|
||||
[thread_4, thread_5, thread_6, thread_7].each do |t|
|
||||
t.add(current_user)
|
||||
|
@ -202,15 +209,23 @@ RSpec.describe ::Chat::LookupChannelThreads do
|
|||
expect(result.threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id])
|
||||
end
|
||||
|
||||
it "only returns threads where the user has their thread notification level as tracking or regular" do
|
||||
it "returns every threads of the channel, no matter the tracking notification level or membership" do
|
||||
thread_4 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_4.add(current_user)
|
||||
thread_4.membership_for(current_user).update!(
|
||||
thread_4.update!(replies_count: 2)
|
||||
|
||||
expect(result.threads.map(&:id)).to match_array(
|
||||
[thread_1.id, thread_2.id, thread_3.id, thread_4.id],
|
||||
)
|
||||
end
|
||||
|
||||
it "doesnt return muted threads" do
|
||||
thread = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread.add(current_user)
|
||||
thread.membership_for(current_user).update!(
|
||||
notification_level: ::Chat::UserChatThreadMembership.notification_levels[:muted],
|
||||
)
|
||||
Fabricate(:chat_thread, channel: channel_1)
|
||||
|
||||
expect(result.threads.map(&:id)).to eq([thread_1.id, thread_2.id, thread_3.id])
|
||||
expect(result.threads.map(&:id)).to_not include(thread.id)
|
||||
end
|
||||
|
||||
it "does not count deleted messages for sort order" do
|
||||
|
|
171
plugins/chat/spec/services/chat/lookup_user_threads_spec.rb
Normal file
171
plugins/chat/spec/services/chat/lookup_user_threads_spec.rb
Normal file
|
@ -0,0 +1,171 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe ::Chat::LookupUserThreads do
|
||||
subject(:result) { described_class.call(params) }
|
||||
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
|
||||
let(:guardian) { Guardian.new(current_user) }
|
||||
let(:channel_id) { channel_1.id }
|
||||
let(:limit) { 10 }
|
||||
let(:offset) { 0 }
|
||||
let(:params) { { guardian: guardian, limit: limit, offset: offset } }
|
||||
|
||||
before { channel_1.add(current_user) }
|
||||
|
||||
context "when all steps pass" do
|
||||
it "returns threads" do
|
||||
thread_1 =
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.threads).to eq([thread_1])
|
||||
end
|
||||
|
||||
it "limits results by default" do
|
||||
Fabricate
|
||||
.times(11, :chat_thread, channel: channel_1, with_replies: 1)
|
||||
.each { |thread| thread.add(current_user) }
|
||||
|
||||
expect(result.threads.length).to eq(10)
|
||||
end
|
||||
|
||||
it "can limit results" do
|
||||
params[:limit] = 1
|
||||
|
||||
Fabricate
|
||||
.times(2, :chat_thread, channel: channel_1, with_replies: 1)
|
||||
.each { |thread| thread.add(current_user) }
|
||||
|
||||
expect(result.threads.length).to eq(params[:limit])
|
||||
end
|
||||
|
||||
it "limits to 1 at least" do
|
||||
params[:limit] = 0
|
||||
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.threads.length).to eq(1)
|
||||
end
|
||||
|
||||
it "has a max limit" do
|
||||
params[:limit] = 11
|
||||
|
||||
Fabricate
|
||||
.times(11, :chat_thread, channel: channel_1, with_replies: 1)
|
||||
.each { |thread| thread.add(current_user) }
|
||||
|
||||
expect(result.threads.length).to eq(10)
|
||||
end
|
||||
|
||||
it "can offset" do
|
||||
params[:offset] = 1
|
||||
|
||||
threads =
|
||||
Fabricate
|
||||
.times(2, :chat_thread, channel: channel_1, with_replies: 1)
|
||||
.each { |thread| thread.add(current_user) }
|
||||
|
||||
# 0 because we sort by last_message.created_at, so the last created thread is the first one
|
||||
expect(result.threads).to eq([threads[0]])
|
||||
end
|
||||
|
||||
it "has a min offset" do
|
||||
params[:offset] = -99
|
||||
|
||||
threads =
|
||||
Fabricate
|
||||
.times(2, :chat_thread, channel: channel_1, with_replies: 1)
|
||||
.each { |thread| thread.add(current_user) }
|
||||
|
||||
# 0 because we sort by last_message.created_at, so the last created thread is the first one
|
||||
expect(result.threads.length).to eq(2)
|
||||
end
|
||||
|
||||
it "fetches tracking" do
|
||||
thread_1 =
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.tracking).to eq(
|
||||
::Chat::TrackingStateReportQuery.call(
|
||||
guardian: current_user.guardian,
|
||||
thread_ids: [thread_1.id],
|
||||
include_threads: true,
|
||||
).thread_tracking,
|
||||
)
|
||||
end
|
||||
|
||||
it "fetches memberships" do
|
||||
thread_1 =
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.memberships).to eq([thread_1.membership_for(current_user)])
|
||||
end
|
||||
|
||||
it "fetches participants" do
|
||||
thread_1 =
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.participants).to eq(
|
||||
::Chat::ThreadParticipantQuery.call(thread_ids: [thread_1.id]),
|
||||
)
|
||||
end
|
||||
|
||||
it "builds a load_more_url" do
|
||||
Fabricate(:chat_thread, channel: channel_1, with_replies: 1).tap do |thread|
|
||||
thread.add(current_user)
|
||||
end
|
||||
|
||||
expect(result.load_more_url).to eq("/chat/api/me/threads?limit=10&offset=10")
|
||||
end
|
||||
end
|
||||
|
||||
it "doesn't return threads with no replies" do
|
||||
thread_1 = Fabricate(:chat_thread, channel: channel_1)
|
||||
thread_1.add(current_user)
|
||||
|
||||
expect(result.threads).to eq([])
|
||||
end
|
||||
|
||||
it "doesn't return threads with no membership" do
|
||||
thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
|
||||
|
||||
expect(result.threads).to eq([])
|
||||
end
|
||||
|
||||
it "doesn't return threads when the channel has not threading enabled" do
|
||||
channel_1.update!(threading_enabled: false)
|
||||
thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
|
||||
thread_1.add(current_user)
|
||||
|
||||
expect(result.threads).to eq([])
|
||||
end
|
||||
|
||||
it "doesn't return muted threads" do
|
||||
thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
|
||||
thread_1.add(current_user)
|
||||
thread_1.membership_for(current_user).update!(
|
||||
notification_level: ::Chat::UserChatThreadMembership.notification_levels[:muted],
|
||||
)
|
||||
|
||||
expect(result.threads).to eq([])
|
||||
end
|
||||
|
||||
it "doesn't return threads when the channel it not open" do
|
||||
channel_1.update!(status: Chat::Channel.statuses[:closed])
|
||||
thread_1 = Fabricate(:chat_thread, channel: channel_1, with_replies: 1)
|
||||
thread_1.add(current_user)
|
||||
|
||||
expect(result.threads).to eq([])
|
||||
end
|
||||
end
|
|
@ -141,7 +141,7 @@ RSpec.describe "Navigation", type: :system do
|
|||
thread_list_page.open_thread(thread)
|
||||
expect(side_panel_page).to have_open_thread(thread)
|
||||
expect(thread_page).to have_back_link_to_thread_list(category_channel)
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
expect(page).to have_current_path("#{category_channel.relative_url}/t")
|
||||
expect(thread_list_page).to have_loaded
|
||||
end
|
||||
|
@ -157,7 +157,7 @@ RSpec.describe "Navigation", type: :system do
|
|||
thread_list_page.open_thread(thread)
|
||||
expect(side_panel_page).to have_open_thread(thread)
|
||||
expect(thread_page).to have_back_link_to_thread_list(category_channel)
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
expect(page).to have_current_path("#{category_channel.relative_url}/t")
|
||||
expect(thread_list_page).to have_loaded
|
||||
end
|
||||
|
@ -173,7 +173,7 @@ RSpec.describe "Navigation", type: :system do
|
|||
channel_page.message_thread_indicator(thread.original_message).click
|
||||
expect(side_panel_page).to have_open_thread(thread)
|
||||
expect(thread_page).to have_back_link_to_thread_list(category_channel)
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
expect(page).to have_current_path("#{category_channel.relative_url}/t")
|
||||
expect(thread_list_page).to have_loaded
|
||||
end
|
||||
|
@ -188,7 +188,7 @@ RSpec.describe "Navigation", type: :system do
|
|||
channel_page.message_thread_indicator(thread.original_message).click
|
||||
expect(side_panel_page).to have_open_thread(thread)
|
||||
expect(thread_page).to have_back_link_to_channel(category_channel)
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
expect(page).to have_current_path("#{category_channel.relative_url}")
|
||||
expect(side_panel_page).to be_closed
|
||||
end
|
||||
|
|
|
@ -53,6 +53,11 @@ module PageObjects
|
|||
has_finished_loading?
|
||||
end
|
||||
|
||||
def visit_user_threads
|
||||
visit("/chat/threads")
|
||||
has_no_css?(".spinner")
|
||||
end
|
||||
|
||||
def visit_thread(thread)
|
||||
visit(thread.url)
|
||||
has_css?(".chat-thread:not(.loading)[data-id=\"#{thread.id}\"]")
|
||||
|
|
|
@ -68,7 +68,7 @@ module PageObjects
|
|||
header.has_css?(".chat-thread__back-to-previous-route[href='#{channel.relative_url}']")
|
||||
end
|
||||
|
||||
def back_to_previous_route
|
||||
def back
|
||||
header.find(".chat-thread__back-to-previous-route").click
|
||||
end
|
||||
|
||||
|
|
16
plugins/chat/spec/system/page_objects/chat/user_threads.rb
Normal file
16
plugins/chat/spec/system/page_objects/chat/user_threads.rb
Normal file
|
@ -0,0 +1,16 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
module PageObjects
|
||||
module Pages
|
||||
class UserThreads < PageObjects::Pages::Base
|
||||
def has_threads?(count: nil)
|
||||
has_no_css?(".spinner")
|
||||
has_css?(".chat__user-threads__thread-container", count: count)
|
||||
end
|
||||
|
||||
def open_thread(thread)
|
||||
find(".chat__user-threads__thread-container[data-id='#{thread.id}']").click
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -37,6 +37,22 @@ module PageObjects
|
|||
channel_index.has_no_unread_channel?(channel)
|
||||
end
|
||||
|
||||
def has_user_threads_section?
|
||||
has_css?(".chat__user-threads-row-container[href='/chat/threads']")
|
||||
end
|
||||
|
||||
def has_unread_user_threads?
|
||||
has_css?(".chat__user-threads-row .chat__unread-indicator")
|
||||
end
|
||||
|
||||
def has_no_unread_user_threads?
|
||||
has_no_css?(".chat__user-threads-row .chat__unread-indicator")
|
||||
end
|
||||
|
||||
def click_user_threads
|
||||
find(".chat__user-threads-row").click
|
||||
end
|
||||
|
||||
def maximize
|
||||
mouseout
|
||||
find("#{VISIBLE_DRAWER} .chat-drawer-header__full-screen-btn").click
|
||||
|
|
|
@ -37,6 +37,25 @@ module PageObjects
|
|||
self
|
||||
end
|
||||
|
||||
def has_user_threads_section?
|
||||
has_css?(
|
||||
".sidebar-section-link[data-link-name='user-threads'][href='/chat/threads']",
|
||||
text: I18n.t("js.chat.my_threads.title"),
|
||||
)
|
||||
end
|
||||
|
||||
def has_unread_user_threads?
|
||||
has_css?(
|
||||
".sidebar-section-link[data-link-name='user-threads'] .sidebar-section-link-suffix.icon.unread",
|
||||
)
|
||||
end
|
||||
|
||||
def has_no_unread_user_threads?
|
||||
has_no_css?(
|
||||
".sidebar-section-link[data-link-name='user-threads'] .sidebar-section-link-suffix.icon.unread",
|
||||
)
|
||||
end
|
||||
|
||||
def has_unread_channel?(channel)
|
||||
has_css?(".sidebar-section-link.channel-#{channel.id} .sidebar-section-link-suffix.unread")
|
||||
end
|
||||
|
|
|
@ -35,7 +35,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru
|
|||
|
||||
expect(thread_page.messages).to have_message(text: text, persisted: true)
|
||||
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
|
||||
expect(channel_page).to have_thread_indicator(original_message)
|
||||
end
|
||||
|
@ -69,7 +69,7 @@ RSpec.describe "Reply to message - channel - mobile", type: :system, mobile: tru
|
|||
expect(thread_page.messages).to have_message(text: message_1.message)
|
||||
expect(thread_page.messages).to have_message(text: "reply to message")
|
||||
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
|
||||
expect(channel_page.message_thread_indicator(original_message)).to have_reply_count(2)
|
||||
expect(channel_page.messages).to have_no_message(text: "reply to message")
|
||||
|
|
|
@ -30,19 +30,19 @@ describe "Thread list in side panel | full page", type: :system do
|
|||
|
||||
before { chat_system_user_bootstrap(user: other_user, channel: channel) }
|
||||
|
||||
it "does not show existing threads in the channel if the user is not tracking them" do
|
||||
Fabricate(:chat_thread, original_message: thread_om, channel: channel, use_service: true)
|
||||
it "it shows threads in the channel even if the user is not tracking them" do
|
||||
thread_1 =
|
||||
Fabricate(
|
||||
:chat_thread,
|
||||
original_message: thread_om,
|
||||
channel: channel,
|
||||
with_replies: 1,
|
||||
use_service: true,
|
||||
)
|
||||
chat_page.visit_channel(channel)
|
||||
channel_page.open_thread_list
|
||||
expect(page).to have_content(I18n.t("js.chat.threads.none"))
|
||||
end
|
||||
|
||||
it "does not show new threads in the channel in the thread list if the user is not tracking them" do
|
||||
chat_page.visit_channel(channel)
|
||||
Fabricate(:chat_message, chat_channel: channel, in_reply_to: thread_om, use_service: true)
|
||||
channel_page.open_thread_list
|
||||
|
||||
expect(page).to have_content(I18n.t("js.chat.threads.none"))
|
||||
expect(thread_list_page).to have_thread(thread_1)
|
||||
end
|
||||
|
||||
describe "when the user creates a new thread" do
|
||||
|
|
|
@ -49,7 +49,7 @@ describe "Thread tracking state | full page", type: :system do
|
|||
|
||||
expect(thread_page).to have_no_unread_list_indicator
|
||||
|
||||
thread_page.back_to_previous_route
|
||||
thread_page.back
|
||||
|
||||
expect(thread_list_page).to have_no_unread_item(thread.id)
|
||||
end
|
||||
|
|
194
plugins/chat/spec/system/user_threads_spec.rb
Normal file
194
plugins/chat/spec/system/user_threads_spec.rb
Normal file
|
@ -0,0 +1,194 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
RSpec.describe "User threads", type: :system do
|
||||
fab!(:current_user) { Fabricate(:user) }
|
||||
fab!(:channel_1) { Fabricate(:chat_channel, threading_enabled: true) }
|
||||
|
||||
let(:chat_page) { PageObjects::Pages::Chat.new }
|
||||
let(:thread_page) { PageObjects::Pages::ChatThread.new }
|
||||
let(:sidebar_page) { PageObjects::Pages::Sidebar.new }
|
||||
let(:drawer_page) { PageObjects::Pages::ChatDrawer.new }
|
||||
let(:channel_page) { PageObjects::Pages::ChatChannel.new }
|
||||
let(:user_threads_page) { PageObjects::Pages::UserThreads.new }
|
||||
|
||||
before do
|
||||
chat_system_bootstrap
|
||||
sign_in(current_user)
|
||||
end
|
||||
|
||||
context "when in sidebar" do
|
||||
it "shows a link to user threads" do
|
||||
visit("/")
|
||||
|
||||
expect(sidebar_page).to have_user_threads_section
|
||||
end
|
||||
|
||||
context "when user has unreads" do
|
||||
before do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
xit "has an unread indicator" do
|
||||
visit("/")
|
||||
|
||||
expect(sidebar_page).to have_unread_user_threads
|
||||
end
|
||||
end
|
||||
|
||||
it "has no unread indicator when user has no unreads" do
|
||||
visit("/")
|
||||
|
||||
expect(sidebar_page).to have_no_unread_user_threads
|
||||
end
|
||||
|
||||
it "lists threads" do
|
||||
Fabricate
|
||||
.times(5, :chat_channel, threading_enabled: true)
|
||||
.each do |channel|
|
||||
chat_thread_chain_bootstrap(
|
||||
channel: channel,
|
||||
users: [current_user, Fabricate(:user)],
|
||||
messages_count: 2,
|
||||
)
|
||||
end
|
||||
|
||||
chat_page.visit_user_threads
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 5)
|
||||
end
|
||||
|
||||
it "can load more threads" do
|
||||
Fabricate
|
||||
.times(20, :chat_channel, threading_enabled: true)
|
||||
.each do |channel|
|
||||
chat_thread_chain_bootstrap(
|
||||
channel: channel,
|
||||
users: [current_user, Fabricate(:user)],
|
||||
messages_count: 2,
|
||||
)
|
||||
end
|
||||
|
||||
chat_page.visit_user_threads
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 10)
|
||||
|
||||
page.execute_script("window.scrollTo(0, document.body.scrollHeight)")
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 20)
|
||||
end
|
||||
|
||||
it "can open a thread" do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
|
||||
chat_page.visit_user_threads
|
||||
user_threads_page.open_thread(channel_1.threads.first)
|
||||
|
||||
expect(chat_page).to have_current_path(
|
||||
"/chat/c/#{channel_1.slug}/#{channel_1.id}/t/#{channel_1.threads.first.id} ",
|
||||
)
|
||||
end
|
||||
|
||||
it "navigating back from a thread opens the user threads" do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
|
||||
chat_page.visit_user_threads
|
||||
user_threads_page.open_thread(channel_1.threads.first)
|
||||
thread_page.back
|
||||
|
||||
expect(user_threads_page).to have_threads
|
||||
end
|
||||
end
|
||||
|
||||
context "when in drawer" do
|
||||
it "shows a link to user threads" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
|
||||
expect(drawer_page).to have_user_threads_section
|
||||
end
|
||||
|
||||
context "when user has unreads" do
|
||||
before do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
end
|
||||
|
||||
xit "has an unread indicator" do
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
|
||||
expect(drawer_page).to have_unread_user_threads
|
||||
end
|
||||
end
|
||||
|
||||
it "has no unread indicator when user has no unreads" do
|
||||
visit("/")
|
||||
|
||||
expect(sidebar_page).to have_no_unread_user_threads
|
||||
end
|
||||
|
||||
it "lists threads" do
|
||||
Fabricate
|
||||
.times(5, :chat_channel, threading_enabled: true)
|
||||
.each do |channel|
|
||||
chat_thread_chain_bootstrap(
|
||||
channel: channel,
|
||||
users: [current_user, Fabricate(:user)],
|
||||
messages_count: 2,
|
||||
)
|
||||
end
|
||||
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.click_user_threads
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 5)
|
||||
end
|
||||
|
||||
it "can load more threads" do
|
||||
Fabricate
|
||||
.times(20, :chat_channel, threading_enabled: true)
|
||||
.each do |channel|
|
||||
chat_thread_chain_bootstrap(
|
||||
channel: channel,
|
||||
users: [current_user, Fabricate(:user)],
|
||||
messages_count: 2,
|
||||
)
|
||||
end
|
||||
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.click_user_threads
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 10)
|
||||
|
||||
page.execute_script(
|
||||
"document.querySelector('.chat-drawer-content').scrollTo(0, document.querySelector('.chat-drawer-content').scrollHeight)",
|
||||
)
|
||||
|
||||
expect(user_threads_page).to have_threads(count: 20)
|
||||
end
|
||||
|
||||
it "can open a thread" do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.click_user_threads
|
||||
user_threads_page.open_thread(channel_1.threads.first)
|
||||
|
||||
expect(drawer_page).to have_open_thread(channel_1.threads.first)
|
||||
end
|
||||
|
||||
it "navigating back from a thread opens the user threads" do
|
||||
chat_thread_chain_bootstrap(channel: channel_1, users: [current_user, Fabricate(:user)])
|
||||
|
||||
visit("/")
|
||||
chat_page.open_from_header
|
||||
drawer_page.click_user_threads
|
||||
user_threads_page.open_thread(channel_1.threads.first)
|
||||
drawer_page.back
|
||||
|
||||
expect(user_threads_page).to have_threads
|
||||
end
|
||||
end
|
||||
end
|
|
@ -6,13 +6,13 @@ import { exists, query } from "discourse/tests/helpers/qunit-helpers";
|
|||
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
|
||||
import { CHATABLE_TYPES } from "discourse/plugins/chat/discourse/models/chat-channel";
|
||||
|
||||
module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
||||
module("Discourse Chat | Component | <ChannelTitle />", function (hooks) {
|
||||
setupRenderingTest(hooks);
|
||||
|
||||
test("category channel", async function (assert) {
|
||||
this.channel = fabricators.channel();
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
assert.strictEqual(
|
||||
query(".chat-channel-title__category-badge").getAttribute("style"),
|
||||
|
@ -30,7 +30,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
|||
title: "<div class='xss'>evil</div>",
|
||||
});
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
assert.false(exists(".xss"));
|
||||
});
|
||||
|
@ -40,7 +40,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
|||
chatable: fabricators.category({ read_restricted: true }),
|
||||
});
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
assert.true(exists(".d-icon-lock"));
|
||||
});
|
||||
|
@ -50,7 +50,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
|||
chatable: fabricators.category({ read_restricted: false }),
|
||||
});
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
assert.false(exists(".d-icon-lock"));
|
||||
});
|
||||
|
@ -62,7 +62,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
|||
}),
|
||||
});
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
const user = this.channel.chatable.users[0];
|
||||
|
||||
|
@ -79,7 +79,7 @@ module("Discourse Chat | Component | chat-channel-title", function (hooks) {
|
|||
});
|
||||
this.channel.chatable.group = true;
|
||||
|
||||
await render(hbs`<ChatChannelTitle @channel={{this.channel}} />`);
|
||||
await render(hbs`<ChannelTitle @channel={{this.channel}} />`);
|
||||
|
||||
const users = this.channel.chatable.users;
|
||||
|
|
@ -429,7 +429,7 @@ module(
|
|||
);
|
||||
assert.true(
|
||||
queryAll(".chat-message-collapser-link-small")[1].innerHTML.includes(
|
||||
"%3Cscript%3Esomeeviltitle%3C/script%3E"
|
||||
"<script>someeviltitle</script>"
|
||||
)
|
||||
);
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue
Block a user