// ==UserScript== // @name Torn Mail Templates // @namespace https://github.com/GlasnostDev/torn-mail-templates // @version 2.0.1 // @description Mail templates, quick send, mobile support, sent log. // @author Glasnost [1844049] // @match https://www.torn.com/messages.php* // @match https://www.torn.com/page.php?sid=UserList* // @updateURL https://raw.githubusercontent.com/GlasnostDev/torn-mail-templates/main/torn-mail-templates.user.js // @downloadURL https://raw.githubusercontent.com/GlasnostDev/torn-mail-templates/main/torn-mail-templates.user.js // @icon https://www.google.com/s2/favicons?sz=64&domain=torn.com // @grant none // ==/UserScript== (function() { 'use strict'; const STORAGE_KEY = 'mailTemplates_v1'; const SEND_TIME_KEY = 'mailTemplates_lastSendTime'; const SENT_LOG_KEY = 'mailTemplates_sentLog'; const OPTIONS_KEY = 'mailTemplates_options'; const COOLDOWN_MS = 10000; const RETENTION_OPTIONS = [1, 3, 7, 14, 30]; function isMobile() { return document.body.classList.contains('tt-mobile') || window.innerWidth < 600; } // ── Options ────────────────────────────────────────────────────────────── function getOptions() { try { return JSON.parse(localStorage.getItem(OPTIONS_KEY)) || {}; } catch(e) { return {}; } } function saveOptions(o) { localStorage.setItem(OPTIONS_KEY, JSON.stringify(o)); } function getRetentionDays() { return getOptions().retentionDays || 7; } function getNewTab() { return getOptions().newTab === true; } // ── Sent log ───────────────────────────────────────────────────────────── function getSentLog() { try { return JSON.parse(localStorage.getItem(SENT_LOG_KEY)) || {}; } catch(e) { return {}; } } function saveSentLog(l) { localStorage.setItem(SENT_LOG_KEY, JSON.stringify(l)); } function recordSent(xid) { if (!xid) return; const l = getSentLog(); l[xid] = Date.now(); saveSentLog(l); } function pruneSentLog() { const l = getSentLog(), cutoff = Date.now() - getRetentionDays() * 86400000; let changed = false; Object.keys(l).forEach(x => { if (l[x] < cutoff) { delete l[x]; changed = true; } }); if (changed) saveSentLog(l); } function recentlySent(xid) { pruneSentLog(); return !!getSentLog()[xid]; } function clearSentLog() { localStorage.removeItem(SENT_LOG_KEY); } // ── Templates ──────────────────────────────────────────────────────────── function getTemplates() { try { return JSON.parse(localStorage.getItem(STORAGE_KEY)) || {}; } catch(e) { return {}; } } function saveTemplates(t) { localStorage.setItem(STORAGE_KEY, JSON.stringify(t)); } function recordSendTime() { localStorage.setItem(SEND_TIME_KEY, Date.now().toString()); } function getLastSendTime() { const v = localStorage.getItem(SEND_TIME_KEY); return v ? parseInt(v, 10) : null; } // ── Compose helpers ────────────────────────────────────────────────────── function getSubject() { const el = document.querySelector('input.subject'); return el ? el.value : ''; } function getBody() { const el = document.querySelector('[contenteditable="true"]'); return el ? el.innerHTML : ''; } function setSubject(val) { const el = document.querySelector('input.subject'); if (el) { el.value = val; el.dispatchEvent(new Event('input', { bubbles: true })); } } function setBody(val) { const el = document.querySelector('[contenteditable="true"]'); if (el) { el.innerHTML = val; el.dispatchEvent(new Event('input', { bubbles: true })); } } // ── Shortcodes ─────────────────────────────────────────────────────────── const SHORTCODES = [ { code: '{{@name}}', description: "Recipient's display name", example: 'Glasnost' }, { code: '{{@xid}}', description: "Recipient's player ID number", example: '1844049' }, ]; function getRecipientInfo() { const n = document.querySelector('input.user-id'), v = n ? n.value : ''; return { name: v.replace(/\s*\[\d+\]$/, '').trim(), xid: (v.match(/\[(\d+)\]/) || [])[1] || '' }; } function resolveVars(text) { const { name, xid } = getRecipientInfo(); return text.replace(/\{\{@name\}\}/gi, name).replace(/\{\{@xid\}\}/gi, xid); } // ── Modal helpers ──────────────────────────────────────────────────────── function makeOverlay(id) { const o = document.createElement('div'); o.id = id; o.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.6);z-index:99999;display:flex;align-items:center;justify-content:center;'; o.addEventListener('click', e => { if (e.target === o) o.remove(); }); return o; } function makeModal() { const m = document.createElement('div'); m.style.cssText = 'background:#222;border:1px solid #444;border-radius:6px;padding:20px;min-width:300px;max-width:90vw;font-family:Arial,sans-serif;'; return m; } function makeTitle(text) { const h = document.createElement('h3'); h.style.cssText = 'color:#eee;margin:0 0 12px;font-size:14px;'; h.textContent = text; return h; } function makeButton(text, bg, onClick) { const b = document.createElement('button'); b.style.cssText = 'border:none;border-radius:3px;padding:6px 14px;cursor:pointer;font-size:13px;color:#fff;background:' + bg + ';transition:background 0.2s;'; b.textContent = text; b.addEventListener('click', onClick); return b; } // ── Export with selection ───────────────────────────────────────────────── function doExport() { const templates = getTemplates(), keys = Object.keys(templates); if (keys.length === 0) { alert('No templates to export.'); return; } const overlay = makeOverlay('mt-exp-ov'), modal = makeModal(); modal.appendChild(makeTitle('📤 Export Templates')); const sub = document.createElement('p'); sub.style.cssText = 'color:#666;font-size:11px;margin:0 0 10px;'; sub.textContent = 'Select templates to export:'; modal.appendChild(sub); const allRow = document.createElement('div'); allRow.style.cssText = 'margin-bottom:8px;display:flex;align-items:center;'; const allCb = document.createElement('input'); allCb.type = 'checkbox'; allCb.checked = true; allCb.style.marginRight = '8px'; const allLbl = document.createElement('label'); allLbl.style.cssText = 'color:#aaa;font-size:12px;cursor:pointer;font-weight:bold;'; allLbl.textContent = 'Select all'; allRow.appendChild(allCb); allRow.appendChild(allLbl); modal.appendChild(allRow); const hr = document.createElement('hr'); hr.style.cssText = 'border:none;border-top:1px solid #333;margin:0 0 10px;'; modal.appendChild(hr); const cbContainer = document.createElement('div'); cbContainer.style.cssText = 'max-height:240px;overflow-y:auto;margin-bottom:14px;'; const cbMap = {}; keys.forEach(name => { const row = document.createElement('div'); row.style.cssText = 'margin-bottom:6px;display:flex;align-items:center;'; const cb = document.createElement('input'); cb.type = 'checkbox'; cb.checked = true; cb.style.marginRight = '8px'; cb.addEventListener('change', () => { allCb.checked = Object.values(cbMap).every(c => c.checked); }); const lbl = document.createElement('label'); lbl.style.cssText = 'color:#ccc;font-size:12px;cursor:pointer;'; lbl.textContent = name; cbMap[name] = cb; row.appendChild(cb); row.appendChild(lbl); cbContainer.appendChild(row); }); allCb.addEventListener('change', () => Object.values(cbMap).forEach(c => c.checked = allCb.checked)); modal.appendChild(cbContainer); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; footer.appendChild(makeButton('Cancel', '#555', () => overlay.remove())); footer.appendChild(makeButton('📤 Export', '#6a1b9a', () => { const selected = {}; Object.entries(cbMap).forEach(([name, cb]) => { if (cb.checked) selected[name] = templates[name]; }); if (Object.keys(selected).length === 0) { alert('No templates selected.'); return; } const json = JSON.stringify(selected, null, 2); const url = URL.createObjectURL(new Blob([json], { type: 'application/json' })); const a = document.createElement('a'); a.href = url; a.download = 'mail-templates.json'; a.click(); URL.revokeObjectURL(url); overlay.remove(); })); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); } // ── Import with conflict resolution ────────────────────────────────────── function doImport() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.json,application/json'; input.addEventListener('change', () => { const file = input.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = e => { try { const imported = JSON.parse(e.target.result); if (typeof imported !== 'object' || Array.isArray(imported)) { alert('Invalid format.'); return; } processImport(imported); } catch(err) { alert('Parse error: ' + err.message); } }; reader.readAsText(file); }); input.click(); } function processImport(imported) { const existing = getTemplates(); const keys = Object.keys(imported); const conflicts = keys.filter(k => existing[k]); const fresh = keys.filter(k => !existing[k]); if (conflicts.length === 0) { saveTemplates(Object.assign({}, existing, imported)); buildUI(); alert('Imported ' + keys.length + ' template(s).'); return; } const overlay = makeOverlay('mt-imp-ov'), modal = makeModal(); modal.appendChild(makeTitle('📥 Import — Conflicts Found')); const sub = document.createElement('p'); sub.style.cssText = 'color:#666;font-size:11px;margin:0 0 12px;'; sub.textContent = fresh.length + ' new template(s). ' + conflicts.length + ' conflict(s) with existing names:'; modal.appendChild(sub); const container = document.createElement('div'); container.style.cssText = 'max-height:220px;overflow-y:auto;margin-bottom:14px;'; const choiceMap = {}; conflicts.forEach(name => { const row = document.createElement('div'); row.style.cssText = 'margin-bottom:8px;padding:8px;background:#1a1a1a;border-radius:3px;'; const nameRow = document.createElement('div'); nameRow.style.cssText = 'color:#e53935;font-size:12px;font-weight:bold;margin-bottom:6px;'; nameRow.textContent = '⚠️ "' + name + '" already exists'; const sel = document.createElement('select'); sel.style.cssText = 'background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;padding:4px 6px;font-size:12px;width:100%;'; var opts = [['overwrite', 'Overwrite existing'], ['skip', 'Skip (keep existing)']]; opts.forEach(function(pair) { const opt = document.createElement('option'); opt.value = pair[0]; opt.textContent = pair[1]; sel.appendChild(opt); }); choiceMap[name] = sel; row.appendChild(nameRow); row.appendChild(sel); container.appendChild(row); }); modal.appendChild(container); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; footer.appendChild(makeButton('Cancel', '#555', () => overlay.remove())); footer.appendChild(makeButton('📥 Import', '#e65100', () => { const merged = Object.assign({}, existing); fresh.forEach(k => { merged[k] = imported[k]; }); conflicts.forEach(name => { if (choiceMap[name].value === 'overwrite') merged[name] = imported[name]; }); saveTemplates(merged); buildUI(); overlay.remove(); const ow = conflicts.filter(n => choiceMap[n].value === 'overwrite').length; const sk = conflicts.filter(n => choiceMap[n].value === 'skip').length; alert('Import complete. ' + fresh.length + ' new, ' + ow + ' overwritten, ' + sk + ' skipped.'); })); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); } // ── Send button ────────────────────────────────────────────────────────── let sendBtnObserver = null, attachedSendBtn = null; function getSendButton() { let found = null; document.querySelectorAll('button.torn-btn').forEach(b => { if (b.textContent.trim() === 'Send') found = b; }); return found; } function attachSendListener() { const btn = getSendButton(); if (btn && btn !== attachedSendBtn) { attachedSendBtn = btn; btn.addEventListener('click', () => { recordSendTime(); const info = getRecipientInfo(); if (info.xid) recordSent(info.xid); }, true); } } function watchSendButton() { if (sendBtnObserver) sendBtnObserver.disconnect(); attachSendListener(); sendBtnObserver = new MutationObserver(() => attachSendListener()); sendBtnObserver.observe(document.body, { childList: true, subtree: true }); } // ── Timers ─────────────────────────────────────────────────────────────── let quickSendInterval = null; function updateQuickSend() { const b = document.getElementById('mt-qs-btn'); if (!b) return; const last = getLastSendTime(), r = last ? COOLDOWN_MS - (Date.now() - last) : 0; if (r > 0) { b.disabled = true; b.textContent = '⏳ Wait ' + (r / 1000).toFixed(1) + 's'; b.style.background = '#555'; b.style.cursor = 'not-allowed'; } else { b.disabled = false; b.textContent = '⚡ Quick Send'; b.style.background = '#1565c0'; b.style.cursor = 'pointer'; } } function startTimers() { if (quickSendInterval) clearInterval(quickSendInterval); quickSendInterval = setInterval(updateQuickSend, 100); } // ── Save modal with conflict detection ─────────────────────────────────── function showSaveModal(subject, body) { const overlay = makeOverlay('mt-save-ov'), modal = makeModal(); modal.appendChild(makeTitle('Save Template')); const inp = document.createElement('input'); inp.type = 'text'; inp.placeholder = 'Template name...'; inp.style.cssText = 'width:100%;box-sizing:border-box;background:#333;color:#eee;border:1px solid #555;border-radius:3px;padding:8px;font-size:13px;margin-bottom:6px;'; modal.appendChild(inp); const warn = document.createElement('p'); warn.style.cssText = 'color:#e53935;font-size:11px;margin:0 0 10px;min-height:16px;'; modal.appendChild(warn); inp.addEventListener('input', () => { warn.textContent = getTemplates()[inp.value.trim()] ? '⚠️ A template with this name already exists.' : ''; }); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; footer.appendChild(makeButton('Cancel', '#555', () => overlay.remove())); const saveBtn = makeButton('Save', '#2e7d32', () => { const name = inp.value.trim(); if (!name) { alert('Enter a name.'); return; } const templates = getTemplates(); if (templates[name]) { showSaveConflict(overlay, name, subject, body); } else { templates[name] = { subject: subject, body: body }; saveTemplates(templates); overlay.remove(); buildUI(); } }); inp.addEventListener('keydown', e => { if (e.key === 'Enter') saveBtn.click(); }); footer.appendChild(saveBtn); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); setTimeout(() => inp.focus(), 50); } function showSaveConflict(existingOverlay, name, subject, body) { existingOverlay.remove(); const overlay = makeOverlay('mt-conf-ov'), modal = makeModal(); modal.appendChild(makeTitle('⚠️ Template Already Exists')); const msg = document.createElement('p'); msg.style.cssText = 'color:#ccc;font-size:12px;margin:0 0 16px;'; msg.textContent = '"' + name + '" already exists. What would you like to do?'; modal.appendChild(msg); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;flex-wrap:wrap;gap:8px;justify-content:flex-end;'; footer.appendChild(makeButton('Overwrite', '#b71c1c', () => { const t = getTemplates(); t[name] = { subject: subject, body: body }; saveTemplates(t); overlay.remove(); buildUI(); })); footer.appendChild(makeButton('Change Name', '#37474f', () => { overlay.remove(); showSaveModal(subject, body); })); footer.appendChild(makeButton('Cancel', '#555', () => overlay.remove())); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); } // ── Help modal ─────────────────────────────────────────────────────────── function showHelpModal() { if (document.getElementById('mt-help-ov')) return; const overlay = makeOverlay('mt-help-ov'), modal = makeModal(); modal.appendChild(makeTitle('📋 Template Shortcodes')); const sub = document.createElement('p'); sub.style.cssText = 'color:#666;font-size:11px;margin:0 0 12px;'; sub.textContent = 'Replaced with live values when a template is loaded.'; modal.appendChild(sub); const table = document.createElement('table'); table.style.cssText = 'width:100%;border-collapse:collapse;'; table.innerHTML = '' + 'Shortcode' + 'Description' + 'Example' + ''; const tbody = document.createElement('tbody'); SHORTCODES.forEach(function(item, i) { const border = i < SHORTCODES.length - 1 ? '1px solid #2a2a2a' : 'none'; const tr = document.createElement('tr'); tr.innerHTML = '' + item.code + '' + '' + item.description + '' + '' + item.example + ''; tbody.appendChild(tr); }); table.appendChild(tbody); modal.appendChild(table); const hr = document.createElement('hr'); hr.style.cssText = 'border:none;border-top:1px solid #333;margin:16px 0 12px;'; modal.appendChild(hr); const credit = document.createElement('p'); credit.style.cssText = 'color:#666;font-size:11px;margin:0 0 14px;text-align:center;'; credit.textContent = 'Created by Glasnost [1844049]. Xanax always appreciated :)'; modal.appendChild(credit); const closeBtn = makeButton('Close', '#555', () => overlay.remove()); closeBtn.style.cssText += 'float:right;'; modal.appendChild(closeBtn); overlay.appendChild(modal); document.body.appendChild(overlay); } // ── Options modal ──────────────────────────────────────────────────────── function showOptionsModal() { if (document.getElementById('mt-opts-ov')) return; const overlay = makeOverlay('mt-opts-ov'), modal = makeModal(); modal.appendChild(makeTitle('⚙️ Options')); const retLabel = document.createElement('p'); retLabel.style.cssText = 'color:#aaa;font-size:12px;font-weight:bold;margin:0 0 6px;'; retLabel.textContent = 'Recently messaged icon — retention period:'; modal.appendChild(retLabel); const retDesc = document.createElement('p'); retDesc.style.cssText = 'color:#666;font-size:11px;margin:0 0 10px;'; retDesc.textContent = 'Mail icon turns red for this many days after sending.'; modal.appendChild(retDesc); const retSel = document.createElement('select'); retSel.style.cssText = 'background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;padding:5px 8px;font-size:12px;width:100%;margin-bottom:18px;'; RETENTION_OPTIONS.forEach(d => { const op = document.createElement('option'); op.value = d; op.textContent = d === 1 ? '1 day' : d + ' days'; if (d === getRetentionDays()) op.selected = true; retSel.appendChild(op); }); modal.appendChild(retSel); var hr1 = document.createElement('hr'); hr1.style.cssText = 'border:none;border-top:1px solid #333;margin:0 0 14px;'; modal.appendChild(hr1); const ntLabel = document.createElement('p'); ntLabel.style.cssText = 'color:#aaa;font-size:12px;font-weight:bold;margin:0 0 6px;'; ntLabel.textContent = 'Advanced Search — mail button behaviour:'; modal.appendChild(ntLabel); const ntDesc = document.createElement('p'); ntDesc.style.cssText = 'color:#666;font-size:11px;margin:0 0 10px;'; ntDesc.textContent = 'Choose whether clicking the mail button opens in the same tab or a new tab.'; modal.appendChild(ntDesc); const ntSel = document.createElement('select'); ntSel.style.cssText = 'background:#2a2a2a;color:#ccc;border:1px solid #444;border-radius:3px;padding:5px 8px;font-size:12px;width:100%;margin-bottom:18px;'; [['false', 'Same tab'], ['true', 'New tab']].forEach(function(pair) { const op = document.createElement('option'); op.value = pair[0]; op.textContent = pair[1]; if (String(getNewTab()) === pair[0]) op.selected = true; ntSel.appendChild(op); }); modal.appendChild(ntSel); var hr2 = document.createElement('hr'); hr2.style.cssText = 'border:none;border-top:1px solid #333;margin:0 0 14px;'; modal.appendChild(hr2); const clLabel = document.createElement('p'); clLabel.style.cssText = 'color:#aaa;font-size:12px;font-weight:bold;margin:0 0 6px;'; clLabel.textContent = 'Clear sent message cache:'; modal.appendChild(clLabel); const clDesc = document.createElement('p'); clDesc.style.cssText = 'color:#666;font-size:11px;margin:0 0 10px;'; clDesc.textContent = 'Removes all records of recently sent messages. Icons will no longer show as red.'; modal.appendChild(clDesc); const clBtn = makeButton('🗑️ Clear All Sent Cache', '#b71c1c', () => { if (confirm('Clear all sent cache?')) { clearSentLog(); clBtn.textContent = '✓ Cleared'; clBtn.style.background = '#2e7d32'; setTimeout(() => { clBtn.textContent = '🗑️ Clear All Sent Cache'; clBtn.style.background = '#b71c1c'; }, 2000); } }); clBtn.style.marginBottom = '18px'; modal.appendChild(clBtn); var hr3 = document.createElement('hr'); hr3.style.cssText = 'border:none;border-top:1px solid #333;margin:0 0 14px;'; modal.appendChild(hr3); const footer = document.createElement('div'); footer.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; footer.appendChild(makeButton('Cancel', '#555', () => overlay.remove())); footer.appendChild(makeButton('Save', '#2e7d32', () => { const opts = getOptions(); opts.retentionDays = parseInt(retSel.value, 10); opts.newTab = (ntSel.value === 'true'); saveOptions(opts); overlay.remove(); })); modal.appendChild(footer); overlay.appendChild(modal); document.body.appendChild(overlay); } // ── UI Builder ─────────────────────────────────────────────────────────── function buildUI() { ['mt-panel', 'mt-trow', 'mt-drow', 'mt-qrow'].forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); const mailboxWrapper = document.querySelector('.mailbox-wrapper.m-top10'); if (!mailboxWrapper) return; const es = document.getElementById('mt-styles'); if (es) es.remove(); const mobile = isMobile(); const style = document.createElement('style'); style.id = 'mt-styles'; style.textContent = [ '#mt-panel, #mt-trow, #mt-drow, #mt-qrow {', ' background: #1e1e1e; border: 1px solid #3a3a3a; padding: 8px 12px;', ' display: flex; flex-wrap: wrap; align-items: center; gap: 8px;', ' font-family: Arial, sans-serif; font-size: 12px;', '}', '#mt-panel { border-radius: 4px 4px 0 0; margin-top: 6px; border-bottom: none; }', '#mt-trow { border-radius: 0; border-top: 1px solid #2a2a2a; border-bottom: none; }', '#mt-drow { border-radius: 0; border-top: 1px solid #2a2a2a; border-bottom: none; background: #1a1a1a; }', '#mt-qrow { border-radius: 0 0 4px 4px; border-top: 1px solid #2a2a2a; margin-bottom: 6px; background: #161616; }', '.mt-label { color: #aaa; font-weight: bold; }', '.mt-btn { border: none; border-radius: 3px; padding: 4px 10px; cursor: pointer; font-size: 12px; color: #fff; transition: background 0.2s; }', '.mt-btn.save { background: #2e7d32; } .mt-btn.save:hover { background: #388e3c; }', '.mt-btn.export { background: #6a1b9a; } .mt-btn.export:hover { background: #7b1fa2; }', '.mt-btn.import { background: #e65100; } .mt-btn.import:hover { background: #ef6c00; }', '.mt-btn.delete { background: #b71c1c; } .mt-btn.delete:hover { background: #c62828; }', '.mt-btn.help { background: #00695c; } .mt-btn.help:hover { background: #00796b; }', '.mt-btn.options { background: #37474f; } .mt-btn.options:hover { background: #455a64; }', '.mt-tbtn { background: #1565c0; color: #fff; border: none; border-radius: 3px; padding: 4px 10px; cursor: pointer; font-size: 12px; transition: background 0.2s; }', '.mt-tbtn:hover { background: #1976d2; }', '#mt-drow select { background: #2a2a2a; color: #ccc; border: 1px solid #444; border-radius: 3px; padding: 3px 6px; font-size: 12px; }', '#mt-qs-btn { flex: 1; border: none; border-radius: 4px; padding: 10px 16px; font-size: 15px; font-weight: bold; color: #fff; background: #1565c0; cursor: pointer; transition: background 0.2s; min-height: 44px; }', '#mt-qs-btn:hover:not(:disabled) { background: #1976d2; }', '#mt-qs-btn:disabled { opacity: 0.85; }', mobile ? '.mt-btn { padding: 8px 14px; font-size: 14px; min-height: 44px; display: inline-flex; align-items: center; }' : '', mobile ? '.mt-tbtn { padding: 8px 14px; font-size: 14px; min-height: 44px; }' : '', mobile ? '.mt-label { font-size: 13px; width: 100%; }' : '', mobile ? '#mt-drow select { font-size: 14px; padding: 6px 8px; min-height: 44px; }' : '', mobile ? '#mt-qs-btn { font-size: 17px; padding: 14px 16px; min-height: 54px; }' : '', ].join('\n'); document.head.appendChild(style); // Row 1: buttons const panel = document.createElement('div'); panel.id = 'mt-panel'; const label = document.createElement('span'); label.className = 'mt-label'; label.textContent = '📋 Mail Templates:'; panel.appendChild(label); function mkBtn(cls, txt, fn) { const b = document.createElement('button'); b.className = 'mt-btn ' + cls; b.textContent = txt; b.addEventListener('click', fn); return b; } panel.appendChild(mkBtn('save', '💾 Save', () => { const s = getSubject(), b = getBody(); const empty = !b || b === '


