1546 lines
41 KiB
JavaScript
1546 lines
41 KiB
JavaScript
import Composer, { SAVE_ICONS, SAVE_LABELS } from "discourse/models/composer";
|
|
import Controller, { inject as controller } from "@ember/controller";
|
|
import EmberObject, { action, computed } from "@ember/object";
|
|
import { alias, and, or, reads } from "@ember/object/computed";
|
|
import {
|
|
authorizesOneOrMoreExtensions,
|
|
uploadIcon,
|
|
} from "discourse/lib/uploads";
|
|
import { cancel, scheduleOnce } from "@ember/runloop";
|
|
import {
|
|
cannotPostAgain,
|
|
durationTextFromSeconds,
|
|
} from "discourse/helpers/slow-mode";
|
|
import discourseComputed, {
|
|
observes,
|
|
on,
|
|
} from "discourse-common/utils/decorators";
|
|
import DiscourseURL from "discourse/lib/url";
|
|
import Draft from "discourse/models/draft";
|
|
import I18n from "I18n";
|
|
import { Promise } from "rsvp";
|
|
import { buildQuote } from "discourse/lib/quote";
|
|
import deprecated from "discourse-common/lib/deprecated";
|
|
import discourseDebounce from "discourse-common/lib/debounce";
|
|
import { emojiUnescape } from "discourse/lib/text";
|
|
import { escapeExpression, modKeysPressed } from "discourse/lib/utilities";
|
|
import { getOwner } from "discourse-common/lib/get-owner";
|
|
import getURL from "discourse-common/lib/get-url";
|
|
import { isEmpty } from "@ember/utils";
|
|
import { isTesting } from "discourse-common/config/environment";
|
|
import { inject as service } from "@ember/service";
|
|
import { shortDate } from "discourse/lib/formatter";
|
|
import showModal from "discourse/lib/show-modal";
|
|
|
|
async function loadDraft(store, opts = {}) {
|
|
let { draft, draftKey, draftSequence } = opts;
|
|
|
|
try {
|
|
if (draft && typeof draft === "string") {
|
|
draft = JSON.parse(draft);
|
|
}
|
|
} catch (error) {
|
|
draft = null;
|
|
Draft.clear(draftKey, draftSequence);
|
|
}
|
|
|
|
if (!draft?.title && !draft?.reply) {
|
|
return;
|
|
}
|
|
|
|
let attrs = {
|
|
draftKey,
|
|
draftSequence,
|
|
draft: true,
|
|
composerState: Composer.DRAFT,
|
|
topic: opts.topic,
|
|
};
|
|
|
|
Composer.serializedFieldsForDraft().forEach((f) => {
|
|
attrs[f] = draft[f] || opts[f];
|
|
});
|
|
|
|
const composer = store.createRecord("composer");
|
|
await composer.open(attrs);
|
|
|
|
return composer;
|
|
}
|
|
|
|
const _popupMenuOptionsCallbacks = [];
|
|
|
|
let _checkDraftPopup = !isTesting();
|
|
|
|
export function toggleCheckDraftPopup(enabled) {
|
|
_checkDraftPopup = enabled;
|
|
}
|
|
|
|
export function clearPopupMenuOptionsCallback() {
|
|
_popupMenuOptionsCallbacks.length = 0;
|
|
}
|
|
|
|
export function addPopupMenuOptionsCallback(callback) {
|
|
_popupMenuOptionsCallbacks.push(callback);
|
|
}
|
|
|
|
export default Controller.extend({
|
|
topicController: controller("topic"),
|
|
router: service(),
|
|
dialog: service(),
|
|
|
|
checkedMessages: false,
|
|
messageCount: null,
|
|
showEditReason: false,
|
|
editReason: null,
|
|
scopedCategoryId: null,
|
|
prioritizedCategoryId: null,
|
|
lastValidatedAt: null,
|
|
isUploading: false,
|
|
isProcessingUpload: false,
|
|
topic: null,
|
|
linkLookup: null,
|
|
showPreview: true,
|
|
composerHeight: null,
|
|
forcePreview: and("site.mobileView", "showPreview"),
|
|
whisperOrUnlistTopic: or("isWhispering", "model.unlistTopic"),
|
|
categories: alias("site.categoriesList"),
|
|
|
|
@on("init")
|
|
_setupPreview() {
|
|
const val = this.site.mobileView
|
|
? false
|
|
: this.keyValueStore.get("composer.showPreview") || "true";
|
|
this.set("showPreview", val === "true");
|
|
},
|
|
|
|
@computed(
|
|
"model.loading",
|
|
"isUploading",
|
|
"isProcessingUpload",
|
|
"_disableSubmit"
|
|
)
|
|
get disableSubmit() {
|
|
return (
|
|
this.model?.loading ||
|
|
this.isUploading ||
|
|
this.isProcessingUpload ||
|
|
this._disableSubmit
|
|
);
|
|
},
|
|
|
|
set disableSubmit(value) {
|
|
return this.set("_disableSubmit", value);
|
|
},
|
|
|
|
@discourseComputed("showPreview")
|
|
toggleText(showPreview) {
|
|
return showPreview
|
|
? I18n.t("composer.hide_preview")
|
|
: I18n.t("composer.show_preview");
|
|
},
|
|
|
|
@observes("showPreview")
|
|
showPreviewChanged() {
|
|
if (!this.site.mobileView) {
|
|
this.keyValueStore.set({
|
|
key: "composer.showPreview",
|
|
value: this.showPreview,
|
|
});
|
|
}
|
|
},
|
|
|
|
@discourseComputed(
|
|
"model.replyingToTopic",
|
|
"model.creatingPrivateMessage",
|
|
"model.targetRecipients",
|
|
"model.composeState"
|
|
)
|
|
focusTarget(replyingToTopic, creatingPM, usernames, composeState) {
|
|
// Focus on usernames if it's blank or if it's just you
|
|
usernames = usernames || "";
|
|
if (
|
|
(creatingPM && usernames.length === 0) ||
|
|
usernames === this.currentUser.username
|
|
) {
|
|
return "usernames";
|
|
}
|
|
|
|
if (replyingToTopic) {
|
|
return "reply";
|
|
}
|
|
|
|
if (composeState === Composer.FULLSCREEN) {
|
|
return "editor";
|
|
}
|
|
|
|
return "title";
|
|
},
|
|
|
|
showToolbar: computed({
|
|
get() {
|
|
const keyValueStore = getOwner(this).lookup("service:key-value-store");
|
|
const storedVal = keyValueStore.get("toolbar-enabled");
|
|
if (this._toolbarEnabled === undefined && storedVal === undefined) {
|
|
// iPhone 6 is 375, anything narrower and toolbar should
|
|
// be default disabled.
|
|
// That said we should remember the state
|
|
this._toolbarEnabled =
|
|
window.innerWidth > 370 && !this.capabilities.isAndroid;
|
|
}
|
|
return this._toolbarEnabled || storedVal === "true";
|
|
},
|
|
set(key, val) {
|
|
const keyValueStore = getOwner(this).lookup("service:key-value-store");
|
|
this._toolbarEnabled = val;
|
|
keyValueStore.set({
|
|
key: "toolbar-enabled",
|
|
value: val ? "true" : "false",
|
|
});
|
|
return val;
|
|
},
|
|
}),
|
|
|
|
topicModel: alias("topicController.model"),
|
|
|
|
@discourseComputed("model.canEditTitle", "model.creatingPrivateMessage")
|
|
canEditTags(canEditTitle, creatingPrivateMessage) {
|
|
if (creatingPrivateMessage && this.site.mobileView) {
|
|
return false;
|
|
}
|
|
|
|
const isPrivateMessage =
|
|
creatingPrivateMessage || this.get("model.topic.isPrivateMessage");
|
|
|
|
return (
|
|
canEditTitle &&
|
|
this.site.can_tag_topics &&
|
|
(!isPrivateMessage || this.site.can_tag_pms)
|
|
);
|
|
},
|
|
|
|
@discourseComputed("model.editingPost", "model.topic.details.can_edit")
|
|
disableCategoryChooser(editingPost, canEditTopic) {
|
|
return editingPost && !canEditTopic;
|
|
},
|
|
|
|
@discourseComputed("model.editingPost", "model.topic.canEditTags")
|
|
disableTagsChooser(editingPost, canEditTags) {
|
|
return editingPost && !canEditTags;
|
|
},
|
|
|
|
isStaffUser: reads("currentUser.staff"),
|
|
whisperer: reads("currentUser.whisperer"),
|
|
|
|
canUnlistTopic: and("model.creatingTopic", "isStaffUser"),
|
|
|
|
@discourseComputed("canWhisper", "replyingToWhisper")
|
|
showWhisperToggle(canWhisper, replyingToWhisper) {
|
|
return canWhisper && !replyingToWhisper;
|
|
},
|
|
|
|
@discourseComputed("model.post")
|
|
replyingToWhisper(repliedToPost) {
|
|
return (
|
|
repliedToPost && repliedToPost.post_type === this.site.post_types.whisper
|
|
);
|
|
},
|
|
|
|
isWhispering: or("replyingToWhisper", "model.whisper"),
|
|
|
|
@discourseComputed("model.action", "isWhispering", "model.privateMessage")
|
|
saveIcon(modelAction, isWhispering, privateMessage) {
|
|
if (isWhispering) {
|
|
return "far-eye-slash";
|
|
}
|
|
if (privateMessage && modelAction === Composer.REPLY) {
|
|
return "envelope";
|
|
}
|
|
|
|
return SAVE_ICONS[modelAction];
|
|
},
|
|
|
|
// Note we update when some other attributes like tag/category change to allow
|
|
// text customizations to use those.
|
|
@discourseComputed(
|
|
"model.action",
|
|
"isWhispering",
|
|
"model.editConflict",
|
|
"model.privateMessage",
|
|
"model.tags",
|
|
"model.category"
|
|
)
|
|
saveLabel(modelAction, isWhispering, editConflict, privateMessage) {
|
|
let result = this.model.customizationFor("saveLabel");
|
|
if (result) {
|
|
return result;
|
|
}
|
|
|
|
if (editConflict) {
|
|
return "composer.overwrite_edit";
|
|
} else if (isWhispering) {
|
|
return "composer.create_whisper";
|
|
} else if (privateMessage && modelAction === Composer.REPLY) {
|
|
return "composer.create_pm";
|
|
}
|
|
|
|
return SAVE_LABELS[modelAction];
|
|
},
|
|
|
|
@discourseComputed("whisperer", "model.action")
|
|
canWhisper(whisperer, modelAction) {
|
|
return (
|
|
this.siteSettings.enable_whispers &&
|
|
Composer.REPLY === modelAction &&
|
|
whisperer
|
|
);
|
|
},
|
|
|
|
_setupPopupMenuOption(callback) {
|
|
let option = callback(this);
|
|
if (typeof option === "undefined") {
|
|
return null;
|
|
}
|
|
|
|
if (typeof option.condition === "undefined") {
|
|
option.condition = true;
|
|
} else if (typeof option.condition === "boolean") {
|
|
// uses existing value
|
|
} else {
|
|
option.condition = this.get(option.condition);
|
|
}
|
|
|
|
return option;
|
|
},
|
|
|
|
@discourseComputed("model.requiredCategoryMissing", "model.replyLength")
|
|
disableTextarea(requiredCategoryMissing, replyLength) {
|
|
return requiredCategoryMissing && replyLength === 0;
|
|
},
|
|
|
|
@discourseComputed("model.composeState", "model.creatingTopic", "model.post")
|
|
popupMenuOptions(composeState) {
|
|
if (composeState === "open" || composeState === "fullscreen") {
|
|
const options = [];
|
|
|
|
options.push(
|
|
this._setupPopupMenuOption(() => {
|
|
return {
|
|
action: "toggleInvisible",
|
|
icon: "far-eye-slash",
|
|
label: "composer.toggle_unlisted",
|
|
condition: "canUnlistTopic",
|
|
};
|
|
})
|
|
);
|
|
|
|
if (this.capabilities.touch) {
|
|
options.push(
|
|
this._setupPopupMenuOption(() => {
|
|
return {
|
|
action: "applyFormatCode",
|
|
icon: "code",
|
|
label: "composer.code_title",
|
|
};
|
|
})
|
|
);
|
|
|
|
options.push(
|
|
this._setupPopupMenuOption(() => {
|
|
return {
|
|
action: "applyUnorderedList",
|
|
icon: "list-ul",
|
|
label: "composer.ulist_title",
|
|
};
|
|
})
|
|
);
|
|
|
|
options.push(
|
|
this._setupPopupMenuOption(() => {
|
|
return {
|
|
action: "applyOrderedList",
|
|
icon: "list-ol",
|
|
label: "composer.olist_title",
|
|
};
|
|
})
|
|
);
|
|
}
|
|
|
|
options.push(
|
|
this._setupPopupMenuOption(() => {
|
|
return {
|
|
action: "toggleWhisper",
|
|
icon: "far-eye-slash",
|
|
label: "composer.toggle_whisper",
|
|
condition: "showWhisperToggle",
|
|
};
|
|
})
|
|
);
|
|
|
|
return options.concat(
|
|
_popupMenuOptionsCallbacks
|
|
.map((callback) => this._setupPopupMenuOption(callback))
|
|
.filter((o) => o)
|
|
);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("model.creatingPrivateMessage", "model.targetRecipients")
|
|
showWarning(creatingPrivateMessage, usernames) {
|
|
if (!this.get("currentUser.staff")) {
|
|
return false;
|
|
}
|
|
|
|
const hasTargetGroups = this.get("model.hasTargetGroups");
|
|
|
|
// We need exactly one user to issue a warning
|
|
if (
|
|
isEmpty(usernames) ||
|
|
usernames.split(",").length !== 1 ||
|
|
hasTargetGroups
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
return creatingPrivateMessage;
|
|
},
|
|
|
|
@discourseComputed("model.topic.title")
|
|
draftTitle(topicTitle) {
|
|
return emojiUnescape(escapeExpression(topicTitle));
|
|
},
|
|
|
|
@discourseComputed
|
|
allowUpload() {
|
|
return authorizesOneOrMoreExtensions(
|
|
this.currentUser.staff,
|
|
this.siteSettings
|
|
);
|
|
},
|
|
|
|
@discourseComputed()
|
|
uploadIcon() {
|
|
return uploadIcon(this.currentUser.staff, this.siteSettings);
|
|
},
|
|
|
|
// Use this to open the composer when you are not sure whether it is
|
|
// already open and whether it already has a draft being worked on. Supports
|
|
// options to append text once the composer is open if required.
|
|
//
|
|
// opts:
|
|
//
|
|
// - topic: if this is present, the composer will be opened with the reply
|
|
// action and the current topic key and draft sequence
|
|
// - fallbackToNewTopic: if true, and there is no draft and no topic,
|
|
// the composer will be opened with the create_topic action and a new
|
|
// topic draft key
|
|
// - insertText: the text to append to the composer once it is opened
|
|
// - openOpts: this object will be passed to this.open if fallbackToNewTopic is
|
|
// true or topic is provided
|
|
@action
|
|
async focusComposer(opts = {}) {
|
|
await this._openComposerForFocus(opts);
|
|
this._focusAndInsertText(opts.insertText);
|
|
},
|
|
|
|
async _openComposerForFocus(opts) {
|
|
if (this.get("model.viewOpen")) {
|
|
return;
|
|
}
|
|
|
|
const opened = this.openIfDraft();
|
|
if (opened) {
|
|
return;
|
|
}
|
|
|
|
if (opts.topic) {
|
|
return await this.open({
|
|
action: Composer.REPLY,
|
|
draftKey: opts.topic.get("draft_key"),
|
|
draftSequence: opts.topic.get("draft_sequence"),
|
|
topic: opts.topic,
|
|
...(opts.openOpts || {}),
|
|
});
|
|
}
|
|
|
|
if (opts.fallbackToNewTopic) {
|
|
return await this.open({
|
|
action: Composer.CREATE_TOPIC,
|
|
draftKey: Composer.NEW_TOPIC_KEY,
|
|
...(opts.openOpts || {}),
|
|
});
|
|
}
|
|
},
|
|
|
|
_focusAndInsertText(insertText) {
|
|
scheduleOnce("afterRender", () => {
|
|
document.querySelector("textarea.d-editor-input")?.focus();
|
|
|
|
if (insertText) {
|
|
this.model.appendText(insertText, null, { new_line: true });
|
|
}
|
|
});
|
|
},
|
|
|
|
@action
|
|
openIfDraft(event) {
|
|
if (!this.get("model.viewDraft")) {
|
|
return false;
|
|
}
|
|
|
|
// when called from shortcut, ensure we don't propagate the key to
|
|
// the composer input title
|
|
if (event) {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
this.set("model.composeState", Composer.OPEN);
|
|
|
|
document.documentElement.style.setProperty(
|
|
"--composer-height",
|
|
this.get("model.composerHeight")
|
|
);
|
|
|
|
return true;
|
|
},
|
|
|
|
@action
|
|
removeFullScreenExitPrompt() {
|
|
this.set("model.showFullScreenExitPrompt", false);
|
|
},
|
|
|
|
@action
|
|
async cancel(event) {
|
|
event?.preventDefault();
|
|
await this.cancelComposer();
|
|
},
|
|
|
|
@action
|
|
cancelUpload(event) {
|
|
event?.preventDefault();
|
|
this.set("model.uploadCancelled", true);
|
|
},
|
|
|
|
@action
|
|
togglePreview(event) {
|
|
event?.preventDefault();
|
|
this.toggleProperty("showPreview");
|
|
},
|
|
|
|
@action
|
|
viewNewReply(event) {
|
|
if (event && modKeysPressed(event).length > 0) {
|
|
return false;
|
|
}
|
|
event?.preventDefault();
|
|
DiscourseURL.routeTo(this.get("model.createdPost.url"));
|
|
this.close();
|
|
},
|
|
|
|
actions: {
|
|
closeComposer() {
|
|
this.close();
|
|
},
|
|
|
|
async openComposer(options, post, topic) {
|
|
await this.open(options);
|
|
|
|
let url = post?.url || topic?.url;
|
|
const topicTitle = topic?.title;
|
|
|
|
if (!url || !topicTitle) {
|
|
return;
|
|
}
|
|
|
|
url = `${location.protocol}//${location.host}${url}`;
|
|
const link = `[${escapeExpression(topicTitle)}](${url})`;
|
|
const continueDiscussion = I18n.t("post.continue_discussion", {
|
|
postLink: link,
|
|
});
|
|
|
|
const reply = this.get("model.reply");
|
|
if (reply?.includes(continueDiscussion)) {
|
|
return;
|
|
}
|
|
|
|
this.model.prependText(continueDiscussion, {
|
|
new_line: true,
|
|
});
|
|
},
|
|
|
|
onPopupMenuAction(menuAction) {
|
|
this.send(menuAction);
|
|
},
|
|
|
|
storeToolbarState(toolbarEvent) {
|
|
this.set("toolbarEvent", toolbarEvent);
|
|
},
|
|
|
|
typed() {
|
|
this.checkReplyLength();
|
|
this.model.typing();
|
|
},
|
|
|
|
cancelled() {
|
|
this.send("hitEsc");
|
|
},
|
|
|
|
addLinkLookup(linkLookup) {
|
|
this.set("linkLookup", linkLookup);
|
|
},
|
|
|
|
afterRefresh($preview) {
|
|
const topic = this.get("model.topic");
|
|
const linkLookup = this.linkLookup;
|
|
|
|
if (!topic || !linkLookup) {
|
|
return;
|
|
}
|
|
|
|
// Don't check if there's only one post
|
|
if (topic.posts_count === 1) {
|
|
return;
|
|
}
|
|
|
|
const post = this.get("model.post");
|
|
const $links = $("a[href]", $preview);
|
|
$links.each((idx, l) => {
|
|
const href = l.href;
|
|
if (href && href.length) {
|
|
// skip links added by watched words
|
|
if (l.dataset.word !== undefined) {
|
|
return true;
|
|
}
|
|
|
|
// skip links in quotes and oneboxes
|
|
for (let element = l; element; element = element.parentElement) {
|
|
if (
|
|
element.tagName === "DIV" &&
|
|
element.classList.contains("d-editor-preview")
|
|
) {
|
|
break;
|
|
}
|
|
|
|
if (
|
|
element.tagName === "ASIDE" &&
|
|
element.classList.contains("quote")
|
|
) {
|
|
return true;
|
|
}
|
|
|
|
if (
|
|
element.tagName === "ASIDE" &&
|
|
element.classList.contains("onebox") &&
|
|
href !== element.dataset["onebox-src"]
|
|
) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
const [linkWarn, linkInfo] = linkLookup.check(post, href);
|
|
|
|
if (linkWarn && !this.get("isWhispering")) {
|
|
const body = I18n.t("composer.duplicate_link", {
|
|
domain: linkInfo.domain,
|
|
username: linkInfo.username,
|
|
post_url: topic.urlForPostNumber(linkInfo.post_number),
|
|
ago: shortDate(linkInfo.posted_at),
|
|
});
|
|
this.appEvents.trigger("composer-messages:create", {
|
|
extraClass: "custom-body",
|
|
templateName: "education",
|
|
body,
|
|
});
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
});
|
|
},
|
|
|
|
toggleWhisper() {
|
|
this.toggleProperty("model.whisper");
|
|
},
|
|
|
|
toggleInvisible() {
|
|
this.toggleProperty("model.unlistTopic");
|
|
},
|
|
|
|
toggleToolbar() {
|
|
this.toggleProperty("showToolbar");
|
|
},
|
|
|
|
// Toggle the reply view
|
|
async toggle() {
|
|
this.closeAutocomplete();
|
|
|
|
const composer = this.model;
|
|
|
|
if (isEmpty(composer?.reply) && isEmpty(composer?.title)) {
|
|
this.close();
|
|
} else if (composer?.viewOpenOrFullscreen) {
|
|
this.shrink();
|
|
} else {
|
|
await this.cancelComposer();
|
|
}
|
|
},
|
|
|
|
fullscreenComposer() {
|
|
this.toggleFullscreen();
|
|
return false;
|
|
},
|
|
|
|
// Import a quote from the post
|
|
async importQuote(toolbarEvent) {
|
|
const postStream = this.get("topic.postStream");
|
|
let postId = this.get("model.post.id");
|
|
|
|
// If there is no current post, use the first post id from the stream
|
|
if (!postId && postStream) {
|
|
postId = postStream.get("stream.firstObject");
|
|
}
|
|
|
|
// If we're editing a post, fetch the reply when importing a quote
|
|
if (this.get("model.editingPost")) {
|
|
const replyToPostNumber = this.get("model.post.reply_to_post_number");
|
|
if (replyToPostNumber) {
|
|
const replyPost = postStream.posts.findBy(
|
|
"post_number",
|
|
replyToPostNumber
|
|
);
|
|
|
|
if (replyPost) {
|
|
postId = replyPost.id;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!postId) {
|
|
return;
|
|
}
|
|
|
|
this.set("model.loading", true);
|
|
|
|
const post = await this.store.find("post", postId);
|
|
const quote = buildQuote(post, post.raw, { full: true });
|
|
|
|
toolbarEvent.addText(quote);
|
|
this.set("model.loading", false);
|
|
},
|
|
|
|
save(ignore, event) {
|
|
this.save(false, {
|
|
jump:
|
|
!(event?.shiftKey && this.get("model.replyingToTopic")) &&
|
|
!this.skipJumpOnSave,
|
|
});
|
|
},
|
|
|
|
displayEditReason() {
|
|
this.set("showEditReason", true);
|
|
},
|
|
|
|
hitEsc() {
|
|
if (
|
|
document.querySelectorAll(".emoji-picker-modal.fadeIn").length === 1
|
|
) {
|
|
this.appEvents.trigger("emoji-picker:close");
|
|
return;
|
|
}
|
|
|
|
if ((this.messageCount || 0) > 0) {
|
|
this.appEvents.trigger("composer-messages:close");
|
|
return;
|
|
}
|
|
|
|
const composer = this.model;
|
|
|
|
if (composer?.viewOpen) {
|
|
this.shrink();
|
|
}
|
|
|
|
if (composer?.viewFullscreen) {
|
|
this.toggleFullscreen();
|
|
this.focusComposer();
|
|
}
|
|
},
|
|
|
|
groupsMentioned(groups) {
|
|
if (
|
|
!this.get("model.creatingPrivateMessage") &&
|
|
!this.get("model.topic.isPrivateMessage")
|
|
) {
|
|
groups.forEach((group) => {
|
|
let body;
|
|
const groupLink = getURL(`/g/${group.name}/members`);
|
|
const maxMentions = parseInt(group.max_mentions, 10);
|
|
const userCount = parseInt(group.user_count, 10);
|
|
|
|
if (maxMentions < userCount) {
|
|
body = I18n.t("composer.group_mentioned_limit", {
|
|
group: `@${group.name}`,
|
|
count: maxMentions,
|
|
group_link: groupLink,
|
|
});
|
|
} else if (group.user_count > 0) {
|
|
body = I18n.t("composer.group_mentioned", {
|
|
group: `@${group.name}`,
|
|
count: userCount,
|
|
group_link: groupLink,
|
|
});
|
|
}
|
|
|
|
if (body) {
|
|
this.appEvents.trigger("composer-messages:create", {
|
|
extraClass: "custom-body",
|
|
templateName: "education",
|
|
body,
|
|
});
|
|
}
|
|
});
|
|
}
|
|
},
|
|
|
|
cannotSeeMention(mentions) {
|
|
mentions.forEach((mention) => {
|
|
this.appEvents.trigger("composer-messages:create", {
|
|
extraClass: "custom-body",
|
|
templateName: "education",
|
|
body: I18n.t(`composer.cannot_see_mention.${mention.reason}`, {
|
|
username: mention.name,
|
|
}),
|
|
});
|
|
});
|
|
},
|
|
|
|
hereMention(count) {
|
|
this.appEvents.trigger("composer-messages:create", {
|
|
extraClass: "custom-body",
|
|
templateName: "education",
|
|
body: I18n.t("composer.here_mention", {
|
|
here: this.siteSettings.here_mention,
|
|
count,
|
|
}),
|
|
});
|
|
},
|
|
|
|
applyFormatCode() {
|
|
this.toolbarEvent.formatCode();
|
|
},
|
|
|
|
applyUnorderedList() {
|
|
this.toolbarEvent.applyList("* ", "list_item");
|
|
},
|
|
|
|
applyOrderedList() {
|
|
this.toolbarEvent.applyList(
|
|
(i) => (!i ? "1. " : `${parseInt(i, 10) + 1}. `),
|
|
"list_item"
|
|
);
|
|
},
|
|
},
|
|
|
|
save(force, options = {}) {
|
|
if (this.disableSubmit) {
|
|
return;
|
|
}
|
|
|
|
// Clear the warning state if we're not showing the checkbox anymore
|
|
if (!this.showWarning) {
|
|
this.set("model.isWarning", false);
|
|
}
|
|
|
|
if (this.site.mobileView && this.showPreview) {
|
|
this.set("showPreview", false);
|
|
}
|
|
|
|
const composer = this.model;
|
|
|
|
if (composer?.cantSubmitPost) {
|
|
if (composer?.viewFullscreen) {
|
|
this.toggleFullscreen();
|
|
}
|
|
|
|
this.set("lastValidatedAt", Date.now());
|
|
return;
|
|
}
|
|
|
|
const topic = composer.topic;
|
|
const slowModePost =
|
|
topic && topic.slow_mode_seconds && topic.user_last_posted_at;
|
|
const notEditing = this.get("model.action") !== "edit";
|
|
|
|
// Editing a topic in slow mode is directly handled by the backend.
|
|
if (slowModePost && notEditing) {
|
|
if (
|
|
cannotPostAgain(
|
|
this.currentUser,
|
|
topic.slow_mode_seconds,
|
|
topic.user_last_posted_at
|
|
)
|
|
) {
|
|
const canPostAt = new moment(topic.user_last_posted_at).add(
|
|
topic.slow_mode_seconds,
|
|
"seconds"
|
|
);
|
|
const timeLeft = moment().diff(canPostAt, "seconds");
|
|
const message = I18n.t("composer.slow_mode.error", {
|
|
timeLeft: durationTextFromSeconds(timeLeft),
|
|
});
|
|
|
|
this.dialog.alert(message);
|
|
return;
|
|
} else {
|
|
// Edge case where the user tries to post again immediately.
|
|
topic.set("user_last_posted_at", new Date().toISOString());
|
|
}
|
|
}
|
|
|
|
composer.set("disableDrafts", true);
|
|
|
|
// for now handle a very narrow use case
|
|
// if we are replying to a topic
|
|
// AND are on on a different topic
|
|
// AND topic is open (or we are staff)
|
|
// --> pop the window up
|
|
if (!force && composer.replyingToTopic) {
|
|
const currentTopic = this.topicModel;
|
|
|
|
if (!currentTopic) {
|
|
this.save(true);
|
|
return;
|
|
}
|
|
|
|
if (
|
|
currentTopic.id !== composer.get("topic.id") &&
|
|
(this.isStaffUser || !currentTopic.closed)
|
|
) {
|
|
this.dialog.alert({
|
|
title: I18n.t("composer.posting_not_on_topic"),
|
|
buttons: [
|
|
{
|
|
label:
|
|
I18n.t("composer.reply_original") +
|
|
"<br/><div class='topic-title overflow-ellipsis'>" +
|
|
this.get("model.topic.fancyTitle") +
|
|
"</div>",
|
|
class: "btn-primary btn-reply-on-original",
|
|
action: () => this.save(true),
|
|
},
|
|
{
|
|
label:
|
|
I18n.t("composer.reply_here") +
|
|
"<br/><div class='topic-title overflow-ellipsis'>" +
|
|
currentTopic.get("fancyTitle") +
|
|
"</div>",
|
|
class: "btn-reply-here",
|
|
action: () => {
|
|
composer.setProperties({ topic: currentTopic, post: null });
|
|
this.save(true);
|
|
},
|
|
},
|
|
{
|
|
label: I18n.t("composer.cancel"),
|
|
class: "btn-flat btn-text btn-reply-where-cancel",
|
|
},
|
|
],
|
|
class: "reply-where-modal",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
|
|
let staged = false;
|
|
|
|
// TODO: This should not happen in model
|
|
const imageSizes = {};
|
|
document
|
|
.querySelectorAll("#reply-control .d-editor-preview img")
|
|
.forEach((e) => {
|
|
const src = e.src;
|
|
|
|
if (src && src.length) {
|
|
imageSizes[src] = { width: e.naturalWidth, height: e.naturalHeight };
|
|
}
|
|
});
|
|
|
|
const promise = composer
|
|
.save({ imageSizes, editReason: this.editReason })
|
|
.then((result) => {
|
|
this.appEvents.trigger("composer:saved");
|
|
|
|
if (result.responseJson.action === "enqueued") {
|
|
this.send("postWasEnqueued", result.responseJson);
|
|
if (result.responseJson.pending_post) {
|
|
let pendingPosts = this.get("topicController.model.pending_posts");
|
|
if (pendingPosts) {
|
|
pendingPosts.pushObject(result.responseJson.pending_post);
|
|
}
|
|
}
|
|
|
|
return this.destroyDraft().then(() => {
|
|
this.close();
|
|
this.appEvents.trigger("post-stream:refresh");
|
|
return result;
|
|
});
|
|
}
|
|
|
|
if (this.get("model.editingPost")) {
|
|
this.appEvents.trigger("composer:edited-post");
|
|
this.appEvents.trigger("post-stream:refresh", {
|
|
id: parseInt(result.responseJson.id, 10),
|
|
});
|
|
if (result.responseJson.post.post_number === 1) {
|
|
this.appEvents.trigger("header:update-topic", composer.topic);
|
|
}
|
|
} else {
|
|
this.appEvents.trigger("post-stream:refresh");
|
|
}
|
|
|
|
if (result.responseJson.action === "create_post") {
|
|
this.appEvents.trigger("composer:created-post");
|
|
this.appEvents.trigger(
|
|
"post:highlight",
|
|
result.payload.post_number,
|
|
options
|
|
);
|
|
}
|
|
|
|
if (this.get("model.draftKey") === Composer.NEW_TOPIC_KEY) {
|
|
this.currentUser.set("has_topic_draft", false);
|
|
}
|
|
|
|
if (result.responseJson.route_to) {
|
|
// TODO: await this:
|
|
this.destroyDraft();
|
|
if (result.responseJson.message) {
|
|
return this.dialog.alert({
|
|
message: result.responseJson.message,
|
|
didConfirm: () => {
|
|
DiscourseURL.routeTo(result.responseJson.route_to);
|
|
},
|
|
});
|
|
}
|
|
return DiscourseURL.routeTo(result.responseJson.route_to);
|
|
}
|
|
|
|
this.close();
|
|
|
|
this.currentUser.set("any_posts", true);
|
|
|
|
const post = result.target;
|
|
|
|
if (post && !staged && options.jump !== false) {
|
|
DiscourseURL.routeTo(post.url, {
|
|
keepFilter: true,
|
|
skipIfOnScreen: true,
|
|
});
|
|
}
|
|
})
|
|
.catch((error) => {
|
|
composer.set("disableDrafts", false);
|
|
if (error) {
|
|
this.appEvents.one("composer:will-open", () =>
|
|
this.dialog.alert(error)
|
|
);
|
|
}
|
|
});
|
|
|
|
if (
|
|
this.router.currentRouteName.split(".")[0] === "topic" &&
|
|
composer.get("topic.id") === this.get("topicModel.id")
|
|
) {
|
|
staged = composer.get("stagedPost");
|
|
}
|
|
|
|
this.appEvents.trigger("post-stream:posted", staged);
|
|
|
|
this.messageBus.pause();
|
|
promise.finally(() => this.messageBus.resume());
|
|
|
|
return promise;
|
|
},
|
|
|
|
// Notify the composer messages controller that a reply has been typed. Some
|
|
// messages only appear after typing.
|
|
checkReplyLength() {
|
|
if (!isEmpty("model.reply")) {
|
|
this.appEvents.trigger("composer:typed-reply");
|
|
}
|
|
},
|
|
|
|
/**
|
|
Open the composer view
|
|
|
|
@method open
|
|
@param {Object} opts Options for creating a post
|
|
@param {String} opts.action The action we're performing: edit, reply, createTopic, createSharedDraft, privateMessage
|
|
@param {String} opts.draftKey
|
|
@param {Post} [opts.post] The post we're replying to
|
|
@param {Topic} [opts.topic] The topic we're replying to
|
|
@param {String} [opts.quote] If we're opening a reply from a quote, the quote we're making
|
|
@param {Boolean} [opts.ignoreIfChanged]
|
|
@param {Boolean} [opts.disableScopedCategory]
|
|
@param {Number} [opts.categoryId] Sets `scopedCategoryId` and `categoryId` on the Composer model
|
|
@param {Number} [opts.prioritizedCategoryId]
|
|
@param {String} [opts.draftSequence]
|
|
@param {Boolean} [opts.skipDraftCheck]
|
|
@param {Boolean} [opts.skipJumpOnSave] Option to skip navigating to the post when saved in this composer session
|
|
**/
|
|
async open(opts = {}) {
|
|
if (!opts.draftKey) {
|
|
throw new Error("composer opened without a proper draft key");
|
|
}
|
|
|
|
let composerModel = this.model;
|
|
|
|
if (
|
|
opts.ignoreIfChanged &&
|
|
composerModel &&
|
|
composerModel.composeState !== Composer.CLOSED
|
|
) {
|
|
return;
|
|
}
|
|
|
|
this.setProperties({
|
|
showEditReason: false,
|
|
editReason: null,
|
|
scopedCategoryId: null,
|
|
prioritizedCategoryId: null,
|
|
skipAutoSave: true,
|
|
});
|
|
|
|
this.set("skipJumpOnSave", !!opts.skipJumpOnSave);
|
|
|
|
// Scope the categories drop down to the category we opened the composer with.
|
|
if (opts.categoryId && !opts.disableScopedCategory) {
|
|
const category = this.site.categories.findBy("id", opts.categoryId);
|
|
if (category) {
|
|
this.set("scopedCategoryId", opts.categoryId);
|
|
}
|
|
}
|
|
|
|
if (opts.prioritizedCategoryId) {
|
|
const category = this.site.categories.findBy(
|
|
"id",
|
|
opts.prioritizedCategoryId
|
|
);
|
|
if (category) {
|
|
this.set("prioritizedCategoryId", opts.prioritizedCategoryId);
|
|
}
|
|
}
|
|
|
|
// If we want a different draft than the current composer, close it and clear our model.
|
|
if (
|
|
composerModel &&
|
|
opts.draftKey !== composerModel.draftKey &&
|
|
composerModel.composeState === Composer.DRAFT
|
|
) {
|
|
this.close();
|
|
composerModel = null;
|
|
}
|
|
|
|
try {
|
|
if (composerModel?.replyDirty) {
|
|
// If we're already open, we don't have to do anything
|
|
if (
|
|
composerModel.composeState === Composer.OPEN &&
|
|
composerModel.draftKey === opts.draftKey &&
|
|
!opts.action
|
|
) {
|
|
return;
|
|
}
|
|
|
|
// If it's the same draft, just open it up again.
|
|
if (
|
|
composerModel.composeState === Composer.DRAFT &&
|
|
composerModel.draftKey === opts.draftKey
|
|
) {
|
|
composerModel.set("composeState", Composer.OPEN);
|
|
if (!opts.action) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
await this.cancelComposer();
|
|
await this.open(opts);
|
|
return;
|
|
}
|
|
|
|
if (composerModel && composerModel.action !== opts.action) {
|
|
composerModel.setProperties({ unlistTopic: false, whisper: false });
|
|
}
|
|
|
|
// we need a draft sequence for the composer to work
|
|
if (opts.draftSequence === undefined) {
|
|
let data = await Draft.get(opts.draftKey);
|
|
|
|
if (opts.skipDraftCheck) {
|
|
data.draft = undefined;
|
|
} else {
|
|
data = await this.confirmDraftAbandon(data);
|
|
}
|
|
|
|
opts.draft ||= data.draft;
|
|
opts.draftSequence = data.draft_sequence;
|
|
|
|
await this._setModel(composerModel, opts);
|
|
return;
|
|
}
|
|
|
|
// otherwise, do the draft check async
|
|
if (!opts.draft && !opts.skipDraftCheck) {
|
|
let data = await Draft.get(opts.draftKey);
|
|
data = await this.confirmDraftAbandon(data);
|
|
|
|
if (data.draft) {
|
|
opts.draft = data.draft;
|
|
opts.draftSequence = data.draft_sequence;
|
|
await this.open(opts);
|
|
}
|
|
}
|
|
|
|
await this._setModel(composerModel, opts);
|
|
} finally {
|
|
this.skipAutoSave = false;
|
|
}
|
|
},
|
|
|
|
// Given a potential instance and options, set the model for this composer.
|
|
async _setModel(optionalComposerModel, opts) {
|
|
this.set("linkLookup", null);
|
|
|
|
let composerModel;
|
|
if (opts.draft) {
|
|
composerModel = await loadDraft(this.store, opts);
|
|
|
|
if (!composerModel) {
|
|
throw new Error("draft was not found");
|
|
}
|
|
} else {
|
|
const model =
|
|
optionalComposerModel || this.store.createRecord("composer");
|
|
|
|
await model.open(opts);
|
|
composerModel = model;
|
|
}
|
|
|
|
this.set("model", composerModel);
|
|
|
|
composerModel.setProperties({
|
|
composeState: Composer.OPEN,
|
|
isWarning: false,
|
|
hasTargetGroups: opts.hasGroups,
|
|
});
|
|
|
|
if (!this.model.targetRecipients) {
|
|
if (opts.usernames) {
|
|
deprecated("`usernames` is deprecated, use `recipients` instead.", {
|
|
id: "discourse.composer.usernames",
|
|
});
|
|
this.model.set("targetRecipients", opts.usernames);
|
|
} else if (opts.recipients) {
|
|
this.model.set("targetRecipients", opts.recipients);
|
|
}
|
|
}
|
|
|
|
if (
|
|
opts.topicTitle &&
|
|
opts.topicTitle.length <= this.siteSettings.max_topic_title_length
|
|
) {
|
|
this.model.set("title", opts.topicTitle);
|
|
}
|
|
|
|
if (opts.topicCategoryId) {
|
|
this.model.set("categoryId", opts.topicCategoryId);
|
|
}
|
|
|
|
if (opts.topicTags && this.site.can_tag_topics) {
|
|
let tags = escapeExpression(opts.topicTags)
|
|
.split(",")
|
|
.slice(0, this.siteSettings.max_tags_per_topic);
|
|
|
|
tags.forEach(
|
|
(tag, index, array) =>
|
|
(array[index] = tag.substring(0, this.siteSettings.max_tag_length))
|
|
);
|
|
|
|
this.model.set("tags", tags);
|
|
}
|
|
|
|
if (opts.topicBody) {
|
|
this.model.set("reply", opts.topicBody);
|
|
}
|
|
|
|
const defaultComposerHeight = this._getDefaultComposerHeight();
|
|
|
|
this.set("model.composerHeight", defaultComposerHeight);
|
|
document.documentElement.style.setProperty(
|
|
"--composer-height",
|
|
defaultComposerHeight
|
|
);
|
|
},
|
|
|
|
_getDefaultComposerHeight() {
|
|
if (this.keyValueStore.getItem("composerHeight")) {
|
|
return this.keyValueStore.getItem("composerHeight");
|
|
}
|
|
|
|
// The two custom properties below can be overriden by themes/plugins to set different default composer heights.
|
|
if (this.model.action === "reply") {
|
|
return "var(--reply-composer-height, 300px)";
|
|
} else {
|
|
return "var(--new-topic-composer-height, 400px)";
|
|
}
|
|
},
|
|
|
|
async destroyDraft(draftSequence = null) {
|
|
const key = this.get("model.draftKey");
|
|
if (!key) {
|
|
return;
|
|
}
|
|
|
|
if (key === Composer.NEW_TOPIC_KEY) {
|
|
this.currentUser.set("has_topic_draft", false);
|
|
}
|
|
|
|
if (this._saveDraftPromise) {
|
|
await this._saveDraftPromise;
|
|
return await this.destroyDraft();
|
|
}
|
|
|
|
const sequence = draftSequence || this.get("model.draftSequence");
|
|
await Draft.clear(key, sequence);
|
|
this.appEvents.trigger("draft:destroyed", key);
|
|
},
|
|
|
|
confirmDraftAbandon(data) {
|
|
if (!data.draft) {
|
|
return data;
|
|
}
|
|
|
|
// do not show abandon dialog if old draft is clean
|
|
const draft = JSON.parse(data.draft);
|
|
if (draft.reply === draft.originalText) {
|
|
data.draft = null;
|
|
return data;
|
|
}
|
|
|
|
if (!_checkDraftPopup) {
|
|
data.draft = null;
|
|
return data;
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
this.dialog.alert({
|
|
message: I18n.t("drafts.abandon.confirm"),
|
|
buttons: [
|
|
{
|
|
label: I18n.t("drafts.abandon.yes_value"),
|
|
class: "btn-danger",
|
|
icon: "far-trash-alt",
|
|
action: () => {
|
|
this.destroyDraft(data.draft_sequence).finally(() => {
|
|
data.draft = null;
|
|
resolve(data);
|
|
});
|
|
},
|
|
},
|
|
{
|
|
label: I18n.t("drafts.abandon.no_value"),
|
|
class: "btn-resume-editing",
|
|
action: () => resolve(data),
|
|
},
|
|
],
|
|
});
|
|
});
|
|
},
|
|
|
|
cancelComposer() {
|
|
this.skipAutoSave = true;
|
|
|
|
if (this._saveDraftDebounce) {
|
|
cancel(this._saveDraftDebounce);
|
|
}
|
|
|
|
return new Promise((resolve) => {
|
|
if (this.get("model.hasMetaData") || this.get("model.replyDirty")) {
|
|
const modal = showModal("discard-draft", {
|
|
model: this.model,
|
|
modalClass: "discard-draft-modal",
|
|
});
|
|
modal.setProperties({
|
|
onDestroyDraft: () => {
|
|
return this.destroyDraft()
|
|
.then(() => {
|
|
this.model.clearState();
|
|
this.close();
|
|
})
|
|
.finally(() => {
|
|
this.appEvents.trigger("composer:cancelled");
|
|
resolve();
|
|
});
|
|
},
|
|
onSaveDraft: () => {
|
|
this._saveDraft();
|
|
this.model.clearState();
|
|
this.close();
|
|
this.appEvents.trigger("composer:cancelled");
|
|
return resolve();
|
|
},
|
|
// needed to resume saving drafts if composer stays open
|
|
onDismissModal: () => resolve(),
|
|
});
|
|
} else {
|
|
// it is possible there is some sort of crazy draft with no body ... just give up on it
|
|
this.destroyDraft()
|
|
.then(() => {
|
|
this.model.clearState();
|
|
this.close();
|
|
})
|
|
.finally(() => {
|
|
this.appEvents.trigger("composer:cancelled");
|
|
resolve();
|
|
});
|
|
}
|
|
}).finally(() => {
|
|
this.skipAutoSave = false;
|
|
});
|
|
},
|
|
|
|
shrink() {
|
|
if (
|
|
this.get("model.replyDirty") ||
|
|
(this.get("model.canEditTitle") && this.get("model.titleDirty"))
|
|
) {
|
|
this.collapse();
|
|
} else {
|
|
this.close();
|
|
}
|
|
},
|
|
|
|
_saveDraft() {
|
|
if (!this.model) {
|
|
return;
|
|
}
|
|
|
|
if (this.model.draftSaving) {
|
|
this._saveDraftDebounce = discourseDebounce(this, this._saveDraft, 2000);
|
|
} else {
|
|
this._saveDraftPromise = this.model
|
|
.saveDraft(this.currentUser)
|
|
.finally(() => {
|
|
this._lastDraftSaved = Date.now();
|
|
this._saveDraftPromise = null;
|
|
});
|
|
}
|
|
},
|
|
|
|
@observes("model.reply", "model.title")
|
|
_shouldSaveDraft() {
|
|
if (
|
|
this.model &&
|
|
!this.model.loading &&
|
|
!this.skipAutoSave &&
|
|
!this.model.disableDrafts
|
|
) {
|
|
if (!this._lastDraftSaved) {
|
|
// pretend so we get a save unconditionally in 15 secs
|
|
this._lastDraftSaved = Date.now();
|
|
}
|
|
if (Date.now() - this._lastDraftSaved > 15000) {
|
|
this._saveDraft();
|
|
} else {
|
|
this._saveDraftDebounce = discourseDebounce(
|
|
this,
|
|
this._saveDraft,
|
|
2000
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
@discourseComputed("model.categoryId", "lastValidatedAt")
|
|
categoryValidation(categoryId, lastValidatedAt) {
|
|
if (!this.siteSettings.allow_uncategorized_topics && !categoryId) {
|
|
return EmberObject.create({
|
|
failed: true,
|
|
reason: I18n.t("composer.error.category_missing"),
|
|
lastShownAt: lastValidatedAt,
|
|
});
|
|
}
|
|
},
|
|
|
|
@discourseComputed("model.category", "model.tags", "lastValidatedAt")
|
|
tagValidation(category, tags, lastValidatedAt) {
|
|
const tagsArray = tags || [];
|
|
if (this.site.can_tag_topics && !this.currentUser.staff && category) {
|
|
// category.minimumRequiredTags incorporates both minimum_required_tags, and required_tag_groups
|
|
if (category.minimumRequiredTags > tagsArray.length) {
|
|
return EmberObject.create({
|
|
failed: true,
|
|
reason: I18n.t("composer.error.tags_missing", {
|
|
count: category.minimumRequiredTags,
|
|
}),
|
|
lastShownAt: lastValidatedAt,
|
|
});
|
|
}
|
|
}
|
|
},
|
|
|
|
collapse() {
|
|
this._saveDraft();
|
|
this.set("model.composeState", Composer.DRAFT);
|
|
document.documentElement.style.setProperty("--composer-height", "40px");
|
|
},
|
|
|
|
toggleFullscreen() {
|
|
this._saveDraft();
|
|
|
|
const composer = this.model;
|
|
|
|
if (composer?.viewFullscreen) {
|
|
composer?.set("composeState", Composer.OPEN);
|
|
} else {
|
|
composer?.set("composeState", Composer.FULLSCREEN);
|
|
composer?.set("showFullScreenExitPrompt", true);
|
|
}
|
|
},
|
|
|
|
@discourseComputed("model.viewFullscreen", "model.showFullScreenExitPrompt")
|
|
showFullScreenPrompt(isFullscreen, showExitPrompt) {
|
|
return isFullscreen && showExitPrompt && !this.capabilities.touch;
|
|
},
|
|
|
|
close() {
|
|
// the 'fullscreen-composer' class is added to remove scrollbars from the
|
|
// document while in fullscreen mode. If the composer is closed for any reason
|
|
// this class should be removed
|
|
|
|
const elem = document.querySelector("html");
|
|
elem.classList.remove("fullscreen-composer");
|
|
elem.classList.remove("composer-open");
|
|
|
|
document.activeElement?.blur();
|
|
document.documentElement.style.removeProperty("--composer-height");
|
|
this.setProperties({ model: null, lastValidatedAt: null });
|
|
},
|
|
|
|
closeAutocomplete() {
|
|
$(".d-editor-input").autocomplete({ cancel: true });
|
|
},
|
|
|
|
@discourseComputed("model.action")
|
|
canEdit(modelAction) {
|
|
return modelAction === "edit" && this.currentUser.can_edit;
|
|
},
|
|
|
|
@discourseComputed("model.composeState")
|
|
visible(state) {
|
|
return state && state !== "closed";
|
|
},
|
|
|
|
clearLastValidatedAt() {
|
|
this.set("lastValidatedAt", null);
|
|
},
|
|
});
|