From 0220006c50ece2cb1462d069bc74257f8c51d1b4 Mon Sep 17 00:00:00 2001 From: hjlarry Date: Sat, 11 Apr 2026 10:47:04 +0800 Subject: [PATCH] fix: add missing app list page online users --- web/app/components/apps/app-card.tsx | 24 ++++++++++++- web/app/components/apps/list.tsx | 50 ++++++++++++++++++++++++++-- 2 files changed, 71 insertions(+), 3 deletions(-) diff --git a/web/app/components/apps/app-card.tsx b/web/app/components/apps/app-card.tsx index 1cc504d7468..a62abf9ebee 100644 --- a/web/app/components/apps/app-card.tsx +++ b/web/app/components/apps/app-card.tsx @@ -5,6 +5,7 @@ import type { HtmlContentProps } from '@/app/components/base/popover' import type { Tag } from '@/app/components/base/tag-management/constant' import type { CreateAppModalProps } from '@/app/components/explore/create-app-modal' import type { EnvironmentVariable } from '@/app/components/workflow/types' +import type { WorkflowOnlineUser } from '@/models/app' import type { App } from '@/types/app' import { RiBuildingLine, RiGlobalLine, RiLockLine, RiMoreFill, RiVerifiedBadgeLine } from '@remixicon/react' import * as React from 'react' @@ -27,6 +28,7 @@ import { AlertDialogTitle, } from '@/app/components/base/ui/alert-dialog' import { toast } from '@/app/components/base/ui/toast' +import { UserAvatarList } from '@/app/components/base/user-avatar-list' import { NEED_REFRESH_APP_LIST_KEY } from '@/config' import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' @@ -65,10 +67,11 @@ const AccessControl = dynamic(() => import('@/app/components/app/app-access-cont type AppCardProps = { app: App + onlineUsers?: WorkflowOnlineUser[] onRefresh?: () => void } -const AppCard = ({ app, onRefresh }: AppCardProps) => { +const AppCard = ({ app, onlineUsers = [], onRefresh }: AppCardProps) => { const { t } = useTranslation() const deleteAppNameInputId = useId() const systemFeatures = useGlobalPublicStore(s => s.systemFeatures) @@ -360,6 +363,20 @@ const AppCard = ({ app, onRefresh }: AppCardProps) => { return `${t('segment.editedAt', { ns: 'datasetDocuments' })} ${timeText}` }, [app.updated_at, app.created_at, t]) + const onlinePresenceUsers = useMemo(() => { + return onlineUsers + .map((user, index) => { + const id = user.user_id || user.sid || `${app.id}-online-${index}` + const name = user.username || user.user_id || user.sid || `${index + 1}` + return { + id, + name, + avatar_url: user.avatar || null, + } + }) + .filter(user => Boolean(user.id)) + }, [app.id, onlineUsers]) + return ( <>
{
ยท
{EditTimeText}
+ {onlinePresenceUsers.length > 0 && ( +
+ +
+ )}
{app.access_mode === AccessMode.PUBLIC && ( diff --git a/web/app/components/apps/list.tsx b/web/app/components/apps/list.tsx index 2ef344f816a..f997b045667 100644 --- a/web/app/components/apps/list.tsx +++ b/web/app/components/apps/list.tsx @@ -1,9 +1,10 @@ 'use client' import type { FC } from 'react' +import type { WorkflowOnlineUser } from '@/models/app' import { useDebounceFn } from 'ahooks' import { parseAsStringLiteral, useQueryState } from 'nuqs' -import { useCallback, useEffect, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import Checkbox from '@/app/components/base/checkbox' import Input from '@/app/components/base/input' @@ -15,6 +16,7 @@ import { useAppContext } from '@/context/app-context' import { useGlobalPublicStore } from '@/context/global-public-context' import { CheckModal } from '@/hooks/use-pay' import dynamic from '@/next/dynamic' +import { fetchWorkflowOnlineUsers } from '@/service/apps' import { useInfiniteAppList } from '@/service/use-apps' import { AppModeEnum, AppModes } from '@/types/app' import { cn } from '@/utils/classnames' @@ -68,6 +70,7 @@ const List: FC = ({ const containerRef = useRef(null) const [showCreateFromDSLModal, setShowCreateFromDSLModal] = useState(false) const [droppedDSLFile, setDroppedDSLFile] = useState() + const [workflowOnlineUsersMap, setWorkflowOnlineUsersMap] = useState>({}) const setKeywords = useCallback((keywords: string) => { setQuery(prev => ({ ...prev, keywords })) }, [setQuery]) @@ -183,6 +186,44 @@ const List: FC = ({ }, [isCreatedByMe, setQuery]) const pages = data?.pages ?? [] + const workflowIds = useMemo(() => { + const ids = new Set() + pages.forEach((page) => { + page.data?.forEach((app) => { + const workflowId = app.workflow?.id + if (workflowId) + ids.add(workflowId) + }) + }) + return Array.from(ids) + }, [pages]) + + const refreshWorkflowOnlineUsers = useCallback(async () => { + if (!workflowIds.length) + return + + try { + const onlineUsersMap = await fetchWorkflowOnlineUsers({ workflowIds }) + setWorkflowOnlineUsersMap(onlineUsersMap) + } + catch { + setWorkflowOnlineUsersMap({}) + } + }, [workflowIds]) + + useEffect(() => { + void refreshWorkflowOnlineUsers() + }, [refreshWorkflowOnlineUsers]) + + useEffect(() => { + const timer = window.setInterval(() => { + void refetch() + void refreshWorkflowOnlineUsers() + }, 10000) + + return () => window.clearInterval(timer) + }, [refetch, refreshWorkflowOnlineUsers]) + const hasAnyApp = (pages[0]?.total ?? 0) > 0 // Show skeleton during initial load or when refetching with no previous data const showSkeleton = isLoading || (isFetching && pages.length === 0) @@ -242,7 +283,12 @@ const List: FC = ({ if (hasAnyApp) { return pages.flatMap(({ data: apps }) => apps).map(app => ( - + )) }