DEV: makes chat modals use the new <DModal /> component (#22495)

This commit also standardize the naming pattern of modals: `<Chat::Modal::FooBar />` and changes css class accordingly.

Co-authored-by: David Taylor <david@taylorhq.com>
This commit is contained in:
Joffrey JAFFEUX 2023-07-10 13:43:33 +02:00 committed by GitHub
parent ed2dae6d1a
commit 9830c40386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
115 changed files with 1366 additions and 1179 deletions

View File

@ -1,5 +1,5 @@
<DButton
@class="btn-flat d-modal-cancel"
@action={{action this.close}}
@action={{@close}}
@translatedLabel={{i18n "cancel"}}
/>

View File

@ -1,4 +0,0 @@
import Component from "@ember/component";
export default Component.extend({
tagName: "",
});

View File

@ -490,4 +490,20 @@ module("Integration | Component | select-kit/single-select", function (hooks) {
assert.dom(".single-select.is-expanded").exists();
});
test("options.formName", async function (assert) {
setDefaultState(this);
await render(hbs`
<SingleSelect
@value={{this.value}}
@content={{this.content}}
@options={{hash formName="foo"}}
/>
`);
assert
.dom('input[name="foo"]')
.hasAttribute("type", "hidden")
.hasAttribute("value", "1");
});
});

View File

