mirror of
https://github.com/Blah-IM/Weblah.git
synced 2025-07-08 23:15:33 +00:00
feat: view transition, chat item sizing & group / unread
This commit is contained in:
parent
6c83b2dc6b
commit
f3f32a5326
18 changed files with 368 additions and 101 deletions
68
src/routes/(app)/+layout.svelte
Normal file
68
src/routes/(app)/+layout.svelte
Normal file
|
@ -0,0 +1,68 @@
|
|||
<script>
|
||||
import { page } from '$app/stores';
|
||||
import { onNavigate } from '$app/navigation';
|
||||
import ChatList from './ChatList.svelte';
|
||||
|
||||
onNavigate((navigation) => {
|
||||
if (!document.startViewTransition) return;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
document.startViewTransition(async () => {
|
||||
resolve();
|
||||
await navigation.complete;
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
$: mainVisible = !!$page.params.chatId;
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="group h-screen sm:flex sm:items-stretch"
|
||||
data-weblah-main-visible={mainVisible ? 'true' : undefined}
|
||||
>
|
||||
<aside
|
||||
class="relative h-screen min-h-0 overflow-hidden border-ss-primary bg-sb-primary shadow-lg [view-transition-name:chat-list] after:pointer-events-none after:absolute after:inset-0 after:size-full after:bg-transparent group-data-[weblah-main-visible]:after:bg-black/30 sm:w-1/3 sm:border-e sm:after:hidden lg:w-1/4"
|
||||
>
|
||||
<ChatList />
|
||||
</aside>
|
||||
{#if mainVisible}
|
||||
<main
|
||||
class="absolute inset-0 w-full bg-sb-secondary shadow-lg [view-transition-name:main] sm:relative sm:flex-1 sm:shadow-none"
|
||||
>
|
||||
<slot></slot>
|
||||
</main>
|
||||
{:else}
|
||||
<div class="hidden flex-1 sm:block"><slot /></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@keyframes slide-in {
|
||||
from {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
@keyframes slide-out {
|
||||
from {
|
||||
transform: translateX(0);
|
||||
}
|
||||
to {
|
||||
transform: translateX(100%);
|
||||
}
|
||||
}
|
||||
|
||||
:root::view-transition-old(root),
|
||||
:root::view-transition-new(root) {
|
||||
animation-duration: 250ms;
|
||||
}
|
||||
:root::view-transition-old(main) {
|
||||
animation: 250ms ease-out slide-out;
|
||||
}
|
||||
:root::view-transition-new(main) {
|
||||
animation: 250ms ease-out slide-in;
|
||||
}
|
||||
</style>
|
8
src/routes/(app)/+page.svelte
Normal file
8
src/routes/(app)/+page.svelte
Normal file
|
@ -0,0 +1,8 @@
|
|||
<script>
|
||||
import BgPattern from '$lib/components/BgPattern.svelte';
|
||||
import ServiceMessage from '$lib/components/ServiceMessage.svelte';
|
||||
</script>
|
||||
|
||||
<BgPattern class="flex h-screen flex-col items-center justify-center gap-4">
|
||||
<ServiceMessage class="text-sm">Select a chat to start messaging</ServiceMessage>
|
||||
</BgPattern>
|
115
src/routes/(app)/ChatList.svelte
Normal file
115
src/routes/(app)/ChatList.svelte
Normal file
|
@ -0,0 +1,115 @@
|
|||
<script>
|
||||
import ChatListHeader from './ChatListHeader.svelte';
|
||||
import ChatListItem from './ChatListItem.svelte';
|
||||
</script>
|
||||
|
||||
<div class="flex h-screen flex-col justify-stretch">
|
||||
<ChatListHeader />
|
||||
<div class="min-h-0 flex-1 touch-pan-y overflow-y-auto">
|
||||
<ul>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-1',
|
||||
name: 'Blah IM Interest Group',
|
||||
lastMessage: {
|
||||
sender: { id: '1', name: 'septs' },
|
||||
content: '窄带通信吧,asn1 + bzip2 效果还是可以的',
|
||||
date: new Date('2024-08-29T02:11Z')
|
||||
},
|
||||
unreadCount: 3
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-2',
|
||||
name: 'Laoself Chat',
|
||||
lastMessage: {
|
||||
sender: { id: '10', name: 'Richard Luo 🐱' },
|
||||
content: '如果durov没事 那你是不是又可以拖延症复发了(',
|
||||
date: new Date('2024-08-29T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: 'room-3',
|
||||
name: 'Ubuntu中文',
|
||||
lastMessage: {
|
||||
sender: { id: '15', name: 'chen jianhao' },
|
||||
content:
|
||||
'就什么也没安装,昨晚好好的,就打开了pycharm而已,写完代码直接按设置好的快捷键alt+s 关机 重启就不行了',
|
||||
date: new Date('2024-08-29T02:27Z')
|
||||
},
|
||||
unreadCount: 827469
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '1',
|
||||
name: 'septs',
|
||||
lastMessage: {
|
||||
sender: { id: '1', name: 'septs' },
|
||||
content: '验证 checksum 是否正确的代价还是可以接受的',
|
||||
date: new Date('2024-08-28T02:54Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '2',
|
||||
name: 'oxa',
|
||||
lastMessage: {
|
||||
sender: { id: '2', name: 'oxa' },
|
||||
content: '但似乎现在大家都讨厌 pgp ,觉得太复杂',
|
||||
date: new Date('2024-08-28T02:37Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '3',
|
||||
name: 'omo',
|
||||
lastMessage: {
|
||||
sender: { id: '3', name: 'omo' },
|
||||
content: '我對 revalidate 的理解是不經過 cache 直接重拉一遍',
|
||||
date: new Date('2024-08-28T02:11Z')
|
||||
},
|
||||
unreadCount: 8
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '4',
|
||||
name: 'Inno Aiolos',
|
||||
lastMessage: {
|
||||
sender: { id: '4', name: 'Inno Aiolos' },
|
||||
content: '至少得把信息分发给所有广播自己是这个public key的destination',
|
||||
date: new Date('2024-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '5',
|
||||
name: 'Gary です',
|
||||
lastMessage: {
|
||||
sender: { id: '5', name: 'Gary です' },
|
||||
content: '没必要8,长毛象那样挺麻烦的',
|
||||
date: new Date('2023-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<ChatListItem
|
||||
chat={{
|
||||
id: '6',
|
||||
name: 'Chtholly Nota Seniorious',
|
||||
lastMessage: {
|
||||
sender: { id: '6', name: 'Chtholly Nota Seniorious' },
|
||||
content: '遥遥领先!\n隔壁 nostr 最开始没有注意到这个问题,然后被狂灌置顶 spam',
|
||||
date: new Date('2022-07-28T02:11Z')
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
45
src/routes/(app)/ChatListHeader.svelte
Normal file
45
src/routes/(app)/ChatListHeader.svelte
Normal file
|
@ -0,0 +1,45 @@
|
|||
<script lang="ts">
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
import InputFrame from '$lib/components/InputFrame.svelte';
|
||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||
</script>
|
||||
|
||||
<header class="flex items-center justify-stretch gap-2 border-b border-ss-secondary p-2 shadow-sm">
|
||||
<div><AvatarBeam size={30} name="Shibo Lyu" /></div>
|
||||
<InputFrame class="flex-1">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
class="size-5 text-slate-400"
|
||||
>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
d="M10.5 3.75a6.75 6.75 0 1 0 0 13.5 6.75 6.75 0 0 0 0-13.5ZM2.25 10.5a8.25 8.25 0 1 1 14.59 5.28l4.69 4.69a.75.75 0 1 1-1.06 1.06l-4.69-4.69A8.25 8.25 0 0 1 2.25 10.5Z"
|
||||
clip-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Search"
|
||||
class="w-full flex-1 bg-transparent text-sm leading-4 text-slate-900 focus:outline-none"
|
||||
/>
|
||||
</InputFrame>
|
||||
<Button class="size-8">
|
||||
<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="m16.862 4.487 1.687-1.688a1.875 1.875 0 1 1 2.652 2.652L10.582 16.07a4.5 4.5 0 0 1-1.897 1.13L6 18l.8-2.685a4.5 4.5 0 0 1 1.13-1.897l8.932-8.931Zm0 0L19.5 7.125M18 14v4.75A2.25 2.25 0 0 1 15.75 21H5.25A2.25 2.25 0 0 1 3 18.75V8.25A2.25 2.25 0 0 1 5.25 6H10"
|
||||
/>
|
||||
</svg>
|
||||
<span class="sr-only">Compose</span>
|
||||
</Button>
|
||||
</header>
|
73
src/routes/(app)/ChatListItem.svelte
Normal file
73
src/routes/(app)/ChatListItem.svelte
Normal file
|
@ -0,0 +1,73 @@
|
|||
<script lang="ts">
|
||||
import { AvatarBeam } from 'svelte-boring-avatars';
|
||||
|
||||
export let chat: {
|
||||
id: string;
|
||||
name: string;
|
||||
lastMessage: { sender: { id: string; name: string }; content: string; date: Date };
|
||||
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>
|
||||
|
||||
<li
|
||||
class="relative after:absolute after:bottom-0 after:end-0 after:start-14 after:border-t-[0.5px] after:border-ss-secondary"
|
||||
>
|
||||
<a href="/chats/{chat.id}" class="flex h-20 cursor-default items-center gap-2 px-2">
|
||||
<div class="size-10">
|
||||
<AvatarBeam size={40} name={chat.name} />
|
||||
</div>
|
||||
<div class="relative min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1">
|
||||
<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>
|
||||
</div>
|
||||
<div class="flex items-end gap-1">
|
||||
<p class="line-clamp-2 h-[2.5em] text-sm leading-tight text-sf-secondary">
|
||||
{#if chat.id !== chat.lastMessage.sender.id}
|
||||
<span class="text-sf-primary">{chat.lastMessage.sender.name}: </span>
|
||||
{/if}
|
||||
{chat.lastMessage.content}
|
||||
</p>
|
||||
{#if chat.unreadCount}
|
||||
<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"
|
||||
>
|
||||
{unreadCountFormatter.format(chat.unreadCount)}
|
||||
</span>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
6
src/routes/(app)/chats/[chatId]/+page.svelte
Normal file
6
src/routes/(app)/chats/[chatId]/+page.svelte
Normal file
|
@ -0,0 +1,6 @@
|
|||
<script lang="ts">
|
||||
import { page } from '$app/stores';
|
||||
import Button from '$lib/components/Button.svelte';
|
||||
</script>
|
||||
|
||||
<p><Button href="/">Close</Button> History Page for {$page.params.chatId}</p>
|
Loading…
Add table
Add a link
Reference in a new issue