DEV: implements <Chat::Navbar /> (#24917)

This new navbar component is used for every navbar in chat, full page or drawer, and any screen.

This commit also uses this opportunity to correctly decouple drawer-routes from full page routes. This will avoid having this kind of properties in components: `@includeHeader={{false}}`. The header is now defined in the parent template using a navbar. Each route has now its own template wrapped in a div of the name of the route, eg: `<div class="c-routes-threads">..</div>`.

The navbar API:

```gjs
<Navbar as |navbar|>
 <navbar.BackButton />
 <navbar.Title @title="Foo" />
 <navbar.ChannelTitle @channel={{@channel}} />
 <navbar.Actions as |action|>
   <action.CloseThreadButton />
 </navbar.Actions>
</navbar>
```

The full list of components is listed in `plugins/chat/assets/javascripts/discourse/components/navbar/index.gjs` and `plugins/chat/assets/javascripts/discourse/components/navbar/actions.gjs`.

Visually the header is not changing much, only in drawer mode the background has been removed.

This commit also introduces a `<List />` component to facilitate rendering lists in chat plugin.
This commit is contained in:
Joffrey JAFFEUX
2023-12-18 17:49:58 +01:00
committed by GitHub
parent a08691a599
commit 53b96638c5
110 changed files with 1370 additions and 1716 deletions

View File

@ -412,11 +412,6 @@ html {
.reviewable .status span.approved { .reviewable .status span.approved {
color: var(--success-hover); color: var(--success-hover);
} }
// Chat
.chat-channel .open-drawer-btn {
color: var(--primary-medium);
}
} }
// chat // chat

View File

