PageSourceSearch

https://pupwife.com/projects/chatroom/scripts/imageAttachments.js

js pupwife.com collected 2026-09-30 15:07:13 UTC 10,763 bytes, 317 lines download raw bytes

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.