mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-04-22 15:09:04 +00:00
图片解密再次优化
This commit is contained in:
@@ -29,6 +29,7 @@ import {
|
||||
onSingleExportDialogStatus,
|
||||
requestExportSessionStatus
|
||||
} from '../services/exportBridge'
|
||||
import '../styles/batchTranscribe.scss'
|
||||
import './ChatPage.scss'
|
||||
|
||||
// 系统消息类型常量
|
||||
@@ -1370,34 +1371,30 @@ function ChatPage(props: ChatPageProps) {
|
||||
const {
|
||||
isBatchTranscribing,
|
||||
runningBatchVoiceTaskType,
|
||||
batchTranscribeProgress,
|
||||
startTranscribe,
|
||||
updateProgress,
|
||||
finishTranscribe,
|
||||
setShowBatchProgress
|
||||
updateTranscribeTaskStatus,
|
||||
finishTranscribe
|
||||
} = useBatchTranscribeStore(useShallow((state) => ({
|
||||
isBatchTranscribing: state.isBatchTranscribing,
|
||||
runningBatchVoiceTaskType: state.taskType,
|
||||
batchTranscribeProgress: state.progress,
|
||||
startTranscribe: state.startTranscribe,
|
||||
updateProgress: state.updateProgress,
|
||||
finishTranscribe: state.finishTranscribe,
|
||||
setShowBatchProgress: state.setShowToast
|
||||
updateTranscribeTaskStatus: state.setTaskStatus,
|
||||
finishTranscribe: state.finishTranscribe
|
||||
})))
|
||||
const {
|
||||
isBatchDecrypting,
|
||||
batchDecryptProgress,
|
||||
startDecrypt,
|
||||
updateDecryptProgress,
|
||||
finishDecrypt,
|
||||
setShowBatchDecryptToast
|
||||
updateDecryptTaskStatus,
|
||||
finishDecrypt
|
||||
} = useBatchImageDecryptStore(useShallow((state) => ({
|
||||
isBatchDecrypting: state.isBatchDecrypting,
|
||||
batchDecryptProgress: state.progress,
|
||||
startDecrypt: state.startDecrypt,
|
||||
updateDecryptProgress: state.updateProgress,
|
||||
finishDecrypt: state.finishDecrypt,
|
||||
setShowBatchDecryptToast: state.setShowToast
|
||||
updateDecryptTaskStatus: state.setTaskStatus,
|
||||
finishDecrypt: state.finishDecrypt
|
||||
})))
|
||||
const [showBatchConfirm, setShowBatchConfirm] = useState(false)
|
||||
const [batchVoiceCount, setBatchVoiceCount] = useState(0)
|
||||
@@ -5730,22 +5727,74 @@ function ChatPage(props: ChatPageProps) {
|
||||
if (!session) return
|
||||
|
||||
const taskType = batchVoiceTaskType
|
||||
startTranscribe(voiceMessages.length, session.displayName || session.username, taskType)
|
||||
|
||||
if (taskType === 'transcribe') {
|
||||
// 检查模型状态
|
||||
const modelStatus = await window.electronAPI.whisper.getModelStatus()
|
||||
if (!modelStatus?.exists) {
|
||||
alert('SenseVoice 模型未下载,请先在设置中下载模型')
|
||||
finishTranscribe(0, 0)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
const totalVoices = voiceMessages.length
|
||||
const taskVerb = taskType === 'decrypt' ? '语音解密' : '语音转写'
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
let completedCount = 0
|
||||
const concurrency = taskType === 'decrypt' ? 12 : 10
|
||||
const controlState = {
|
||||
cancelRequested: false,
|
||||
pauseRequested: false,
|
||||
pauseAnnounced: false,
|
||||
resumeWaiters: [] as Array<() => void>
|
||||
}
|
||||
const resolveResumeWaiters = () => {
|
||||
const waiters = [...controlState.resumeWaiters]
|
||||
controlState.resumeWaiters.length = 0
|
||||
waiters.forEach(resolve => resolve())
|
||||
}
|
||||
const waitIfPaused = async () => {
|
||||
while (controlState.pauseRequested && !controlState.cancelRequested) {
|
||||
if (!controlState.pauseAnnounced) {
|
||||
controlState.pauseAnnounced = true
|
||||
updateTranscribeTaskStatus(
|
||||
`${taskVerb}任务已中断,等待继续...`,
|
||||
`${completedCount} / ${totalVoices}`,
|
||||
'paused'
|
||||
)
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controlState.resumeWaiters.push(resolve)
|
||||
})
|
||||
}
|
||||
if (controlState.pauseAnnounced && !controlState.cancelRequested) {
|
||||
controlState.pauseAnnounced = false
|
||||
updateTranscribeTaskStatus(
|
||||
`继续${taskVerb}(${completedCount}/${totalVoices})`,
|
||||
`${completedCount} / ${totalVoices}`,
|
||||
'running'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startTranscribe(totalVoices, session.displayName || session.username, taskType, 'chat', {
|
||||
cancelable: true,
|
||||
resumable: true,
|
||||
onPause: () => {
|
||||
controlState.pauseRequested = true
|
||||
updateTranscribeTaskStatus(
|
||||
`${taskVerb}中断请求已发出,当前处理完成后暂停...`,
|
||||
`${completedCount} / ${totalVoices}`,
|
||||
'pause_requested'
|
||||
)
|
||||
},
|
||||
onResume: () => {
|
||||
controlState.pauseRequested = false
|
||||
resolveResumeWaiters()
|
||||
},
|
||||
onCancel: () => {
|
||||
controlState.cancelRequested = true
|
||||
controlState.pauseRequested = false
|
||||
resolveResumeWaiters()
|
||||
updateTranscribeTaskStatus(
|
||||
`${taskVerb}停止请求已发出,当前处理完成后结束...`,
|
||||
`${completedCount} / ${totalVoices}`,
|
||||
'cancel_requested'
|
||||
)
|
||||
}
|
||||
})
|
||||
updateTranscribeTaskStatus(`正在准备${taskVerb}任务...`, `0 / ${totalVoices}`, 'running')
|
||||
|
||||
const runOne = async (msg: Message) => {
|
||||
try {
|
||||
@@ -5769,20 +5818,74 @@ function ChatPage(props: ChatPageProps) {
|
||||
}
|
||||
}
|
||||
|
||||
for (let i = 0; i < voiceMessages.length; i += concurrency) {
|
||||
const batch = voiceMessages.slice(i, i + concurrency)
|
||||
const results = await Promise.all(batch.map(msg => runOne(msg)))
|
||||
try {
|
||||
if (taskType === 'transcribe') {
|
||||
updateTranscribeTaskStatus('正在检查转写模型...', `0 / ${totalVoices}`)
|
||||
const modelStatus = await window.electronAPI.whisper.getModelStatus()
|
||||
if (!modelStatus?.exists) {
|
||||
alert('SenseVoice 模型未下载,请先在设置中下载模型')
|
||||
updateTranscribeTaskStatus('转写模型缺失,任务已停止', `0 / ${totalVoices}`)
|
||||
finishTranscribe(0, totalVoices)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
results.forEach(result => {
|
||||
updateTranscribeTaskStatus(`正在${taskVerb}(0/${totalVoices})`, `0 / ${totalVoices}`)
|
||||
const pool = new Set<Promise<void>>()
|
||||
|
||||
const runOneTracked = async (msg: Message) => {
|
||||
if (controlState.cancelRequested) return
|
||||
const result = await runOne(msg)
|
||||
if (result.success) successCount++
|
||||
else failCount++
|
||||
completedCount++
|
||||
updateProgress(completedCount, voiceMessages.length)
|
||||
})
|
||||
}
|
||||
updateProgress(completedCount, totalVoices)
|
||||
}
|
||||
|
||||
finishTranscribe(successCount, failCount)
|
||||
}, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages, batchVoiceTaskType, startTranscribe, updateProgress, finishTranscribe])
|
||||
for (const msg of voiceMessages) {
|
||||
if (controlState.cancelRequested) break
|
||||
await waitIfPaused()
|
||||
if (controlState.cancelRequested) break
|
||||
if (pool.size >= concurrency) {
|
||||
await Promise.race(pool)
|
||||
if (controlState.cancelRequested) break
|
||||
await waitIfPaused()
|
||||
if (controlState.cancelRequested) break
|
||||
}
|
||||
let p: Promise<void> = Promise.resolve()
|
||||
p = runOneTracked(msg).finally(() => {
|
||||
pool.delete(p)
|
||||
})
|
||||
pool.add(p)
|
||||
}
|
||||
|
||||
while (pool.size > 0) {
|
||||
await Promise.race(pool)
|
||||
}
|
||||
|
||||
if (controlState.cancelRequested) {
|
||||
const remaining = Math.max(0, totalVoices - completedCount)
|
||||
finishTranscribe(successCount, failCount, {
|
||||
status: 'canceled',
|
||||
detail: `${taskVerb}任务已中断:已完成 ${completedCount}/${totalVoices}(成功 ${successCount},失败 ${failCount},未处理 ${remaining})`,
|
||||
progressText: `${completedCount} / ${totalVoices}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
finishTranscribe(successCount, failCount, {
|
||||
status: failCount > 0 ? 'failed' : 'completed'
|
||||
})
|
||||
} catch (error) {
|
||||
const remaining = Math.max(0, totalVoices - completedCount)
|
||||
failCount += remaining
|
||||
updateTranscribeTaskStatus(`${taskVerb}过程中发生异常,正在结束任务...`, `${completedCount} / ${totalVoices}`)
|
||||
finishTranscribe(successCount, failCount, {
|
||||
status: 'failed'
|
||||
})
|
||||
alert(`批量${taskVerb}失败:${String(error)}`)
|
||||
}
|
||||
}, [sessions, currentSessionId, batchSelectedDates, batchVoiceMessages, batchVoiceTaskType, startTranscribe, updateTranscribeTaskStatus, updateProgress, finishTranscribe])
|
||||
|
||||
// 批量转写:按日期的消息数量
|
||||
const batchCountByDate = useMemo(() => {
|
||||
@@ -5845,14 +5948,114 @@ function ChatPage(props: ChatPageProps) {
|
||||
setBatchImageDates([])
|
||||
setBatchImageSelectedDates(new Set())
|
||||
|
||||
startDecrypt(images.length, session.displayName || session.username)
|
||||
|
||||
const totalImages = images.length
|
||||
let successCount = 0
|
||||
let failCount = 0
|
||||
let notFoundCount = 0
|
||||
let decryptFailedCount = 0
|
||||
let completed = 0
|
||||
const controlState = {
|
||||
cancelRequested: false,
|
||||
pauseRequested: false,
|
||||
pauseAnnounced: false,
|
||||
resumeWaiters: [] as Array<() => void>
|
||||
}
|
||||
const resolveResumeWaiters = () => {
|
||||
const waiters = [...controlState.resumeWaiters]
|
||||
controlState.resumeWaiters.length = 0
|
||||
waiters.forEach(resolve => resolve())
|
||||
}
|
||||
const waitIfPaused = async () => {
|
||||
while (controlState.pauseRequested && !controlState.cancelRequested) {
|
||||
if (!controlState.pauseAnnounced) {
|
||||
controlState.pauseAnnounced = true
|
||||
updateDecryptTaskStatus(
|
||||
'图片批量解密任务已中断,等待继续...',
|
||||
`${completed} / ${totalImages}`,
|
||||
'paused'
|
||||
)
|
||||
}
|
||||
await new Promise<void>(resolve => {
|
||||
controlState.resumeWaiters.push(resolve)
|
||||
})
|
||||
}
|
||||
if (controlState.pauseAnnounced && !controlState.cancelRequested) {
|
||||
controlState.pauseAnnounced = false
|
||||
updateDecryptTaskStatus(
|
||||
`继续批量解密图片(${completed}/${totalImages})`,
|
||||
`${completed} / ${totalImages}`,
|
||||
'running'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
startDecrypt(totalImages, session.displayName || session.username, 'chat', {
|
||||
cancelable: true,
|
||||
resumable: true,
|
||||
onPause: () => {
|
||||
controlState.pauseRequested = true
|
||||
updateDecryptTaskStatus(
|
||||
'图片解密中断请求已发出,当前处理完成后暂停...',
|
||||
`${completed} / ${totalImages}`,
|
||||
'pause_requested'
|
||||
)
|
||||
},
|
||||
onResume: () => {
|
||||
controlState.pauseRequested = false
|
||||
resolveResumeWaiters()
|
||||
},
|
||||
onCancel: () => {
|
||||
controlState.cancelRequested = true
|
||||
controlState.pauseRequested = false
|
||||
resolveResumeWaiters()
|
||||
updateDecryptTaskStatus(
|
||||
'图片解密停止请求已发出,当前处理完成后结束...',
|
||||
`${completed} / ${totalImages}`,
|
||||
'cancel_requested'
|
||||
)
|
||||
}
|
||||
})
|
||||
updateDecryptTaskStatus('正在准备批量图片解密任务...', `0 / ${totalImages}`, 'running')
|
||||
|
||||
const hardlinkMd5Set = new Set<string>()
|
||||
for (const img of images) {
|
||||
const imageMd5 = String(img.imageMd5 || '').trim().toLowerCase()
|
||||
if (imageMd5) {
|
||||
hardlinkMd5Set.add(imageMd5)
|
||||
continue
|
||||
}
|
||||
const imageDatName = String(img.imageDatName || '').trim().toLowerCase()
|
||||
if (/^[a-f0-9]{32}$/i.test(imageDatName)) {
|
||||
hardlinkMd5Set.add(imageDatName)
|
||||
}
|
||||
}
|
||||
if (hardlinkMd5Set.size > 0) {
|
||||
await waitIfPaused()
|
||||
if (controlState.cancelRequested) {
|
||||
const remaining = Math.max(0, totalImages - completed)
|
||||
finishDecrypt(successCount, failCount, {
|
||||
status: 'canceled',
|
||||
detail: `图片批量解密已中断:已处理 ${completed}/${totalImages}(成功 ${successCount},未找到 ${notFoundCount},解密失败 ${decryptFailedCount},未处理 ${remaining})`,
|
||||
progressText: `成功 ${successCount} / 未找到 ${notFoundCount} / 解密失败 ${decryptFailedCount}`
|
||||
})
|
||||
return
|
||||
}
|
||||
updateDecryptTaskStatus(
|
||||
`正在预热图片索引(${hardlinkMd5Set.size} 个标识)...`,
|
||||
`0 / ${totalImages}`
|
||||
)
|
||||
try {
|
||||
await window.electronAPI.image.preloadHardlinkMd5s(Array.from(hardlinkMd5Set))
|
||||
} catch {
|
||||
// ignore preload failures and continue decrypt
|
||||
}
|
||||
}
|
||||
updateDecryptTaskStatus(`开始批量解密图片(0/${totalImages})`, `0 / ${totalImages}`)
|
||||
|
||||
const concurrency = batchDecryptConcurrency
|
||||
|
||||
const decryptOne = async (img: typeof images[0]) => {
|
||||
if (controlState.cancelRequested) return
|
||||
try {
|
||||
const r = await window.electronAPI.image.decrypt({
|
||||
sessionId: session.username,
|
||||
@@ -5861,32 +6064,59 @@ function ChatPage(props: ChatPageProps) {
|
||||
createTime: img.createTime,
|
||||
force: true,
|
||||
preferFilePath: true,
|
||||
hardlinkOnly: true
|
||||
hardlinkOnly: true,
|
||||
disableUpdateCheck: true,
|
||||
suppressEvents: true
|
||||
})
|
||||
if (r?.success) successCount++
|
||||
else failCount++
|
||||
else {
|
||||
failCount++
|
||||
if (r?.failureKind === 'decrypt_failed') decryptFailedCount++
|
||||
else notFoundCount++
|
||||
}
|
||||
} catch {
|
||||
failCount++
|
||||
notFoundCount++
|
||||
}
|
||||
completed++
|
||||
updateDecryptProgress(completed, images.length)
|
||||
updateDecryptProgress(completed, totalImages)
|
||||
}
|
||||
|
||||
// 并发池:同时跑 concurrency 个任务
|
||||
const pool = new Set<Promise<void>>()
|
||||
for (const img of images) {
|
||||
const p = decryptOne(img).then(() => { pool.delete(p) })
|
||||
pool.add(p)
|
||||
if (controlState.cancelRequested) break
|
||||
await waitIfPaused()
|
||||
if (controlState.cancelRequested) break
|
||||
if (pool.size >= concurrency) {
|
||||
await Promise.race(pool)
|
||||
if (controlState.cancelRequested) break
|
||||
await waitIfPaused()
|
||||
if (controlState.cancelRequested) break
|
||||
}
|
||||
let p: Promise<void> = Promise.resolve()
|
||||
p = decryptOne(img).then(() => { pool.delete(p) })
|
||||
pool.add(p)
|
||||
}
|
||||
if (pool.size > 0) {
|
||||
await Promise.all(pool)
|
||||
while (pool.size > 0) {
|
||||
await Promise.race(pool)
|
||||
}
|
||||
|
||||
finishDecrypt(successCount, failCount)
|
||||
}, [batchImageMessages, batchImageSelectedDates, batchDecryptConcurrency, currentSessionId, finishDecrypt, sessions, startDecrypt, updateDecryptProgress])
|
||||
if (controlState.cancelRequested) {
|
||||
const remaining = Math.max(0, totalImages - completed)
|
||||
finishDecrypt(successCount, failCount, {
|
||||
status: 'canceled',
|
||||
detail: `图片批量解密已中断:已处理 ${completed}/${totalImages}(成功 ${successCount},未找到 ${notFoundCount},解密失败 ${decryptFailedCount},未处理 ${remaining})`,
|
||||
progressText: `成功 ${successCount} / 未找到 ${notFoundCount} / 解密失败 ${decryptFailedCount}`
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
finishDecrypt(successCount, failCount, {
|
||||
status: decryptFailedCount > 0 ? 'failed' : 'completed',
|
||||
detail: `图片批量解密完成:成功 ${successCount},未找到 ${notFoundCount},解密失败 ${decryptFailedCount}`,
|
||||
progressText: `成功 ${successCount} / 未找到 ${notFoundCount} / 解密失败 ${decryptFailedCount}`
|
||||
})
|
||||
}, [batchImageMessages, batchImageSelectedDates, batchDecryptConcurrency, currentSessionId, finishDecrypt, sessions, startDecrypt, updateDecryptTaskStatus, updateDecryptProgress])
|
||||
|
||||
const batchImageCountByDate = useMemo(() => {
|
||||
const map = new Map<string, number>()
|
||||
@@ -6621,16 +6851,10 @@ function ChatPage(props: ChatPageProps) {
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-transcribe-btn${isBatchTranscribing ? ' transcribing' : ''}`}
|
||||
onClick={() => {
|
||||
if (isBatchTranscribing) {
|
||||
setShowBatchProgress(true)
|
||||
} else {
|
||||
handleBatchTranscribe()
|
||||
}
|
||||
}}
|
||||
onClick={handleBatchTranscribe}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchTranscribing
|
||||
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中 (${batchTranscribeProgress.current}/${batchTranscribeProgress.total}),点击查看进度`
|
||||
? `${runningBatchVoiceTaskType === 'decrypt' ? '批量语音解密' : '批量转写'}中,可在导出页任务中心查看进度`
|
||||
: '批量语音处理(解密/转文字)'}
|
||||
>
|
||||
{isBatchTranscribing ? (
|
||||
@@ -6643,16 +6867,10 @@ function ChatPage(props: ChatPageProps) {
|
||||
{!standaloneSessionWindow && (
|
||||
<button
|
||||
className={`icon-btn batch-decrypt-btn${isBatchDecrypting ? ' transcribing' : ''}`}
|
||||
onClick={() => {
|
||||
if (isBatchDecrypting) {
|
||||
setShowBatchDecryptToast(true)
|
||||
} else {
|
||||
handleBatchDecrypt()
|
||||
}
|
||||
}}
|
||||
onClick={handleBatchDecrypt}
|
||||
disabled={!currentSessionId}
|
||||
title={isBatchDecrypting
|
||||
? `批量解密中 (${batchDecryptProgress.current}/${batchDecryptProgress.total}),点击查看进度`
|
||||
? '批量解密中,可在导出页任务中心查看进度'
|
||||
: '批量解密图片'}
|
||||
>
|
||||
{isBatchDecrypting ? (
|
||||
@@ -7330,8 +7548,8 @@ function ChatPage(props: ChatPageProps) {
|
||||
<AlertCircle size={16} />
|
||||
<span>
|
||||
{batchVoiceTaskType === 'decrypt'
|
||||
? '批量解密会预先缓存语音数据,之后播放和转写会更快。解密过程中可以继续使用其他功能。'
|
||||
: '批量转写可能需要较长时间,转写过程中可以继续使用其他功能。已转写过的语音会自动跳过。'}
|
||||
? '批量解密会预先缓存语音数据,之后播放和转写会更快。解密过程中可以继续使用其他功能,进度会写入导出页任务中心。'
|
||||
: '批量转写可能需要较长时间,转写过程中可以继续使用其他功能。已转写过的语音会自动跳过,进度会写入导出页任务中心。'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -7398,17 +7616,17 @@ function ChatPage(props: ChatPageProps) {
|
||||
className={`batch-concurrency-trigger ${showConcurrencyDropdown ? 'open' : ''}`}
|
||||
onClick={() => setShowConcurrencyDropdown(!showConcurrencyDropdown)}
|
||||
>
|
||||
<span>{batchDecryptConcurrency === 1 ? '1(最慢,最稳)' : batchDecryptConcurrency === 6 ? '6(推荐)' : batchDecryptConcurrency === 20 ? '20(最快,可能卡顿)' : String(batchDecryptConcurrency)}</span>
|
||||
<span>{batchDecryptConcurrency === 1 ? '1' : batchDecryptConcurrency === 6 ? '6' : batchDecryptConcurrency === 20 ? '20' : String(batchDecryptConcurrency)}</span>
|
||||
<ChevronDown size={14} />
|
||||
</button>
|
||||
{showConcurrencyDropdown && (
|
||||
<div className="batch-concurrency-dropdown">
|
||||
{[
|
||||
{ value: 1, label: '1(最慢,最稳)' },
|
||||
{ value: 1, label: '1' },
|
||||
{ value: 3, label: '3' },
|
||||
{ value: 6, label: '6(推荐)' },
|
||||
{ value: 6, label: '6' },
|
||||
{ value: 10, label: '10' },
|
||||
{ value: 20, label: '20(最快,可能卡顿)' },
|
||||
{ value: 20, label: '20' },
|
||||
].map(opt => (
|
||||
<button
|
||||
key={opt.value}
|
||||
@@ -7426,7 +7644,7 @@ function ChatPage(props: ChatPageProps) {
|
||||
</div>
|
||||
<div className="batch-warning">
|
||||
<AlertCircle size={16} />
|
||||
<span>批量解密可能需要较长时间,进行中会在右下角显示非阻塞进度浮层。</span>
|
||||
<span>批量解密可能需要较长时间,进度会自动写入导出页任务中心(含准备阶段状态)。</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="batch-modal-footer">
|
||||
@@ -7789,7 +8007,13 @@ const emojiDataUrlCache = new Map<string, string>()
|
||||
const imageDataUrlCache = new Map<string, string>()
|
||||
const voiceDataUrlCache = new Map<string, string>()
|
||||
const voiceTranscriptCache = new Map<string, string>()
|
||||
type SharedImageDecryptResult = { success: boolean; localPath?: string; liveVideoPath?: string; error?: string }
|
||||
type SharedImageDecryptResult = {
|
||||
success: boolean
|
||||
localPath?: string
|
||||
liveVideoPath?: string
|
||||
error?: string
|
||||
failureKind?: 'not_found' | 'decrypt_failed'
|
||||
}
|
||||
const imageDecryptInFlight = new Map<string, Promise<SharedImageDecryptResult>>()
|
||||
const senderAvatarCache = new Map<string, { avatarUrl?: string; displayName?: string }>()
|
||||
const senderAvatarLoading = new Map<string, Promise<{ avatarUrl?: string; displayName?: string } | null>>()
|
||||
|
||||
Reference in New Issue
Block a user