@ -1,88 +0,0 @@
<div class="chat-browse-view__header chat-full-page-header">
{{#if this.site.mobileView}}
<LinkTo
@route="chat.index"
class="chat-full-page-header__back-btn no-text btn-flat btn"
title={{i18n "chat.browse.back"}}
>
{{d-icon "chevron-left"}}
</LinkTo>
{{/if}}
<span class="chat-browse-view__title">{{i18n "chat.browse.title"}}</span>
{{#if this.currentUser.staff}}
<DButton
@action={{this.createChannel}}
@icon="plus"
@label={{if this.site.desktopView "chat.create_channel.title"}}
class={{concat-class
"new-channel-btn"
(if this.site.mobileView "btn-flat")
}}
/>
{{/if}}
</div>
<div class="chat-browse-view">
<div class="chat-browse-view__actions">
<nav>
<ul class="nav-pills chat-browse-view__filters">
{{#each this.tabs as |tab|}}
<li class={{concat "chat-browse-view__filter -" tab}}>
<LinkTo
@route={{concat "chat.browse." tab}}
class={{concat "chat-browse-view__filter-link -" tab}}
>
{{i18n (concat "chat.browse.filter_" tab)}}
</LinkTo>
</li>
{{/each}}
</ul>
</nav>
<DcFilterInput
{{did-insert (action this.focusFilterInput)}}
@filterAction={{this.debouncedFiltering}}
@icons={{hash right="search"}}
@containerClass="filter-input"
placeholder={{i18n "chat.browse.filter_input_placeholder"}}
/>
</div>
{{#if
(and
this.channelsCollection.fetchedOnce (not this.channelsCollection.length)
)
}}
<div class="empty-state">
<span class="empty-state-title">{{i18n "chat.empty_state.title"}}</span>
<div class="empty-state-body">
<p>{{i18n "chat.empty_state.direct_message"}}</p>
<DButton
@action={{this.showChatNewMessageModal}}
@label="chat.empty_state.direct_message_cta"
/>
</div>
</div>
{{else if this.channelsCollection.length}}
<LoadMore
@selector=".chat-channel-card"
@action={{this.channelsCollection.load}}
>
<div class="chat-browse-view__content_wrapper">
<div class="chat-browse-view__content">
<div class="chat-browse-view__cards">
{{#each this.channelsCollection as |channel|}}
<ChatChannelCard @channel={{channel}} />
{{/each}}
</div>
</div>
</div>
<ConditionalLoadingSpinner
@condition={{this.channelsCollection.loading}}
/>
</LoadMore>
{{/if}}
</div>

View File

@ -1,76 +0,0 @@
import Component from "@ember/component";
import { action, computed } from "@ember/object";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { INPUT_DELAY } from "discourse-common/config/environment";
import discourseDebounce from "discourse-common/lib/debounce";
import ChatModalCreateChannel from "discourse/plugins/chat/discourse/components/chat/modal/create-channel";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
const TABS = ["all", "open", "closed", "archived"];
export default class ChatBrowseView extends Component {
@service chatApi;
@service modal;
tagName = "";
didReceiveAttrs() {
super.didReceiveAttrs(...arguments);
if (!this.channelsCollection) {
this.set("channelsCollection", this.chatApi.channels());
}
this.channelsCollection.load({
filter: this.filter,
status: this.status,
});
}
@computed("siteSettings.chat_allow_archiving_channels")
get tabs() {
if (this.siteSettings.chat_allow_archiving_channels) {
return TABS;
} else {
return [...TABS].removeObject("archived");
}
}
@action
showChatNewMessageModal() {
this.modal.show(ChatModalNewMessage);
}
@action
onScroll() {
discourseDebounce(
this,
this.channelsCollection.load,
{ filter: this.filter, status: this.status },
INPUT_DELAY
);
}
@action
debouncedFiltering(event) {
this.set("channelsCollection", this.chatApi.channels());
discourseDebounce(
this,
this.channelsCollection.load,
{ filter: event.target.value, status: this.status },
INPUT_DELAY
);
}
@action
createChannel() {
this.modal.show(ChatModalCreateChannel);
}
@action
focusFilterInput(input) {
schedule("afterRender", () => input?.focus());
}
}

View File

@ -1,110 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
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";
export default class ChatChannelMessageEmojiPicker extends Component {
@service chatChannelInfoRouteOriginManager;
@service site;
@service modal;
@service chatGuardian;
membersLabel = I18n.t("chat.channel_info.tabs.members");
settingsLabel = I18n.t("chat.channel_info.tabs.settings");
backToChannelLabel = I18n.t("chat.channel_info.back_to_all_channel");
backToAllChannelsLabel = I18n.t("chat.channel_info.back_to_channel");
get showTabs() {
return this.site.desktopView && this.args.channel.isOpen;
}
get canEditChannel() {
return (
this.chatGuardian.canEditChatChannel() &&
(this.args.channel.isCategoryChannel ||
(this.args.channel.isDirectMessageChannel &&
this.args.channel.chatable.group))
);
}
@action
editChannelTitle() {
return this.modal.show(ChatModalEditChannelName, {
model: this.args.channel,
});
}
<template>
<div class="chat-full-page-header">
<div class="chat-channel-header-details">
<div class="chat-full-page-header__left-actions">
{{#if this.chatChannelInfoRouteOriginManager.isBrowse}}
<LinkTo
@route="chat.browse"
class="chat-full-page-header__back-btn no-text btn-flat btn"
title={{this.backToAllChannelsLabel}}
>
{{icon "chevron-left"}}
</LinkTo>
{{else}}
<LinkTo
@route="chat.channel"
@models={{@channel.routeModels}}
class="chat-full-page-header__back-btn no-text btn-flat btn"
title={{this.backToChannelLabel}}
>
{{icon "chevron-left"}}
</LinkTo>
{{/if}}
</div>
<ChannelTitle @channel={{@channel}} />
{{#if this.canEditChannel}}
<DButton
@icon="pencil-alt"
class="btn-flat"
@action={{this.editChannelTitle}}
/>
{{/if}}
</div>
</div>
<ChatChannelStatus @channel={{@channel}} />
<div class="chat-channel-info">
{{#if this.showTabs}}
<nav class="chat-channel-info__nav">
<ul class="nav nav-pills">
<li>
<LinkTo
@route="chat.channel.info.settings"
@model={{@channel}}
@replace={{true}}
>
{{this.settingsLabel}}
</LinkTo>
</li>
<li>
<LinkTo
@route="chat.channel.info.members"
@model={{@channel}}
@replace={{true}}
>
{{this.membersLabel}}
</LinkTo>
</li>
</ul>
</nav>
{{/if}}
{{outlet}}
</div>
</template>
}

View File

@ -21,6 +21,7 @@ import discourseDebounce from "discourse-common/lib/debounce";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import and from "truth-helpers/helpers/and"; import and from "truth-helpers/helpers/and";
import not from "truth-helpers/helpers/not"; import not from "truth-helpers/helpers/not";
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager"; import ChatChannelSubscriptionManager from "discourse/plugins/chat/discourse/lib/chat-channel-subscription-manager";
import { import {
FUTURE, FUTURE,
@ -45,7 +46,6 @@ import ChatComposerChannel from "./chat/composer/channel";
import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow"; import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow";
import ChatSelectionManager from "./chat/selection-manager"; import ChatSelectionManager from "./chat/selection-manager";
import ChatChannelPreviewCard from "./chat-channel-preview-card"; import ChatChannelPreviewCard from "./chat-channel-preview-card";
import ChatFullPageHeader from "./chat-full-page-header";
import ChatMentionWarnings from "./chat-mention-warnings"; import ChatMentionWarnings from "./chat-mention-warnings";
import Message from "./chat-message"; import Message from "./chat-message";
import ChatNotices from "./chat-notices"; import ChatNotices from "./chat-notices";
@ -719,14 +719,9 @@ export default class ChatChannel extends Component {
{{didUpdate this.loadMessages @targetMessageId}} {{didUpdate this.loadMessages @targetMessageId}}
data-id={{@channel.id}} data-id={{@channel.id}}
> >
<ChatFullPageHeader
@channel={{@channel}}
@onCloseFullScreen={{this.onCloseFullScreen}}
@displayed={{this.includeHeader}}
/>
<ChatChannelStatus @channel={{@channel}} />
<ChatNotices @channel={{@channel}} /> <ChatNotices @channel={{@channel}} />
<ChatMentionWarnings /> <ChatMentionWarnings />
<div <div

View File

@ -1,70 +0,0 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import I18n from "discourse-i18n";
import and from "truth-helpers/helpers/and";
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 ChatThreadList from "discourse/plugins/chat/discourse/components/chat-thread-list";
export default class ChatDrawerChannelThreads extends Component {
@service appEvents;
@service chat;
@service chatStateManager;
@service chatChannelsManager;
backLinkTitle = I18n.t("chat.return_to_list");
@action
fetchChannel() {
if (!this.args.params?.channelId) {
return;
}
return this.chatChannelsManager
.find(this.args.params.channelId)
.then((channel) => {
this.chat.activeChannel = channel;
});
}
<template>
<ChatDrawerHeader @toggleExpand={{@drawerActions.toggleExpand}}>
{{#if
(and this.chatStateManager.isDrawerExpanded this.chat.activeChannel)
}}
<div class="chat-drawer-header__left-actions">
<div class="chat-drawer-header__top-line">
<ChatDrawerHeaderBackLink
@route="chat.channel"
@title={{this.backLinkTitle}}
@routeModels={{this.chat.activeChannel.routeModels}}
/>
</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" {{didInsert this.fetchChannel}}>
{{#if this.chat.activeChannel}}
<ChatThreadList
@channel={{this.chat.activeChannel}}
@includeHeader={{false}}
/>
{{/if}}
</div>
{{/if}}
</template>
}

View File

@ -1,25 +0,0 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { inject as service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
export default class ChatDrawerHeader extends Component {
@service chatStateManager;
<template>
{{! template-lint-disable no-invalid-interactive }}
<div
role="region"
aria-label={{i18n "chat.aria_roles.header"}}
class="chat-drawer-header"
{{on "click" @toggleExpand}}
title={{if
this.chatStateManager.isDrawerExpanded
(i18n "chat.collapse")
(i18n "chat.expand")
}}
>
{{yield}}
</div>
</template>
}

View File

@ -1,21 +0,0 @@
import Component from "@glimmer/component";
import { array } from "@ember/helper";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import dIcon from "discourse-common/helpers/d-icon";
import or from "truth-helpers/helpers/or";
export default class ChatDrawerHeaderBackLink extends Component {
@service chatStateManager;
<template>
<LinkTo
title={{@title}}
class="chat-drawer-header__back-btn"
@route={{@route}}
@models={{or @routeModels (array)}}
>
{{dIcon "chevron-left"}}
</LinkTo>
</template>
}

View File

@ -1,45 +0,0 @@
import Component from "@glimmer/component";
import { on } from "@ember/modifier";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import ChannelTitle from "../../channel-title";
export default class ChatDrawerChannelHeaderTitle extends Component {
@service chatStateManager;
<template>
{{#if @channel}}
{{#if this.chatStateManager.isDrawerExpanded}}
<LinkTo
@route={{if
@channel.isDirectMessageChannel
"chat.channel.info.settings"
"chat.channel.info.members"
}}
@models={{@channel.routeModels}}
class="chat-drawer-header__title"
>
<div class="chat-drawer-header__top-line">
<ChannelTitle @channel={{@channel}} />
</div>
</LinkTo>
{{else}}
<div
role="button"
{{on "click" @drawerActions.toggleExpand}}
class="chat-drawer-header__title"
>
<div class="chat-drawer-header__top-line">
<ChannelTitle @channel={{@channel}}>
{{#if @channel.tracking.unreadCount}}
<span class="chat-unread-count">
{{@channel.tracking.unreadCount}}
</span>
{{/if}}
</ChannelTitle>
</div>
</div>
{{/if}}
{{/if}}
</template>
}

View File

@ -1,12 +0,0 @@
import DButton from "discourse/components/d-button";
const CloseButton = <template>
<DButton
@icon="times"
@action={{@close}}
@title="chat.close"
class="btn-flat btn-link chat-drawer-header__close-btn"
/>
</template>;
export default CloseButton;

View File

@ -1,18 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
export default class ChatDrawerHeaderFullPageButton extends Component {
@service chatStateManager;
<template>
{{#if this.chatStateManager.isDrawerExpanded}}
<DButton
@icon="discourse-expand"
class="btn-flat btn-link chat-drawer-header__full-screen-btn"
@title="chat.open_full_page"
@action={{@openInFullPage}}
/>
{{/if}}
</template>
}

View File

@ -1,18 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
import BackLink from "./back-link";
export default class ChatDrawerHeaderLeftActions extends Component {
@service chatStateManager;
<template>
{{#if this.chatStateManager.isDrawerExpanded}}
<div class="chat-drawer-header__left-actions">
<div class="chat-drawer-header__top-line">
<BackLink @route="chat" @title={{i18n "chat.return_to_list"}} />
</div>
</div>
{{/if}}
</template>
}

View File

@ -1,30 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import ThreadsListButton from "../../chat/thread/threads-list-button";
import CloseButton from "./close-button";
import FullPageButton from "./full-page-button";
import ToggleExpandButton from "./toggle-expand-button";
export default class ChatDrawerHeaderRightActions extends Component {
@service chat;
get showThreadsListButton() {
return this.chat.activeChannel?.threadingEnabled;
}
<template>
<div class="chat-drawer-header__right-actions">
<div class="chat-drawer-header__top-line">
{{#if this.showThreadsListButton}}
<ThreadsListButton @channel={{this.chat.activeChannel}} />
{{/if}}
<ToggleExpandButton @toggleExpand={{@drawerActions.toggleExpand}} />
<FullPageButton @openInFullPage={{@drawerActions.openInFullPage}} />
<CloseButton @close={{@drawerActions.close}} />
</div>
</div>
</template>
}

View File

@ -1,40 +0,0 @@
import Component from "@glimmer/component";
import replaceEmoji from "discourse/helpers/replace-emoji";
import icon from "discourse-common/helpers/d-icon";
import I18n from "discourse-i18n";
export default class ChatDrawerHeaderTitle extends Component {
get headerTitle() {
if (this.args.title) {
return I18n.t(this.args.title);
}
return replaceEmoji(this.args.translatedTitle);
}
get showChannel() {
return this.args.channelName ?? false;
}
get showIcon() {
return this.args.icon ?? false;
}
<template>
<span class="chat-drawer-header__title">
<div class="chat-drawer-header__top-line">
<span class="chat-drawer-header__icon">
{{#if this.showIcon}}
{{icon @icon}}
{{/if}}
</span>
<span class="chat-drawer-header__title-text">{{this.headerTitle}}</span>
{{#if this.showChannel}}
<span class="chat-drawer-header__divider">-</span>
<span class="chat-drawer-header__channel-name">{{@channelName}}</span>
{{/if}}
</div>
</span>
</template>
}

View File

@ -1,28 +0,0 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
import ChannelsList from "../channels-list";
import Header from "./header";
import RightActions from "./header/right-actions";
export default class ChatDrawerIndex extends Component {
@service chatStateManager;
<template>
<Header @toggleExpand={{@drawerActions.toggleExpand}}>
<div class="chat-drawer-header__title">
<div class="chat-drawer-header__top-line">
{{i18n "chat.heading"}}
</div>
</div>
<RightActions @drawerActions={{@drawerActions}} />
</Header>
{{#if this.chatStateManager.isDrawerExpanded}}
<div class="chat-drawer-content">
<ChannelsList />
</div>
{{/if}}
</template>
}

View File

@ -1,47 +0,0 @@
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>
}

View File

@ -1,99 +0,0 @@
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";
import DButton from "discourse/components/d-button";
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";
export default class ChatFullPageHeader extends Component {
@service chatStateManager;
@service modal;
@service router;
@service site;
get displayed() {
return this.args.displayed ?? true;
}
get showThreadsListButton() {
return (
this.args.channel.threadingEnabled &&
this.router.currentRoute.name !== "chat.channel.threads" &&
this.router.currentRoute.name !== "chat.channel.thread.index" &&
this.router.currentRoute.name !== "chat.channel.thread"
);
}
@action
editChannelTitle() {
return this.modal.show(ChatModalEditChannelName, {
model: this.args.channel,
});
}
@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}}
<div class="chat-full-page-header__left-actions">
<LinkTo
@route="chat"
class="chat-full-page-header__back-btn no-text btn-flat"
>
{{icon "chevron-left"}}
</LinkTo>
</div>
{{/if}}
<LinkTo
@route="chat.channel.info"
@models={{@channel.routeModels}}
class="chat-channel-title-wrapper"
>
<ChannelTitle @channel={{@channel}} />
</LinkTo>
{{#if (or @channel.threadingEnabled this.site.desktopView)}}
<div class="chat-full-page-header__right-actions">
{{#if this.site.desktopView}}
<DButton
@icon="discourse-compress"
@title="chat.close_full_page"
class="open-drawer-btn btn-flat"
@action={{@onCloseFullScreen}}
/>
{{/if}}
{{#if this.showThreadsListButton}}
<ThreadsListButton @channel={{@channel}} />
{{/if}}
</div>
{{/if}}
</div>
</div>
<ChatChannelStatus @channel={{@channel}} />
{{/if}}
</template>
}

View File

@ -98,12 +98,15 @@ export default class ChatMessageThreadIndicator extends Component {
@bind @bind
openThread(event) { openThread(event) {
if (event.type === "keydown" && event.key !== "Enter") { if (event?.type === "keydown" && event?.key !== "Enter") {
return; return;
} }
// handle middle mouse // handle middle mouse
if (event.type === "mousedown" && (event.which === 2 || event.shiftKey)) { if (
event?.type === "mousedown" &&
(event?.which === 2 || event?.shiftKey)
) {
window.open( window.open(
getURL( getURL(
this.router.urlFor( this.router.urlFor(

View File

@ -41,7 +41,7 @@ export default class ChatRetentionReminder extends Component {
<DButton <DButton
@action={{this.dismiss}} @action={{this.dismiss}}
@icon="times" @icon="times"
class="btn-flat dismiss-btn" class="btn no-text btn-icon btn-flat no-text dismiss-btn"
/> />
</div> </div>
{{/if}} {{/if}}

View File

@ -7,7 +7,6 @@ import isElementInViewport from "discourse/lib/is-element-in-viewport";
import { bind } from "discourse-common/utils/decorators"; import { bind } from "discourse-common/utils/decorators";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import eq from "truth-helpers/helpers/eq"; import eq from "truth-helpers/helpers/eq";
import ChatThreadListHeader from "discourse/plugins/chat/discourse/components/chat/thread-list/header";
import ChatThreadListItem from "discourse/plugins/chat/discourse/components/chat/thread-list/item"; import ChatThreadListItem from "discourse/plugins/chat/discourse/components/chat/thread-list/item";
import ChatTrackMessage from "discourse/plugins/chat/discourse/modifiers/chat/track-message"; import ChatTrackMessage from "discourse/plugins/chat/discourse/modifiers/chat/track-message";
@ -179,10 +178,6 @@ export default class ChatThreadList extends Component {
<template> <template>
{{#if this.shouldRender}} {{#if this.shouldRender}}
<div class="chat-thread-list" {{this.subscribe @channel}}> <div class="chat-thread-list" {{this.subscribe @channel}}>
{{#if @includeHeader}}
<ChatThreadListHeader @channel={{@channel}} />
{{/if}}
<div class="chat-thread-list__items" {{this.fill}}> <div class="chat-thread-list__items" {{this.fill}}>
{{#each this.sortedThreads key="id" as |thread|}} {{#each this.sortedThreads key="id" as |thread|}}

View File

@ -0,0 +1,14 @@
import NotificationsButtonComponent from "select-kit/components/notifications-button";
import { threadNotificationButtonLevels } from "discourse/plugins/chat/discourse/lib/chat-notification-levels";
export default NotificationsButtonComponent.extend({
pluginApiIdentifiers: ["thread-notifications-button"],
classNames: ["thread-notifications-button"],
content: threadNotificationButtonLevels,
selectKitOptions: {
i18nPrefix: "chat.thread.notifications",
showFullTitle: false,
btnCustomClasses: "btn-flat",
},
});

View File

@ -36,7 +36,6 @@ import ChatScrollableList from "../modifiers/chat/scrollable-list";
import ChatComposerThread from "./chat/composer/thread"; import ChatComposerThread from "./chat/composer/thread";
import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow"; import ChatScrollToBottomArrow from "./chat/scroll-to-bottom-arrow";
import ChatSelectionManager from "./chat/selection-manager"; import ChatSelectionManager from "./chat/selection-manager";
import ChatThreadHeader from "./chat/thread/header";
import Message from "./chat-message"; import Message from "./chat-message";
import ChatSkeleton from "./chat-skeleton"; import ChatSkeleton from "./chat-skeleton";
import ChatUploadDropZone from "./chat-upload-drop-zone"; import ChatUploadDropZone from "./chat-upload-drop-zone";
@ -493,10 +492,6 @@ export default class ChatThread extends Component {
{{didInsert this.setup}} {{didInsert this.setup}}
{{willDestroy this.teardown}} {{willDestroy this.teardown}}
> >
{{#if @includeHeader}}
<ChatThreadHeader @channel={{@thread.channel}} @thread={{@thread}} />
{{/if}}
<div <div
class="chat-thread__body popper-viewport chat-messages-scroll" class="chat-thread__body popper-viewport chat-messages-scroll"
{{didInsert this.setScrollable}} {{didInsert this.setScrollable}}

View File

@ -4,24 +4,20 @@ import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import and from "truth-helpers/helpers/and"; import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
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 ChatThread from "discourse/plugins/chat/discourse/components/chat-thread"; import ChatThread from "discourse/plugins/chat/discourse/components/chat-thread";
export default class ChatDrawerThread extends Component { export default class ChatDrawerRoutesChannelThread extends Component {
@service appEvents;
@service chat; @service chat;
@service chatStateManager; @service chatStateManager;
@service chatChannelsManager; @service chatChannelsManager;
@service chatHistory; @service chatHistory;
get backLink() { get backButton() {
const link = { const link = {
models: this.chat.activeChannel.routeModels, models: this.chat.activeChannel?.routeModels,
}; };
if (this.chatHistory.previousRoute?.name === "chat.channel.threads") { if (this.chatHistory.previousRoute?.name === "chat.channel.threads") {
@ -47,14 +43,16 @@ export default class ChatDrawerThread extends Component {
} }
@action @action
fetchChannelAndThread() { async fetchChannelAndThread() {
if (!this.args.params?.channelId || !this.args.params?.threadId) { if (!this.args.params?.channelId || !this.args.params?.threadId) {
return; return;
} }
return this.chatChannelsManager try {
.find(this.args.params.channelId) const channel = await this.chatChannelsManager.find(
.then((channel) => { this.args.params.channelId
);
this.chat.activeChannel = channel; this.chat.activeChannel = channel;
channel.threadsManager channel.threadsManager
@ -62,29 +60,25 @@ export default class ChatDrawerThread extends Component {
.then((thread) => { .then((thread) => {
this.chat.activeChannel.activeThread = thread; this.chat.activeChannel.activeThread = thread;
}); });
}); } catch (error) {
popupAjaxError(error);
}
} }
<template> <template>
<ChatDrawerHeader @toggleExpand={{@drawerActions.toggleExpand}}> <Navbar @onClick={{this.chat.toggleDrawer}} as |navbar|>
{{#if <navbar.BackButton
(and this.chatStateManager.isDrawerExpanded this.chat.activeChannel) @title={{this.backButton.title}}
}} @route={{this.backButton.route}}
<div class="chat-drawer-header__left-actions"> @routeModels={{this.backButton.models}}
<div class="chat-drawer-header__top-line">
<ChatDrawerHeaderBackLink
@route={{this.backLink.route}}
@title={{this.backLink.title}}
@routeModels={{this.backLink.models}}
/> />
</div> <navbar.Title @title={{this.threadTitle}} @icon="discourse-threads" />
</div> <navbar.Actions as |action|>
{{/if}} <action.ToggleDrawerButton />
<action.FullPageButton />
<ChatDrawerHeaderTitle @translatedTitle={{this.threadTitle}} /> <action.CloseDrawerButton />
</navbar.Actions>
<ChatDrawerHeaderRightActions @drawerActions={{@drawerActions}} /> </Navbar>
</ChatDrawerHeader>
{{#if this.chatStateManager.isDrawerExpanded}} {{#if this.chatStateManager.isDrawerExpanded}}
<div <div

View File

@ -0,0 +1,68 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { inject as service } from "@ember/service";
import replaceEmoji from "discourse/helpers/replace-emoji";
import { popupAjaxError } from "discourse/lib/ajax-error";
import htmlSafe from "discourse-common/helpers/html-safe";
import I18n from "discourse-i18n";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import ChatThreadList from "discourse/plugins/chat/discourse/components/chat-thread-list";
export default class ChatDrawerRoutesChannelThreads extends Component {
@service chat;
@service chatChannelsManager;
backLinkTitle = I18n.t("chat.return_to_list");
get title() {
return htmlSafe(
I18n.t("chat.threads.list") +
" - " +
replaceEmoji(this.chat.activeChannel.title)
);
}
@action
async fetchChannel() {
if (!this.args.params?.channelId) {
return;
}
try {
const channel = await this.chatChannelsManager.find(
this.args.params.channelId
);
this.chat.activeChannel = channel;
} catch (error) {
popupAjaxError(error);
}
}
<template>
{{#if this.chat.activeChannel}}
<Navbar @onClick={{this.chat.toggleDrawer}} as |navbar|>
<navbar.BackButton
@title={{this.backLinkTitle}}
@route="chat.channel"
@routeModels={{this.chat.activeChannel?.routeModels}}
/>
<navbar.Title @title={{this.title}} @icon="discourse-threads" />
<navbar.Actions as |action|>
<action.ToggleDrawerButton />
<action.FullPageButton />
<action.CloseDrawerButton />
</navbar.Actions>
</Navbar>
{{/if}}
<div class="chat-drawer-content" {{didInsert this.fetchChannel}}>
{{#if this.chat.activeChannel}}
<ChatThreadList
@channel={{this.chat.activeChannel}}
@includeHeader={{false}}
/>
{{/if}}
</div>
</template>
}

View File

@ -4,14 +4,10 @@ import { action } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert"; import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import didUpdate from "@ember/render-modifiers/modifiers/did-update"; import didUpdate from "@ember/render-modifiers/modifiers/did-update";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import ChatChannel from "../chat-channel"; import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import Header from "./header"; import ChatChannel from "discourse/plugins/chat/discourse/components/chat-channel";
import ChannelTitle from "./header/channel-title";
import LeftActions from "./header/left-actions";
import RightActions from "./header/right-actions";
export default class ChatDrawerChannel extends Component { export default class ChatDrawerRoutesChannel extends Component {
@service appEvents;
@service chat; @service chat;
@service chatStateManager; @service chatStateManager;
@service chatChannelsManager; @service chatChannelsManager;
@ -30,16 +26,16 @@ export default class ChatDrawerChannel extends Component {
} }
<template> <template>
<Header @toggleExpand={{@drawerActions.toggleExpand}}> <Navbar @onClick={{this.chat.toggleDrawer}} as |navbar|>
<LeftActions /> <navbar.BackButton />
<navbar.ChannelTitle @channel={{this.chat.activeChannel}} />
<ChannelTitle <navbar.Actions as |action|>
@channel={{this.chat.activeChannel}} <action.ThreadsListButton @channel={{this.chat.activeChannel}} />
@drawerActions={{@drawerActions}} <action.ToggleDrawerButton />
/> <action.FullPageButton />
<action.CloseDrawerButton />
<RightActions @drawerActions={{@drawerActions}} /> </navbar.Actions>
</Header> </Navbar>
{{#if this.chatStateManager.isDrawerExpanded}} {{#if this.chatStateManager.isDrawerExpanded}}
<div <div

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
import ChannelsList from "discourse/plugins/chat/discourse/components/channels-list";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
export default class ChatDrawerRoutesChannels extends Component {
@service chat;
@service chatStateManager;
<template>
<Navbar @onClick={{this.chat.toggleDrawer}} as |navbar|>
<navbar.Title @title={{i18n "chat.heading"}} />
<navbar.Actions as |action|>
<action.ToggleDrawerButton />
<action.FullPageButton />
<action.CloseDrawerButton />
</navbar.Actions>
</Navbar>
{{#if this.chatStateManager.isDrawerExpanded}}
<div class="chat-drawer-content">
<ChannelsList />
</div>
{{/if}}
</template>
}

View File

@ -0,0 +1,38 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import UserThreads from "discourse/plugins/chat/discourse/components/user-threads";
export default class ChatDrawerRoutesThreads extends Component {
@service chat;
@service chatStateManager;
backButtonTitle = I18n.t("chat.return_to_list");
<template>
<Navbar @onClick={{this.chat.toggleDrawer}} as |navbar|>
<navbar.BackButton @title={{this.backButtonTitle}} />
<navbar.Title
@title={{i18n "chat.threads.list"}}
@icon="discourse-threads"
as |title|
>
<title.SubTitle @title={{this.chat.activeChannel.title}} />
</navbar.Title>
<navbar.Actions as |action|>
<action.ThreadsListButton />
<action.ToggleDrawerButton />
<action.FullPageButton />
<action.CloseDrawerButton />
</navbar.Actions>
</Navbar>
{{#if this.chatStateManager.isDrawerExpanded}}
<div class="chat-drawer-content">
<UserThreads />
</div>
{{/if}}
</template>
}

View File

@ -0,0 +1,9 @@
import Component from "@glimmer/component";
export default class EmptyState extends Component {
<template>
<div class="c-list-empty-state" ...attributes>
{{yield}}
</div>
</template>
}

View File

@ -0,0 +1,72 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { action } from "@ember/object";
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 EmptyState from "./empty-state";
import Item from "./item";
export default class List extends Component {
loadMore = modifier((element) => {
this.intersectionObserver = new IntersectionObserver(this.loadCollection);
this.intersectionObserver.observe(element);
return () => {
this.intersectionObserver.disconnect();
};
});
fill = modifier((element) => {
this.resizeObserver = new ResizeObserver(() => {
if (isElementInViewport(element)) {
this.loadCollection();
}
});
this.resizeObserver.observe(element);
return () => {
this.resizeObserver.disconnect();
};
});
get itemComponent() {
return this.args.itemComponent ?? Item;
}
get emptyStateComponent() {
return EmptyState;
}
@action
loadCollection() {
discourseDebounce(this, this.debouncedLoadCollection, INPUT_DELAY);
}
async debouncedLoadCollection() {
await this.args.collection.load({ limit: 10 });
}
<template>
<div class="c-list">
<div {{this.fill}} ...attributes>
{{#each @collection.items as |item|}}
{{yield (hash Item=(component this.itemComponent item=item))}}
{{else}}
{{#if @collection.fetchedOnce}}
{{yield (hash EmptyState=this.emptyStateComponent)}}
{{/if}}
{{/each}}
</div>
<div {{this.loadMore}}>
<br />
</div>
<ConditionalLoadingSpinner @condition={{@collection.loading}} />
</div>
</template>
}

View File

@ -0,0 +1,7 @@
import Component from "@glimmer/component";
export default class Item extends Component {
<template>
{{yield @item}}
</template>
}

View File

@ -0,0 +1,73 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import CloseDrawerButton from "./close-drawer-button";
import CloseThreadButton from "./close-thread-button";
import CloseThreadsButton from "./close-threads-button";
import FullPageButton from "./full-page-button";
import NewChannelButton from "./new-channel-button";
import OpenDrawerButton from "./open-drawer-button";
import ThreadSettingsButton from "./thread-settings-button";
import ThreadTrackingDropdown from "./thread-tracking-dropdown";
import ThreadsListButton from "./threads-list-button";
import ToggleDrawerButton from "./toggle-drawer-button";
export default class ChatNavbarActions extends Component {
get openDrawerButtonComponent() {
return OpenDrawerButton;
}
get newChannelButtonComponent() {
return NewChannelButton;
}
get threadTrackingDropdownComponent() {
return ThreadTrackingDropdown;
}
get closeThreadButtonComponent() {
return CloseThreadButton;
}
get closeThreadsButtonComponent() {
return CloseThreadsButton;
}
get threadSettingsButtonComponent() {
return ThreadSettingsButton;
}
get threadsListButtonComponent() {
return ThreadsListButton;
}
get closeDrawerButtonComponent() {
return CloseDrawerButton;
}
get toggleDrawerButtonComponent() {
return ToggleDrawerButton;
}
get chatNavbarFullPageButtonComponent() {
return FullPageButton;
}
<template>
<nav class="c-navbar__actions">
{{yield
(hash
OpenDrawerButton=this.openDrawerButtonComponent
NewChannelButton=this.newChannelButtonComponent
ThreadTrackingDropdown=this.threadTrackingDropdownComponent
CloseThreadButton=this.closeThreadButtonComponent
CloseThreadsButton=this.closeThreadsButtonComponent
ThreadSettingsButton=this.threadSettingsButtonComponent
ThreadsListButton=this.threadsListButtonComponent
CloseDrawerButton=this.closeDrawerButtonComponent
ToggleDrawerButton=this.toggleDrawerButtonComponent
FullPageButton=this.chatNavbarFullPageButtonComponent
)
}}
</nav>
</template>
}

View File

@ -0,0 +1,43 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
export default class ChatNavbarBackButton extends Component {
get icon() {
return this.args.icon ?? "chevron-left";
}
get title() {
return this.args.title ?? I18n.t("chat.browse.back");
}
<template>
{{#if @routeModels}}
<LinkTo
@route={{@route}}
@models={{@routeModels}}
class="c-navbar__back-button no-text btn-flat btn"
title={{this.title}}
>
{{#if (has-block)}}
{{yield}}
{{else}}
{{icon this.icon}}
{{/if}}
</LinkTo>
{{else}}
<LinkTo
@route="chat"
class="c-navbar__back-button no-text btn-flat btn"
title={{this.title}}
>
{{#if (has-block)}}
{{yield}}
{{else}}
{{icon this.icon}}
{{/if}}
</LinkTo>
{{/if}}
</template>
}

View File

@ -0,0 +1,17 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
export default class ChatNavbarChannelTitle extends Component {
<template>
{{#if @channel}}
<LinkTo
@route="chat.channel.info.members"
@models={{@channel.routeModels}}
class="c-navbar__channel-title"
>
<ChannelTitle @channel={{@channel}} />
</LinkTo>
{{/if}}
</template>
}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
export default class ChatNavbarCloseDrawerButton extends Component {
@service chat;
@service chatStateManager;
@action
closeDrawer() {
this.chatStateManager.didCloseDrawer();
this.chat.activeChannel = null;
}
<template>
<DButton
@icon="times"
@action={{this.closeDrawer}}
@title="chat.close"
class="btn-flat no-text c-navbar__close-drawer-button"
/>
</template>
}

View File

@ -0,0 +1,22 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import icon from "discourse-common/helpers/d-icon";
import i18n from "discourse-common/helpers/i18n";
export default class ChatNavbarCloseThreadButton extends Component {
@service site;
<template>
{{#if this.site.desktopView}}
<LinkTo
class="c-navbar__close-thread-button btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{@thread.channel.routeModels}}
title={{i18n "chat.thread.close"}}
>
{{icon "times"}}
</LinkTo>
{{/if}}
</template>
}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
export default class ChatNavbarCloseThreadsButton extends Component {
@service site;
closeButtonTitle = I18n.t("chat.thread.close");
<template>
{{#if this.site.desktopView}}
<LinkTo
class="c-navbar__close-threads-button btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{@channel.routeModels}}
title={{this.closeButtonTitle}}
>
{{icon "times"}}
</LinkTo>
{{/if}}
</template>
}

View File

@ -0,0 +1,33 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { next } from "@ember/runloop";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import DiscourseURL from "discourse/lib/url";
export default class ChatNavbarFullPageButton extends Component {
@service chat;
@service chatStateManager;
@action
async openInFullPage() {
this.chatStateManager.storeAppURL();
this.chatStateManager.prefersFullPage();
this.chat.activeChannel = null;
await new Promise((resolve) => next(resolve));
DiscourseURL.routeTo(this.chatStateManager.lastKnownChatURL);
}
<template>
{{#if this.chatStateManager.isDrawerExpanded}}
<DButton
@icon="discourse-expand"
class="btn-flat no-text c-navbar__full-page-button"
@title="chat.open_full_page"
@action={{this.openInFullPage}}
/>
{{/if}}
</template>
}

View File

@ -0,0 +1,46 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import { on } from "@ember/modifier";
import concatClass from "discourse/helpers/concat-class";
import noop from "discourse/helpers/noop";
import Actions from "./actions";
import BackButton from "./back-button";
import ChannelTitle from "./channel-title";
import Title from "./title";
export default class ChatNavbar extends Component {
get buttonComponent() {
return BackButton;
}
get titleComponent() {
return Title;
}
get actionsComponent() {
return Actions;
}
get channelTitleComponent() {
return ChannelTitle;
}
<template>
{{! template-lint-disable no-invalid-interactive }}
<div
class={{concatClass "c-navbar-container" (if @onClick "-clickable")}}
{{on "click" (if @onClick @onClick (noop))}}
>
<nav class="c-navbar">
{{yield
(hash
BackButton=this.buttonComponent
ChannelTitle=this.channelTitleComponent
Title=this.titleComponent
Actions=this.actionsComponent
)
}}
</nav>
</div>
</template>
}

View File

@ -0,0 +1,32 @@
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 concatClass from "discourse/helpers/concat-class";
import CreateChannelModal from "discourse/plugins/chat/discourse/components/chat/modal/create-channel";
export default class ChatNavbarNewChannelButton extends Component {
@service chatStateManager;
@service currentUser;
@service modal;
@service site;
@action
createChannel() {
this.modal.show(CreateChannelModal);
}
<template>
{{#if this.currentUser.staff}}
<DButton
@action={{this.createChannel}}
@icon="plus"
@label={{if this.site.desktopView "chat.create_channel.title"}}
class={{concatClass
"c-navbar__new-channel-button"
(if this.site.mobileView "btn-flat")
}}
/>
{{/if}}
</template>
}

View File

@ -0,0 +1,30 @@
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 ChatNavbarOpenDrawerButton extends Component {
@service chatStateManager;
@service site;
@action
async openDrawer() {
this.chatStateManager.prefersDrawer();
DiscourseURL.routeTo(this.chatStateManager.lastKnownAppURL).then(() => {
DiscourseURL.routeTo(this.chatStateManager.lastKnownChatURL);
});
}
<template>
{{#if this.site.desktopView}}
<DButton
@icon="discourse-compress"
@title="chat.close_full_page"
class="c-navbar__open-drawer-button btn-flat"
@action={{this.openDrawer}}
/>
{{/if}}
</template>
}

View File

@ -0,0 +1,18 @@
import Component from "@glimmer/component";
import SubTitle from "./sub-title";
export default class ChatNavbarSubTitle extends Component {
get subTitleComponent() {
return SubTitle;
}
<template>
<div class="c-navbar__sub-title">
{{#if (has-block)}}
{{yield}}
{{else}}
{{@title}}
{{/if}}
</div>
</template>
}

View File

@ -0,0 +1,37 @@
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 ThreadSettingsModal from "discourse/plugins/chat/discourse/components/chat/modal/thread-settings";
export default class ChatNavbarThreadSettingsButton extends Component {
@service currentUser;
@service modal;
get canChangeThreadSettings() {
if (!this.args.thread) {
return false;
}
return (
this.currentUser.staff ||
this.currentUser.id === this.args.thread.originalMessage.user.id
);
}
@action
openThreadSettings() {
this.modal.show(ThreadSettingsModal, { model: this.args.thread });
}
<template>
{{#if this.canChangeThreadSettings}}
<DButton
@action={{this.openThreadSettings}}
@icon="cog"
@title="chat.thread.settings"
class="btn-flat c-navbar__thread-settings-button"
/>
{{/if}}
</template>
}

View File

@ -0,0 +1,66 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { NotificationLevels } from "discourse/lib/notification-levels";
import ThreadTrackingDropdown from "discourse/plugins/chat/discourse/components/chat-thread-tracking-dropdown";
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
export default class ChatNavbarThreadTrackingDropdown extends Component {
@service chatApi;
@tracked persistedNotificationLevel = true;
get threadNotificationLevel() {
return this.membership?.notificationLevel || NotificationLevels.REGULAR;
}
get membership() {
return this.args.thread.currentUserMembership;
}
@action
async updateThreadNotificationLevel(newNotificationLevel) {
this.persistedNotificationLevel = false;
let currentNotificationLevel;
if (this.membership) {
currentNotificationLevel = this.membership.notificationLevel;
this.membership.notificationLevel = newNotificationLevel;
} else {
this.args.thread.currentUserMembership = UserChatThreadMembership.create({
notification_level: newNotificationLevel,
last_read_message_id: null,
});
}
try {
const response =
await this.chatApi.updateCurrentUserThreadNotificationsSettings(
this.args.thread.channel.id,
this.args.thread.id,
{ notificationLevel: newNotificationLevel }
);
this.membership.last_read_message_id =
response.membership.last_read_message_id;
this.persistedNotificationLevel = true;
} catch (error) {
this.membership.notificationLevel = currentNotificationLevel;
popupAjaxError(error);
}
}
<template>
<ThreadTrackingDropdown
@value={{this.threadNotificationLevel}}
@onChange={{this.updateThreadNotificationLevel}}
@class={{concatClass
"c-navbar__thread-tracking-dropdown"
(if this.persistedNotificationLevel "-persisted")
}}
/>
</template>
}

View File

@ -0,0 +1,42 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
import ThreadHeaderUnreadIndicator from "discourse/plugins/chat/discourse/components/chat/thread/header-unread-indicator";
export default class ChatNavbarThreadsListButton extends Component {
@service router;
threadsListLabel = I18n.t("chat.threads.list");
get showThreadsListButton() {
return (
this.args.channel?.threadingEnabled &&
this.router.currentRoute.name !== "chat.channel.threads" &&
this.router.currentRoute.name !== "chat.channel.thread" &&
this.router.currentRoute.name !== "chat.channel.thread.index"
);
}
<template>
{{#if this.showThreadsListButton}}
<LinkTo
@route="chat.channel.threads"
@models={{@channel.routeModels}}
title={{this.threadsListLabel}}
class={{concatClass
"c-navbar__threads-list-button"
"btn"
"no-text"
"btn-flat"
(if @channel.threadsManager.unreadThreadCount "has-unreads")
}}
>
{{icon "discourse-threads"}}
<ThreadHeaderUnreadIndicator @channel={{@channel}} />
</LinkTo>
{{/if}}
</template>
}

View File

@ -0,0 +1,27 @@
import Component from "@glimmer/component";
import { hash } from "@ember/helper";
import icon from "discourse-common/helpers/d-icon";
import SubTitle from "./sub-title";
export default class ChatNavbarTitle extends Component {
get subTitleComponent() {
return SubTitle;
}
<template>
<div class="c-navbar__title">
{{#if (has-block)}}
{{#if @icon}}
{{icon @icon}}
{{/if}}
{{@title}}
{{yield (hash SubTitle=this.subTitleComponent)}}
{{else}}
{{#if @icon}}
{{icon @icon}}
{{/if}}
{{@title}}
{{/if}}
</div>
</template>
}

View File

@ -2,7 +2,8 @@ import Component from "@glimmer/component";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button"; import DButton from "discourse/components/d-button";
export default class ChatDrawerHeaderToggleExpandButton extends Component { export default class ChatNavbarToggleDrawerButton extends Component {
@service chat;
@service chatStateManager; @service chatStateManager;
<template> <template>
@ -12,13 +13,13 @@ export default class ChatDrawerHeaderToggleExpandButton extends Component {
"angle-double-down" "angle-double-down"
"angle-double-up" "angle-double-up"
}} }}
@action={{@toggleExpand}} @action={{this.chat.toggleDrawer}}
@title={{if @title={{if
this.chatStateManager.isDrawerExpanded this.chatStateManager.isDrawerExpanded
"chat.collapse" "chat.collapse"
"chat.expand" "chat.expand"
}} }}
class="btn-flat btn-link chat-drawer-header__expand-btn" class="btn-flat no-text c-navbar__toggle-drawer-button"
/> />
</template> </template>
} }

View File

@ -0,0 +1,131 @@
import { cached, tracked } from "@glimmer/tracking";
import Component from "@ember/component";
import { concat, hash } from "@ember/helper";
import { action, computed } from "@ember/object";
import didInsert from "@ember/render-modifiers/modifiers/did-insert";
import { LinkTo } from "@ember/routing";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import { INPUT_DELAY } from "discourse-common/config/environment";
import i18n from "discourse-common/helpers/i18n";
import discourseDebounce from "discourse-common/lib/debounce";
import List from "discourse/plugins/chat/discourse/components/chat/list";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import ChatChannelCard from "discourse/plugins/chat/discourse/components/chat-channel-card";
import DcFilterInput from "discourse/plugins/chat/discourse/components/dc-filter-input";
const TABS = ["all", "open", "closed", "archived"];
export default class ChatRoutesBrowse extends Component {
@service chatApi;
@service modal;
@tracked filter = "";
@cached
get channelsCollection() {
return this.chatApi.channels({
filter: this.filter,
status: this.attrs.status,
});
}
@computed("siteSettings.chat_allow_archiving_channels")
get tabs() {
if (this.siteSettings.chat_allow_archiving_channels) {
return TABS;
} else {
return [...TABS].removeObject("archived");
}
}
@action
showChatNewMessageModal() {
this.modal.show(ChatModalNewMessage);
}
@action
setFilter(event) {
this.filter = event.target.value;
discourseDebounce(this.debouncedLoad, INPUT_DELAY);
}
@action
debouncedLoad() {
this.channelsCollection.load({ limit: 10 });
}
@action
focusFilterInput(input) {
schedule("afterRender", () => input?.focus());
}
<template>
<div class="c-routes-browse">
<Navbar as |navbar|>
<navbar.BackButton />
<navbar.Title @title={{i18n "chat.browse.title"}} />
<navbar.Actions as |action|>
<action.NewChannelButton />
</navbar.Actions>
</Navbar>
<div class="chat-browse-view">
<div class="chat-browse-view__actions">
<nav>
<ul class="nav-pills chat-browse-view__filters">
{{#each this.tabs as |tab|}}
<li class={{concat "chat-browse-view__filter -" tab}}>
<LinkTo
@route={{concat "chat.browse." tab}}
class={{concat "chat-browse-view__filter-link -" tab}}
>
{{i18n (concat "chat.browse.filter_" tab)}}
</LinkTo>
</li>
{{/each}}
</ul>
</nav>
<DcFilterInput
{{didInsert this.focusFilterInput}}
@filterAction={{this.setFilter}}
@icons={{hash right="search"}}
@containerClass="filter-input"
placeholder={{i18n "chat.browse.filter_input_placeholder"}}
/>
</div>
<div class="chat-browse-view__content_wrapper">
<div class="chat-browse-view__content">
<List
@collection={{this.channelsCollection}}
class="chat-browse-view__cards"
as |list|
>
<list.Item as |channel|>
<ChatChannelCard @channel={{channel}} />
</list.Item>
<list.EmptyState>
<span class="empty-state-title">
{{i18n "chat.empty_state.title"}}
</span>
<div class="empty-state-body">
<p>{{i18n "chat.empty_state.direct_message"}}</p>
<DButton
@action={{this.showChatNewMessageModal}}
@label="chat.empty_state.direct_message_cta"
/>
</div>
</list.EmptyState>
</List>
</div>
</div>
</div>
</div>
</template>
}

View File

@ -13,11 +13,11 @@ import icon from "discourse-common/helpers/d-icon";
import discourseDebounce from "discourse-common/lib/debounce"; import discourseDebounce from "discourse-common/lib/debounce";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import MessageCreator from "discourse/plugins/chat/discourse/components/chat/message-creator"; import MessageCreator from "discourse/plugins/chat/discourse/components/chat/message-creator";
import { MODES } from "discourse/plugins/chat/discourse/components/chat/message-creator/constants";
import ChatUserInfo from "discourse/plugins/chat/discourse/components/chat-user-info"; import ChatUserInfo from "discourse/plugins/chat/discourse/components/chat-user-info";
import DcFilterInput from "discourse/plugins/chat/discourse/components/dc-filter-input"; import DcFilterInput from "discourse/plugins/chat/discourse/components/dc-filter-input";
import { MODES } from "./chat/message-creator/constants";
export default class ChatChannelMembers extends Component { export default class ChatRouteChannelInfoMembers extends Component {
@service appEvents; @service appEvents;
@service chatApi; @service chatApi;
@service modal; @service modal;

View File

@ -28,7 +28,7 @@ const NOTIFICATION_LEVELS = [
{ name: I18n.t("chat.notification_levels.always"), value: "always" }, { name: I18n.t("chat.notification_levels.always"), value: "always" },
]; ];
export default class ChatAboutScreen extends Component { export default class ChatRouteChannelInfoSettings extends Component {
@service chatApi; @service chatApi;
@service chatGuardian; @service chatGuardian;
@service chatChannelsManager; @service chatChannelsManager;

View File

@ -0,0 +1,91 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service";
import I18n from "discourse-i18n";
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import ChatChannelStatus from "discourse/plugins/chat/discourse/components/chat-channel-status";
export default class ChatRoutesChannelInfo extends Component {
@service chatChannelInfoRouteOriginManager;
@service site;
@service modal;
@service chatGuardian;
membersLabel = I18n.t("chat.channel_info.tabs.members");
settingsLabel = I18n.t("chat.channel_info.tabs.settings");
backToChannelLabel = I18n.t("chat.channel_info.back_to_all_channel");
backToAllChannelsLabel = I18n.t("chat.channel_info.back_to_channel");
get showTabs() {
return this.site.desktopView && this.args.channel.isOpen;
}
get canEditChannel() {
return (
this.chatGuardian.canEditChatChannel() &&
(this.args.channel.isCategoryChannel ||
(this.args.channel.isDirectMessageChannel &&
this.args.channel.chatable.group))
);
}
@action
editChannelTitle() {
return this.modal.show(ChatModalEditChannelName, {
model: this.args.channel,
});
}
<template>
<div class="c-routes-channel-info">
<Navbar as |navbar|>
{{#if this.chatChannelInfoRouteOriginManager.isBrowse}}
<navbar.BackButton
@route="chat.browse"
@title={{this.backToAllChannelsLabel}}
/>
{{else}}
<navbar.BackButton
@route="chat.channel"
@routeModels={{@channel.routeModels}}
@title={{this.backToChannelLabel}}
/>
{{/if}}
<navbar.ChannelTitle @channel={{@channel}} />
</Navbar>
<ChatChannelStatus @channel={{@channel}} />
<div class="chat-channel-info">
{{#if this.showTabs}}
<nav class="chat-channel-info__nav">
<ul class="nav nav-pills">
<li>
<LinkTo
@route="chat.channel.info.settings"
@model={{@channel}}
@replace={{true}}
>
{{this.settingsLabel}}
</LinkTo>
</li>
<li>
<LinkTo
@route="chat.channel.info.members"
@model={{@channel}}
@replace={{true}}
>
{{this.membersLabel}}
</LinkTo>
</li>
</ul>
</nav>
{{/if}}
{{outlet}}
</div>
</div>
</template>
}

View File

@ -0,0 +1,20 @@
import Component from "@glimmer/component";
import { array } from "@ember/helper";
import ThreadHeader from "discourse/plugins/chat/discourse/components/chat/thread/header";
import Thread from "discourse/plugins/chat/discourse/components/chat-thread";
export default class ChatRoutesChannelThread extends Component {
<template>
<div class="c-routes-channel-thread">
{{#each (array @thread) as |thread|}}
<ThreadHeader @thread={{thread}} />
<Thread
@thread={{thread}}
@targetMessageId={{@targetMessageId}}
@includeHeader={{true}}
/>
{{/each}}
</div>
</template>
}

View File

@ -0,0 +1,12 @@
import Component from "@glimmer/component";
import ChatThreadListHeader from "discourse/plugins/chat/discourse/components/chat/thread-list/header";
import ChatThreadList from "discourse/plugins/chat/discourse/components/chat-thread-list";
export default class ChatRoutesChannelThreads extends Component {
<template>
<div class="c-routes-channel-threads">
<ChatThreadListHeader @channel={{@channel}} />
<ChatThreadList @channel={{@channel}} @includeHeader={{true}} />
</div>
</template>
}

View File

@ -0,0 +1,33 @@
import Component from "@glimmer/component";
import { inject as service } from "@ember/service";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import SidePanel from "discourse/plugins/chat/discourse/components/chat-side-panel";
import FullPageChat from "discourse/plugins/chat/discourse/components/full-page-chat";
export default class ChatRoutesChannel extends Component {
@service site;
<template>
<div class="c-routes-channel">
<Navbar as |navbar|>
{{#if this.site.mobileView}}
<navbar.BackButton />
{{/if}}
<navbar.ChannelTitle @channel={{@channel}} />
<navbar.Actions as |action|>
<action.OpenDrawerButton />
<action.ThreadsListButton @channel={{@channel}} />
</navbar.Actions>
</Navbar>
<FullPageChat
@channel={{@channel}}
@targetMessageId={{@targetMessageId}}
/>
</div>
<SidePanel>
{{outlet}}
</SidePanel>
</template>
}

View File

@ -0,0 +1,24 @@
import Component from "@glimmer/component";
import i18n from "discourse-common/helpers/i18n";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import UserThreads from "discourse/plugins/chat/discourse/components/user-threads";
export default class ChatRoutesThreads extends Component {
<template>
<div class="c-routes-threads">
<Navbar as |navbar|>
<navbar.BackButton />
<navbar.Title
@title={{i18n "chat.my_threads.title"}}
@icon="discourse-threads"
/>
<navbar.Actions as |action|>
<action.OpenDrawerButton />
</navbar.Actions>
</Navbar>
<UserThreads />
</div>
</template>
}

View File

@ -1,70 +1,37 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import replaceEmoji from "discourse/helpers/replace-emoji"; import replaceEmoji from "discourse/helpers/replace-emoji";
import icon from "discourse-common/helpers/d-icon"; import i18n from "discourse-common/helpers/i18n";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
export default class ChatThreadListHeader extends Component { export default class ChatThreadListHeader extends Component {
@service router; @service router;
@service site; @service site;
threadListTitle = I18n.t("chat.threads.list"); threadListTitle = I18n.t("chat.threads.list");
closeButtonTitle = I18n.t("chat.thread.close");
showCloseButton = !this.site.mobileView;
get showBackButton() {
return this.args.channel && this.site.mobileView;
}
get backButton() {
return {
route: "chat.channel.index",
models: this.args.channel.routeModels,
title: I18n.t("chat.return_to_channel"),
};
}
<template> <template>
<div class="chat-thread-list-header"> <Navbar as |navbar|>
<div class="chat-thread-header__left-buttons"> <navbar.BackButton
{{#if this.showBackButton}}
<LinkTo
class="chat-thread__back-to-previous-route btn-flat btn btn-icon no-text"
@route={{this.backButton.route}}
@models={{this.backButton.models}}
title={{this.backButton.title}}
>
{{icon "chevron-left"}}
</LinkTo>
{{/if}}
</div>
<div class="chat-thread-list-header__label">
<span>
{{icon "discourse-threads"}}
{{replaceEmoji this.threadListTitle}}
</span>
{{#if this.site.mobileView}}
<div class="chat-thread-list-header__label-channel">
{{replaceEmoji @channel.title}}
</div>
{{/if}}
</div>
{{#if this.showCloseButton}}
<div class="chat-thread-header__buttons">
<LinkTo
class="chat-thread__close btn-flat btn btn-icon no-text"
@route="chat.channel" @route="chat.channel"
@models={{@channel.routeModels}} @routeModels={{@channel.routeModels}}
title={{this.closeButtonTitle}} @title={{i18n "chat.return_to_channel"}}
/>
<navbar.Title
@title={{replaceEmoji this.threadListTitle}}
@icon="discourse-threads"
as |title|
> >
{{icon "times"}} {{#if this.site.mobileView}}
</LinkTo> <title.SubTitle @title={{replaceEmoji @channel.title}} />
</div>
{{/if}} {{/if}}
</div> </navbar.Title>
<navbar.Actions as |action|>
<action.CloseThreadsButton @channel={{@channel}} />
</navbar.Actions>
</Navbar>
</template> </template>
} }

View File

@ -50,7 +50,6 @@ export default class ChatThreadListItem extends Component {
</div> </div>
<div class="chat-thread-list-item__metadata"> <div class="chat-thread-list-item__metadata">
<div class="chat-thread-list-item__members"> <div class="chat-thread-list-item__members">
<ChatUserAvatar <ChatUserAvatar
@user={{@thread.originalMessage.user}} @user={{@thread.originalMessage.user}}
@ -72,7 +71,6 @@ export default class ChatThreadListItem extends Component {
}} }}
{{/if}} {{/if}}
</div> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,32 +1,15 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { action } from "@ember/object";
import { LinkTo } from "@ember/routing";
import { inject as service } from "@ember/service"; import { inject as service } from "@ember/service";
import DButton from "discourse/components/d-button";
import concatClass from "discourse/helpers/concat-class";
import replaceEmoji from "discourse/helpers/replace-emoji"; import replaceEmoji from "discourse/helpers/replace-emoji";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { NotificationLevels } from "discourse/lib/notification-levels";
import icon from "discourse-common/helpers/d-icon"; import icon from "discourse-common/helpers/d-icon";
import I18n from "discourse-i18n"; import I18n from "discourse-i18n";
import ChatModalThreadSettings from "discourse/plugins/chat/discourse/components/chat/modal/thread-settings"; import Navbar from "discourse/plugins/chat/discourse/components/chat/navbar";
import ChatThreadHeaderUnreadIndicator from "discourse/plugins/chat/discourse/components/chat/thread/header-unread-indicator"; import ChatThreadHeaderUnreadIndicator from "discourse/plugins/chat/discourse/components/chat/thread/header-unread-indicator";
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
import ThreadNotificationsButton from "discourse/plugins/chat/select-kit/addons/components/thread-notifications-button";
export default class ChatThreadHeader extends Component { export default class ChatThreadHeader extends Component {
@service currentUser; @service currentUser;
@service chatApi;
@service router;
@service chatStateManager;
@service chatHistory; @service chatHistory;
@service site; @service site;
@service modal;
@tracked persistedNotificationLevel = true;
closeThreadTitle = I18n.t("chat.thread.close");
get backLink() { get backLink() {
const prevPage = this.chatHistory.previousRoute?.name; const prevPage = this.chatHistory.previousRoute?.name;
@ -35,15 +18,15 @@ export default class ChatThreadHeader extends Component {
if (prevPage === "chat.channel.threads") { if (prevPage === "chat.channel.threads") {
route = "chat.channel.threads"; route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list"); title = I18n.t("chat.return_to_threads_list");
models = this.args.channel.routeModels; models = this.channel?.routeModels;
} else if (prevPage === "chat.channel.index" && !this.site.mobileView) { } else if (prevPage === "chat.channel.index" && !this.site.mobileView) {
route = "chat.channel.threads"; route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list"); title = I18n.t("chat.return_to_threads_list");
models = this.args.channel.routeModels; models = this.channel?.routeModels;
} else if (!this.currentUser.isInDoNotDisturb() && this.unreadCount > 0) { } else if (!this.currentUser.isInDoNotDisturb() && this.unreadCount > 0) {
route = "chat.channel.threads"; route = "chat.channel.threads";
title = I18n.t("chat.return_to_threads_list"); title = I18n.t("chat.return_to_threads_list");
models = this.args.channel.routeModels; models = this.channel?.routeModels;
} else if (prevPage === "chat.threads") { } else if (prevPage === "chat.threads") {
route = "chat.threads"; route = "chat.threads";
title = I18n.t("chat.my_threads.title"); title = I18n.t("chat.my_threads.title");
@ -51,29 +34,14 @@ export default class ChatThreadHeader extends Component {
} else { } else {
route = "chat.channel.index"; route = "chat.channel.index";
title = I18n.t("chat.return_to_channel"); title = I18n.t("chat.return_to_channel");
models = this.args.channel.routeModels; models = this.channel?.routeModels;
} }
return { route, models, title }; return { route, models, title };
} }
get canChangeThreadSettings() { get channel() {
if (!this.args.thread) { return this.args.thread?.channel;
return false;
}
return (
this.currentUser.staff ||
this.currentUser.id === this.args.thread.originalMessage.user.id
);
}
get threadNotificationLevel() {
return this.membership?.notificationLevel || NotificationLevels.REGULAR;
}
get membership() {
return this.args.thread.currentUserMembership;
} }
get headerTitle() { get headerTitle() {
@ -81,97 +49,28 @@ export default class ChatThreadHeader extends Component {
} }
get unreadCount() { get unreadCount() {
return this.args.channel.threadsManager.unreadThreadCount; return this.channel?.threadsManager?.unreadThreadCount;
}
@action
openThreadSettings() {
this.modal.show(ChatModalThreadSettings, { model: this.args.thread });
}
@action
updateThreadNotificationLevel(newNotificationLevel) {
this.persistedNotificationLevel = false;
let currentNotificationLevel;
if (this.membership) {
currentNotificationLevel = this.membership.notificationLevel;
this.membership.notificationLevel = newNotificationLevel;
} else {
this.args.thread.currentUserMembership = UserChatThreadMembership.create({
notification_level: newNotificationLevel,
last_read_message_id: null,
});
}
return this.chatApi
.updateCurrentUserThreadNotificationsSettings(
this.args.thread.channel.id,
this.args.thread.id,
{ notificationLevel: newNotificationLevel }
)
.then((response) => {
this.membership.last_read_message_id =
response.membership.last_read_message_id;
this.persistedNotificationLevel = true;
})
.catch((err) => {
this.membership.notificationLevel = currentNotificationLevel;
popupAjaxError(err);
});
} }
<template> <template>
<div class="chat-thread-header"> <Navbar as |navbar|>
<div class="chat-thread-header__left-buttons">
{{#if @thread}} {{#if @thread}}
<LinkTo <navbar.BackButton
class="chat-thread__back-to-previous-route btn-flat btn btn-icon no-text"
@route={{this.backLink.route}} @route={{this.backLink.route}}
@models={{this.backLink.models}} @routeModels={{this.backLink.models}}
title={{this.backLink.title}} @title={{this.backLink.title}}
> >
<ChatThreadHeaderUnreadIndicator @channel={{@thread.channel}} /> <ChatThreadHeaderUnreadIndicator @channel={{this.channel}} />
{{icon "chevron-left"}} {{icon "chevron-left"}}
</LinkTo> </navbar.BackButton>
{{/if}} {{/if}}
</div>
<span class="chat-thread-header__label overflow-ellipsis"> <navbar.Title @title={{replaceEmoji this.headerTitle}} />
{{replaceEmoji this.headerTitle}} <navbar.Actions as |action|>
</span> <action.ThreadTrackingDropdown @thread={{@thread}} />
<action.ThreadSettingsButton @thread={{@thread}} />
<div <action.CloseThreadButton @thread={{@thread}} />
class={{concatClass </navbar.Actions>
"chat-thread-header__buttons" </Navbar>
(if this.persistedNotificationLevel "-persisted")
}}
>
<ThreadNotificationsButton
@value={{this.threadNotificationLevel}}
@onChange={{this.updateThreadNotificationLevel}}
/>
{{#if this.canChangeThreadSettings}}
<DButton
@action={{this.openThreadSettings}}
@icon="cog"
@title="chat.thread.settings"
class="btn-flat chat-thread-header__settings"
/>
{{/if}}
{{#unless this.site.mobileView}}
<LinkTo
class="chat-thread__close btn-flat btn btn-icon no-text"
@route="chat.channel"
@models={{@thread.channel.routeModels}}
title={{this.closeThreadTitle}}
>
{{icon "times"}}
</LinkTo>
{{/unless}}
</div>
</div>
</template> </template>
} }

View File

@ -1,28 +0,0 @@
import Component from "@glimmer/component";
import { LinkTo } from "@ember/routing";
import concatClass from "discourse/helpers/concat-class";
import icon from "discourse-common/helpers/d-icon";
import I18n from "I18n";
import ThreadHeaderUnreadIndicator from "discourse/plugins/chat/discourse/components/chat/thread/header-unread-indicator";
export default class ThreadsListButton extends Component {
threadsListLabel = I18n.t("chat.threads.list");
<template>
<LinkTo
@route="chat.channel.threads"
@models={{@channel.routeModels}}
title={{this.threadsListLabel}}
class={{concatClass
"chat-threads-list-button"
"btn"
"btn-flat"
(if @channel.threadsManager.unreadThreadCount "has-unreads")
}}
>
{{icon "discourse-threads"}}
<ThreadHeaderUnreadIndicator @channel={{@channel}} />
</LinkTo>
</template>
}

View File

@ -1,20 +0,0 @@
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>
}

View File

@ -1,44 +0,0 @@
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>
}

View File

@ -1,14 +1,9 @@
import Component from "@glimmer/component"; import Component from "@glimmer/component";
import { cached } from "@glimmer/tracking"; import { cached } from "@glimmer/tracking";
import { action } from "@ember/object";
import { inject as service } from "@ember/service"; 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 { bind } from "discourse-common/utils/decorators";
import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title"; import ChannelTitle from "discourse/plugins/chat/discourse/components/channel-title";
import List from "discourse/plugins/chat/discourse/components/chat/list";
import ThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator"; import ThreadIndicator from "discourse/plugins/chat/discourse/components/chat-message-thread-indicator";
import ThreadTitle from "discourse/plugins/chat/discourse/components/thread-title"; import ThreadTitle from "discourse/plugins/chat/discourse/components/thread-title";
import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel"; import ChatChannel from "discourse/plugins/chat/discourse/models/chat-channel";
@ -17,45 +12,12 @@ import ChatThread from "discourse/plugins/chat/discourse/models/chat-thread";
export default class UserThreads extends Component { export default class UserThreads extends Component {
@service chat; @service chat;
@service chatApi; @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 @cached
get threadsCollection() { get threadsCollection() {
return this.chatApi.userThreads(this.handleLoadedThreads); return this.chatApi.userThreads(this.handleLoadedThreads);
} }
@action
loadThreads() {
discourseDebounce(this, this.debouncedLoadThreads, INPUT_DELAY);
}
async debouncedLoadThreads() {
await this.threadsCollection.load({ limit: 10 });
}
@bind @bind
handleLoadedThreads(result) { handleLoadedThreads(result) {
return result.threads.map((threadObject) => { return result.threads.map((threadObject) => {
@ -71,12 +33,15 @@ export default class UserThreads extends Component {
} }
<template> <template>
<div class="c-user-threads" {{this.fill}}> <List
{{#each this.threadsCollection.items as |thread|}} @collection={{this.threadsCollection}}
class="c-user-threads"
as |list|
>
<list.Item as |thread|>
<div class="c-user-thread" data-id={{thread.id}}> <div class="c-user-thread" data-id={{thread.id}}>
<ThreadTitle @thread={{thread}} /> <ThreadTitle @thread={{thread}} />
<ChannelTitle @channel={{thread.channel}} /> <ChannelTitle @channel={{thread.channel}} />
<ThreadIndicator <ThreadIndicator
@message={{thread.originalMessage}} @message={{thread.originalMessage}}
@interactiveUser={{false}} @interactiveUser={{false}}
@ -84,16 +49,7 @@ export default class UserThreads extends Component {
tabindex="-1" tabindex="-1"
/> />
</div> </div>
{{/each}} </list.Item>
</List>
<div {{this.loadMore}}>
<br />
</div>
<ConditionalLoadingSpinner
@condition={{this.threadsCollection.loading}}
/>
</div>
</template> </template>
} }

View File

@ -94,12 +94,16 @@ export default class ChatApi extends Service {
* *
* this.chatApi.channels.then(channels => { ... }) * this.chatApi.channels.then(channels => { ... })
*/ */
channels() { channels(params = {}) {
return new Collection(`${this.#basePath}/channels`, (response) => { return new Collection(
`${this.#basePath}/channels`,
(response) => {
return response.channels.map((channel) => return response.channels.map((channel) =>
this.chatChannelsManager.store(channel) this.chatChannelsManager.store(channel)
); );
}); },
params
);
} }
/** /**

View File

@ -1,15 +1,15 @@
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import Service, { inject as service } from "@ember/service"; import Service, { inject as service } from "@ember/service";
import ChatDrawerChannel from "discourse/plugins/chat/discourse/components/chat-drawer/channel"; import ChatDrawerRoutesChannel from "discourse/plugins/chat/discourse/components/chat/drawer-routes/channel";
import ChatDrawerChannelThreads from "discourse/plugins/chat/discourse/components/chat-drawer/channel-threads"; import ChatDrawerRoutesChannelThread from "discourse/plugins/chat/discourse/components/chat/drawer-routes/channel-thread";
import ChatDrawerIndex from "discourse/plugins/chat/discourse/components/chat-drawer/index"; import ChatDrawerRoutesChannelThreads from "discourse/plugins/chat/discourse/components/chat/drawer-routes/channel-threads";
import ChatDrawerThread from "discourse/plugins/chat/discourse/components/chat-drawer/thread"; import ChatDrawerRoutesChannels from "discourse/plugins/chat/discourse/components/chat/drawer-routes/channels";
import ChatDrawerThreads from "discourse/plugins/chat/discourse/components/chat-drawer/threads"; import ChatDrawerRoutesThreads from "discourse/plugins/chat/discourse/components/chat/drawer-routes/threads";
const ROUTES = { const ROUTES = {
"chat.channel": { name: ChatDrawerChannel }, "chat.channel": { name: ChatDrawerRoutesChannel },
"chat.channel.thread": { "chat.channel.thread": {
name: ChatDrawerThread, name: ChatDrawerRoutesChannelThread,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.parent.params.channelId, channelId: route.parent.params.channelId,
@ -18,7 +18,7 @@ const ROUTES = {
}, },
}, },
"chat.channel.thread.index": { "chat.channel.thread.index": {
name: ChatDrawerThread, name: ChatDrawerRoutesChannelThread,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.parent.params.channelId, channelId: route.parent.params.channelId,
@ -27,7 +27,7 @@ const ROUTES = {
}, },
}, },
"chat.channel.thread.near-message": { "chat.channel.thread.near-message": {
name: ChatDrawerThread, name: ChatDrawerRoutesChannelThread,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.parent.parent.params.channelId, channelId: route.parent.parent.params.channelId,
@ -37,7 +37,7 @@ const ROUTES = {
}, },
}, },
"chat.channel.threads": { "chat.channel.threads": {
name: ChatDrawerChannelThreads, name: ChatDrawerRoutesChannelThreads,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.parent.params.channelId, channelId: route.parent.params.channelId,
@ -45,11 +45,11 @@ const ROUTES = {
}, },
}, },
"chat.threads": { "chat.threads": {
name: ChatDrawerThreads, name: ChatDrawerRoutesThreads,
}, },
chat: { name: ChatDrawerIndex }, chat: { name: ChatDrawerRoutesChannels },
"chat.channel.near-message": { "chat.channel.near-message": {
name: ChatDrawerChannel, name: ChatDrawerRoutesChannel,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.parent.params.channelId, channelId: route.parent.params.channelId,
@ -58,7 +58,7 @@ const ROUTES = {
}, },
}, },
"chat.channel-legacy": { "chat.channel-legacy": {
name: ChatDrawerChannel, name: ChatDrawerRoutesChannel,
extractParams: (route) => { extractParams: (route) => {
return { return {
channelId: route.params.channelId, channelId: route.params.channelId,
@ -83,7 +83,7 @@ export default class ChatDrawerRouter extends Service {
this.drawerRoute = ROUTES[route.name]; this.drawerRoute = ROUTES[route.name];
this.params = this.drawerRoute?.extractParams?.(route) || route.params; this.params = this.drawerRoute?.extractParams?.(route) || route.params;
this.component = this.drawerRoute?.name || ChatDrawerIndex; this.component = this.drawerRoute?.name || ChatDrawerRoutesChannels;
this.drawerRoute.activate?.(route); this.drawerRoute.activate?.(route);
} }

View File

@ -1,5 +1,5 @@
import { tracked } from "@glimmer/tracking"; import { tracked } from "@glimmer/tracking";
import { computed } from "@ember/object"; import { action, computed } from "@ember/object";
import { and } from "@ember/object/computed"; import { and } from "@ember/object/computed";
import { cancel, next } from "@ember/runloop"; import { cancel, next } from "@ember/runloop";
import Service, { inject as service } from "@ember/service"; import Service, { inject as service } from "@ember/service";
@ -414,4 +414,13 @@ export default class Chat extends Service {
"Use the new chat API `api.registerChatComposerButton` instead of `chat.addToolbarButton`" "Use the new chat API `api.registerChatComposerButton` instead of `chat.addToolbarButton`"
); );
} }
@action
toggleDrawer() {
this.chatStateManager.didToggleDrawer();
this.appEvents.trigger(
"chat:toggle-expand",
this.chatStateManager.isDrawerExpanded
);
}
} }

View File

@ -1 +1 @@
<ChatBrowseView @status="all" /> <Chat::Routes::Browse @status="all" />

View File

@ -1 +1 @@
<ChatBrowseView @status="archived" /> <Chat::Routes::Browse @status="archived" />

View File

@ -1 +1 @@
<ChatBrowseView @status="closed" /> <Chat::Routes::Browse @status="closed" />

View File

@ -1 +1 @@
<ChatBrowseView @status="open" /> <Chat::Routes::Browse @status="open" />

View File

@ -1 +1 @@
<ChatChannelMembers @channel={{this.model}} /> <Chat::Routes::ChannelInfoMembers @channel={{this.model}} />

View File

@ -1 +1 @@
<ChatChannelSettings @channel={{this.model}} /> <Chat::Routes::ChannelInfoSettings @channel={{this.model}} />

View File

@ -1 +1 @@
<ChatChannelInfo @channel={{this.model}} /> <Chat::Routes::ChannelInfo @channel={{this.model}} />

View File

@ -1,7 +1,4 @@
{{#each (array this.model) as |thread|}} <Chat::Routes::ChannelThread
<ChatThread @thread={{this.model}}
@thread={{thread}}
@targetMessageId={{this.targetMessageId}} @targetMessageId={{this.targetMessageId}}
@includeHeader={{true}}
/> />
{{/each}}

View File

@ -1 +1 @@
<ChatThreadList @channel={{this.model}} @includeHeader={{true}} /> <Chat::Routes::ChannelThreads @channel={{this.model}} />

View File

@ -1,8 +1,4 @@
<FullPageChat <Chat::Routes::Channel
@channel={{this.model}} @channel={{this.model}}
@targetMessageId={{this.targetMessageId}} @targetMessageId={{this.targetMessageId}}
/> />
<ChatSidePanel>
{{outlet}}
</ChatSidePanel>

View File

@ -1 +0,0 @@
<ChatDraftChannelScreen />

View File

@ -1 +1 @@
<Chat::Threads /> <Chat::Routes::Threads />

View File

@ -3,7 +3,7 @@
--full-page-border-radius: 12px; --full-page-border-radius: 12px;
--full-page-sidebar-width: 275px; --full-page-sidebar-width: 275px;
--channel-list-avatar-size: 30px; --channel-list-avatar-size: 30px;
--chat-header-offset: 50px; --chat-header-offset: 46px;
} }
// Very specific hack to ensure the contextual menu (copy/paste/...) is // Very specific hack to ensure the contextual menu (copy/paste/...) is
@ -198,40 +198,9 @@ body.has-full-page-chat {
grid-template-columns: var(--full-page-sidebar-width) 1fr; grid-template-columns: var(--full-page-sidebar-width) 1fr;
background: var(--d-content-background); background: var(--d-content-background);
.chat-full-page-header { .c-navbar-container {
border-bottom: 1px solid var(--primary-low); position: sticky;
background: var(--secondary); top: var(--header-offset);
z-index: 3;
display: flex;
align-items: center;
&__back-btn {
width: 40px;
min-width: 40px;
display: flex;
align-items: center;
justify-content: center;
}
.chat-channel-title {
.category-chat-name,
.chat-name,
.dm-usernames {
color: var(--primary);
display: inline;
text-overflow: ellipsis;
white-space: nowrap;
}
}
.-not-following {
.chat-channel-title {
max-width: calc(100% - 50px);
}
.join-channel-btn {
margin-left: auto;
}
}
} }
.chat-messages-scroll { .chat-messages-scroll {
@ -240,67 +209,6 @@ body.has-full-page-chat {
} }
} }
.chat-full-page-header__left-actions {
display: flex;
align-items: stretch;
}
.chat-full-page-header__title {
display: flex;
align-items: stretch;
}
.chat-full-page-header__right-actions {
align-items: stretch;
display: flex;
flex-grow: 1;
gap: 0.5rem;
font-size: var(--font-up-1);
justify-content: flex-end;
}
.chat-full-page-header {
box-sizing: border-box;
.chat-channel-header-details {
display: flex;
align-items: stretch;
flex: 1;
max-width: 100%;
.chat-channel-archive-status {
text-align: right;
padding-right: 1em;
}
}
.chat-channel-title {
margin: 0;
max-width: 100%;
.d-icon:not(.d-icon-lock) {
height: 1.25em;
width: 1.25em;
}
.category-chat-name,
.dm-username {
font-weight: 700;
font-size: var(--font-up-1);
line-height: var(--font-up-1);
}
.dm-usernames {
overflow: hidden;
text-overflow: ellipsis;
}
}
.chat-channel-retry-archive {
display: flex;
margin-top: 1em;
}
}
.user-preferences .chat-setting .controls { .user-preferences .chat-setting .controls {
margin-bottom: 0; margin-bottom: 0;
} }

View File

@ -7,10 +7,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-start; justify-content: flex-start;
.new-channel-btn {
margin-left: auto;
}
} }
&__title { &__title {

View File

@ -1,6 +1,5 @@
.chat-channel-info { .chat-channel-info {
display: flex; display: flex;
height: 100%;
padding: 1rem; padding: 1rem;
flex-direction: column; flex-direction: column;

View File

@ -1,10 +1,3 @@
//appears in: header of chat pane, channel info, preview card
.chat-channel-title-wrapper {
display: flex;
align-items: center;
overflow: hidden;
}
.chat-channel-title { .chat-channel-title {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@ -7,6 +7,7 @@ body.composer-open .chat-drawer-outlet-container {
top: -5px; top: -5px;
width: 15px; width: 15px;
height: 15px; height: 15px;
z-index: z("composer", "content");
} }
html:not(.rtl) { html:not(.rtl) {
@ -30,36 +31,25 @@ html.rtl {
right: var(--composer-right, 20px); right: var(--composer-right, 20px);
left: 0; left: 0;
max-height: calc(100% - var(--header-offset) - 15px); max-height: calc(100% - var(--header-offset) - 15px);
.rtl & {
left: var(--composer-right, 20px);
right: 0;
}
margin: 0; margin: 0;
padding: 0; padding: 0;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
pointer-events: none !important; pointer-events: none !important;
bottom: 0; bottom: 0;
> * {
pointer-events: auto;
}
.no-channel-title {
font-weight: bold;
margin-left: 0.5rem;
}
&.composer-draft-collapsed {
bottom: 40px;
}
box-sizing: border-box; box-sizing: border-box;
padding-bottom: var(--composer-height, 0); padding-bottom: var(--composer-height, 0);
transition: all 100ms ease-in; transition: all 100ms ease-in;
transition-property: bottom, padding-bottom; transition-property: bottom, padding-bottom;
.rtl & {
left: var(--composer-right, 20px);
right: 0;
}
> * {
pointer-events: auto;
}
} }
.chat-drawer { .chat-drawer {
@ -101,170 +91,11 @@ html.rtl {
} }
} }
.chat-drawer-header__left-actions {
display: flex;
height: 2rem;
}
.chat-drawer-header__right-actions {
display: flex;
height: 2rem;
margin-left: auto;
}
.chat-drawer-header__top-line {
display: flex;
align-items: center;
}
.chat-drawer-header__bottom-line {
height: 1.5rem;
display: flex;
align-items: start;
}
.chat-drawer-header__title {
@include ellipsis;
display: flex;
width: auto;
font-weight: 700;
padding: 0 0.5rem 0 0;
cursor: pointer;
height: 2rem;
align-items: center;
.chat-drawer-header__top-line {
padding: 0.25rem;
width: 100%;
}
}
a.chat-drawer-header__title {
&:hover {
.chat-drawer-header__top-line {
background: var(--primary-low);
border-radius: var(--d-border-radius);
}
}
}
.chat-drawer-header__icon {
margin-right: 0.25rem;
}
.chat-drawer-header__divider {
margin: 0 0.25rem;
}
.chat-drawer-header {
box-sizing: border-box;
border-bottom: solid 1px var(--primary-low);
border-radius: var(--d-border-radius-large) var(--d-border-radius-large) 0 0;
background: var(--primary-very-low);
width: 100%;
display: flex;
align-items: flex-start;
cursor: pointer;
padding: 0.25rem;
.btn {
height: 100%;
}
.chat-channel-title {
font-weight: 700;
width: 100%;
&__user-info {
overflow: hidden;
}
.chat-name,
.chat-drawer-name,
.category-chat-name,
.dm-usernames {
color: var(--primary);
}
.category-chat-badge,
.chat-drawer-badge {
display: flex;
justify-content: center;
align-content: center;
.d-icon:not(.d-icon-lock) {
width: 1.25em;
height: 1.25em;
}
}
.dm-usernames {
max-width: 100%;
overflow: hidden;
text-overflow: ellipsis;
}
.d-icon:not(.d-icon-d-chat) {
color: var(--primary-high);
}
.category-hashtag {
padding: 2px 4px;
}
}
&__close-btn,
&__back-btn,
&__full-screen-btn,
&__thread-list-btn,
&__expand-btn {
height: 30px;
width: 30px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 100%;
&:hover:active {
background: var(--primary-low);
}
.d-icon {
color: var(--primary-low-mid);
margin-right: 0;
}
&:visited {
.d-icon {
color: var(--primary-low-mid);
}
}
&:focus {
outline: none;
background: none;
.d-icon {
background: none;
color: var(--primary);
}
}
&:hover {
.d-icon {
color: var(--primary);
}
}
}
&__thread-list-btn.has-unreads {
margin-right: 0.5rem;
}
}
.chat-drawer-content { .chat-drawer-content {
@include chat-scrollbar(); @include chat-scrollbar();
box-sizing: border-box; box-sizing: border-box;
height: 100%; height: 100%;
min-height: 1px; min-height: 1px;
padding-bottom: 0.25em;
position: relative; position: relative;
overflow-y: auto; overflow-y: auto;
overscroll-behavior: contain; overscroll-behavior: contain;

View File

@ -1,8 +1,9 @@
@mixin chat-height($inset: 0px) { @mixin chat-height($inset: 0px) {
// desktop and mobile // desktop and mobile
// 46px is the height of the navbar
height: calc( height: calc(
var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) - var(--chat-vh, 1vh) * 100 - var(--header-offset, 0px) -
var(--composer-height, 0px) var(--composer-height, 0px) - var(--chat-header-offset)
); );
// mobile with keyboard opened // mobile with keyboard opened

View File

@ -26,5 +26,5 @@
} }
.full-page-chat .chat-mention-warnings { .full-page-chat .chat-mention-warnings {
top: 4rem; top: 2rem;
} }

View File

@ -1,23 +1,58 @@
.chat-navbar { .c-navbar {
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
gap: 0.25rem;
&-container { &-container {
padding-inline: 1rem; padding-inline: 0.5rem;
position: sticky;
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);
background: var(--secondary); background: var(--secondary);
top: var(--header-offset); height: var(--chat-header-offset);
height: 50px; min-height: var(--chat-header-offset);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
z-index: z("header") - 1; z-index: z("composer", "content") - 1;
&.-clickable {
cursor: pointer;
} }
} }
.chat-navbar__right-actions { .single-select-header {
padding: 0.3675rem 0.584rem;
}
.c-navbar__channel-title {
@include ellipsis();
font-weight: 700;
}
.c-navbar__title {
@include ellipsis();
font-weight: 700;
}
.c-navbar__sub-title {
line-height: var(--line-height-small);
font-size: var(--font-down-1-rem);
font-weight: normal;
}
.c-navbar__threads-list-button {
gap: 0.25rem;
&.has-unreads {
.d-icon-discourse-threads {
color: var(--tertiary);
}
}
}
}
.c-navbar__actions {
list-style: none; list-style: none;
margin-left: auto; margin-left: auto;
display: flex;
align-items: center;
} }

View File

@ -18,32 +18,25 @@
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
background: var(--tertiary-low); background: var(--tertiary-low);
padding: 0.5em 0 0.5em 1em; padding: 0.5rem;
color: var(--primary); color: var(--primary);
padding: 0.5em 0 0.5em 1em;
min-width: 280px; min-width: 280px;
margin-left: auto; margin-left: auto;
margin-right: auto; margin-right: auto;
gap: 0.25rem;
} }
.dismiss-btn { .dismiss-btn {
margin: 0 0.25em;
color: var(--primary-medium); color: var(--primary-medium);
align-self: flex-start; align-self: flex-start;
&:hover, &:hover,
&:focus { &:focus {
background-color: transparent; background: var(--tertiary-medium);
.d-icon {
color: var(--primary);
}
}
.d-icon {
color: var(--primary-medium);
} }
} }
} }
.full-page-chat .chat-notices { .full-page-chat .chat-notices {
top: 4rem; top: 2rem;
} }

View File

@ -1,7 +1,6 @@
.chat-side-panel-resizer { .chat-side-panel-resizer {
top: 0; top: 0;
bottom: 0; bottom: 0;
position: absolute; position: absolute;
z-index: z("composer", "content") - 1; z-index: z("composer", "content") - 1;
transition: background-color 0.15s 0.15s; transition: background-color 0.15s 0.15s;

View File

@ -1,66 +0,0 @@
@mixin chat-channel-header-button {
color: var(--primary-low-mid);
padding: 0.25em 0.4em;
.d-icon {
color: inherit;
}
&:visited {
color: var(--primary-low-mid);
}
&:hover {
color: var(--primary-medium);
background: var(--primary-very-low);
border-radius: var(--d-border-radius);
&:hover {
.d-icon {
color: inherit;
}
}
}
> * {
pointer-events: none;
}
}
.chat-channel {
.chat-threads-list-button {
@include chat-channel-header-button;
position: relative;
display: flex;
align-items: center;
&.has-unreads {
color: var(--tertiary-med-or-tertiary);
gap: 0.25rem;
&:hover {
color: var(--tertiary-hover);
}
}
.d-icon {
margin-right: 0;
}
&:hover {
.discourse-touch & {
background: none !important;
}
}
&:active {
.discourse-touch & {
background: var(--secondary-very-high) !important;
}
}
}
.open-drawer-btn {
@include chat-channel-header-button;
}
}

View File

@ -1,32 +1,8 @@
.chat-thread-header { .chat-thread-header {
height: var(--chat-header-offset);
min-height: var(--chat-header-offset);
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);
border-top: 1px solid var(--primary-low); border-top: 1px solid var(--primary-low);
box-sizing: border-box; box-sizing: border-box;
display: flex; display: flex;
align-items: center; align-items: center;
padding-inline: 0.5rem; padding-inline: 0.5rem;
.touch & {
&__label {
font-size: var(--font-up-1-rem);
}
}
.chat-thread__back-to-previous-route {
padding: 0.5rem 0;
margin-right: 0.5rem;
}
&__buttons {
display: flex;
margin-left: auto;
}
&__left-buttons {
display: flex;
flex-direction: row;
align-items: center;
}
} }

View File

@ -1,6 +1,4 @@
.chat-thread-list-header { .chat-thread-list-header {
height: var(--chat-header-offset);
min-height: var(--chat-header-offset);
border-bottom: 1px solid var(--primary-low); border-bottom: 1px solid var(--primary-low);
border-top: 1px solid var(--primary-low); border-top: 1px solid var(--primary-low);
box-sizing: border-box; box-sizing: border-box;

View File

@ -1,46 +0,0 @@
.full-page-chat-header {
display: flex;
padding: 0.25rem;
border-bottom: 1px solid var(--primary-low);
justify-content: space-between;
@include ellipsis;
flex-direction: column;
.chat-channel-info-link {
justify-self: flex-end;
}
}
.full-page-chat-header__about-link {
@include ellipsis;
padding-right: 0.25rem;
.chat-channel-title__name {
font-weight: 700;
}
.chat-channel-title {
padding: 0.5rem 0.5rem 0.25rem 0.5rem;
}
}
.full-page-chat-header__members-link {
padding: 0 0.5rem 0.5rem 0.5rem;
font-size: var(--font-down-1);
color: var(--primary-medium);
&:visited {
color: var(--primary-medium);
}
}
.full-page-chat-header__first-row {
display: flex;
height: 45px;
align-items: center;
}
.full-page-chat-header__second-row {
display: flex;
height: 32px;
align-items: center;
}

View File

@ -1,6 +1,5 @@
@import "chat-unread-indicator"; @import "chat-unread-indicator";
@import "chat-height-mixin"; @import "chat-height-mixin";
@import "chat-thread-header-buttons";
@import "base-common"; @import "base-common";
@import "sidebar-extensions"; @import "sidebar-extensions";
@import "chat-browse"; @import "chat-browse";
@ -41,7 +40,6 @@
@import "chat-transcript"; @import "chat-transcript";
@import "core-extensions"; @import "core-extensions";
@import "dc-filter-input"; @import "dc-filter-input";
@import "full-page-chat-header";
@import "incoming-chat-webhooks"; @import "incoming-chat-webhooks";
@import "reviewable-chat-message"; @import "reviewable-chat-message";
@import "chat-thread-list-item"; @import "chat-thread-list-item";

View File

@ -12,13 +12,6 @@
} }
} }
.chat-full-page-header {
padding: 0 1rem;
height: var(--chat-header-offset);
min-height: var(--chat-header-offset);
flex-shrink: 0;
}
.chat-channel { .chat-channel {
.chat-messages-container { .chat-messages-container {
&.has-reply { &.has-reply {

View File

@ -1,14 +0,0 @@
.chat-channel-title-wrapper {
padding: 0.25rem;
&:hover {
background: var(--primary-very-low);
border-radius: var(--d-border-radius);
}
.chat-channel-title {
&__user-info {
overflow: hidden;
}
}
}

View File

@ -1,5 +1,4 @@
@import "base-desktop"; @import "base-desktop";
@import "chat-channel-title";
@import "chat-composer-uploads"; @import "chat-composer-uploads";
@import "chat-index-drawer"; @import "chat-index-drawer";
@import "chat-index-full-page"; @import "chat-index-full-page";

View File

@ -25,7 +25,7 @@ html.has-full-page-chat {
grid-template-columns: 1fr; grid-template-columns: 1fr;
grid-template-areas: "threads"; grid-template-areas: "threads";
.chat-channel { .c-routes-channel {
display: none; display: none;
} }
} }
@ -54,13 +54,6 @@ html.has-full-page-chat {
.chat-drawer { .chat-drawer {
width: 100%; width: 100%;
} }
.chat-full-page-header {
background-color: var(--secondary);
padding: 0 10px;
height: 50px;
min-height: 50px;
}
} }
.sidebar-container .channels-list .chat-channel-divider { .sidebar-container .channels-list .chat-channel-divider {
@ -77,18 +70,6 @@ html.has-full-page-chat {
} }
} }
.chat-full-page-header {
.chat-channel-header-details {
.chat-channel-retry-archive {
flex-direction: column;
.chat-channel-archive-failed-retry {
margin-top: 0.5em;
}
}
}
}
.chat-message-separator { .chat-message-separator {
margin-left: 0; margin-left: 0;
} }

View File

@ -67,7 +67,7 @@ RSpec.describe "Browse page", type: :system do
context "when on mobile", mobile: true do context "when on mobile", mobile: true do
it "has a back button" do it "has a back button" do
chat_page.visit_browse chat_page.visit_browse
find(".chat-full-page-header__back-btn").click find(".c-navbar__back-button").click
expect(browse_page).to have_current_path("/chat") expect(browse_page).to have_current_path("/chat")
end end
@ -80,6 +80,7 @@ RSpec.describe "Browse page", type: :system do
context "when results are found" do context "when results are found" do
it "lists expected results" do it "lists expected results" do
chat_page.visit_browse chat_page.visit_browse
browse_page.search(category_channel_1.name) browse_page.search(category_channel_1.name)
expect(browse_page).to have_channel(name: category_channel_1.name) expect(browse_page).to have_channel(name: category_channel_1.name)

View File

@ -18,7 +18,7 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
it "redirects to browse page" do it "redirects to browse page" do
chat_page.visit_browse chat_page.visit_browse
find(".chat-channel-card__setting").click find(".chat-channel-card__setting").click
find(".chat-full-page-header__back-btn").click find(".c-navbar__back-button").click
expect(page).to have_current_path("/chat/browse/open") expect(page).to have_current_path("/chat/browse/open")
end end
@ -29,8 +29,8 @@ RSpec.describe "Channel - Info - Settings page", type: :system do
context "when clicking back button" do context "when clicking back button" do
it "redirects to channel page" do it "redirects to channel page" do
chat_page.visit_channel(channel_1) chat_page.visit_channel(channel_1)
find(".chat-channel-title-wrapper").click find(".c-navbar__channel-title").click
find(".chat-full-page-header__back-btn").click find(".c-navbar__back-button").click
expect(page).to have_current_path(chat.channel_path(channel_1.slug, channel_1.id)) expect(page).to have_current_path(chat.channel_path(channel_1.slug, channel_1.id))
end end

View File

@ -22,7 +22,7 @@ RSpec.describe "Drawer", type: :system do
visit("/") visit("/")
chat_page.open_from_header chat_page.open_from_header
drawer_page.open_channel(channel) drawer_page.open_channel(channel)
page.find(".chat-channel-title").click page.find(".c-navbar__channel-title").click
expect(page).to have_current_path("/chat/c/#{channel.slug}/#{channel.id}/info/members") expect(page).to have_current_path("/chat/c/#{channel.slug}/#{channel.id}/info/members")
end end
@ -94,7 +94,7 @@ RSpec.describe "Drawer", type: :system do
chat_page.open_from_header chat_page.open_from_header
expect(page).to have_selector(".chat-drawer.is-expanded") expect(page).to have_selector(".chat-drawer.is-expanded")
page.find(".chat-drawer-header").click page.find(".c-navbar").click
expect(page).to have_selector(".chat-drawer:not(.is-expanded)") expect(page).to have_selector(".chat-drawer:not(.is-expanded)")
end end

View File

@ -337,7 +337,7 @@ RSpec.describe "Navigation", type: :system do
context "when going back to channel from channel settings in full page" do context "when going back to channel from channel settings in full page" do
it "activates the channel in the sidebar" do it "activates the channel in the sidebar" do
visit("/chat/c/#{category_channel.slug}/#{category_channel.id}/info/settings") visit("/chat/c/#{category_channel.slug}/#{category_channel.id}/info/settings")
find(".chat-full-page-header__back-btn").click find(".c-navbar__back-button").click
expect(page).to have_content(message.message) expect(page).to have_content(message.message)
end end
end end

View File

@ -93,10 +93,10 @@ module PageObjects
end end
def minimize_full_page def minimize_full_page
find(".open-drawer-btn").click find(".c-navbar__open-drawer-button").click
end end
NEW_CHANNEL_BUTTON_SELECTOR = ".new-channel-btn" NEW_CHANNEL_BUTTON_SELECTOR = ".c-navbar__new-channel-button"
def new_channel_button def new_channel_button
find(NEW_CHANNEL_BUTTON_SELECTOR) find(NEW_CHANNEL_BUTTON_SELECTOR)

Some files were not shown because too many files have changed in this diff Show More