Refactor inline images PR for project conventions

This commit is contained in:
Abhinav Raut
2026-05-13 01:22:45 +05:30
parent b96a9c599a
commit acda125ad6
11 changed files with 337 additions and 382 deletions
@@ -42,16 +42,16 @@
:href="currentImage.url"
download
class="text-white/70 hover:text-white"
:title="t('imageLightbox.download')"
:aria-label="t('imageLightbox.download')"
:title="t('globals.terms.download')"
:aria-label="t('globals.terms.download')"
@click.stop
>
<Download :size="20" />
</a>
<button
class="text-white hover:text-gray-300"
:title="t('imageLightbox.close')"
:aria-label="t('imageLightbox.close')"
:title="t('globals.messages.close')"
:aria-label="t('globals.messages.close')"
@click="close"
>
<X :size="24" />
@@ -141,7 +141,7 @@ import {
DialogDescription
} from '@shared-ui/components/ui/dialog'
import Placeholder from '@tiptap/extension-placeholder'
import Image from '@tiptap/extension-image'
import ResizableImage from './extensions/ResizableImage'
import StarterKit from '@tiptap/starter-kit'
import Link from '@tiptap/extension-link'
import Mention from '@tiptap/extension-mention'
@@ -374,165 +374,6 @@ const CustomMention = Mention.extend({
}
})
// Custom Image extension with drag-handle resizing and Gmail-style size presets
// (Small / Best fit / Original / Remove). Renders a node-view that wraps the
// <img> with a corner resize handle and a hover toolbar.
const ResizableImage = Image.extend({
addAttributes () {
return {
...this.parent?.(),
width: {
default: null,
parseHTML: (el) => el.getAttribute('width') || el.style.width?.replace('px', '') || null,
renderHTML: (attrs) => {
if (!attrs.width) return {}
return { width: attrs.width, style: `width: ${attrs.width}px` }
}
},
height: {
default: null,
parseHTML: (el) => el.getAttribute('height') || null,
renderHTML: (attrs) => (attrs.height ? { height: attrs.height } : {})
}
}
},
addNodeView () {
return ({ node, getPos, editor: nodeEditor }) => {
const wrapper = document.createElement('div')
wrapper.classList.add('image-resizer')
wrapper.style.display = 'inline-block'
wrapper.style.position = 'relative'
wrapper.style.lineHeight = '0'
const img = document.createElement('img')
img.src = node.attrs.src
img.alt = node.attrs.alt || ''
img.title = node.attrs.title || ''
img.classList.add('inline-image')
img.style.maxWidth = '100%'
img.style.height = 'auto'
if (node.attrs.width) img.style.width = node.attrs.width + 'px'
wrapper.appendChild(img)
// Toolbar (visible when wrapper is selected)
const toolbar = document.createElement('div')
toolbar.classList.add('image-size-toolbar')
let naturalWidth = 0
img.addEventListener('load', () => { naturalWidth = img.naturalWidth })
const commitWidth = (newWidth) => {
const pos = getPos()
if (typeof pos !== 'number') return
nodeEditor.chain().focus().command(({ tr }) => {
tr.setNodeMarkup(pos, undefined, { ...node.attrs, width: newWidth || null })
return true
}).run()
}
const sizes = [
{ label: 'Small', value: 400 },
{ label: 'Best fit', value: 'fit' },
{ label: 'Original', value: 'original' }
]
// Toolbar buttons use pointerdown so touch + pen + mouse all work.
// preventDefault avoids stealing focus from the editor.
sizes.forEach(({ label, value }) => {
const btn = document.createElement('button')
btn.textContent = label
btn.type = 'button'
btn.addEventListener('pointerdown', (e) => {
e.preventDefault()
e.stopPropagation()
if (value === 'original') {
img.style.width = naturalWidth ? naturalWidth + 'px' : 'auto'
commitWidth(naturalWidth || null)
} else if (value === 'fit') {
img.style.width = ''
commitWidth(null)
} else {
img.style.width = value + 'px'
commitWidth(value)
}
})
toolbar.appendChild(btn)
})
const sep = document.createElement('span')
sep.classList.add('image-toolbar-sep')
toolbar.appendChild(sep)
const removeBtn = document.createElement('button')
removeBtn.textContent = 'Remove'
removeBtn.type = 'button'
removeBtn.classList.add('image-toolbar-remove')
removeBtn.addEventListener('pointerdown', (e) => {
e.preventDefault()
e.stopPropagation()
const pos = getPos()
if (typeof pos === 'number') {
nodeEditor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
}
})
toolbar.appendChild(removeBtn)
wrapper.appendChild(toolbar)
// Bottom-right resize handle. We don't manage selected state ourselves;
// CSS keys off ProseMirror's `.ProseMirror-selectednode` class which
// ProseMirror toggles automatically when the image node is selected.
// That avoids a global document click listener per image (which leaks
// closures across the entire page for every embedded image).
const handle = document.createElement('div')
handle.classList.add('image-resize-handle')
wrapper.appendChild(handle)
// Drag the corner handle to resize. Pointer events for touch + pen.
let startX = 0
let startWidth = 0
const onPointerMove = (e) => {
const newWidth = Math.max(50, startWidth + (e.clientX - startX))
img.style.width = newWidth + 'px'
}
const onPointerUp = () => {
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
wrapper.classList.remove('resizing')
try {
commitWidth(Math.round(img.offsetWidth))
} catch (err) {
// Node may have been removed/replaced mid-drag (autosave
// re-render, paste over selection, etc.). Drop the commit.
}
}
const onPointerDown = (e) => {
e.preventDefault()
e.stopPropagation()
startX = e.clientX
startWidth = img.offsetWidth
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
wrapper.classList.add('resizing')
}
handle.addEventListener('pointerdown', onPointerDown)
return {
dom: wrapper,
update: (updatedNode) => {
if (updatedNode.type.name !== 'image') return false
img.src = updatedNode.attrs.src
img.style.width = updatedNode.attrs.width ? updatedNode.attrs.width + 'px' : ''
return true
},
destroy: () => {
handle.removeEventListener('pointerdown', onPointerDown)
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
}
}
}
}
})
const isInternalUpdate = ref(false)
const buildExtensions = () => {
@@ -0,0 +1,160 @@
import Image from '@tiptap/extension-image'
// Custom Image extension with drag-handle resizing and Gmail-style size presets
// (Small / Best fit / Original / Remove). Styles for .image-resizer,
// .image-resize-handle, and .image-size-toolbar live in TextEditor.vue's
// global <style> block.
export const ResizableImage = Image.extend({
addAttributes () {
return {
...this.parent?.(),
width: {
default: null,
parseHTML: (el) => el.getAttribute('width') || el.style.width?.replace('px', '') || null,
renderHTML: (attrs) => {
if (!attrs.width) return {}
return { width: attrs.width, style: `width: ${attrs.width}px` }
}
},
height: {
default: null,
parseHTML: (el) => el.getAttribute('height') || null,
renderHTML: (attrs) => (attrs.height ? { height: attrs.height } : {})
}
}
},
addNodeView () {
return ({ node, getPos, editor: nodeEditor }) => {
const wrapper = document.createElement('div')
wrapper.classList.add('image-resizer')
wrapper.style.display = 'inline-block'
wrapper.style.position = 'relative'
wrapper.style.lineHeight = '0'
const img = document.createElement('img')
img.src = node.attrs.src
img.alt = node.attrs.alt || ''
img.title = node.attrs.title || ''
img.classList.add('inline-image')
img.style.maxWidth = '100%'
img.style.height = 'auto'
if (node.attrs.width) img.style.width = node.attrs.width + 'px'
wrapper.appendChild(img)
const toolbar = document.createElement('div')
toolbar.classList.add('image-size-toolbar')
let naturalWidth = 0
img.addEventListener('load', () => { naturalWidth = img.naturalWidth })
const commitWidth = (newWidth) => {
const pos = getPos()
if (typeof pos !== 'number') return
nodeEditor.chain().focus().command(({ tr }) => {
tr.setNodeMarkup(pos, undefined, { ...node.attrs, width: newWidth || null })
return true
}).run()
}
const sizes = [
{ label: 'Small', value: 400 },
{ label: 'Best fit', value: 'fit' },
{ label: 'Original', value: 'original' }
]
// Toolbar buttons use pointerdown so touch + pen + mouse all work.
// preventDefault avoids stealing focus from the editor.
sizes.forEach(({ label, value }) => {
const btn = document.createElement('button')
btn.textContent = label
btn.type = 'button'
btn.addEventListener('pointerdown', (e) => {
e.preventDefault()
e.stopPropagation()
if (value === 'original') {
img.style.width = naturalWidth ? naturalWidth + 'px' : 'auto'
commitWidth(naturalWidth || null)
} else if (value === 'fit') {
img.style.width = ''
commitWidth(null)
} else {
img.style.width = value + 'px'
commitWidth(value)
}
})
toolbar.appendChild(btn)
})
const sep = document.createElement('span')
sep.classList.add('image-toolbar-sep')
toolbar.appendChild(sep)
const removeBtn = document.createElement('button')
removeBtn.textContent = 'Remove'
removeBtn.type = 'button'
removeBtn.classList.add('image-toolbar-remove')
removeBtn.addEventListener('pointerdown', (e) => {
e.preventDefault()
e.stopPropagation()
const pos = getPos()
if (typeof pos === 'number') {
nodeEditor.chain().focus().deleteRange({ from: pos, to: pos + 1 }).run()
}
})
toolbar.appendChild(removeBtn)
wrapper.appendChild(toolbar)
// CSS keys off ProseMirror's `.ProseMirror-selectednode` class which
// ProseMirror toggles automatically when the image node is selected.
// Avoids a global document click listener per image (which would leak
// closures across the entire page for every embedded image).
const handle = document.createElement('div')
handle.classList.add('image-resize-handle')
wrapper.appendChild(handle)
let startX = 0
let startWidth = 0
const onPointerMove = (e) => {
const newWidth = Math.max(50, startWidth + (e.clientX - startX))
img.style.width = newWidth + 'px'
}
const onPointerUp = () => {
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
wrapper.classList.remove('resizing')
try {
commitWidth(Math.round(img.offsetWidth))
} catch (err) {
// Node may have been removed/replaced mid-drag (autosave
// re-render, paste over selection, etc.). Drop the commit.
}
}
const onPointerDown = (e) => {
e.preventDefault()
e.stopPropagation()
startX = e.clientX
startWidth = img.offsetWidth
window.addEventListener('pointermove', onPointerMove)
window.addEventListener('pointerup', onPointerUp)
wrapper.classList.add('resizing')
}
handle.addEventListener('pointerdown', onPointerDown)
return {
dom: wrapper,
update: (updatedNode) => {
if (updatedNode.type.name !== 'image') return false
img.src = updatedNode.attrs.src
img.style.width = updatedNode.attrs.width ? updatedNode.attrs.width + 'px' : ''
return true
},
destroy: () => {
handle.removeEventListener('pointerdown', onPointerDown)
window.removeEventListener('pointermove', onPointerMove)
window.removeEventListener('pointerup', onPointerUp)
}
}
}
}
})
export default ResizableImage
@@ -299,4 +299,12 @@ const showEnvelope = computed(() => {
props.message.meta?.subject
)
})
</script>
</script>
<style scoped lang="scss">
.native-html :deep(img) {
max-width: 100%;
height: auto;
cursor: zoom-in;
}
</style>
@@ -0,0 +1,156 @@
<template>
<div class="flex items-center group text-left">
<div
class="relative w-36 h-28 rounded border overflow-hidden cursor-pointer transition-colors"
:class="
isImage
? ''
: 'flex flex-col items-center justify-between bg-muted/40 hover:bg-muted p-3'
"
@click="onClick"
>
<template v-if="isImage">
<img
:src="getThumbFilepath(attachment.url)"
:alt="attachment.name"
class="w-full h-full object-cover"
/>
<div
class="absolute inset-0 p-1 pr-12 text-gray-50 opacity-0 group-hover:opacity-100 overlay text-wrap"
>
<p class="font-bold text-xs">{{ shortName(attachment.name) }}</p>
<p class="text-xs">{{ formatBytes(attachment.size) }}</p>
</div>
</template>
<template v-else>
<div class="flex-1 flex items-center justify-center">
<component :is="fileIcon" class="w-10 h-10" :class="iconColor" />
</div>
<div class="w-full text-center">
<p
class="text-xs font-medium text-foreground truncate"
:title="attachment.name"
>
{{ shortName(attachment.name) }}
</p>
<p class="text-xs text-muted-foreground">{{ formatBytes(attachment.size) }}</p>
</div>
</template>
<a
:href="attachment.url"
target="_blank"
rel="noopener noreferrer"
class="absolute top-1.5 right-1.5 p-0.5 rounded opacity-0 group-hover:opacity-100 transition-opacity"
:class="isImage ? 'hover:text-white/80' : 'hover:bg-background'"
:title="t('globals.terms.download')"
:aria-label="t('globals.terms.download')"
@click.stop
>
<Download
class="w-4 h-4"
:class="isImage ? '' : 'text-muted-foreground'"
/>
</a>
</div>
<Teleport to="body">
<div
v-if="showPdfPreview"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80"
@click.self="showPdfPreview = false"
>
<button
class="absolute top-4 right-4 text-white hover:text-gray-300 z-10"
:aria-label="t('globals.messages.close')"
@click="showPdfPreview = false"
>
<X :size="28" />
</button>
<a
:href="attachment.url"
download
class="absolute top-4 right-14 text-white hover:text-gray-300 z-10"
:title="t('globals.terms.download')"
:aria-label="t('globals.terms.download')"
>
<Download :size="24" />
</a>
<iframe
:src="attachment.url"
:title="attachment.name"
class="w-[90vw] h-[90vh] rounded shadow-2xl bg-white"
/>
</div>
</Teleport>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatBytes, getThumbFilepath } from '@shared-ui/utils/file'
import {
Download,
X,
FileText,
FileSpreadsheet,
File,
FileImage,
FileArchive,
FileCode
} from 'lucide-vue-next'
const props = defineProps({
attachment: { type: Object, required: true }
})
const emit = defineEmits(['preview'])
const { t } = useI18n()
const showPdfPreview = ref(false)
const shortName = (name) => (name || '').substring(0, 40)
const isImage = computed(() =>
(props.attachment.content_type || '').startsWith('image/')
)
const ext = computed(() => {
const parts = (props.attachment.name || '').split('.')
return parts.length > 1 ? parts.pop().toLowerCase() : ''
})
const canPreviewPdf = computed(() => !isImage.value && ext.value === 'pdf')
const fileIcon = computed(() => {
const e = ext.value
if (e === 'pdf') return FileText
if (['xls', 'xlsx', 'csv'].includes(e)) return FileSpreadsheet
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(e)) return FileImage
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(e)) return FileArchive
if (['html', 'xml', 'json', 'js', 'css'].includes(e)) return FileCode
if (['doc', 'docx', 'txt', 'rtf'].includes(e)) return FileText
return File
})
const iconColor = computed(() => {
const e = ext.value
if (e === 'pdf') return 'text-red-500'
if (['xls', 'xlsx', 'csv'].includes(e)) return 'text-green-600'
if (['doc', 'docx', 'txt', 'rtf'].includes(e)) return 'text-blue-500'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(e)) return 'text-amber-600'
return 'text-muted-foreground'
})
const onClick = () => {
if (isImage.value) {
emit('preview', props.attachment)
} else if (canPreviewPdf.value) {
showPdfPreview.value = true
} else {
window.open(props.attachment.url, '_blank')
}
}
</script>
@@ -1,142 +0,0 @@
<template>
<div class="flex items-center group text-left">
<div
class="relative w-36 h-28 flex flex-col items-center justify-between rounded-lg border bg-muted/40 p-3 hover:bg-muted transition-colors cursor-pointer"
@click="onClick"
>
<div class="flex-1 flex items-center justify-center">
<component :is="fileIcon" class="w-10 h-10" :class="iconColor" />
</div>
<div class="w-full text-center">
<p
class="text-xs font-medium text-foreground truncate"
:title="attachment.name"
>
{{ shortName(attachment.name) }}
</p>
<p class="text-xs text-muted-foreground">{{ formatBytes(attachment.size) }}</p>
</div>
<div
class="absolute top-1.5 right-1.5 opacity-0 group-hover:opacity-100 transition-opacity flex gap-1"
>
<button
v-if="canPreview"
class="p-0.5 rounded hover:bg-background"
:title="t('attachment.preview')"
:aria-label="t('attachment.preview')"
@click.stop="openPreview"
>
<Eye class="w-4 h-4 text-muted-foreground" />
</button>
<a
:href="attachment.url"
download
class="p-0.5 rounded hover:bg-background"
:title="t('imageLightbox.download')"
:aria-label="t('imageLightbox.download')"
@click.stop
>
<Download class="w-4 h-4 text-muted-foreground" />
</a>
</div>
</div>
<!-- PDF preview overlay (PDFs only non-image inline preview) -->
<Teleport to="body">
<div
v-if="showPdfPreview"
class="fixed inset-0 z-[9999] flex items-center justify-center bg-black/80"
@click.self="showPdfPreview = false"
>
<button
class="absolute top-4 right-4 text-white hover:text-gray-300 z-10"
:aria-label="t('imageLightbox.close')"
@click="showPdfPreview = false"
>
<X :size="28" />
</button>
<a
:href="attachment.url"
download
class="absolute top-4 right-14 text-white hover:text-gray-300 z-10"
:title="t('imageLightbox.download')"
:aria-label="t('imageLightbox.download')"
>
<Download :size="24" />
</a>
<iframe
:src="attachment.url"
:title="attachment.name"
class="w-[90vw] h-[90vh] rounded shadow-2xl bg-white"
/>
</div>
</Teleport>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { formatBytes } from '@shared-ui/utils/file'
import {
Download,
Eye,
X,
FileText,
FileSpreadsheet,
File,
FileImage,
FileArchive,
FileCode
} from 'lucide-vue-next'
const props = defineProps({
attachment: { type: Object, required: true }
})
const { t } = useI18n()
const showPdfPreview = ref(false)
const shortName = (name) => (name || '').substring(0, 30)
const ext = computed(() => {
const name = props.attachment.name || ''
const parts = name.split('.')
return parts.length > 1 ? parts.pop().toLowerCase() : ''
})
const canPreview = computed(() => ext.value === 'pdf')
const fileIcon = computed(() => {
const e = ext.value
if (e === 'pdf') return FileText
if (['xls', 'xlsx', 'csv'].includes(e)) return FileSpreadsheet
if (['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'].includes(e)) return FileImage
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(e)) return FileArchive
if (['html', 'xml', 'json', 'js', 'css'].includes(e)) return FileCode
if (['doc', 'docx', 'txt', 'rtf'].includes(e)) return FileText
return File
})
const iconColor = computed(() => {
const e = ext.value
if (e === 'pdf') return 'text-red-500'
if (['xls', 'xlsx', 'csv'].includes(e)) return 'text-green-600'
if (['doc', 'docx', 'txt', 'rtf'].includes(e)) return 'text-blue-500'
if (['zip', 'rar', '7z', 'tar', 'gz'].includes(e)) return 'text-amber-600'
return 'text-muted-foreground'
})
const openPreview = () => {
showPdfPreview.value = true
}
const onClick = () => {
if (canPreview.value) {
openPreview()
} else {
window.open(props.attachment.url, '_blank')
}
}
</script>
@@ -1,46 +0,0 @@
<template>
<div class="flex flex-wrap items-center group text-left">
<div class="relative cursor-pointer" @click="$emit('preview', attachment)">
<img
:src="getThumbFilepath(attachment.url)"
:alt="attachment.name"
class="w-36 h-28 flex items-center object-cover"
/>
<div class="p-1 absolute inset-0 text-gray-50 opacity-0 group-hover:opacity-100 overlay text-wrap">
<div class="flex flex-col justify-between h-full">
<div>
<p class="font-bold text-xs">{{ trimAttachmentName(attachment.name) }}</p>
<p class="text-xs">{{ formatBytes(attachment.size) }}</p>
</div>
<div class="flex items-center gap-2">
<Eye :size="20" />
<a
:href="attachment.url"
download
class="hover:text-white/80"
:aria-label="t('imageLightbox.download')"
@click.stop
>
<Download :size="20" />
</a>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { useI18n } from 'vue-i18n'
import { formatBytes, getThumbFilepath } from '@shared-ui/utils/file'
import { Download, Eye } from 'lucide-vue-next'
defineProps({
attachment: { type: Object, required: true }
})
defineEmits(['preview'])
const { t } = useI18n()
const trimAttachmentName = (name) => (name || '').substring(0, 40)
</script>
@@ -6,14 +6,9 @@
class="flex items-center cursor-pointer"
>
<div>
<ImageAttachmentPreview
v-if="isImage(attachment)"
:attachment="attachment"
@preview="openLightbox"
/>
<div
v-else-if="isAudio(attachment)"
class="flex items-center gap-2 rounded-lg border bg-muted/40 px-3 py-2"
v-if="isAudio(attachment)"
class="flex items-center gap-2 rounded border bg-muted/40 px-3 py-2"
>
<audio controls preload="auto" class="h-8 max-w-[260px]">
<source :src="attachment.url" />
@@ -22,14 +17,14 @@
:href="attachment.url"
download
class="p-1 rounded hover:bg-muted shrink-0"
:title="t('imageLightbox.download')"
:aria-label="t('imageLightbox.download')"
:title="t('globals.terms.download')"
:aria-label="t('globals.terms.download')"
@click.stop
>
<Download class="w-4 h-4 text-muted-foreground" />
</a>
</div>
<FileAttachmentPreview v-else :attachment="attachment" />
<AttachmentItem v-else :attachment="attachment" @preview="openLightbox" />
</div>
</div>
</div>
@@ -45,8 +40,7 @@
import { ref, computed } from 'vue'
import { useI18n } from 'vue-i18n'
import { Download } from 'lucide-vue-next'
import ImageAttachmentPreview from '@/features/conversation/message/attachment/ImageAttachmentPreview.vue'
import FileAttachmentPreview from '@/features/conversation/message/attachment/FileAttachmentPreview.vue'
import AttachmentItem from '@/features/conversation/message/attachment/AttachmentItem.vue'
import ImageLightbox from '@/components/ImageLightbox.vue'
const props = defineProps({
@@ -51,14 +51,6 @@
margin-bottom: 0.5rem;
}
// Stop emails with explicit width/height attributes from forcing a
// wider rendered width than the message bubble.
img {
max-width: 100%;
height: auto;
cursor: zoom-in;
}
ul {
list-style-type: disc;
margin-left: 1.5rem;
+1 -3
View File
@@ -394,7 +394,6 @@
"ai.apiKey.description": "{provider} API Key is not set or invalid. Please enter a valid API key to use AI features.",
"ai.apiKeyNotSet": "{provider} API Key is not set. Please ask your administrator to set it up",
"ai.enterOpenAIAPIKey": "Enter OpenAI API Key",
"attachment.preview": "Preview",
"auth.backToLogin": "Back to login",
"auth.checkEmailForReset": "Check your email for the password reset link.",
"auth.confirmPassword": "Confirm password",
@@ -690,6 +689,7 @@
"globals.terms.description": "Description | Descriptions",
"globals.terms.disabled": "Disabled",
"globals.terms.draft": "Draft",
"globals.terms.download": "Download",
"globals.terms.email": "Email | Emails",
"globals.terms.enabled": "Enabled",
"globals.terms.error": "Error | Errors",
@@ -838,8 +838,6 @@
"globals.terms.white": "White",
"globals.terms.workspace": "Workspace",
"globals.terms.you": "You",
"imageLightbox.close": "Close",
"imageLightbox.download": "Download",
"imageLightbox.next": "Next image",
"imageLightbox.previous": "Previous image",
"imageLightbox.resetZoom": "Reset zoom",
-6
View File
@@ -1214,12 +1214,6 @@ func (m *Manager) uploadMessageAttachments(message *models.Message) error {
}
}
// Now that the file is uploaded, swap any cid: reference for the upload URL.
// For non-images this turns the broken <img> tag into a download link.
if contentID != "" {
message.Content = replaceCIDInContent(message.Content, fmt.Sprintf("cid:%s", contentID), "/uploads/"+media.UUID, attachment.Name, attachment.ContentType)
}
message.Media = append(message.Media, media)
}
return nil