@ -311,6 +311,7 @@ export default Component.extend(
hiddenValues: null,
disabled: false,
expandedOnInsert: false,
formName: null,
},
autoFilterable: computed("content.[]", "selectKit.filter", function () {

View File

@ -6,6 +6,14 @@
data-name={{this.name}}
class="select-kit-selected-name selected-name choice"
>
{{#if this.selectKit.options.formName}}
<input
type="hidden"
name={{this.selectKit.options.formName}}
value={{this.value}}
/>
{{/if}}
{{#if this.item.icon}}
{{d-icon this.item.icon}}
{{/if}}

View File

@ -1,22 +0,0 @@
<DModalBody @title="chat.summarization.title">
<span>{{i18n "chat.summarization.description"}}</span>
<ComboBox
@value={{this.sinceHours}}
@content={{this.sinceOptions}}
@onChange={{action this.summarize}}
@valueProperty="value"
@class="summarization-since"
/>
<div class="channel-summary">
<ConditionalLoadingSpinner @condition={{this.loading}} />
{{#unless this.loading}}
<p class="summary-area">{{this.summary}}</p>
{{/unless}}
</div>
</DModalBody>
<div class="modal-footer">
<DModalCancel @close={{route-action "closeModal"}} />
</div>

View File

@ -1,65 +0,0 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { ajax } from "discourse/lib/ajax";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import I18n from "I18n";
export default class ChannelSumarry extends Component {
@tracked sinceHours = null;
@tracked loading = false;
@tracked availableSummaries = {};
@tracked summary = null;
sinceOptions = [
{
name: I18n.t("chat.summarization.since", { count: 1 }),
value: 1,
},
{
name: I18n.t("chat.summarization.since", { count: 3 }),
value: 3,
},
{
name: I18n.t("chat.summarization.since", { count: 6 }),
value: 6,
},
{
name: I18n.t("chat.summarization.since", { count: 12 }),
value: 12,
},
{
name: I18n.t("chat.summarization.since", { count: 24 }),
value: 24,
},
{
name: I18n.t("chat.summarization.since", { count: 72 }),
value: 72,
},
{
name: I18n.t("chat.summarization.since", { count: 168 }),
value: 168,
},
];
@action
summarize(value) {
this.loading = true;
if (this.availableSummaries[value]) {
this.summary = this.availableSummaries[value];
this.loading = false;
return;
}
ajax(`/chat/api/channels/${this.args.channelId}/summarize`, {
method: "GET",
data: { since: value },
})
.then((data) => {
this.availableSummaries[this.sinceHours] = data.summary;
this.summary = this.availableSummaries[this.sinceHours];
})
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
}

View File

@ -4,7 +4,7 @@ import { action } from "@ember/object";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
export default class ChannelsList extends Component {
@service chat;
@ -30,7 +30,7 @@ export default class ChannelsList extends Component {
@action
openNewMessageModal() {
this.modal.show(ChatNewMessageModal);
this.modal.show(ChatModalNewMessage);
}
get showMobileDirectMessageButton() {

View File

@ -4,13 +4,15 @@ import { action, computed } from "@ember/object";
import { schedule } from "@ember/runloop";
import { inject as service } from "@ember/service";
import discourseDebounce from "discourse-common/lib/debounce";
import showModal from "discourse/lib/show-modal";
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
import ChatModalCreateChannel from "discourse/plugins/chat/discourse/components/chat/modal/create-channel";
const TABS = ["all", "open", "closed", "archived"];
export default class ChatBrowseView extends Component {
@service chatApi;
@service modal;
tagName = "";
didReceiveAttrs() {
@ -41,7 +43,7 @@ export default class ChatBrowseView extends Component {
@action
showChatNewMessageModal() {
this.modal.show(ChatNewMessageModal);
this.modal.show(ChatModalNewMessage);
}
@action
@ -66,7 +68,7 @@ export default class ChatBrowseView extends Component {
@action
createChannel() {
showModal("create-channel");
this.modal.show(ChatModalCreateChannel);
}
@action

View File

@ -1,30 +0,0 @@
<DModalBody @title="chat.channel_archive.title">
<div
id="chat-channel-archive-modal-inner"
class="chat-channel-archive-modal-inner"
>
<p
class="chat-channel-archive-modal-instructions"
>{{this.instructionsText}}</p>
<ChatToTopicSelector
@selection={{this.selection}}
@topicTitle={{this.topicTitle}}
@categoryId={{this.categoryId}}
@tags={{this.tags}}
@selectedTopicId={{this.selectedTopicId}}
@instructionLabels={{this.instructionLabels}}
@allowNewMessage={{false}}
/>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{action "archiveChannel"}}
@label="chat.channel_archive.title"
@id="chat-confirm-archive-channel"
/>
</div>

View File

@ -1,108 +0,0 @@
import Component from "@ember/component";
import I18n from "I18n";
import discourseLater from "discourse-common/lib/later";
import { isEmpty } from "@ember/utils";
import discourseComputed from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import { equal } from "@ember/object/computed";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import {
EXISTING_TOPIC_SELECTION,
NEW_TOPIC_SELECTION,
} from "discourse/plugins/chat/discourse/components/chat-to-topic-selector";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
import { htmlSafe } from "@ember/template";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Component.extend(ModalFunctionality, {
chat: service(),
chatApi: service(),
tagName: "",
chatChannel: null,
selection: NEW_TOPIC_SELECTION,
newTopic: equal("selection", NEW_TOPIC_SELECTION),
existingTopic: equal("selection", EXISTING_TOPIC_SELECTION),
saving: false,
topicTitle: null,
categoryId: null,
tags: null,
selectedTopicId: null,
@action
archiveChannel() {
this.set("saving", true);
return this.chatApi
.createChannelArchive(this.chatChannel.id, this._data())
.then(() => {
this.flash(I18n.t("chat.channel_archive.process_started"), "success");
this.chatChannel.status = CHANNEL_STATUSES.archived;
discourseLater(() => {
this.closeModal();
}, 3000);
})
.catch(popupAjaxError)
.finally(() => this.set("saving", false));
},
_data() {
const data = {
type: this.selection,
};
if (this.newTopic) {
data.title = this.topicTitle;
data.category_id = this.categoryId;
data.tags = this.tags;
}
if (this.existingTopic) {
data.topic_id = this.selectedTopicId;
}
return data;
},
@discourseComputed("saving", "selectedTopicId", "topicTitle", "selection")
buttonDisabled(saving, selectedTopicId, topicTitle) {
if (saving) {
return true;
}
if (
this.newTopic &&
(!topicTitle ||
topicTitle.length < this.siteSettings.min_topic_title_length ||
topicTitle.length > this.siteSettings.max_topic_title_length)
) {
return true;
}
if (this.existingTopic && isEmpty(selectedTopicId)) {
return true;
}
return false;
},
@discourseComputed()
instructionLabels() {
const labels = {};
labels[NEW_TOPIC_SELECTION] = I18n.t(
"chat.selection.new_topic.instructions_channel_archive"
);
labels[EXISTING_TOPIC_SELECTION] = I18n.t(
"chat.selection.existing_topic.instructions_channel_archive"
);
return labels;
},
@discourseComputed()
instructionsText() {
return htmlSafe(
I18n.t("chat.channel_archive.instructions", {
channelTitle: this.chatChannel.escapedTitle,
})
);
},
});

View File

@ -1,30 +0,0 @@
<DModalBody @title="chat.channel_delete.title">
<div
id="chat-channel-delete-modal-inner"
class="chat-channel-delete-modal-inner"
>
<p class="chat-channel-delete-modal-instructions">
{{this.instructionsText}}
</p>
</div>
<TextField
@value={{this.channelNameConfirmation}}
@id="channel-delete-confirm-name"
@placeholderKey="chat.channel_delete.confirm_channel_name"
@autocorrect="off"
@autocapitalize="off"
/>
</DModalBody>
<div class="modal-footer">
<DButton
@disabled={{this.buttonDisabled}}
@class="btn-danger"
@action={{action "deleteChannel"}}
@label="chat.channel_delete.confirm"
@id="chat-confirm-delete-channel"
/>
<DButton @label="cancel" @class="btn-flat" @action={{this.closeModal}} />
</div>

View File

@ -1,65 +0,0 @@
import Component from "@ember/component";
import { isEmpty } from "@ember/utils";
import I18n from "I18n";
import discourseComputed from "discourse-common/utils/decorators";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseLater from "discourse-common/lib/later";
import { htmlSafe } from "@ember/template";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default Component.extend(ModalFunctionality, {
chat: service(),
chatApi: service(),
router: service(),
tagName: "",
chatChannel: null,
channelNameConfirmation: null,
deleting: false,
confirmed: false,
@discourseComputed("deleting", "channelNameConfirmation", "confirmed")
buttonDisabled(deleting, channelNameConfirmation, confirmed) {
if (deleting || confirmed) {
return true;
}
if (
isEmpty(channelNameConfirmation) ||
channelNameConfirmation.toLowerCase() !==
this.chatChannel.title.toLowerCase()
) {
return true;
}
return false;
},
@action
deleteChannel() {
this.set("deleting", true);
return this.chatApi
.destroyChannel(this.chatChannel.id, this.channelNameConfirmation)
.then(() => {
this.set("confirmed", true);
this.flash(I18n.t("chat.channel_delete.process_started"), "success");
discourseLater(() => {
this.closeModal();
this.router.transitionTo("chat");
}, 3000);
})
.catch(popupAjaxError)
.finally(() => this.set("deleting", false));
},
@discourseComputed()
instructionsText() {
return htmlSafe(
I18n.t("chat.channel_delete.instructions", {
name: this.chatChannel.escapedTitle,
})
);
},
});

View File

@ -1,8 +1,10 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import showModal from "discourse/lib/show-modal";
import I18n from "I18n";
import ChatModalArchiveChannel from "discourse/plugins/chat/discourse/components/chat/modal/archive-channel";
import ChatModalDeleteChannel from "discourse/plugins/chat/discourse/components/chat/modal/delete-channel";
import ChatModalToggleChannelStatus from "discourse/plugins/chat/discourse/components/chat/modal/toggle-channel-status";
const NOTIFICATION_LEVELS = [
{ name: I18n.t("chat.notification_levels.never"), value: "never" },
@ -41,6 +43,7 @@ export default class ChatChannelSettingsView extends Component {
@service siteSettings;
@service router;
@service dialog;
@service modal;
notificationLevels = NOTIFICATION_LEVELS;
mutedOptions = MUTED_OPTIONS;
@ -107,20 +110,21 @@ export default class ChatChannelSettingsView extends Component {
@action
onArchiveChannel() {
const controller = showModal("chat-channel-archive-modal");
controller.set("chatChannel", this.args.channel);
return this.modal.show(ChatModalArchiveChannel, {
model: { channel: this.args.channel },
});
}
@action
onDeleteChannel() {
const controller = showModal("chat-channel-delete-modal");
controller.set("chatChannel", this.args.channel);
return this.modal.show(ChatModalDeleteChannel, {
model: { channel: this.args.channel },
});
}
@action
onToggleChannelState() {
const controller = showModal("chat-channel-toggle");
controller.set("chatChannel", this.args.channel);
this.modal.show(ChatModalToggleChannelStatus, { model: this.args.channel });
}
@action

View File

@ -1,14 +0,0 @@
<DModalBody @title={{this.modalTitle}}>
<div id="chat-channel-toggle" class="chat-channel-toggle">
<p>{{this.instructions}}</p>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary"
@action={{action "changeChannelStatus"}}
@label={{this.buttonLabel}}
@id="chat-channel-toggle-btn"
/>
</div>

View File

@ -25,6 +25,7 @@ import {
initUserStatusHtml,
renderUserStatusHtml,
} from "discourse/lib/user-status-on-autocomplete";
import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary";
export default class ChatComposer extends Component {
@service capabilities;
@ -39,6 +40,7 @@ export default class ChatComposer extends Component {
@service currentUser;
@service chatApi;
@service chatDraftsManager;
@service modal;
@tracked isFocused = false;
@tracked inProgressUploadsCount = 0;
@ -377,8 +379,8 @@ export default class ChatComposer extends Component {
@action
showChannelSummaryModal() {
showModal("channel-summary").setProperties({
channelId: this.args.channel.id,
this.modal.show(ChatModalChannelSummary, {
model: { channelId: this.args.channel.id },
});
}

View File

@ -1,28 +0,0 @@
<DModalBody @title="chat.move_to_channel.title">
<div
id="chat-message-move-to-channel-modal-inner"
class="chat-message-move-to-channel-modal-inner"
>
<p>{{this.instructionsText}}</p>
</div>
<ChatChannelChooser
@class="chat-move-message-channel-chooser"
@content={{this.availableChannels}}
@value={{this.destinationChannelId}}
@nameProperty="title"
/>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary"
@icon="sign-out-alt"
@disabled={{this.disableMoveButton}}
@action={{this.moveMessages}}
@label="chat.move_to_channel.confirm_move"
@id="chat-confirm-move-messages-to-channel"
/>
<DButton @label="cancel" @class="btn-flat" @action={{this.closeModal}} />
</div>

View File

@ -5,11 +5,7 @@
(if @message.newest "with-last-visit")
}}
role="button"
{{on
"click"
(fn @fetchMessagesByDate @message.firstMessageOfTheDayAt)
passive=true
}}
{{on "click" this.onDateClick passive=true}}
>
<div
class="chat-message-separator__text-container"

View File

@ -0,0 +1,11 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
export default class ChatMessageSeparatorDate extends Component {
@action
onDateClick() {
return this.args.fetchMessagesByDate?.(
this.args.message.firstMessageOfTheDayAt
);
}
}

View File

@ -281,6 +281,7 @@ export default class ChatMessageCreator extends Component {
return;
} else if (this.query?.length === 0) {
this.openChannel(this.selection);
event.preventDefault();
return;
}
}

View File

@ -0,0 +1,32 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-archive-channel"
@inline={{@inline}}
@title={{i18n "chat.channel_archive.title"}}
@flash={{this.flash}}
@flashType={{this.flashType}}
>
<:body>
<p class="chat-modal-archive-channel__instructions">
{{this.instructionsText}}
</p>
<ChatToTopicSelector
@selection={{this.selection}}
@topicTitle={{this.topicTitle}}
@categoryId={{this.categoryId}}
@tags={{this.tags}}
@selectedTopicId={{this.selectedTopicId}}
@instructionLabels={{this.instructionLabels}}
@allowNewMessage={{false}}
/>
</:body>
<:footer>
<DButton
@class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{this.archiveChannel}}
@label="chat.channel_archive.title"
@id="chat-confirm-archive-channel"
/>
</:footer>
</DModal>

View File

@ -0,0 +1,112 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
import discourseLater from "discourse-common/lib/later";
import {
EXISTING_TOPIC_SELECTION,
NEW_TOPIC_SELECTION,
} from "discourse/plugins/chat/discourse/components/chat-to-topic-selector";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
import { isEmpty } from "@ember/utils";
export default class ChatModalArchiveChannel extends Component {
@service chatApi;
@service siteSettings;
@tracked selection = NEW_TOPIC_SELECTION;
@tracked saving = false;
@tracked topicTitle = null;
@tracked categoryId = null;
@tracked tags = null;
@tracked selectedTopicId = null;
@tracked flash;
@tracked flashType;
get channel() {
return this.args.model.channel;
}
get newTopic() {
return this.selection === NEW_TOPIC_SELECTION;
}
get existingTopic() {
return this.selection === EXISTING_TOPIC_SELECTION;
}
get buttonDisabled() {
if (this.saving) {
return true;
}
if (
this.newTopic &&
(!this.topicTitle ||
this.topicTitle.length < this.siteSettings.min_topic_title_length ||
this.topicTitle.length > this.siteSettings.max_topic_title_length)
) {
return true;
}
if (this.existingTopic && isEmpty(this.selectedTopicId)) {
return true;
}
return false;
}
get instructionLabels() {
const labels = {};
labels[NEW_TOPIC_SELECTION] = I18n.t(
"chat.selection.new_topic.instructions_channel_archive"
);
labels[EXISTING_TOPIC_SELECTION] = I18n.t(
"chat.selection.existing_topic.instructions_channel_archive"
);
return labels;
}
get instructionsText() {
return htmlSafe(
I18n.t("chat.channel_archive.instructions", {
channelTitle: this.channel.escapedTitle,
})
);
}
@action
archiveChannel() {
this.saving = true;
return this.chatApi
.createChannelArchive(this.channel.id, this.#data())
.then(() => {
this.flash = I18n.t("chat.channel_archive.process_started");
this.flashType = "success";
this.channel.status = CHANNEL_STATUSES.archived;
discourseLater(() => {
this.args.closeModal();
}, 3000);
})
.catch(popupAjaxError)
.finally(() => (this.saving = false));
}
#data() {
const data = { type: this.selection };
if (this.newTopic) {
data.title = this.topicTitle;
data.category_id = this.categoryId;
data.tags = this.tags;
}
if (this.existingTopic) {
data.topic_id = this.selectedTopicId;
}
return data;
}
}

View File

@ -0,0 +1,22 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-channel-summary"
@title={{i18n "chat.summarization.title"}}
>
<:body>
<span>{{i18n "chat.summarization.description"}}</span>
<ComboBox
@value={{this.sinceHours}}
@content={{this.sinceOptions}}
@onChange={{this.summarize}}
@valueProperty="value"
@class="summarization-since"
/>
<ConditionalLoadingSection @isLoading={{this.loading}}>
<p class="summary-area">{{this.summary}}</p>
</ConditionalLoadingSection>
</:body>
<:footer>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>

View File

@ -0,0 +1,47 @@
import Component from "@glimmer/component";
import { tracked } from "@glimmer/tracking";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { action } from "@ember/object";
import I18n from "I18n";
import { inject as service } from "@ember/service";
export default class ChatModalChannelSummary extends Component {
@service chatApi;
@tracked sinceHours = null;
@tracked loading = false;
@tracked summary = null;
availableSummaries = {};
sinceOptions = [1, 3, 6, 12, 24, 72, 168].map((hours) => {
return {
name: I18n.t("chat.summarization.since", { count: hours }),
value: hours,
};
});
get channelId() {
return this.args.model.channelId;
}
@action
summarize(since) {
this.loading = true;
if (this.availableSummaries[since]) {
this.summary = this.availableSummaries[since];
this.loading = false;
return;
}
return this.chatApi
.summarize(this.channelId, { since })
.then((data) => {
this.availableSummaries[this.sinceHours] = data.summary;
this.summary = this.availableSummaries[this.sinceHours];
})
.catch(popupAjaxError)
.finally(() => (this.loading = false));
}
}

View File

@ -0,0 +1,136 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-create-channel"
@inline={{@inline}}
@title={{i18n "chat.create_channel.title"}}
@flash={{this.flash}}
@tagName="form"
{{on "submit" this.onSave}}
>
<:body>
<div class="chat-modal-create-channel__control -name">
<label for="name" class="chat-modal-create-channel__label">
{{i18n "chat.create_channel.name"}}
</label>
<Input
name="name"
class="chat-modal-create-channel__input"
@type="text"
@value={{this.name}}
{{on "input" (action this.onNameChange value="target.value")}}
/>
</div>
<div class="chat-modal-create-channel__control -slug">
<label for="slug" class="chat-modal-create-channel__label">
{{i18n "chat.create_channel.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>
{{i18n "chat.channel_edit_name_slug_modal.slug_description"}}
</DTooltip>
</span>
</label>
<Input
name="slug"
class="chat-modal-create-channel__input"
@type="text"
@value={{this.slug}}
placeholder={{this.autoGeneratedSlug}}
/>
</div>
<div class="chat-modal-create-channel__control -description">
<label for="description" class="chat-modal-create-channel__label">
{{i18n "chat.create_channel.description"}}
</label>
<Input
name="description"
class="chat-modal-create-channel__input"
@type="textarea"
@value={{this.description}}
/>
</div>
<div class="chat-modal-create-channel__control">
<label class="chat-modal-create-channel__label">
{{i18n "chat.create_channel.choose_category.label"}}
</label>
<CategoryChooser
@value={{this.categoryId}}
@onChange={{action this.onCategoryChange}}
@options={{hash
formName="chatable_id"
none="chat.create_channel.choose_category.none"
}}
/>
{{#if this.categoryPermissionsHint}}
<div
class={{concat-class
"chat-modal-create-channel__hint"
(if this.loadingPermissionHint "loading-permissions")
}}
>
{{this.categoryPermissionsHint}}
</div>
{{/if}}
</div>
{{#if this.autoJoinAvailable}}
<div class="chat-modal-create-channel__control -auto-join">
<label class="chat-modal-create-channel__label">
<Input
name="auto_join_users"
@type="checkbox"
@checked={{this.autoJoinUsers}}
/>
<div class="auto-join-channel">
<span class="chat-modal-create-channel__label-title">
{{i18n "chat.settings.auto_join_users_label"}}
</span>
<p class="chat-modal-create-channel__label-description">
{{#if this.categoryName}}
{{i18n
"chat.settings.auto_join_users_info"
category=this.categoryName
}}
{{else}}
{{i18n "chat.settings.auto_join_users_info_no_category"}}
{{/if}}
</p>
</div>
</label>
</div>
{{/if}}
{{#if this.threadingAvailable}}
<div class="chat-modal-create-channel__control -threading-toggle">
<label class="chat-modal-create-channel__label">
<Input
name="threading_enabled"
@type="checkbox"
@checked={{this.threadingEnabled}}
/>
<div class="threading-channel">
<span class="chat-modal-create-channel__label-title">
{{i18n "chat.create_channel.threading.label"}}
</span>
<p class="chat-modal-create-channel__label-description">
{{i18n "chat.settings.channel_threading_description"}}
</p>
</div>
</label>
</div>
{{/if}}
</:body>
<:footer>
<button
class="btn btn-primary create"
disabled={{this.createDisabled}}
type="submit"
>
{{i18n "chat.create_channel.create"}}
</button>
</:footer>
</DModal>

View File

@ -2,14 +2,13 @@ import { escapeExpression } from "discourse/lib/utilities";
import { ajax } from "discourse/lib/ajax";
import { cancel } from "@ember/runloop";
import discourseDebounce from "discourse-common/lib/debounce";
import Controller from "@ember/controller";
import Component from "@glimmer/component";
import I18n from "I18n";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action, computed } from "@ember/object";
import { gt, notEmpty } from "@ember/object/computed";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { isBlank, isPresent } from "@ember/utils";
import { htmlSafe } from "@ember/template";
import { tracked } from "@glimmer/tracking";
const DEFAULT_HINT = htmlSafe(
I18n.t("chat.create_channel.choose_category.default_hint", {
@ -18,9 +17,7 @@ const DEFAULT_HINT = htmlSafe(
})
);
export default class CreateChannelController extends Controller.extend(
ModalFunctionality
) {
export default class ChatModalCreateChannel extends Component {
@service chat;
@service dialog;
@service chatChannelsManager;
@ -28,21 +25,30 @@ export default class CreateChannelController extends Controller.extend(
@service router;
@service currentUser;
@service siteSettings;
@service site;
category = null;
categoryId = null;
name = "";
slug = "";
autoGeneratedSlug = "";
description = "";
categoryPermissionsHint = null;
autoJoinUsers = false;
autoJoinWarning = "";
loadingPermissionHint = false;
threadingEnabled = false;
@tracked flash;
@tracked name;
@tracked category;
@tracked categoryId;
@tracked autoGeneratedSlug = "";
@tracked categoryPermissionsHint;
@tracked autoJoinWarning = "";
@tracked loadingPermissionHint = false;
@notEmpty("category") categorySelected;
@gt("siteSettings.max_chat_auto_joined_users", 0) autoJoinAvailable;
#generateSlugHandler = null;
willDestroy() {
cancel(this.#generateSlugHandler);
}
get autoJoinAvailable() {
return this.siteSettings.max_chat_auto_joined_users > 0;
}
get categorySelected() {
return isPresent(this.category);
}
get threadingAvailable() {
return (
@ -51,69 +57,88 @@ export default class CreateChannelController extends Controller.extend(
);
}
@computed("categorySelected", "name")
get createDisabled() {
return !this.categorySelected || isBlank(this.name);
}
@computed("categorySelected", "name")
get categoryName() {
return this.categorySelected && isPresent(this.name)
? escapeExpression(this.name)
: null;
}
@action
onShow() {
this.set("categoryPermissionsHint", DEFAULT_HINT);
this.categoryPermissionsHint = DEFAULT_HINT;
}
onClose() {
cancel(this.generateSlugHandler);
this.setProperties({
categoryId: null,
category: null,
name: "",
description: "",
slug: "",
autoGeneratedSlug: "",
categoryPermissionsHint: DEFAULT_HINT,
autoJoinWarning: "",
});
@action
onCategoryChange(categoryId) {
const category = categoryId
? this.site.categories.findBy("id", categoryId)
: null;
this.#updatePermissionsHint(category);
const name = this.name || category?.name || "";
this.categoryId = categoryId;
this.category = category;
this.name = name;
this.#debouncedGenerateSlug(name);
}
_createChannel() {
const data = {
chatable_id: this.categoryId,
name: this.name,
slug: this.slug || this.autoGeneratedSlug,
description: this.description,
auto_join_users: this.autoJoinUsers,
threading_enabled: this.threadingEnabled,
};
@action
onNameChange(name) {
this.#debouncedGenerateSlug(name);
}
@action
onSave(event) {
event.preventDefault();
if (this.createDisabled) {
return;
}
const formData = new FormData(event.currentTarget);
const data = Object.fromEntries(formData.entries());
data.auto_join_users = data.auto_join_users === "on";
data.slug ??= this.autoGeneratedSlug;
data.threading_enabled = data.threading_enabled === "on";
if (data.auto_join_users) {
this.dialog.yesNoConfirm({
message: this.autoJoinWarning,
didConfirm: () => this.#createChannel(data),
});
} else {
this.#createChannel(data);
}
}
#createChannel(data) {
return this.chatApi
.createChannel(data)
.then((channel) => {
this.send("closeModal");
this.args.closeModal();
this.chatChannelsManager.follow(channel);
this.router.transitionTo("chat.channel", ...channel.routeModels);
})
.catch((e) => {
this.flash(e.jqXHR.responseJSON.errors[0], "error");
this.flash = e.jqXHR.responseJSON.errors[0];
});
}
_buildCategorySlug(category) {
#buildCategorySlug(category) {
const parent = category.parentCategory;
if (parent) {
return `${this._buildCategorySlug(parent)}/${category.slug}`;
return `${this.#buildCategorySlug(parent)}/${category.slug}`;
} else {
return category.slug;
}
}
_updateAutoJoinConfirmWarning(category, catPermissions) {
#updateAutoJoinConfirmWarning(category, catPermissions) {
const allowedGroups = catPermissions.allowed_groups;
let warning;
@ -158,19 +183,19 @@ export default class CreateChannelController extends Controller.extend(
);
}
this.set("autoJoinWarning", warning);
this.autoJoinWarning = warning;
}
_updatePermissionsHint(category) {
#updatePermissionsHint(category) {
if (category) {
const fullSlug = this._buildCategorySlug(category);
const fullSlug = this.#buildCategorySlug(category);
this.set("loadingPermissionHint", true);
this.loadingPermissionHint = true;
return this.chatApi
.categoryPermissions(category.id)
.then((catPermissions) => {
this._updateAutoJoinConfirmWarning(category, catPermissions);
this.#updateAutoJoinConfirmWarning(category, catPermissions);
const allowedGroups = catPermissions.allowed_groups;
const settingLink = `/c/${escapeExpression(fullSlug)}/edit/security`;
let hint;
@ -207,77 +232,40 @@ export default class CreateChannelController extends Controller.extend(
break;
}
this.set("categoryPermissionsHint", htmlSafe(hint));
this.categoryPermissionsHint = htmlSafe(hint);
})
.finally(() => {
this.set("loadingPermissionHint", false);
this.loadingPermissionHint = false;
});
} else {
this.set("categoryPermissionsHint", DEFAULT_HINT);
this.set("autoJoinWarning", "");
this.categoryPermissionsHint = DEFAULT_HINT;
this.autoJoinWarning = "";
}
}
// intentionally not showing AJAX error for this, we will autogenerate
// the slug server-side if they leave it blank
_generateSlug(name) {
ajax("/slugs.json", { type: "POST", data: { name } }).then((response) => {
this.set("autoGeneratedSlug", response.slug);
});
#generateSlug(name) {
return ajax("/slugs.json", { type: "POST", data: { name } }).then(
(response) => {
this.autoGeneratedSlug = response.slug;
}
);
}
_debouncedGenerateSlug(name) {
cancel(this.generateSlugHandler);
this._clearAutoGeneratedSlug();
#debouncedGenerateSlug(name) {
cancel(this.#generateSlugHandler);
this.autoGeneratedSlug = "";
if (!name) {
return;
}
this.generateSlugHandler = discourseDebounce(
this.#generateSlugHandler = discourseDebounce(
this,
this._generateSlug,
this.#generateSlug,
name,
300
);
}
_clearAutoGeneratedSlug() {
this.set("autoGeneratedSlug", "");
}
@action
onCategoryChange(categoryId) {
let category = categoryId
? this.site.categories.findBy("id", categoryId)
: null;
this._updatePermissionsHint(category);
const name = this.name || category?.name || "";
this.setProperties({
categoryId,
category,
name,
});
this._debouncedGenerateSlug(name);
}
@action
onNameChange(name) {
this._debouncedGenerateSlug(name);
}
@action
create() {
if (this.createDisabled) {
return;
}
if (this.autoJoinUsers) {
this.dialog.yesNoConfirm({
message: this.autoJoinWarning,
didConfirm: () => this._createChannel(),
});
} else {
this._createChannel();
}
}
}

View File

@ -0,0 +1,32 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-delete-channel"
@inline={{@inline}}
@title={{i18n "chat.channel_delete.title"}}
@flash={{this.flash}}
@flashType={{this.flashType}}
>
<:body>
<p class="chat-modal-delete-channel__instructions">
{{this.instructionsText}}
</p>
<TextField
@value={{this.channelNameConfirmation}}
@id="channel-delete-confirm-name"
@placeholderKey="chat.channel_delete.confirm_channel_name"
@autocorrect="off"
@autocapitalize="off"
/>
</:body>
<:footer>
<DButton
@disabled={{this.buttonDisabled}}
@class="btn-danger"
@action={{this.deleteChannel}}
@label="chat.channel_delete.confirm"
@id="chat-confirm-delete-channel"
/>
<DButton @label="cancel" @class="btn-flat" @action={{@closeModal}} />
</:footer>
</DModal>

View File

@ -0,0 +1,68 @@
import Component from "@glimmer/component";
import { isEmpty } from "@ember/utils";
import I18n from "I18n";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
import discourseLater from "discourse-common/lib/later";
import { htmlSafe } from "@ember/template";
import { tracked } from "@glimmer/tracking";
export default class ChatModalDeleteChannel extends Component {
@service chatApi;
@service router;
@tracked channelNameConfirmation;
@tracked deleting = false;
@tracked confirmed = false;
@tracked flash;
@tracked flashType;
get channel() {
return this.args.model.channel;
}
get buttonDisabled() {
if (this.deleting || this.confirmed) {
return true;
}
if (
isEmpty(this.channelNameConfirmation) ||
this.channelNameConfirmation.toLowerCase() !==
this.channel.title.toLowerCase()
) {
return true;
}
return false;
}
get instructionsText() {
return htmlSafe(
I18n.t("chat.channel_delete.instructions", {
name: this.channel.escapedTitle,
})
);
}
@action
deleteChannel() {
this.deleting = true;
return this.chatApi
.destroyChannel(this.channel.id, this.channelNameConfirmation)
.then(() => {
this.confirmed = true;
this.flash = I18n.t("chat.channel_delete.process_started");
this.flashType = "success";
discourseLater(() => {
this.args.closeModal();
this.router.transitionTo("chat");
}, 3000);
})
.catch(popupAjaxError)
.finally(() => (this.deleting = false));
}
}

View File

@ -0,0 +1,37 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-edit-channel-description"
@inline={{@inline}}
@title={{i18n "chat.channel_edit_description_modal.title"}}
@flash={{this.flash}}
>
<:body>
<span class="chat-modal-edit-channel-description__description">{{i18n
"chat.channel_edit_description_modal.description"
}}</span>
<CharCounter
@value={{this.editedDescription}}
@max={{this.descriptionMaxLength}}
>
<textarea
{{on
"input"
(action this.onChangeChatChannelDescription value="target.value")
}}
class="chat-modal-edit-channel-description__description-input"
placeholder={{i18n
"chat.channel_edit_description_modal.input_placeholder"
}}
>{{this.editedDescription}}</textarea>
</CharCounter>
</:body>
<:footer>
<DButton
@class="btn-primary create"
@action={{this.onSaveChatChannelDescription}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>

View File

@ -0,0 +1,49 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
const DESCRIPTION_MAX_LENGTH = 280;
export default class ChatModalEditChannelDescription extends Component {
@service chatApi;
@tracked editedDescription = this.channel.description || "";
@tracked flash;
get channel() {
return this.args.model;
}
get isSaveDisabled() {
return (
this.channel.description === this.editedDescription ||
this.editedDescription?.length > DESCRIPTION_MAX_LENGTH
);
}
get descriptionMaxLength() {
return DESCRIPTION_MAX_LENGTH;
}
@action
onSaveChatChannelDescription() {
return this.chatApi
.updateChannel(this.channel.id, { description: this.editedDescription })
.then((result) => {
this.channel.description = result.channel.description;
this.args.closeModal();
})
.catch((event) => {
if (event.jqXHR?.responseJSON?.errors) {
this.flash = event.jqXHR.responseJSON.errors.join("\n");
}
});
}
@action
onChangeChatChannelDescription(description) {
this.flash = null;
this.editedDescription = description;
}
}

View File

@ -0,0 +1,54 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-edit-channel-name"
@inline={{@inline}}
@title={{i18n "chat.channel_edit_name_slug_modal.title"}}
@flash={{this.flash}}
>
<:body>
<div class="edit-channel-control">
<label for="channel-name" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.name"}}
</label>
<Input
name="channel-name"
class="chat-channel-edit-name-slug-modal__name-input"
placeholder={{i18n
"chat.channel_edit_name_slug_modal.input_placeholder"
}}
@type="text"
@value={{this.editedName}}
{{on "input" (action "onChangeChatChannelName" value="target.value")}}
/>
</div>
<div class="edit-channel-control">
<label for="channel-slug" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>{{i18n
"chat.channel_edit_name_slug_modal.slug_description"
}}</DTooltip>
</span>
</label>
<Input
name="channel-slug"
class="chat-channel-edit-name-slug-modal__slug-input"
placeholder={{this.autoGeneratedSlug}}
{{on "input" (action "onChangeChatChannelSlug" value="target.value")}}
@type="text"
@value={{this.editedSlug}}
/>
</div>
</:body>
<:footer>
<DButton
@class="btn-primary create"
@action={{this.onSave}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{@closeModal}} />
</:footer>
</DModal>

View File

@ -0,0 +1,84 @@
import Component from "@glimmer/component";
import discourseDebounce from "discourse-common/lib/debounce";
import { ajax } from "discourse/lib/ajax";
import { cancel } from "@ember/runloop";
import { action } from "@ember/object";
import { extractError } from "discourse/lib/ajax-error";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
export default class ChatModalEditChannelName extends Component {
@service chatApi;
@service siteSettings;
@tracked editedName = this.channel.title;
@tracked editedSlug = this.channel.slug;
@tracked autoGeneratedSlug = "";
@tracked flash;
#generateSlugHandler = null;
get channel() {
return this.args.model;
}
get isSaveDisabled() {
return (
(this.channel.title === this.editedName &&
this.channel.slug === this.editedSlug) ||
this.editedName?.length > this.siteSettings.max_topic_title_length
);
}
@action
onSave() {
return this.chatApi
.updateChannel(this.channel.id, {
name: this.editedName,
slug: this.editedSlug || this.autoGeneratedSlug || this.channel.slug,
})
.then((result) => {
this.channel.title = result.channel.title;
this.args.closeModal();
})
.catch((error) => (this.flash = extractError(error)));
}
@action
onChangeChatChannelName(title) {
this.flash = null;
this.#debouncedGenerateSlug(title);
}
@action
onChangeChatChannelSlug() {
this.flash = null;
this.#debouncedGenerateSlug(this.editedName);
}
#debouncedGenerateSlug(name) {
cancel(this.#generateSlugHandler);
this.autoGeneratedSlug = "";
if (!name) {
return;
}
this.#generateSlugHandler = discourseDebounce(
this,
this.#generateSlug,
name,
300
);
}
// intentionally not showing AJAX error for this, we will autogenerate
// the slug server-side if they leave it blank
#generateSlug(name) {
return ajax("/slugs.json", { type: "POST", data: { name } }).then(
(response) => {
this.autoGeneratedSlug = response.slug;
}
);
}
}

View File

@ -0,0 +1,29 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-move-message-to-channel"
@inline={{@inline}}
@title={{i18n "chat.move_to_channel.title"}}
>
<:body>
{{#if this.selectedMessageCount}}
<p>{{this.instructionsText}}</p>
{{/if}}
<ChatChannelChooser
@class="chat-modal-move-message-to-channel__channel-chooser"
@content={{this.availableChannels}}
@value={{this.destinationChannelId}}
@nameProperty="title"
/>
</:body>
<:footer>
<DButton
@class="btn-primary"
@icon="sign-out-alt"
@disabled={{this.disableMoveButton}}
@action={{this.moveMessages}}
@label="chat.move_to_channel.confirm_move"
/>
<DButton @label="cancel" @class="btn-flat" @action={{@closeModal}} />
</:footer>
</DModal>

View File

@ -1,35 +1,49 @@
import Component from "@ember/component";
import I18n from "I18n";
import { reads } from "@ember/object/computed";
import Component from "@glimmer/component";
import { isBlank } from "@ember/utils";
import { action, computed } from "@ember/object";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
import { tracked } from "@glimmer/tracking";
import { popupAjaxError } from "discourse/lib/ajax-error";
import { htmlSafe } from "@ember/template";
import I18n from "I18n";
export default class MoveToChannelModalInner extends Component {
export default class ChatModalMoveMessageToChannel extends Component {
@service chat;
@service chatApi;
@service router;
@service chatChannelsManager;
tagName = "";
sourceChannel = null;
destinationChannelId = null;
selectedMessageIds = null;
@tracked destinationChannelId;
@reads("selectedMessageIds.length") selectedMessageCount;
get sourceChannel() {
return this.args.model.sourceChannel;
}
get selectedMessageIds() {
return this.args.model.selectedMessageIds;
}
get selectedMessageCount() {
return this.selectedMessageIds?.length;
}
@computed("destinationChannelId")
get disableMoveButton() {
return isBlank(this.destinationChannelId);
}
@computed("chatChannelsManager.publicMessageChannels.[]")
get availableChannels() {
return this.chatChannelsManager.publicMessageChannels.rejectBy(
"id",
this.sourceChannel.id
return (
this.args.model.availableChannels ||
this.chatChannelsManager.publicMessageChannels
).rejectBy("id", this.sourceChannel.id);
}
get instructionsText() {
return htmlSafe(
I18n.t("chat.move_to_channel.instructions", {
channelTitle: this.sourceChannel.escapedTitle,
count: this.selectedMessageCount,
})
);
}
@ -50,14 +64,4 @@ export default class MoveToChannelModalInner extends Component {
})
.catch(popupAjaxError);
}
@computed()
get instructionsText() {
return htmlSafe(
I18n.t("chat.move_to_channel.instructions", {
channelTitle: this.sourceChannel.escapedTitle,
count: this.selectedMessageCount,
})
);
}
}

View File

@ -0,0 +1,8 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-new-message"
@title="chat.new_message_modal.title"
@inline={{@inline}}
>
<Chat::MessageCreator @onClose={{@closeModal}} />
</DModal>

View File

@ -0,0 +1,26 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-thread-settings"
@inline={{@inline}}
@title={{i18n "chat.thread.settings"}}
>
<:body>
<label for="thread-title" class="thread-title-label">
{{i18n "chat.thread.title"}}
</label>
<Input
name="thread-title"
class="chat-modal-thread-settings__title-input"
@type="text"
@value={{this.editedTitle}}
/>
</:body>
<:footer>
<DButton
@class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{this.saveThread}}
@label="save"
/>
</:footer>
</DModal>

View File

@ -4,25 +4,30 @@ import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatThreadSettingsModalInner extends Component {
export default class ChatModalThreadSettings extends Component {
@service chatApi;
@tracked editedTitle = this.args.thread.title || "";
@tracked editedTitle = this.thread.title || "";
@tracked saving = false;
get buttonDisabled() {
return this.saving;
}
get thread() {
return this.args.model;
}
@action
saveThread() {
this.saving = true;
this.chatApi
.editThread(this.args.thread.channel.id, this.args.thread.id, {
.editThread(this.thread.channel.id, this.thread.id, {
title: this.editedTitle,
})
.then(() => {
this.args.thread.title = this.editedTitle;
this.thread.title = this.editedTitle;
this.args.closeModal();
})
.catch(popupAjaxError)

View File

@ -0,0 +1,20 @@
<DModal
@closeModal={{@closeModal}}
class="chat-modal-toggle-channel-status"
@inline={{@inline}}
@title={{i18n this.modalTitle}}
>
<:body>
<p
class="chat-modal-toggle-channel-status__instructions"
>{{this.instructions}}</p>
</:body>
<:footer>
<DButton
@class="btn-primary"
@action={{this.onStatusChange}}
@label={{this.buttonLabel}}
@id="chat-channel-toggle-btn"
/>
</:footer>
</DModal>

View File

@ -1,40 +1,37 @@
import Component from "@ember/component";
import Component from "@glimmer/component";
import { action } from "@ember/object";
import { htmlSafe } from "@ember/template";
import { CHANNEL_STATUSES } from "discourse/plugins/chat/discourse/models/chat-channel";
import I18n from "I18n";
import { action, computed } from "@ember/object";
import { inject as service } from "@ember/service";
import { popupAjaxError } from "discourse/lib/ajax-error";
export default class ChatChannelToggleView extends Component {
@service chat;
export default class ChatModalToggleChannelStatus extends Component {
@service chatApi;
@service router;
tagName = "";
channel = null;
onStatusChange = null;
@computed("channel.isClosed")
get channel() {
return this.args.model;
}
get buttonLabel() {
if (this.channel.isClosed) {
if (this.channel?.isClosed) {
return "chat.channel_settings.open_channel";
} else {
return "chat.channel_settings.close_channel";
}
}
@computed("channel.isClosed")
get instructions() {
if (this.channel.isClosed) {
if (this.channel?.isClosed) {
return htmlSafe(I18n.t("chat.channel_open.instructions"));
} else {
return htmlSafe(I18n.t("chat.channel_close.instructions"));
}
}
@computed("channel.isClosed")
get modalTitle() {
if (this.channel.isClosed) {
if (this.channel?.isClosed) {
return "chat.channel_open.title";
} else {
return "chat.channel_close.title";
@ -42,15 +39,16 @@ export default class ChatChannelToggleView extends Component {
}
@action
changeChannelStatus() {
onStatusChange() {
const status = this.channel.isClosed
? CHANNEL_STATUSES.open
: CHANNEL_STATUSES.closed;
return this.chatApi
.updateChannelStatus(this.channel.id, status)
.finally(() => {
this.onStatusChange?.(this.channel);
.then(() => {
this.args.closeModal();
this.router.transitionTo("chat.channel", ...this.channel.routeModels);
})
.catch(popupAjaxError);
}

View File

@ -1,6 +1,5 @@
import Component from "@glimmer/component";
import { action } from "@ember/object";
import showModal from "discourse/lib/show-modal";
import { clipboardCopyAsync } from "discourse/lib/utilities";
import { getOwner } from "discourse-common/lib/get-owner";
import { isTesting } from "discourse-common/config/environment";
@ -8,10 +7,12 @@ import { popupAjaxError } from "discourse/lib/ajax-error";
import { inject as service } from "@ember/service";
import { bind } from "discourse-common/utils/decorators";
import { tracked } from "@glimmer/tracking";
import ChatModalMoveMessageToChannel from "discourse/plugins/chat/discourse/components/chat/modal/move-message-to-channel";
export default class ChatSelectionManager extends Component {
@service("composer") topicComposer;
@service router;
@service modal;
@service site;
@service("chat-api") api;
@ -42,9 +43,11 @@ export default class ChatSelectionManager extends Component {
@action
openMoveMessageModal() {
showModal("chat-message-move-to-channel-modal").setProperties({
sourceChannel: this.args.pane.channel,
selectedMessageIds: this.args.pane.selectedMessageIds,
this.modal.show(ChatModalMoveMessageToChannel, {
model: {
sourceChannel: this.args.pane.channel,
selectedMessageIds: this.args.pane.selectedMessageIds,
},
});
}

View File

@ -124,6 +124,7 @@ export default class ChatThreadList extends Component {
}
#unsubscribe() {
// TODO (joffrey) In drawer we won't have channel anymore at this point
this.messageBus.unsubscribe(
`/chat/${this.args.channel.id}`,
this.onMessageBus

View File

@ -1,11 +1,11 @@
import Component from "@glimmer/component";
import { NotificationLevels } from "discourse/lib/notification-levels";
import { popupAjaxError } from "discourse/lib/ajax-error";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import UserChatThreadMembership from "discourse/plugins/chat/discourse/models/user-chat-thread-membership";
import { tracked } from "@glimmer/tracking";
import ChatModalThreadSettings from "discourse/plugins/chat/discourse/components/chat/modal/thread-settings";
export default class ChatThreadHeader extends Component {
@service currentUser;
@ -14,6 +14,7 @@ export default class ChatThreadHeader extends Component {
@service chatStateManager;
@service chatHistory;
@service site;
@service modal;
@tracked persistedNotificationLevel = true;
@ -60,8 +61,7 @@ export default class ChatThreadHeader extends Component {
@action
openThreadSettings() {
const controller = showModal("chat-thread-settings-modal");
controller.set("thread", this.args.thread);
this.modal.show(ChatModalThreadSettings, { model: this.args.thread });
}
@action

View File

@ -1,22 +0,0 @@
<DModalBody @title="chat.thread.settings">
<div>
<label for="thread-title" class="thread-title-label">
{{i18n "chat.thread.title"}}
</label>
<Input
name="thread-title"
class="thread-title-input"
@type="text"
@value={{this.editedTitle}}
/>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary"
@disabled={{this.buttonDisabled}}
@action={{action "saveThread"}}
@label="save"
/>
</div>

View File

@ -1,7 +0,0 @@
<DModal
@closeModal={{@closeModal}}
class="chat-new-message-modal"
@title="chat.new_message_modal.title"
>
<Chat::MessageCreator @onClose={{route-action "closeModal"}} />
</DModal>

View File

@ -1,6 +0,0 @@
import Component from "@ember/component";
import { inject as service } from "@ember/service";
export default class ChatNewMessageModal extends Component {
@service chat;
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::ArchiveChannel>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,20 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import ChatModalArchiveChannel from "discourse/plugins/chat/discourse/components/chat/modal/archive-channel";
import Component from "@glimmer/component";
export default class ChatStyleguideChatModalArchiveChannel extends Component {
@service modal;
channel = fabricators.channel();
@action
openModal() {
return this.modal.show(ChatModalArchiveChannel, {
model: {
channel: this.channel,
},
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::ChannelSummary>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,16 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import Component from "@glimmer/component";
import ChatModalChannelSummary from "discourse/plugins/chat/discourse/components/chat/modal/channel-summary";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatStyleguideChatModalChannelSummary extends Component {
@service modal;
@action
openModal() {
return this.modal.show(ChatModalChannelSummary, {
model: { channelId: fabricators.channel().id },
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::CreateChannel>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,13 @@
import Component from "@glimmer/component";
import ChatModalCreateChannel from "discourse/plugins/chat/discourse/components/chat/modal/create-channel";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatStyleguideChatModalCreateChannel extends Component {
@service modal;
@action
openModal() {
return this.modal.show(ChatModalCreateChannel);
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::DeleteChannel>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,18 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import ChatModalDeleteChannel from "discourse/plugins/chat/discourse/components/chat/modal/delete-channel";
import Component from "@glimmer/component";
export default class ChatStyleguideChatModalDeleteChannel extends Component {
@service modal;
channel = fabricators.channel();
@action
openModal() {
return this.modal.show(ChatModalDeleteChannel, {
model: { channel: this.channel },
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::EditChannelDescription>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,18 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import ChatModalEditChannelDescription from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-description";
import Component from "@glimmer/component";
export default class ChatStyleguideChatModalEditChannelDescription extends Component {
@service modal;
channel = fabricators.channel();
@action
openModal() {
return this.modal.show(ChatModalEditChannelDescription, {
model: this.channel,
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::EditChannelName>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,18 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
import Component from "@glimmer/component";
export default class ChatStyleguideChatModalEditChannelName extends Component {
@service modal;
channel = fabricators.channel();
@action
openModal() {
return this.modal.show(ChatModalEditChannelName, {
model: this.channel,
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::MoveMessageToChannel>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,26 @@
import Component from "@glimmer/component";
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
import ChatModalMoveMessageToChannel from "discourse/plugins/chat/discourse/components/chat/modal/move-message-to-channel";
export default class ChatStyleguideChatModalMoveMessageToChannel extends Component {
@service modal;
channel = fabricators.channel();
selectedMessageIds = [fabricators.message({ channel: this.channel })].mapBy(
"id"
);
@action
openModal() {
return this.modal.show(ChatModalMoveMessageToChannel, {
model: {
sourceChannel: this.channel,
selectedMessageIds: [
fabricators.message({ channel: this.channel }),
].mapBy("id"),
},
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::NewMessage>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,13 @@
import Component from "@glimmer/component";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatStyleguideChatModalNewMessage extends Component {
@service modal;
@action
openModal() {
return this.modal.show(ChatModalNewMessage);
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::ThreadSettings>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,16 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import Component from "@glimmer/component";
import ChatModalThreadSettings from "discourse/plugins/chat/discourse/components/modal/chat/thread-settings";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatStyleguideChatModalThreadSettings extends Component {
@service modal;
@action
openModal() {
return this.modal.show(ChatModalThreadSettings, {
model: fabricators.thread(),
});
}
}

View File

@ -0,0 +1,5 @@
<StyleguideExample @title="<Chat::Modal::ToggleChannelStatus>">
<Styleguide::Controls::Row>
<DButton @translatedLabel="Open modal" @action={{this.openModal}} />
</Styleguide::Controls::Row>
</StyleguideExample>

View File

@ -0,0 +1,16 @@
import fabricators from "discourse/plugins/chat/discourse/lib/fabricators";
import Component from "@glimmer/component";
import ChatModalToggleChannelStatus from "discourse/plugins/chat/discourse/components/chat/modal/toggle-channel-status";
import { inject as service } from "@ember/service";
import { action } from "@ember/object";
export default class ChatStyleguideChatModalToggleChannelStatus extends Component {
@service modal;
@action
openModal() {
return this.modal.show(ChatModalToggleChannelStatus, {
model: fabricators.channel(),
});
}
}

View File

@ -3,4 +3,17 @@
<Styleguide::ChatThreadListItem />
<Styleguide::ChatComposerMessageDetails />
<Styleguide::ChatHeaderIcon />
<Styleguide::ChatMessageMentionWarning />
<Styleguide::ChatMessageMentionWarning />
<h2>Modals</h2>
<Styleguide::ChatModalArchiveChannel />
<Styleguide::ChatModalMoveMessageToChannel />
<Styleguide::ChatModalDeleteChannel />
<Styleguide::ChatModalEditChannelDescription />
<Styleguide::ChatModalEditChannelName />
<Styleguide::ChatModalThreadSettings />
<Styleguide::ChatModalCreateChannel />
<Styleguide::ChatModalToggleChannelStatus />
<Styleguide::ChatModalNewMessage />
<Styleguide::ChatModalChannelSummary />

View File

@ -1,8 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class ChatChannelArchiveModalController extends Controller.extend(
ModalFunctionality
) {
chatChannel = null;
}

View File

@ -1,8 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class ChatChannelDeleteModalController extends Controller.extend(
ModalFunctionality
) {
chatChannel = null;
}

View File

@ -1,53 +0,0 @@
import Controller from "@ember/controller";
import { action, computed } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { tracked } from "@glimmer/tracking";
import { inject as service } from "@ember/service";
const DESCRIPTION_MAX_LENGTH = 280;
export default class ChatChannelEditDescriptionController extends Controller.extend(
ModalFunctionality
) {
@service chatApi;
@tracked editedDescription = this.model.description || "";
@computed("model.description", "editedDescription")
get isSaveDisabled() {
return (
this.model.description === this.editedDescription ||
this.editedDescription?.length > DESCRIPTION_MAX_LENGTH
);
}
get descriptionMaxLength() {
return DESCRIPTION_MAX_LENGTH;
}
onClose() {
this.clearFlash();
}
@action
onSaveChatChannelDescription() {
return this.chatApi
.updateChannel(this.model.id, {
description: this.editedDescription,
})
.then((result) => {
this.model.set("description", result.channel.description);
this.send("closeModal");
})
.catch((event) => {
if (event.jqXHR?.responseJSON?.errors) {
this.flash(event.jqXHR.responseJSON.errors.join("\n"), "error");
}
});
}
@action
onChangeChatChannelDescription(description) {
this.clearFlash();
this.editedDescription = description;
}
}

View File

@ -1,92 +0,0 @@
import Controller from "@ember/controller";
import discourseDebounce from "discourse-common/lib/debounce";
import { ajax } from "discourse/lib/ajax";
import { cancel } from "@ember/runloop";
import { action, computed } from "@ember/object";
import { flashAjaxError } from "discourse/lib/ajax-error";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { inject as service } from "@ember/service";
export default class ChatChannelEditTitleController extends Controller.extend(
ModalFunctionality
) {
@service chatApi;
editedName = "";
editedSlug = "";
autoGeneratedSlug = "";
@computed("model.title", "editedName", "editedSlug")
get isSaveDisabled() {
return (
(this.model.title === this.editedName &&
this.model.slug === this.editedSlug) ||
this.editedName?.length > this.siteSettings.max_topic_title_length
);
}
onShow() {
this.setProperties({
editedName: this.model.title,
editedSlug: this.model.slug,
});
}
onClose() {
this.setProperties({
editedName: "",
editedSlug: "",
});
this.clearFlash();
}
@action
onSaveChatChannelName() {
return this.chatApi
.updateChannel(this.model.id, {
name: this.editedName,
slug: this.editedSlug || this.autoGeneratedSlug || this.model.slug,
})
.then((result) => {
this.model.title = result.channel.title;
this.send("closeModal");
})
.catch(flashAjaxError(this));
}
@action
onChangeChatChannelName(title) {
this.clearFlash();
this._debouncedGenerateSlug(title);
}
@action
onChangeChatChannelSlug() {
this.clearFlash();
this._debouncedGenerateSlug(this.editedName);
}
_clearAutoGeneratedSlug() {
this.set("autoGeneratedSlug", "");
}
_debouncedGenerateSlug(name) {
cancel(this.generateSlugHandler);
this._clearAutoGeneratedSlug();
if (!name) {
return;
}
this.generateSlugHandler = discourseDebounce(
this,
this._generateSlug,
name,
300
);
}
// intentionally not showing AJAX error for this, we will autogenerate
// the slug server-side if they leave it blank
_generateSlug(name) {
ajax("/slugs.json", { type: "POST", data: { name } }).then((response) => {
this.set("autoGeneratedSlug", response.slug);
});
}
}

View File

@ -1,19 +1,25 @@
import Controller from "@ember/controller";
import { action } from "@ember/object";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import showModal from "discourse/lib/show-modal";
import { inject as service } from "@ember/service";
import ChatModalEditChannelDescription from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-description";
import ChatModalEditChannelName from "discourse/plugins/chat/discourse/components/chat/modal/edit-channel-name";
export default class ChatChannelInfoAboutController extends Controller.extend(
ModalFunctionality
) {
@service modal;
@action
onEditChatChannelName() {
showModal("chat-channel-edit-name-slug", { model: this.model });
return this.modal.show(ChatModalEditChannelName, {
model: this.model,
});
}
@action
onEditChatChannelDescription() {
showModal("chat-channel-edit-description", {
return this.modal.show(ChatModalEditChannelDescription, {
model: this.model,
});
}

View File

@ -1,12 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
export default class ChatChannelSelectorModalController extends Controller.extend(
ModalFunctionality
) {
@action
closeSelf() {
this.send("closeModal");
}
}

View File

@ -1,19 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
import { action } from "@ember/object";
import { inject as service } from "@ember/service";
export default class ChatChannelToggleController extends Controller.extend(
ModalFunctionality
) {
@service chat;
@service router;
chatChannel = null;
@action
channelStatusChanged(channel) {
this.send("closeModal");
this.router.transitionTo("chat.channel", ...channel.routeModels);
}
}

View File

@ -1,8 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class ChatMessageMoveToChannelModalController extends Controller.extend(
ModalFunctionality
) {
chatChannel = null;
}

View File

@ -1,8 +0,0 @@
import Controller from "@ember/controller";
import ModalFunctionality from "discourse/mixins/modal-functionality";
export default class ChatThreadSettingsModalController extends Controller.extend(
ModalFunctionality
) {
thread = null;
}

View File

@ -1,6 +1,6 @@
import { withPluginApi } from "discourse/lib/plugin-api";
import { PLATFORM_KEY_MODIFIER } from "discourse/lib/keyboard-shortcuts";
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
export default {
name: "chat-keyboard-shortcuts",
@ -22,10 +22,10 @@ export default {
const chatChannelsManager = container.lookup(
"service:chat-channels-manager"
);
const openChannelSelector = (e) => {
const openQuickChannelSelector = (e) => {
e.preventDefault();
e.stopPropagation();
modal.show(ChatNewMessageModal);
modal.show(ChatModalNewMessage);
};
const handleMoveUpShortcut = (e) => {
@ -126,7 +126,7 @@ export default {
withPluginApi("0.12.1", (api) => {
api.addKeyboardShortcut(
`${PLATFORM_KEY_MODIFIER}+k`,
openChannelSelector,
openQuickChannelSelector,
{
global: true,
help: {

View File

@ -9,7 +9,7 @@ import { emojiUnescape } from "discourse/lib/text";
import { decorateUsername } from "discourse/helpers/decorate-username-selector";
import { until } from "discourse/lib/formatter";
import { inject as service } from "@ember/service";
import ChatNewMessageModal from "discourse/plugins/chat/discourse/components/modal/chat-new-message";
import ChatModalNewMessage from "discourse/plugins/chat/discourse/components/chat/modal/new-message";
export default {
name: "chat-sidebar",
@ -379,7 +379,7 @@ export default {
id: "startDm",
title: I18n.t("chat.direct_messages.new"),
action: () => {
this.modal.show(ChatNewMessageModal);
this.modal.show(ChatModalNewMessage);
},
},
];

View File

@ -68,6 +68,7 @@ function channelFabricator(args = {}) {
description: args.description,
chatable: args.chatable || categoryFabricator(),
status: CHANNEL_STATUSES.open,
slug: args.chatable?.slug || "general",
},
args
)

View File

@ -476,7 +476,8 @@ export default class ChatApi extends Service {
*
* @param {number} channelId - The ID of the channel.
* @param {Array<number>} userIds - The IDs of the users to invite.
* @param {Array<number>} [messageId] - The ID of a message to highlight when opening the notification.
* @param {object} options
* @param {number} options.chat_message_id - A message ID to display in the invite.
*/
invite(channelId, userIds, options = {}) {
return ajax(`/chat/${channelId}/invite`, {
@ -485,6 +486,17 @@ export default class ChatApi extends Service {
});
}
/**
* Summarize a channel.
*
* @param {number} channelId - The ID of the channel to summarize.
* @param {object} options
* @param {number} options.since - Number of hours ago the summary should start (1, 3, 6, 12, 24, 72, 168).
*/
summarize(channelId, options = {}) {
return this.#getRequest(`/channels/${channelId}/summarize`, options);
}
get #basePath() {
return "/chat/api";
}

View File

@ -1,4 +0,0 @@
<ChannelSummary
@channelId={{this.channelId}}
@closeModal={{route-action "closeModal"}}
/>

View File

@ -1,4 +0,0 @@
<ChatChannelArchiveModalInner
@chatChannel={{this.chatChannel}}
@closeModal={{route-action "closeModal"}}
/>

View File

@ -1,4 +0,0 @@
<ChatChannelDeleteModalInner
@chatChannel={{this.chatChannel}}
@closeModal={{action "closeModal"}}
/>

View File

@ -1,32 +0,0 @@
<DModalBody @title="chat.channel_edit_description_modal.title">
<span class="chat-channel-edit-description-modal__description">
{{i18n "chat.channel_edit_description_modal.description"}}
</span>
<CharCounter
@value={{this.editedDescription}}
@max={{this.descriptionMaxLength}}
>
<textarea
{{on
"input"
(action this.onChangeChatChannelDescription value="target.value")
}}
class="chat-channel-edit-description-modal__description-input"
placeholder={{i18n
"chat.channel_edit_description_modal.input_placeholder"
}}
>
{{this.editedDescription}}
</textarea>
</CharCounter>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary create"
@action={{action "onSaveChatChannelDescription"}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{route-action "closeModal"}} />
</div>

View File

@ -1,45 +0,0 @@
<DModalBody @title="chat.channel_edit_name_slug_modal.title">
<div class="edit-channel-control">
<label for="channel-name" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.name"}}
</label>
<Input
name="channel-name"
class="chat-channel-edit-name-slug-modal__name-input"
placeholder={{i18n "chat.channel_edit_name_slug_modal.input_placeholder"}}
@type="text"
@value={{this.editedName}}
{{on "input" (action "onChangeChatChannelName" value="target.value")}}
/>
</div>
<div class="edit-channel-control">
<label for="channel-slug" class="edit-channel-label">
{{i18n "chat.channel_edit_name_slug_modal.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>{{i18n
"chat.channel_edit_name_slug_modal.slug_description"
}}</DTooltip>
</span>
</label>
<Input
name="channel-slug"
class="chat-channel-edit-name-slug-modal__slug-input"
placeholder={{this.autoGeneratedSlug}}
{{on "input" (action "onChangeChatChannelSlug" value="target.value")}}
@type="text"
@value={{this.editedSlug}}
/>
</div>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary create"
@action={{action "onSaveChatChannelName"}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{route-action "closeModal"}} />
</div>

View File

@ -1,22 +0,0 @@
<DModalBody @title="chat.channel_edit_name_modal.title">
<input
class="chat-channel-edit-name-modal__name-input"
placeholder={{i18n "chat.channel_edit_name_modal.input_placeholder"}}
{{on "input" (action "onChangeChatChannelName" value="target.value")}}
type="text"
value={{this.model.title}}
/>
<span class="chat-channel-edit-name-modal__description">
{{i18n "chat.channel_edit_name_modal.description"}}
</span>
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary create"
@action={{action "onSaveChatChannelName"}}
@label="save"
@disabled={{this.isSaveDisabled}}
/>
<DModalCancel @close={{route-action "closeModal"}} />
</div>

View File

@ -1 +0,0 @@
<ChatChannelSelectorModalInner @close={{this.closeSelf}} />

View File

@ -1,5 +0,0 @@
<ChatChannelToggleView
@channel={{this.chatChannel}}
@closeModal={{action "closeModal"}}
@onStatusChange={{action "channelStatusChanged"}}
/>

View File

@ -1,5 +0,0 @@
<ChatMessageMoveToChannelModalInner
@sourceChannel={{this.sourceChannel}}
@selectedMessageIds={{this.selectedMessageIds}}
@closeModal={{action "closeModal"}}
/>

View File

@ -1,4 +0,0 @@
<Chat::Thread::SettingsModalInner
@thread={{this.thread}}
@closeModal={{route-action "closeModal"}}
/>

View File

@ -1,115 +0,0 @@
<DModalBody @title="chat.create_channel.title">
<div class="create-channel__control -name">
<label for="channel-name" class="create-channel__label">
{{i18n "chat.create_channel.name"}}
</label>
<Input
name="channel-name"
class="create-channel__input"
@type="text"
@value={{this.name}}
{{on "input" (action "onNameChange" value="target.value")}}
/>
</div>
<div class="create-channel__control -slug">
<label for="channel-slug" class="create-channel__label">
{{i18n "chat.create_channel.slug"}}&nbsp;
<span>
{{d-icon "info-circle"}}
<DTooltip>
{{i18n "chat.channel_edit_name_slug_modal.slug_description"}}
</DTooltip>
</span>
</label>
<Input
name="channel-slug"
class="create-channel__input"
@type="text"
@value={{this.slug}}
placeholder={{this.autoGeneratedSlug}}
/>
</div>
<div class="create-channel__control -description">
<label for="channel-description" class="create-channel__label">
{{i18n "chat.create_channel.description"}}
</label>
<Input
name="channel-description"
class="create-channel__input"
@type="textarea"
@value={{this.description}}
/>
</div>
<div class="create-channel__control">
<label class="create-channel__label">
{{i18n "chat.create_channel.choose_category.label"}}
</label>
<CategoryChooser
@value={{this.categoryId}}
@onChange={{action "onCategoryChange"}}
@options={{hash none="chat.create_channel.choose_category.none"}}
/>
{{#if this.categoryPermissionsHint}}
<div
class={{concat-class
"create-channel__hint"
(if this.loadingPermissionHint "loading-permissions")
}}
>
{{this.categoryPermissionsHint}}
</div>
{{/if}}
</div>
{{#if this.autoJoinAvailable}}
<div class="create-channel__control -auto-join">
<label class="create-channel__label">
<Input @type="checkbox" @checked={{this.autoJoinUsers}} />
<div class="auto-join-channel">
<span class="create-channel__label-title">
{{i18n "chat.settings.auto_join_users_label"}}
</span>
<p class="create-channel__label-description">
{{#if this.categoryName}}
{{i18n
"chat.settings.auto_join_users_info"
category=this.categoryName
}}
{{else}}
{{i18n "chat.settings.auto_join_users_info_no_category"}}
{{/if}}
</p>
</div>
</label>
</div>
{{/if}}
{{#if this.threadingAvailable}}
<div class="create-channel__control -threading-toggle">
<label class="create-channel__label">
<Input @type="checkbox" @checked={{this.threadingEnabled}} />
<div class="threading-channel">
<span class="create-channel__label-title">
{{i18n "chat.create_channel.threading.label"}}
</span>
<p class="create-channel__label-description">
{{i18n "chat.settings.channel_threading_description"}}
</p>
</div>
</label>
</div>
{{/if}}
</DModalBody>
<div class="modal-footer">
<DButton
@class="btn-primary create"
@action={{action "create"}}
@label="chat.create_channel.create"
@disabled={{this.createDisabled}}
/>
</div>

View File

@ -16,19 +16,6 @@ html.ios-device.keyboard-visible body #main-outlet .full-page-chat {
padding-bottom: 0.2rem;
}
.chat-message-move-to-channel-modal-modal {
.modal-inner-container {
.chat-move-message-channel-chooser {
width: 100%;
.category-chat-badge {
.d-icon {
color: inherit;
}
}
}
}
}
.uppy-is-drag-over .chat-composer .drop-a-file {
display: flex;
position: absolute;
@ -315,38 +302,6 @@ body.has-full-page-chat {
}
}
.chat-channel-archive-modal-inner {
.chat-to-topic-selector {
width: 500px;
height: 300px;
}
.radios {
margin-bottom: 10px;
display: flex;
flex-direction: row;
.radio-label {
margin-right: 10px;
}
}
details {
margin-bottom: 9px;
}
input[type="text"],
.select-kit.combo-box.category-chooser {
width: 100%;
}
}
.chat-channel-archive-modal-inner {
.chat-to-topic-selector {
width: auto;
}
}
.user-preferences .chat-setting .controls {
margin-bottom: 0;
}

View File

@ -139,25 +139,3 @@ input.channel-members-view__search-input {
padding: 0.5rem 0;
color: var(--primary-medium);
}
// Channel info edit description modal
.chat-channel-edit-description-modal {
.exceeded-word-count {
.chat-channel-edit-description-modal__description-input {
outline: 1px solid var(--danger);
border: 1px solid var(--danger);
}
}
}
.chat-channel-edit-description-modal__description-input {
display: flex;
margin: 0;
min-height: 200px;
}
.chat-channel-edit-description-modal__description {
display: flex;
padding-bottom: 0.75rem;
color: var(--primary-medium);
}

View File

@ -0,0 +1,25 @@
.chat-modal-archive-channel {
.chat-to-topic-selector {
width: auto;
height: 300px;
}
.radios {
margin-bottom: 10px;
display: flex;
flex-direction: row;
.radio-label {
margin-right: 10px;
}
}
details {
margin-bottom: 9px;
}
input[type="text"],
.select-kit.combo-box.category-chooser {
width: 100%;
}
}

View File

@ -1,4 +1,4 @@
.channel-summary-modal {
.chat-modal-channel-summary {
.summarization-since,
.summary-area {
margin: 10px 0 10px 0;

View File

@ -1,4 +1,4 @@
.create-channel-modal {
.chat-modal-create-channel {
.modal-inner-container {
width: 500px;
}
@ -9,7 +9,7 @@
}
.select-kit.combo-box,
.create-channel__input,
&__input,
#choose-topic-title {
width: 100%;
margin-bottom: 0;
@ -22,18 +22,18 @@
}
}
.create-channel__hint {
&__hint {
font-size: var(--font-down-1);
padding-top: 0.25rem;
color: var(--secondary-low);
}
.create-channel__control,
&__control,
.edit-channel-control {
margin-bottom: 1rem;
}
.create-channel__label-description {
&__label-description {
margin: 0;
padding-top: 0.25rem;
color: var(--secondary-low);

View File

@ -0,0 +1,20 @@
.chat-modal-edit-channel-description {
.exceeded-word-count {
.chat-modal-edit-channel-description__description-input {
outline: 1px solid var(--danger);
border: 1px solid var(--danger);
}
}
&__description-input {
display: flex;
margin: 0;
min-height: 200px;
}
&__description {
display: flex;
padding-bottom: 0.75rem;
color: var(--primary-medium);
}
}

View File

@ -0,0 +1,10 @@
.chat-modal-move-message-to-channel {
&__channel-chooser {
width: 100%;
.category-chat-badge {
.d-icon {
color: inherit;
}
}
}
}

View File

@ -1,4 +1,4 @@
.chat-new-message-modal {
.chat-modal-new-message {
& + .modal-backdrop {
opacity: 1;
background: transparent;

View File

@ -41,7 +41,6 @@
@import "chat-upload-drop-zone";
@import "chat-transcript";
@import "core-extensions";
@import "create-channel-modal";
@import "dc-filter-input";
@import "full-page-chat-header";
@import "incoming-chat-webhooks";
@ -53,9 +52,14 @@
@import "chat-thread-list-header";
@import "chat-thread-unread-indicator";
@import "chat-thread-participants";
@import "channel-summary-modal";
@import "chat-message-mention-warning";
@import "chat-message-error";
@import "chat-new-message-modal";
@import "chat-message-creator";
@import "chat-user-avatar";
@import "chat-modal-new-message";
@import "chat-modal-archive-channel";
@import "chat-modal-edit-channel-description";
@import "chat-modal-create-channel";
@import "chat-modal-create-channel";
@import "chat-modal-channel-summary";
@import "chat-modal-move-message-to-channel";

View File

@ -0,0 +1,9 @@
.modal-chat-thread-settings {
.modal-inner-container {
width: 98%;
}
&__title-input {
width: 100%;
}
}

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