mirror of
https://github.com/open-webui/open-webui
synced 2025-01-31 15:01:00 +00:00
feat: channel threads
This commit is contained in:
parent
16a48ef4eb
commit
a754a4388a
@ -94,12 +94,18 @@
|
|||||||
}
|
}
|
||||||
} else if (type === 'message:delete') {
|
} else if (type === 'message:delete') {
|
||||||
messages = messages.filter((message) => message.id !== data.id);
|
messages = messages.filter((message) => message.id !== data.id);
|
||||||
|
} else if (type === 'message:reply') {
|
||||||
|
const idx = messages.findIndex((message) => message.id === data.id);
|
||||||
|
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx] = data;
|
||||||
|
}
|
||||||
} else if (type.includes('message:reaction')) {
|
} else if (type.includes('message:reaction')) {
|
||||||
const idx = messages.findIndex((message) => message.id === data.id);
|
const idx = messages.findIndex((message) => message.id === data.id);
|
||||||
if (idx !== -1) {
|
if (idx !== -1) {
|
||||||
messages[idx] = data;
|
messages[idx] = data;
|
||||||
}
|
}
|
||||||
} else if (type === 'typing') {
|
} else if (type === 'typing' && event.message_id === null) {
|
||||||
if (event.user.id === $user.id) {
|
if (event.user.id === $user.id) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -242,6 +248,7 @@
|
|||||||
|
|
||||||
<div class=" pb-[1rem]">
|
<div class=" pb-[1rem]">
|
||||||
<MessageInput
|
<MessageInput
|
||||||
|
id="root"
|
||||||
{typingUsers}
|
{typingUsers}
|
||||||
{onChange}
|
{onChange}
|
||||||
onSubmit={submitHandler}
|
onSubmit={submitHandler}
|
||||||
|
@ -23,6 +23,8 @@
|
|||||||
export let placeholder = $i18n.t('Send a Message');
|
export let placeholder = $i18n.t('Send a Message');
|
||||||
export let transparentBackground = false;
|
export let transparentBackground = false;
|
||||||
|
|
||||||
|
export let id = null;
|
||||||
|
|
||||||
let draggedOver = false;
|
let draggedOver = false;
|
||||||
|
|
||||||
let recording = false;
|
let recording = false;
|
||||||
@ -257,7 +259,7 @@
|
|||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
|
|
||||||
const chatInputElement = document.getElementById('chat-input');
|
const chatInputElement = document.getElementById(`chat-input-${id}`);
|
||||||
chatInputElement?.focus();
|
chatInputElement?.focus();
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -267,7 +269,7 @@
|
|||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
window.setTimeout(() => {
|
window.setTimeout(() => {
|
||||||
const chatInput = document.getElementById('chat-input');
|
const chatInput = document.getElementById(`chat-input-${id}`);
|
||||||
chatInput?.focus();
|
chatInput?.focus();
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@ -373,7 +375,7 @@
|
|||||||
recording = false;
|
recording = false;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
document.getElementById('chat-input')?.focus();
|
document.getElementById(`chat-input-${id}`)?.focus();
|
||||||
}}
|
}}
|
||||||
on:confirm={async (e) => {
|
on:confirm={async (e) => {
|
||||||
const { text, filename } = e.detail;
|
const { text, filename } = e.detail;
|
||||||
@ -381,7 +383,7 @@
|
|||||||
recording = false;
|
recording = false;
|
||||||
|
|
||||||
await tick();
|
await tick();
|
||||||
document.getElementById('chat-input')?.focus();
|
document.getElementById(`chat-input-${id}`)?.focus();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
@ -478,61 +480,21 @@
|
|||||||
</InputMenu>
|
</InputMenu>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $settings?.richTextInput ?? true}
|
<div
|
||||||
<div
|
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
||||||
class="scrollbar-hidden text-left bg-transparent dark:text-gray-100 outline-none w-full py-2.5 px-1 rounded-xl resize-none h-fit max-h-80 overflow-auto"
|
>
|
||||||
>
|
<RichTextInput
|
||||||
<RichTextInput
|
|
||||||
bind:value={content}
|
|
||||||
id="chat-input"
|
|
||||||
messageInput={true}
|
|
||||||
shiftEnter={!$mobile ||
|
|
||||||
!(
|
|
||||||
'ontouchstart' in window ||
|
|
||||||
navigator.maxTouchPoints > 0 ||
|
|
||||||
navigator.msMaxTouchPoints > 0
|
|
||||||
)}
|
|
||||||
{placeholder}
|
|
||||||
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
|
||||||
on:keydown={async (e) => {
|
|
||||||
e = e.detail.event;
|
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
|
||||||
if (
|
|
||||||
!$mobile ||
|
|
||||||
!(
|
|
||||||
'ontouchstart' in window ||
|
|
||||||
navigator.maxTouchPoints > 0 ||
|
|
||||||
navigator.msMaxTouchPoints > 0
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
// Prevent Enter key from creating a new line
|
|
||||||
// Uses keyCode '13' for Enter key for chinese/japanese keyboards
|
|
||||||
if (e.keyCode === 13 && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Submit the content when Enter key is pressed
|
|
||||||
if (content !== '' && e.keyCode === 13 && !e.shiftKey) {
|
|
||||||
submitHandler();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.key === 'Escape') {
|
|
||||||
console.log('Escape');
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
on:paste={async (e) => {
|
|
||||||
e = e.detail.event;
|
|
||||||
console.log(e);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{:else}
|
|
||||||
<textarea
|
|
||||||
id="chat-input"
|
|
||||||
class="scrollbar-hidden bg-transparent dark:text-gray-100 outline-none w-full py-3 px-1 rounded-xl resize-none h-[48px]"
|
|
||||||
{placeholder}
|
|
||||||
bind:value={content}
|
bind:value={content}
|
||||||
|
id={`chat-input-${id}`}
|
||||||
|
messageInput={true}
|
||||||
|
shiftEnter={!$mobile ||
|
||||||
|
!(
|
||||||
|
'ontouchstart' in window ||
|
||||||
|
navigator.maxTouchPoints > 0 ||
|
||||||
|
navigator.msMaxTouchPoints > 0
|
||||||
|
)}
|
||||||
|
{placeholder}
|
||||||
|
largeTextAsFile={$settings?.largeTextAsFile ?? false}
|
||||||
on:keydown={async (e) => {
|
on:keydown={async (e) => {
|
||||||
e = e.detail.event;
|
e = e.detail.event;
|
||||||
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
const isCtrlPressed = e.ctrlKey || e.metaKey; // metaKey is for Cmd key on Mac
|
||||||
@ -560,17 +522,12 @@
|
|||||||
console.log('Escape');
|
console.log('Escape');
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
rows="1"
|
on:paste={async (e) => {
|
||||||
on:input={async (e) => {
|
e = e.detail.event;
|
||||||
e.target.style.height = '';
|
console.log(e);
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
|
||||||
}}
|
|
||||||
on:focus={async (e) => {
|
|
||||||
e.target.style.height = '';
|
|
||||||
e.target.style.height = Math.min(e.target.scrollHeight, 320) + 'px';
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{/if}
|
</div>
|
||||||
|
|
||||||
<div class="self-end mb-1.5 flex space-x-1 mr-1">
|
<div class="self-end mb-1.5 flex space-x-1 mr-1">
|
||||||
{#if content === ''}
|
{#if content === ''}
|
||||||
|
@ -77,7 +77,7 @@
|
|||||||
? 'max-w-full'
|
? 'max-w-full'
|
||||||
: 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
: 'max-w-5xl'} mx-auto group hover:bg-gray-300/5 dark:hover:bg-gray-700/5 transition relative"
|
||||||
>
|
>
|
||||||
{#if (message.user_id === $user.id || $user.role === 'admin') && !edit}
|
{#if !edit}
|
||||||
<div
|
<div
|
||||||
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-30"
|
class=" absolute {showButtons ? '' : 'invisible group-hover:visible'} right-1 -top-2 z-30"
|
||||||
>
|
>
|
||||||
@ -116,26 +116,28 @@
|
|||||||
</Tooltip>
|
</Tooltip>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Edit')}>
|
{#if message.user_id === $user.id || $user.role === 'admin'}
|
||||||
<button
|
<Tooltip content={$i18n.t('Edit')}>
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
<button
|
||||||
on:click={() => {
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||||
edit = true;
|
on:click={() => {
|
||||||
editedContent = message.content;
|
edit = true;
|
||||||
}}
|
editedContent = message.content;
|
||||||
>
|
}}
|
||||||
<Pencil />
|
>
|
||||||
</button>
|
<Pencil />
|
||||||
</Tooltip>
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={$i18n.t('Delete')}>
|
<Tooltip content={$i18n.t('Delete')}>
|
||||||
<button
|
<button
|
||||||
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
class="hover:bg-gray-100 dark:hover:bg-gray-800 transition rounded-lg p-1"
|
||||||
on:click={() => (showDeleteConfirmDialog = true)}
|
on:click={() => (showDeleteConfirmDialog = true)}
|
||||||
>
|
>
|
||||||
<GarbageBin />
|
<GarbageBin />
|
||||||
</button>
|
</button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
@ -326,7 +328,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if message.reply_count > 0}
|
{#if !thread && message.reply_count > 0}
|
||||||
<div class="flex items-center gap-1.5 -mt-0.5 mb-1.5">
|
<div class="flex items-center gap-1.5 -mt-0.5 mb-1.5">
|
||||||
<button
|
<button
|
||||||
class="flex items-center text-xs py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
class="flex items-center text-xs py-1 text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300 transition"
|
||||||
|
@ -1,14 +1,14 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
|
|
||||||
import { socket } from '$lib/stores';
|
import { socket, user } from '$lib/stores';
|
||||||
|
|
||||||
import { getChannelThreadMessages, sendMessage } from '$lib/apis/channels';
|
import { getChannelThreadMessages, sendMessage } from '$lib/apis/channels';
|
||||||
|
|
||||||
import XMark from '$lib/components/icons/XMark.svelte';
|
import XMark from '$lib/components/icons/XMark.svelte';
|
||||||
import MessageInput from './MessageInput.svelte';
|
import MessageInput from './MessageInput.svelte';
|
||||||
import Messages from './Messages.svelte';
|
import Messages from './Messages.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount, tick } from 'svelte';
|
||||||
import { toast } from 'svelte-sonner';
|
import { toast } from 'svelte-sonner';
|
||||||
|
|
||||||
export let threadId = null;
|
export let threadId = null;
|
||||||
@ -44,6 +44,64 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const channelEventHandler = async (event) => {
|
||||||
|
console.log(event);
|
||||||
|
|
||||||
|
if (event.channel_id === channel.id) {
|
||||||
|
const type = event?.data?.type ?? null;
|
||||||
|
const data = event?.data?.data ?? null;
|
||||||
|
|
||||||
|
if (type === 'message') {
|
||||||
|
if ((data?.parent_id ?? null) === threadId) {
|
||||||
|
messages = [data, ...messages];
|
||||||
|
|
||||||
|
if (typingUsers.find((user) => user.id === event.user.id)) {
|
||||||
|
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (type === 'message:update') {
|
||||||
|
const idx = messages.findIndex((message) => message.id === data.id);
|
||||||
|
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx] = data;
|
||||||
|
}
|
||||||
|
} else if (type === 'message:delete') {
|
||||||
|
messages = messages.filter((message) => message.id !== data.id);
|
||||||
|
} else if (type.includes('message:reaction')) {
|
||||||
|
const idx = messages.findIndex((message) => message.id === data.id);
|
||||||
|
if (idx !== -1) {
|
||||||
|
messages[idx] = data;
|
||||||
|
}
|
||||||
|
} else if (type === 'typing' && event.message_id === threadId) {
|
||||||
|
if (event.user.id === $user.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
typingUsers = data.typing
|
||||||
|
? [
|
||||||
|
...typingUsers,
|
||||||
|
...(typingUsers.find((user) => user.id === event.user.id)
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
id: event.user.id,
|
||||||
|
name: event.user.name
|
||||||
|
}
|
||||||
|
])
|
||||||
|
]
|
||||||
|
: typingUsers.filter((user) => user.id !== event.user.id);
|
||||||
|
|
||||||
|
if (typingUsersTimeout[event.user.id]) {
|
||||||
|
clearTimeout(typingUsersTimeout[event.user.id]);
|
||||||
|
}
|
||||||
|
|
||||||
|
typingUsersTimeout[event.user.id] = setTimeout(() => {
|
||||||
|
typingUsers = typingUsers.filter((user) => user.id !== event.user.id);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const submitHandler = async ({ content, data }) => {
|
const submitHandler = async ({ content, data }) => {
|
||||||
if (!content) {
|
if (!content) {
|
||||||
return;
|
return;
|
||||||
@ -71,6 +129,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
$socket?.on('channel-events', channelEventHandler);
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if channel}
|
{#if channel}
|
||||||
@ -113,6 +175,6 @@
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<MessageInput {typingUsers} {onChange} onSubmit={submitHandler} />
|
<MessageInput id={threadId} {typingUsers} {onChange} onSubmit={submitHandler} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
Loading…
Reference in New Issue
Block a user