1/** 2 * Context Menu System (Discord-inspired) 3 * Right-click context menu for messages 4 */ 5 6window.ChatroomContextMenu = (function() { 7 'use strict'; 8 9 let currentMenu = null; 10 let currentMessage = null; 11 12 function menuIcon(name) { 13 if (window.ChatroomIcons && window.ChatroomIcons.icon) { 14 return window.ChatroomIcons.icon(name); 15 } 16 return ''; 17 } 18 19 function menuItem(action, iconName, label, extraClass) { 20 return ( 21 '<button type="button" class="context-menu-item' + (extraClass ? ' ' + extraClass : '') + '" data-action="' + action + '">' + 22 '<span class="cr-menu-icon">' + menuIcon(iconName) + '</span>' + 23 '<span class="cr-menu-label">' + label + '</span>' + 24 '</button>' 25 ); 26 } 27 28 /** 29 * Initialize context menu 30 */ 31 function init() { 32 if (window.ChatroomIcons && window.ChatroomIcons.ensure) { 33 window.ChatroomIcons.ensure(); 34 } 35 36 // Listen for right-clicks - prevent default context menu everywhere 37 document.addEventListener('contextmenu', (e) => { 38 e.preventDefault(); 39 40 const message = e.target.closest('.message'); 41 if (message) { 42 showContextMenu(e, message); 43 } else { 44 hideContextMenu(); 45 } 46 }); 47 48 document.addEventListener('click', (e) => { 49 if (currentMenu && !currentMenu.contains(e.target)) { 50 hideContextMenu(); 51 } 52 }); 53 54 document.addEventListener('scroll', hideContextMenu, true); 55 } 56 57 /** 58 * Show context menu 59 */ 60 function getMessageUsername(message) { 61 const el = message.querySelector('.message-username'); 62 if (!el) return ''; 63 return el.textContent.replace(/ð/g, '').trim(); 64 } 65 66 function showContextMenu(event, message) { 67 hideContextMenu(); 68 69 currentMessage = message; 70 const messageId = message.dataset.messageId; 71 const username = getMessageUsername(message); 72 const currentUser = window.__sharedUsername ? window.__sharedUsername.get() : ''; 73 const isModerator = window.ChatroomModerator && window.ChatroomModerator.checkIsModerator(); 74 const isOwnMessage = username && username === currentUser; 75 76 const menu = document.createElement('div'); 77 menu.className = 'context-menu'; 78 menu.style.position = 'fixed'; 79 menu.style.left = event.clientX + 'px'; 80 menu.style.top = event.clientY + 'px'; 81 menu.style.zIndex = '3000'; 82 83 let menuItems = ''; 84 85 menuItems += menuItem('copy', 'copy', 'Copy Message'); 86 if (isModerator) { 87 menuItems += menuItem('copy-id', 'key', 'Copy Message ID'); 88 } 89 menuItems += menuItem('add-reaction', 'smile', 'Add Reaction'); 90 menuItems += menuItem('reply', 'reply', 'Reply'); 91 if (isModerator) { 92 menuItems += menuItem('pin', 'thumbtack', 'Pin Message'); 93 } 94 if (isOwnMessage) { 95 menuItems += menuItem('edit', 'pen', 'Edit Message'); 96 } 97 if (isOwnMessage || isModerator) { 98 menuItems += menuItem('delete', 'trash', 'Delete Message', 'context-menu-danger'); 99 } 100 101 menuItems += '<div class="context-menu-divider"></div>'; 102 menuItems += menuItem('profile', 'user', 'View Profile'); 103 menuItems += menuItem('dm', 'envelope', 'Send Message'); 104 105 menu.innerHTML = menuItems; 106 document.body.appendChild(menu); 107 currentMenu = menu; 108 109 const rect = menu.getBoundingClientRect(); 110 let left = event.clientX; 111 let top = event.clientY; 112 if (rect.right > window.innerWidth) { 113 left = event.clientX - rect.width; 114 } 115 if (rect.bottom > window.innerHeight) { 116 top = event.clientY - rect.height; 117 } 118 menu.style.left = Math.max(8, left) + 'px'; 119 menu.style.top = Math.max(8, top) + 'px'; 120 121 menu.querySelectorAll('.context-menu-item').forEach(item => { 122 item.addEventListener('click', () => { 123 handleContextAction(item.dataset.action, message); 124 hideContextMenu(); 125 }); 126 }); 127 } 128 129 /** 130 * Handle context menu action 131 */ 132 function handleContextAction(action, message) { 133 const messageId = message.dataset.messageId; 134 const username = getMessageUsername(message); 135 const messageText = message.querySelector('.message-text')?.textContent || ''; 136 137 switch (action) { 138 case 'copy': 139 navigator.clipboard.writeText(messageText).then(() => { 140 showToast('Message copied to clipboard!'); 141 }); 142 break; 143 144 case 'copy-id': 145 navigator.clipboard.writeText(messageId).then(() => { 146 showToast('Message ID copied!'); 147 }); 148 break; 149 150 case 'add-reaction': 151 if (window.ChatroomReactions) { 152 window.ChatroomReactions.showReactionPicker(messageId); 153 } 154 break; 155 156 case 'reply': 157 if (window.ChatroomReplies) { 158 window.ChatroomReplies.setPending(message); 159 } 160 break; 161 162 case 'delete': 163 if (window.ChatroomModerator) { 164 window.ChatroomModerator.deleteMessage(messageId); 165 } 166 break; 167 168 case 'edit': 169 if (window.ChatroomMessageMeta && window.ChatroomMessageMeta.startEdit) { 170 window.ChatroomMessageMeta.startEdit(message); 171 } else { 172 editMessage(message); 173 } 174 break; 175 176 case 'profile': 177 if (window.ChatroomUserProfile) { 178 window.ChatroomUserProfile.showProfile(username); 179 } 180 break; 181 182 case 'dm': 183 if (window.ChatroomDMs && username) { 184 window.ChatroomDMs.openDM(username); 185 } 186 break; 187 188 case 'pin': 189 if (window.ChatroomMessageMeta && window.ChatroomMessageMeta.pinMessage) { 190 window.ChatroomMessageMeta.pinMessage(message); 191 } 192 break; 193 } 194 } 195 196 /** 197 * Edit message 198 */ 199 function editMessage(messageElement) { 200 const messageText = messageElement.querySelector('.message-text'); 201 const originalText = messageText.textContent; 202 203 const input = document.createElement('textarea'); 204 input.value = originalText; 205 input.className = 'message-edit-input'; 206 input.style.width = '100%'; 207 input.style.minHeight = '60px'; 208 input.style.padding = '8px'; 209 input.style.border = '2px solid var(--accent-2)'; 210 input.style.borderRadius = '8px'; 211 input.style.background = 'var(--bg-2)'; 212 input.style.color = 'var(--text-2)'; 213 input.style.fontFamily = 'inherit'; 214 input.style.fontSize = 'inherit'; 215 216 messageText.replaceWith(input); 217 input.focus(); 218 input.select(); 219 220 const saveEdit = () => { 221 const newText = input.value.trim(); 222 if (newText && newText !== originalText) { 223 showToast('Message editing coming soon!'); 224 } 225 input.replaceWith(messageText); 226 }; 227 228 input.addEventListener('blur', saveEdit); 229 input.addEventListener('keydown', (e) => { 230 if (e.key === 'Enter' && !e.shiftKey) { 231 e.preventDefault(); 232 saveEdit(); 233 } 234 if (e.key === 'Escape') { 235 input.replaceWith(messageText); 236 } 237 }); 238 } 239 240 function hideContextMenu() { 241 if (currentMenu) { 242 currentMenu.remove(); 243 currentMenu = null; 244 currentMessage = null; 245 } 246 } 247 248 function showToast(message) { 249 const toast = document.createElement('div'); 250 toast.className = 'toast'; 251 toast.textContent = message; 252 document.body.appendChild(toast); 253 254 setTimeout(() => { 255 toast.classList.add('show'); 256 }, 10); 257 258 setTimeout(() => { 259 toast.classList.remove('show'); 260 setTimeout(() => toast.remove(), 300); 261 }, 2000); 262 } 263 264 if (document.readyState === 'loading') { 265 document.addEventListener('DOMContentLoaded', init); 266 } else { 267 init(); 268 } 269 270 function showContextMenuAt(coords, message) { 271 showContextMenu({ 272 clientX: coords.clientX, 273 clientY: coords.clientY, 274 preventDefault: function () {} 275 }, message); 276 } 277 278 return { 279 showContextMenu, 280 showContextMenuAt, 281 hideContextMenu, 282 init 283 }; 284})();
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.