' || b.trim() === ''; if (!s && empty) { alert('Nothing to save.'); return; } showSaveModal(s, b); })); panel.appendChild(mkBtn('export', '📤 Export', doExport)); panel.appendChild(mkBtn('import', '📥 Import', doImport)); panel.appendChild(mkBtn('help', '❓ Help', showHelpModal)); panel.appendChild(mkBtn('options', '⚙️ Options', showOptionsModal)); // Row 2: Load templates const trow = document.createElement('div'); trow.id = 'mt-trow'; const tlbl = document.createElement('span'); tlbl.className = 'mt-label'; tlbl.textContent = '📂 Load template:'; trow.appendChild(tlbl); const templates = getTemplates(), tkeys = Object.keys(templates); if (tkeys.length === 0) { const none = document.createElement('span'); none.style.color = '#555'; none.textContent = 'No saved templates'; trow.appendChild(none); } else { tkeys.forEach(name => { const b = document.createElement('button'); b.className = 'mt-tbtn'; b.textContent = '📂 ' + name; b.title = 'Load: ' + name; b.addEventListener('click', () => { const t = getTemplates()[name]; if (t) { setSubject(resolveVars(t.subject || '')); setBody(resolveVars(t.body || '')); } }); trow.appendChild(b); }); } // Row 3: Delete const drow = document.createElement('div'); drow.id = 'mt-drow'; const dlbl = document.createElement('span'); dlbl.className = 'mt-label'; dlbl.textContent = '🗑️ Delete template:'; drow.appendChild(dlbl); const dsel = document.createElement('select'); dsel.id = 'mt-dsel'; const dkeys = Object.keys(getTemplates()); if (dkeys.length === 0) { const op = document.createElement('option'); op.value = ''; op.textContent = '— no templates —'; dsel.appendChild(op); } else { dkeys.forEach(n => { const op = document.createElement('option'); op.value = n; op.textContent = n; dsel.appendChild(op); }); } drow.appendChild(dsel); drow.appendChild(mkBtn('delete', 'Delete', () => { const s = document.getElementById('mt-dsel'); if (!s || !s.value) return; const t = getTemplates(); if (!t[s.value]) return; if (confirm('Delete "' + s.value + '"?')) { delete t[s.value]; saveTemplates(t); buildUI(); } })); // Row 4: Quick Send const qrow = document.createElement('div'); qrow.id = 'mt-qrow'; const qbtn = document.createElement('button'); qbtn.id = 'mt-qs-btn'; qbtn.addEventListener('click', () => { const sb = getSendButton(); if (sb && !qbtn.disabled) sb.click(); }); qrow.appendChild(qbtn); mailboxWrapper.after(panel); panel.after(trow); trow.after(drow); drow.after(qrow); startTimers(); updateQuickSend(); } // ── Cross-tab sync ─────────────────────────────────────────────────────── window.addEventListener('storage', e => { if (e.key === SEND_TIME_KEY) { updateQuickSend(); } }); // ── Compose page init ──────────────────────────────────────────────────── function initCompose() { if (!window.location.hash.includes('p=compose')) return; const check = setInterval(() => { if (document.querySelector('.mailbox-wrapper.m-top10')) { clearInterval(check); buildUI(); watchSendButton(); } }, 300); } window.addEventListener('hashchange', () => { var ids = ['mt-panel', 'mt-trow', 'mt-drow', 'mt-qrow']; if (window.location.hash.includes('p=compose')) { if (!document.getElementById('mt-panel')) initCompose(); else watchSendButton(); } else { ids.forEach(id => { const el = document.getElementById(id); if (el) el.remove(); }); const sw = document.getElementById('mt-ss-wrap'); if (sw) sw.remove(); if (quickSendInterval) { clearInterval(quickSendInterval); quickSendInterval = null; } if (sendBtnObserver) { sendBtnObserver.disconnect(); sendBtnObserver = null; } attachedSendBtn = null; } }); // ════════════════════════════════════════════════════════════════════════ // ── UserList / Search page: mail buttons ───────────────────────────────── // ════════════════════════════════════════════════════════════════════════ function injectUserListStyles() { if (document.getElementById('mt-ul-styles')) return; const style = document.createElement('style'); style.id = 'mt-ul-styles'; style.textContent = [ '.mt-mb {', ' display: inline-flex; align-items: center; justify-content: center;', ' width: 18px; height: 18px; margin-right: 4px; background: none;', ' border: none; padding: 0; cursor: pointer; opacity: 0.55;', ' vertical-align: middle; flex-shrink: 0;', ' transition: opacity 0.15s; text-decoration: none;', '}', '.mt-mb:hover { opacity: 1; }', '.mt-mb svg { width: 14px; height: 14px; fill: #8ab4e8; display: block; }', '.mt-mb.mt-mb-sent svg { fill: #8b1a1a; }', ].join('\n'); document.head.appendChild(style); } function makeMailSVG() { const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); svg.setAttribute('viewBox', '0 0 20 16'); svg.innerHTML = ''; return svg; } function injectMailButtons() { const userList = document.querySelector('.user-info-list-wrap'); if (!userList) return; pruneSentLog(); userList.querySelectorAll(':scope > li').forEach(li => { if (li.querySelector('.mt-mb')) return; const expander = li.querySelector('.expander'); if (!expander) return; const profileLink = expander.querySelector('a.user.name'); if (!profileLink) return; const xidMatch = (profileLink.getAttribute('href') || '').match(/XID=(\d+)/); if (!xidMatch) return; const xid = xidMatch[1]; const statusUl = expander.querySelector('ul.big.svg.singleicon'); if (!statusUl) return; const a = document.createElement('a'); a.className = 'mt-mb' + (recentlySent(xid) ? ' mt-mb-sent' : ''); a.href = '/messages.php#/p=compose&XID=' + xid; if (getNewTab()) a.target = '_blank'; a.title = recentlySent(xid) ? 'Message ' + profileLink.textContent.trim() + ' (sent recently)' : 'Message ' + profileLink.textContent.trim(); a.appendChild(makeMailSVG()); a.addEventListener('click', e => e.stopPropagation()); expander.insertBefore(a, statusUl); // Constrain name width to prevent row wrapping caused by our mail button. // On mobile: shrink by a fixed 22px (mail button width + margin). // On desktop: calculate available space from actual expander width so honor // bar images (which can be 200px wide) don't push the row onto two lines. if (isMobile()) { const currentWidth = profileLink.offsetWidth; profileLink.style.width = (currentWidth - 22) + 'px'; profileLink.style.overflow = 'hidden'; profileLink.style.textOverflow = 'ellipsis'; profileLink.style.whiteSpace = 'nowrap'; profileLink.style.display = 'inline-block'; } else { // mail(18) + mail margin-right(4) + status(30) + faction(35) + name margin-left(4) = 91px overhead const maxNameW = expander.offsetWidth - 91; if (maxNameW > 0 && profileLink.offsetWidth > maxNameW) { profileLink.style.maxWidth = maxNameW + 'px'; profileLink.style.overflow = 'hidden'; profileLink.style.verticalAlign = 'middle'; profileLink.style.display = 'inline-block'; } } }); } function initUserList() { injectUserListStyles(); injectMailButtons(); new MutationObserver(() => injectMailButtons()).observe(document.body, { childList: true, subtree: true }); } // ── Entry point ────────────────────────────────────────────────────────── if (window.location.href.includes('messages.php')) { initCompose(); } else if (window.location.href.includes('sid=UserList')) { initUserList(); } })();