\n )}\n >\n );\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/utils/DiscussionControls'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/EditPostComposer'];","import app from 'flarum/forum/app';\nimport DiscussionControls from 'flarum/forum/utils/DiscussionControls';\nimport EditPostComposer from 'flarum/forum/components/EditPostComposer';\n\nexport function insertMention(post, composer, quote) {\n return new Promise((resolve) => {\n const mention = app.mentionFormats.mentionable('post').replacement(post) + ' ';\n\n // If the composer is empty, then assume we're starting a new reply.\n // In which case we don't want the user to have to confirm if they\n // close the composer straight away.\n if (!composer.fields.content()) {\n composer.body.attrs.originalContent = mention;\n }\n\n const cursorPosition = composer.editor.getSelectionRange()[0];\n const preceding = composer.fields.content().slice(0, cursorPosition);\n const precedingNewlines = preceding.length == 0 ? 0 : 3 - preceding.match(/(\\n{0,2})$/)[0].length;\n\n composer.editor.insertAtCursor(\n Array(precedingNewlines).join('\\n') + // Insert up to two newlines, depending on preceding whitespace\n (quote ? '> ' + mention + quote.trim().replace(/\\n/g, '\\n> ') + '\\n\\n' : mention),\n false\n );\n return resolve(composer);\n });\n}\n\nexport default function reply(post, quote) {\n if (app.composer.bodyMatches(EditPostComposer) && app.composer.body.attrs.post.discussion() === post.discussion()) {\n // If we're already editing a post in the discussion of post we're quoting,\n // insert the mention directly.\n return insertMention(post, app.composer, quote);\n } else {\n // The default \"Reply\" action behavior will only open a new composer if\n // necessary, but it will always be a ReplyComposer, hence the exceptional\n // case above.\n return DiscussionControls.replyAction.call(post.discussion()).then((composer) => insertMention(post, composer, quote));\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Fragment'];","import app from 'flarum/forum/app';\nimport Fragment from 'flarum/common/Fragment';\nimport icon from 'flarum/common/helpers/icon';\n\nimport reply from '../utils/reply';\n\nexport default class PostQuoteButton extends Fragment {\n constructor(post) {\n super();\n\n this.post = post;\n }\n\n view() {\n return (\n \n );\n }\n\n show(left, top) {\n const $this = this.$().show();\n const parentOffset = $this.offsetParent().offset();\n\n $this.css('left', left - parentOffset.left).css('top', top - parentOffset.top);\n\n this.hideHandler = this.hide.bind(this);\n $(document).on('mouseup', this.hideHandler);\n }\n\n showStart(left, top) {\n const $this = this.$();\n\n this.show(left, $(window).scrollTop() + top - $this.outerHeight() - 5);\n }\n\n showEnd(right, bottom) {\n const $this = this.$();\n\n this.show(right - $this.outerWidth(), $(window).scrollTop() + bottom + 5);\n }\n\n hide() {\n this.$().hide();\n $(document).off('mouseup', this.hideHandler);\n }\n}\n","/**\n * Finds the selected text in the provided composer body.\n */\nexport default function selectedText(body) {\n const selection = window.getSelection();\n\n if (!selection.isCollapsed) {\n const range = selection.getRangeAt(0);\n const parent = range.commonAncestorContainer;\n\n if (body[0] === parent || $.contains(body[0], parent)) {\n const clone = $('
').append(range.cloneContents());\n\n // Replace emoji images with their shortcode (found in alt attribute)\n clone.find('img.emoji').replaceWith(function () {\n return this.alt;\n });\n\n // Replace all other images with a Markdown image\n clone.find('img').replaceWith(function () {\n return ``;\n });\n\n // Replace all links with a Markdown link\n clone.find('a').replaceWith(function () {\n return `[${this.innerText}](${this.href})`;\n });\n\n return clone.text();\n }\n }\n return '';\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/TextEditor'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/TextEditorButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/KeyboardNavigatable'];","export default function _typeof(obj) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) {\n return typeof obj;\n } : function (obj) {\n return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n }, _typeof(obj);\n}","import toPropertyKey from \"./toPropertyKey.js\";\nexport default function _defineProperty(obj, key, value) {\n key = toPropertyKey(key);\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n return obj;\n}","import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nexport default function _toPropertyKey(arg) {\n var key = toPrimitive(arg, \"string\");\n return _typeof(key) === \"symbol\" ? key : String(key);\n}","import _typeof from \"./typeof.js\";\nexport default function _toPrimitive(input, hint) {\n if (_typeof(input) !== \"object\" || input === null) return input;\n var prim = input[Symbol.toPrimitive];\n if (prim !== undefined) {\n var res = prim.call(input, hint || \"default\");\n if (_typeof(res) !== \"object\") return res;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (hint === \"string\" ? String : Number)(input);\n}","import Fragment from 'flarum/common/Fragment';\n\nexport default class AutocompleteDropdown extends Fragment {\n items = [];\n active = false;\n index = 0;\n keyWasJustPressed = false;\n\n view() {\n return (\n
\n {this.items.map((item) => (\n
{item}
\n ))}\n
\n );\n }\n\n show(left, top) {\n this.$()\n .show()\n .css({\n left: left + 'px',\n top: top + 'px',\n });\n this.active = true;\n }\n\n hide() {\n this.$().hide();\n this.active = false;\n }\n\n navigate(delta) {\n this.keyWasJustPressed = true;\n this.setIndex(this.index + delta, true);\n clearTimeout(this.keyWasJustPressedTimeout);\n this.keyWasJustPressedTimeout = setTimeout(() => (this.keyWasJustPressed = false), 500);\n }\n\n complete() {\n this.$('li').eq(this.index).find('button').click();\n }\n\n setIndex(index, scrollToItem) {\n if (this.keyWasJustPressed && !scrollToItem) return;\n\n const $dropdown = this.$();\n const $items = $dropdown.find('li');\n let rangedIndex = index;\n\n if (rangedIndex < 0) {\n rangedIndex = $items.length - 1;\n } else if (rangedIndex >= $items.length) {\n rangedIndex = 0;\n }\n\n this.index = rangedIndex;\n\n const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');\n\n if (scrollToItem) {\n const dropdownScroll = $dropdown.scrollTop();\n const dropdownTop = $dropdown.offset().top;\n const dropdownBottom = dropdownTop + $dropdown.outerHeight();\n const itemTop = $item.offset().top;\n const itemBottom = itemTop + $item.outerHeight();\n\n let scrollTop;\n if (itemTop < dropdownTop) {\n scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);\n } else if (itemBottom > dropdownBottom) {\n scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);\n }\n\n if (typeof scrollTop !== 'undefined') {\n $dropdown.stop(true).animate({ scrollTop }, 100);\n }\n }\n }\n}\n","import type MentionableModel from '../MentionableModel';\nimport type Model from 'flarum/common/Model';\n\nexport default abstract class MentionFormat {\n protected instances?: MentionableModel[];\n\n public makeMentionables(): MentionableModel[] {\n return this.instances ?? (this.instances = this.mentionables.map((Mentionable) => new Mentionable(this)));\n }\n\n public getMentionable(type: string): MentionableModel | null {\n return this.makeMentionables().find((mentionable) => mentionable.type() === type) ?? null;\n }\n\n public extend(mentionable: new (...args: any[]) => MentionableModel): void {\n if (!this.extendable) throw new Error('This mention format does not allow extending.');\n\n this.mentionables.push(mentionable);\n }\n\n abstract mentionables: (new (...args: any[]) => MentionableModel)[];\n\n protected abstract extendable: boolean;\n\n abstract trigger(): string;\n\n /**\n * Picks the term to search in the API from the typed text.\n * @example:\n * * Full text = `Hello @\"John D`\n * * Typed text = `\"John D`\n * * Query = `John D`\n */\n abstract queryFromTyped(typed: string): string | null;\n\n abstract format(...args: any): string;\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/avatar'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/highlight'];","import type Mithril from 'mithril';\nimport type Model from 'flarum/common/Model';\nimport type MentionFormat from './formats/MentionFormat';\n\nexport default abstract class MentionableModel {\n public format: Format;\n\n public constructor(format: Format) {\n this.format = format;\n }\n\n abstract type(): string;\n abstract initialResults(): M[];\n abstract search(typed: string): Promise;\n abstract replacement(model: M): string;\n abstract suggestion(model: M, typed: string): Mithril.Children;\n abstract matches(model: M, typed: string): boolean;\n abstract maxStoreMatchedResults(): number | null;\n abstract enabled(): boolean;\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/extractText'];","import app from 'flarum/forum/app';\nimport extractText from 'flarum/common/utils/extractText';\n\n/**\n * Whether to use the old mentions format.\n *\n * `'@username'` or `'@\"Display name\"'`\n */\nexport const shouldUseOldFormat = () => app.forum.attribute('allowUsernameMentionFormat') || false;\n\nconst getDeletedUserText = () => extractText(app.translator.trans('core.lib.username.deleted_text'));\n\n/**\n * Fetches a user's username or display name.\n *\n * Chooses based on the format option set in the admin settings page.\n *\n * @param user An instance of the User model to fetch the username for\n * @param useDisplayName If `true`, uses `user.displayName()`, otherwise, uses `user.username()`\n */\nexport default function getCleanDisplayName(user, useDisplayName = true) {\n if (!user) return getDeletedUserText().replace(/\"#[a-z]{0,3}[0-9]+/, '_');\n\n const text = (useDisplayName ? user.displayName() : user.username()) || getDeletedUserText();\n\n return text.replace(/\"#[a-z]{0,3}[0-9]+/, '_');\n}\n","import app from 'flarum/forum/app';\nimport type Mithril from 'mithril';\nimport type User from 'flarum/common/models/User';\nimport usernameHelper from 'flarum/common/helpers/username';\nimport avatar from 'flarum/common/helpers/avatar';\nimport highlight from 'flarum/common/helpers/highlight';\nimport MentionableModel from './MentionableModel';\nimport getCleanDisplayName, { shouldUseOldFormat } from '../utils/getCleanDisplayName';\nimport AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class UserMention extends MentionableModel {\n type(): string {\n return 'user';\n }\n\n initialResults(): User[] {\n return Array.from(app.store.all('users'));\n }\n\n /**\n * Automatically determines which mention syntax to be used based on the option in the\n * admin dashboard. Also performs display name clean-up automatically.\n *\n * @\"Display name\"#UserID or `@username`\n *\n * @example
New display name syntax
\n * // '@\"user\"#1'\n * forUser(User) // User is ID 1, display name is 'User'\n *\n * @example
Using old syntax
\n * // '@username'\n * forUser(user) // User's username is 'username'\n */\n public replacement(user: User): string {\n if (shouldUseOldFormat()) {\n const cleanText = getCleanDisplayName(user, false);\n return this.format.format(cleanText);\n }\n\n const cleanText = getCleanDisplayName(user);\n return this.format.format(cleanText, '', user.id());\n }\n\n suggestion(model: User, typed: string): Mithril.Children {\n const username = usernameHelper(model);\n\n if (typed) {\n username.children = [highlight((username.text ?? '') as string, typed)];\n delete username.text;\n }\n\n return (\n <>\n {avatar(model)}\n {username}\n >\n );\n }\n\n matches(model: User, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.username(), model.displayName()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n async search(typed: string): Promise {\n return await app.store.find('users', { filter: { q: typed }, page: { limit: 5 } });\n }\n\n enabled(): boolean {\n return true;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/ReplyComposer'];","import app from 'flarum/forum/app';\nimport MentionableModel from './MentionableModel';\nimport type Post from 'flarum/common/models/Post';\nimport type Mithril from 'mithril';\nimport usernameHelper from 'flarum/common/helpers/username';\nimport avatar from 'flarum/common/helpers/avatar';\nimport highlight from 'flarum/common/helpers/highlight';\nimport { truncate } from 'flarum/common/utils/string';\nimport ReplyComposer from 'flarum/forum/components/ReplyComposer';\nimport EditPostComposer from 'flarum/forum/components/EditPostComposer';\nimport getCleanDisplayName from '../utils/getCleanDisplayName';\nimport type AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class PostMention extends MentionableModel {\n type(): string {\n return 'post';\n }\n\n /**\n * If the user is replying to a discussion, or if they are editing a\n * post, then we can suggest other posts in the discussion to mention.\n * We will add the 5 most recent comments in the discussion which\n * match any username characters that have been typed.\n */\n initialResults(): Post[] {\n if (!app.composer.bodyMatches(ReplyComposer) && !app.composer.bodyMatches(EditPostComposer)) {\n return [];\n }\n\n // @ts-ignore\n const composerAttrs = app.composer.body.attrs;\n const composerPost = composerAttrs.post;\n const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;\n\n return (\n discussion\n .posts()\n // Filter to only comment posts, and replies before this message\n .filter((post: Post) => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))\n // Sort by new to old\n .sort((a: Post, b: Post) => b.createdAt().getTime() - a.createdAt().getTime())\n );\n }\n\n /**\n * Generates the syntax for mentioning of a post. Also cleans up the display name.\n *\n * @example
Post mention
\n * // '@\"User\"#p13'\n * // @\"Display name\"#pPostID\n * forPostMention(user, 13) // User display name is 'User', post ID is 13\n */\n public replacement(post: Post): string {\n const user = post.user();\n const cleanText = getCleanDisplayName(user);\n return this.format.format(cleanText, 'p', post.id());\n }\n\n suggestion(model: Post, typed: string): Mithril.Children {\n const user = model.user() || null;\n const username = usernameHelper(user);\n\n if (typed) {\n username.children = [highlight((username.text ?? '') as string, typed)];\n delete username.text;\n }\n\n return (\n <>\n {avatar(user)}\n {username}\n {[\n app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: model.number() }),\n ' — ',\n truncate(model.contentPlain() ?? '', 200),\n ]}\n >\n );\n }\n\n matches(model: Post, typed: string): boolean {\n const user = model.user();\n const userMentionable = app.mentionFormats.mentionable('user')!;\n\n return !typed || (user && userMentionable.matches(user, typed));\n }\n\n maxStoreMatchedResults(): number {\n return 5;\n }\n\n /**\n * Post mention suggestions are only offered from current discussion posts.\n */\n search(typed: string): Promise {\n return Promise.resolve([]);\n }\n\n enabled(): boolean {\n return true;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Group'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Badge'];","import app from 'flarum/forum/app';\nimport Group from 'flarum/common/models/Group';\nimport MentionableModel from './MentionableModel';\nimport type Mithril from 'mithril';\nimport Badge from 'flarum/common/components/Badge';\nimport highlight from 'flarum/common/helpers/highlight';\nimport type AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class GroupMention extends MentionableModel {\n type(): string {\n return 'group';\n }\n\n initialResults(): Group[] {\n return Array.from(\n app.store.all('groups').filter((g: Group) => {\n return g.id() !== Group.GUEST_ID && g.id() !== Group.MEMBER_ID;\n })\n );\n }\n\n /**\n * Generates the mention syntax for a group mention.\n *\n * @\"Name Plural\"#gGroupID\n *\n * @example
Group mention
\n * // '@\"Mods\"#g4'\n * forGroup(group) // Group display name is 'Mods', group ID is 4\n */\n public replacement(group: Group): string {\n return this.format.format(group.namePlural(), 'g', group.id());\n }\n\n suggestion(model: Group, typed: string): Mithril.Children {\n let groupName: Mithril.Children = model.namePlural();\n\n if (typed) {\n groupName = highlight(groupName, typed);\n }\n\n return (\n <>\n \n {groupName}\n >\n );\n }\n\n matches(model: Group, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.namePlural().toLowerCase(), model.nameSingular().toLowerCase()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n /**\n * All groups are already loaded, so we don't need to search for them.\n */\n search(typed: string): Promise {\n return Promise.resolve([]);\n }\n\n enabled(): boolean {\n return app.session?.user?.canMentionGroups() ?? false;\n }\n}\n","import MentionFormat from './MentionFormat';\nimport type MentionableModel from '../MentionableModel';\nimport UserMention from '../UserMention';\nimport PostMention from '../PostMention';\nimport GroupMention from '../GroupMention';\n\nexport default class AtMentionFormat extends MentionFormat {\n public mentionables: (new (...args: any[]) => MentionableModel)[] = [UserMention, PostMention, GroupMention];\n protected extendable: boolean = true;\n\n public trigger(): string {\n return '@';\n }\n\n public queryFromTyped(typed: string): string | null {\n const matchTyped = typed.match(/^[\"“]?((?:(?!\"#).)+)$/);\n\n return matchTyped ? matchTyped[1] : null;\n }\n\n public format(name: string, char: string | null = '', id: string | null = null): string {\n return {\n simple: `@${name}`,\n safe: `@\"${name}\"#${char}${id}`,\n }[id ? 'safe' : 'simple'];\n }\n}\n","import app from 'flarum/forum/app';\nimport Badge from 'flarum/common/components/Badge';\nimport highlight from 'flarum/common/helpers/highlight';\nimport type Tag from 'flarum/tags/common/models/Tag';\nimport type Mithril from 'mithril';\nimport MentionableModel from './MentionableModel';\nimport type HashMentionFormat from './formats/HashMentionFormat';\n\nexport default class TagMention extends MentionableModel {\n type(): string {\n return 'tag';\n }\n\n initialResults(): Tag[] {\n return Array.from(app.store.all('tags'));\n }\n\n /**\n * Generates the mention syntax for a tag mention.\n *\n * ~tagSlug\n *\n * @example
Tag mention
\n * // ~general\n * forTag(tag) // Tag display name is 'Tag', tag ID is 5\n */\n public replacement(tag: Tag): string {\n return this.format.format(tag.slug());\n }\n\n matches(model: Tag, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.name().toLowerCase()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n async search(typed: string): Promise {\n return await app.store.find('tags', { filter: { q: typed }, page: { limit: 5 } });\n }\n\n suggestion(model: Tag, typed: string): Mithril.Children {\n let tagName: Mithril.Children = model.name();\n\n if (typed) {\n tagName = highlight(tagName, typed);\n }\n\n return (\n <>\n \n {tagName}\n >\n );\n }\n\n enabled(): boolean {\n return 'flarum-tags' in flarum.extensions;\n }\n}\n","import MentionFormat from './MentionFormat';\nimport MentionableModel from '../MentionableModel';\nimport TagMention from '../TagMention';\n\nexport default class HashMentionFormat extends MentionFormat {\n public mentionables: (new (...args: any[]) => MentionableModel)[] = [TagMention];\n protected extendable: boolean = false;\n\n public trigger(): string {\n return '#';\n }\n\n public queryFromTyped(typed: string): string | null {\n const matchTyped = typed.match(/^[-_\\p{L}\\p{N}\\p{M}]+$/giu);\n\n return matchTyped ? matchTyped[0] : null;\n }\n\n public format(slug: string): string {\n return `#${slug}`;\n }\n}\n","import AtMentionFormat from './AtMentionFormat';\nimport HashMentionFormat from './HashMentionFormat';\nimport type MentionFormat from './MentionFormat';\nimport MentionableModel from '../MentionableModel';\n\nexport default class MentionFormats {\n protected formats: MentionFormat[] = [new AtMentionFormat(), new HashMentionFormat()];\n\n public get(symbol: string): MentionFormat | null {\n return this.formats.find((f) => f.trigger() === symbol) ?? null;\n }\n\n public mentionable(type: string): MentionableModel | null {\n for (const format of this.formats) {\n const mentionable = format.getMentionable(type);\n\n if (mentionable) return mentionable;\n }\n\n return null;\n }\n\n public extend(format: new () => MentionFormat) {\n this.formats.push(new format());\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/classList'];","import Component from 'flarum/common/Component';\nimport type { ComponentAttrs } from 'flarum/common/Component';\nimport classList from 'flarum/common/utils/classList';\nimport type MentionableModel from '../mentionables/MentionableModel';\nimport type Mithril from 'mithril';\n\nexport interface IMentionsDropdownItemAttrs extends ComponentAttrs {\n mentionable: MentionableModel;\n onclick: () => void;\n onmouseenter: () => void;\n}\n\nexport default class MentionsDropdownItem extends Component {\n view(vnode: Mithril.Vnode): Mithril.Children {\n const { mentionable, ...attrs } = this.attrs;\n\n const className = classList('MentionsDropdownItem', 'PostPreview', `MentionsDropdown-${mentionable.type()}`);\n\n return (\n \n );\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/throttleDebounce'];","import type MentionableModel from './MentionableModel';\nimport type Model from 'flarum/common/Model';\nimport type Mithril from 'mithril';\nimport MentionsDropdownItem from '../components/MentionsDropdownItem';\nimport { throttle } from 'flarum/common/utils/throttleDebounce';\n\nexport default class MentionableModels {\n protected mentionables?: MentionableModel[];\n /**\n * We store models returned from an API here to preserve order in which they are returned\n * This prevents the list jumping around while models are returned.\n * We also use a hashmap for model IDs to provide O(1) lookup for the users already in the list.\n */\n private results: Record> = {};\n public typed: string | null = null;\n private searched: string[] = [];\n private dropdownItemAttrs: Record = {};\n\n constructor(dropdownItemAttrs: Record) {\n this.dropdownItemAttrs = dropdownItemAttrs;\n }\n\n public init(mentionables: MentionableModel[]): void {\n this.typed = null;\n this.mentionables = mentionables;\n\n for (const mentionable of this.mentionables) {\n this.results[mentionable.type()] = new Map(mentionable.initialResults().map((result) => [result.id() as string, result]));\n }\n }\n\n /**\n * Don't send API calls searching for models until at least 2 characters have been typed.\n * This focuses the mention results on models already loaded.\n */\n public readonly search = throttle(250, async (): Promise => {\n if (!this.typed || this.typed.length <= 1) return;\n\n const typedLower = this.typed.toLowerCase();\n\n if (this.searched.includes(typedLower)) return;\n\n for (const mentionable of this.mentionables!) {\n for (const model of await mentionable.search(typedLower)) {\n if (!this.results[mentionable.type()].has(model.id() as string)) {\n this.results[mentionable.type()].set(model.id() as string, model);\n }\n }\n }\n\n this.searched.push(typedLower);\n\n return Promise.resolve();\n });\n\n public matches(mentionable: MentionableModel, model: Model): boolean {\n return mentionable.matches(model, this.typed?.toLowerCase() || '');\n }\n\n public makeSuggestion(mentionable: MentionableModel, model: Model): Mithril.Children {\n const content = mentionable.suggestion(model, this.typed!);\n const replacement = mentionable.replacement(model);\n\n const { onclick, ...attrs } = this.dropdownItemAttrs;\n\n return (\n onclick(replacement)} {...attrs}>\n {content}\n \n );\n }\n\n public buildSuggestions(): Mithril.Children {\n const suggestions: Mithril.Children = [];\n\n for (const mentionable of this.mentionables!) {\n if (!mentionable.enabled()) continue;\n\n let matches = Array.from(this.results[mentionable.type()].values()).filter((model) => this.matches(mentionable, model));\n\n const max = mentionable.maxStoreMatchedResults();\n if (max) matches = matches.splice(0, max);\n\n for (const model of matches) {\n const dropdownItem = this.makeSuggestion(mentionable, model);\n suggestions.push(dropdownItem);\n }\n }\n\n return suggestions;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/Notification'];","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class PostMentionedNotification extends Notification {\n icon() {\n return 'fas fa-reply';\n }\n\n href() {\n const notification = this.attrs.notification;\n const post = notification.subject();\n const content = notification.content();\n\n return app.route.discussion(post.discussion(), content && content.replyNumber);\n }\n\n content() {\n const notification = this.attrs.notification;\n const user = notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.post_mentioned_text', { user, count: 1 });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain() || '', 200);\n }\n}\n","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class UserMentionedNotification extends Notification {\n icon() {\n return 'fas fa-at';\n }\n\n href() {\n const post = this.attrs.notification.subject();\n\n return app.route.discussion(post.discussion(), post.number());\n }\n\n content() {\n const user = this.attrs.notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.user_mentioned_text', { user });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain(), 200);\n }\n}\n","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class GroupMentionedNotification extends Notification {\n icon() {\n return 'fas fa-at';\n }\n\n href() {\n const post = this.attrs.notification.subject();\n\n return app.route.discussion(post.discussion(), post.number());\n }\n\n content() {\n const user = this.attrs.notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.group_mentioned_text', { user });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain(), 200);\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/UserPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/User'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extenders'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Post'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/PostsUserPage'];","import app from 'flarum/forum/app';\nimport PostsUserPage from 'flarum/forum/components/PostsUserPage';\n\n/**\n * The `MentionsUserPage` component shows post which user Mentioned at\n */\nexport default class MentionsUserPage extends PostsUserPage {\n /**\n * Load a new page of the user's activity feed.\n *\n * @param {Integer} [offset] The position to start getting results from.\n * @return {Promise}\n * @protected\n */\n loadResults(offset) {\n return app.store.find('posts', {\n filter: {\n type: 'comment',\n mentioned: this.user.id(),\n },\n page: { offset, limit: this.loadLimit },\n sort: '-createdAt',\n });\n }\n}\n","import Extend from 'flarum/common/extenders';\nimport Post from 'flarum/common/models/Post';\nimport User from 'flarum/common/models/User';\nimport MentionsUserPage from './components/MentionsUserPage';\n\nexport default [\n new Extend.Routes() //\n .add('user.mentions', '/u/:username/mentions', MentionsUserPage),\n\n new Extend.Model(Post) //\n .hasMany('mentionedBy')\n .attribute('mentionedByCount'),\n\n new Extend.Model(User) //\n .attribute('canMentionGroups'),\n];\n","import app from 'flarum/forum/app';\nimport username from 'flarum/common/helpers/username';\nimport extractText from 'flarum/common/utils/extractText';\n\nexport function filterUserMentions(tag) {\n let user;\n\n if (app.forum.attribute('allowUsernameMentionFormat') && tag.hasAttribute('username'))\n user = app.store.getBy('users', 'username', tag.getAttribute('username'));\n else if (tag.hasAttribute('id')) user = app.store.getById('users', tag.getAttribute('id'));\n\n if (user) {\n tag.setAttribute('id', user.id());\n tag.setAttribute('slug', user.slug());\n tag.setAttribute('displayname', extractText(username(user)));\n\n return true;\n }\n\n tag.invalidate();\n}\n\nexport function postFilterUserMentions(tag) {\n tag.setAttribute('deleted', false);\n}\n\nexport function filterPostMentions(tag) {\n const post = app.store.getById('posts', tag.getAttribute('id'));\n\n if (post) {\n tag.setAttribute('discussionid', post.discussion().id());\n tag.setAttribute('number', post.number());\n tag.setAttribute('displayname', extractText(username(post.user())));\n\n return true;\n }\n}\n\nexport function postFilterPostMentions(tag) {\n tag.setAttribute('deleted', false);\n}\n\nexport function filterGroupMentions(tag) {\n if (app.session?.user?.canMentionGroups()) {\n const group = app.store.getById('groups', tag.getAttribute('id'));\n\n if (group) {\n tag.setAttribute('groupname', extractText(group.namePlural()));\n\n return true;\n }\n }\n\n tag.invalidate();\n}\n\nexport function postFilterGroupMentions(tag) {\n if (app.session?.user?.canMentionGroups()) {\n const group = app.store.getById('groups', tag.getAttribute('id'));\n\n tag.setAttribute('color', group.color());\n tag.setAttribute('icon', group.icon());\n tag.setAttribute('deleted', false);\n }\n}\n\nexport function filterTagMentions(tag) {\n if ('flarum-tags' in flarum.extensions) {\n const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));\n\n if (model) {\n tag.setAttribute('id', model.id());\n tag.setAttribute('tagname', model.name());\n\n return true;\n }\n }\n\n tag.invalidate();\n}\n\nexport function postFilterTagMentions(tag) {\n if ('flarum-tags' in flarum.extensions) {\n const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));\n\n tag.setAttribute('icon', model.icon());\n tag.setAttribute('color', model.color());\n tag.setAttribute('deleted', false);\n }\n}\n","import GroupMentionedNotification from './components/GroupMentionedNotification';\nimport MentionsUserPage from './components/MentionsUserPage';\nimport PostMentionedNotification from './components/PostMentionedNotification';\nimport UserMentionedNotification from './components/UserMentionedNotification';\nimport AutocompleteDropdown from './fragments/AutocompleteDropdown';\nimport PostQuoteButton from './fragments/PostQuoteButton';\nimport getCleanDisplayName from './utils/getCleanDisplayName';\nimport getMentionText from './utils/getMentionText';\nimport * as reply from './utils/reply';\nimport selectedText from './utils/selectedText';\nimport * as textFormatter from './utils/textFormatter';\nimport MentionableModel from './mentionables/MentionableModel';\nimport MentionFormat from './mentionables/formats/MentionFormat';\nimport Mentionables from './extenders/Mentionables';\n\nexport default {\n 'mentions/components/MentionsUserPage': MentionsUserPage,\n 'mentions/components/PostMentionedNotification': PostMentionedNotification,\n 'mentions/components/UserMentionedNotification': UserMentionedNotification,\n 'mentions/components/GroupMentionedNotification': GroupMentionedNotification,\n 'mentions/fragments/AutocompleteDropdown': AutocompleteDropdown,\n 'mentions/fragments/PostQuoteButton': PostQuoteButton,\n 'mentions/utils/getCleanDisplayName': getCleanDisplayName,\n 'mentions/utils/getMentionText': getMentionText,\n 'mentions/utils/reply': reply,\n 'mentions/utils/selectedText': selectedText,\n 'mentions/utils/textFormatter': textFormatter,\n 'mentions/mentionables/MentionableModel': MentionableModel,\n 'mentions/mentionables/formats/MentionFormat': MentionFormat,\n 'mentions/extenders/Mentionables': Mentionables,\n};\n","import app from 'flarum/forum/app';\n\n/**\n * Fetches the mention text for a specified user (and optionally a post ID for replies or group).\n *\n * Automatically determines which mention syntax to be used based on the option in the\n * admin dashboard. Also performs display name clean-up automatically.\n *\n * @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.\n */\nexport default function getMentionText(user, postId, group) {\n if (user !== undefined && postId === undefined) {\n return app.mentionables.get('user').replacement(user);\n } else if (user !== undefined && postId !== undefined) {\n return app.mentionables.get('post').replacement(app.store.getById('posts', postId));\n } else if (group !== undefined) {\n return app.mentionables.get('group').replacement(group);\n }\n\n throw 'No parameters were passed';\n}\n","import type ForumApplication from 'flarum/forum/ForumApplication';\nimport type IExtender from 'flarum/common/extenders/IExtender';\nimport type MentionableModel from '../mentionables/MentionableModel';\nimport type MentionFormat from '../mentionables/formats/MentionFormat';\n\nexport default class Mentionables implements IExtender {\n protected formats: (new () => MentionFormat)[] = [];\n protected mentionables: Record MentionableModel)[]> = {};\n\n /**\n * Register a new mention format.\n * Must extend MentionFormat and have a unique unused trigger symbol.\n */\n format(format: new () => MentionFormat): this {\n this.formats.push(format);\n\n return this;\n }\n\n /**\n * Register a new mentionable model to a mention format.\n * Only works if the format has already been registered,\n * and the format allows using multiple mentionables.\n *\n * @param symbol The trigger symbol of the format to extend (ex: @).\n * @param mentionable The mentionable instance to register.\n * Must extend MentionableModel.\n */\n mentionable(symbol: string, mentionable: new () => MentionableModel): this {\n if (!this.mentionables[symbol]) {\n this.mentionables[symbol] = [];\n }\n\n this.mentionables[symbol].push(mentionable);\n\n return this;\n }\n\n extend(app: ForumApplication): void {\n for (const format of this.formats) {\n app.mentionFormats.extend(format);\n }\n\n for (const symbol in this.mentionables) {\n const format = app.mentionFormats.get(symbol);\n\n if (!format) continue;\n\n for (const mentionable of this.mentionables[symbol]) {\n format.extend(mentionable);\n }\n }\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core;","import { extend } from 'flarum/common/extend';\nimport app from 'flarum/forum/app';\nimport NotificationGrid from 'flarum/forum/components/NotificationGrid';\nimport { getPlainContent } from 'flarum/common/utils/string';\nimport textContrastClass from 'flarum/common/helpers/textContrastClass';\nimport Post from 'flarum/forum/components/Post';\n\nimport addPostMentionPreviews from './addPostMentionPreviews';\nimport addMentionedByList from './addMentionedByList';\nimport addPostReplyAction from './addPostReplyAction';\nimport addPostQuoteButton from './addPostQuoteButton';\nimport addComposerAutocomplete from './addComposerAutocomplete';\nimport PostMentionedNotification from './components/PostMentionedNotification';\nimport UserMentionedNotification from './components/UserMentionedNotification';\nimport GroupMentionedNotification from './components/GroupMentionedNotification';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport User from 'flarum/common/models/User';\nimport Model from 'flarum/common/Model';\n\nexport { default as extend } from './extend';\n\napp.initializers.add('flarum-mentions', function () {\n // For every mention of a post inside a post's content, set up a hover handler\n // that shows a preview of the mentioned post.\n addPostMentionPreviews();\n\n // In the footer of each post, show information about who has replied (i.e.\n // who the post has been mentioned by).\n addMentionedByList();\n\n // Add a 'reply' control to the footer of each post. When clicked, it will\n // open up the composer and add a post mention to its contents.\n addPostReplyAction();\n\n // Show a Quote button when Post text is selected\n addPostQuoteButton();\n\n // After typing '@' in the composer, show a dropdown suggesting a bunch of\n // posts or users that the user could mention.\n addComposerAutocomplete();\n\n app.notificationComponents.postMentioned = PostMentionedNotification;\n app.notificationComponents.userMentioned = UserMentionedNotification;\n app.notificationComponents.groupMentioned = GroupMentionedNotification;\n\n // Add notification preferences.\n extend(NotificationGrid.prototype, 'notificationTypes', function (items) {\n items.add('postMentioned', {\n name: 'postMentioned',\n icon: 'fas fa-reply',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_post_mentioned_label'),\n });\n\n items.add('userMentioned', {\n name: 'userMentioned',\n icon: 'fas fa-at',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_user_mentioned_label'),\n });\n\n items.add('groupMentioned', {\n name: 'groupMentioned',\n icon: 'fas fa-at',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_group_mentioned_label'),\n });\n });\n\n // Add mentions tab in user profile\n extend(UserPage.prototype, 'navItems', function (items) {\n const user = this.user;\n items.add(\n 'mentions',\n \n {app.translator.trans('flarum-mentions.forum.user.mentions_link')}\n ,\n 80\n );\n });\n\n // Remove post mentions when rendering post previews.\n getPlainContent.removeSelectors.push('a.PostMention');\n\n // Apply color contrast fix on group mentions.\n extend(Post.prototype, 'oncreate', function () {\n this.$('.GroupMention--colored, .TagMention--colored').each(function () {\n this.classList.add(textContrastClass(getComputedStyle(this).getPropertyValue('--color')));\n });\n });\n});\n\nexport * from './utils/textFormatter';\n\n// Expose compat API\nimport mentionsCompat from './compat';\nimport { compat } from '@flarum/core/forum';\n\nObject.assign(compat, mentionsCompat);\n","import { extend } from 'flarum/common/extend';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nimport PostPreview from 'flarum/forum/components/PostPreview';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\n\nexport default function addPostMentionPreviews() {\n function addPreviews() {\n const contentHtml = this.attrs.post.contentHtml();\n\n if (contentHtml === this.oldPostContentHtml || this.isEditing()) return;\n\n this.oldPostContentHtml = contentHtml;\n\n const parentPost = this.attrs.post;\n const $parentPost = this.$();\n\n this.$().on(\n 'click',\n '.UserMention:not(.UserMention--deleted), .PostMention:not(.PostMention--deleted), .TagMention:not(.TagMention--deleted)',\n function (e) {\n m.route.set(this.getAttribute('href'));\n e.preventDefault();\n }\n );\n\n this.$('.PostMention:not(.PostMention--deleted)').each(function () {\n const $this = $(this);\n const id = $this.data('id');\n let timeout;\n\n // Wrap the mention link in a wrapper element so that we can insert a\n // preview popup as its sibling and relatively position it.\n const $preview = $('
');\n $parentPost.append($preview);\n\n const getPostElement = () => {\n return $(`.PostStream-item[data-id=\"${id}\"]`);\n };\n\n const showPreview = () => {\n // When the user hovers their mouse over the mention, look for the\n // post that it's referring to in the stream, and determine if it's\n // in the viewport. If it is, we will \"pulsate\" it.\n const $post = getPostElement();\n let visible = false;\n if ($post.length) {\n const top = $post.offset().top;\n const scrollTop = window.pageYOffset;\n if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {\n $post.addClass('pulsate');\n visible = true;\n }\n }\n\n // Otherwise, we will show a popup preview of the post. If the post\n // hasn't yet been loaded, we will need to do that.\n if (!visible) {\n // Position the preview so that it appears above the mention.\n // (The offsetParent should be .Post-body.)\n const positionPreview = () => {\n const previewHeight = $preview.outerHeight(true);\n let offset = 0;\n\n // If the preview goes off the top of the viewport, reposition it to\n // be below the mention.\n if ($this.offset().top - previewHeight < $(window).scrollTop() + $('#header').outerHeight()) {\n offset += $this.outerHeight(true);\n } else {\n offset -= previewHeight;\n }\n\n $preview\n .show()\n .css('top', $this.offset().top - $parentPost.offset().top + offset)\n .css('left', $this.offsetParent().offset().left - $parentPost.offset().left)\n .css('max-width', $this.offsetParent().width());\n };\n\n const showPost = (post) => {\n const discussion = post.discussion();\n\n m.render($preview[0], [\n discussion !== parentPost.discussion() && (\n
\n {discussion.title()}\n
\n ),\n
\n \n
,\n ]);\n positionPreview();\n };\n\n const post = app.store.getById('posts', id);\n if (post && post.discussion()) {\n showPost(post);\n } else {\n m.render($preview[0], );\n app.store.find('posts', id).then(showPost);\n positionPreview();\n }\n\n setTimeout(() => $preview.off('transitionend').addClass('in'));\n }\n };\n\n const hidePreview = () => {\n getPostElement().removeClass('pulsate');\n if ($preview.hasClass('in')) {\n $preview.removeClass('in').one('transitionend', () => $preview.hide());\n }\n };\n\n // On a touch (mobile) device we cannot hover the link to reveal the preview.\n // Instead we cancel the navigation so that a click reveals the preview.\n // Users can then click on the preview to go to the post if desired.\n $this.on('touchend', (e) => {\n if (e.cancelable) {\n e.preventDefault();\n }\n });\n\n $this\n .add($preview)\n .hover(\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(showPreview, 250);\n },\n () => {\n clearTimeout(timeout);\n getPostElement().removeClass('pulsate');\n timeout = setTimeout(hidePreview, 250);\n }\n )\n .on('touchend', (e) => {\n showPreview();\n e.stopPropagation();\n });\n\n $(document).on('touchend', hidePreview);\n });\n }\n\n extend(CommentPost.prototype, 'oncreate', addPreviews);\n extend(CommentPost.prototype, 'onupdate', addPreviews);\n}\n","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nimport Link from 'flarum/common/components/Link';\nimport PostPreview from 'flarum/forum/components/PostPreview';\nimport punctuateSeries from 'flarum/common/helpers/punctuateSeries';\nimport username from 'flarum/common/helpers/username';\nimport icon from 'flarum/common/helpers/icon';\nimport Button from 'flarum/common/components/Button';\nimport MentionedByModal from './components/MentionedByModal';\n\nexport default function addMentionedByList() {\n function hidePreview() {\n this.$('.Post-mentionedBy-preview')\n .removeClass('in')\n .one('transitionend', function () {\n $(this).hide();\n });\n }\n\n extend(CommentPost.prototype, 'oncreate', function () {\n let timeout;\n const post = this.attrs.post;\n const replies = post.mentionedBy();\n\n if (replies && replies.length) {\n const $preview = $('
');\n this.$().append($preview);\n\n const $parentPost = this.$();\n const $this = this.$('.Post-mentionedBy');\n\n const showPreview = () => {\n if (!$preview.hasClass('in') && $preview.is(':visible')) return;\n\n // When the user hovers their mouse over the list of people who have\n // replied to the post, render a list of reply previews into a\n // popup.\n m.render(\n $preview[0],\n <>\n {replies.map((reply) => (\n
\n )}\n >\n );\n\n $preview\n .show()\n .css('top', $this.offset().top - $parentPost.offset().top + $this.outerHeight(true))\n .css('left', $this.offsetParent().offset().left - $parentPost.offset().left)\n .css('max-width', $parentPost.width());\n\n setTimeout(() => $preview.off('transitionend').addClass('in'));\n };\n\n $this.add($preview).hover(\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(showPreview, 250);\n },\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(hidePreview, 250);\n }\n );\n\n // Whenever the user hovers their mouse over a particular name in the\n // list of repliers, highlight the corresponding post in the preview\n // popup.\n this.$()\n .find('.Post-mentionedBy-summary a')\n .hover(\n function () {\n $preview.find('[data-number=\"' + $(this).data('number') + '\"]').addClass('active');\n },\n function () {\n $preview.find('[data-number]').removeClass('active');\n }\n );\n }\n });\n\n extend(CommentPost.prototype, 'footerItems', function (items) {\n const post = this.attrs.post;\n const replies = post.mentionedBy();\n\n if (replies && replies.length) {\n const users = [];\n const repliers = replies\n .sort((reply) => (reply.user() === app.session.user ? -1 : 0))\n .filter((reply) => {\n const user = reply.user();\n if (users.indexOf(user) === -1) {\n users.push(user);\n return true;\n }\n });\n\n const limit = 4;\n const overLimit = repliers.length > limit;\n\n // Create a list of unique users who have replied. So even if a user has\n // replied twice, they will only be in this array once.\n const names = repliers.slice(0, overLimit ? limit - 1 : limit).map((reply) => {\n const user = reply.user();\n\n return (\n \n {app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}\n \n );\n });\n\n // If there are more users that we've run out of room to display, add a \"x\n // others\" name to the end of the list. Clicking on it will display a modal\n // with a full list of names.\n if (overLimit) {\n const count = repliers.length - names.length;\n\n names.push(app.translator.trans('flarum-mentions.forum.post.others_text', { count }));\n }\n\n items.add(\n 'replies',\n
\n )}\n >\n );\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/utils/DiscussionControls'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/EditPostComposer'];","import app from 'flarum/forum/app';\nimport DiscussionControls from 'flarum/forum/utils/DiscussionControls';\nimport EditPostComposer from 'flarum/forum/components/EditPostComposer';\n\nexport function insertMention(post, composer, quote) {\n return new Promise((resolve) => {\n const mention = app.mentionFormats.mentionable('post').replacement(post) + ' ';\n\n // If the composer is empty, then assume we're starting a new reply.\n // In which case we don't want the user to have to confirm if they\n // close the composer straight away.\n if (!composer.fields.content()) {\n composer.body.attrs.originalContent = mention;\n }\n\n const cursorPosition = composer.editor.getSelectionRange()[0];\n const preceding = composer.fields.content().slice(0, cursorPosition);\n const precedingNewlines = preceding.length == 0 ? 0 : 3 - preceding.match(/(\\n{0,2})$/)[0].length;\n\n composer.editor.insertAtCursor(\n Array(precedingNewlines).join('\\n') + // Insert up to two newlines, depending on preceding whitespace\n (quote ? '> ' + mention + quote.trim().replace(/\\n/g, '\\n> ') + '\\n\\n' : mention),\n false\n );\n return resolve(composer);\n });\n}\n\nexport default function reply(post, quote) {\n if (app.composer.bodyMatches(EditPostComposer) && app.composer.body.attrs.post.discussion() === post.discussion()) {\n // If we're already editing a post in the discussion of post we're quoting,\n // insert the mention directly.\n return insertMention(post, app.composer, quote);\n } else {\n // The default \"Reply\" action behavior will only open a new composer if\n // necessary, but it will always be a ReplyComposer, hence the exceptional\n // case above.\n return DiscussionControls.replyAction.call(post.discussion()).then((composer) => insertMention(post, composer, quote));\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Fragment'];","import app from 'flarum/forum/app';\nimport Fragment from 'flarum/common/Fragment';\nimport icon from 'flarum/common/helpers/icon';\n\nimport reply from '../utils/reply';\n\nexport default class PostQuoteButton extends Fragment {\n constructor(post) {\n super();\n\n this.post = post;\n }\n\n view() {\n return (\n \n );\n }\n\n show(left, top) {\n const $this = this.$().show();\n const parentOffset = $this.offsetParent().offset();\n\n $this.css('left', left - parentOffset.left).css('top', top - parentOffset.top);\n\n this.hideHandler = this.hide.bind(this);\n $(document).on('mouseup', this.hideHandler);\n }\n\n showStart(left, top) {\n const $this = this.$();\n\n this.show(left, $(window).scrollTop() + top - $this.outerHeight() - 5);\n }\n\n showEnd(right, bottom) {\n const $this = this.$();\n\n this.show(right - $this.outerWidth(), $(window).scrollTop() + bottom + 5);\n }\n\n hide() {\n this.$().hide();\n $(document).off('mouseup', this.hideHandler);\n }\n}\n","/**\n * Finds the selected text in the provided composer body.\n */\nexport default function selectedText(body) {\n const selection = window.getSelection();\n\n if (!selection.isCollapsed) {\n const range = selection.getRangeAt(0);\n const parent = range.commonAncestorContainer;\n\n if (body[0] === parent || $.contains(body[0], parent)) {\n const clone = $('
').append(range.cloneContents());\n\n // Replace emoji images with their shortcode (found in alt attribute)\n clone.find('img.emoji').replaceWith(function () {\n return this.alt;\n });\n\n // Replace all other images with a Markdown image\n clone.find('img').replaceWith(function () {\n return ``;\n });\n\n // Replace all links with a Markdown link\n clone.find('a').replaceWith(function () {\n return `[${this.innerText}](${this.href})`;\n });\n\n return clone.text();\n }\n }\n return '';\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/TextEditor'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/TextEditorButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/KeyboardNavigatable'];","export default function _typeof(obj) {\n \"@babel/helpers - typeof\";\n\n return _typeof = \"function\" == typeof Symbol && \"symbol\" == typeof Symbol.iterator ? function (obj) {\n return typeof obj;\n } : function (obj) {\n return obj && \"function\" == typeof Symbol && obj.constructor === Symbol && obj !== Symbol.prototype ? \"symbol\" : typeof obj;\n }, _typeof(obj);\n}","import toPropertyKey from \"./toPropertyKey.js\";\nexport default function _defineProperty(obj, key, value) {\n key = toPropertyKey(key);\n if (key in obj) {\n Object.defineProperty(obj, key, {\n value: value,\n enumerable: true,\n configurable: true,\n writable: true\n });\n } else {\n obj[key] = value;\n }\n return obj;\n}","import _typeof from \"./typeof.js\";\nimport toPrimitive from \"./toPrimitive.js\";\nexport default function _toPropertyKey(arg) {\n var key = toPrimitive(arg, \"string\");\n return _typeof(key) === \"symbol\" ? key : String(key);\n}","import _typeof from \"./typeof.js\";\nexport default function _toPrimitive(input, hint) {\n if (_typeof(input) !== \"object\" || input === null) return input;\n var prim = input[Symbol.toPrimitive];\n if (prim !== undefined) {\n var res = prim.call(input, hint || \"default\");\n if (_typeof(res) !== \"object\") return res;\n throw new TypeError(\"@@toPrimitive must return a primitive value.\");\n }\n return (hint === \"string\" ? String : Number)(input);\n}","import Fragment from 'flarum/common/Fragment';\n\nexport default class AutocompleteDropdown extends Fragment {\n items = [];\n active = false;\n index = 0;\n keyWasJustPressed = false;\n\n view() {\n return (\n
\n {this.items.map((item) => (\n
{item}
\n ))}\n
\n );\n }\n\n show(left, top) {\n this.$()\n .show()\n .css({\n left: left + 'px',\n top: top + 'px',\n });\n this.active = true;\n }\n\n hide() {\n this.$().hide();\n this.active = false;\n }\n\n navigate(delta) {\n this.keyWasJustPressed = true;\n this.setIndex(this.index + delta, true);\n clearTimeout(this.keyWasJustPressedTimeout);\n this.keyWasJustPressedTimeout = setTimeout(() => (this.keyWasJustPressed = false), 500);\n }\n\n complete() {\n this.$('li').eq(this.index).find('button').click();\n }\n\n setIndex(index, scrollToItem) {\n if (this.keyWasJustPressed && !scrollToItem) return;\n\n const $dropdown = this.$();\n const $items = $dropdown.find('li');\n let rangedIndex = index;\n\n if (rangedIndex < 0) {\n rangedIndex = $items.length - 1;\n } else if (rangedIndex >= $items.length) {\n rangedIndex = 0;\n }\n\n this.index = rangedIndex;\n\n const $item = $items.removeClass('active').eq(rangedIndex).addClass('active');\n\n if (scrollToItem) {\n const dropdownScroll = $dropdown.scrollTop();\n const dropdownTop = $dropdown.offset().top;\n const dropdownBottom = dropdownTop + $dropdown.outerHeight();\n const itemTop = $item.offset().top;\n const itemBottom = itemTop + $item.outerHeight();\n\n let scrollTop;\n if (itemTop < dropdownTop) {\n scrollTop = dropdownScroll - dropdownTop + itemTop - parseInt($dropdown.css('padding-top'), 10);\n } else if (itemBottom > dropdownBottom) {\n scrollTop = dropdownScroll - dropdownBottom + itemBottom + parseInt($dropdown.css('padding-bottom'), 10);\n }\n\n if (typeof scrollTop !== 'undefined') {\n $dropdown.stop(true).animate({ scrollTop }, 100);\n }\n }\n }\n}\n","import type MentionableModel from '../MentionableModel';\nimport type Model from 'flarum/common/Model';\n\nexport default abstract class MentionFormat {\n protected instances?: MentionableModel[];\n\n public makeMentionables(): MentionableModel[] {\n return this.instances ?? (this.instances = this.mentionables.map((Mentionable) => new Mentionable(this)));\n }\n\n public getMentionable(type: string): MentionableModel | null {\n return this.makeMentionables().find((mentionable) => mentionable.type() === type) ?? null;\n }\n\n public extend(mentionable: new (...args: any[]) => MentionableModel): void {\n if (!this.extendable) throw new Error('This mention format does not allow extending.');\n\n this.mentionables.push(mentionable);\n }\n\n abstract mentionables: (new (...args: any[]) => MentionableModel)[];\n\n protected abstract extendable: boolean;\n\n abstract trigger(): string;\n\n /**\n * Picks the term to search in the API from the typed text.\n * @example:\n * * Full text = `Hello @\"John D`\n * * Typed text = `\"John D`\n * * Query = `John D`\n */\n abstract queryFromTyped(typed: string): string | null;\n\n abstract format(...args: any): string;\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/avatar'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/helpers/highlight'];","import type Mithril from 'mithril';\nimport type Model from 'flarum/common/Model';\nimport type MentionFormat from './formats/MentionFormat';\n\nexport default abstract class MentionableModel {\n public format: Format;\n\n public constructor(format: Format) {\n this.format = format;\n }\n\n abstract type(): string;\n abstract initialResults(): M[];\n abstract search(typed: string): Promise;\n abstract replacement(model: M): string;\n abstract suggestion(model: M, typed: string): Mithril.Children;\n abstract matches(model: M, typed: string): boolean;\n abstract maxStoreMatchedResults(): number | null;\n abstract enabled(): boolean;\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/extractText'];","import app from 'flarum/forum/app';\nimport extractText from 'flarum/common/utils/extractText';\n\n/**\n * Whether to use the old mentions format.\n *\n * `'@username'` or `'@\"Display name\"'`\n */\nexport const shouldUseOldFormat = () => app.forum.attribute('allowUsernameMentionFormat') || false;\n\nconst getDeletedUserText = () => extractText(app.translator.trans('core.lib.username.deleted_text'));\n\n/**\n * Fetches a user's username or display name.\n *\n * Chooses based on the format option set in the admin settings page.\n *\n * @param user An instance of the User model to fetch the username for\n * @param useDisplayName If `true`, uses `user.displayName()`, otherwise, uses `user.username()`\n */\nexport default function getCleanDisplayName(user, useDisplayName = true) {\n if (!user) return getDeletedUserText().replace(/\"#[a-z]{0,3}[0-9]+/, '_');\n\n const text = (useDisplayName ? user.displayName() : user.username()) || getDeletedUserText();\n\n return text.replace(/\"#[a-z]{0,3}[0-9]+/, '_');\n}\n","import app from 'flarum/forum/app';\nimport type Mithril from 'mithril';\nimport type User from 'flarum/common/models/User';\nimport usernameHelper from 'flarum/common/helpers/username';\nimport avatar from 'flarum/common/helpers/avatar';\nimport highlight from 'flarum/common/helpers/highlight';\nimport MentionableModel from './MentionableModel';\nimport getCleanDisplayName, { shouldUseOldFormat } from '../utils/getCleanDisplayName';\nimport AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class UserMention extends MentionableModel {\n type(): string {\n return 'user';\n }\n\n initialResults(): User[] {\n return Array.from(app.store.all('users'));\n }\n\n /**\n * Automatically determines which mention syntax to be used based on the option in the\n * admin dashboard. Also performs display name clean-up automatically.\n *\n * @\"Display name\"#UserID or `@username`\n *\n * @example
New display name syntax
\n * // '@\"user\"#1'\n * forUser(User) // User is ID 1, display name is 'User'\n *\n * @example
Using old syntax
\n * // '@username'\n * forUser(user) // User's username is 'username'\n */\n public replacement(user: User): string {\n if (shouldUseOldFormat()) {\n const cleanText = getCleanDisplayName(user, false);\n return this.format.format(cleanText);\n }\n\n const cleanText = getCleanDisplayName(user);\n return this.format.format(cleanText, '', user.id());\n }\n\n suggestion(model: User, typed: string): Mithril.Children {\n const username = usernameHelper(model, (name: string) => highlight(name, typed));\n\n return (\n <>\n {avatar(model)}\n {username}\n >\n );\n }\n\n matches(model: User, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.username(), model.displayName()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n async search(typed: string): Promise {\n return await app.store.find('users', { filter: { q: typed }, page: { limit: 5 } });\n }\n\n enabled(): boolean {\n return true;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/ReplyComposer'];","import app from 'flarum/forum/app';\nimport MentionableModel from './MentionableModel';\nimport type Post from 'flarum/common/models/Post';\nimport type Mithril from 'mithril';\nimport usernameHelper from 'flarum/common/helpers/username';\nimport avatar from 'flarum/common/helpers/avatar';\nimport highlight from 'flarum/common/helpers/highlight';\nimport { truncate } from 'flarum/common/utils/string';\nimport ReplyComposer from 'flarum/forum/components/ReplyComposer';\nimport EditPostComposer from 'flarum/forum/components/EditPostComposer';\nimport getCleanDisplayName from '../utils/getCleanDisplayName';\nimport type AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class PostMention extends MentionableModel {\n type(): string {\n return 'post';\n }\n\n /**\n * If the user is replying to a discussion, or if they are editing a\n * post, then we can suggest other posts in the discussion to mention.\n * We will add the 5 most recent comments in the discussion which\n * match any username characters that have been typed.\n */\n initialResults(): Post[] {\n if (!app.composer.bodyMatches(ReplyComposer) && !app.composer.bodyMatches(EditPostComposer)) {\n return [];\n }\n\n // @ts-ignore\n const composerAttrs = app.composer.body.attrs;\n const composerPost = composerAttrs.post;\n const discussion = (composerPost && composerPost.discussion()) || composerAttrs.discussion;\n\n return (\n discussion\n .posts()\n // Filter to only comment posts, and replies before this message\n .filter((post: Post) => post && post.contentType() === 'comment' && (!composerPost || post.number() < composerPost.number()))\n // Sort by new to old\n .sort((a: Post, b: Post) => b.createdAt().getTime() - a.createdAt().getTime())\n );\n }\n\n /**\n * Generates the syntax for mentioning of a post. Also cleans up the display name.\n *\n * @example
Post mention
\n * // '@\"User\"#p13'\n * // @\"Display name\"#pPostID\n * forPostMention(user, 13) // User display name is 'User', post ID is 13\n */\n public replacement(post: Post): string {\n const user = post.user();\n const cleanText = getCleanDisplayName(user);\n return this.format.format(cleanText, 'p', post.id());\n }\n\n suggestion(model: Post, typed: string): Mithril.Children {\n const user = model.user() || null;\n const username = usernameHelper(user, (name: string) => highlight(name, typed));\n\n return (\n <>\n {avatar(user)}\n {username}\n {[\n app.translator.trans('flarum-mentions.forum.composer.reply_to_post_text', { number: model.number() }),\n ' — ',\n truncate(model.contentPlain() ?? '', 200),\n ]}\n >\n );\n }\n\n matches(model: Post, typed: string): boolean {\n const user = model.user();\n const userMentionable = app.mentionFormats.mentionable('user')!;\n\n return !typed || (user && userMentionable.matches(user, typed));\n }\n\n maxStoreMatchedResults(): number {\n return 5;\n }\n\n /**\n * Post mention suggestions are only offered from current discussion posts.\n */\n search(typed: string): Promise {\n return Promise.resolve([]);\n }\n\n enabled(): boolean {\n return true;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Group'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/Badge'];","import app from 'flarum/forum/app';\nimport Group from 'flarum/common/models/Group';\nimport MentionableModel from './MentionableModel';\nimport type Mithril from 'mithril';\nimport Badge from 'flarum/common/components/Badge';\nimport highlight from 'flarum/common/helpers/highlight';\nimport type AtMentionFormat from './formats/AtMentionFormat';\n\nexport default class GroupMention extends MentionableModel {\n type(): string {\n return 'group';\n }\n\n initialResults(): Group[] {\n return Array.from(\n app.store.all('groups').filter((g: Group) => {\n return g.id() !== Group.GUEST_ID && g.id() !== Group.MEMBER_ID;\n })\n );\n }\n\n /**\n * Generates the mention syntax for a group mention.\n *\n * @\"Name Plural\"#gGroupID\n *\n * @example
Group mention
\n * // '@\"Mods\"#g4'\n * forGroup(group) // Group display name is 'Mods', group ID is 4\n */\n public replacement(group: Group): string {\n return this.format.format(group.namePlural(), 'g', group.id());\n }\n\n suggestion(model: Group, typed: string): Mithril.Children {\n let groupName: Mithril.Children = model.namePlural();\n\n if (typed) {\n groupName = highlight(groupName, typed);\n }\n\n return (\n <>\n \n {groupName}\n >\n );\n }\n\n matches(model: Group, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.namePlural().toLowerCase(), model.nameSingular().toLowerCase()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n /**\n * All groups are already loaded, so we don't need to search for them.\n */\n search(typed: string): Promise {\n return Promise.resolve([]);\n }\n\n enabled(): boolean {\n return app.session?.user?.canMentionGroups() ?? false;\n }\n}\n","import MentionFormat from './MentionFormat';\nimport type MentionableModel from '../MentionableModel';\nimport UserMention from '../UserMention';\nimport PostMention from '../PostMention';\nimport GroupMention from '../GroupMention';\n\nexport default class AtMentionFormat extends MentionFormat {\n public mentionables: (new (...args: any[]) => MentionableModel)[] = [UserMention, PostMention, GroupMention];\n protected extendable: boolean = true;\n\n public trigger(): string {\n return '@';\n }\n\n public queryFromTyped(typed: string): string | null {\n const matchTyped = typed.match(/^[\"“]?((?:(?!\"#).)+)$/);\n\n return matchTyped ? matchTyped[1] : null;\n }\n\n public format(name: string, char: string | null = '', id: string | null = null): string {\n return {\n simple: `@${name}`,\n safe: `@\"${name}\"#${char}${id}`,\n }[id ? 'safe' : 'simple'];\n }\n}\n","import app from 'flarum/forum/app';\nimport Badge from 'flarum/common/components/Badge';\nimport highlight from 'flarum/common/helpers/highlight';\nimport type Tag from 'flarum/tags/common/models/Tag';\nimport type Mithril from 'mithril';\nimport MentionableModel from './MentionableModel';\nimport type HashMentionFormat from './formats/HashMentionFormat';\n\nexport default class TagMention extends MentionableModel {\n type(): string {\n return 'tag';\n }\n\n initialResults(): Tag[] {\n return Array.from(app.store.all('tags'));\n }\n\n /**\n * Generates the mention syntax for a tag mention.\n *\n * ~tagSlug\n *\n * @example
Tag mention
\n * // ~general\n * forTag(tag) // Tag display name is 'Tag', tag ID is 5\n */\n public replacement(tag: Tag): string {\n return this.format.format(tag.slug());\n }\n\n matches(model: Tag, typed: string): boolean {\n if (!typed) return false;\n\n const names = [model.name().toLowerCase()];\n\n return names.some((name) => name.toLowerCase().substr(0, typed.length) === typed);\n }\n\n maxStoreMatchedResults(): null {\n return null;\n }\n\n async search(typed: string): Promise {\n return await app.store.find('tags', { filter: { q: typed }, page: { limit: 5 } });\n }\n\n suggestion(model: Tag, typed: string): Mithril.Children {\n let tagName: Mithril.Children = model.name();\n\n if (typed) {\n tagName = highlight(tagName, typed);\n }\n\n return (\n <>\n \n {tagName}\n >\n );\n }\n\n enabled(): boolean {\n return 'flarum-tags' in flarum.extensions;\n }\n}\n","import MentionFormat from './MentionFormat';\nimport MentionableModel from '../MentionableModel';\nimport TagMention from '../TagMention';\n\nexport default class HashMentionFormat extends MentionFormat {\n public mentionables: (new (...args: any[]) => MentionableModel)[] = [TagMention];\n protected extendable: boolean = false;\n\n public trigger(): string {\n return '#';\n }\n\n public queryFromTyped(typed: string): string | null {\n const matchTyped = typed.match(/^[-_\\p{L}\\p{N}\\p{M}]+$/giu);\n\n return matchTyped ? matchTyped[0] : null;\n }\n\n public format(slug: string): string {\n return `#${slug}`;\n }\n}\n","import AtMentionFormat from './AtMentionFormat';\nimport HashMentionFormat from './HashMentionFormat';\nimport type MentionFormat from './MentionFormat';\nimport MentionableModel from '../MentionableModel';\n\nexport default class MentionFormats {\n protected formats: MentionFormat[] = [new AtMentionFormat(), new HashMentionFormat()];\n\n public get(symbol: string): MentionFormat | null {\n return this.formats.find((f) => f.trigger() === symbol) ?? null;\n }\n\n public mentionable(type: string): MentionableModel | null {\n for (const format of this.formats) {\n const mentionable = format.getMentionable(type);\n\n if (mentionable) return mentionable;\n }\n\n return null;\n }\n\n public extend(format: new () => MentionFormat) {\n this.formats.push(new format());\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Component'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/classList'];","import Component from 'flarum/common/Component';\nimport type { ComponentAttrs } from 'flarum/common/Component';\nimport classList from 'flarum/common/utils/classList';\nimport type MentionableModel from '../mentionables/MentionableModel';\nimport type Mithril from 'mithril';\n\nexport interface IMentionsDropdownItemAttrs extends ComponentAttrs {\n mentionable: MentionableModel;\n onclick: () => void;\n onmouseenter: () => void;\n}\n\nexport default class MentionsDropdownItem extends Component {\n view(vnode: Mithril.Vnode): Mithril.Children {\n const { mentionable, ...attrs } = this.attrs;\n\n const className = classList('MentionsDropdownItem', 'PostPreview', `MentionsDropdown-${mentionable.type()}`);\n\n return (\n \n );\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/utils/throttleDebounce'];","import type MentionableModel from './MentionableModel';\nimport type Model from 'flarum/common/Model';\nimport type Mithril from 'mithril';\nimport MentionsDropdownItem from '../components/MentionsDropdownItem';\nimport { throttle } from 'flarum/common/utils/throttleDebounce';\n\nexport default class MentionableModels {\n protected mentionables?: MentionableModel[];\n /**\n * We store models returned from an API here to preserve order in which they are returned\n * This prevents the list jumping around while models are returned.\n * We also use a hashmap for model IDs to provide O(1) lookup for the users already in the list.\n */\n private results: Record> = {};\n public typed: string | null = null;\n private searched: string[] = [];\n private dropdownItemAttrs: Record = {};\n\n constructor(dropdownItemAttrs: Record) {\n this.dropdownItemAttrs = dropdownItemAttrs;\n }\n\n public init(mentionables: MentionableModel[]): void {\n this.typed = null;\n this.mentionables = mentionables;\n\n for (const mentionable of this.mentionables) {\n this.results[mentionable.type()] = new Map(mentionable.initialResults().map((result) => [result.id() as string, result]));\n }\n }\n\n /**\n * Don't send API calls searching for models until at least 2 characters have been typed.\n * This focuses the mention results on models already loaded.\n */\n public readonly search = throttle(250, async (): Promise => {\n if (!this.typed || this.typed.length <= 1) return;\n\n const typedLower = this.typed.toLowerCase();\n\n if (this.searched.includes(typedLower)) return;\n\n for (const mentionable of this.mentionables!) {\n for (const model of await mentionable.search(typedLower)) {\n if (!this.results[mentionable.type()].has(model.id() as string)) {\n this.results[mentionable.type()].set(model.id() as string, model);\n }\n }\n }\n\n this.searched.push(typedLower);\n\n return Promise.resolve();\n });\n\n public matches(mentionable: MentionableModel, model: Model): boolean {\n return mentionable.matches(model, this.typed?.toLowerCase() || '');\n }\n\n public makeSuggestion(mentionable: MentionableModel, model: Model): Mithril.Children {\n const content = mentionable.suggestion(model, this.typed!);\n const replacement = mentionable.replacement(model);\n\n const { onclick, ...attrs } = this.dropdownItemAttrs;\n\n return (\n onclick(replacement)} {...attrs}>\n {content}\n \n );\n }\n\n public buildSuggestions(): Mithril.Children {\n const suggestions: Mithril.Children = [];\n\n for (const mentionable of this.mentionables!) {\n if (!mentionable.enabled()) continue;\n\n let matches = Array.from(this.results[mentionable.type()].values()).filter((model) => this.matches(mentionable, model));\n\n const max = mentionable.maxStoreMatchedResults();\n if (max) matches = matches.splice(0, max);\n\n for (const model of matches) {\n const dropdownItem = this.makeSuggestion(mentionable, model);\n suggestions.push(dropdownItem);\n }\n }\n\n return suggestions;\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/Notification'];","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class PostMentionedNotification extends Notification {\n icon() {\n return 'fas fa-reply';\n }\n\n href() {\n const notification = this.attrs.notification;\n const post = notification.subject();\n const content = notification.content();\n\n return app.route.discussion(post.discussion(), content && content.replyNumber);\n }\n\n content() {\n const notification = this.attrs.notification;\n const user = notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.post_mentioned_text', { user, count: 1 });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain() || '', 200);\n }\n}\n","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class UserMentionedNotification extends Notification {\n icon() {\n return 'fas fa-at';\n }\n\n href() {\n const post = this.attrs.notification.subject();\n\n return app.route.discussion(post.discussion(), post.number());\n }\n\n content() {\n const user = this.attrs.notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.user_mentioned_text', { user });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain(), 200);\n }\n}\n","import app from 'flarum/forum/app';\nimport Notification from 'flarum/forum/components/Notification';\nimport { truncate } from 'flarum/common/utils/string';\n\nexport default class GroupMentionedNotification extends Notification {\n icon() {\n return 'fas fa-at';\n }\n\n href() {\n const post = this.attrs.notification.subject();\n\n return app.route.discussion(post.discussion(), post.number());\n }\n\n content() {\n const user = this.attrs.notification.fromUser();\n\n return app.translator.trans('flarum-mentions.forum.notifications.group_mentioned_text', { user });\n }\n\n excerpt() {\n return truncate(this.attrs.notification.subject().contentPlain(), 200);\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/UserPage'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/components/LinkButton'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/User'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/Model'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/extenders'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['common/models/Post'];","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core.compat['forum/components/PostsUserPage'];","import app from 'flarum/forum/app';\nimport PostsUserPage from 'flarum/forum/components/PostsUserPage';\n\n/**\n * The `MentionsUserPage` component shows post which user Mentioned at\n */\nexport default class MentionsUserPage extends PostsUserPage {\n /**\n * Load a new page of the user's activity feed.\n *\n * @param {Integer} [offset] The position to start getting results from.\n * @return {Promise}\n * @protected\n */\n loadResults(offset) {\n return app.store.find('posts', {\n filter: {\n type: 'comment',\n mentioned: this.user.id(),\n },\n page: { offset, limit: this.loadLimit },\n sort: '-createdAt',\n });\n }\n}\n","import Extend from 'flarum/common/extenders';\nimport Post from 'flarum/common/models/Post';\nimport User from 'flarum/common/models/User';\nimport MentionsUserPage from './components/MentionsUserPage';\n\nexport default [\n new Extend.Routes() //\n .add('user.mentions', '/u/:username/mentions', MentionsUserPage),\n\n new Extend.Model(Post) //\n .hasMany('mentionedBy')\n .attribute('mentionedByCount'),\n\n new Extend.Model(User) //\n .attribute('canMentionGroups'),\n];\n","import app from 'flarum/forum/app';\nimport username from 'flarum/common/helpers/username';\nimport extractText from 'flarum/common/utils/extractText';\n\nexport function filterUserMentions(tag) {\n let user;\n\n if (app.forum.attribute('allowUsernameMentionFormat') && tag.hasAttribute('username'))\n user = app.store.getBy('users', 'username', tag.getAttribute('username'));\n else if (tag.hasAttribute('id')) user = app.store.getById('users', tag.getAttribute('id'));\n\n if (user) {\n tag.setAttribute('id', user.id());\n tag.setAttribute('slug', user.slug());\n tag.setAttribute('displayname', extractText(username(user)));\n\n return true;\n }\n\n tag.invalidate();\n}\n\nexport function postFilterUserMentions(tag) {\n tag.setAttribute('deleted', false);\n}\n\nexport function filterPostMentions(tag) {\n const post = app.store.getById('posts', tag.getAttribute('id'));\n\n if (post) {\n tag.setAttribute('discussionid', post.discussion().id());\n tag.setAttribute('number', post.number());\n tag.setAttribute('displayname', extractText(username(post.user())));\n\n return true;\n }\n}\n\nexport function postFilterPostMentions(tag) {\n tag.setAttribute('deleted', false);\n}\n\nexport function filterGroupMentions(tag) {\n if (app.session?.user?.canMentionGroups()) {\n const group = app.store.getById('groups', tag.getAttribute('id'));\n\n if (group) {\n tag.setAttribute('groupname', extractText(group.namePlural()));\n\n return true;\n }\n }\n\n tag.invalidate();\n}\n\nexport function postFilterGroupMentions(tag) {\n if (app.session?.user?.canMentionGroups()) {\n const group = app.store.getById('groups', tag.getAttribute('id'));\n\n tag.setAttribute('color', group.color());\n tag.setAttribute('icon', group.icon());\n tag.setAttribute('deleted', false);\n }\n}\n\nexport function filterTagMentions(tag) {\n if ('flarum-tags' in flarum.extensions) {\n const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));\n\n if (model) {\n tag.setAttribute('id', model.id());\n tag.setAttribute('tagname', model.name());\n\n return true;\n }\n }\n\n tag.invalidate();\n}\n\nexport function postFilterTagMentions(tag) {\n if ('flarum-tags' in flarum.extensions) {\n const model = app.store.getBy('tags', 'slug', tag.getAttribute('slug'));\n\n tag.setAttribute('icon', model.icon());\n tag.setAttribute('color', model.color());\n tag.setAttribute('deleted', false);\n }\n}\n","import GroupMentionedNotification from './components/GroupMentionedNotification';\nimport MentionsUserPage from './components/MentionsUserPage';\nimport PostMentionedNotification from './components/PostMentionedNotification';\nimport UserMentionedNotification from './components/UserMentionedNotification';\nimport AutocompleteDropdown from './fragments/AutocompleteDropdown';\nimport PostQuoteButton from './fragments/PostQuoteButton';\nimport getCleanDisplayName from './utils/getCleanDisplayName';\nimport getMentionText from './utils/getMentionText';\nimport * as reply from './utils/reply';\nimport selectedText from './utils/selectedText';\nimport * as textFormatter from './utils/textFormatter';\nimport MentionableModel from './mentionables/MentionableModel';\nimport MentionFormat from './mentionables/formats/MentionFormat';\nimport Mentionables from './extenders/Mentionables';\n\nexport default {\n 'mentions/components/MentionsUserPage': MentionsUserPage,\n 'mentions/components/PostMentionedNotification': PostMentionedNotification,\n 'mentions/components/UserMentionedNotification': UserMentionedNotification,\n 'mentions/components/GroupMentionedNotification': GroupMentionedNotification,\n 'mentions/fragments/AutocompleteDropdown': AutocompleteDropdown,\n 'mentions/fragments/PostQuoteButton': PostQuoteButton,\n 'mentions/utils/getCleanDisplayName': getCleanDisplayName,\n 'mentions/utils/getMentionText': getMentionText,\n 'mentions/utils/reply': reply,\n 'mentions/utils/selectedText': selectedText,\n 'mentions/utils/textFormatter': textFormatter,\n 'mentions/mentionables/MentionableModel': MentionableModel,\n 'mentions/mentionables/formats/MentionFormat': MentionFormat,\n 'mentions/extenders/Mentionables': Mentionables,\n};\n","import app from 'flarum/forum/app';\n\n/**\n * Fetches the mention text for a specified user (and optionally a post ID for replies or group).\n *\n * Automatically determines which mention syntax to be used based on the option in the\n * admin dashboard. Also performs display name clean-up automatically.\n *\n * @deprecated Use `app.mentionables.get('user').replacement(user)` instead. Will be removed in 2.0.\n */\nexport default function getMentionText(user, postId, group) {\n if (user !== undefined && postId === undefined) {\n return app.mentionables.get('user').replacement(user);\n } else if (user !== undefined && postId !== undefined) {\n return app.mentionables.get('post').replacement(app.store.getById('posts', postId));\n } else if (group !== undefined) {\n return app.mentionables.get('group').replacement(group);\n }\n\n throw 'No parameters were passed';\n}\n","import type ForumApplication from 'flarum/forum/ForumApplication';\nimport type IExtender from 'flarum/common/extenders/IExtender';\nimport type MentionableModel from '../mentionables/MentionableModel';\nimport type MentionFormat from '../mentionables/formats/MentionFormat';\n\nexport default class Mentionables implements IExtender {\n protected formats: (new () => MentionFormat)[] = [];\n protected mentionables: Record MentionableModel)[]> = {};\n\n /**\n * Register a new mention format.\n * Must extend MentionFormat and have a unique unused trigger symbol.\n */\n format(format: new () => MentionFormat): this {\n this.formats.push(format);\n\n return this;\n }\n\n /**\n * Register a new mentionable model to a mention format.\n * Only works if the format has already been registered,\n * and the format allows using multiple mentionables.\n *\n * @param symbol The trigger symbol of the format to extend (ex: @).\n * @param mentionable The mentionable instance to register.\n * Must extend MentionableModel.\n */\n mentionable(symbol: string, mentionable: new () => MentionableModel): this {\n if (!this.mentionables[symbol]) {\n this.mentionables[symbol] = [];\n }\n\n this.mentionables[symbol].push(mentionable);\n\n return this;\n }\n\n extend(app: ForumApplication): void {\n for (const format of this.formats) {\n app.mentionFormats.extend(format);\n }\n\n for (const symbol in this.mentionables) {\n const format = app.mentionFormats.get(symbol);\n\n if (!format) continue;\n\n for (const mentionable of this.mentionables[symbol]) {\n format.extend(mentionable);\n }\n }\n }\n}\n","const __WEBPACK_NAMESPACE_OBJECT__ = flarum.core;","import { extend } from 'flarum/common/extend';\nimport app from 'flarum/forum/app';\nimport NotificationGrid from 'flarum/forum/components/NotificationGrid';\nimport { getPlainContent } from 'flarum/common/utils/string';\nimport textContrastClass from 'flarum/common/helpers/textContrastClass';\nimport Post from 'flarum/forum/components/Post';\n\nimport addPostMentionPreviews from './addPostMentionPreviews';\nimport addMentionedByList from './addMentionedByList';\nimport addPostReplyAction from './addPostReplyAction';\nimport addPostQuoteButton from './addPostQuoteButton';\nimport addComposerAutocomplete from './addComposerAutocomplete';\nimport PostMentionedNotification from './components/PostMentionedNotification';\nimport UserMentionedNotification from './components/UserMentionedNotification';\nimport GroupMentionedNotification from './components/GroupMentionedNotification';\nimport UserPage from 'flarum/forum/components/UserPage';\nimport LinkButton from 'flarum/common/components/LinkButton';\nimport User from 'flarum/common/models/User';\nimport Model from 'flarum/common/Model';\n\nexport { default as extend } from './extend';\n\napp.initializers.add('flarum-mentions', function () {\n // For every mention of a post inside a post's content, set up a hover handler\n // that shows a preview of the mentioned post.\n addPostMentionPreviews();\n\n // In the footer of each post, show information about who has replied (i.e.\n // who the post has been mentioned by).\n addMentionedByList();\n\n // Add a 'reply' control to the footer of each post. When clicked, it will\n // open up the composer and add a post mention to its contents.\n addPostReplyAction();\n\n // Show a Quote button when Post text is selected\n addPostQuoteButton();\n\n // After typing '@' in the composer, show a dropdown suggesting a bunch of\n // posts or users that the user could mention.\n addComposerAutocomplete();\n\n app.notificationComponents.postMentioned = PostMentionedNotification;\n app.notificationComponents.userMentioned = UserMentionedNotification;\n app.notificationComponents.groupMentioned = GroupMentionedNotification;\n\n // Add notification preferences.\n extend(NotificationGrid.prototype, 'notificationTypes', function (items) {\n items.add('postMentioned', {\n name: 'postMentioned',\n icon: 'fas fa-reply',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_post_mentioned_label'),\n });\n\n items.add('userMentioned', {\n name: 'userMentioned',\n icon: 'fas fa-at',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_user_mentioned_label'),\n });\n\n items.add('groupMentioned', {\n name: 'groupMentioned',\n icon: 'fas fa-at',\n label: app.translator.trans('flarum-mentions.forum.settings.notify_group_mentioned_label'),\n });\n });\n\n // Add mentions tab in user profile\n extend(UserPage.prototype, 'navItems', function (items) {\n const user = this.user;\n items.add(\n 'mentions',\n \n {app.translator.trans('flarum-mentions.forum.user.mentions_link')}\n ,\n 80\n );\n });\n\n // Remove post mentions when rendering post previews.\n getPlainContent.removeSelectors.push('a.PostMention');\n\n // Apply color contrast fix on group mentions.\n extend(Post.prototype, 'oncreate', function () {\n this.$('.GroupMention--colored, .TagMention--colored').each(function () {\n this.classList.add(textContrastClass(getComputedStyle(this).getPropertyValue('--color')));\n });\n });\n});\n\nexport * from './utils/textFormatter';\n\n// Expose compat API\nimport mentionsCompat from './compat';\nimport { compat } from '@flarum/core/forum';\n\nObject.assign(compat, mentionsCompat);\n","import { extend } from 'flarum/common/extend';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nimport PostPreview from 'flarum/forum/components/PostPreview';\nimport LoadingIndicator from 'flarum/common/components/LoadingIndicator';\n\nexport default function addPostMentionPreviews() {\n function addPreviews() {\n const contentHtml = this.attrs.post.contentHtml();\n\n if (contentHtml === this.oldPostContentHtml || this.isEditing()) return;\n\n this.oldPostContentHtml = contentHtml;\n\n const parentPost = this.attrs.post;\n const $parentPost = this.$();\n\n this.$().on(\n 'click',\n '.UserMention:not(.UserMention--deleted), .PostMention:not(.PostMention--deleted), .TagMention:not(.TagMention--deleted)',\n function (e) {\n m.route.set(this.getAttribute('href'));\n e.preventDefault();\n }\n );\n\n this.$('.PostMention:not(.PostMention--deleted)').each(function () {\n const $this = $(this);\n const id = $this.data('id');\n let timeout;\n\n // Wrap the mention link in a wrapper element so that we can insert a\n // preview popup as its sibling and relatively position it.\n const $preview = $('
');\n $parentPost.append($preview);\n\n const getPostElement = () => {\n return $(`.PostStream-item[data-id=\"${id}\"]`);\n };\n\n const showPreview = () => {\n // When the user hovers their mouse over the mention, look for the\n // post that it's referring to in the stream, and determine if it's\n // in the viewport. If it is, we will \"pulsate\" it.\n const $post = getPostElement();\n let visible = false;\n if ($post.length) {\n const top = $post.offset().top;\n const scrollTop = window.pageYOffset;\n if (top > scrollTop && top + $post.height() < scrollTop + $(window).height()) {\n $post.addClass('pulsate');\n visible = true;\n }\n }\n\n // Otherwise, we will show a popup preview of the post. If the post\n // hasn't yet been loaded, we will need to do that.\n if (!visible) {\n // Position the preview so that it appears above the mention.\n // (The offsetParent should be .Post-body.)\n const positionPreview = () => {\n const previewHeight = $preview.outerHeight(true);\n let offset = 0;\n\n // If the preview goes off the top of the viewport, reposition it to\n // be below the mention.\n if ($this.offset().top - previewHeight < $(window).scrollTop() + $('#header').outerHeight()) {\n offset += $this.outerHeight(true);\n } else {\n offset -= previewHeight;\n }\n\n $preview\n .show()\n .css('top', $this.offset().top - $parentPost.offset().top + offset)\n .css('left', $this.offsetParent().offset().left - $parentPost.offset().left)\n .css('max-width', $this.offsetParent().width());\n };\n\n const showPost = (post) => {\n const discussion = post.discussion();\n\n m.render($preview[0], [\n discussion !== parentPost.discussion() && (\n
\n {discussion.title()}\n
\n ),\n
\n \n
,\n ]);\n positionPreview();\n };\n\n const post = app.store.getById('posts', id);\n if (post && post.discussion()) {\n showPost(post);\n } else {\n m.render($preview[0], );\n app.store.find('posts', id).then(showPost);\n positionPreview();\n }\n\n setTimeout(() => $preview.off('transitionend').addClass('in'));\n }\n };\n\n const hidePreview = () => {\n getPostElement().removeClass('pulsate');\n if ($preview.hasClass('in')) {\n $preview.removeClass('in').one('transitionend', () => $preview.hide());\n }\n };\n\n // On a touch (mobile) device we cannot hover the link to reveal the preview.\n // Instead we cancel the navigation so that a click reveals the preview.\n // Users can then click on the preview to go to the post if desired.\n $this.on('touchend', (e) => {\n if (e.cancelable) {\n e.preventDefault();\n }\n });\n\n $this\n .add($preview)\n .hover(\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(showPreview, 250);\n },\n () => {\n clearTimeout(timeout);\n getPostElement().removeClass('pulsate');\n timeout = setTimeout(hidePreview, 250);\n }\n )\n .on('touchend', (e) => {\n showPreview();\n e.stopPropagation();\n });\n\n $(document).on('touchend', hidePreview);\n });\n }\n\n extend(CommentPost.prototype, 'oncreate', addPreviews);\n extend(CommentPost.prototype, 'onupdate', addPreviews);\n}\n","import app from 'flarum/forum/app';\nimport { extend } from 'flarum/common/extend';\nimport CommentPost from 'flarum/forum/components/CommentPost';\nimport Link from 'flarum/common/components/Link';\nimport PostPreview from 'flarum/forum/components/PostPreview';\nimport punctuateSeries from 'flarum/common/helpers/punctuateSeries';\nimport username from 'flarum/common/helpers/username';\nimport icon from 'flarum/common/helpers/icon';\nimport Button from 'flarum/common/components/Button';\nimport MentionedByModal from './components/MentionedByModal';\n\nexport default function addMentionedByList() {\n function hidePreview() {\n this.$('.Post-mentionedBy-preview')\n .removeClass('in')\n .one('transitionend', function () {\n $(this).hide();\n });\n }\n\n extend(CommentPost.prototype, 'oncreate', function () {\n let timeout;\n const post = this.attrs.post;\n const replies = post.mentionedBy();\n\n if (replies && replies.length) {\n const $preview = $('
');\n this.$().append($preview);\n\n const $parentPost = this.$();\n const $this = this.$('.Post-mentionedBy');\n\n const showPreview = () => {\n if (!$preview.hasClass('in') && $preview.is(':visible')) return;\n\n // When the user hovers their mouse over the list of people who have\n // replied to the post, render a list of reply previews into a\n // popup.\n m.render(\n $preview[0],\n <>\n {replies.map((reply) => (\n
\n )}\n >\n );\n\n $preview\n .show()\n .css('top', $this.offset().top - $parentPost.offset().top + $this.outerHeight(true))\n .css('left', $this.offsetParent().offset().left - $parentPost.offset().left)\n .css('max-width', $parentPost.width());\n\n setTimeout(() => $preview.off('transitionend').addClass('in'));\n };\n\n $this.add($preview).hover(\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(showPreview, 250);\n },\n () => {\n clearTimeout(timeout);\n timeout = setTimeout(hidePreview, 250);\n }\n );\n\n // Whenever the user hovers their mouse over a particular name in the\n // list of repliers, highlight the corresponding post in the preview\n // popup.\n this.$()\n .find('.Post-mentionedBy-summary a')\n .hover(\n function () {\n $preview.find('[data-number=\"' + $(this).data('number') + '\"]').addClass('active');\n },\n function () {\n $preview.find('[data-number]').removeClass('active');\n }\n );\n }\n });\n\n extend(CommentPost.prototype, 'footerItems', function (items) {\n const post = this.attrs.post;\n const replies = post.mentionedBy();\n\n if (replies && replies.length) {\n const users = [];\n const repliers = replies\n .sort((reply) => (reply.user() === app.session.user ? -1 : 0))\n .filter((reply) => {\n const user = reply.user();\n if (users.indexOf(user) === -1) {\n users.push(user);\n return true;\n }\n });\n\n const limit = 4;\n const overLimit = repliers.length > limit;\n\n // Create a list of unique users who have replied. So even if a user has\n // replied twice, they will only be in this array once.\n const names = repliers.slice(0, overLimit ? limit - 1 : limit).map((reply) => {\n const user = reply.user();\n\n return (\n \n {app.session.user === user ? app.translator.trans('flarum-mentions.forum.post.you_text') : username(user)}\n \n );\n });\n\n // If there are more users that we've run out of room to display, add a \"x\n // others\" name to the end of the list. Clicking on it will display a modal\n // with a full list of names.\n if (overLimit) {\n const count = repliers.length - names.length;\n\n names.push(app.translator.trans('flarum-mentions.forum.post.others_text', { count }));\n }\n\n items.add(\n 'replies',\n