Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 62323e42b2 | |||
| dfcba82aa4 | |||
| bde093fb21 | |||
| 5118790353 | |||
| c11c5074ce | |||
| fa9f88f6f6 |
@@ -5,3 +5,4 @@ build/
|
||||
compile_commands.json
|
||||
CMakeLists.txt.user
|
||||
tags
|
||||
project.el
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "Flowy.llink",
|
||||
"productName": "Flowy.llink",
|
||||
"version": "1.7.0",
|
||||
"version": "1.7.1",
|
||||
"description": "Flowy.llink is a video messaging app for teams",
|
||||
"main": ".vite/build/main.js",
|
||||
"private": true,
|
||||
|
||||
@@ -29,7 +29,7 @@ export function HumanAvatar({
|
||||
const url = useAvatarUrl(avatarObjectId);
|
||||
return (
|
||||
<Avatar {...props}>
|
||||
{url && <AvatarImage src={url} alt={initials} />}
|
||||
<AvatarImage src={url} alt={initials} />
|
||||
<AvatarFallback className={fallbackClassName}>{initials}</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
|
||||
@@ -16,7 +16,7 @@ function ScrollArea({
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1 [&>div]:!w-full"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
|
||||
@@ -10,7 +10,6 @@ import { KeyHint } from '@/components/key-hint';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
|
||||
interface StreamListSidebarProps {
|
||||
streamName: string;
|
||||
items: Particle[];
|
||||
networkId: string;
|
||||
currentIndex: number;
|
||||
@@ -24,7 +23,6 @@ interface StreamListSidebarProps {
|
||||
* nothing auto-advances.
|
||||
*/
|
||||
export function StreamListSidebar({
|
||||
streamName,
|
||||
items,
|
||||
networkId,
|
||||
currentIndex,
|
||||
@@ -41,13 +39,12 @@ export function StreamListSidebar({
|
||||
}, [currentIndex]);
|
||||
|
||||
return (
|
||||
<aside className="dark flex w-96 shrink-0 flex-col border-l border-white/10 bg-zinc-950">
|
||||
<aside className="dark flex w-60 shrink-0 flex-col overflow-hidden border-l border-white/10 bg-zinc-950">
|
||||
<div className="flex shrink-0 items-center gap-2 border-b border-white/10 px-4 py-3">
|
||||
<List className="size-3.5 text-white/40" />
|
||||
<span className="truncate text-sm font-medium text-white/90">
|
||||
{streamName}
|
||||
<span className="truncate text-sm font-medium text-white/90 mr-auto">
|
||||
{items.length} messages
|
||||
</span>
|
||||
<span className="ml-auto text-xs text-white/40">{items.length}</span>
|
||||
<KeyHint
|
||||
keys="L"
|
||||
onClick={onToggle}
|
||||
@@ -108,7 +105,7 @@ function ChatRow({
|
||||
if (e.key === 'Enter') onClick();
|
||||
}}
|
||||
className={cn(
|
||||
'flex w-full cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
|
||||
'flex cursor-pointer items-start gap-2.5 rounded-md px-2 py-2 text-left transition-colors',
|
||||
isSelected ? 'bg-white/10' : 'hover:bg-white/5',
|
||||
)}
|
||||
>
|
||||
@@ -119,7 +116,7 @@ function ChatRow({
|
||||
avatarObjectId={sender.avatarObjectId}
|
||||
fallbackClassName="bg-white/10 text-white/80 text-[10px] font-medium"
|
||||
/>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="min-w-0 flex-1 overflow-hidden">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="truncate text-xs font-semibold text-white/90">
|
||||
{sender.displayName}
|
||||
@@ -144,7 +141,7 @@ function ChatRowContent({ particle }: { particle: Particle }) {
|
||||
switch (particle.type) {
|
||||
case 'text':
|
||||
return (
|
||||
<p className="line-clamp-3 text-xs leading-relaxed whitespace-pre-line text-white/70">
|
||||
<p className="line-clamp-2 text-xs leading-relaxed [overflow-wrap:anywhere] whitespace-pre-line text-white/70">
|
||||
{particle.properties.content}
|
||||
</p>
|
||||
);
|
||||
|
||||
@@ -212,11 +212,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
platform.autoplay.dismiss();
|
||||
});
|
||||
|
||||
const userId = useAuthStore((s) => s.user?.id);
|
||||
const { mode, toggle: toggleViewMode } = useStreamViewMode(
|
||||
streamParticle,
|
||||
userId,
|
||||
);
|
||||
const { mode, toggle: toggleViewMode } = useStreamViewMode();
|
||||
|
||||
const {
|
||||
children,
|
||||
@@ -572,7 +568,6 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
|
||||
{/* Browse sidebar — a separate chat-like panel beside the stream */}
|
||||
{mode === 'list' && (
|
||||
<StreamListSidebar
|
||||
streamName={streamParticle.properties.name}
|
||||
items={children}
|
||||
networkId={networkId}
|
||||
currentIndex={currentIndex}
|
||||
|
||||
@@ -145,13 +145,14 @@ export default function SettingsPage() {
|
||||
aria-label="Change profile picture"
|
||||
>
|
||||
<HumanAvatar
|
||||
size="lg"
|
||||
className="size-16"
|
||||
avatarObjectId={user?.avatar_object_id}
|
||||
initials={initials}
|
||||
fallbackClassName="bg-primary/10 text-primary font-medium"
|
||||
fallbackClassName="bg-primary/10 text-primary text-xl font-medium"
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
<Camera className="size-4 text-white" />
|
||||
<span className="absolute inset-0 flex items-center justify-center rounded-full bg-black/40 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||
<span className="bg-primary text-primary-foreground ring-background absolute bottom-0 right-0 flex size-5 items-center justify-center rounded-full ring-2">
|
||||
<Camera className="size-2.5" />
|
||||
</span>
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
|
||||
@@ -1,41 +1,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
import type { Particle } from '@/api/types';
|
||||
|
||||
export type StreamViewMode = 'player' | 'list';
|
||||
|
||||
function decideMode(
|
||||
streamParticle: Particle & { type: 'stream' },
|
||||
userId: string | undefined,
|
||||
): StreamViewMode {
|
||||
const marker = userId ? streamParticle.playback_markers?.[userId] : undefined;
|
||||
const lastChildAt = streamParticle.last_child_created_at;
|
||||
const caughtUp =
|
||||
!!marker && !!lastChildAt && lastChildAt.getTime() <= marker.getTime();
|
||||
return caughtUp ? 'list' : 'player';
|
||||
}
|
||||
|
||||
/**
|
||||
* Which mode a stream opens in: the player (autoplay catch-up) when there's
|
||||
* unseen content, the browsable list when the user is fully caught up.
|
||||
* Decided once on entry from the playback marker vs. the stream's last
|
||||
* activity — browsing afterwards advances the marker, but the mode only
|
||||
* changes via the user's toggle.
|
||||
*/
|
||||
export function useStreamViewMode(
|
||||
streamParticle: Particle & { type: 'stream' },
|
||||
userId: string | undefined,
|
||||
): { mode: StreamViewMode; toggle: () => void } {
|
||||
const [mode, setMode] = useState<StreamViewMode>(() =>
|
||||
decideMode(streamParticle, userId),
|
||||
);
|
||||
|
||||
// Re-decide when navigating between streams without an unmount.
|
||||
const [prevStreamId, setPrevStreamId] = useState(streamParticle.id);
|
||||
if (prevStreamId !== streamParticle.id) {
|
||||
setPrevStreamId(streamParticle.id);
|
||||
setMode(decideMode(streamParticle, userId));
|
||||
}
|
||||
|
||||
export function useStreamViewMode(): {
|
||||
mode: StreamViewMode;
|
||||
toggle: () => void;
|
||||
} {
|
||||
const [mode, setMode] = useState<StreamViewMode>('player');
|
||||
const toggle = useCallback(() => {
|
||||
setMode((prev) => (prev === 'player' ? 'list' : 'player'));
|
||||
}, []);
|
||||
|
||||
Reference in New Issue
Block a user