feat: refresh topic sharing experience (share page + popover) (#15581)

This commit is contained in:
René Wang
2026-06-10 17:43:02 +08:00
committed by GitHub
parent e8e4b2e822
commit b8b37cffa3
15 changed files with 213 additions and 72 deletions
+16 -6
View File
@@ -487,7 +487,7 @@
"sessionGroup.tooLong": "Category name length should be between 1-20",
"shareModal.copy": "Copy",
"shareModal.copyLink": "Copy Link",
"shareModal.copyLinkSuccess": "Link copied",
"shareModal.copyLinkSuccess": "Share link copied to clipboard",
"shareModal.download": "Download Screenshot",
"shareModal.downloadError": "Download failed",
"shareModal.downloadFile": "Download File",
@@ -517,17 +517,23 @@
"shareModal.pdfErrorDescription": "An error occurred while generating the PDF, please try again",
"shareModal.pdfGenerationError": "PDF generation failed",
"shareModal.pdfReady": "PDF is ready",
"shareModal.popover.moreOptions": "More share options",
"shareModal.popover.privacyWarning.confirm": "I understand, continue",
"shareModal.popover.privacyWarning.content": "Please make sure your conversation doesn't contain any personal or sensitive information. You are responsible for any content you choose to share and its consequences.",
"shareModal.popover.export": "Export",
"shareModal.popover.privacyWarning.confirm": "Share & copy link",
"shareModal.popover.privacyWarning.content": "Anyone with the link can open this whole conversation, so take a moment to make sure there's nothing here you'd rather keep private. A shared topic may include:",
"shareModal.popover.privacyWarning.doNotShowAgain": "Don't show this again",
"shareModal.popover.privacyWarning.title": "Privacy Notice",
"shareModal.popover.privacyWarning.items.credentials": "Credentials",
"shareModal.popover.privacyWarning.items.files": "Uploaded files",
"shareModal.popover.privacyWarning.items.images": "Images",
"shareModal.popover.privacyWarning.items.toolCalls": "Connector calling details",
"shareModal.popover.privacyWarning.note": "You can switch back to private anytime.",
"shareModal.popover.privacyWarning.title": "Before you share this link",
"shareModal.popover.title": "Share Topic",
"shareModal.popover.visibility": "Visibility",
"shareModal.regeneratePdf": "Regenerate PDF",
"shareModal.screenshot": "Screenshot",
"shareModal.settings": "Export Settings",
"shareModal.text": "Text",
"shareModal.title": "Export",
"shareModal.widthMode.label": "Width Mode",
"shareModal.widthMode.narrow": "Narrow",
"shareModal.widthMode.wide": "Wide",
@@ -545,7 +551,11 @@
"sharePage.error.unauthorized.action": "Sign In",
"sharePage.error.unauthorized.subtitle": "Please sign in to view this shared topic.",
"sharePage.error.unauthorized.title": "Sign In Required",
"sharePageDisclaimer": "This content is shared by a user and does not represent the views of LobeHub. LobeHub is not responsible for any consequences arising from this shared content.",
"sharePage.menu.copyLink": "Copy Link",
"sharePage.menu.goToLobeHub": "Go to LobeHub",
"sharePage.menu.more": "More",
"sharePage.menu.report": "Report",
"sharePageDisclaimer": "Shared by a user. The content reflects their views, not LobeHub's, and LobeHub takes no responsibility for it.",
"signalCallbacks.collapse": "Hide details",
"signalCallbacks.empty": "No callback messages",
"signalCallbacks.expand": "Show details",
+16 -6
View File
@@ -487,7 +487,7 @@
"sessionGroup.tooLong": "分类名称长度需为 120 个字符",
"shareModal.copy": "复制",
"shareModal.copyLink": "复制链接",
"shareModal.copyLinkSuccess": "链接已复制",
"shareModal.copyLinkSuccess": "分享链接已复制到剪贴板",
"shareModal.download": "下载截图",
"shareModal.downloadError": "下载失败,请检查网络后重试",
"shareModal.downloadFile": "下载文件",
@@ -517,17 +517,23 @@
"shareModal.pdfErrorDescription": "生成 PDF 时出错,请重试或联系支持",
"shareModal.pdfGenerationError": "PDF 生成失败",
"shareModal.pdfReady": "PDF 已准备就绪",
"shareModal.popover.moreOptions": "更多分享方式",
"shareModal.popover.privacyWarning.confirm": "我已了解,继续",
"shareModal.popover.privacyWarning.content": "请确保对话中不含个人隐私或敏感信息,LobeHub 不对任何分享的内容及其产生的后果负责。",
"shareModal.popover.export": "导出",
"shareModal.popover.privacyWarning.confirm": "分享并复制链接",
"shareModal.popover.privacyWarning.content": "拿到链接的人都能查看整段对话,分享前花点时间确认其中没有不便公开的内容。被分享的话题可能包含:",
"shareModal.popover.privacyWarning.doNotShowAgain": "不再显示此提示",
"shareModal.popover.privacyWarning.title": "隐私提醒",
"shareModal.popover.privacyWarning.items.credentials": "凭证与密钥",
"shareModal.popover.privacyWarning.items.files": "上传的文件",
"shareModal.popover.privacyWarning.items.images": "图片",
"shareModal.popover.privacyWarning.items.toolCalls": "连接器调用详情",
"shareModal.popover.privacyWarning.note": "你可以随时切回私密状态。",
"shareModal.popover.privacyWarning.title": "分享链接前请留意",
"shareModal.popover.title": "分享话题",
"shareModal.popover.visibility": "可见性",
"shareModal.regeneratePdf": "重新生成 PDF",
"shareModal.screenshot": "截图",
"shareModal.settings": "导出设置",
"shareModal.text": "文本",
"shareModal.title": "导出",
"shareModal.widthMode.label": "宽度模式",
"shareModal.widthMode.narrow": "窄屏",
"shareModal.widthMode.wide": "宽屏",
@@ -545,7 +551,11 @@
"sharePage.error.unauthorized.action": "登录",
"sharePage.error.unauthorized.subtitle": "请登录后查看此分享话题。",
"sharePage.error.unauthorized.title": "需要登录",
"sharePageDisclaimer": "此内容由用户分享,不代表 LobeHub 观点。LobeHub 不对该分享内容产生的任何后果承担责任。",
"sharePage.menu.copyLink": "复制链接",
"sharePage.menu.goToLobeHub": "前往 LobeHub",
"sharePage.menu.more": "更多",
"sharePage.menu.report": "举报",
"sharePageDisclaimer": "由用户分享,仅代表其个人观点,不代表 LobeHub 立场;LobeHub 不对该内容承担责任。",
"signalCallbacks.collapse": "隐藏详情",
"signalCallbacks.empty": "没有回调消息",
"signalCallbacks.expand": "显示详情",
+16 -6
View File
@@ -547,7 +547,7 @@ export default {
'sessionGroup.tooLong': 'Category name length should be between 1-20',
'shareModal.copy': 'Copy',
'shareModal.copyLink': 'Copy Link',
'shareModal.copyLinkSuccess': 'Link copied',
'shareModal.copyLinkSuccess': 'Share link copied to clipboard',
'shareModal.download': 'Download Screenshot',
'shareModal.downloadError': 'Download failed',
'shareModal.downloadFile': 'Download File',
@@ -577,18 +577,24 @@ export default {
'shareModal.pdfErrorDescription': 'An error occurred while generating the PDF, please try again',
'shareModal.pdfGenerationError': 'PDF generation failed',
'shareModal.pdfReady': 'PDF is ready',
'shareModal.popover.moreOptions': 'More share options',
'shareModal.popover.privacyWarning.confirm': 'I understand, continue',
'shareModal.popover.export': 'Export',
'shareModal.popover.privacyWarning.confirm': 'Share & copy link',
'shareModal.popover.privacyWarning.content':
"Please make sure your conversation doesn't contain any personal or sensitive information. You are responsible for any content you choose to share and its consequences.",
"Anyone with the link can open this whole conversation, so take a moment to make sure there's nothing here you'd rather keep private. A shared topic may include:",
'shareModal.popover.privacyWarning.doNotShowAgain': "Don't show this again",
'shareModal.popover.privacyWarning.title': 'Privacy Notice',
'shareModal.popover.privacyWarning.items.credentials': 'Credentials',
'shareModal.popover.privacyWarning.items.files': 'Uploaded files',
'shareModal.popover.privacyWarning.items.images': 'Images',
'shareModal.popover.privacyWarning.items.toolCalls': 'Connector calling details',
'shareModal.popover.privacyWarning.note': 'You can switch back to private anytime.',
'shareModal.popover.privacyWarning.title': 'Before you share this link',
'shareModal.popover.title': 'Share Topic',
'shareModal.popover.visibility': 'Visibility',
'shareModal.regeneratePdf': 'Regenerate PDF',
'shareModal.screenshot': 'Screenshot',
'shareModal.settings': 'Export Settings',
'shareModal.text': 'Text',
'shareModal.title': 'Export',
'shareModal.widthMode.label': 'Width Mode',
'shareModal.widthMode.narrow': 'Narrow',
'shareModal.widthMode.wide': 'Wide',
@@ -606,8 +612,12 @@ export default {
'sharePage.error.unauthorized.action': 'Sign In',
'sharePage.error.unauthorized.subtitle': 'Please sign in to view this shared topic.',
'sharePage.error.unauthorized.title': 'Sign In Required',
'sharePage.menu.copyLink': 'Copy Link',
'sharePage.menu.goToLobeHub': 'Go to LobeHub',
'sharePage.menu.more': 'More',
'sharePage.menu.report': 'Report',
'sharePageDisclaimer':
'This content is shared by a user and does not represent the views of LobeHub. LobeHub is not responsible for any consequences arising from this shared content.',
"Shared by a user. The content reflects their views, not LobeHub's, and LobeHub takes no responsibility for it.",
'signalCallbacks.collapse': 'Hide details',
'signalCallbacks.empty': 'No callback messages',
'signalCallbacks.expand': 'Show details',
+1 -20
View File
@@ -1,8 +1,6 @@
import { t } from 'i18next';
import { MessageSquare, Settings } from 'lucide-react';
import useSWR from 'swr';
import { Settings } from 'lucide-react';
import { lambdaClient } from '@/libs/trpc/client';
import { routeMeta } from '@/spa/router/routeMeta';
import { useAgentStore } from '@/store/agent';
import { agentSelectors } from '@/store/agent/selectors';
@@ -20,20 +18,3 @@ export const mobileAgentSettingsRouteMeta = routeMeta({
};
},
});
export const shareTopicRouteMeta = routeMeta({
icon: MessageSquare,
titleKey: 'navigation.chat',
useDynamicMeta: (params) => {
const shareId = params.id;
const { data } = useSWR(
shareId ? ['shared-topic', shareId] : null,
() => lambdaClient.share.getSharedTopic.query({ shareId: shareId! }),
{ revalidateOnFocus: false },
);
return {
title: data?.title || undefined,
};
},
});
+1 -1
View File
@@ -105,7 +105,7 @@ export const openShareModal = ({
styles: {
content: { height: 'min(80vh, 800px)' },
},
title: t('share', { ns: 'common' }),
title: t('shareModal.title', { ns: 'chat' }),
width: 'min(90vw, 1024px)',
});
+45 -17
View File
@@ -12,7 +12,15 @@ import {
} from '@lobehub/ui';
import { confirmModal, Select } from '@lobehub/ui/base-ui';
import { App, Divider } from 'antd';
import { ExternalLinkIcon, LinkIcon, LockIcon } from 'lucide-react';
import {
FileOutputIcon,
ImageIcon,
KeyRoundIcon,
LinkIcon,
LockIcon,
PaperclipIcon,
WrenchIcon,
} from 'lucide-react';
import { type ReactNode } from 'react';
import { memo, useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next';
@@ -30,6 +38,13 @@ import { styles } from './style';
type Visibility = 'private' | 'link';
const PRIVACY_WARNING_ITEMS = [
{ icon: WrenchIcon, labelKey: 'shareModal.popover.privacyWarning.items.toolCalls' },
{ icon: KeyRoundIcon, labelKey: 'shareModal.popover.privacyWarning.items.credentials' },
{ icon: ImageIcon, labelKey: 'shareModal.popover.privacyWarning.items.images' },
{ icon: PaperclipIcon, labelKey: 'shareModal.popover.privacyWarning.items.files' },
] as const;
interface SharePopoverContentProps {
onOpenModal?: () => void;
topicId?: string;
@@ -79,14 +94,20 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal, topic
try {
await topicService.updateShareVisibility(activeTopicId, visibility);
await mutate();
message.success(t('shareModal.link.visibilityUpdated'));
// Auto-copy the share link the moment link sharing is enabled
if (visibility === 'link' && shareUrl) {
await copyToClipboard(shareUrl);
message.success(t('shareModal.copyLinkSuccess'));
} else {
message.success(t('shareModal.link.visibilityUpdated'));
}
} catch {
message.error(t('shareModal.link.updateError'));
} finally {
setUpdating(false);
}
},
[activeTopicId, mutate, message, t],
[activeTopicId, mutate, message, t, shareUrl],
);
const handleVisibilityChange = useCallback(
@@ -102,18 +123,25 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal, topic
confirmModal({
cancelText: t('cancel', { ns: 'common' }),
content: (
<div>
<p>{t('shareModal.popover.privacyWarning.content')}</p>
<div style={{ marginTop: 16 }}>
<Checkbox
onChange={(v) => {
doNotShowAgain = v;
}}
>
{t('shareModal.popover.privacyWarning.doNotShowAgain')}
</Checkbox>
</div>
</div>
<Flexbox gap={16}>
<Text>{t('shareModal.popover.privacyWarning.content')}</Text>
<Flexbox gap={12} paddingBlock={8}>
{PRIVACY_WARNING_ITEMS.map(({ icon: ItemIcon, labelKey }) => (
<Flexbox horizontal align="center" gap={8} key={labelKey}>
<ItemIcon size={16} />
<Text>{t(labelKey)}</Text>
</Flexbox>
))}
</Flexbox>
<Text>{t('shareModal.popover.privacyWarning.note')}</Text>
<Checkbox
onChange={(v) => {
doNotShowAgain = v;
}}
>
{t('shareModal.popover.privacyWarning.doNotShowAgain')}
</Checkbox>
</Flexbox>
),
okText: t('shareModal.popover.privacyWarning.confirm'),
onOk: () => {
@@ -223,13 +251,13 @@ const SharePopoverContent = memo<SharePopoverContentProps>(({ onOpenModal, topic
<Flexbox horizontal align="center" justify="space-between">
<Button
icon={ExternalLinkIcon}
icon={FileOutputIcon}
size="small"
type="text"
variant="text"
onClick={handleOpenModal}
>
{t('shareModal.popover.moreOptions')}
{t('shareModal.popover.export')}
</Button>
{currentVisibility !== 'private' && (
<Button icon={LinkIcon} size="small" type="primary" onClick={handleCopyLink}>
+14 -1
View File
@@ -1,6 +1,8 @@
'use client';
import { Flexbox, Text } from '@lobehub/ui';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ChatList, ConversationProvider, MessageItem } from '@/features/Conversation';
import { useChatStore } from '@/store/chat';
@@ -14,6 +16,7 @@ interface SharedMessageListProps {
}
const SharedMessageList = memo<SharedMessageListProps>(({ agentId, groupId, shareId, topicId }) => {
const { t } = useTranslation('chat');
const context = useMemo(
() => ({
agentId: agentId ?? '',
@@ -43,7 +46,17 @@ const SharedMessageList = memo<SharedMessageListProps>(({ agentId, groupId, shar
replaceMessages(messages, { context: ctx });
}}
>
<ChatList disableActionsBar itemContent={itemContent} />
<ChatList
disableActionsBar
itemContent={itemContent}
footerSlot={
<Flexbox align={'center'} paddingBlock={'16px 80px'} paddingInline={24}>
<Text fontSize={12} style={{ maxWidth: 480, textAlign: 'center' }} type={'secondary'}>
{t('sharePageDisclaimer')}
</Text>
</Flexbox>
}
/>
</ConversationProvider>
);
});
@@ -0,0 +1,58 @@
'use client';
import { ActionIcon, copyToClipboard } from '@lobehub/ui';
import { type DropdownItem, DropdownMenu } from '@lobehub/ui/base-ui';
import { App } from 'antd';
import { ExternalLink, Flag, LinkIcon, MoreHorizontal } from 'lucide-react';
import { memo, useCallback, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { mailTo, OFFICIAL_SITE } from '@/const/url';
const REPORT_EMAIL = 'hi@lobehub.com';
const HeaderMenu = memo(() => {
const { t } = useTranslation('chat');
const { message } = App.useApp();
const handleCopyLink = useCallback(async () => {
await copyToClipboard(window.location.href);
message.success(t('shareModal.copyLinkSuccess'));
}, [message, t]);
const items = useMemo<DropdownItem[]>(
() => [
{
icon: <LinkIcon size={16} />,
key: 'copy-link',
label: t('sharePage.menu.copyLink'),
onClick: handleCopyLink,
},
{
icon: <ExternalLink size={16} />,
key: 'go-to-lobehub',
label: (
<a href={OFFICIAL_SITE} rel="noopener noreferrer" target="_blank">
{t('sharePage.menu.goToLobeHub')}
</a>
),
},
{
icon: <Flag size={16} />,
key: 'report',
label: <a href={mailTo(REPORT_EMAIL)}>{t('sharePage.menu.report')}</a>,
},
],
[t, handleCopyLink],
);
return (
<DropdownMenu items={items} placement={'bottomRight'}>
<ActionIcon icon={MoreHorizontal} title={t('sharePage.menu.more')} />
</DropdownMenu>
);
});
HeaderMenu.displayName = 'ShareTopicHeaderMenu';
export default HeaderMenu;
+5 -7
View File
@@ -1,11 +1,10 @@
'use client';
import { Alert, Center, Flexbox } from '@lobehub/ui';
import { Center, Flexbox } from '@lobehub/ui';
import { cx } from 'antd-style';
import NextLink from 'next/link';
import { type PropsWithChildren } from 'react';
import { memo, Suspense } from 'react';
import { useTranslation } from 'react-i18next';
import { Link, Outlet } from 'react-router-dom';
import { ProductLogo } from '@/components/Branding';
@@ -17,11 +16,11 @@ import { useUserStore } from '@/store/user';
import { authSelectors } from '@/store/user/slices/auth/selectors';
import SharePortal from '../features/Portal';
import HeaderMenu from './HeaderMenu';
import { styles } from './style';
import Title from './Title';
const ShareTopicLayout = memo<PropsWithChildren>(({ children }) => {
const { t } = useTranslation('chat');
const isDarkMode = useIsDark();
const isLogin = useUserStore(authSelectors.isLogin);
@@ -68,7 +67,9 @@ const ShareTopicLayout = memo<PropsWithChildren>(({ children }) => {
<Title />
</Suspense>
</Center>
<Flexbox horizontal align="center" flex={1} gap={12} justify={'flex-end'} />
<Flexbox horizontal align="center" flex={1} gap={12} justify={'flex-end'}>
<HeaderMenu />
</Flexbox>
</Flexbox>
<Flexbox horizontal className={styles.content} style={{ overflow: 'hidden' }}>
<Flexbox flex={1} style={{ overflow: 'hidden' }}>
@@ -78,9 +79,6 @@ const ShareTopicLayout = memo<PropsWithChildren>(({ children }) => {
</Flexbox>
<SharePortal />
</Flexbox>
<Center padding={8} style={{ opacity: 0.25 }}>
<Alert title={t('sharePageDisclaimer')} type={'secondary'} variant={'borderless'} />
</Center>
</Flexbox>
</Flexbox>
);
@@ -81,6 +81,7 @@ const ActionBar = memo<ActionBarProps>(({ data }) => {
borderRadius: 48,
boxShadow: '0 2px 12px -4px rgba(0, 0, 0, 0.1)',
maxWidth: 960,
pointerEvents: 'auto',
}}
>
<Flexbox horizontal align="center" gap={8}>
+12 -4
View File
@@ -1,6 +1,6 @@
'use client';
import { Button, Center } from '@lobehub/ui';
import { Button, Center, Flexbox } from '@lobehub/ui';
import { TRPCClientError } from '@trpc/client';
import { createStaticStyles } from 'antd-style';
import { memo } from 'react';
@@ -106,17 +106,25 @@ const ShareTopicPage = memo(() => {
if (!data) return null;
return (
<>
<Flexbox height={'100%'} style={{ position: 'relative' }} width={'100%'}>
<SharedMessageList
agentId={data.agentId}
groupId={data.groupId}
shareId={data.shareId}
topicId={data.topicId}
/>
<Center padding={8}>
<Center
paddingBlock={16}
style={{
bottom: 0,
insetInline: 0,
pointerEvents: 'none',
position: 'absolute',
}}
>
<ActionBar data={data} />
</Center>
</>
</Flexbox>
);
});
+22
View File
@@ -0,0 +1,22 @@
import { MessageSquare } from 'lucide-react';
import useSWR from 'swr';
import { lambdaClient } from '@/libs/trpc/client';
import { routeMeta } from '@/spa/router/routeMeta';
export const shareTopicRouteMeta = routeMeta({
icon: MessageSquare,
titleKey: 'navigation.chat',
useDynamicMeta: (params) => {
const shareId = params.id;
const { data } = useSWR(
shareId ? ['shared-topic', shareId] : null,
() => lambdaClient.share.getSharedTopic.query({ shareId: shareId! }),
{ revalidateOnFocus: false },
);
return {
title: data?.title || undefined,
};
},
});
@@ -114,6 +114,7 @@ import AllTasksPage from '@/routes/(main)/tasks';
import SharePagePage from '@/routes/share/page/[id]';
import ShareTopicPage from '@/routes/share/t/[id]';
import ShareTopicLayout from '@/routes/share/t/[id]/_layout';
import { shareTopicRouteMeta } from '@/routes/share/t/[id]/routeMeta';
import { routeMeta } from '@/spa/router/routeMeta';
import { SettingsTabs } from '@/store/global/initialState';
import { ErrorBoundary, redirectElement } from '@/utils/router';
@@ -693,6 +694,7 @@ export const desktopRoutes: RouteObject[] = [
children: [
{
element: <ShareTopicPage />,
handle: { meta: shareTopicRouteMeta },
path: ':id',
},
],
+2
View File
@@ -20,6 +20,7 @@ import { pageRouteMeta } from '@/features/Pages/routeMeta';
import { agentRouteMeta } from '@/routes/(main)/agent/features/routeMeta';
import { groupRouteMeta } from '@/routes/(main)/group/features/routeMeta';
import { settingsRouteMeta } from '@/routes/(main)/settings/features/routeMeta';
import { shareTopicRouteMeta } from '@/routes/share/t/[id]/routeMeta';
import { routeMeta } from '@/spa/router/routeMeta';
import { SettingsTabs } from '@/store/global/initialState';
import { dynamicElement, dynamicLayout, ErrorBoundary, redirectElement } from '@/utils/router';
@@ -890,6 +891,7 @@ export const desktopRoutes: RouteObject[] = [
children: [
{
element: dynamicElement(() => import('@/routes/share/t/[id]'), 'Desktop > Share > Topic'),
handle: { meta: shareTopicRouteMeta },
path: ':id',
},
],
+2 -4
View File
@@ -6,11 +6,9 @@ import {
BusinessMobileRoutesWithMainLayout,
BusinessMobileRoutesWithoutMainLayout,
} from '@/business/client/BusinessMobileRoutes';
import {
mobileAgentSettingsRouteMeta,
shareTopicRouteMeta,
} from '@/features/RouteMeta/mobileRouteMeta';
import { mobileAgentSettingsRouteMeta } from '@/features/RouteMeta/mobileRouteMeta';
import { agentRouteMeta } from '@/routes/(main)/agent/features/routeMeta';
import { shareTopicRouteMeta } from '@/routes/share/t/[id]/routeMeta';
import { dynamicElement, dynamicLayout, ErrorBoundary, redirectElement } from '@/utils/router';
/**