feat(inbox): global thread scroll + close (X) on another agent's draft mirror
- Email posts now render at FULL content height (fitFrame cap raised 1800→20000 guard)
so there is no per-message internal scroll — the whole thread scrolls as one.
- The live draft mirror ("X rédige une réponse…") gets a close X (closeOtherDraft):
dismiss another agent's in-progress draft preview; reset on conversation switch so
a genuinely new draft re-appears.
Verified: a 60-paragraph email renders at 2681px (no inner scrollbar); the mirror
shows a close button that hides it on click.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
f519bb3dd6
commit
54f0d271dd
|
|
@ -383,7 +383,7 @@
|
||||||
|
|
||||||
<!-- Brouillon miroir temps réel : ce qu'un AUTRE agent est en train de rédiger (dernier qui écrit gagne) -->
|
<!-- Brouillon miroir temps réel : ce qu'un AUTRE agent est en train de rédiger (dernier qui écrit gagne) -->
|
||||||
<div v-if="otherDraft" class="coord-draft-mirror">
|
<div v-if="otherDraft" class="coord-draft-mirror">
|
||||||
<div class="coord-draft-head"><q-icon name="edit_note" size="15px" /> <b>{{ shortAgent(otherDraft.agent) }}</b> rédige une réponse en direct…</div>
|
<div class="coord-draft-head row items-center no-wrap"><q-icon name="edit_note" size="15px" /> <b class="q-ml-xs">{{ shortAgent(otherDraft.agent) }}</b> <span class="q-ml-xs">rédige une réponse en direct…</span><q-space /><q-btn flat dense round size="xs" icon="close" color="indigo-4" @click="closeOtherDraft"><q-tooltip>Fermer cet aperçu</q-tooltip></q-btn></div>
|
||||||
<div class="coord-draft-body" v-html="otherDraft.html || (otherDraft.text || '').replace(/\n/g, '<br>')"></div>
|
<div class="coord-draft-body" v-html="otherDraft.html || (otherDraft.text || '').replace(/\n/g, '<br>')"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
@ -974,7 +974,9 @@ const readers = computed(() => {
|
||||||
return Object.keys(rb).filter(e => e && e !== meEmail.value).map(e => ({ email: e, ...rb[e] })).sort((a, b) => String(b.readAt || '').localeCompare(String(a.readAt || '')))
|
return Object.keys(rb).filter(e => e && e !== meEmail.value).map(e => ({ email: e, ...rb[e] })).sort((a, b) => String(b.readAt || '').localeCompare(String(a.readAt || '')))
|
||||||
})
|
})
|
||||||
// Brouillon d'un AUTRE agent sur cette conversation (miroir temps réel ; null si c'est le mien ou aucun).
|
// Brouillon d'un AUTRE agent sur cette conversation (miroir temps réel ; null si c'est le mien ou aucun).
|
||||||
const otherDraft = computed(() => sharedDraft.value[coordToken.value] || null) // handleConvDraft a déjà retiré mon propre onglet (filtre sid)
|
const closedDrafts = ref(new Set()) // miroirs fermés manuellement via le X (réaffichés au changement de fil)
|
||||||
|
const otherDraft = computed(() => (closedDrafts.value.has(coordToken.value) ? null : (sharedDraft.value[coordToken.value] || null))) // handleConvDraft a déjà retiré mon propre onglet (filtre sid)
|
||||||
|
function closeOtherDraft () { const s = new Set(closedDrafts.value); s.add(coordToken.value); closedDrafts.value = s }
|
||||||
const coordQueue = computed(() => activeConv.value?.queue || activeDiscussion.value?.queue || '')
|
const coordQueue = computed(() => activeConv.value?.queue || activeDiscussion.value?.queue || '')
|
||||||
// Signature SUGGÉRÉE selon la FILE (département) du fil : la réponse pré-écrite dont mailbox === file. One-tap, jamais forcée (le brouillon est partagé → ne pas écraser/diffuser une surprise).
|
// Signature SUGGÉRÉE selon la FILE (département) du fil : la réponse pré-écrite dont mailbox === file. One-tap, jamais forcée (le brouillon est partagé → ne pas écraser/diffuser une surprise).
|
||||||
const suggestedSignature = computed(() => { if (!isEmailConv.value) return null; const q = coordQueue.value; return q ? (cannedList.value.find(c => c.html && c.mailbox === q) || null) : null })
|
const suggestedSignature = computed(() => { if (!isEmailConv.value) return null; const q = coordQueue.value; return q ? (cannedList.value.find(c => c.html && c.mailbox === q) || null) : null })
|
||||||
|
|
@ -998,7 +1000,8 @@ function openReply () { replyOpen.value = true; if (coordToken.value) notifyTypi
|
||||||
// Ajuste un iframe courriel à SON contenu (fini le 300px fixe / les blancs). Sûr : sandbox `allow-same-origin` SANS `allow-scripts` → le parent lit le DOM pour mesurer, aucun script du courriel ne s'exécute.
|
// Ajuste un iframe courriel à SON contenu (fini le 300px fixe / les blancs). Sûr : sandbox `allow-same-origin` SANS `allow-scripts` → le parent lit le DOM pour mesurer, aucun script du courriel ne s'exécute.
|
||||||
function fitFrame (f) {
|
function fitFrame (f) {
|
||||||
if (!f) return
|
if (!f) return
|
||||||
const measure = () => { try { const h = f.contentDocument.body.scrollHeight; if (h > 4) f.style.height = Math.min(Math.max(h + 8, 48), 1800) + 'px' } catch (err) { f.style.height = '300px' } }
|
// Hauteur = TOUT le contenu (pas de plafond → aucun scroll interne au message ; c'est le fil entier qui défile globalement). Garde-fou 20000px contre une valeur aberrante.
|
||||||
|
const measure = () => { try { const h = f.contentDocument.body.scrollHeight; if (h > 4) f.style.height = Math.min(Math.max(h + 8, 48), 20000) + 'px' } catch (err) { f.style.height = '300px' } }
|
||||||
measure(); setTimeout(measure, 80) // @load (ou un déplié) peut précéder la mise en page → 2e mesure après stabilisation
|
measure(); setTimeout(measure, 80) // @load (ou un déplié) peut précéder la mise en page → 2e mesure après stabilisation
|
||||||
}
|
}
|
||||||
function fitEmailFrame (e) { fitFrame(e.target) }
|
function fitEmailFrame (e) { fitFrame(e.target) }
|
||||||
|
|
@ -1076,7 +1079,7 @@ function msgSnippet (m) { const t = String(m.text || '').replace(/^✉️[^\n]*\
|
||||||
function cleanBody (t) { return String(t || '').replace(/^✉️[^\n]*\n+/, '') } // retire l'ancien préfixe sujet collé au corps (double en-tête)
|
function cleanBody (t) { return String(t || '').replace(/^✉️[^\n]*\n+/, '') } // retire l'ancien préfixe sujet collé au corps (double en-tête)
|
||||||
const lastMessageId = computed(() => { const ms = activeDiscussion.value?.messages || []; return ms.length ? ms[ms.length - 1].id : null })
|
const lastMessageId = computed(() => { const ms = activeDiscussion.value?.messages || []; return ms.length ? ms[ms.length - 1].id : null })
|
||||||
function expandLast () { const ms = activeDiscussion.value?.messages || []; expandedMsgs.value = new Set(ms.length ? [ms[ms.length - 1].id] : []); refitFrames() }
|
function expandLast () { const ms = activeDiscussion.value?.messages || []; expandedMsgs.value = new Set(ms.length ? [ms[ms.length - 1].id] : []); refitFrames() }
|
||||||
watch(coordToken, () => { svcStatus.value = null; expandLast(); replyOpen.value = false; summaryText.value = ''; summaryErr.value = ''; fetchAgents().then(a => { agents.value = a }) })
|
watch(coordToken, () => { svcStatus.value = null; expandLast(); replyOpen.value = false; summaryText.value = ''; summaryErr.value = ''; closedDrafts.value = new Set(); fetchAgents().then(a => { agents.value = a }) })
|
||||||
watch(() => (activeDiscussion.value?.messages || []).length, (n, o) => { if (n > (o || 0)) { const ms = activeDiscussion.value?.messages || []; if (ms.length) { const s = new Set(expandedMsgs.value); s.add(ms[ms.length - 1].id); expandedMsgs.value = s } } })
|
watch(() => (activeDiscussion.value?.messages || []).length, (n, o) => { if (n > (o || 0)) { const ms = activeDiscussion.value?.messages || []; if (ms.length) { const s = new Set(expandedMsgs.value); s.add(ms[ms.length - 1].id); expandedMsgs.value = s } } })
|
||||||
const gmailReplyUrl = computed(() => activeConv.value?.gmailReplyUrl || activeDiscussion.value?.gmailReplyUrl || null)
|
const gmailReplyUrl = computed(() => activeConv.value?.gmailReplyUrl || activeDiscussion.value?.gmailReplyUrl || null)
|
||||||
const coordNotes = computed(() => activeConv.value?.notes || [])
|
const coordNotes = computed(() => activeConv.value?.notes || [])
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user