mirror of
https://github.com/discourse/discourse.git
synced 2025-06-06 02:54:41 +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:
@ -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 />
|
Reference in New Issue
Block a user