fix: Reply Setting

This commit is contained in:
Jason
2026-05-12 23:08:34 +08:00
parent d5d64b2b50
commit 16608b2c8e
3 changed files with 69 additions and 7 deletions

View File

@@ -2549,6 +2549,25 @@
}
// Ghost preview — appears on hover, frosted glass
&.preview-below {
margin-top: 4px;
margin-bottom: 0;
.reply-ghost {
top: calc(100% + 6px);
bottom: auto;
transform: translateY(-4px) scale(0.98);
}
&:hover .reply-ghost {
transform: translateY(0) scale(1);
}
}
&.preview-above {
margin-bottom: 4px;
}
.reply-ghost {
position: absolute;
bottom: calc(100% + 6px);

View File

@@ -1510,6 +1510,7 @@ function ChatPage(props: ChatPageProps) {
const [groupMemberSearchKeyword, setGroupMemberSearchKeyword] = useState('')
const [copiedField, setCopiedField] = useState<string | null>(null)
const [highlightedMessageKeys, setHighlightedMessageKeys] = useState<string[]>([])
const [quoteLayout, setQuoteLayout] = useState<configService.QuoteLayout>('quote-top')
const [isRefreshingSessions, setIsRefreshingSessions] = useState(false)
const [foldedView, setFoldedView] = useState(false) // 是否在"折叠的群聊"视图
const [bizView, setBizView] = useState(false) // 是否在"公众号"视图
@@ -3066,6 +3067,33 @@ function ChatPage(props: ChatPageProps) {
}
}, [])
useEffect(() => {
let canceled = false
const loadQuoteLayout = () => {
void configService.getQuoteLayout()
.then((layout) => {
if (!canceled) setQuoteLayout(layout)
})
.catch(() => {
if (!canceled) setQuoteLayout('quote-top')
})
}
loadQuoteLayout()
const handleFocus = () => loadQuoteLayout()
const handleQuoteLayoutChanged = (event: Event) => {
const layout = (event as CustomEvent<configService.QuoteLayout>).detail
setQuoteLayout(layout === 'quote-bottom' ? 'quote-bottom' : 'quote-top')
}
window.addEventListener('focus', handleFocus)
window.addEventListener('quote-layout-changed', handleQuoteLayoutChanged)
return () => {
canceled = true
window.removeEventListener('focus', handleFocus)
window.removeEventListener('quote-layout-changed', handleQuoteLayoutChanged)
}
}, [])
useEffect(() => {
let cancelled = false
void (async () => {
@@ -6820,6 +6848,7 @@ function ChatPage(props: ChatPageProps) {
myAvatarUrl={myAvatarUrl}
myWxid={myWxid}
isGroupChat={isCurrentSessionGroup}
quoteLayout={quoteLayout}
autoTranscribeVoiceEnabled={autoTranscribeVoiceEnabled}
onRequireModelDownload={handleRequireModelDownload}
onContextMenu={handleContextMenu}
@@ -6840,6 +6869,7 @@ function ChatPage(props: ChatPageProps) {
myAvatarUrl,
myWxid,
isCurrentSessionGroup,
quoteLayout,
autoTranscribeVoiceEnabled,
handleRequireModelDownload,
handleContextMenu,
@@ -8395,6 +8425,7 @@ function MessageBubble({
myAvatarUrl,
myWxid,
isGroupChat,
quoteLayout,
autoTranscribeVoiceEnabled,
onRequireModelDownload,
onContextMenu,
@@ -8410,6 +8441,7 @@ function MessageBubble({
myAvatarUrl?: string;
myWxid?: string;
isGroupChat?: boolean;
quoteLayout: configService.QuoteLayout;
autoTranscribeVoiceEnabled?: boolean;
onRequireModelDownload?: (sessionId: string, messageId: string) => void;
onContextMenu?: (e: React.MouseEvent, message: Message) => void;
@@ -9665,17 +9697,26 @@ function MessageBubble({
event.stopPropagation()
onJumpToQuotedMessage(quotedJumpTarget)
}, [isSelectionMode, onJumpToQuotedMessage, quotedJumpTarget])
// Ambient Reply: single fixed layout (anchor above, message below)
const isQuoteBelow = quoteLayout === 'quote-bottom'
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => (
<div className="bubble-content">
{quotedNode}
{messageNode}
<div className={`bubble-content ${isQuoteBelow ? 'quote-layout-bottom' : 'quote-layout-top'}`}>
{isQuoteBelow ? (
<>
{messageNode}
{quotedNode}
</>
) : (
<>
{quotedNode}
{messageNode}
</>
)}
</div>
), [])
), [isQuoteBelow])
// Ambient Reply: render reply-anchor + ghost preview
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
<div className="ambient-reply-wrapper">
<div className={`ambient-reply-wrapper ${isQuoteBelow ? 'preview-below' : 'preview-above'}`}>
{/* Reply anchor - always visible, subtle */}
<div
className={`reply-anchor ${quotedJumpTarget ? 'jumpable' : ''}`}
@@ -9698,7 +9739,7 @@ function MessageBubble({
<div className="reply-ghost-text">{contentNode}</div>
</div>
</div>
), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isSelectionMode, quotedJumpTarget])
), [displayQuotedSenderName, handleQuotedJumpClick, handleQuotedJumpKeyDown, isQuoteBelow, isSelectionMode, quotedJumpTarget])
const handlePlayVideo = useCallback(async () => {
if (!videoInfo?.videoUrl) return
@@ -10893,6 +10934,7 @@ const MemoMessageBubble = React.memo(MessageBubble, (prevProps, nextProps) => {
if (prevProps.myAvatarUrl !== nextProps.myAvatarUrl) return false
if (prevProps.myWxid !== nextProps.myWxid) return false
if (prevProps.isGroupChat !== nextProps.isGroupChat) return false
if (prevProps.quoteLayout !== nextProps.quoteLayout) return false
if (prevProps.autoTranscribeVoiceEnabled !== nextProps.autoTranscribeVoiceEnabled) return false
if (prevProps.isSelectionMode !== nextProps.isSelectionMode) return false
if (prevProps.isSelected !== nextProps.isSelected) return false

View File

@@ -1698,6 +1698,7 @@ function SettingsPage({ onClose }: SettingsPageProps = {}) {
if (selected) return
setQuoteLayout(option.value)
await configService.setQuoteLayout(option.value)
window.dispatchEvent(new CustomEvent('quote-layout-changed', { detail: option.value }))
showMessage(option.successMessage, true)
}}
role="radio"