1/** 2 * Image Attachments System 3 * Handles image pasting, file selection, and attachment to messages 4 */ 5 6window.ChatroomImageAttachments = (function() { 7 'use strict'; 8 9 let pendingImages = []; 10 const MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 2MB (reduced to save Firebase storage) 11 const MAX_IMAGES_PER_MESSAGE = 10; 12 13 /** 14 * Initialize image attachment system 15 */ 16 function init() { 17 setupFileInput(); 18 setupPasteHandler(); 19 setupAttachButton(); 20 } 21 22 /** 23 * Setup file input for attach button 24 */ 25 function setupFileInput() { 26 const attachBtn = document.querySelector('.input-attach-btn'); 27 if (!attachBtn) return; 28 29 // Create hidden file input 30 const fileInput = document.createElement('input'); 31 fileInput.type = 'file'; 32 fileInput.accept = 'image/*'; 33 fileInput.multiple = true; 34 fileInput.style.display = 'none'; 35 fileInput.id = 'image-file-input'; 36 document.body.appendChild(fileInput); 37 38 // Handle file selection 39 fileInput.addEventListener('change', (e) => { 40 const files = Array.from(e.target.files); 41 handleImageFiles(files); 42 // Reset input so same file can be selected again 43 fileInput.value = ''; 44 }); 45 46 // Click attach button to trigger file input 47 attachBtn.addEventListener('click', () => { 48 fileInput.click(); 49 }); 50 } 51 52 /** 53 * Setup paste handler for images 54 */ 55 function setupPasteHandler() { 56 const messageInput = document.getElementById('message-input'); 57 if (!messageInput) return; 58 59 messageInput.addEventListener('paste', (e) => { 60 const items = e.clipboardData?.items; 61 if (!items) return; 62 63 const imageFiles = []; 64 for (let i = 0; i < items.length; i++) { 65 const item = items[i]; 66 if (item.type.indexOf('image') !== -1) { 67 const file = item.getAsFile(); 68 if (file) { 69 imageFiles.push(file); 70 } 71 } 72 } 73 74 if (imageFiles.length > 0) { 75 e.preventDefault(); 76 handleImageFiles(imageFiles); 77 } 78 }); 79 } 80 81 /** 82 * Setup attach button click handler 83 */ 84 function setupAttachButton() { 85 const attachBtn = document.querySelector('.input-attach-btn'); 86 if (!attachBtn) return; 87 88 // Update button title 89 attachBtn.title = 'Attach Image'; 90 } 91 92 /** 93 * Handle image files (from file input or paste) 94 */ 95 function handleImageFiles(files) { 96 if (pendingImages.length + files.length > MAX_IMAGES_PER_MESSAGE) { 97 showToast(`Maximum ${MAX_IMAGES_PER_MESSAGE} images per message`, 'error'); 98 return; 99 } 100 101 files.forEach(file => { 102 // Check file size 103 if (file.size > MAX_IMAGE_SIZE) { 104 showToast(`Image "${file.name}" is too large (max 2MB). Please compress or resize the image.`, 'error'); 105 return; 106 } 107 108 // Check if it's an image 109 if (!file.type.startsWith('image/')) { 110 showToast(`"${file.name}" is not an image`, 'error'); 111 return; 112 } 113 114 // Convert to base64 with compression 115 const reader = new FileReader(); 116 reader.onload = (e) => { 117 const img = new Image(); 118 img.onload = () => { 119 // Compress image to reduce Firebase storage 120 const canvas = document.createElement('canvas'); 121 const ctx = canvas.getContext('2d'); 122 123 // Calculate dimensions (max 1920px width/height) 124 let width = img.width; 125 let height = img.height; 126 const maxDimension = 1920; 127 128 if (width > maxDimension || height > maxDimension) { 129 if (width > height) { 130 height = (height / width) * maxDimension; 131 width = maxDimension; 132 } else { 133 width = (width / height) * maxDimension; 134 height = maxDimension; 135 } 136 } 137 138 canvas.width = width; 139 canvas.height = height; 140 ctx.drawImage(img, 0, 0, width, height); 141 142 // Convert to base64 with quality compression (0.7 = 70% quality) 143 const base64 = canvas.toDataURL('image/jpeg', 0.7); 144 145 const imageData = { 146 data: base64, 147 name: file.name, 148 type: 'image/jpeg', // Always JPEG after compression 149 size: file.size 150 }; 151 pendingImages.push(imageData); 152 showImagePreview(imageData); 153 updateAttachButton(); 154 }; 155 img.onerror = () => { 156 // Fallback: use original file if compression fails 157 const base64 = e.target.result; 158 const imageData = { 159 data: base64, 160 name: file.name, 161 type: file.type, 162 size: file.size 163 }; 164 pendingImages.push(imageData); 165 showImagePreview(imageData); 166 updateAttachButton(); 167 }; 168 img.src = e.target.result; 169 }; 170 reader.onerror = () => { 171 showToast(`Error reading image "${file.name}"`, 'error'); 172 };
173 reader.readAsDataURL(file); 174 }); 175 } 176 177 /** 178 * Show image preview in input area 179 */ 180 function showImagePreview(imageData) { 181 const messageInput = document.getElementById('message-input'); 182 if (!messageInput) return; 183 184 // Create preview container if it doesn't exist 185 let previewContainer = document.getElementById('image-preview-container'); 186 if (!previewContainer) { 187 previewContainer = document.createElement('div'); 188 previewContainer.id = 'image-preview-container'; 189 previewContainer.className = 'image-preview-container'; 190 191 const inputWrapper = messageInput.closest('.input-wrapper'); 192 if (inputWrapper) { 193 inputWrapper.insertBefore(previewContainer, messageInput); 194 } 195 } 196 197 // Create preview item 198 const previewItem = document.createElement('div'); 199 previewItem.className = 'image-preview-item'; 200 previewItem.dataset.imageIndex = pendingImages.length - 1; 201 202 const img = document.createElement('img'); 203 img.src = imageData.data; 204 img.className = 'image-preview-img'; 205 img.alt = imageData.name; 206 207 const removeBtn = document.createElement('button'); 208 removeBtn.className = 'image-preview-remove'; 209 removeBtn.innerHTML = 'Ã'; 210 removeBtn.title = 'Remove image'; 211 removeBtn.addEventListener('click', () => { 212 const index = parseInt(previewItem.dataset.imageIndex); 213 pendingImages.splice(index, 1); 214 previewItem.remove(); 215 updateImageIndices(); 216 updateAttachButton(); 217 218 // Remove container if no images left 219 if (pendingImages.length === 0) { 220 previewContainer.remove(); 221 } 222 }); 223 224 previewItem.appendChild(img); 225 previewItem.appendChild(removeBtn); 226 previewContainer.appendChild(previewItem); 227 } 228 229 /** 230 * Update image indices after removal 231 */ 232 function updateImageIndices() { 233 const previewItems = document.querySelectorAll('.image-preview-item'); 234 previewItems.forEach((item, index) => { 235 item.dataset.imageIndex = index; 236 }); 237 } 238 239 /** 240 * Update attach button to show count 241 */ 242 function updateAttachButton() { 243 const attachBtn = document.querySelector('.input-attach-btn'); 244 if (!attachBtn) return; 245 246 if (pendingImages.length > 0) { 247 attachBtn.innerHTML = `ð <span style="font-size: 11px;">(${pendingImages.length})</span>`; 248 } else { 249 attachBtn.innerHTML = '+'; 250 } 251 } 252 253 /** 254 * Add image from URL (e.g. GIF/Sticker from KLIPY) 255 */ 256 function addImageFromUrl(url) { 257 if (!url || typeof url !== 'string') return; 258 if (pendingImages.length >= MAX_IMAGES_PER_MESSAGE) { 259 showToast(`Maximum ${MAX_IMAGES_PER_MESSAGE} images per message`, 'error'); 260 return; 261 } 262 const imageData = { data: url, name: 'gif', type: 'url' }; 263 pendingImages.push(imageData); 264 showImagePreview(imageData); 265 updateAttachButton(); 266 } 267 268 /** 269 * Get pending images and clear them 270 */ 271 function getPendingImages() { 272 const images = [...pendingImages]; 273 clearPendingImages(); 274 return images; 275 } 276 277 /** 278 * Clear pending images 279 */ 280 function clearPendingImages() { 281 pendingImages = []; 282 const previewContainer = document.getElementById('image-preview-container'); 283 if (previewContainer) { 284 previewContainer.remove(); 285 } 286 updateAttachButton(); 287 } 288 289 /** 290 * Show toast notification 291 */ 292 function showToast(message, type = 'info') { 293 // Use existing toast system if available 294 if (window.ChatroomNotifications && window.ChatroomNotifications.showToast) { 295 window.ChatroomNotifications.showToast(message, type); 296 } else { 297 // Fallback to alert 298 alert(message); 299 } 300 } 301 302 // Initialize on load 303 if (document.readyState === 'loading') { 304 document.addEventListener('DOMContentLoaded', init); 305 } else { 306 init(); 307 } 308 309 return { 310 init, 311 getPendingImages, 312 clearPendingImages, 313 addImageFromUrl, 314 hasPendingImages: () => pendingImages.length > 0 315 }; 316})(); 317
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.