mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-05-01 08:41:08 +00:00
feat: chat detail frame
This commit is contained in:
parent
78339cd0b9
commit
793217a2a0
10 changed files with 176 additions and 51 deletions
|
@ -1,8 +1,11 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { browser } from '$app/environment';
|
import { browser } from '$app/environment';
|
||||||
import type { Delta } from 'typewriter-editor';
|
import type { Delta } from 'typewriter-editor';
|
||||||
|
import InputFrame from '$lib/components/InputFrame.svelte';
|
||||||
|
import { tw } from '$lib/tw';
|
||||||
|
|
||||||
export let delta: Delta;
|
export let delta: Delta | null = null;
|
||||||
|
export let placeholder: string = '';
|
||||||
|
|
||||||
let className = '';
|
let className = '';
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
@ -14,6 +17,14 @@
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await loadClientComponent() then Input}
|
<InputFrame class={tw('overflow-y-auto', className)}>
|
||||||
<svelte:component this={Input} bind:delta class={className}><slot /></svelte:component>
|
{#await loadClientComponent()}
|
||||||
|
<div class="rich-text opacity-50">
|
||||||
|
<p>{placeholder}</p>
|
||||||
|
</div>
|
||||||
|
{:then Input}
|
||||||
|
<svelte:component this={Input} bind:delta class={className} {placeholder}>
|
||||||
|
<slot />
|
||||||
|
</svelte:component>
|
||||||
{/await}
|
{/await}
|
||||||
|
</InputFrame>
|
||||||
|
|
|
@ -1,12 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import InputFrame from '$lib/components/InputFrame.svelte';
|
|
||||||
import { tw } from '$lib/tw';
|
|
||||||
import { Delta, Editor, asRoot } from 'typewriter-editor';
|
import { Delta, Editor, asRoot } from 'typewriter-editor';
|
||||||
|
|
||||||
let className = '';
|
export let delta: Delta = new Delta();
|
||||||
export { className as class };
|
export let placeholder: string = '';
|
||||||
|
|
||||||
export let delta: Delta;
|
|
||||||
|
|
||||||
const editor = new Editor();
|
const editor = new Editor();
|
||||||
delta = editor.getDelta();
|
delta = editor.getDelta();
|
||||||
|
@ -17,8 +13,13 @@
|
||||||
$: editor.setDelta(delta);
|
$: editor.setDelta(delta);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InputFrame class={tw('overflow-y-auto', className)}>
|
<div
|
||||||
<div class="rich-text w-full outline-none" use:asRoot={editor}>
|
class="rich-text relative w-full outline-none before:absolute before:hidden before:leading-tight before:opacity-50 before:content-[attr(data-weblah-placeholder)] data-[weblah-is-empty]:before:block"
|
||||||
|
use:asRoot={editor}
|
||||||
|
data-weblah-is-empty={!delta || (delta.ops.length === 1 && delta.ops[0].insert === '\n')
|
||||||
|
? 'true'
|
||||||
|
: undefined}
|
||||||
|
data-weblah-placeholder={placeholder}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
</InputFrame>
|
|
||||||
|
|
45
src/lib/formatters.ts
Normal file
45
src/lib/formatters.ts
Normal file
|
@ -0,0 +1,45 @@
|
||||||
|
const unreadCountFormatter = new Intl.NumberFormat('default', {
|
||||||
|
notation: 'compact',
|
||||||
|
compactDisplay: 'short'
|
||||||
|
});
|
||||||
|
|
||||||
|
export function formatUnreadCount(count: number) {
|
||||||
|
return unreadCountFormatter.format(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
const sameDayFormatter = new Intl.DateTimeFormat('default', {
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit'
|
||||||
|
});
|
||||||
|
const sameYearFormatter = new Intl.DateTimeFormat('default', {
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
const otherYearFormatter = new Intl.DateTimeFormat('default', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric'
|
||||||
|
});
|
||||||
|
const fullDateTimeFormatter = new Intl.DateTimeFormat('default', {
|
||||||
|
year: 'numeric',
|
||||||
|
month: 'short',
|
||||||
|
day: 'numeric',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit'
|
||||||
|
});
|
||||||
|
|
||||||
|
export const formatMessageDate = (date: Date, full: boolean = false) => {
|
||||||
|
if (full) return fullDateTimeFormatter.format(date);
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
if (date.getFullYear() === now.getFullYear()) {
|
||||||
|
if (date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) {
|
||||||
|
return sameDayFormatter.format(date);
|
||||||
|
} else {
|
||||||
|
return sameYearFormatter.format(date);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return otherYearFormatter.format(date);
|
||||||
|
}
|
||||||
|
};
|
6
src/lib/types/chat.ts
Normal file
6
src/lib/types/chat.ts
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
export type Chat = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
profilePictureUrl?: string;
|
||||||
|
type: 'group' | 'peer' | 'channel';
|
||||||
|
};
|
1
src/lib/types/index.ts
Normal file
1
src/lib/types/index.ts
Normal file
|
@ -0,0 +1 @@
|
||||||
|
export * from './chat';
|
|
@ -1,4 +1,5 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import { formatMessageDate, formatUnreadCount } from '$lib/formatters';
|
||||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
|
|
||||||
export let chat: {
|
export let chat: {
|
||||||
|
@ -7,38 +8,6 @@
|
||||||
lastMessage: { sender: { id: string; name: string }; content: string; date: Date };
|
lastMessage: { sender: { id: string; name: string }; content: string; date: Date };
|
||||||
unreadCount?: number;
|
unreadCount?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const sameDayFormatter = new Intl.DateTimeFormat('default', {
|
|
||||||
hour: '2-digit',
|
|
||||||
minute: '2-digit'
|
|
||||||
});
|
|
||||||
const sameYearFormatter = new Intl.DateTimeFormat('default', {
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
const otherYearFormatter = new Intl.DateTimeFormat('default', {
|
|
||||||
year: 'numeric',
|
|
||||||
month: 'short',
|
|
||||||
day: 'numeric'
|
|
||||||
});
|
|
||||||
|
|
||||||
const formatDate = (date: Date) => {
|
|
||||||
const now = new Date();
|
|
||||||
if (date.getFullYear() === now.getFullYear()) {
|
|
||||||
if (date.getMonth() === now.getMonth() && date.getDate() === now.getDate()) {
|
|
||||||
return sameDayFormatter.format(date);
|
|
||||||
} else {
|
|
||||||
return sameYearFormatter.format(date);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return otherYearFormatter.format(date);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const unreadCountFormatter = new Intl.NumberFormat('default', {
|
|
||||||
notation: 'compact',
|
|
||||||
compactDisplay: 'short'
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<li
|
<li
|
||||||
|
@ -51,7 +20,13 @@
|
||||||
<div class="relative min-w-0 flex-1">
|
<div class="relative min-w-0 flex-1">
|
||||||
<div class="flex items-center gap-1">
|
<div class="flex items-center gap-1">
|
||||||
<h3 class="flex-1 truncate text-sm font-semibold">{chat.name}</h3>
|
<h3 class="flex-1 truncate text-sm font-semibold">{chat.name}</h3>
|
||||||
<time class="truncate text-xs text-sf-tertiary">{formatDate(chat.lastMessage.date)}</time>
|
<time
|
||||||
|
class="truncate text-xs text-sf-tertiary"
|
||||||
|
datetime={chat.lastMessage.date.toISOString()}
|
||||||
|
title={formatMessageDate(chat.lastMessage.date, true)}
|
||||||
|
>
|
||||||
|
{formatMessageDate(chat.lastMessage.date)}
|
||||||
|
</time>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-end gap-1">
|
<div class="flex items-end gap-1">
|
||||||
<p class="line-clamp-2 h-[2.5em] text-sm leading-tight text-sf-secondary">
|
<p class="line-clamp-2 h-[2.5em] text-sm leading-tight text-sf-secondary">
|
||||||
|
@ -64,7 +39,7 @@
|
||||||
<span
|
<span
|
||||||
class="whitespace-nowrap rounded-full bg-slate-400 px-1.5 py-0.5 text-xs text-slate-50 dark:bg-slate-500 dark:text-slate-950"
|
class="whitespace-nowrap rounded-full bg-slate-400 px-1.5 py-0.5 text-xs text-slate-50 dark:bg-slate-500 dark:text-slate-950"
|
||||||
>
|
>
|
||||||
{unreadCountFormatter.format(chat.unreadCount)}
|
{formatUnreadCount(chat.unreadCount)}
|
||||||
</span>
|
</span>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,6 +1,15 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import Button from '$lib/components/Button.svelte';
|
import BgPattern from '$lib/components/BgPattern.svelte';
|
||||||
|
import ChatHeader from './ChatHeader.svelte';
|
||||||
|
import ChatInput from './ChatInput.svelte';
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<p><Button href="/">Close</Button> History Page for {$page.params.chatId}</p>
|
<div class="flex h-full w-full flex-col justify-stretch">
|
||||||
|
<ChatHeader
|
||||||
|
chat={{ id: 'blah', name: 'Blah IM Interest Group', type: 'group' }}
|
||||||
|
outsideUnreadCount={263723}
|
||||||
|
/>
|
||||||
|
<BgPattern class="flex-1" pattern="charlieBrown"></BgPattern>
|
||||||
|
<ChatInput />
|
||||||
|
</div>
|
||||||
|
|
32
src/routes/(app)/chats/[chatId]/ChatHeader.svelte
Normal file
32
src/routes/(app)/chats/[chatId]/ChatHeader.svelte
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import { formatUnreadCount } from '$lib/formatters';
|
||||||
|
import type { Chat } from '$lib/types';
|
||||||
|
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||||
|
|
||||||
|
export let chat: Chat;
|
||||||
|
export let outsideUnreadCount = 0;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex w-full gap-2 border-b border-ss-secondary bg-sb-primary p-2 shadow-sm">
|
||||||
|
<Button href="/" class="rounded-full sm:hidden">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="-me-0.5 -ms-1 size-5"
|
||||||
|
>
|
||||||
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15.75 19.5 8.25 12l7.5-7.5" />
|
||||||
|
</svg>
|
||||||
|
{#if outsideUnreadCount}
|
||||||
|
<span class="text-xs text-sf-tertiary">{formatUnreadCount(outsideUnreadCount)}</span>
|
||||||
|
{/if}
|
||||||
|
<span class="sr-only">Back</span>
|
||||||
|
</Button>
|
||||||
|
<div class="flex flex-1 flex-col justify-center text-center">
|
||||||
|
<h3 class="truncate text-sm font-semibold">{chat.name}</h3>
|
||||||
|
</div>
|
||||||
|
<AvatarBeam size={30} name={chat.name} />
|
||||||
|
</div>
|
43
src/routes/(app)/chats/[chatId]/ChatInput.svelte
Normal file
43
src/routes/(app)/chats/[chatId]/ChatInput.svelte
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<script lang="ts">
|
||||||
|
import Button from '$lib/components/Button.svelte';
|
||||||
|
import RichTextInput from '$lib/components/RichTextInput.svelte';
|
||||||
|
import type { Delta } from 'typewriter-editor';
|
||||||
|
|
||||||
|
let delta: Delta | null = null;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="flex items-end gap-2 border-t border-ss-secondary bg-sb-primary p-2 shadow-sm">
|
||||||
|
<Button class="p-1.5">
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
stroke-width="1.5"
|
||||||
|
stroke="currentColor"
|
||||||
|
class="size-5"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
d="m18.375 12.739-7.693 7.693a4.5 4.5 0 0 1-6.364-6.364l10.94-10.94A3 3 0 1 1 19.5 7.372L8.552 18.32m.009-.01-.01.01m5.699-9.941-7.81 7.81a1.5 1.5 0 0 0 2.112 2.13"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Attach</span>
|
||||||
|
</Button>
|
||||||
|
<RichTextInput bind:delta placeholder="Message" class="max-h-40 flex-1" />
|
||||||
|
<Button
|
||||||
|
class="before:from-accent-400 before:to-accent-500 relative p-1.5 ring-0 before:absolute before:-inset-px before:rounded-[7px] before:bg-gradient-to-b before:from-40% before:ring-1 before:ring-inset before:ring-black/10"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="currentColor"
|
||||||
|
class="z-10 size-5 text-slate-50 drop-shadow-[0_-1px_0_theme(colors.black/0.2)]"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M3.478 2.404a.75.75 0 0 0-.926.941l2.432 7.905H13.5a.75.75 0 0 1 0 1.5H4.984l-2.432 7.905a.75.75 0 0 0 .926.94 60.519 60.519 0 0 0 18.445-8.986.75.75 0 0 0 0-1.218A60.517 60.517 0 0 0 3.478 2.404Z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Send</span>
|
||||||
|
</Button>
|
||||||
|
</div>
|
|
@ -1,4 +1,5 @@
|
||||||
import type { Config } from 'tailwindcss';
|
import type { Config } from 'tailwindcss';
|
||||||
|
import colors from 'tailwindcss/colors';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
content: ['./src/**/*.{html,js,svelte,ts}'],
|
content: ['./src/**/*.{html,js,svelte,ts}'],
|
||||||
|
@ -14,6 +15,7 @@ export default {
|
||||||
theme: {
|
theme: {
|
||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
|
accent: colors.blue,
|
||||||
// Semantic Background
|
// Semantic Background
|
||||||
sb: {
|
sb: {
|
||||||
primary: 'var(--weblah-color-sb-primary)',
|
primary: 'var(--weblah-color-sb-primary)',
|
||||||
|
|
Loading…
Add table
Reference in a new issue