mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-15 04:18:18 +00:00
refac
This commit is contained in:
parent
d51fffb468
commit
f9c524d31d
11 changed files with 327 additions and 60 deletions
|
|
@ -1,5 +1,5 @@
|
|||
import { WEBUI_API_BASE_URL } from '$lib/constants';
|
||||
import type { App } from '$lib/apis/apps';
|
||||
import { getApps, type App } from '$lib/apis/apps';
|
||||
|
||||
type ChannelForm = {
|
||||
type?: string;
|
||||
|
|
@ -970,6 +970,11 @@ export const getChannelAppsById = async (
|
|||
return res;
|
||||
};
|
||||
|
||||
export const getChannelMentionApps = async (token: string, channelId: string): Promise<App[]> => {
|
||||
const [apps, joined] = await Promise.all([getApps(token), getChannelAppsById(token, channelId)]);
|
||||
return apps.filter((app) => app.is_active && joined.some((member) => member.id === app.id));
|
||||
};
|
||||
|
||||
export const inviteAppToChannel = async (
|
||||
token: string = '',
|
||||
channel_id: string,
|
||||
|
|
|
|||
|
|
@ -374,7 +374,7 @@
|
|||
<Messages
|
||||
{channel}
|
||||
{top}
|
||||
{messages}
|
||||
bind:messages
|
||||
{replyToMessage}
|
||||
onReply={async (message) => {
|
||||
replyToMessage = message;
|
||||
|
|
@ -407,6 +407,7 @@
|
|||
<div class=" pb-3 px-3">
|
||||
<MessageInput
|
||||
id="root"
|
||||
{messages}
|
||||
bind:chatInputElement
|
||||
bind:replyToMessage
|
||||
{typingUsers}
|
||||
|
|
|
|||
|
|
@ -7,8 +7,19 @@
|
|||
|
||||
const i18n: any = getContext('i18n');
|
||||
|
||||
import { channelRequestQueues, config, mobile, settings, socket, user } from '$lib/stores';
|
||||
import {
|
||||
channelRequestQueues,
|
||||
channels,
|
||||
models,
|
||||
config,
|
||||
mobile,
|
||||
settings,
|
||||
socket,
|
||||
user
|
||||
} from '$lib/stores';
|
||||
import { processingQueueChannels, processNextInQueue } from '$lib/utils/channelQueue';
|
||||
import { getMentionUserQueries, resolveMentions, getChannelMentionCandidates } from '$lib/utils/channelMentions';
|
||||
import type { Editor } from '@tiptap/core';
|
||||
import QueuedMessageItem from '../chat/MessageInput/QueuedMessageItem.svelte';
|
||||
import {
|
||||
convertHeicToJpeg,
|
||||
|
|
@ -50,6 +61,7 @@
|
|||
|
||||
export let id: string | null = null;
|
||||
export let channel: any = null;
|
||||
export let messages: any[] | null = [];
|
||||
|
||||
export let typingUsers = [];
|
||||
export let inputLoading = false;
|
||||
|
|
@ -77,6 +89,7 @@
|
|||
|
||||
let recording = false;
|
||||
let content = '';
|
||||
let inputEditor: Editor | null = null;
|
||||
let files: any[] = [];
|
||||
let isDestroyed = false;
|
||||
$: parentId = id === 'root' ? null : id;
|
||||
|
|
@ -579,10 +592,43 @@
|
|||
void processNextInQueue(queueKey);
|
||||
};
|
||||
|
||||
const resolveInputMentions = async () => {
|
||||
const targetQueueKey = queueKey;
|
||||
const editor = inputEditor;
|
||||
if (editor && /[@#]/.test(content)) {
|
||||
const state = editor.state;
|
||||
try {
|
||||
const candidates = await getChannelMentionCandidates(
|
||||
localStorage.token,
|
||||
channel.id,
|
||||
getMentionUserQueries(state.doc),
|
||||
{
|
||||
users: userSuggestions,
|
||||
apps: $config?.features?.enable_apps ?? false,
|
||||
models: $models,
|
||||
channels: channelSuggestions ? $channels : []
|
||||
}
|
||||
);
|
||||
if (isDestroyed || targetQueueKey !== queueKey || editor.isDestroyed) return false;
|
||||
const tr = resolveMentions(state, candidates);
|
||||
// A paste lookup may have finished while Send was waiting for the same result.
|
||||
if (editor.state.doc.eq(tr.doc)) return true;
|
||||
if (editor.state.doc !== state.doc) return false;
|
||||
if (tr.docChanged) editor.view.dispatch(tr);
|
||||
} catch (error) {
|
||||
toast.error(`${error}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const submitHandler = async () => {
|
||||
if (disabled || !$user?.id || (content === '' && files.length === 0)) return;
|
||||
|
||||
const targetQueueKey = queueKey;
|
||||
if (inputEditor && /[@#]/.test(content) && !(await resolveInputMentions())) return;
|
||||
if (content === '' && files.length === 0) return;
|
||||
const entry = {
|
||||
id: uuidv4(),
|
||||
prompt: content,
|
||||
|
|
@ -999,6 +1045,7 @@
|
|||
id="chat-input"
|
||||
className="input-prose min-h-fit h-full text-sm!"
|
||||
bind:this={chatInputElement}
|
||||
bind:editor={inputEditor}
|
||||
json={true}
|
||||
messageInput={true}
|
||||
editable={!disabled}
|
||||
|
|
@ -1015,6 +1062,7 @@
|
|||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||
floatingMenuPlacement={'top-start'}
|
||||
{suggestions}
|
||||
on:pasted={resolveInputMentions}
|
||||
onChange={(e) => {
|
||||
const { md } = e;
|
||||
content = md;
|
||||
|
|
@ -1028,6 +1076,30 @@
|
|||
document.getElementById('suggestions-container');
|
||||
|
||||
if (!suggestionsContainerElement) {
|
||||
if (
|
||||
e.key === 'ArrowUp' &&
|
||||
!e.isComposing &&
|
||||
!isCtrlPressed &&
|
||||
!e.shiftKey &&
|
||||
!e.altKey &&
|
||||
!disabled &&
|
||||
content === '' &&
|
||||
files.length === 0
|
||||
) {
|
||||
const message = messages?.find(
|
||||
(message) => message.user_id === $user?.id && !message.temp_id
|
||||
);
|
||||
if (message) {
|
||||
const messageId = parentId ? `${parentId}-${message.id}` : message.id;
|
||||
const editButton = document.getElementById(
|
||||
`edit-message-${messageId}`
|
||||
);
|
||||
if (editButton) {
|
||||
e.preventDefault();
|
||||
editButton.click();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (
|
||||
!$mobile ||
|
||||
!(
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
import { getAbortSignal, getContext, onMount } from 'svelte';
|
||||
const i18n = getContext('i18n');
|
||||
|
||||
import { getChannelAppsById } from '$lib/apis/channels';
|
||||
import { getApps } from '$lib/apis/apps';
|
||||
import { getChannelMentionApps } from '$lib/apis/channels';
|
||||
import { mentionAttributes } from '$lib/utils/channelMentions';
|
||||
import { channels, models, config } from '$lib/stores';
|
||||
import Tooltip from '$lib/components/common/Tooltip.svelte';
|
||||
import Grid from '$lib/components/icons/Grid.svelte';
|
||||
|
|
@ -90,14 +90,7 @@
|
|||
const item = filteredItems[index];
|
||||
if (!item) return;
|
||||
|
||||
// Add the "U:", "M:" or "C:" prefix to the id
|
||||
// and also append the label after a pipe |
|
||||
// so that the mention renderer can show the label
|
||||
if (item)
|
||||
command({
|
||||
id: `${item.type === 'user' ? 'U' : item.type === 'model' ? 'M' : item.type === 'app' ? 'A' : 'C'}:${item.id}|${item.label}`,
|
||||
label: item.label
|
||||
});
|
||||
command(mentionAttributes(item));
|
||||
};
|
||||
|
||||
const onKeyDown = (event: KeyboardEvent) => {
|
||||
|
|
@ -138,16 +131,14 @@
|
|||
|
||||
onMount(() => {
|
||||
if ($config?.features?.enable_apps && channelId && triggerChar === '@') {
|
||||
Promise.all([getApps(localStorage.token), getChannelAppsById(localStorage.token, channelId)])
|
||||
.then(([apps, joined]) => {
|
||||
_apps = apps
|
||||
.filter((a) => a.is_active && joined.some((member) => member.id === a.id))
|
||||
.map((a) => ({
|
||||
type: 'app',
|
||||
id: a.id,
|
||||
label: a.name,
|
||||
profile_image_url: a.data.manifest.profile_image_url
|
||||
}));
|
||||
getChannelMentionApps(localStorage.token, channelId)
|
||||
.then((apps) => {
|
||||
_apps = apps.map((a) => ({
|
||||
type: 'app',
|
||||
id: a.id,
|
||||
label: a.name,
|
||||
profile_image_url: a.data.manifest.profile_image_url
|
||||
}));
|
||||
})
|
||||
.catch(() => {});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -160,6 +160,9 @@
|
|||
disabled={!channel?.write_access || message?.temp_id}
|
||||
pending={!!message?.temp_id}
|
||||
showUserProfile={messageIdx === 0 ||
|
||||
Object.values(appReplies).some(
|
||||
(event) => event.message_id === messageList.at(messageIdx - 1)?.id
|
||||
) ||
|
||||
messageList.at(messageIdx - 1)?.user_id !== message.user_id ||
|
||||
messageList.at(messageIdx - 1)?.user?.id !== message.user?.id ||
|
||||
messageList.at(messageIdx - 1)?.meta?.model_id !== message?.meta?.model_id ||
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@
|
|||
dayjs.extend(isYesterday);
|
||||
dayjs.extend(localizedFormat);
|
||||
|
||||
import { getContext } from 'svelte';
|
||||
import { getContext, tick } from 'svelte';
|
||||
const i18n = getContext<Writable<i18nType>>('i18n');
|
||||
|
||||
import { formatDate } from '$lib/utils';
|
||||
|
|
@ -286,10 +286,18 @@
|
|||
{#if onEdit}
|
||||
<Tooltip content={$i18n.t('Edit')}>
|
||||
<button
|
||||
id="edit-message-{renderedMessageId}"
|
||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||
on:click={() => {
|
||||
on:click={async () => {
|
||||
edit = true;
|
||||
editedContent = message.content;
|
||||
await tick();
|
||||
const textarea = document
|
||||
.getElementById(`message-${renderedMessageId}`)
|
||||
?.querySelector('textarea');
|
||||
textarea?.focus();
|
||||
textarea?.setSelectionRange(textarea.value.length, textarea.value.length);
|
||||
textarea?.scrollIntoView({ block: 'center' });
|
||||
}}
|
||||
>
|
||||
<Pencil />
|
||||
|
|
@ -543,7 +551,7 @@
|
|||
{#if edit}
|
||||
<div class="py-2">
|
||||
<Textarea
|
||||
className=" bg-transparent outline-hidden w-full resize-none"
|
||||
className=" bg-transparent outline-hidden w-full resize-none text-sm leading-[1.375rem]"
|
||||
bind:value={editedContent}
|
||||
onKeydown={(e) => {
|
||||
if (e.key === 'Escape') {
|
||||
|
|
|
|||
|
|
@ -222,7 +222,7 @@
|
|||
id={threadId}
|
||||
{channel}
|
||||
{top}
|
||||
{messages}
|
||||
bind:messages
|
||||
{replyToMessage}
|
||||
thread={true}
|
||||
{onPin}
|
||||
|
|
@ -258,6 +258,7 @@
|
|||
|
||||
<div class=" pb-3 px-3 w-full">
|
||||
<MessageInput
|
||||
{messages}
|
||||
bind:replyToMessage
|
||||
bind:chatInputElement
|
||||
id={threadId}
|
||||
|
|
|
|||
|
|
@ -134,37 +134,45 @@
|
|||
|
||||
<LinkPreview.Root openDelay={0} closeDelay={0} bind:open={openPreview}>
|
||||
<LinkPreview.Trigger class=" cursor-pointer no-underline! font-normal! ">
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
{#snippet child({ props })}
|
||||
<span {...props}>
|
||||
<!-- svelte-ignore a11y-click-events-have-key-events -->
|
||||
<!-- svelte-ignore a11y-no-static-element-interactions -->
|
||||
|
||||
<span
|
||||
class="mention"
|
||||
on:click={async () => {
|
||||
if (triggerChar === '@') {
|
||||
if (idType === 'U') {
|
||||
// Open user profile
|
||||
console.log('Clicked user mention', id);
|
||||
} else if (idType === 'M') {
|
||||
console.log('Clicked model mention', id);
|
||||
await goto(`/?model=${id}`);
|
||||
}
|
||||
} else if (triggerChar === '#') {
|
||||
if (idType === 'C') {
|
||||
// Open channel
|
||||
if ($channels.find((c) => c.id === id)) {
|
||||
await goto(`/channels/${id}`);
|
||||
<span
|
||||
class="mention"
|
||||
data-type="mention"
|
||||
data-id={`${token.id.split('|')[0]}|${label}`}
|
||||
data-label={label}
|
||||
data-mention-suggestion-char={triggerChar}
|
||||
on:click={async () => {
|
||||
if (triggerChar === '@') {
|
||||
if (idType === 'U') {
|
||||
// Open user profile
|
||||
console.log('Clicked user mention', id);
|
||||
} else if (idType === 'M') {
|
||||
console.log('Clicked model mention', id);
|
||||
await goto(`/?model=${id}`);
|
||||
}
|
||||
} else if (triggerChar === '#') {
|
||||
if (idType === 'C') {
|
||||
// Open channel
|
||||
if ($channels.find((c) => c.id === id)) {
|
||||
await goto(`/channels/${id}`);
|
||||
}
|
||||
} else if (idType === 'T') {
|
||||
// Open thread
|
||||
}
|
||||
} else {
|
||||
// Unknown trigger char, just log
|
||||
console.log('Clicked mention', id);
|
||||
}
|
||||
} else if (idType === 'T') {
|
||||
// Open thread
|
||||
}
|
||||
} else {
|
||||
// Unknown trigger char, just log
|
||||
console.log('Clicked mention', id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{triggerChar}{label}
|
||||
</span>
|
||||
}}
|
||||
>
|
||||
{triggerChar}{label}
|
||||
</span>
|
||||
</span>
|
||||
{/snippet}
|
||||
</LinkPreview.Trigger>
|
||||
|
||||
{#if triggerChar === '@' && idType === 'U'}
|
||||
|
|
|
|||
|
|
@ -138,7 +138,7 @@
|
|||
const i18n = getContext<typeof import('$lib/i18n').default>('i18n');
|
||||
const eventDispatch = createEventDispatcher();
|
||||
|
||||
import { Fragment, DOMParser } from 'prosemirror-model';
|
||||
import { Fragment, DOMParser, Slice, type Node, type Schema } from 'prosemirror-model';
|
||||
import { EditorState, Plugin, PluginKey, TextSelection, Selection } from 'prosemirror-state';
|
||||
import { Decoration, DecorationSet } from 'prosemirror-view';
|
||||
import { Editor, Extension, markInputRule, mergeAttributes } from '@tiptap/core';
|
||||
|
|
@ -303,6 +303,27 @@
|
|||
return `${char}${label}`;
|
||||
};
|
||||
|
||||
// Plain-text mode still preserves mention identities from the clipboard's HTML.
|
||||
const plainMentionPaste = (slice: Slice, schema: Schema): Fragment | null => {
|
||||
const nodes: Node[] = [];
|
||||
let hasMention = false;
|
||||
let firstBlock = true;
|
||||
slice.content.descendants((node) => {
|
||||
if (node.isTextblock) {
|
||||
if (!firstBlock) nodes.push(schema.nodes.hardBreak.create());
|
||||
firstBlock = false;
|
||||
}
|
||||
if (node.type.name === 'mention') {
|
||||
hasMention = true;
|
||||
nodes.push(node.mark([]));
|
||||
return false;
|
||||
}
|
||||
if (node.isText) nodes.push(schema.text(node.text!));
|
||||
else if (node.type.name === 'hardBreak') nodes.push(node.mark([]));
|
||||
});
|
||||
return hasMention ? Fragment.fromArray(nodes) : null;
|
||||
};
|
||||
|
||||
export let onSelectionUpdate = (e) => {};
|
||||
|
||||
export let id = '';
|
||||
|
|
@ -992,6 +1013,7 @@
|
|||
json: jsonValue,
|
||||
md: mdValue
|
||||
});
|
||||
if (transaction.getMeta('paste')) eventDispatch('pasted');
|
||||
|
||||
if (json) {
|
||||
value = jsonValue;
|
||||
|
|
@ -1016,6 +1038,18 @@
|
|||
// the tiptap placeholder never becomes the field's accessible name;
|
||||
// function form so a placeholder change is picked up after mount
|
||||
attributes: () => ({ id, 'aria-label': _placeholder }),
|
||||
transformPasted: (slice) => {
|
||||
const stripMentionLinks = (fragment: Fragment): Fragment => {
|
||||
const nodes: Node[] = [];
|
||||
fragment.forEach((node) => {
|
||||
nodes.push(node.type.name === 'mention'
|
||||
? node.mark(node.marks.filter((mark) => mark.type.name !== 'link'))
|
||||
: node.isLeaf ? node : node.copy(stripMentionLinks(node.content)));
|
||||
});
|
||||
return Fragment.fromArray(nodes);
|
||||
};
|
||||
return new Slice(stripMentionLinks(slice.content), slice.openStart, slice.openEnd);
|
||||
},
|
||||
handleDrop: (view, event) => {
|
||||
// Intercept sidebar chat item drops to prevent ProseMirror
|
||||
// from inserting the raw JSON as text. The actual handling
|
||||
|
|
@ -1034,7 +1068,7 @@
|
|||
}
|
||||
return false;
|
||||
},
|
||||
handlePaste: (view, event) => {
|
||||
handlePaste: (view, event, slice) => {
|
||||
// Paste literal text when automatic formatting is disabled.
|
||||
if (!richText || !autoFormat) {
|
||||
// swallow HTML completely
|
||||
|
|
@ -1051,6 +1085,15 @@
|
|||
return true;
|
||||
}
|
||||
|
||||
const mentions = plainMentionPaste(slice, state.schema);
|
||||
if (mentions) {
|
||||
dispatch(
|
||||
state.tr.replaceWith(state.selection.from, state.selection.to, mentions)
|
||||
.setMeta('paste', true).scrollIntoView()
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
const lines = plainText.split('\n');
|
||||
const nodes = [];
|
||||
|
||||
|
|
@ -1067,6 +1110,7 @@
|
|||
dispatch(
|
||||
state.tr
|
||||
.replaceWith(state.selection.from, state.selection.to, fragment)
|
||||
.setMeta('paste', true)
|
||||
.scrollIntoView()
|
||||
);
|
||||
|
||||
|
|
@ -1252,7 +1296,7 @@
|
|||
});
|
||||
|
||||
const fragment = Fragment.fromArray(nodes);
|
||||
const tr = state.tr.replaceWith(from, to, fragment);
|
||||
const tr = state.tr.replaceWith(from, to, fragment).setMeta('paste', true);
|
||||
dispatch(tr.scrollIntoView());
|
||||
event.preventDefault();
|
||||
return true;
|
||||
|
|
|
|||
133
src/lib/utils/channelMentions.ts
Normal file
133
src/lib/utils/channelMentions.ts
Normal file
|
|
@ -0,0 +1,133 @@
|
|||
import { searchUsers } from '$lib/apis/users';
|
||||
import { getChannelMentionApps } from '$lib/apis/channels';
|
||||
|
||||
import type { Node } from '@tiptap/pm/model';
|
||||
import type { EditorState } from '@tiptap/pm/state';
|
||||
|
||||
export type MentionItem = {
|
||||
type: 'user' | 'model' | 'app' | 'channel';
|
||||
id: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export const mentionAttributes = (item: MentionItem) => ({
|
||||
id: `${{ user: 'U', model: 'M', app: 'A', channel: 'C' }[item.type]}:${item.id}|${item.label}`,
|
||||
label: item.label,
|
||||
mentionSuggestionChar: item.type === 'channel' ? '#' : '@'
|
||||
});
|
||||
|
||||
const mentionTextBlocks = (doc: Node) => {
|
||||
const blocks: { text: string; pos: number }[] = [];
|
||||
doc.descendants((node, pos) => {
|
||||
if (node.type.spec.code) return false;
|
||||
if (!node.isTextblock) return;
|
||||
let text = '';
|
||||
node.forEach((child) => {
|
||||
text +=
|
||||
child.type.name === 'hardBreak'
|
||||
? '\n'
|
||||
: child.isText &&
|
||||
!child.marks.some((mark) => mark.type.spec.code || mark.type.name === 'link')
|
||||
? child.text
|
||||
: '\ufffc'.repeat(child.nodeSize);
|
||||
});
|
||||
blocks.push({ text, pos: pos + 1 });
|
||||
return false;
|
||||
});
|
||||
return blocks;
|
||||
};
|
||||
|
||||
export const getMentionUserQueries = (doc: Node) => {
|
||||
const queries = new Set<string>();
|
||||
for (const { text } of mentionTextBlocks(doc)) {
|
||||
for (const match of text.matchAll(
|
||||
/<[@#$][^>]*>|(?:^|[\s([{])@([^\s@#<>.,!?;:()[\]{}\ufffc]+)/gu
|
||||
)) {
|
||||
if (match[1]) queries.add(match[1].toLowerCase());
|
||||
}
|
||||
}
|
||||
return [...queries];
|
||||
};
|
||||
|
||||
export const resolveMentions = (state: EditorState, items: MentionItem[]) => {
|
||||
const tr = state.tr;
|
||||
const names = new Map<string, Map<string, MentionItem>>();
|
||||
for (const item of items) {
|
||||
if (!item.label || /[<>|\r\n]/.test(item.label)) continue;
|
||||
const attrs = mentionAttributes(item);
|
||||
for (const name of [item.label, item.id]) {
|
||||
const key = `${attrs.mentionSuggestionChar}${name}`.toLowerCase();
|
||||
if (!names.has(key)) names.set(key, new Map());
|
||||
names.get(key)!.set(attrs.id, item);
|
||||
}
|
||||
}
|
||||
if (!names.size || !state.schema.nodes.mention) return tr;
|
||||
|
||||
const pattern = new RegExp(
|
||||
`<[@#$][^>]*>|(^|[\\s([{])(${[...names.keys()]
|
||||
.sort((a, b) => b.length - a.length)
|
||||
.map((name) => name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
|
||||
.join('|')})(?=$|[\\s.,!?;:)\\]}])`,
|
||||
'giu'
|
||||
);
|
||||
for (const { text, pos } of mentionTextBlocks(state.doc)) {
|
||||
for (const match of text.matchAll(pattern)) {
|
||||
if (!match[2]) continue;
|
||||
const matches = names.get(match[2].toLowerCase())!;
|
||||
// Ambiguous names require an explicit selection from the menu.
|
||||
if (matches.size !== 1) continue;
|
||||
const item = [...matches.values()][0];
|
||||
const from = pos + match.index! + match[1].length;
|
||||
tr.replaceWith(
|
||||
tr.mapping.map(from),
|
||||
tr.mapping.map(from + match[2].length),
|
||||
state.schema.nodes.mention.create(
|
||||
mentionAttributes(item),
|
||||
null,
|
||||
state.doc.resolve(from).marks()
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
return tr;
|
||||
};
|
||||
|
||||
export const getChannelMentionCandidates = async (
|
||||
token: string,
|
||||
channelId: string,
|
||||
queries: string[],
|
||||
options: {
|
||||
users: boolean;
|
||||
apps: boolean;
|
||||
models: { id: string; name: string; direct?: boolean }[];
|
||||
channels: { id: string; name?: unknown; type?: string | null }[];
|
||||
}
|
||||
): Promise<MentionItem[]> => {
|
||||
const [users, apps] = await Promise.all([
|
||||
options.users
|
||||
? Promise.all(
|
||||
queries.map(async (query) => {
|
||||
const users: { id: string; name: string }[] = [];
|
||||
for (let page = 1; ; page++) {
|
||||
const result = await searchUsers(token, query, 'name', 'asc', page);
|
||||
if (!result) throw new Error('Failed to load mention suggestions');
|
||||
users.push(...result.users);
|
||||
if (!result.users.length || users.length >= result.total) break;
|
||||
}
|
||||
return users;
|
||||
})
|
||||
).then((results) => results.flat())
|
||||
: [],
|
||||
options.apps && queries.length ? getChannelMentionApps(token, channelId) : []
|
||||
]);
|
||||
return [
|
||||
...users.map((u) => ({ type: 'user' as const, id: u.id, label: u.name })),
|
||||
...options.models
|
||||
.filter((m) => !m.direct)
|
||||
.map((m) => ({ type: 'model' as const, id: m.id, label: m.name })),
|
||||
...apps.map((a) => ({ type: 'app' as const, id: a.id, label: a.name })),
|
||||
...options.channels
|
||||
.filter((c) => c.type !== 'dm' && typeof c.name === 'string')
|
||||
.map((c) => ({ type: 'channel' as const, id: c.id, label: c.name as string }))
|
||||
];
|
||||
};
|
||||
|
|
@ -37,7 +37,8 @@ function mentionRenderer(token: any, options: MentionOptions = {}) {
|
|||
const attrs = Object.entries({
|
||||
class: cls,
|
||||
'data-type': 'mention',
|
||||
'data-id': token.id,
|
||||
'data-id': `${token.id}|${token.label}`,
|
||||
'data-label': token.label,
|
||||
'data-mention-suggestion-char': trigger,
|
||||
...extra
|
||||
})
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue