all repos — stealth-developers @ d39ae47f58e5af56319af1523ba4ec707721f646

web: render attachments in messages
vi did:web:vt3e.cat
Wed, 01 Jul 2026 01:11:30 +0100
commit

d39ae47f58e5af56319af1523ba4ec707721f646

parent

f5e7e9d5d97ce1737c11c28956c43d5af187851f

1 files changed, 115 insertions(+), 93 deletions(-)

jump to
M apps/web/src/views/TicketDetailView.vueapps/web/src/views/TicketDetailView.vue

@@ -195,6 +195,20 @@ fileSize: att.fileSize || 0,

url: getAttachmentUrl(att), })); }); + +const attachmentsByMessageId = computed(() => { + const map: Record<string, typeof formattedAttachments.value> = {}; + for (const att of formattedAttachments.value) { + const msgId = att.messageId || att.ticketMessageId; + if (msgId) { + if (!map[msgId]) { + map[msgId] = []; + } + map[msgId].push(att); + } + } + return map; +}); </script> <template>

@@ -260,7 +274,6 @@ <p class="reason-value">{{ ticket.privateReason }}</p>

</div> </div> - <!-- Attachments Block --> <div class="ticket-attachments"> <div class="attachments-header"> <span class="label">Attachments</span>

@@ -289,29 +302,11 @@ </a>

</template> </div> <div v-else class="no-attachments">No attachments uploaded yet.</div> - - <!-- <div class="ticket-attachments-upload"> - <label - class="upload-btn button" - :class="{ highlighted: highlightUploadButton }" - @click="highlightUploadButton = false" - > - <input type="file" multiple @change="handleFiles" class="hidden-input" /> - <span>Add Attachments</span> - </label> - <div class="upload-status" :class="{ visible: uploading || uploadError }"> - <span v-if="uploading" class="uploading">Uploading… {{ uploadProgress }}%</span> - <span v-if="uploadError" class="error">{{ uploadError }}</span> - </div> - <div class="progress-bar-container" :class="{ visible: uploading }"> - <div class="progress-bar" :style="{ width: uploadProgress + '%' }"></div> - </div> - </div> --> </div> </div> <div class="ticket-comments" v-if="comments.length > 0"> - <h2>Staff Comments</h2> + <h2>Comments</h2> <div class="comments-list"> <div v-for="comment in comments" :key="comment.id" class="comment"> <UserChip :user="comment.actors?.author" />

@@ -359,6 +354,33 @@ </span>

<span v-else class="text-content">{{ token.value }}</span> </template> </div> + + <div + class="message-attachments" + v-if="attachmentsByMessageId[msg.id] && attachmentsByMessageId[msg.id].length > 0" + > + <div class="attachments-grid"> + <template v-for="att in attachmentsByMessageId[msg.id]" :key="att.id"> + <div v-if="att.fileType.startsWith('video/')" class="attachment-card video"> + <video :src="att.url" controls class="attachment-media"></video> + </div> + <a v-else :href="att.url" target="_blank" class="attachment-card link"> + <img + v-if="att.fileType.startsWith('image/')" + :src="att.url" + :alt="att.fileName" + class="attachment-media" + /> + <div class="attachment-file" v-else> + <div class="file-info"> + <span class="filename" :title="att.fileName">{{ att.fileName }}</span> + <span class="size">{{ formatSize(att.fileSize) }}</span> + </div> + </div> + </a> + </template> + </div> + </div> </div> </div> </div>

@@ -511,78 +533,6 @@ font-style: italic;

font-size: 0.9rem; } - .attachments-grid { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - - .attachment-card { - display: flex; - flex-direction: column; - border-radius: 0.75rem; - overflow: hidden; - background-color: hsla(var(--surface0) / 0.3); - border: 1px solid hsla(var(--surface0) / 0.5); - max-width: 100%; - - &.link { - text-decoration: none; - transition: border-color var(--transition); - - &:hover { - border-color: hsla(var(--accent) / 0.5); - } - } - - .attachment-media { - display: block; - max-width: 100%; - max-height: 160px; - object-fit: cover; - } - - img.attachment-media { - min-width: 160px; - } - - video.attachment-media { - max-width: 300px; - } - - .attachment-file { - display: flex; - align-items: center; - gap: 0.75rem; - padding: 0.75rem 1rem; - color: hsl(var(--text)); - - .file-icon { - font-size: 1.5rem; - } - - .file-info { - display: flex; - flex-direction: column; - min-width: 0; - - .filename { - font-weight: 600; - font-size: 0.9rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - max-width: 180px; - } - - .size { - color: hsl(var(--subtext0)); - font-size: 0.75rem; - } - } - } - } - } - .ticket-attachments-upload { display: flex; align-items: center;

@@ -665,6 +615,74 @@ .progress-bar {

height: 100%; background-color: hsl(var(--accent)); transition: width 0.1s ease; + } + } + } +} + +.attachments-grid { + display: flex; + flex-wrap: wrap; + gap: 0.75rem; + + .attachment-card { + display: flex; + flex-direction: column; + border-radius: 0.75rem; + overflow: hidden; + background-color: hsla(var(--surface0) / 0.3); + border: 1px solid hsla(var(--surface0) / 0.5); + max-width: 100%; + + &.link { + text-decoration: none; + transition: border-color var(--transition); + + &:hover { + border-color: hsla(var(--accent) / 0.5); + } + } + + .attachment-media { + display: block; + max-width: 100%; + max-height: 160px; + object-fit: cover; + } + + img.attachment-media { + min-width: 160px; + } + + video.attachment-media { + max-width: 300px; + } + + .attachment-file { + display: flex; + align-items: center; + gap: 0.75rem; + padding: 0.75rem 1rem; + color: hsl(var(--text)); + + .file-info { + display: flex; + flex-direction: column; + min-width: 0; + + .filename { + font-weight: 600; + font-size: 0.9rem; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 180px; + } + + .size { + color: hsl(var(--subtext0)); + font-size: 0.75rem; + } } } }

@@ -834,9 +852,13 @@ .mention {

background-color: hsla(var(--accent) / 0.15); color: hsl(var(--accent)); font-weight: 700; - padding: 0.1rem 0.3rem; + padding: 0.1; border-radius: 0.25rem; } + } + + .message-attachments { + margin-top: 0.5rem; } } }