Merge pull request #450 from abhinavxd/fix/s3-thumbnail-preview

Fix live chat image thumbnails for S3 store.
This commit is contained in:
Abhinav Raut
2026-07-31 02:12:31 +05:30
committed by GitHub
8 changed files with 64 additions and 30 deletions
-3
View File
@@ -688,9 +688,6 @@ func sendChatMessageResponse(app *App, r *fastglue.Request, messageUUID string)
return r.SendErrorEnvelope(fasthttp.StatusInternalServerError, app.i18n.T("globals.messages.somethingWentWrong"), nil, envelope.GeneralError)
}
for i := range message.Attachments {
message.Attachments[i].URL = app.media.GetSignedURL(message.Attachments[i].UUID)
}
app.conversation.SignAvatarURL(&message.Author.AvatarURL)
// Strip agent email from widget responses.
+2 -9
View File
@@ -66,11 +66,7 @@ func handleGetMessages(r *fastglue.Request) error {
rootURL, _ := app.setting.GetAppRootURL()
for i := range messages {
total = messages[i].Total
// Populate attachment URLs
for j := range messages[i].Attachments {
att := messages[i].Attachments[j]
messages[i].Attachments[j].URL = app.media.GetURL(att.UUID, att.ContentType, att.Name)
}
app.conversation.SignAttachmentURLs(messages[i].Attachments)
resolveQuotedCIDs(app, &messages[i])
resolveAttachmentCIDs(&messages[i], rootURL)
}
@@ -135,10 +131,7 @@ func handleGetMessage(r *fastglue.Request) error {
}
rootURL, _ := app.setting.GetAppRootURL()
for j := range message.Attachments {
att := message.Attachments[j]
message.Attachments[j].URL = app.media.GetURL(att.UUID, att.ContentType, att.Name)
}
app.conversation.SignAttachmentURLs(message.Attachments)
resolveQuotedCIDs(app, &message)
resolveAttachmentCIDs(&message, rootURL)
@@ -13,9 +13,10 @@
>
<template v-if="isImage">
<img
:src="getThumbFilepath(attachment.url)"
:src="attachment.thumbnail_url || getThumbFilepath(attachment.url)"
:alt="attachment.name"
class="w-full h-full object-cover"
@error="fallbackToOriginal($event, attachment.url)"
/>
<div
class="absolute inset-x-0 top-0 flex items-start justify-between gap-2 px-2 pt-1.5 pb-5 bg-gradient-to-b from-black/75 via-black/40 to-transparent opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
@@ -84,6 +85,12 @@ const showAudio = ref(false)
const shortName = (name) => (name || '').substring(0, 40)
const fallbackToOriginal = (event, originalUrl) => {
if (event.target.dataset.originalFallback) return
event.target.dataset.originalFallback = 'true'
event.target.src = originalUrl
}
const isImage = computed(() => (props.attachment.content_type || '').startsWith('image/'))
const isAudio = computed(() => (props.attachment.content_type || '').startsWith('audio/'))
@@ -11,6 +11,7 @@
:src="getThumbnailUrl(attachment)"
:alt="attachment.name"
class="max-w-48 max-h-32 rounded-lg object-cover"
@error="fallbackToOriginal($event, attachment.url)"
@click="openImage(attachment.url)"
/>
</div>
@@ -49,7 +50,13 @@ const isImage = (attachment) => {
const getThumbnailUrl = (attachment) => {
if (!isImage(attachment)) return attachment.url
return getThumbFilepath(attachment.url)
return attachment.thumbnail_url || getThumbFilepath(attachment.url)
}
const fallbackToOriginal = (event, originalUrl) => {
if (event.target.dataset.originalFallback) return
event.target.dataset.originalFallback = 'true'
event.target.src = originalUrl
}
const openImage = (url) => {
+10 -9
View File
@@ -13,15 +13,16 @@ const (
// Attachment represents a file or blob attachment that can be sent or received on a message.
type Attachment struct {
Name string `json:"name"`
Size int `json:"size"`
Content []byte `json:"content"`
ContentID string `json:"content_id"`
ContentType string `json:"content_type"`
Disposition string `json:"disposition"`
UUID string `json:"uuid"`
URL string `json:"url"`
Header textproto.MIMEHeader `json:"-"`
Name string `json:"name"`
Size int `json:"size"`
Content []byte `json:"content"`
ContentID string `json:"content_id"`
ContentType string `json:"content_type"`
Disposition string `json:"disposition"`
UUID string `json:"uuid"`
URL string `json:"url"`
ThumbnailURL string `json:"thumbnail_url"`
Header textproto.MIMEHeader `json:"-"`
}
type Attachments []Attachment
+2 -3
View File
@@ -168,6 +168,7 @@ type mediaStore interface {
GetBlob(name string) ([]byte, error)
GetURL(uuid, contentType, fileName string) string
GetSignedURL(name string) string
GetThumbnailURL(uuid string) string
Attach(id int, model string, modelID int) error
SetContentID(id int, contentID string) error
GetByModel(id int, model string) ([]mmodels.Media, error)
@@ -1829,9 +1830,7 @@ func (m *Manager) BuildWidgetConversationResponse(conversation models.Conversati
for _, msg := range messages {
m.SignAvatarURL(&msg.Author.AvatarURL)
attachments := msg.Attachments
for j := range attachments {
attachments[j].URL = m.mediaStore.GetSignedURL(attachments[j].UUID)
}
m.SignAttachmentURLs(attachments)
// Strip agent email from widget responses.
author := msg.Author
+16 -4
View File
@@ -392,13 +392,21 @@ func (m *Manager) GetMessage(uuid string) (models.Message, error) {
}
// Generate signed URLs for attachments.
for i := range message.Attachments {
message.Attachments[i].URL = m.mediaStore.GetSignedURL(message.Attachments[i].UUID)
}
m.SignAttachmentURLs(message.Attachments)
return message, nil
}
// SignAttachmentURLs adds access URLs for the original image and its thumbnail.
func (m *Manager) SignAttachmentURLs(attachments attachment.Attachments) {
for i := range attachments {
attachments[i].URL = m.mediaStore.GetURL(attachments[i].UUID, attachments[i].ContentType, attachments[i].Name)
if strings.HasPrefix(attachments[i].ContentType, "image/") {
attachments[i].ThumbnailURL = m.mediaStore.GetThumbnailURL(attachments[i].UUID)
}
}
}
// UpdateMessageStatus updates the status of a message.
func (m *Manager) UpdateMessageStatus(messageUUID string, status string) error {
if _, err := m.q.UpdateMessageStatus.Exec(status, messageUUID); err != nil {
@@ -1314,7 +1322,10 @@ func (m *Manager) fetchMessageAttachments(messageID int) (attachment.Attachments
Content: blob,
Size: media.Size,
Header: attachment.MakeHeader(media.ContentType, contentID, media.Filename, "base64", media.Disposition.String),
URL: m.mediaStore.GetSignedURL(media.UUID),
URL: m.mediaStore.GetURL(media.UUID, media.ContentType, media.Filename),
}
if strings.HasPrefix(media.ContentType, "image/") {
attachment.ThumbnailURL = m.mediaStore.GetThumbnailURL(media.UUID)
}
attachments = append(attachments, attachment)
}
@@ -1449,6 +1460,7 @@ func (m *Manager) broadcastMessageToWidgetClients(message *models.Message) {
return
}
m.SignAttachmentURLs(message.Attachments)
m.SignAvatarURL(&message.Author.AvatarURL)
liveChatInbox.BroadcastMessageToClients(message.ConversationUUID, conversation.ContactID, models.ChatMessage{
UUID: message.UUID,
+18
View File
@@ -9,6 +9,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"os"
"strings"
"time"
@@ -241,6 +242,23 @@ func (m *Manager) GetSignedURL(name string) string {
return m.GetURL(name, "", "")
}
// GetThumbnailURL returns the URL for an image thumbnail.
func (m *Manager) GetThumbnailURL(uuid string) string {
if m.store.Name() == "fs" {
// FS validates thumbnail requests with the original UUID signature.
u, err := url.Parse(m.GetSignedURL(uuid))
if err == nil {
if idx := strings.LastIndex(u.Path, "/"); idx >= 0 {
u.Path = u.Path[:idx+1] + image.ThumbPrefix + uuid
} else {
u.Path = image.ThumbPrefix + uuid
}
return u.String()
}
}
return m.store.GetURL(image.ThumbPrefix+uuid, "inline", "")
}
// SignedURLValidator returns the store's signature validator if available.
// Returns nil if the store doesn't support signed URL validation.
func (m *Manager) SignedURLValidator() func(name, sig string, exp int64) bool {