fix: Reply UI
This commit is contained in:
1 parent
e41a1197cb
commit
762a2ec832
2 files changed
+215
-92
No files matched your search
+189
-58
@@ -1,4 +1,4 @@
|
|||||||
.chat-page {
|
.chat-page {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -2448,13 +2448,197 @@
|
|||||||
white-space: nowrap;
|
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 {
|
.quoted-message {
|
||||||
background: rgba(0, 0, 0, 0.04);
|
background: rgba(0, 0, 0, 0.04);
|
||||||
border-left: 2px solid var(--primary);
|
border-left: 2px solid var(--primary);
|
||||||
padding: 6px 10px;
|
padding: 4px 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
|
|
||||||
.quoted-sender {
|
.quoted-sender {
|
||||||
color: var(--primary);
|
color: var(--primary);
|
||||||
@@ -2468,38 +2652,9 @@
|
|||||||
|
|
||||||
.quoted-text {
|
.quoted-text {
|
||||||
color: var(--text-secondary);
|
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 {
|
.bubble-body {
|
||||||
display: flex;
|
display: flex;
|
||||||
@@ -2514,14 +2669,6 @@
|
|||||||
|
|
||||||
.bubble-content {
|
.bubble-content {
|
||||||
-webkit-app-region: no-drag;
|
-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;
|
margin-bottom: 5px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.quoted-message {
|
// Ambient Reply dark mode / alternate adjustments handled via CSS variables
|
||||||
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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.link-message,
|
.link-message,
|
||||||
.card-message,
|
.card-message,
|
||||||
|
|||||||
+26
-34
@@ -64,7 +64,7 @@ interface GlobalMsgPrefixCacheEntry {
|
|||||||
completed: boolean
|
completed: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type QuoteLayout = configService.QuoteLayout
|
|
||||||
|
|
||||||
const GLOBAL_MSG_PER_SESSION_LIMIT = 10
|
const GLOBAL_MSG_PER_SESSION_LIMIT = 10
|
||||||
const GLOBAL_MSG_SEED_LIMIT = 120
|
const GLOBAL_MSG_SEED_LIMIT = 120
|
||||||
@@ -8252,7 +8252,6 @@ function MessageBubble({
|
|||||||
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
|
const [senderAvatarUrl, setSenderAvatarUrl] = useState<string | undefined>(undefined)
|
||||||
const [senderName, setSenderName] = useState<string | undefined>(undefined)
|
const [senderName, setSenderName] = useState<string | undefined>(undefined)
|
||||||
const [quotedSenderName, setQuotedSenderName] = useState<string | undefined>(undefined)
|
const [quotedSenderName, setQuotedSenderName] = useState<string | undefined>(undefined)
|
||||||
const [quoteLayout, setQuoteLayout] = useState<QuoteLayout>('quote-top')
|
|
||||||
const [solitaireExpanded, setSolitaireExpanded] = useState(false)
|
const [solitaireExpanded, setSolitaireExpanded] = useState(false)
|
||||||
const senderProfileRequestSeqRef = useRef(0)
|
const senderProfileRequestSeqRef = useRef(0)
|
||||||
const [emojiError, setEmojiError] = useState(false)
|
const [emojiError, setEmojiError] = useState(false)
|
||||||
@@ -9401,17 +9400,7 @@ function MessageBubble({
|
|||||||
myWxid
|
myWxid
|
||||||
])
|
])
|
||||||
|
|
||||||
useEffect(() => {
|
// quoteLayout config removed - Ambient Reply uses a single fixed layout
|
||||||
let cancelled = false
|
|
||||||
void configService.getQuoteLayout().then((layout) => {
|
|
||||||
if (!cancelled) setQuoteLayout(layout)
|
|
||||||
}).catch(() => {
|
|
||||||
if (!cancelled) setQuoteLayout('quote-top')
|
|
||||||
})
|
|
||||||
return () => {
|
|
||||||
cancelled = true
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const locationMessageMeta = useMemo(() => {
|
const locationMessageMeta = useMemo(() => {
|
||||||
if (message.localType !== 48) return null
|
if (message.localType !== 48) return null
|
||||||
@@ -9448,29 +9437,32 @@ function MessageBubble({
|
|||||||
// 是否有引用消息
|
// 是否有引用消息
|
||||||
const hasQuote = quotedContent.length > 0
|
const hasQuote = quotedContent.length > 0
|
||||||
const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName
|
const displayQuotedSenderName = quotedSenderName || quotedSenderFallbackName
|
||||||
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => {
|
// Ambient Reply: single fixed layout (anchor above, message below)
|
||||||
const quoteFirst = quoteLayout !== 'quote-bottom'
|
const renderBubbleWithQuote = useCallback((quotedNode: React.ReactNode, messageNode: React.ReactNode) => (
|
||||||
return (
|
<div className="bubble-content">
|
||||||
<div className={`bubble-content ${quoteFirst ? 'quote-layout-top' : 'quote-layout-bottom'}`}>
|
{quotedNode}
|
||||||
{quoteFirst ? (
|
{messageNode}
|
||||||
<>
|
</div>
|
||||||
{quotedNode}
|
), [])
|
||||||
{messageNode}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
{messageNode}
|
|
||||||
{quotedNode}
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}, [quoteLayout])
|
|
||||||
|
|
||||||
|
// Ambient Reply: render reply-anchor + ghost preview
|
||||||
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
|
const renderQuotedMessageBlock = useCallback((contentNode: React.ReactNode) => (
|
||||||
<div className="quoted-message">
|
<div className="ambient-reply-wrapper">
|
||||||
{displayQuotedSenderName && <span className="quoted-sender">{displayQuotedSenderName}</span>}
|
{/* Reply anchor - always visible, subtle */}
|
||||||
<span className="quoted-text">{contentNode}</span>
|
<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>
|
</div>
|
||||||
), [displayQuotedSenderName])
|
), [displayQuotedSenderName])
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user