mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-21 17:41:08 +00:00
feat: chat history ui
This commit is contained in:
parent
0582dffa1c
commit
aebe100799
16 changed files with 325 additions and 8 deletions
31
package-lock.json
generated
31
package-lock.json
generated
|
@ -12,6 +12,7 @@
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"typewriter-editor": "^0.12.6",
|
"typewriter-editor": "^0.12.6",
|
||||||
|
"virtua": "^0.33.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
@ -5724,6 +5725,36 @@
|
||||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
|
"node_modules/virtua": {
|
||||||
|
"version": "0.33.7",
|
||||||
|
"resolved": "https://registry.npmjs.org/virtua/-/virtua-0.33.7.tgz",
|
||||||
|
"integrity": "sha512-IepZaMD/oeEh/ymTqokeQGLrMuRV25+lizPegxVIhOwqX+dEeV9ml1P57Eosok4qiZaeBeQIbIkF9QZrT+EeRQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.14.0",
|
||||||
|
"react-dom": ">=16.14.0",
|
||||||
|
"solid-js": ">=1.0",
|
||||||
|
"svelte": ">=4.0",
|
||||||
|
"vue": ">=3.2"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"react-dom": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"solid-js": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"svelte": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/vite": {
|
"node_modules/vite": {
|
||||||
"version": "5.4.2",
|
"version": "5.4.2",
|
||||||
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
|
||||||
|
|
|
@ -40,6 +40,7 @@
|
||||||
"svelte-boring-avatars": "^1.2.6",
|
"svelte-boring-avatars": "^1.2.6",
|
||||||
"tailwind-merge": "^2.5.2",
|
"tailwind-merge": "^2.5.2",
|
||||||
"typewriter-editor": "^0.12.6",
|
"typewriter-editor": "^0.12.6",
|
||||||
|
"virtua": "^0.33.4",
|
||||||
"zod": "^3.23.8"
|
"zod": "^3.23.8"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
|
|
||||||
@layer utilities {
|
@layer utilities {
|
||||||
.rich-text {
|
.rich-text {
|
||||||
@apply prose prose-slate max-w-none dark:prose-invert prose-p:my-0 prose-p:leading-tight prose-code:before:content-[''] prose-code:after:content-[''];
|
@apply prose prose-slate max-w-none dark:prose-invert;
|
||||||
|
@apply prose-p:my-0 prose-p:leading-tight prose-code:before:content-[''] prose-code:after:content-[''];
|
||||||
|
@apply [&_span[data-weblah-brt=underline]]:underline;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
<p>{placeholder}</p>
|
<p>{placeholder}</p>
|
||||||
</div>
|
</div>
|
||||||
{:then Input}
|
{:then Input}
|
||||||
<svelte:component this={Input} bind:delta class={className} {placeholder}>
|
<svelte:component this={Input} bind:delta {placeholder}>
|
||||||
<slot />
|
<slot />
|
||||||
</svelte:component>
|
</svelte:component>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -1,10 +1,28 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { Delta, Editor, asRoot } from 'typewriter-editor';
|
import { Delta, Editor, asRoot, h } from 'typewriter-editor';
|
||||||
|
|
||||||
export let delta: Delta = new Delta();
|
export let delta: Delta = new Delta();
|
||||||
export let placeholder: string = '';
|
export let placeholder: string = '';
|
||||||
|
|
||||||
const editor = new Editor();
|
const editor = new Editor();
|
||||||
|
editor.typeset.formats.add({
|
||||||
|
name: 'underline',
|
||||||
|
selector: 'span[data-weblah-brt=underline]',
|
||||||
|
styleSelector: '[style*="text-decoration:underline"], [style*="text-decoration: underline"]',
|
||||||
|
commands: (editor) => () => editor.toggleTextFormat({ underline: true }),
|
||||||
|
shortcuts: 'Mod+U',
|
||||||
|
render: (attributes, children) => h('span', { 'data-weblah-brt': 'underline' }, children)
|
||||||
|
});
|
||||||
|
editor.typeset.formats.add({
|
||||||
|
name: 'strikethrough',
|
||||||
|
selector: 's',
|
||||||
|
styleSelector:
|
||||||
|
'[style*="text-decoration:line-through"], [style*="text-decoration: line-through"]',
|
||||||
|
commands: (editor) => () => editor.toggleTextFormat({ strikethrough: true }),
|
||||||
|
shortcuts: 'Mod+Shift+X',
|
||||||
|
render: (attributes, children) => h('s', null, children)
|
||||||
|
});
|
||||||
|
|
||||||
delta = editor.getDelta();
|
delta = editor.getDelta();
|
||||||
editor.on('change', () => {
|
editor.on('change', () => {
|
||||||
delta = editor.getDelta();
|
delta = editor.getDelta();
|
||||||
|
|
28
src/lib/components/RichTextRenderer.svelte
Normal file
28
src/lib/components/RichTextRenderer.svelte
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
import { tw } from '$lib/tw';
|
||||||
|
import RichTextSpan from './RichTextRenderer/RichTextSpan.svelte';
|
||||||
|
|
||||||
|
export let content: BlahRichText;
|
||||||
|
let className = '';
|
||||||
|
export { className as class };
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={tw('rich-text', className)}>
|
||||||
|
{#each content as block}
|
||||||
|
<p>
|
||||||
|
{#each block as span}
|
||||||
|
{#if typeof span === 'string'}
|
||||||
|
{#if span === ''}
|
||||||
|
<br />
|
||||||
|
{:else}
|
||||||
|
{span}
|
||||||
|
{/if}
|
||||||
|
{:else}
|
||||||
|
{@const [text, attributes] = span}
|
||||||
|
<RichTextSpan {text} {attributes} />
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
</p>
|
||||||
|
{/each}
|
||||||
|
</div>
|
53
src/lib/components/RichTextRenderer/RichTextSpan.svelte
Normal file
53
src/lib/components/RichTextRenderer/RichTextSpan.svelte
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import type { BlahRichTextSpanAttributes } from '$lib/richText';
|
||||||
|
|
||||||
|
// From outside to inside, better align this with the RichTextInput
|
||||||
|
const renderOrder: (keyof BlahRichTextSpanAttributes)[] = [
|
||||||
|
'link',
|
||||||
|
'hashtag',
|
||||||
|
'b',
|
||||||
|
'i',
|
||||||
|
'm',
|
||||||
|
'u',
|
||||||
|
's'
|
||||||
|
];
|
||||||
|
|
||||||
|
const tagMap: Partial<Record<keyof BlahRichTextSpanAttributes, string>> = {
|
||||||
|
s: 's',
|
||||||
|
b: 'strong',
|
||||||
|
i: 'em',
|
||||||
|
m: 'code'
|
||||||
|
};
|
||||||
|
|
||||||
|
const dataAttributeBrtMap: Partial<Record<keyof BlahRichTextSpanAttributes, string>> = {
|
||||||
|
u: 'underline'
|
||||||
|
};
|
||||||
|
|
||||||
|
export let text: string;
|
||||||
|
export let attributes: BlahRichTextSpanAttributes;
|
||||||
|
export let attribute: keyof BlahRichTextSpanAttributes | '' = renderOrder[0];
|
||||||
|
|
||||||
|
const nextAttribute = attribute ? (renderOrder[renderOrder.indexOf(attribute) + 1] ?? '') : null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{#if attribute === '' || !attributes[attribute]}
|
||||||
|
{text}
|
||||||
|
{:else if attribute === 'link'}
|
||||||
|
<a href={attributes.link} target="_blank">
|
||||||
|
<svelte:self {...$$props} attribute={nextAttribute} />
|
||||||
|
</a>
|
||||||
|
{:else if attribute === 'hashtag'}
|
||||||
|
<a href={`/search?q=${encodeURIComponent(text)}`}>
|
||||||
|
<svelte:self {...$$props} attribute={nextAttribute} />
|
||||||
|
</a>
|
||||||
|
{:else if tagMap[attribute]}
|
||||||
|
<svelte:element this={tagMap[attribute]}>
|
||||||
|
<svelte:self {...$$props} attribute={nextAttribute} />
|
||||||
|
</svelte:element>
|
||||||
|
{:else if dataAttributeBrtMap[attribute]}
|
||||||
|
<span data-weblah-brt={dataAttributeBrtMap[attribute]}>
|
||||||
|
<svelte:self {...$$props} attribute={nextAttribute} />
|
||||||
|
</span>
|
||||||
|
{:else}
|
||||||
|
<svelte:self {...$$props} attribute={nextAttribute} />
|
||||||
|
{/if}
|
63
src/lib/mock/messages.ts
Normal file
63
src/lib/mock/messages.ts
Normal file
|
@ -0,0 +1,63 @@
|
||||||
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
import type { Message } from '$lib/types';
|
||||||
|
import { getRandomUser } from './users';
|
||||||
|
|
||||||
|
const messageContents: BlahRichText[] = [
|
||||||
|
[['更好的例子可能是link和hashtag不應該共存']],
|
||||||
|
[['理論上mono是可以BIUS的,只是可能不太常見']],
|
||||||
|
[[['這個是一個link', { link: 'https://google.com' }]]],
|
||||||
|
[['這是一個', ['#hashtag', { hashtag: true }]]],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'這是一個',
|
||||||
|
['link', { link: 'https://google.com' }],
|
||||||
|
'和一個',
|
||||||
|
['#hashtag', { hashtag: true }]
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[['可以, 反正我都手写了(']],
|
||||||
|
[['但我們也可以約定這種entity一定要有plain text fallback']],
|
||||||
|
[['這樣的話,我們就可以在不支援的地方用plain text fallback']],
|
||||||
|
[['有可能有僅attribute的run']],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'我现在是约定 text piece 一定非空,也就是说空字符串应该是空数组(但能不能真的这么发言要打个问号)'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[['确实合并相邻的 run 就可以 canonicalize']],
|
||||||
|
[['比如我可能不希望往数据库里这么存 json ,还需要考虑检索之类的']],
|
||||||
|
[['是我蠢了']],
|
||||||
|
[['我觉得这个问题是因为我们没有定义好什么是一个 run']],
|
||||||
|
[['目标是如果前端/后端使用和协议不一致的格式存储的话 roundtrip 后一致']],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'你们这个 canonicalize 真的靠谱吗,,,感觉哪怕跑了一遍以后也会有多个不同 message 视觉效果相同的情况'
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[['稍微有点烦(']],
|
||||||
|
[
|
||||||
|
[
|
||||||
|
[
|
||||||
|
'奥运会究竟该如何报道?中国媒体的表现真的这么不堪吗?',
|
||||||
|
{ link: 'https://www.bilibili.com/video/BV1TZ421L7hj/' }
|
||||||
|
]
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'在视频中,可爸深入分析了中国媒体在巴黎奥运会中的表现,探讨了媒体人员的规模、采访类型、团队构成以及值得称赞与批评的采访案例。文章指出,尽管注册媒体工作者数量庞大,但真正的记者数量相对较少,且面临专业能力不足和流量逻辑冲击等问题。同时,作者强调了媒体在维护国家荣誉和传递奥运精神方面的重要性,呼吁媒体挖掘运动员故事,以建立观众与运动员之间的情感联系。这篇文章为了解当前中国体育媒体的现状和未来发展提供了深刻的见解和反思。'
|
||||||
|
],
|
||||||
|
[''],
|
||||||
|
['---'],
|
||||||
|
[''],
|
||||||
|
['非常好的视频。强烈推荐观看。']
|
||||||
|
],
|
||||||
|
[['pieces:[], attrs:[] 两者等长。然后判断合并就是 attrs 有没有相邻重复元素']]
|
||||||
|
];
|
||||||
|
|
||||||
|
export function createRandomMessage(): Message {
|
||||||
|
return {
|
||||||
|
id: Math.random().toString(),
|
||||||
|
sender: getRandomUser(),
|
||||||
|
content: messageContents[Math.floor(Math.random() * messageContents.length)],
|
||||||
|
date: new Date()
|
||||||
|
};
|
||||||
|
}
|
45
src/lib/mock/users.ts
Normal file
45
src/lib/mock/users.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
import type { User } from '$lib/types';
|
||||||
|
|
||||||
|
const users = [
|
||||||
|
{ id: '1', name: 'Alice' },
|
||||||
|
{ id: '2', name: 'Bob' },
|
||||||
|
{ id: '3', name: 'Shibo Lyu' },
|
||||||
|
{ id: '4', name: 'oxa' },
|
||||||
|
{ id: '5', name: 'septs' },
|
||||||
|
{ id: '6', name: '柑橘 12%' },
|
||||||
|
{ id: '7', name: 'Richard Luo 🐱' },
|
||||||
|
{ id: '8', name: 'Inno Aiolos' },
|
||||||
|
{ id: '9', name: 'omo' },
|
||||||
|
{ id: '10', name: 'Chaoses Ib' },
|
||||||
|
{ id: '11', name: 'L' },
|
||||||
|
{ id: '12', name: 'FlyingSky' },
|
||||||
|
{ id: '13', name: 'Hexagram 喵|🕊️' },
|
||||||
|
{ id: '14', name: 'Cαmber Kirisame (ver. Rolling)' },
|
||||||
|
{ id: '15', name: 'lelenext 轩' },
|
||||||
|
{ id: '16', name: 'Gary です' },
|
||||||
|
{ id: '17', name: 'Criphc' },
|
||||||
|
{ id: '18', name: 'Grady Bing' },
|
||||||
|
{ id: '19', name: 'Thomas Chang' },
|
||||||
|
{ id: '20', name: '化 生' },
|
||||||
|
{ id: '21', name: 'Eddy' },
|
||||||
|
{ id: '22', name: 'Asuna 🍓' },
|
||||||
|
{ id: '23', name: '♡️️Eve️️ ♡' },
|
||||||
|
{ id: '24', name: '你' },
|
||||||
|
{ id: '25', name: 'G°_ ヤン' },
|
||||||
|
{ id: '26', name: '302ye' },
|
||||||
|
{ id: '27', name: 'Chclt' },
|
||||||
|
{ id: '28', name: 'Kaito ゾ' },
|
||||||
|
{ id: '29', name: '🈚 (only know copy' },
|
||||||
|
{ id: '30', name: '喵' },
|
||||||
|
{ id: '31', name: '🦑 没有 premium' },
|
||||||
|
{ id: '32', name: 'Hut' },
|
||||||
|
{ id: '33', name: '瑜琳 洛' },
|
||||||
|
{ id: '34', name: 'LUO Chestnut' },
|
||||||
|
{ id: '35', name: 'ka' },
|
||||||
|
{ id: '36', name: 'Cinnamon' },
|
||||||
|
{ id: '37', name: 'Yves Lelouch' }
|
||||||
|
] satisfies User[];
|
||||||
|
|
||||||
|
export function getRandomUser() {
|
||||||
|
return users[Math.floor(Math.random() * users.length)];
|
||||||
|
}
|
|
@ -13,7 +13,7 @@ export const blahRichTextSpanAttributesSchema = z.object({
|
||||||
export type BlahRichTextSpanAttributes = z.input<typeof blahRichTextSpanAttributesSchema>;
|
export type BlahRichTextSpanAttributes = z.input<typeof blahRichTextSpanAttributesSchema>;
|
||||||
|
|
||||||
export const blahRichTextSpanSchema = z.union([
|
export const blahRichTextSpanSchema = z.union([
|
||||||
z.tuple([z.string()]),
|
z.string(),
|
||||||
z.tuple([z.string(), blahRichTextSpanAttributesSchema])
|
z.tuple([z.string(), blahRichTextSpanAttributesSchema])
|
||||||
]);
|
]);
|
||||||
export type BlahRichTextSpan = z.input<typeof blahRichTextSpanSchema>;
|
export type BlahRichTextSpan = z.input<typeof blahRichTextSpanSchema>;
|
||||||
|
@ -42,7 +42,7 @@ function deltaAttributesToBlahRichTextSpanAttributes(
|
||||||
if (attributes.link) blahRichTextSpanAttributes.link = attributes.link;
|
if (attributes.link) blahRichTextSpanAttributes.link = attributes.link;
|
||||||
|
|
||||||
if (attributes.underline) blahRichTextSpanAttributes.u = true;
|
if (attributes.underline) blahRichTextSpanAttributes.u = true;
|
||||||
if (attributes.strike) blahRichTextSpanAttributes.s = true;
|
if (attributes.strikethrough) blahRichTextSpanAttributes.s = true;
|
||||||
|
|
||||||
return isObjectEmpty(blahRichTextSpanAttributes) ? null : blahRichTextSpanAttributes;
|
return isObjectEmpty(blahRichTextSpanAttributes) ? null : blahRichTextSpanAttributes;
|
||||||
}
|
}
|
||||||
|
@ -57,12 +57,12 @@ export function deltaToBlahRichText(delta: Delta): BlahRichText {
|
||||||
const attributes = deltaAttributesToBlahRichTextSpanAttributes(op.attributes);
|
const attributes = deltaAttributesToBlahRichTextSpanAttributes(op.attributes);
|
||||||
|
|
||||||
const line = lines.shift();
|
const line = lines.shift();
|
||||||
block.push(attributes ? [line, attributes] : [line]);
|
block.push(attributes ? [line, attributes] : line);
|
||||||
|
|
||||||
for (const line of lines) {
|
for (const line of lines) {
|
||||||
blocks.push(block);
|
blocks.push(block);
|
||||||
block = [];
|
block = [];
|
||||||
block.push(attributes ? [line, attributes] : [line]);
|
block.push(attributes ? [line, attributes] : line);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1 +1,3 @@
|
||||||
export * from './chat';
|
export * from './chat';
|
||||||
|
export * from './message';
|
||||||
|
export * from './user';
|
||||||
|
|
8
src/lib/types/message.ts
Normal file
8
src/lib/types/message.ts
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
import type { BlahRichText } from '$lib/richText';
|
||||||
|
|
||||||
|
export type Message = {
|
||||||
|
id: string;
|
||||||
|
sender: { id: string; name: string };
|
||||||
|
content: BlahRichText;
|
||||||
|
date: Date;
|
||||||
|
};
|
5
src/lib/types/user.ts
Normal file
5
src/lib/types/user.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
export type User = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
profilePictureUrl?: string;
|
||||||
|
};
|
|
@ -1,8 +1,24 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import BgPattern from '$lib/components/BgPattern.svelte';
|
import BgPattern from '$lib/components/BgPattern.svelte';
|
||||||
|
import { createRandomMessage } from '$lib/mock/messages';
|
||||||
|
import type { Message } from '$lib/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import ChatHeader from './ChatHeader.svelte';
|
import ChatHeader from './ChatHeader.svelte';
|
||||||
|
import ChatHistory from './ChatHistory.svelte';
|
||||||
import ChatInput from './ChatInput.svelte';
|
import ChatInput from './ChatInput.svelte';
|
||||||
|
|
||||||
|
let messages: Message[] = Array.from({ length: 10 }).map(createRandomMessage);
|
||||||
|
|
||||||
|
// onMount(() => {
|
||||||
|
// const interval = setInterval(
|
||||||
|
// () => {
|
||||||
|
// messages = [...messages, createRandomMessage()];
|
||||||
|
// },
|
||||||
|
// 3000 + Math.random() * 10000
|
||||||
|
// );
|
||||||
|
// return () => clearInterval(interval);
|
||||||
|
// });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="flex h-full w-full flex-col justify-stretch">
|
<div class="flex h-full w-full flex-col justify-stretch">
|
||||||
|
@ -10,6 +26,8 @@
|
||||||
chat={{ id: 'blah', name: 'Blah IM Interest Group', type: 'group' }}
|
chat={{ id: 'blah', name: 'Blah IM Interest Group', type: 'group' }}
|
||||||
outsideUnreadCount={263723}
|
outsideUnreadCount={263723}
|
||||||
/>
|
/>
|
||||||
<BgPattern class="flex-1" pattern="charlieBrown"></BgPattern>
|
<BgPattern class="flex-1" pattern="charlieBrown">
|
||||||
|
<ChatHistory {messages} mySenderId={'_send'} />
|
||||||
|
</BgPattern>
|
||||||
<ChatInput />
|
<ChatInput />
|
||||||
</div>
|
</div>
|
||||||
|
|
17
src/routes/(app)/chats/[chatId]/ChatHistory.svelte
Normal file
17
src/routes/(app)/chats/[chatId]/ChatHistory.svelte
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import { VList } from 'virtua/svelte';
|
||||||
|
|
||||||
|
import type { Message } from '$lib/types';
|
||||||
|
import ChatMessage from './ChatMessage.svelte';
|
||||||
|
|
||||||
|
export let messages: Message[] = [];
|
||||||
|
export let mySenderId: string;
|
||||||
|
|
||||||
|
let ref: VList<Message>;
|
||||||
|
|
||||||
|
$: ref?.scrollToIndex(messages.length - 1, { align: 'end', smooth: true });
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<VList data={messages} let:item={message} class="size-full pt-2" bind:this={ref}>
|
||||||
|
<ChatMessage {message} isMyself={mySenderId === message.sender.id} />
|
||||||
|
</VList>
|
26
src/routes/(app)/chats/[chatId]/ChatMessage.svelte
Normal file
26
src/routes/(app)/chats/[chatId]/ChatMessage.svelte
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import RichTextRenderer from '$lib/components/RichTextRenderer.svelte';
|
||||||
|
import { tw } from '$lib/tw';
|
||||||
|
import type { Message } from '$lib/types';
|
||||||
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
|
|
||||||
|
export let message: Message;
|
||||||
|
export let isMyself: boolean;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class={tw('mb-2 flex items-end gap-2 px-2', isMyself && 'flex-row-reverse')}>
|
||||||
|
<div>
|
||||||
|
<AvatarBeam size={30} name={message.sender.name} />
|
||||||
|
</div>
|
||||||
|
<div class="relative flex-1">
|
||||||
|
<div
|
||||||
|
class="
|
||||||
|
relative inline-block max-w-[50%] rounded-2xl bg-sb-primary px-3 py-2 shadow-sm ring-1 ring-ss-secondary
|
||||||
|
before:absolute before:-bottom-[2px] before:-start-5 before:z-0 before:box-content before:h-6 before:w-5 before:rounded-ee-[16px_12px] before:border-e-[10px] before:border-sb-primary before:text-ss-secondary before:drop-shadow-[-1px_0]
|
||||||
|
after:absolute after:-bottom-[2px] after:-start-5 after:-z-10 after:box-content after:h-6 after:w-5 after:rounded-ee-[16px_12px] after:border-e-[10px] after:text-ss-secondary after:drop-shadow-[0_1px]
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<RichTextRenderer content={message.content} class="z-10" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
Loading…
Add table
Reference in a new issue