mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-05-13 15:10:05 +00:00
fix: Reply UI
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
.chat-page {
|
||||
.chat-page {
|
||||
position: relative;
|
||||
display: flex;
|
||||
height: 100%;
|
||||
@@ -2448,13 +2448,197 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
// 引用消息样式
|
||||
// ═══════════════════════════════════════════
|
||||
// Ambient Reply System — "Spectral Thread"
|
||||
// ═══════════════════════════════════════════
|
||||
|
||||
// Wrapper for the entire ambient reply UI
|
||||
.ambient-reply-wrapper {
|
||||
position: relative;
|
||||
cursor: pointer;
|
||||
margin-bottom: 4px;
|
||||
|
||||
// Reply anchor — always visible, minimal
|
||||
.reply-anchor {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 2px 6px 2px 4px;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
opacity: 0.42;
|
||||
transition: opacity 0.35s ease, background-color 0.35s ease;
|
||||
user-select: none;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
&:hover .reply-anchor {
|
||||
opacity: 1;
|
||||
background: color-mix(in srgb, var(--primary) 8%, transparent);
|
||||
}
|
||||
|
||||
.reply-anchor-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
color: var(--primary);
|
||||
opacity: 0.7;
|
||||
flex-shrink: 0;
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
&:hover .reply-anchor-icon {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.reply-anchor-name {
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
letter-spacing: 0.01em;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-sep {
|
||||
font-size: 10px;
|
||||
color: var(--text-tertiary);
|
||||
margin: 0 1px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-excerpt {
|
||||
font-size: 12px;
|
||||
color: var(--text-secondary);
|
||||
max-width: 200px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
opacity: 0.7;
|
||||
|
||||
// Hide inline emoji images in anchor excerpt
|
||||
.inline-emoji {
|
||||
width: 14px !important;
|
||||
height: 14px !important;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
// Ghost preview — appears on hover, frosted glass
|
||||
.reply-ghost {
|
||||
position: absolute;
|
||||
bottom: calc(100% + 6px);
|
||||
max-width: 320px;
|
||||
min-width: 160px;
|
||||
padding: 8px 12px 10px 12px;
|
||||
border-radius: 12px;
|
||||
background: color-mix(in srgb, var(--bg-primary) 75%, transparent);
|
||||
backdrop-filter: blur(20px) saturate(1.3);
|
||||
-webkit-backdrop-filter: blur(20px) saturate(1.3);
|
||||
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.04);
|
||||
opacity: 0;
|
||||
transform: translateY(4px) scale(0.98);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.35s cubic-bezier(0.16, 1, 0.3, 1),
|
||||
transform 0.35s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
z-index: 50;
|
||||
|
||||
// Gradient accent bar on left edge
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 8px;
|
||||
bottom: 8px;
|
||||
width: 2.5px;
|
||||
border-radius: 2px;
|
||||
background: linear-gradient(to bottom, color-mix(in srgb, var(--primary) 40%, transparent), transparent);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover .reply-ghost {
|
||||
opacity: 1;
|
||||
transform: translateY(0) scale(1);
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.reply-ghost-sender {
|
||||
font-size: 11px;
|
||||
font-weight: 500;
|
||||
color: var(--primary);
|
||||
opacity: 0.75;
|
||||
margin-bottom: 3px;
|
||||
padding-left: 6px;
|
||||
}
|
||||
|
||||
.reply-ghost-text {
|
||||
font-size: 12.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--text-secondary);
|
||||
opacity: 0.82;
|
||||
padding-left: 6px;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
word-break: break-word;
|
||||
|
||||
// Edge fade effect
|
||||
-webkit-mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
|
||||
mask-image: linear-gradient(to bottom, #000 55%, transparent 100%);
|
||||
|
||||
.inline-emoji {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
vertical-align: -2px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sent message — adjust ghost position to right
|
||||
.message-bubble.sent .ambient-reply-wrapper {
|
||||
.reply-ghost {
|
||||
right: 0;
|
||||
}
|
||||
|
||||
.reply-anchor-name {
|
||||
color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-excerpt {
|
||||
color: color-mix(in srgb, var(--on-primary) 72%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-sep {
|
||||
color: color-mix(in srgb, var(--on-primary) 50%, var(--primary));
|
||||
}
|
||||
|
||||
.reply-anchor-icon {
|
||||
color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
|
||||
}
|
||||
|
||||
&:hover .reply-anchor {
|
||||
background: color-mix(in srgb, var(--on-primary) 10%, transparent);
|
||||
}
|
||||
|
||||
.reply-ghost {
|
||||
background: color-mix(in srgb, var(--bg-primary) 80%, transparent);
|
||||
}
|
||||
}
|
||||
|
||||
// Received message — ghost to left
|
||||
.message-bubble.received .ambient-reply-wrapper {
|
||||
.reply-ghost {
|
||||
left: 0;
|
||||
}
|
||||
}
|
||||
// Legacy .quoted-message — used by SettingsPage quote-layout preview widget
|
||||
.quoted-message {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
border-left: 2px solid var(--primary);
|
||||
padding: 6px 10px;
|
||||
padding: 4px 8px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
font-size: 12px;
|
||||
|
||||
.quoted-sender {
|
||||
color: var(--primary);
|
||||
@@ -2468,38 +2652,9 @@
|
||||
|
||||
.quoted-text {
|
||||
color: var(--text-secondary);
|
||||
white-space: pre-wrap;
|
||||
|
||||
.quoted-type-label {
|
||||
font-style: italic;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.quoted-emoji-image {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
vertical-align: middle;
|
||||
object-fit: contain;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 自己发送的消息中的引用样式
|
||||
.message-bubble.sent .quoted-message {
|
||||
background: color-mix(in srgb, var(--on-primary) 12%, var(--primary));
|
||||
border-left-color: color-mix(in srgb, var(--on-primary) 36%, var(--primary));
|
||||
|
||||
.quoted-sender {
|
||||
color: color-mix(in srgb, var(--on-primary) 92%, var(--primary));
|
||||
}
|
||||
|
||||
.quoted-text {
|
||||
color: color-mix(in srgb, var(--on-primary) 80%, var(--primary));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// 气泡内容区域(包含名字和内容)
|
||||
.bubble-body {
|
||||
display: flex;
|
||||
@@ -2514,14 +2669,6 @@
|
||||
|
||||
.bubble-content {
|
||||
-webkit-app-region: no-drag;
|
||||
|
||||
&.quote-layout-top .quoted-message {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
&.quote-layout-bottom .quoted-message {
|
||||
margin-top: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
// 时间分隔
|
||||
@@ -5611,23 +5758,7 @@
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.quoted-message {
|
||||
background: transparent;
|
||||
border-left: 2px solid color-mix(in srgb, var(--primary) 62%, var(--text-tertiary));
|
||||
border-radius: 0;
|
||||
padding: 2px 0 2px 10px;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.message-bubble.sent .quoted-message {
|
||||
background: color-mix(in srgb, var(--on-primary) 12%, transparent);
|
||||
border-left-color: color-mix(in srgb, var(--on-primary) 62%, var(--primary));
|
||||
|
||||
.quoted-sender,
|
||||
.quoted-text {
|
||||
color: color-mix(in srgb, var(--on-primary) 82%, var(--primary));
|
||||
}
|
||||
}
|
||||
// Ambient Reply dark mode / alternate adjustments handled via CSS variables
|
||||
|
||||
.link-message,
|
||||
.card-message,
|
||||
|
||||
@@ -64,7 +64,7 @@ interface GlobalMsgPrefixCacheEntry {
|
||||
completed: boolean
|
||||
}
|
||||
|
||||
type QuoteLayout = configService.QuoteLayout
|
||||
|
||||
|
||||
const GLOBAL_MSG_PER_SESSION_LIMIT = 10
|
||||
const GLOBAL_MSG_SEED_LIMIT = 120
|
||||
@@ -8252,7 +8252,6 @@ function MessageBubble({
|
||||
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
|
||||
const [senderName, setSenderName] = useState<string | undefined>(undefined)
|
||||
const [quotedSenderName, setQuotedSenderName] = useState<string | undefined>(undefined)
|
||||
const [quoteLayout, setQuoteLayout] = useState<QuoteLayout>('quote-top')
|
||||
const [solitaireExpanded, setSolitaireExpanded] = useState(false)
|
||||
const senderProfileRequestSeqRef = useRef(0)
|
||||
const [emojiError, setEmojiError] = useState(false)
|
||||
@@ -9401,17 +9400,7 @@ function MessageBubble({
|
||||
myWxid
|
||||
])
|
||||
|
||||
useEffect(() => {
|
||||
let cancelled = false
|
||||
void configService.getQuoteLayout().then((layout) => {
|
||||
if (!cancelled) setQuoteLayout(layout)
|
||||
}).catch(() => {
|
||||
if (!cancelled) setQuoteLayout('quote-top')
|
||||
})
|
||||
return () => {
|
||||
cancelled = true
|
||||
}
|
||||
}, [])
|
||||
// quoteLayout config removed - Ambient Reply uses a single fixed layout
|
||||
|
||||
const locationMessageMeta = useMemo(() => {
|
||||
if (message.localType !== 48) return null
|
||||
@@ -9448,29 +9437,32 @@ function MessageBubble({
|
||||
// 是否有引用消息
|
||||
const hasQuote = quotedContent.length > 0
|
||||
const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName
|
||||
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => {
|
||||
const quoteFirst = quoteLayout !== 'quote-bottom'
|
||||
return (
|
||||
<div className={`bubble-content ${quoteFirst ? 'quote-layout-top' : 'quote-layout-bottom'}`}>
|
||||
{quoteFirst ? (
|
||||
<>
|
||||
{quotedNode}
|
||||
{messageNode}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{messageNode}
|
||||
{quotedNode}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}, [quoteLayout])
|
||||
// Ambient Reply: single fixed layout (anchor above, message below)
|
||||
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => (
|
||||
<div className="bubble-content">
|
||||
{quotedNode}
|
||||
{messageNode}
|
||||
</div>
|
||||
), [])
|
||||
|
||||
// Ambient Reply: render reply-anchor + ghost preview
|
||||
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
|
||||
<div className="quoted-message">
|
||||
{displayQuotedSenderName && <span className="quoted-sender">{displayQuotedSenderName}</span>}
|
||||
<span className="quoted-text">{contentNode}</span>
|
||||
<div className="ambient-reply-wrapper">
|
||||
{/* Reply anchor - always visible, subtle */}
|
||||
<div className="reply-anchor">
|
||||
<svg className="reply-anchor-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<polyline points="9 14 4 9 9 4" />
|
||||
<path d="M20 20v-7a4 4 0 0 0-4-4H4" />
|
||||
</svg>
|
||||
{displayQuotedSenderName && <span className="reply-anchor-name">{displayQuotedSenderName}</span>}
|
||||
<span className="reply-anchor-sep">·</span>
|
||||
<span className="reply-anchor-excerpt">{contentNode}</span>
|
||||
</div>
|
||||
{/* Ghost preview - appears on hover */}
|
||||
<div className="reply-ghost">
|
||||
{displayQuotedSenderName && <div className="reply-ghost-sender">{displayQuotedSenderName}</div>}
|
||||
<div className="reply-ghost-text">{contentNode}</div>
|
||||
</div>
|
||||
</div>
|
||||
), [displayQuotedSenderName])
|
||||
|
||||
|
||||
Reference in New Issue
Block a user