diff --git a/package-lock.json b/package-lock.json
index 1428527..42ad533 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
"svelte-boring-avatars": "^1.2.6",
"tailwind-merge": "^2.5.2",
"typewriter-editor": "^0.12.6",
+ "virtua": "^0.33.4",
"zod": "^3.23.8"
},
"devDependencies": {
@@ -5724,6 +5725,36 @@
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
"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": {
"version": "5.4.2",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.2.tgz",
diff --git a/package.json b/package.json
index bf875a4..ffeb681 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
"svelte-boring-avatars": "^1.2.6",
"tailwind-merge": "^2.5.2",
"typewriter-editor": "^0.12.6",
+ "virtua": "^0.33.4",
"zod": "^3.23.8"
}
}
diff --git a/src/app.css b/src/app.css
index d0c4ce9..4c19da9 100644
--- a/src/app.css
+++ b/src/app.css
@@ -46,6 +46,8 @@
@layer utilities {
.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;
}
}
diff --git a/src/lib/components/RichTextInput.svelte b/src/lib/components/RichTextInput.svelte
index 0d6be5c..9e71d6f 100644
--- a/src/lib/components/RichTextInput.svelte
+++ b/src/lib/components/RichTextInput.svelte
@@ -23,7 +23,7 @@
{placeholder}
{:then Input}
-
+
{/await}
diff --git a/src/lib/components/RichTextInput/ClientInput.svelte b/src/lib/components/RichTextInput/ClientInput.svelte
index 2455b73..54f6d02 100644
--- a/src/lib/components/RichTextInput/ClientInput.svelte
+++ b/src/lib/components/RichTextInput/ClientInput.svelte
@@ -1,10 +1,28 @@
+
+
+ {#each content as block}
+
+ {#each block as span}
+ {#if typeof span === 'string'}
+ {#if span === ''}
+
+ {:else}
+ {span}
+ {/if}
+ {:else}
+ {@const [text, attributes] = span}
+
+ {/if}
+ {/each}
+
+ {/each}
+
diff --git a/src/lib/components/RichTextRenderer/RichTextSpan.svelte b/src/lib/components/RichTextRenderer/RichTextSpan.svelte
new file mode 100644
index 0000000..69bb3e5
--- /dev/null
+++ b/src/lib/components/RichTextRenderer/RichTextSpan.svelte
@@ -0,0 +1,53 @@
+
+
+{#if attribute === '' || !attributes[attribute]}
+ {text}
+{:else if attribute === 'link'}
+
+
+
+{:else if attribute === 'hashtag'}
+
+
+
+{:else if tagMap[attribute]}
+
+
+
+{:else if dataAttributeBrtMap[attribute]}
+
+
+
+{:else}
+
+{/if}
diff --git a/src/lib/mock/messages.ts b/src/lib/mock/messages.ts
new file mode 100644
index 0000000..db3d040
--- /dev/null
+++ b/src/lib/mock/messages.ts
@@ -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()
+ };
+}
diff --git a/src/lib/mock/users.ts b/src/lib/mock/users.ts
new file mode 100644
index 0000000..969e83b
--- /dev/null
+++ b/src/lib/mock/users.ts
@@ -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)];
+}
diff --git a/src/lib/richText.ts b/src/lib/richText.ts
index 9695753..ceca1ca 100644
--- a/src/lib/richText.ts
+++ b/src/lib/richText.ts
@@ -13,7 +13,7 @@ export const blahRichTextSpanAttributesSchema = z.object({
export type BlahRichTextSpanAttributes = z.input;
export const blahRichTextSpanSchema = z.union([
- z.tuple([z.string()]),
+ z.string(),
z.tuple([z.string(), blahRichTextSpanAttributesSchema])
]);
export type BlahRichTextSpan = z.input;
@@ -42,7 +42,7 @@ function deltaAttributesToBlahRichTextSpanAttributes(
if (attributes.link) blahRichTextSpanAttributes.link = attributes.link;
if (attributes.underline) blahRichTextSpanAttributes.u = true;
- if (attributes.strike) blahRichTextSpanAttributes.s = true;
+ if (attributes.strikethrough) blahRichTextSpanAttributes.s = true;
return isObjectEmpty(blahRichTextSpanAttributes) ? null : blahRichTextSpanAttributes;
}
@@ -57,12 +57,12 @@ export function deltaToBlahRichText(delta: Delta): BlahRichText {
const attributes = deltaAttributesToBlahRichTextSpanAttributes(op.attributes);
const line = lines.shift();
- block.push(attributes ? [line, attributes] : [line]);
+ block.push(attributes ? [line, attributes] : line);
for (const line of lines) {
blocks.push(block);
block = [];
- block.push(attributes ? [line, attributes] : [line]);
+ block.push(attributes ? [line, attributes] : line);
}
}
diff --git a/src/lib/types/index.ts b/src/lib/types/index.ts
index 043e5b3..77b5983 100644
--- a/src/lib/types/index.ts
+++ b/src/lib/types/index.ts
@@ -1 +1,3 @@
export * from './chat';
+export * from './message';
+export * from './user';
diff --git a/src/lib/types/message.ts b/src/lib/types/message.ts
new file mode 100644
index 0000000..d14205a
--- /dev/null
+++ b/src/lib/types/message.ts
@@ -0,0 +1,8 @@
+import type { BlahRichText } from '$lib/richText';
+
+export type Message = {
+ id: string;
+ sender: { id: string; name: string };
+ content: BlahRichText;
+ date: Date;
+};
diff --git a/src/lib/types/user.ts b/src/lib/types/user.ts
new file mode 100644
index 0000000..b05798c
--- /dev/null
+++ b/src/lib/types/user.ts
@@ -0,0 +1,5 @@
+export type User = {
+ id: string;
+ name: string;
+ profilePictureUrl?: string;
+};
diff --git a/src/routes/(app)/chats/[chatId]/+page.svelte b/src/routes/(app)/chats/[chatId]/+page.svelte
index f3102c2..351b113 100644
--- a/src/routes/(app)/chats/[chatId]/+page.svelte
+++ b/src/routes/(app)/chats/[chatId]/+page.svelte
@@ -1,8 +1,24 @@
@@ -10,6 +26,8 @@
chat={{ id: 'blah', name: 'Blah IM Interest Group', type: 'group' }}
outsideUnreadCount={263723}
/>
-
+
+
+
diff --git a/src/routes/(app)/chats/[chatId]/ChatHistory.svelte b/src/routes/(app)/chats/[chatId]/ChatHistory.svelte
new file mode 100644
index 0000000..c705f7e
--- /dev/null
+++ b/src/routes/(app)/chats/[chatId]/ChatHistory.svelte
@@ -0,0 +1,17 @@
+
+
+
+
+
diff --git a/src/routes/(app)/chats/[chatId]/ChatMessage.svelte b/src/routes/(app)/chats/[chatId]/ChatMessage.svelte
new file mode 100644
index 0000000..72b7ee3
--- /dev/null
+++ b/src/routes/(app)/chats/[chatId]/ChatMessage.svelte
@@ -0,0 +1,26 @@
+
+
+