6 Commits

Author SHA1 Message Date
Arjun Patel 62323e42b2 nit 2026-06-20 14:34:40 -07:00
Arjun Patel dfcba82aa4 ignore emacs project.el 2026-06-20 14:27:45 -07:00
Arjun Patel bde093fb21 fix: sidebar padding glitchy
Long markdown content would make each chat row expand and remove the padding in the sidebar
2026-06-20 13:30:07 -07:00
Arjun Patel 5118790353 fix: avatar showing blank when switching 2026-06-20 13:07:41 -07:00
Arjun Patel c11c5074ce chore: bump version to v1.7.1 (#285) 2026-06-13 12:18:21 -07:00
Arjun Patel fa9f88f6f6 improve stream sidebar experience and avatar affordance (#284)
* fix awkward stream list experience

* fix avatar change affordance

* cleanup unnecessary logic

* cleanup unnecessary logic and complexity

* unused var
2026-06-13 11:39:29 -07:00
8 changed files with 21 additions and 56 deletions
+1
View File
@@ -5,3 +5,4 @@ build/
compile_commands.json compile_commands.json
CMakeLists.txt.user CMakeLists.txt.user
tags tags
project.el
+1 -1
View File
@@ -1,7 +1,7 @@
{ {
"name": "Flowy.llink", "name": "Flowy.llink",
"productName": "Flowy.llink", "productName": "Flowy.llink",
"version": "1.7.0", "version": "1.7.1",
"description": "Flowy.llink is a video messaging app for teams", "description": "Flowy.llink is a video messaging app for teams",
"main": ".vite/build/main.js", "main": ".vite/build/main.js",
"private": true, "private": true,
+1 -1
View File
@@ -29,7 +29,7 @@ export function HumanAvatar({
const url = useAvatarUrl(avatarObjectId); const url = useAvatarUrl(avatarObjectId);
return ( return (
<Avatar {...props}> <Avatar {...props}>
{url && <AvatarImage src={url} alt={initials} />} <AvatarImage src={url} alt={initials} />
<AvatarFallback className={fallbackClassName}>{initials}</AvatarFallback> <AvatarFallback className={fallbackClassName}>{initials}</AvatarFallback>
</Avatar> </Avatar>
); );
+1 -1
View File
@@ -16,7 +16,7 @@ function ScrollArea({
> >
<ScrollAreaPrimitive.Viewport <ScrollAreaPrimitive.Viewport
data-slot="scroll-area-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} {children}
</ScrollAreaPrimitive.Viewport> </ScrollAreaPrimitive.Viewport>
@@ -10,7 +10,6 @@ import { KeyHint } from '@/components/key-hint';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
interface StreamListSidebarProps { interface StreamListSidebarProps {
streamName: string;
items: Particle[]; items: Particle[];
networkId: string; networkId: string;
currentIndex: number; currentIndex: number;
@@ -24,7 +23,6 @@ interface StreamListSidebarProps {
* nothing auto-advances. * nothing auto-advances.
*/ */
export function StreamListSidebar({ export function StreamListSidebar({
streamName,
items, items,
networkId, networkId,
currentIndex, currentIndex,
@@ -41,13 +39,12 @@ export function StreamListSidebar({
}, [currentIndex]); }, [currentIndex]);
return ( 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"> <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" /> <List className="size-3.5 text-white/40" />
<span className="truncate text-sm font-medium text-white/90"> <span className="truncate text-sm font-medium text-white/90 mr-auto">
{streamName} {items.length} messages
</span> </span>
<span className="ml-auto text-xs text-white/40">{items.length}</span>
<KeyHint <KeyHint
keys="L" keys="L"
onClick={onToggle} onClick={onToggle}
@@ -108,7 +105,7 @@ function ChatRow({
if (e.key === 'Enter') onClick(); if (e.key === 'Enter') onClick();
}} }}
className={cn( 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', isSelected ? 'bg-white/10' : 'hover:bg-white/5',
)} )}
> >
@@ -119,7 +116,7 @@ function ChatRow({
avatarObjectId={sender.avatarObjectId} avatarObjectId={sender.avatarObjectId}
fallbackClassName="bg-white/10 text-white/80 text-[10px] font-medium" 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"> <div className="flex items-baseline justify-between gap-2">
<span className="truncate text-xs font-semibold text-white/90"> <span className="truncate text-xs font-semibold text-white/90">
{sender.displayName} {sender.displayName}
@@ -144,7 +141,7 @@ function ChatRowContent({ particle }: { particle: Particle }) {
switch (particle.type) { switch (particle.type) {
case 'text': case 'text':
return ( 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} {particle.properties.content}
</p> </p>
); );
@@ -212,11 +212,7 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
platform.autoplay.dismiss(); platform.autoplay.dismiss();
}); });
const userId = useAuthStore((s) => s.user?.id); const { mode, toggle: toggleViewMode } = useStreamViewMode();
const { mode, toggle: toggleViewMode } = useStreamViewMode(
streamParticle,
userId,
);
const { const {
children, children,
@@ -572,7 +568,6 @@ function StreamViewInner({ path, streamParticle }: StreamViewProps) {
{/* Browse sidebar — a separate chat-like panel beside the stream */} {/* Browse sidebar — a separate chat-like panel beside the stream */}
{mode === 'list' && ( {mode === 'list' && (
<StreamListSidebar <StreamListSidebar
streamName={streamParticle.properties.name}
items={children} items={children}
networkId={networkId} networkId={networkId}
currentIndex={currentIndex} currentIndex={currentIndex}
+5 -4
View File
@@ -145,13 +145,14 @@ export default function SettingsPage() {
aria-label="Change profile picture" aria-label="Change profile picture"
> >
<HumanAvatar <HumanAvatar
size="lg" className="size-16"
avatarObjectId={user?.avatar_object_id} avatarObjectId={user?.avatar_object_id}
initials={initials} 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"> <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="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> </span>
</button> </button>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
+5 -34
View File
@@ -1,41 +1,12 @@
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
import type { Particle } from '@/api/types';
export type StreamViewMode = 'player' | 'list'; export type StreamViewMode = 'player' | 'list';
function decideMode( export function useStreamViewMode(): {
streamParticle: Particle & { type: 'stream' }, mode: StreamViewMode;
userId: string | undefined, toggle: () => void;
): StreamViewMode { } {
const marker = userId ? streamParticle.playback_markers?.[userId] : undefined; const [mode, setMode] = useState<StreamViewMode>('player');
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));
}
const toggle = useCallback(() => { const toggle = useCallback(() => {
setMode((prev) => (prev === 'player' ? 'list' : 'player')); setMode((prev) => (prev === 'player' ? 'list' : 'player'));
}, []); }, []);