MediaWiki:Common.js: Difference between revisions
No edit summary |
Footage deck: opt-out (all in by default, admins remove) |
||
| (125 intermediate revisions by 2 users not shown) | |||
| Line 1: | Line 1: | ||
/* | /* Load CRUST brand fonts — ResourceLoader strips @import in Common.css, so inject the link here */ | ||
(function(){ | |||
var l=document.createElement('link'); l.rel='stylesheet'; | |||
l.href='https://fonts.googleapis.com/css2?family=Anton&family=Libre+Franklin:wght@400;500;600;700;800;900&family=Spline+Sans+Mono:wght@400;500;600&display=swap'; | |||
document.head.appendChild(l); | |||
})(); | |||
(function () { | (function () { | ||
'use strict'; | 'use strict'; | ||
| Line 11: | Line 12: | ||
var DAYS = 14; | var DAYS = 14; | ||
if ((mw.config.get('wgUserName')!==null)) return; | |||
if ( | |||
var until = parseInt(localStorage.getItem(KEY) || '0', 10); | var until = parseInt(localStorage.getItem(KEY) || '0', 10); | ||
if (Date.now() < until) return; | if (Date.now() < until) return; | ||
| Line 22: | Line 21: | ||
if (!siteNotice) return; | if (!siteNotice) return; | ||
// If | // If empty, do nothing | ||
var txt = (siteNotice.textContent || '').replace(/\s+/g, ' ').trim(); | var txt = (siteNotice.textContent || '').replace(/\s+/g, ' ').trim(); | ||
if (!txt) return; | if (!txt) return; | ||
siteNotice.classList.add('icelist-anon-donate'); | siteNotice.classList.add('icelist-anon-donate'); | ||
// Add close button once | |||
// Add close button | |||
if (!siteNotice.querySelector('.icelist-notice-close')) { | if (!siteNotice.querySelector('.icelist-notice-close')) { | ||
var btn = document.createElement('button'); | var btn = document.createElement('button'); | ||
| Line 46: | Line 34: | ||
btn.setAttribute('aria-label', 'Close'); | btn.setAttribute('aria-label', 'Close'); | ||
btn.textContent = '×'; | btn.textContent = '×'; | ||
siteNotice.insertBefore(btn, siteNotice.firstChild); | siteNotice.insertBefore(btn, siteNotice.firstChild); | ||
btn.addEventListener('click', function (e) { | btn.addEventListener('click', function (e) { | ||
e.preventDefault(); | e.preventDefault(); | ||
siteNotice.style.display = 'none'; | siteNotice.style.display = 'none'; | ||
localStorage.setItem(KEY, String(Date.now() + DAYS * 24 * 60 * 60 * 1000)); | localStorage.setItem(KEY, String(Date.now() + DAYS * 24 * 60 * 60 * 1000)); | ||
| Line 59: | Line 44: | ||
} | } | ||
if (document.readyState === 'loading') { | |||
document.addEventListener('DOMContentLoaded', mount); | document.addEventListener('DOMContentLoaded', mount); | ||
} else { | } else { | ||
| Line 70: | Line 50: | ||
} | } | ||
})(); | })(); | ||
mw.loader.using(['jquery']).then(function () { | |||
var s = document.createElement('script'); | |||
s.src = 'https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js'; | |||
s.setAttribute('data-name', 'BMC-Widget'); | |||
s.setAttribute('data-cfasync', 'false'); | |||
s.setAttribute('data-id', 'crustnews'); | |||
s.setAttribute('data-description', 'Support me on Buy me a coffee!'); | |||
s.setAttribute('data-message', ''); | |||
s.setAttribute('data-color', '#FF5F5F'); | |||
s.setAttribute('data-position', 'Right'); | |||
s.setAttribute('data-x_margin', '18'); | |||
s.setAttribute('data-y_margin', '18'); | |||
document.body.appendChild(s); | |||
}); | |||
// Tag agent pages (any page showing an agent card) so the duplicate title/heading can be hidden | |||
if ( document.querySelector( '.ic-agent-card' ) ) { | |||
document.body.classList.add( 'ic-agent-page' ); | |||
} | |||
/* ---- Mark anonymous users so anon-only banners can show (fail-safe: hidden by default) ---- */ | |||
if ((mw.config.get('wgUserName')===null)) { document.documentElement.classList.add('vol-anon'); } | |||
/* ---- Incident candidates page: logged-in-only + per-row dismiss ---- */ | |||
if (/Incident_candidates$/.test(mw.config.get('wgPageName'))) { | |||
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () { | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) return; | |||
if ((mw.config.get('wgUserName')===null)) { | |||
content.innerHTML = '<div class="vol-login">This page is for logged-in ICE List volunteers. ' + | |||
'<a href="' + mw.util.getUrl('Special:UserLogin', {returnto: mw.config.get('wgPageName')}) + | |||
'">Log in</a> to review incident candidates.</div>'; | |||
return; | |||
} | |||
var api = new mw.Api(); | |||
content.querySelectorAll('li').forEach(function (li) { | |||
var create = Array.prototype.find.call(li.querySelectorAll('a'), function (a) { | |||
return /FormEdit\/Incident/.test(a.getAttribute('href') || ''); | |||
}); | |||
if (!create) return; | |||
var art = Array.prototype.find.call(li.querySelectorAll('a[href^="http"]'), function (a) { | |||
return a !== create && !/FormEdit\/Incident/.test(a.getAttribute('href') || ''); | |||
}); | |||
var url = art ? art.href : null; | |||
var btn = document.createElement('button'); | |||
btn.textContent = '✕ dismiss'; | |||
btn.className = 'vol-dismiss'; | |||
btn.onclick = function () { | |||
if (!url) { li.remove(); return; } | |||
btn.disabled = true; btn.textContent = '…'; | |||
api.get({action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', formatversion: 2}) | |||
.then(function (r) { | |||
var lines = r.parse.wikitext.split('\n'), out = [], removed = false; | |||
for (var i = 0; i < lines.length; i++) { | |||
if (!removed && lines[i].indexOf(url) !== -1) { | |||
if (out.length && /^<!-- cand:/.test(out[out.length - 1])) out.pop(); | |||
removed = true; continue; | |||
} | |||
out.push(lines[i]); | |||
} | |||
return api.postWithToken('csrf', {action: 'edit', title: mw.config.get('wgPageName'), | |||
text: out.join('\n'), summary: 'Dismiss incident candidate'}); | |||
}) | |||
.then(function () { li.remove(); }) | |||
.catch(function () { btn.disabled = false; btn.textContent = '✕ dismiss'; }); | |||
}; | |||
li.appendChild(document.createTextNode(' ')); | |||
li.appendChild(btn); | |||
}); | |||
}); | |||
} | |||
/* ============ ICE List client tools (vanilla; robust login detection) ============ */ | |||
(function () { | |||
function ready(fn) { if (document.readyState !== 'loading') { fn(); } else { document.addEventListener('DOMContentLoaded', fn); } } | |||
var IC_ST = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming',DC:'District of Columbia' }; | |||
function icNormState(x){ x=(x||'').trim(); var u=x.toUpperCase(); if(x.length<=2 && IC_ST[u]){ return IC_ST[u]; } return x; } | |||
function icConf(txt){ var c={}; (txt||'').split('\n').forEach(function(l){ l=l.replace(/^[*#\s]+/,'').trim(); if(l && l.indexOf('<')!==0){ c[l]=1; } }); return c; } | |||
/* Essential information: fully gadget-rendered interactive accordion (no wiki-parser fragility) */ | |||
ready(function () { | |||
var mount = document.getElementById('ic-essential'); if (!mount) { return; } | |||
function U(t) { return '/index.php/' + encodeURI(t.replace(/ /g, '_')); } | |||
var G = [ | |||
['Your rights', 'If ICE shows up', [['Your Rights at Your Door', 'At your door'], ['Your Rights on the Street', 'On the street'], ['Your Rights in Your Car', 'In your car'], ['Your Rights at Work', 'At work'], ['Courthouses, Schools and Hospitals', 'Courthouses, schools & hospitals']]], | |||
['Your rights', 'Protest & bear witness', [['Protest Rights', 'Protest rights'], ['If You Witness a Raid', 'If you witness a raid'], ['Recording Incidents Safely', 'Recording incidents safely'], ['Information to Document', 'What to document']]], | |||
['Family', 'For immigrants & families', [['Family Preparedness Plan', 'Family preparedness plan'], ['Red Cards and Rights Cards', 'Red cards: your rights without speaking'], ['If You Are Detained', 'If you are detained'], ['If a Loved One Is Detained', 'If a loved one is detained']]], | |||
['Support', 'Get help', [['Finding an Immigration Lawyer', 'Find a real lawyer, free if needed'], ['Emergency Hotlines and Rapid Response', 'Hotlines: save these numbers'], ['How to Report an Incident', 'Report an incident']]], | |||
['Field guide', 'Spotting ICE', [['How to Spot ICE', 'How to spot ICE'], ['Plainclothes and Gear Patterns', 'Plainclothes & gear patterns'], ['ICE Vehicle Identification', 'Vehicle identification'], ['Joint Operations With Local Police', 'Joint ops with local police']]], | |||
['Explainer', 'Understanding ICE', [['ICE vs CBP vs HSI vs ERO', 'ICE vs CBP vs HSI vs ERO'], ['287g Agreements Explained', '287(g) explained'], ['How Field Offices Operate', 'How field offices operate']]] | |||
]; | |||
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<'); } | |||
var html = '<div class="ic-ess">'; | |||
G.forEach(function (g, i) { | |||
var rows = g[2].map(function (it) { return '<a class="ic-ess-row" href="' + U(it[0]) + '"><span>' + esc(it[1]) + '</span><span class="ic-ess-arr">→</span></a>'; }).join(''); | |||
html += '<div class="ic-ess-g' + (i === 0 ? ' ic-ess-g--open' : '') + '">' | |||
+ '<button type="button" class="ic-ess-h"><span class="ic-ess-k">' + esc(g[0]) + '</span><span class="ic-ess-t">' + esc(g[1]) + '</span><span class="ic-ess-tog"></span></button>' | |||
+ '<div class="ic-ess-b">' + rows + '</div></div>'; | |||
}); | |||
html += '<a class="ic-ess-all" href="' + U('ICE List:Essential information') + '">All guides in one place →</a></div>'; | |||
mount.innerHTML = html; | |||
mount.addEventListener('click', function (e) { var h = e.target.closest('.ic-ess-h'); if (h) { h.parentNode.classList.toggle('ic-ess-g--open'); } }); | |||
}); | |||
/* DPL page normalizer — MUST run first. {{FULLPAGENAME}} inside a DPL include wrongly | |||
resolves to the listing page, so every card's data-page is wrong. The DPL wrapper | |||
.ic-sub-dpl carries the true per-item page (via %PAGE%); copy it onto the inner card | |||
so every downstream gadget reads the correct page unchanged. */ | |||
ready(function () { | |||
var ws = document.querySelectorAll('.ic-sub-dpl[data-page]'); | |||
for (var i = 0; i < ws.length; i++) { | |||
var p = ws[i].getAttribute('data-page'); | |||
if (!p) { continue; } | |||
var inner = ws[i].querySelectorAll('[data-page]'); | |||
for (var j = 0; j < inner.length; j++) { inner[j].setAttribute('data-page', p); } | |||
} | |||
}); | |||
/* Detect logged-in state robustly: mw.user first, then the DOM logout link | |||
(wgUserName can be null in cached RLCONF, so trust the personal-tools chrome). */ | |||
function markMember() { | |||
var el = document.documentElement; | |||
if (el.classList.contains('vol-member')) { return true; } | |||
var inn = false; | |||
try { if (window.mw && mw.user && (mw.config.get('wgUserName')!==null)) { inn = true; } } catch (e) {} | |||
if (!inn && (document.getElementById('pt-logout') || | |||
document.querySelector('a[href*="Special:UserLogout"], a[href*="UserLogout"], #pt-userpage'))) { inn = true; } | |||
if (inn) { el.classList.add('vol-member'); } | |||
return inn; | |||
} | |||
markMember(); | |||
ready(markMember); | |||
/* Incident pages: footage + agents front and centre, volunteer add-forms */ | |||
ready(function () { | |||
try { | |||
var ns = mw.config.get('wgNamespaceNumber'); | |||
var cats = mw.config.get('wgCategories') || []; | |||
var pn = mw.config.get('wgPageName'); | |||
var pnText = pn.replace(/_/g, ' '); | |||
var isIncident = (ns === 0 && cats.indexOf('Incidents') !== -1); | |||
var isAgent = (ns === 0 && cats.indexOf('Agents') !== -1) || (ns === 4 && /^ICE List:Unidentified agent - /.test(pnText)); | |||
if (!isIncident && !isAgent) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var isVol = (typeof markMember === 'function') && markMember(); | |||
var hero = document.createElement('div'); hero.className = 'ic-incident-hero'; | |||
var panels = {}; | |||
var box = content.querySelector('.ic-incident-box'); | |||
var incState = ''; | |||
if (box) { | |||
[].slice.call(box.querySelectorAll('.ic-incident-meta > div')).forEach(function (r) { | |||
var dt = r.querySelector('dt'), dd = r.querySelector('dd'); | |||
if (dt && dd && /^state$/i.test(dt.textContent.trim())) { incState = dd.textContent.trim(); } | |||
}); | |||
} | |||
if (isVol) { | |||
var bar = document.createElement('div'); bar.className = 'ic-incident-actions'; | |||
function mkBtn(label, key, cls) { | |||
var b = document.createElement('button'); b.className = 'ic-incident-actbtn' + (cls ? ' ' + cls : ''); | |||
b.innerHTML = label; | |||
b.addEventListener('click', function () { | |||
var open = panels[key] && panels[key].style.display === 'block'; | |||
Object.keys(panels).forEach(function (k) { if (panels[k]) { panels[k].style.display = 'none'; } }); | |||
if (panels[key] && !open) { panels[key].style.display = 'block'; } | |||
}); | |||
bar.appendChild(b); return b; | |||
} | |||
mkBtn('✎ Add information', 'info', 'primary'); | |||
mkBtn('▶ Add footage / media', 'media'); | |||
if (isIncident) { mkBtn('+ Add an agent', 'agent'); } | |||
hero.appendChild(bar); | |||
} | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); } | |||
function panel(title) { | |||
var p = document.createElement('div'); p.className = 'ic-incident-panel'; p.style.display = 'none'; | |||
var h = document.createElement('div'); h.className = 'ic-incident-panel__h'; h.textContent = title; p.appendChild(h); | |||
return p; | |||
} | |||
function inp(ph, val) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph || ''; i.className = 'ic-incident-input'; if (val) { i.value = val; } return i; } | |||
if (isVol) { | |||
/* INFO */ | |||
var pInfo = panel('Add information'); | |||
var infoP = document.createElement('p'); infoP.textContent = 'Open the editor to add or correct information on this incident.'; | |||
var infoA = document.createElement('a'); infoA.className = 'ic-incident-actbtn primary'; infoA.textContent = 'Open editor'; infoA.href = '/index.php/' + pn + '?action=edit'; | |||
pInfo.appendChild(infoP); pInfo.appendChild(infoA); hero.appendChild(pInfo); panels.info = pInfo; | |||
/* MEDIA */ | |||
var pMedia = panel('Add footage / media'); | |||
var urlI = inp('Paste any social/video link (YouTube, X, TikTok, Instagram, Threads, Facebook, Vimeo)'); | |||
var capI = inp('Caption (optional)'); | |||
var mBtn = document.createElement('button'); mBtn.className = 'ic-incident-actbtn primary'; mBtn.textContent = 'Embed on this page'; | |||
var mMsg = document.createElement('div'); mMsg.className = 'ic-incident-msg'; | |||
pMedia.appendChild(urlI); pMedia.appendChild(capI); pMedia.appendChild(mBtn); pMedia.appendChild(mMsg); | |||
hero.appendChild(pMedia); panels.media = pMedia; | |||
mBtn.addEventListener('click', function () { | |||
var u = urlI.value.trim(); | |||
if (!/^https?:\/\//.test(u)) { mMsg.textContent = 'Enter a valid https link.'; return; } | |||
mBtn.disabled = true; mMsg.textContent = 'Embedding…'; | |||
var emb = '{{Embed|url=' + esc(u) + (capI.value.trim() ? '|caption=' + esc(capI.value.trim()) : '') + '}}'; | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) { | |||
var pg = d.query.pages[0]; var txt = (pg.revisions && pg.revisions[0].slots.main.content) || ''; | |||
var marker = '<div class="ic-incident-footage">'; | |||
if (txt.indexOf(marker) !== -1) { txt = txt.replace(marker, marker + '\n' + emb); } | |||
else { var ci = txt.search(/\n\[\[Category:/); var blk = '\n' + marker + '\n' + emb + '\n</div>\n'; txt = (ci !== -1) ? txt.slice(0, ci) + blk + txt.slice(ci) : txt + blk; } | |||
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Add footage/media via incident form' }); | |||
}).then(function () { mMsg.textContent = '✓ Added — reloading…'; setTimeout(function () { location.reload(); }, 900); }) | |||
.catch(function (e) { mBtn.disabled = false; mMsg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
if (isIncident) { | |||
/* AGENT */ | |||
var pAgent = panel('Add an agent'); | |||
var help = document.createElement('div'); help.className = 'ic-incident-help'; | |||
help.innerHTML = 'Leave <b>name</b> blank for an <b>unidentified</b> agent (goes to the Most Wanted board). Enter a name to create an <b>identified</b> agent page. Either way it links to this incident.'; | |||
pAgent.appendChild(help); | |||
var nameI = inp('Name — blank if unidentified'); | |||
var codeI = inp('Code for unidentified (e.g. ' + (incState ? incState.replace(/[^A-Za-z]/g, '').slice(0, 2).toUpperCase() : 'XX') + '-0001)'); | |||
var agencyI = inp('Agency (ICE / CBP / Unknown)'); | |||
var stateI = inp('State', incState || ''); | |||
var descI = inp('What they did / notes'); | |||
[nameI, codeI, agencyI, stateI, descI].forEach(function (e) { pAgent.appendChild(e); }); | |||
var mwRow = document.createElement('label'); mwRow.className = 'ic-incident-check'; | |||
var mwCb = document.createElement('input'); mwCb.type = 'checkbox'; mwRow.appendChild(mwCb); | |||
mwRow.appendChild(document.createTextNode(' Flag as Most Wanted (unidentified only)')); pAgent.appendChild(mwRow); | |||
var aBtn = document.createElement('button'); aBtn.className = 'ic-incident-actbtn primary'; aBtn.textContent = 'Create & link to this incident'; | |||
var aMsg = document.createElement('div'); aMsg.className = 'ic-incident-msg'; | |||
pAgent.appendChild(aBtn); pAgent.appendChild(aMsg); hero.appendChild(pAgent); panels.agent = pAgent; | |||
aBtn.addEventListener('click', function () { | |||
var name = nameI.value.trim(), code = codeI.value.trim().replace(/[^A-Za-z0-9 .\-]/g, ''); | |||
var agency = agencyI.value.trim() || 'Unknown', st = stateI.value.trim(), desc = descI.value.trim(); | |||
var named = !!name; | |||
if (!named && !code) { aMsg.textContent = 'Enter a name, or a code for an unidentified agent.'; return; } | |||
aBtn.disabled = true; aMsg.textContent = 'Creating…'; | |||
var title, body, agentLink, stcat = st || 'No State Given'; | |||
if (named) { | |||
title = name; agentLink = '[[' + name + ']]'; | |||
body = '{{Agent page\n|name=' + esc(name) + '\n|agency=' + esc(agency) + '\n|state=' + esc(st) + '\n|status=Active\n|image=Agent-placeholder.jpg\n|summary=' + esc(name + ' was documented in connection with ' + pnText + '.') + '\n}}\n\n== Documented Incidents ==\n{{IncidentsForAgent}}\n\n{{MediaForAgent}}\n\n[[Category:Agents]][[Category:Agents in ' + stcat + ']][[Category:Agents needing a photo]]\n'; | |||
} else { | |||
title = 'ICE List:Unidentified agent - ' + code; agentLink = '[[' + title + ']]'; | |||
var wanted = mwCb.checked ? 'Most wanted' : 'Standard'; | |||
var wcat = mwCb.checked ? 'Most wanted unidentified agents' : 'Standard unidentified agents'; | |||
body = '{{UnknownAgent\n | code = ' + esc(code) + '\n | image = Agent-placeholder.jpg\n | description = ' + esc(desc || ('Recorded during ' + pnText)) + '. Source: [[' + pnText + ']]\n | agency = ' + esc(agency) + '\n | state = ' + esc(st) + '\n | seen = \n | status = Open\n | wanted = ' + wanted + '\n}}\n<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n{{MediaForAgent}}\n\n[[Category:Unidentified agents]][[Category:' + wcat + ']][[Category:Unidentified agents in ' + stcat + ']][[Category:Agents in ' + stcat + ']]\n'; | |||
} | |||
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('That agent page already exists: ' + title); } | |||
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create agent via incident form' }); | |||
}).then(function () { | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }); | |||
}).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
if (txt.indexOf(agentLink) === -1) { | |||
txt = txt.replace(/(\|\s*agents_involved\s*=\s*)([^\n]*)/, function (m, p, val) { return p + (val.trim() ? val.trim() + '; ' : '') + agentLink; }); | |||
} | |||
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Link agent ' + title + ' to incident' }); | |||
}).then(function () { | |||
aMsg.innerHTML = '✓ Created ' + (named ? 'identified agent' : 'unidentified agent (now on the Most Wanted board)') + '. Reloading…'; | |||
setTimeout(function () { location.reload(); }, 1300); | |||
}).catch(function (e) { aBtn.disabled = false; aMsg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
} | |||
} | |||
/* FOOTAGE hoist */ | |||
var fWrap = document.createElement('div'); | |||
var media = content.querySelector('.ic-media-block'); | |||
var inFoot = content.querySelector('.ic-incident-footage'); | |||
if (media || inFoot) { var fh = document.createElement('div'); fh.className = 'ic-incident-hero__h'; fh.textContent = 'Footage & media'; fWrap.appendChild(fh); } | |||
if (inFoot) { fWrap.appendChild(inFoot); } | |||
if (media) { fWrap.appendChild(media); } | |||
hero.appendChild(fWrap); | |||
/* AGENTS strip */ | |||
var seen = {}, codes = []; | |||
if (box) { | |||
[].slice.call(box.querySelectorAll('a[href]')).forEach(function (a) { | |||
var m = decodeURIComponent(a.getAttribute('href') || '').replace(/_/g, ' ').match(/Unidentified agent - ([^"#?|\]&]+)/); | |||
if (m) { var c = m[1].trim(); if (!seen[c]) { seen[c] = 1; codes.push(c); } } | |||
}); | |||
} | |||
if (codes.length) { | |||
var aw = document.createElement('div'); | |||
var ah = document.createElement('div'); ah.className = 'ic-incident-hero__h'; ah.textContent = 'Agents at the scene'; aw.appendChild(ah); | |||
var strip = document.createElement('div'); strip.className = 'ic-incident-agents'; | |||
codes.forEach(function (c) { | |||
var src = '/index.php/Special:Redirect/file/' + encodeURIComponent('Wanted-' + c + '.png'); | |||
var card = document.createElement('div'); card.className = 'ic-incident-agent'; | |||
card.innerHTML = '<a href="/index.php/ICE_List:Unidentified_agent_-_' + encodeURIComponent(c.replace(/ /g, '_')) + '"><img alt="Agent ' + c + '" loading="lazy"></a><div class="ic-incident-agent__cap">Agent ' + c + ' · unidentified</div>'; | |||
var img = card.querySelector('img'); img.onerror = function () { card.style.display = 'none'; }; img.src = src; | |||
strip.appendChild(card); | |||
}); | |||
aw.appendChild(strip); hero.appendChild(aw); | |||
} | |||
var firstH = content.querySelector('.mw-heading2, h2'); | |||
if (firstH) { content.insertBefore(hero, firstH); } else { content.appendChild(hero); } | |||
}); | |||
} catch (e) {} | |||
}); | |||
/* Identify: convert an unidentified-agent page into a named agent page */ | |||
ready(function () { | |||
try { | |||
if (typeof markMember !== 'function' || !markMember()) { return; } | |||
if (mw.config.get('wgAction') !== 'view') { return; } | |||
if ((mw.config.get('wgCategories') || []).indexOf('Unidentified agents') === -1) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var code = pn.replace('ICE List:Unidentified agent - ', '').trim(); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); } | |||
function inp(ph) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph; i.className = 'ic-incident-input'; return i; } | |||
var box = document.createElement('div'); box.className = 'ic-identify'; | |||
var h = document.createElement('div'); h.className = 'ic-identify__h'; h.textContent = 'Identified this agent? Convert to a named agent page'; | |||
box.appendChild(h); | |||
var nameI = inp('Full name (required)'), agencyI = inp('Agency — ICE, CBP, HSI…'), roleI = inp('Role / title (optional)'), officeI = inp('Field office (optional)'); | |||
[nameI, agencyI, roleI, officeI].forEach(function (e) { box.appendChild(e); }); | |||
var btn = document.createElement('button'); btn.className = 'ic-incident-actbtn primary'; btn.textContent = 'Convert & remove from Most Wanted'; | |||
var msg = document.createElement('div'); msg.className = 'ic-incident-msg'; | |||
box.appendChild(btn); box.appendChild(msg); | |||
content.insertBefore(box, content.firstChild); | |||
btn.addEventListener('click', function () { | |||
var name = nameI.value.trim(); | |||
if (!name) { msg.textContent = 'Enter the agent’s name.'; return; } | |||
var agency = agencyI.value.trim() || 'Unknown', role = roleI.value.trim(), office = officeI.value.trim(); | |||
btn.disabled = true; msg.textContent = 'Working…'; | |||
var image = '', state = '', desc = ''; | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
var mi = txt.match(/\|\s*image\s*=\s*([^\n|]*)/); image = mi ? mi[1].trim() : ''; | |||
var ms = txt.match(/\|\s*state\s*=\s*([^\n|]*)/); state = ms ? ms[1].trim() : ''; | |||
var md = txt.match(/\|\s*description\s*=\s*([^\n|]*)/); desc = md ? md[1].trim() : ''; | |||
return api.get({ action: 'query', titles: name, formatversion: 2 }); | |||
}).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + name + '" already exists — merge manually.'); } | |||
return api.postWithToken('csrf', { action: 'move', from: pn, to: name, reason: 'Agent identified as ' + name, movetalk: 1 }); | |||
}).then(function () { | |||
var ph = !image || /placeholder|nopfp|no-pfp|noimage/i.test(image); | |||
var stcat = state || 'No State Given'; | |||
var body = '{{Agent page\n|name=' + esc(name) + '\n|agency=' + esc(agency) + | |||
'\n|role=' + esc(role) + '\n|field_office=' + esc(office) + '\n|state=' + esc(state) + | |||
'\n|status=Active\n|image=' + (image || 'nopfp.png') + '\n|verification=Verified\n|summary=' + | |||
esc(name + ' was documented as the federal agent previously recorded as unidentified agent ' + code + '. ' + desc) + | |||
'\n}}\n\n== Documented Incidents ==\n\'\'This list updates automatically when incident pages link to this agent.\'\'\n\n{{IncidentsForAgent}}\n\n' + | |||
'== Evidence and Sources ==\n* Formerly documented as unidentified agent \'\'\'' + code + '\'\'\'.\n* Here extra evidence can be provided, including social media links\n\n' + | |||
'{{MediaForAgent}}\n\n[[Category:Agents]][[Category:Agents in ' + stcat + ']]' + | |||
(ph ? '[[Category:Agents needing a photo]]' : '[[Category:Agents with photo]]') + '[[Category:Formerly unidentified agents]]\n'; | |||
return api.postWithToken('csrf', { action: 'edit', title: name, text: body, summary: 'Convert unidentified agent ' + code + ' to identified agent page' }); | |||
}).then(function () { | |||
return api.get({ action: 'query', list: 'backlinks', bltitle: pn, bllimit: 50, blnamespace: 0 }); | |||
}).then(function (d) { | |||
var links = (d.query.backlinks || []).map(function (b) { return b.title; }).filter(function (t) { return t !== name; }); | |||
var chain = Promise.resolve(); | |||
var resrc = '\\[\\[\\s*' + pn.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(\\|[^\\]]*)?\\]\\]'; | |||
links.forEach(function (t) { | |||
chain = chain.then(function () { | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: t, formatversion: 2 }).then(function (dd) { | |||
var pg = dd.query.pages[0]; if (!pg.revisions) { return; } | |||
var tx = pg.revisions[0].slots.main.content; | |||
var nx = tx.replace(new RegExp(resrc, 'g'), function (m, p1) { return '[[' + name + (p1 || '') + ']]'; }); | |||
if (nx !== tx) { return api.postWithToken('csrf', { action: 'edit', title: t, text: nx, summary: 'Attribute identified agent ' + name }); } | |||
}); | |||
}); | |||
}); | |||
return chain; | |||
}).then(function () { | |||
var url = '/index.php/' + encodeURIComponent(name.replace(/ /g, '_')); | |||
msg.innerHTML = '✓ Converted. Redirecting to <a href="' + url + '">' + name + '</a>…'; | |||
setTimeout(function () { location.href = url; }, 1200); | |||
}).catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
}); | |||
} catch (e) {} | |||
}); | |||
/* Submissions hub: filter chips */ | |||
ready(function () { | |||
var tb = document.getElementById('ic-sub-toolbar'); | |||
if (!tb) { return; } | |||
var cards = [].slice.call(document.querySelectorAll('.ic-sub-card')); | |||
if (!cards.length) { return; } | |||
var defs = [['all', 'All'], ['matched', 'Matched to a page'], ['unmatched', 'Unmatched'], ['file', 'Has file'], ['link', 'Has link'], ['video', 'Video']]; | |||
var chips = [], cnt = document.createElement('span'); cnt.className = 'ic-sub-count'; | |||
function match(c, f) { return f === 'all' ? true : c.getAttribute('data-' + f) === '1'; } | |||
function apply(f) { | |||
var n = 0; | |||
cards.forEach(function (c) { var ok = match(c, f); c.style.display = ok ? '' : 'none'; if (ok) { n++; } }); | |||
chips.forEach(function (ch) { ch.classList.toggle('is-active', ch.getAttribute('data-f') === f); }); | |||
cnt.textContent = n + ' shown'; | |||
} | |||
defs.forEach(function (d) { | |||
var b = document.createElement('button'); b.className = 'ic-sub-chip'; b.setAttribute('data-f', d[0]); b.textContent = d[1]; | |||
b.addEventListener('click', function () { apply(d[0]); }); tb.appendChild(b); chips.push(b); | |||
}); | |||
tb.appendChild(cnt); apply('all'); | |||
}); | |||
/* Submission review queue: route each submission; routing removes the options */ | |||
ready(function () { | |||
var q = document.querySelector('.ic-subq'); | |||
if (!q) { return; } | |||
if (!(typeof markMember === 'function' && markMember())) { | |||
var g = q.querySelector('.ic-subq__buttons'); if (g) { g.innerHTML = '<em>Log in as a volunteer to route this submission.</em>'; } | |||
return; | |||
} | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
var pn = mw.config.get('wgPageName'); | |||
var file = q.getAttribute('data-file') || '', link = q.getAttribute('data-link') || '', suggested = q.getAttribute('data-suggested') || ''; | |||
var VIDEO = /(youtube\.com|youtu\.be|tiktok\.com|instagram\.com|threads\.(com|net)|facebook\.com|fb\.watch|vimeo\.com|x\.com|twitter\.com)/i; | |||
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); } | |||
var wrap = q.querySelector('.ic-subq__buttons'); wrap.innerHTML = ''; | |||
var msg = document.createElement('div'); msg.className = 'ic-subq__msg'; | |||
function attachTo(target, done) { | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: target, formatversion: 2 }).then(function (d) { | |||
var p = d.query.pages[0]; | |||
if (!p || p.missing !== undefined) { throw new Error('Page not found: ' + target); } | |||
var txt = p.revisions[0].slots.main.content, media = ''; | |||
if (file) { media += '[[File:' + file + '|thumb|300px|Submitted media]]'; } | |||
if (link && VIDEO.test(link)) { media += (media ? '\n' : '') + '{{Embed|url=' + esc(link) + '}}'; } | |||
if (media) { txt = (txt.indexOf('{{MediaForAgent}}') !== -1) ? txt.replace('{{MediaForAgent}}', media + '\n{{MediaForAgent}}') : (txt.trimEnd() + '\n\n' + media + '\n'); } | |||
if (link && !VIDEO.test(link)) { | |||
var ev = '* ' + esc(link) + ' — submitted via form'; | |||
if (/==\s*Evidence and Sources\s*==/.test(txt)) { txt = txt.replace(/(==\s*Evidence and Sources\s*==[^\n]*\n)/, '$1' + ev + '\n'); } | |||
else { txt = txt.trimEnd() + '\n\n== Evidence and Sources ==\n' + ev + '\n'; } | |||
} | |||
return api.postWithToken('csrf', { action: 'edit', title: target, text: txt, summary: 'Add submitted media (review queue)' }); | |||
}).then(function () { return resolve('routed to [[' + target + ']]'); }).then(done).catch(function (e) { fail(e); }); | |||
} | |||
function resolve(note) { | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
txt = txt.replace(/\{\{Submission entry[^}]*\}\}/, '').replace('[[Category:Submission review]]', ''); | |||
txt = "'''Resolved:''' " + note + '\n\n' + txt + '\n[[Category:Resolved submissions]]\n'; | |||
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Resolve submission: ' + note }); | |||
}); | |||
} | |||
function fail(e) { msg.textContent = 'Error: ' + (e && e.message ? e.message : e); [].forEach.call(wrap.querySelectorAll('button,input'), function (x) { x.disabled = false; }); } | |||
function done() { msg.innerHTML = '✓ Routed — reloading…'; setTimeout(function () { location.reload(); }, 900); } | |||
function busy() { [].forEach.call(wrap.querySelectorAll('button,input'), function (x) { x.disabled = true; }); msg.textContent = 'Working…'; } | |||
function btn(label, cls, fn) { var b = document.createElement('button'); b.className = 'ic-incident-actbtn' + (cls ? ' ' + cls : ''); b.textContent = label; b.addEventListener('click', fn); wrap.appendChild(b); return b; } | |||
if (suggested) { | |||
btn('Attach to ' + suggested, 'primary', function () { busy(); attachTo(suggested, done); }); | |||
} | |||
var row = document.createElement('span'); row.className = 'ic-subq__row'; | |||
var tin = document.createElement('input'); tin.type = 'text'; tin.className = 'ic-incident-input ic-subq__input'; tin.placeholder = 'Attach to another page (exact title)'; | |||
var tbtn = document.createElement('button'); tbtn.className = 'ic-incident-actbtn'; tbtn.textContent = 'Attach'; | |||
tbtn.addEventListener('click', function () { var t = tin.value.trim(); if (!t) { msg.textContent = 'Enter a page title.'; return; } busy(); attachTo(t, done); }); | |||
row.appendChild(tin); row.appendChild(tbtn); wrap.appendChild(row); | |||
btn('Create unidentified agent', '', function () { | |||
var code = prompt('Code for the new unidentified agent (e.g. NY-0007):', ''); if (!code) { return; } | |||
code = code.replace(/[^A-Za-z0-9 .\-]/g, ''); var st = prompt('State (optional):', '') || ''; | |||
busy(); | |||
var title = 'ICE List:Unidentified agent - ' + code, stcat = st.trim() || 'No State Given'; | |||
var body = '{{UnknownAgent\n | code = ' + esc(code) + '\n | image = ' + (file ? file : 'Agent-placeholder.jpg') + '\n | description = Submitted via media form.' + (link ? ' Source: ' + esc(link) : '') + '\n | agency = Unknown\n | state = ' + esc(st) + '\n | seen = \n | status = Open\n | wanted = Standard\n}}\n<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n' + (link && VIDEO.test(link) ? '{{Embed|url=' + esc(link) + '}}\n' : '') + '{{MediaForAgent}}\n\n[[Category:Unidentified agents]][[Category:Standard unidentified agents]][[Category:Unidentified agents in ' + stcat + ']][[Category:Agents in ' + stcat + ']]' + (file ? '[[Category:Agents with photo]]' : '') + '\n'; | |||
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('Already exists: ' + title); } | |||
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create unidentified agent from submission' }); | |||
}).then(function () { return resolve('created [[' + title + ']]'); }).then(done).catch(fail); | |||
}); | |||
btn('Promote to incident', '', function () { | |||
var title = prompt('New incident page title (include date + location):', ''); | |||
if (!title) { return; } | |||
var corr = prompt('Corroborating source URL (independent evidence \u2014 required to promote a tip):', ''); | |||
if (!corr || !/^https?:\/\//.test(corr)) { msg.textContent = 'A corroborating source URL is required to promote a tip to an incident.'; return; } | |||
busy(); | |||
var media = file ? '[[File:' + file + '|thumb|360px|Submitted media]]\n' : ''; | |||
if (link && VIDEO.test(link)) { media += '{{Embed|url=' + esc(link) + '}}\n'; } | |||
var body = '{{Infobox incident\n| title = ' + esc(title) + '\n| status = Developing (promoted from a community tip)\n| verification = Developing. Originated as a community tip, corroborated by an independent source (below).\n| key_sources =\n* Tip: ' + esc(link || file || 'submitted media') + '\n* Corroboration: ' + esc(corr) + '\n}}\n\n' + "''Promoted from a community tip after independent corroboration. Records only what is sourced; will be updated as verified.''\n\n" + media + '\n{{MediaForIncident}}\n\n[[Category:Incidents]][[Category:Incidents involving ICE]]\n'; | |||
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + title + '" already exists.'); } | |||
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create incident from corroborated tip' }); | |||
}).then(function () { return resolve('promoted to [[' + title + ']]'); }).then(function () { | |||
msg.innerHTML = '\u2713 Created <a href="/index.php/' + encodeURIComponent(title.replace(/ /g, '_')) + '">' + title + '</a> \u2014 reloading\u2026'; setTimeout(function () { location.reload(); }, 1200); | |||
}).catch(fail); | |||
}); | |||
btn('Discard', '', function () { if (!confirm('Discard this submission as not usable?')) { return; } busy(); resolve('discarded (not usable)').then(done).catch(fail); }); | |||
wrap.appendChild(msg); | |||
}); | |||
}); | |||
/* Wanted posters: with/without photo filter (per-state pages) */ | |||
ready(function () { | |||
var bar = document.getElementById('ic-photo-filter'); | |||
if (!bar) { return; } | |||
var secs = [].slice.call(document.querySelectorAll('.ic-poster-section[data-photo]')); | |||
if (!secs.length) { return; } | |||
function hasTiles(sec) { return sec && sec.querySelector('.ic-poster-tile'); } | |||
secs.forEach(function (s) { if (!hasTiles(s)) { s.style.display = 'none'; } }); | |||
var live = secs.filter(hasTiles); | |||
var hasW = live.some(function (s) { return s.getAttribute('data-photo') === 'yes'; }); | |||
var hasN = live.some(function (s) { return s.getAttribute('data-photo') === 'no'; }); | |||
if (!(hasW && hasN)) { return; } | |||
var lbl = document.createElement('span'); lbl.className = 'ic-photo-lbl'; lbl.textContent = 'Show:'; bar.appendChild(lbl); | |||
var defs = [['all', 'All'], ['with', 'With photo'], ['needed', 'Photo needed']]; | |||
var btns = []; | |||
function apply(mode) { | |||
live.forEach(function (s) { var p = s.getAttribute('data-photo'); s.style.display = (mode === 'all' || (mode === 'with' && p === 'yes') || (mode === 'needed' && p === 'no')) ? '' : 'none'; }); | |||
btns.forEach(function (b) { b.classList.toggle('is-active', b.getAttribute('data-m') === mode); }); | |||
} | |||
defs.forEach(function (d) { | |||
var b = document.createElement('button'); b.className = 'ic-photo-btn'; b.setAttribute('data-m', d[0]); b.textContent = d[1]; | |||
b.addEventListener('click', function () { apply(d[0]); }); bar.appendChild(b); btns.push(b); | |||
}); | |||
apply('all'); | |||
}); | |||
/* Homepage: strip native title tooltips on interactive tiles */ | |||
ready(function () { | |||
if (mw.config.get('wgPageName') === 'Main_Page') { | |||
document.querySelectorAll('.ic-scenarios a, .ic-stat a, .ic-hero__actions a').forEach(function (a) { a.removeAttribute('title'); }); | |||
} | |||
}); | |||
/* Render video / social embeds (all users, client-side for privacy) */ | |||
ready(function () { | |||
var nodes = document.querySelectorAll('.ic-embed:not([data-done])'); | |||
if (!nodes.length) { return; } | |||
var needX = false, needIG = false, needTh = false, needTT = false; | |||
function ytId(u){ var m=u.match(/[?&]v=([\w-]{6,})/)||u.match(/youtu\.be\/([\w-]{6,})/)||u.match(/\/(?:shorts|embed|live)\/([\w-]{6,})/); return m?m[1]:''; } | |||
function vimeoId(u){ var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/); return m?m[1]:''; } | |||
function detect(u){ var h; try{ h=new URL(u).hostname.replace(/^www\./,'').toLowerCase(); }catch(e){ return ''; } | |||
if(h.indexOf('youtube')>-1||h==='youtu.be') return 'youtube'; | |||
if(h.indexOf('vimeo')>-1) return 'vimeo'; | |||
if(h.indexOf('instagram')>-1) return 'instagram'; | |||
if(h.indexOf('tiktok')>-1) return 'tiktok'; | |||
if(h.indexOf('facebook')>-1||h==='fb.watch') return 'facebook'; | |||
if(h==='x.com'||h.indexOf('twitter')>-1) return 'x'; | |||
if(h.indexOf('threads')>-1) return 'threads'; | |||
return ''; } | |||
nodes.forEach(function (el) { | |||
el.setAttribute('data-done', '1'); | |||
var id = el.getAttribute('data-id'), url = el.getAttribute('data-url'); | |||
var plat = (el.getAttribute('data-platform') || '').toLowerCase().trim(); | |||
if (!plat) { | |||
if (el.classList.contains('ic-embed-youtube')) { plat = 'youtube'; } | |||
else if (el.classList.contains('ic-embed-vimeo')) { plat = 'vimeo'; } | |||
else if (el.classList.contains('ic-embed-x')) { plat = 'x'; } | |||
else if (el.classList.contains('ic-embed-instagram')) { plat = 'instagram'; } | |||
else if (el.classList.contains('ic-embed-threads')) { plat = 'threads'; } | |||
} | |||
if (!plat && url) { plat = detect(url); } | |||
if (plat === 'youtube' && !id && url) { id = ytId(url); } | |||
if (plat === 'vimeo' && !id && url) { id = vimeoId(url); } | |||
if (plat === 'youtube' && id) { | |||
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen allow="encrypted-media; picture-in-picture" src="https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '"></iframe>'; | |||
} else if (plat === 'vimeo' && id) { | |||
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen src="https://player.vimeo.com/video/' + encodeURIComponent(id) + '"></iframe>'; | |||
} else if ((plat === 'x' || plat === 'twitter') && url) { | |||
el.innerHTML = '<blockquote class="twitter-tweet"><a href="' + url + '"></a></blockquote>'; needX = true; | |||
} else if (plat === 'instagram' && url) { | |||
el.innerHTML = '<blockquote class="instagram-media" data-instgrm-permalink="' + url + '" data-instgrm-version="14"></blockquote>'; needIG = true; | |||
} else if (plat === 'threads' && url) { | |||
el.innerHTML = '<blockquote class="text-post-media" data-text-post-permalink="' + url + '"></blockquote>'; needTh = true; | |||
} else if (plat === 'tiktok' && url) { | |||
el.innerHTML = '<blockquote class="tiktok-embed" cite="' + url + '" style="max-width:605px;min-width:325px;"><a href="' + url + '"></a></blockquote>'; needTT = true; | |||
} else if (plat === 'facebook' && url) { | |||
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen scrolling="no" frameborder="0" src="https://www.facebook.com/plugins/video.php?show_text=false&href=' + encodeURIComponent(url) + '"></iframe>'; | |||
} else if (url) { | |||
el.innerHTML = '<a class="ic-embed-link" href="' + url + '" target="_blank" rel="noopener">' + (el.textContent || 'View media') + ' ↗</a>'; | |||
} | |||
}); | |||
function load(src) { var sc = document.createElement('script'); sc.async = true; sc.src = src; document.body.appendChild(sc); } | |||
if (needX) { load('https://platform.twitter.com/widgets.js'); } | |||
if (needIG) { load('https://www.instagram.com/embed.js'); } | |||
if (needTh) { load('https://www.threads.net/embed.js'); } | |||
if (needTT) { load('https://www.tiktok.com/embed.js'); } | |||
}); | |||
/* Show the shareable accountability card on named-agent pages */ | |||
ready(function () { | |||
try { | |||
if (mw.config.get('wgNamespaceNumber') !== 0) { return; } | |||
if ((mw.config.get('wgCategories') || []).indexOf('Agents') === -1) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var infoImg = content.querySelector('.ic-agent-card__photo img, .infobox img'); | |||
var noPhoto = !infoImg || /placeholder|nopfp|no[-_]?pfp|noimage/i.test((infoImg.getAttribute('src') || '') + '|' + (infoImg.getAttribute('alt') || '')); | |||
if (noPhoto) { | |||
var pn = document.createElement('div'); pn.className = 'ic-photo-needed'; | |||
pn.innerHTML = '📷 <strong>We don\'t have a photo of this agent yet.</strong> Do you? <a href="/index.php/ICE_List:Submit_media_form">Submit a photo</a> to help identify and document them — your identity is not recorded.'; | |||
content.insertBefore(pn, content.firstChild); | |||
} | |||
var fn = 'Card-' + mw.config.get('wgPageName') + '.png'; | |||
var src = mw.util ? mw.util.getUrl('Special:Redirect/file/' + fn) : '/index.php/Special:Redirect/file/' + encodeURIComponent(fn); | |||
var wrap = document.createElement('div'); wrap.className = 'ic-agent-share'; | |||
wrap.innerHTML = '<div class="ic-agent-share-h">Share this agent</div>' | |||
+ '<a class="ic-agent-share-img" href="' + src + '" target="_blank" rel="noopener"><img alt="Accountability share card" loading="lazy"></a>' | |||
+ '<div class="ic-agent-share-c">Download and share this card. Recognize them, or have a clearer photo? <a href="/index.php/ICE_List:Submit_media_form">Submit info</a> — your identity is not recorded.</div>'; | |||
var img = wrap.querySelector('img'); | |||
img.onerror = function () { wrap.style.display = 'none'; }; | |||
img.src = src; | |||
content.appendChild(wrap); | |||
} catch (e) {} | |||
}); | |||
/* Wanted posters gallery: filter by state */ | |||
ready(function () { | |||
var bar = document.getElementById('ic-poster-filter'); | |||
if (!bar) { return; } | |||
var tiles = [].slice.call(document.querySelectorAll('.ic-poster-tile[data-state]')); | |||
if (!tiles.length) { return; } | |||
var counts = {}; | |||
tiles.forEach(function (t) { var st = (t.getAttribute('data-state') || '').trim(); if (st) { counts[st] = (counts[st] || 0) + 1; } }); | |||
var sel = document.createElement('select'); sel.className = 'ic-wanted-statesel'; | |||
var all = document.createElement('option'); all.value = ''; all.textContent = 'All states'; sel.appendChild(all); | |||
Object.keys(counts).sort().forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st + ' (' + counts[st] + ')'; sel.appendChild(o); }); | |||
var lab = document.createElement('span'); lab.className = 'ic-wanted-filter-label'; lab.textContent = 'Filter by state: '; | |||
bar.appendChild(lab); bar.appendChild(sel); | |||
sel.addEventListener('change', function () { | |||
var v = sel.value; | |||
tiles.forEach(function (t) { t.style.display = (!v || (t.getAttribute('data-state') || '').trim() === v) ? '' : 'none'; }); | |||
}); | |||
}); | |||
/* Most Wanted board: filter by state */ | |||
ready(function () { | |||
var bar = document.getElementById('ic-wanted-filter'); | |||
if (!bar) { return; } | |||
var cards = [].slice.call(document.querySelectorAll('.ic-wanted-card[data-state]')); | |||
if (!cards.length) { return; } | |||
var counts = {}; | |||
cards.forEach(function (c) { var st = (c.getAttribute('data-state') || '').trim(); if (st) { counts[st] = (counts[st] || 0) + 1; } }); | |||
var sel = document.createElement('select'); sel.className = 'ic-wanted-statesel'; | |||
var all = document.createElement('option'); all.value = ''; all.textContent = 'All states'; sel.appendChild(all); | |||
Object.keys(counts).sort().forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st + ' (' + counts[st] + ')'; sel.appendChild(o); }); | |||
var lab = document.createElement('span'); lab.className = 'ic-wanted-filter-label'; lab.textContent = 'Filter by state: '; | |||
bar.appendChild(lab); bar.appendChild(sel); | |||
function apply() { | |||
var v = sel.value; | |||
cards.forEach(function (c) { | |||
var wrap = c.closest('.ic-wanted-entry') || c; | |||
wrap.style.display = (!v || (c.getAttribute('data-state') || '').trim() === v) ? '' : 'none'; | |||
}); | |||
document.querySelectorAll('.ic-wanted-grid').forEach(function (g) { | |||
var any = [].slice.call(g.querySelectorAll('.ic-wanted-entry')).some(function (e) { return e.style.display !== 'none'; }); | |||
var h = g.previousElementSibling; | |||
}); | |||
} | |||
sel.addEventListener('change', apply); | |||
}); | |||
/* Add unidentified agent form (Most Wanted board, logged-in only) */ | |||
ready(function () { | |||
try { | |||
var box = document.getElementById('ic-addagent'); | |||
if (!box || !markMember()) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); box.innerHTML = ''; | |||
var form = document.createElement('div'); form.className = 'ic-addform'; | |||
var drop = document.createElement('div'); drop.className = 'ic-drop'; | |||
var hint = document.createElement('span'); hint.textContent = 'Drag & drop a photo here, or click to choose'; | |||
var fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none'; | |||
var preview = document.createElement('img'); preview.className = 'ic-drop-preview'; preview.style.display = 'none'; | |||
drop.appendChild(hint); drop.appendChild(fileInput); drop.appendChild(preview); | |||
var chosenFile = null; | |||
function setFile(f) { if (!f || f.type.indexOf('image/') !== 0) { return; } chosenFile = f; var r = new FileReader(); r.onload = function (e) { preview.src = e.target.result; preview.style.display = 'block'; hint.textContent = f.name; }; r.readAsDataURL(f); } | |||
drop.addEventListener('click', function () { fileInput.click(); }); | |||
fileInput.addEventListener('change', function () { setFile(fileInput.files[0]); }); | |||
['dragover', 'dragenter'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('drag'); }); }); | |||
['dragleave', 'drop'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('drag'); }); }); | |||
drop.addEventListener('drop', function (e) { if (e.dataTransfer.files[0]) { setFile(e.dataTransfer.files[0]); } }); | |||
form.appendChild(drop); | |||
function field(label, ph, ta) { | |||
var w = document.createElement('label'); w.className = 'ic-addrow'; w.appendChild(document.createTextNode(label)); | |||
var i = document.createElement(ta ? 'textarea' : 'input'); if (!ta) { i.type = 'text'; } i.placeholder = ph || ''; w.appendChild(i); form.appendChild(w); return i; | |||
} | |||
var codeI = field('Code (e.g. IL-0008)', 'required'); var descI = field('Description / what they did', '', true); | |||
var agencyI = field('Agency', 'ICE / CBP / Unknown'); var stateI = field('Location / state', ''); | |||
var srcI = field('Source or incident (optional)', 'link or [[Incident page]]'); | |||
var vidI = field('Video link (optional)', 'paste any social video link'); | |||
var mwRow = document.createElement('label'); mwRow.className = 'ic-addrow ic-addcheck'; | |||
var mwCb = document.createElement('input'); mwCb.type = 'checkbox'; mwRow.appendChild(mwCb); mwRow.appendChild(document.createTextNode(' Add to Most wanted')); form.appendChild(mwRow); | |||
var btn = document.createElement('button'); btn.className = 'uk-flag'; btn.textContent = 'Create entry'; | |||
var msg = document.createElement('div'); msg.className = 'ic-addmsg'; form.appendChild(btn); form.appendChild(msg); box.appendChild(form); | |||
btn.addEventListener('click', function () { | |||
var code = codeI.value.trim().replace(/[^A-Za-z0-9 .\-]/g, ''); | |||
if (!code) { msg.textContent = 'Enter a code.'; return; } | |||
var title = 'ICE List:Unidentified agent - ' + code, wanted = mwCb.checked ? 'Most wanted' : 'Standard'; | |||
btn.disabled = true; msg.textContent = 'Working…'; | |||
function esc(x) { return x.replace(/[|]/g, '|'); } | |||
function createPage(img) { | |||
var desc = descI.value.trim(); if (srcI.value.trim()) { desc += (desc ? '. ' : '') + 'Source: ' + srcI.value.trim(); } | |||
var body = '{{UnknownAgent\n | code = ' + code + '\n | image = ' + (img || 'Agent-placeholder.jpg') + | |||
'\n | description = ' + esc(desc) + '\n | agency = ' + (esc(agencyI.value.trim()) || 'Unknown') + | |||
'\n | state = ' + esc(stateI.value.trim()) + '\n | seen = \n | status = Open\n | wanted = ' + wanted + '\n}}\n' + | |||
'<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n' + (vidI.value.trim() ? '{{Embed|url=' + vidI.value.trim() + '}}\n' : '') + '{{MediaForAgent}}\n' + | |||
'[[Category:Unidentified agents]][[Category:' + wanted + ' unidentified agents]]\n'; | |||
api.create(title, { summary: 'Add unidentified agent (board form)' }, body).then(function () { | |||
msg.innerHTML = 'Created. <a href="/index.php/' + encodeURIComponent(title.replace(/ /g, '_')) + '">Open ' + code + ' →</a>'; btn.disabled = false; | |||
form.querySelectorAll('input,textarea').forEach(function (el) { if (el.type !== 'file') { el.value = ''; } el.checked = false; }); | |||
preview.style.display = 'none'; chosenFile = null; hint.textContent = 'Drag & drop a photo here, or click to choose'; | |||
}).catch(function (e) { msg.textContent = (e === 'articleexists') ? 'An entry with that code already exists.' : ('Create failed: ' + e); btn.disabled = false; }); | |||
} | |||
if (chosenFile) { | |||
var ext = (chosenFile.name.split('.').pop() || 'jpg').toLowerCase(); if (ext.length > 4 || !ext) { ext = 'jpg'; } | |||
api.upload(chosenFile, { filename: code + '.' + ext, comment: 'Unidentified agent photo', text: '[[Category:Unidentified agents]]', ignorewarnings: true }) | |||
.then(function () { createPage(code + '.' + ext); }) | |||
.catch(function (e) { msg.textContent = 'Photo upload unavailable (' + e + ') — creating entry without it…'; createPage(null); }); | |||
} else { createPage(null); } | |||
}); | |||
}); | |||
} catch (e) {} | |||
}); | |||
/* Unidentified agent marking panel (Most Wanted board) */ | |||
ready(function () { | |||
try { | |||
if (!markMember()) { return; } | |||
if (mw.config.get('wgAction') !== 'view') { return; } | |||
if ((mw.config.get('wgCategories') || []).indexOf('Unidentified agents') === -1) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(), page = mw.config.get('wgPageName'); | |||
var FIELDS = [['description', 'Description'], ['agency', 'Agency'], ['state', 'Location'], ['status', 'Status']]; | |||
var panel = document.createElement('div'); panel.className = 'vol-panel uk-panel'; | |||
panel.innerHTML = '<h4>Unidentified agent <span class="vol-status">loading…</span></h4>'; | |||
var status = panel.querySelector('.vol-status'), inputs = {}; | |||
var wl = document.createElement('label'); wl.className = 'uk-row'; | |||
var wcb = document.createElement('input'); wcb.type = 'checkbox'; wcb.disabled = true; | |||
wl.appendChild(wcb); wl.appendChild(document.createTextNode(' Most wanted (feature at top of board)')); panel.appendChild(wl); | |||
FIELDS.forEach(function (f) { | |||
var r = document.createElement('label'); r.className = 'uk-row'; r.appendChild(document.createTextNode(f[1] + ': ')); | |||
var i = document.createElement('input'); i.type = 'text'; i.disabled = true; r.appendChild(i); panel.appendChild(r); inputs[f[0]] = i; | |||
}); | |||
var btn = document.createElement('button'); btn.className = 'uk-flag'; btn.textContent = 'Save'; btn.disabled = true; panel.appendChild(btn); | |||
var vrow = document.createElement('div'); vrow.className = 'uk-row'; vrow.style.marginTop = '.5rem'; | |||
vrow.appendChild(document.createTextNode('Add video (paste any social link): ')); | |||
var vin = document.createElement('input'); vin.type = 'text'; vin.placeholder = 'https://…'; vin.disabled = true; vin.style.minWidth = '16rem'; | |||
var vbtn = document.createElement('button'); vbtn.className = 'uk-flag'; vbtn.textContent = 'Add video'; vbtn.disabled = true; vbtn.style.marginLeft = '.3rem'; | |||
vrow.appendChild(vin); vrow.appendChild(vbtn); panel.appendChild(vrow); | |||
vbtn.addEventListener('click', function () { | |||
var u = vin.value.trim(); | |||
if (!/^https?:\/\//.test(u)) { status.textContent = 'enter a full URL'; return; } | |||
status.textContent = 'adding…'; vbtn.disabled = true; | |||
var w = wt, emb = '{{Embed|url=' + u + '}}', idx = w.search(/\n\[\[Category:/); | |||
w = idx >= 0 ? (w.slice(0, idx) + '\n' + emb + w.slice(idx)) : (w.replace(/\s*$/, '') + '\n' + emb + '\n'); | |||
api.postWithToken('csrf', { action: 'edit', title: page, text: w, summary: 'Add video embed' }) | |||
.then(function () { wt = w; vin.value = ''; status.textContent = 'video added ✓ — reload to view'; vbtn.disabled = false; }) | |||
.catch(function () { status.textContent = 'add failed (blocked link?)'; vbtn.disabled = false; }); | |||
}); | |||
content.insertBefore(panel, content.firstChild); | |||
var wt = ''; | |||
function gv(k) { var m = wt.match(new RegExp('\\|\\s*' + k + '\\s*=\\s*([^\\n|}]*)')); return m ? m[1].trim() : ''; } | |||
function setP(w, k, v) { var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^\\n|}]*'); if (re.test(w)) { return w.replace(re, '$1 ' + v.replace(/\$/g, '$$$$')); } return w.replace(/(\{\{UnknownAgent[\s\S]*?)(\n\}\})/, '$1\n | ' + k + ' = ' + v + '$2'); } | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) { | |||
try { wt = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wt = ''; } | |||
wcb.checked = /Category:Most wanted unidentified agents/.test(wt) || gv('wanted') === 'Most wanted'; | |||
FIELDS.forEach(function (f) { inputs[f[0]].value = gv(f[0]); inputs[f[0]].disabled = false; }); | |||
wcb.disabled = false; btn.disabled = false; vin.disabled = false; vbtn.disabled = false; status.textContent = ''; | |||
}); | |||
btn.addEventListener('click', function () { | |||
status.textContent = 'saving…'; btn.disabled = true; | |||
var w = wt, wanted = wcb.checked ? 'Most wanted' : 'Standard'; | |||
w = setP(w, 'wanted', wanted); | |||
FIELDS.forEach(function (f) { w = setP(w, f[0], inputs[f[0]].value.trim()); }); | |||
if (/Category:(?:Most wanted|Standard) unidentified agents/.test(w)) { | |||
w = w.replace(/\[\[Category:(?:Most wanted|Standard) unidentified agents\]\]/, '[[Category:' + wanted + ' unidentified agents]]'); | |||
} else { | |||
w = w.replace(/(\[\[Category:Unidentified agents\]\])/, '$1[[Category:' + wanted + ' unidentified agents]]'); | |||
} | |||
api.postWithToken('csrf', { action: 'edit', title: page, text: w, summary: 'Mark unidentified agent (' + wanted + ')' }) | |||
.then(function () { wt = w; status.textContent = 'saved ✓ — reload to update'; btn.disabled = false; }) | |||
.catch(function () { status.textContent = 'save failed'; btn.disabled = false; }); | |||
}); | |||
}); | |||
} catch (e) {} | |||
}); | |||
/* Volunteer media tagging panel (Cargo Media items) */ | |||
ready(function () { | |||
try { | |||
if (!markMember()) { return; } | |||
if (mw.config.get('wgAction') !== 'view') { return; } | |||
if ((mw.config.get('wgCategories') || []).indexOf('Media') === -1) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(), page = mw.config.get('wgPageName'); | |||
var FIELDS = [['agents','Agents (separate with ;)'],['incidents','Incidents (separate with ;)'],['state','State'],['agency','Agency']]; | |||
var STATUS = ['Unverified','Pending','Verified']; | |||
var panel = document.createElement('div'); panel.className = 'vol-panel'; | |||
panel.innerHTML = '<h4>Tag media <span class="vol-status">loading…</span></h4>'; | |||
var status = panel.querySelector('.vol-status'), inputs = {}; | |||
FIELDS.forEach(function (f) { | |||
var row = document.createElement('label'); row.className = 'vol-tag-row'; | |||
row.appendChild(document.createTextNode(f[1] + ': ')); | |||
var inp = document.createElement('input'); inp.type = 'text'; inp.disabled = true; | |||
row.appendChild(inp); panel.appendChild(row); inputs[f[0]] = inp; | |||
}); | |||
var srow = document.createElement('label'); srow.className = 'vol-tag-row'; | |||
srow.appendChild(document.createTextNode('Status: ')); | |||
var sel = document.createElement('select'); sel.disabled = true; | |||
STATUS.forEach(function (v) { var o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); }); | |||
srow.appendChild(sel); panel.appendChild(srow); inputs.status = sel; | |||
var btn = document.createElement('button'); btn.textContent = 'Save tags'; btn.disabled = true; btn.className = 'vol-tag-save'; | |||
panel.appendChild(btn); | |||
content.insertBefore(panel, content.firstChild); | |||
var wikitext = ''; | |||
function getVal(k) { var m = wikitext.match(new RegExp('\\|\\s*' + k + '\\s*=\\s*([^\\n|}]*)')); return m ? m[1].trim() : ''; } | |||
function setParam(wt, k, v) { | |||
var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^\\n|}]*'); | |||
if (re.test(wt)) { return wt.replace(re, '$1 ' + v.replace(/\$/g, '$$$$')); } | |||
return wt.replace(/(\{\{Media[\s\S]*?)(\n\}\})/, '$1\n | ' + k + ' = ' + v + '$2'); | |||
} | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) { | |||
try { wikitext = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wikitext = ''; } | |||
if (wikitext.indexOf('{{Media') === -1) { panel.parentNode.removeChild(panel); return; } | |||
FIELDS.forEach(function (f) { inputs[f[0]].value = getVal(f[0]); inputs[f[0]].disabled = false; }); | |||
sel.value = getVal('status') || 'Unverified'; sel.disabled = false; btn.disabled = false; status.textContent = ''; | |||
}); | |||
btn.addEventListener('click', function () { | |||
status.textContent = 'saving…'; btn.disabled = true; | |||
var wt = wikitext; | |||
FIELDS.forEach(function (f) { wt = setParam(wt, f[0], inputs[f[0]].value.trim()); }); | |||
wt = setParam(wt, 'status', sel.value); | |||
api.postWithToken('csrf', { action: 'edit', title: page, text: wt, summary: 'Tag media (agents/incidents/state/status)' }).then(function () { | |||
wikitext = wt; status.textContent = 'saved ✓ — reload to update embeds'; btn.disabled = false; | |||
}).catch(function () { status.textContent = 'save failed'; btn.disabled = false; }); | |||
}); | |||
}); | |||
} catch (e) {} | |||
}); | |||
/* Volunteer incident marking + stage tool */ | |||
ready(function () { | |||
if (!markMember()) { return; } | |||
if (mw.config.get('wgAction') !== 'view') { return; } | |||
var cats = mw.config.get('wgCategories') || []; | |||
if (cats.indexOf('Incidents') === -1) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var STAGES = ['', 'Intake', 'OSINT', 'Detailing', 'Verification', 'Writing', 'Published']; | |||
var MARKS = ['Agents recorded', 'Plates recorded', 'Faces captured', 'Video secured', 'Location confirmed', 'Witnesses identified', 'Needs review', 'Reviewed']; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
var page = mw.config.get('wgPageName'); | |||
var panel = document.createElement('div'); panel.className = 'vol-panel'; | |||
panel.innerHTML = '<h4>Volunteer triage <span class="vol-status">loading…</span></h4>'; | |||
var status = panel.querySelector('.vol-status'); | |||
var stageRow = document.createElement('div'); stageRow.className = 'vol-stage-row'; | |||
stageRow.appendChild(document.createTextNode('Stage: ')); | |||
var stageSel = document.createElement('select'); stageSel.disabled = true; | |||
STAGES.forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st || '(unstaged)'; stageSel.appendChild(o); }); | |||
stageRow.appendChild(stageSel); panel.appendChild(stageRow); | |||
var boxes = {}; | |||
MARKS.forEach(function (m) { | |||
var lab = document.createElement('label'); var cb = document.createElement('input'); cb.type = 'checkbox'; cb.disabled = true; | |||
lab.appendChild(cb); lab.appendChild(document.createTextNode(' ' + m)); panel.appendChild(lab); boxes[m] = cb; | |||
}); | |||
content.insertBefore(panel, content.firstChild); | |||
var wikitext = ''; | |||
function parseMarks(wt) { var re = /\{\{\s*Mark\s*\|\s*([^}|]+?)\s*\}\}/g, mm, set = {}; while ((mm = re.exec(wt))) { set[mm[1].trim()] = true; } return set; } | |||
function parseStage(wt) { var m = wt.match(/\{\{\s*Stage\s*\|\s*([^}|]+?)\s*\}\}/); return m ? m[1].trim() : ''; } | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) { | |||
try { wikitext = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wikitext = ''; } | |||
var set = parseMarks(wikitext); | |||
MARKS.forEach(function (m) { boxes[m].checked = !!set[m]; boxes[m].disabled = false; }); | |||
stageSel.value = parseStage(wikitext); stageSel.disabled = false; | |||
status.textContent = ''; | |||
}); | |||
function save() { | |||
status.textContent = 'saving…'; | |||
stageSel.disabled = true; MARKS.forEach(function (m) { boxes[m].disabled = true; }); | |||
var stg = stageSel.value; | |||
var chosen = MARKS.filter(function (m) { return boxes[m].checked; }); | |||
var inner = (stg ? '{{Stage|' + stg + '}}' : '') + chosen.map(function (m) { return '{{Mark|' + m + '}}'; }).join(''); | |||
var region = '<div class="vol-only vol-marks"><!--VOLMARKS-->' + inner + '<!--/VOLMARKS--></div>'; | |||
var done = false, wt = wikitext; | |||
wt = wikitext.replace(/<div class="vol-only vol-marks"><!--VOLMARKS-->[\s\S]*?<!--\/VOLMARKS--><\/div>/, function () { done = true; return region; }); | |||
if (!done) { wt = wikitext.replace(/<!--VOLMARKS-->[\s\S]*?<!--\/VOLMARKS-->/, function () { done = true; return '<!--VOLMARKS-->' + inner + '<!--/VOLMARKS-->'; }); } | |||
if (!done) { var idx = wt.search(/\n\[\[Category:/); if (idx >= 0) { wt = wt.slice(0, idx) + '\n' + region + wt.slice(idx); } else { wt = wt.replace(/\s*$/, '') + '\n\n' + region + '\n'; } } | |||
api.postWithToken('csrf', { action: 'edit', title: page, text: wt, summary: 'Update volunteer triage / stage' }).then(function () { | |||
wikitext = wt; status.textContent = 'saved ✓'; | |||
stageSel.disabled = false; MARKS.forEach(function (m) { boxes[m].disabled = false; }); | |||
setTimeout(function () { status.textContent = ''; }, 1500); | |||
}).catch(function () { status.textContent = 'save failed'; stageSel.disabled = false; MARKS.forEach(function (m) { boxes[m].disabled = false; }); }); | |||
} | |||
stageSel.addEventListener('change', save); | |||
MARKS.forEach(function (m) { boxes[m].addEventListener('change', save); }); | |||
}); | |||
}); | |||
/* Culture index: featured spotlight + tabs + search over poster tiles */ | |||
ready(function () { | |||
var app = document.getElementById('ic-cult-app'); | |||
if (!app) { return; } | |||
var grid = app.querySelector('.ic-cult-grid'); | |||
if (!grid) { return; } | |||
var cards = [].slice.call(grid.querySelectorAll('.ic-cult-card')); | |||
if (!cards.length) { return; } | |||
var LABEL = { documentary: 'Documentaries', film: 'Film & TV', music: 'Music', book: 'Books', podcast: 'Podcasts' }; | |||
var GROUP = { books: ['book', 'podcast'] }; | |||
var byType = {}; | |||
function tx(el, sel) { var n = el.querySelector(sel); return n ? n.textContent.trim() : ''; } | |||
cards.forEach(function (c) { | |||
var ty = c.getAttribute('data-type') || 'other'; | |||
byType[ty] = (byType[ty] || 0) + 1; | |||
c.setAttribute('data-s', (tx(c, '.ic-cult-mtitle') + ' ' + tx(c, '.ic-cult-mby') + ' ' + tx(c, '.ic-cult-ov')).toLowerCase()); | |||
var lk = c.getAttribute('data-link'); | |||
if (lk) { c.addEventListener('click', function (e) { if (e.target.closest('a')) { return; } window.open(lk, '_blank', 'noopener'); }); } | |||
}); | |||
var fc = grid.querySelector('.ic-cult-card[data-featured="1"]') || cards[0]; | |||
var fty = fc.getAttribute('data-type') || '', flk = fc.getAttribute('data-link') || ''; | |||
var fEl = document.createElement('section'); fEl.className = 'ic-cult-featured'; | |||
var wrap = document.createElement('div'); wrap.className = 'ic-cult-fcovwrap'; | |||
var cov = fc.querySelector('.ic-cult-cov'); if (cov) { wrap.appendChild(cov.cloneNode(true)); } | |||
var fx = document.createElement('div'); fx.className = 'ic-cult-fx'; | |||
fx.innerHTML = '<div class="ic-cult-feyebrow">Featured</div><div class="ic-cult-ftitle"></div><div class="ic-cult-fmeta"></div><p class="ic-cult-fblurb"></p>'; | |||
fx.querySelector('.ic-cult-ftitle').textContent = tx(fc, '.ic-cult-mtitle'); | |||
fx.querySelector('.ic-cult-fmeta').textContent = (LABEL[fty] || '') + ' \u00b7 ' + (fc.getAttribute('data-year') || '') + ' \u00b7 ' + tx(fc, '.ic-cult-mby'); | |||
fx.querySelector('.ic-cult-fblurb').textContent = tx(fc, '.ic-cult-ov'); | |||
if (flk) { var a = document.createElement('a'); a.className = 'ic-cult-fcta'; a.href = flk; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Where to find it \u2192'; fx.appendChild(a); } | |||
fEl.appendChild(wrap); fEl.appendChild(fx); | |||
var bar = document.createElement('div'); bar.className = 'ic-cult-bar'; | |||
var tabsEl = document.createElement('div'); tabsEl.className = 'ic-cult-tabs'; | |||
var search = document.createElement('input'); search.type = 'search'; search.className = 'ic-cult-search'; search.placeholder = 'Search titles, creators\u2026'; | |||
bar.appendChild(tabsEl); bar.appendChild(search); | |||
var TABS = [['all', 'All'], ['documentary', 'Documentaries'], ['film', 'Film & TV'], ['music', 'Music'], ['books', 'Books & Podcasts']] | |||
.filter(function (x) { return x[0] === 'all' || (GROUP[x[0]] ? GROUP[x[0]].some(function (g) { return byType[g]; }) : byType[x[0]]); }); | |||
var active = 'all', query = '', chips = []; | |||
function inTab(ty2) { return active === 'all' || (GROUP[active] ? GROUP[active].indexOf(ty2) >= 0 : ty2 === active); } | |||
function cnt(k) { return cards.filter(function (c) { var ty2 = c.getAttribute('data-type'); return k === 'all' ? true : (GROUP[k] ? GROUP[k].indexOf(ty2) >= 0 : ty2 === k); }).length; } | |||
function apply() { | |||
cards.forEach(function (c) { var ok = inTab(c.getAttribute('data-type')) && (!query || (c.getAttribute('data-s') || '').indexOf(query) >= 0); c.style.display = ok ? '' : 'none'; }); | |||
chips.forEach(function (ch) { ch.setAttribute('aria-selected', ch.getAttribute('data-f') === active); }); | |||
} | |||
TABS.forEach(function (x) { | |||
var b = document.createElement('button'); b.className = 'ic-cult-tab'; b.setAttribute('data-f', x[0]); b.setAttribute('aria-selected', x[0] === active); | |||
b.appendChild(document.createTextNode(x[1] + ' ')); | |||
var n = document.createElement('span'); n.className = 'ic-cult-tn'; n.textContent = cnt(x[0]); b.appendChild(n); | |||
b.addEventListener('click', function () { active = x[0]; apply(); }); | |||
tabsEl.appendChild(b); chips.push(b); | |||
}); | |||
search.addEventListener('input', function () { query = search.value.trim().toLowerCase(); apply(); }); | |||
app.insertBefore(fEl, grid); app.insertBefore(bar, grid); apply(); | |||
}); | |||
/* Culture: recommend-a-title form -> gated review queue */ | |||
ready(function () { | |||
var box = document.getElementById('ic-rec-form'); | |||
if (!box) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
var user = mw.config.get('wgUserName'); | |||
if (!user) { | |||
box.innerHTML = '<p><em>Please <a href="/index.php?title=Special:UserLogin&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">log in</a> to recommend a title. New here? See the <a href="/index.php/ICE_List:Contribute">Contribute</a> page.</em></p>'; | |||
return; | |||
} | |||
function fld(label, el) { var w = document.createElement('label'); w.className = 'ic-rec-field'; var s = document.createElement('span'); s.textContent = label; w.appendChild(s); w.appendChild(el); return w; } | |||
function inp(ph) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph; return i; } | |||
var sel = document.createElement('select'); | |||
['Documentary', 'Film & TV', 'Music', 'Book', 'Podcast'].forEach(function (o) { var op = document.createElement('option'); op.textContent = o; sel.appendChild(op); }); | |||
var title = inp('e.g. El Norte'), year = inp('e.g. 1983'), creator = inp('Director / artist / author'), link = inp('Where to watch / listen / read (URL)'); | |||
var blurb = document.createElement('textarea'); blurb.rows = 3; blurb.placeholder = 'One sentence: what it is and why it belongs.'; | |||
var btn = document.createElement('button'); btn.className = 'ic-cult-fcta'; btn.textContent = 'Submit recommendation'; | |||
var msg = document.createElement('div'); msg.className = 'ic-rec-msg'; | |||
[fld('Type', sel), fld('Title', title), fld('Year', year), fld('Creator', creator), fld('Link', link), fld('Why it belongs', blurb)].forEach(function (f) { box.appendChild(f); }); | |||
box.appendChild(btn); box.appendChild(msg); | |||
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); } | |||
btn.addEventListener('click', function () { | |||
if (!title.value.trim()) { msg.textContent = 'Please enter a title.'; return; } | |||
btn.disabled = true; msg.textContent = 'Submitting…'; | |||
var id = 'cult-' + Date.now().toString(36) + Math.floor(Math.random() * 10000); | |||
var pn = 'ICE List:Submission - ' + id; | |||
var body = "'''Culture recommendation''' (submitted by [[User:" + user + "]]) — review and, if suitable, add to [[ICE List:Culture]].\n\n" + | |||
"* '''Type:''' " + esc(sel.value) + "\n* '''Title:''' " + esc(title.value) + "\n* '''Year:''' " + esc(year.value) + | |||
"\n* '''Creator:''' " + esc(creator.value) + "\n* '''Link:''' <nowiki>" + esc(link.value) + "</nowiki>\n* '''Why it belongs:''' " + esc(blurb.value) + "\n\n" + | |||
"[[Cat" + "egory:Culture recommendations]]\n"; | |||
api.postWithToken('csrf', { action: 'edit', title: pn, text: body, createonly: 1, summary: 'Culture recommendation via form' }) | |||
.then(function () { box.innerHTML = '<p><strong>✓ Thank you.</strong> Your recommendation has gone to the review queue and will appear on the library once an admin approves it.</p>'; }) | |||
.catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
}); | |||
}); | |||
/* Profile "My desk": dashboard — stats, tools, pull-a-task buttons */ | |||
ready(function () { | |||
var user = mw.config.get('wgUserName'); | |||
if (!user) { return; } | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'User:' + user) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
if (!/^ICEListAdmin[0-9]+$/.test(user) && !['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var panel = document.createElement('div'); panel.className = 'ic-desk'; | |||
panel.innerHTML = | |||
'<div class="ic-desk__kicker">My desk</div>' + | |||
'<div class="ic-desk__stats"></div>' + | |||
'<div class="ic-desk__tools"></div>' + | |||
'<div class="ic-desk__pulls"><div class="ic-desk__pullkick">Pull something to work on</div><div class="ic-desk__pullbtns"><span class="ic-desk__loading">Loading your queues…</span></div><div class="ic-desk__pullresult"></div></div>'; | |||
content.insertBefore(panel, content.firstChild); | |||
if (user === 'ICEListAdmin6') { | |||
var cs = document.createElement('div'); cs.className = 'ic-desk__culture'; | |||
cs.innerHTML = '<div class="ic-desk__kicker">Culture recommendations</div><div id="ic-culture-inbox">Loading…</div>'; | |||
panel.appendChild(cs); | |||
} | |||
var toolsEl = panel.querySelector('.ic-desk__tools'); | |||
[['Review hub', '/index.php/ICE_List:Review'], ['Pending changes', '/index.php/Special:PendingChanges'], ['Unreviewed pages', '/index.php/Special:UnreviewedPages'], ['Submissions queue', '/index.php/ICE_List:Submissions']].forEach(function (l) { | |||
var sp = document.createElement('span'); sp.className = 'ic-hero__btn'; var a = document.createElement('a'); a.href = l[1]; a.target = '_blank'; a.rel = 'noopener'; a.textContent = l[0]; sp.appendChild(a); toolsEl.appendChild(sp); | |||
}); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function unrevList() { return api.get({ action: 'query', list: 'unreviewedpages', urnamespace: 0, urlimit: 500, formatversion: 2 }).then(function (d) { return ((d.query && d.query.unreviewedpages) || []).map(function (p) { return p.title; }); }).catch(function () { return []; }); } | |||
function catList(cat) { return api.get({ action: 'query', list: 'categorymembers', cmtitle: cat, cmnamespace: 0, cmlimit: 200, formatversion: 2 }).then(function (d) { return ((d.query && d.query.categorymembers) || []).map(function (p) { return p.title; }); }).catch(function () { return []; }); } | |||
var statsEl = panel.querySelector('.ic-desk__stats'), btnsEl = panel.querySelector('.ic-desk__pullbtns'), resEl = panel.querySelector('.ic-desk__pullresult'); | |||
function stat(n, label, href, red) { var d = document.createElement('div'); d.className = 'ic-stat' + (red ? ' ic-stat--red' : ''); d.innerHTML = '<a href="' + href + '" target="_blank" rel="noopener"><span class="n">' + n + '</span><span class="l">' + label + '</span></a>'; statsEl.appendChild(d); } | |||
function rnd(a) { return a[Math.floor(Math.random() * a.length)]; } | |||
function esc(x) { return x.replace(/</g, '<'); } | |||
function url(t) { return '/index.php/' + encodeURIComponent(t.replace(/ /g, '_')); } | |||
function render(label, t, href, red, titles, review) { | |||
resEl.innerHTML = '<div class="ic-desk__task' + (red ? ' ic-desk__task--red' : '') + '">' + | |||
'<div class="ic-desk__taskkick">' + label + '</div>' + | |||
'<a class="ic-desk__title" href="' + href + '" target="_blank" rel="noopener">' + esc(t) + '</a>' + | |||
'<div class="ic-desk__act"><span class="ic-hero__btn ic-hero__btn--primary"><a href="' + href + '" target="_blank" rel="noopener">' + (review ? 'Review it →' : 'Open →') + '</a></span>' + | |||
'<button class="ic-desk__another" type="button">Pull another</button></div></div>'; | |||
resEl.querySelector('.ic-desk__another').addEventListener('click', function () { showResult(label, titles, red, review); }); | |||
} | |||
function showResult(label, titles, red, review) { | |||
if (!titles.length) { resEl.innerHTML = '<div class="ic-desk__task"><div class="ic-desk__taskkick">' + label + '</div>Nothing in this queue right now.</div>'; return; } | |||
var t = rnd(titles); | |||
if (review) { | |||
api.get({ action: 'query', prop: 'flagged', titles: t, formatversion: 2 }).then(function (d) { | |||
var p = d.query.pages[0], st = p.flagged && p.flagged.stable_revid; | |||
var href = st ? ('/index.php?title=' + encodeURIComponent(t.replace(/ /g, '_')) + '&diff=cur&oldid=' + st) : (url(t) + '?stable=0'); | |||
render(label, t, href, red, titles, review); | |||
}).catch(function () { render(label, t, url(t), red, titles, review); }); | |||
} else { | |||
render(label, t, url(t), red, titles, review); | |||
} | |||
} | |||
function addBtn(label, titles, red, review) { | |||
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary'; | |||
var a = document.createElement('a'); a.href = '#'; a.textContent = label + ' →'; | |||
a.addEventListener('click', function (e) { e.preventDefault(); showResult(label, titles, red, review); }); | |||
sp.appendChild(a); btnsEl.appendChild(sp); | |||
} | |||
Promise.all([unrevList(), catList('Category:Submission review'), catList('Category:Agents needing a photo')]).then(function (res) { | |||
var unrev = res[0], subs = res[1].filter(function (t) { return t.indexOf('ICE List:Submission - cult-') !== 0; }), photos = res[2]; | |||
stat(unrev.length >= 500 ? '500+' : unrev.length, 'Pending review', '/index.php/Special:UnreviewedPages', true); | |||
stat(subs.length, 'Submissions to route', '/index.php/ICE_List:Submissions', false); | |||
stat(photos.length, 'Photos needed', '/index.php/Category:Agents_needing_a_photo', false); | |||
btnsEl.innerHTML = ''; | |||
addBtn('Review a page', unrev, true, true); | |||
addBtn('Route a submission', subs, false, false); | |||
addBtn('Photo an agent', photos, false, false); | |||
}).catch(function () { btnsEl.innerHTML = '<em>Could not load your queues.</em>'; }); | |||
}); | |||
}); | |||
/* Culture inbox: one-click review -> add to library (ICEListAdmin6) */ | |||
ready(function () { | |||
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { return; } | |||
var box = document.getElementById('ic-culture-inbox'); | |||
if (!box) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function field(txt, label) { | |||
var lines = txt.split('\n'), pre = "* '''" + label + ":'''"; | |||
for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf(pre) === 0) { return lines[i].slice(pre.length).replace(/<\/?nowiki>/g, '').trim(); } } | |||
return ''; | |||
} | |||
function esc(x) { return (x || '').replace(/[|\[\]{}]/g, '').trim(); } | |||
api.get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Culture recommendations', cmlimit: 100, formatversion: 2 }).then(function (d) { | |||
var mems = ((d.query && d.query.categorymembers) || []).filter(function (p) { return p.ns === 0 && p.title.indexOf('ICE List:Submission - cult-') === 0; }); | |||
box.innerHTML = ''; | |||
if (!mems.length) { box.innerHTML = '<p><em>No pending recommendations.</em></p>'; return; } | |||
mems.forEach(function (p) { renderCard(p.title); }); | |||
}); | |||
function renderCard(title) { | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) { | |||
var pg = d.query.pages[0]; if (!pg.revisions) { return; } | |||
var txt = pg.revisions[0].slots.main.content; | |||
var sm = txt.indexOf('[[User:'), sub = ''; | |||
if (sm >= 0) { sub = txt.slice(sm + 7, txt.indexOf(']]', sm)).split('|')[0]; } | |||
var rec = { type: field(txt, 'Type'), title: field(txt, 'Title'), year: field(txt, 'Year'), creator: field(txt, 'Creator'), link: field(txt, 'Link'), blurb: field(txt, 'Why it belongs'), sub: sub }; | |||
var card = document.createElement('div'); card.className = 'ic-inbox-card'; | |||
card.innerHTML = '<div class="ic-inbox-h"><strong>' + esc(rec.title) + '</strong> <span class="ic-inbox-meta">' + esc(rec.type) + ' · ' + esc(rec.year) + ' · ' + esc(rec.creator) + '</span></div>' + | |||
'<div class="ic-inbox-blurb">' + esc(rec.blurb) + '</div>' + | |||
'<div class="ic-inbox-link">' + (rec.link ? '<a href="' + esc(rec.link) + '" target="_blank" rel="noopener">' + esc(rec.link) + '</a>' : '(no link)') + '</div>' + | |||
'<div class="ic-inbox-sub">from ' + esc(rec.sub) + '</div>'; | |||
var actions = document.createElement('div'); actions.className = 'ic-inbox-actions'; | |||
var msg = document.createElement('span'); msg.className = 'ic-inbox-msg'; | |||
var addb = document.createElement('button'); addb.className = 'ic-inbox-add'; addb.textContent = 'Add to library'; | |||
var disb = document.createElement('button'); disb.className = 'ic-inbox-discard'; disb.textContent = 'Discard'; | |||
addb.addEventListener('click', function () { | |||
addb.disabled = true; disb.disabled = true; msg.textContent = 'Adding…'; | |||
addToLibrary(rec).then(function () { return resolve(title, 'added to [[ICE List:Culture]]'); }).then(function () { | |||
msg.textContent = '✓ Added.'; card.style.opacity = 0.5; setTimeout(function () { card.remove(); }, 700); | |||
}).catch(function (e) { addb.disabled = false; disb.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
disb.addEventListener('click', function () { | |||
if (!confirm('Discard this recommendation?')) { return; } | |||
addb.disabled = true; disb.disabled = true; msg.textContent = '…'; | |||
resolve(title, 'discarded').then(function () { card.style.opacity = 0.5; setTimeout(function () { card.remove(); }, 500); }).catch(function () { msg.textContent = 'Error'; }); | |||
}); | |||
actions.appendChild(addb); actions.appendChild(disb); actions.appendChild(msg); card.appendChild(actions); | |||
box.appendChild(card); | |||
}); | |||
} | |||
function addToLibrary(rec) { | |||
var pick = '{{Culture pick|type=' + esc(rec.type) + '|title=' + esc(rec.title) + '|year=' + esc(rec.year) + '|creator=' + esc(rec.creator) + '|blurb=' + esc(rec.blurb) + '|link=' + esc(rec.link) + '}}'; | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: 'ICE List:Culture', formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
var anchor = '\n</div>\n</div>', i = txt.indexOf(anchor); | |||
if (i < 0) { throw new Error('library grid not found'); } | |||
var nt = txt.slice(0, i) + '\n' + pick + txt.slice(i); | |||
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Culture', text: nt, summary: 'Add culture pick from inbox: ' + esc(rec.title) }); | |||
}); | |||
} | |||
function resolve(title, note) { | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
txt = txt.replace('[[Cat' + 'egory:Culture recommendations]]', '[[Cat' + 'egory:Resolved culture recommendations]]'); | |||
txt = "'''Resolved:''' " + note + '\n\n' + txt; | |||
return api.postWithToken('csrf', { action: 'edit', title: title, text: txt, summary: 'Resolve culture recommendation: ' + note }); | |||
}); | |||
} | |||
}); | |||
}); /* On-wiki submission forms (incident / agent / vehicle) -> gated review queue */ | |||
ready(function () { | |||
var box = document.getElementById('ic-submit-form'); | |||
if (!box) { return; } | |||
var kind = (box.getAttribute('data-kind') || '').toLowerCase(); | |||
var FIELDS = { | |||
incident: [['date', 'Date (YYYY-MM-DD)'], ['city', 'City'], ['state', 'State'], ['agencies', 'Agencies involved (ICE, CBP…)'], ['description', 'What happened', 1], ['source', 'Source link(s)']], | |||
agent: [['name', 'Agent name (leave blank if unidentified)'], ['agency', 'Agency (ICE, CBP, HSI…)'], ['role', 'Role / title'], ['field_office', 'Field office'], ['state', 'State'], ['description', 'Description / distinguishing details', 1], ['source', 'Source link(s)']], | |||
vehicle: [['plate', 'License plate'], ['state', 'Plate state'], ['make_model', 'Make / model'], ['color', 'Color'], ['agency', 'Agency (if known)'], ['location', 'Where seen'], ['description', 'Notes', 1], ['source', 'Source link(s)']] | |||
}; | |||
var defs = FIELDS[kind]; | |||
if (!defs) { box.innerHTML = '<em>Unknown submission type.</em>'; return; } | |||
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
var user = mw.config.get('wgUserName'); | |||
if (!user) { box.innerHTML = '<p><em>Please <a href="/index.php?title=Special:UserLogin&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">log in</a> to submit.</em></p>'; return; } | |||
var inputs = {}; | |||
defs.forEach(function (f) { | |||
var w = document.createElement('label'); w.className = 'ic-rec-field'; | |||
var s = document.createElement('span'); s.textContent = f[1]; w.appendChild(s); | |||
var el = f[2] ? document.createElement('textarea') : document.createElement('input'); | |||
if (f[2]) { el.rows = 3; } else { el.type = 'text'; } | |||
w.appendChild(el); box.appendChild(w); inputs[f[0]] = el; | |||
}); | |||
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-cult-fcta'; btn.textContent = 'Submit for review'; | |||
var msg = document.createElement('div'); msg.className = 'ic-rec-msg'; | |||
box.appendChild(btn); box.appendChild(msg); | |||
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); } | |||
btn.addEventListener('click', function () { | |||
if (!inputs.description.value.trim() && !inputs[defs[0][0]].value.trim()) { msg.textContent = 'Please fill in the details.'; return; } | |||
btn.disabled = true; msg.textContent = 'Submitting…'; | |||
var id = kind + '-' + Date.now().toString(36) + Math.floor(Math.random() * 10000); | |||
var pn = 'ICE List:Submission - ' + id; | |||
var body = "'''" + kind.charAt(0).toUpperCase() + kind.slice(1) + " submission''' (submitted by [[User:" + user + "]]) - review and route.\n\n* '''Kind:''' " + kind + "\n"; | |||
defs.forEach(function (f) { | |||
var v = inputs[f[0]].value; | |||
if (v && v.trim()) { | |||
var lbl = f[0].charAt(0).toUpperCase() + f[0].slice(1).replace(/_/g, ' '); | |||
body += (f[0] === 'source') ? "* '''Source:''' <nowiki>" + esc(v) + "</nowiki>\n" : "* '''" + lbl + ":''' " + esc(v) + "\n"; | |||
} | |||
}); | |||
body += "\n" + CAT('Submission review') + CAT('Pending submissions') + "\n"; | |||
api.postWithToken('csrf', { action: 'edit', title: pn, text: body, createonly: 1, summary: 'On-wiki ' + kind + ' submission' }) | |||
.then(function () { box.innerHTML = '<p><strong>✓ Thank you.</strong> Your ' + kind + ' submission has gone to the review queue. A reviewer will check it before anything is published — and your identity is not recorded.</p>'; }) | |||
.catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
}); | |||
}); | |||
/* Submission review inbox: list on-wiki submissions, promote to a page or discard */ | |||
ready(function () { | |||
var box = document.getElementById('ic-sub-inbox'); | |||
if (!box) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
if (!['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { box.innerHTML = '<em>Reviewer access required.</em>'; return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function field(txt, label) { var lines = txt.split('\n'), pre = "* '''" + label + ":'''"; for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf(pre) === 0) { return lines[i].slice(pre.length).replace(/<\/?nowiki>/g, '').trim(); } } return ''; } | |||
function esc(x) { return (x || '').replace(/[|\[\]{}]/g, '').trim(); } | |||
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; } | |||
var OB = '{' + '{'; | |||
function yearCat(date) { var m = (date || '').match(/20\d\d/); return m ? '[[Cat' + 'egory:Incidents in ' + m[0] + ']]' : ''; } | |||
var ALL = ['Date', 'City', 'County', 'State', 'Agencies', 'Name', 'Agency', 'Role', 'Field office', 'Plate', 'Make model', 'Color', 'Location', 'Description', 'Source']; | |||
api.get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Pending submissions', cmnamespace: 0, cmlimit: 100, formatversion: 2 }).then(function (d) { | |||
var mems = ((d.query && d.query.categorymembers) || []).filter(function (p) { return p.title.indexOf('ICE List:Submission - ') === 0; }); | |||
box.innerHTML = ''; if (!mems.length) { box.innerHTML = '<p><em>No pending submissions.</em></p>'; return; } | |||
mems.forEach(function (p) { card(p.title); }); | |||
}); | |||
function card(title) { | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) { | |||
var pg = d.query.pages[0]; if (!pg.revisions) { return; } | |||
var txt = pg.revisions[0].slots.main.content; | |||
var kind = field(txt, 'Kind') || 'submission'; | |||
var sm = txt.indexOf('[[User:'), sub = ''; if (sm >= 0) { sub = txt.slice(sm + 7, txt.indexOf(']]', sm)).split('|')[0]; } | |||
var rec = { kind: kind, sub: sub }; ALL.forEach(function (l) { var v = field(txt, l); if (v) { rec[l.toLowerCase().replace(/ /g, '_')] = v; } }); | |||
var c = document.createElement('div'); c.className = 'ic-inbox-card'; | |||
var rows = ALL.map(function (l) { var k = l.toLowerCase().replace(/ /g, '_'); return rec[k] ? ('<div class="ic-subrow"><span>' + l + '</span> ' + esc(rec[k]) + '</div>') : ''; }).join(''); | |||
c.innerHTML = '<div class="ic-inbox-h"><strong>' + kind.toUpperCase() + ' submission</strong> <span class="ic-inbox-meta">from ' + esc(sub) + '</span></div>' + rows; | |||
var act = document.createElement('div'); act.className = 'ic-inbox-actions'; | |||
var msg = document.createElement('span'); msg.className = 'ic-inbox-msg'; | |||
var pb = document.createElement('button'); pb.className = 'ic-inbox-add'; pb.type = 'button'; pb.textContent = 'Promote to page'; | |||
var db = document.createElement('button'); db.type = 'button'; db.textContent = 'Discard'; | |||
pb.addEventListener('click', function () { promote(title, kind, rec, c, msg, pb, db); }); | |||
db.addEventListener('click', function () { if (!confirm('Discard this submission?')) { return; } pb.disabled = db.disabled = true; msg.textContent = '…'; resolve(title, 'discarded').then(function () { c.style.opacity = 0.5; setTimeout(function () { c.remove(); }, 500); }).catch(function () { msg.textContent = 'Error'; }); }); | |||
act.appendChild(pb); act.appendChild(db); act.appendChild(msg); c.appendChild(act); box.appendChild(c); | |||
}); | |||
} | |||
function buildPage(kind, rec) { | |||
if (kind === 'incident') { | |||
var loc = [rec.city, rec.state].filter(Boolean).join(', '); | |||
var title = ((rec.date ? rec.date + ' ' : '') + 'Incident' + (loc ? ' in ' + loc : '')).trim(); | |||
var body = OB + 'Infobox incident\n| title = ' + esc(title) + '\n| date = ' + esc(rec.date || '') + '\n| city = ' + esc(rec.city || '') + '\n| state = ' + esc(rec.state || '') + '\n| agencies = ' + esc(rec.agencies || '') + '\n| status = Developing (from a volunteer submission).\n| verification = Developing - submitted for review.\n| key_sources =\n* ' + esc(rec.source || '-') + '\n}}\n\n' + esc(rec.description || '') + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (rec.state ? CAT('Incidents in ' + esc(rec.state)) : '') + yearCat(rec.date) + '\n'; | |||
return { title: title, body: body }; | |||
} | |||
if (kind === 'agent') { | |||
if (!rec.name) { return null; } | |||
var b = OB + 'Agent page\n|name=' + esc(rec.name) + '\n|agency=' + esc(rec.agency || 'Unknown') + '\n|role=' + esc(rec.role || '') + '\n|field_office=' + esc(rec.field_office || '') + '\n|state=' + esc(rec.state || '') + '\n|status=Active\n|image=nopfp.png\n|verification=Submitted for review\n|summary=' + esc(rec.description || '') + '\n}}\n\n== Documented Incidents ==\n' + OB + 'IncidentsForAgent}}\n\n' + OB + 'MediaForAgent}}\n\n' + CAT('Agents') + CAT('Agents needing a photo') + '\n'; | |||
return { title: rec.name, body: b }; | |||
} | |||
if (kind === 'vehicle') { | |||
var vt = rec.plate ? ('Vehicle:' + rec.plate) : ('Vehicle submission ' + (rec.state || '')); | |||
var vb = "'''Vehicle'''" + (rec.plate ? ' plate ' + esc(rec.plate) : '') + (rec.state ? ' (' + esc(rec.state) + ')' : '') + '.\n\n* Make/model: ' + esc(rec.make_model || '-') + '\n* Color: ' + esc(rec.color || '-') + '\n* Agency: ' + esc(rec.agency || '-') + '\n* Where seen: ' + esc(rec.location || '-') + '\n* Notes: ' + esc(rec.description || '-') + '\n* Source: ' + esc(rec.source || '-') + '\n\n' + CAT('Vehicles') + '\n'; | |||
return { title: vt, body: vb }; | |||
} | |||
return null; | |||
} | |||
function promote(title, kind, rec, c, msg, pb, db) { | |||
var pg = buildPage(kind, rec); | |||
if (!pg) { msg.textContent = 'Unidentified agent / unknown kind — open the submission to build it manually.'; return; } | |||
pb.disabled = db.disabled = true; msg.textContent = 'Creating ' + pg.title + '…'; | |||
api.get({ action: 'query', titles: pg.title, formatversion: 2 }).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + pg.title + '" already exists.'); } | |||
return api.postWithToken('csrf', { action: 'edit', title: pg.title, createonly: 1, text: pg.body, summary: 'Create from reviewed submission' }); | |||
}).then(function () { return resolve(title, 'promoted to [[' + pg.title + ']]'); }).then(function () { | |||
msg.innerHTML = '✓ Created <a href="/index.php/' + encodeURIComponent(pg.title.replace(/ /g, '_')) + '" target="_blank" rel="noopener">' + esc(pg.title) + '</a> (now pending review).'; | |||
c.style.opacity = 0.6; setTimeout(function () { c.remove(); }, 1600); | |||
}).catch(function (e) { pb.disabled = db.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
} | |||
function resolve(title, note) { | |||
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
txt = txt.replace(CAT('Pending submissions'), CAT('Resolved submissions')); | |||
txt = "'''Resolved:''' " + note + '\n\n' + txt; | |||
return api.postWithToken('csrf', { action: 'edit', title: title, text: txt, summary: 'Resolve submission: ' + note }); | |||
}); | |||
} | |||
}); | |||
}); /* Quick add: create incident / agent / vehicle / death directly from your profile */ | |||
ready(function () { | |||
var user = mw.config.get('wgUserName'); | |||
if (!user) { return; } | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'User:' + user) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
if (!/^ICEListAdmin[0-9]+$/.test(user) && !['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var panel = document.createElement('div'); panel.className = 'ic-desk ic-quickadd'; | |||
panel.innerHTML = '<div class="ic-desk__kicker">Quick add</div><div class="ic-qa__btns"></div><div class="ic-qa__form"></div>'; | |||
var desk = content.querySelector('.ic-desk'); | |||
if (desk) { content.insertBefore(panel, desk.nextSibling); } else { content.insertBefore(panel, content.firstChild); } | |||
var TYPES = { | |||
incident: [['date', 'Date (YYYY-MM-DD)'], ['city', 'City'], ['state', 'State'], ['agencies', 'Agencies (ICE, CBP…)'], ['description', 'What happened', 1], ['source', 'Source link']], | |||
agent: [['name', 'Full name (required)'], ['agency', 'Agency'], ['role', 'Role / title'], ['field_office', 'Field office'], ['state', 'State'], ['description', 'Description / details', 1], ['source', 'Source link']], | |||
vehicle: [['plate', 'License plate (required)'], ['state', 'Plate state'], ['make_model', 'Make / model'], ['color', 'Color'], ['agency', 'Agency'], ['location', 'Where seen'], ['description', 'Notes', 1], ['source', 'Source link']], | |||
death: [['name', 'Name of the deceased (required)'], ['age', 'Age'], ['nationality', 'Nationality'], ['date', 'Date of death (YYYY-MM-DD)'], ['location', 'Location / facility'], ['state', 'State'], ['agency', 'Agency'], ['cause', 'Reported cause'], ['description', 'What happened', 1], ['source', 'Source link']] | |||
}; | |||
var btnsEl = panel.querySelector('.ic-qa__btns'), formEl = panel.querySelector('.ic-qa__form'), api = null; | |||
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); } | |||
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; } | |||
var OB = '{' + '{'; | |||
function yearCat(date) { var m = (date || '').match(/20\d\d/); return m ? CAT('Incidents in ' + m[0]) : ''; } | |||
Object.keys(TYPES).forEach(function (k) { | |||
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary'; | |||
var a = document.createElement('a'); a.href = '#'; a.textContent = 'Add ' + k; | |||
a.addEventListener('click', function (e) { e.preventDefault(); openForm(k); }); | |||
sp.appendChild(a); btnsEl.appendChild(sp); | |||
}); | |||
if (mw.config.get('wgUserName') === 'ICEListAdmin6') { var mv = document.createElement('div'); mv.className = 'ic-qa__admin'; mv.innerHTML = '<a href="/index.php/ICE_List:Add_volunteer">+ Add a submission volunteer →</a>'; panel.appendChild(mv); } | |||
function build(kind, v) { | |||
if (kind === 'incident') { | |||
var loc = [v.city, v.state].filter(Boolean).join(', '); | |||
var title = ((v.date ? v.date + ' ' : '') + 'Incident' + (loc ? ' in ' + loc : '')).trim(); | |||
return { title: title, body: OB + 'Infobox incident\n| title = ' + esc(title) + '\n| date = ' + esc(v.date) + '\n| city = ' + esc(v.city) + '\n| state = ' + esc(v.state) + '\n| agencies = ' + esc(v.agencies) + '\n| status = Developing.\n| verification = Added via quick-add.\n| key_sources =\n* ' + esc(v.source || '-') + '\n}}\n\n' + esc(v.description) + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (v.state ? CAT('Incidents in ' + esc(v.state)) : '') + yearCat(v.date) + '\n' }; | |||
} | |||
if (kind === 'agent') { | |||
if (!v.name) { return { err: 'Name is required.' }; } | |||
return { title: v.name, body: OB + 'Agent page\n|name=' + esc(v.name) + '\n|agency=' + esc(v.agency || 'Unknown') + '\n|role=' + esc(v.role) + '\n|field_office=' + esc(v.field_office) + '\n|state=' + esc(v.state) + '\n|status=Active\n|image=nopfp.png\n|verification=Added via quick-add\n|summary=' + esc(v.description) + '\n}}\n\n== Documented Incidents ==\n' + OB + 'IncidentsForAgent}}\n\n' + OB + 'MediaForAgent}}\n\n' + CAT('Agents') + CAT('Agents needing a photo') + '\n' }; | |||
} | |||
if (kind === 'vehicle') { | |||
if (!v.plate) { return { err: 'Plate is required.' }; } | |||
var vsum = (v.description || ('Vehicle plate ' + v.plate)) + (v.color ? ' Colour: ' + v.color + '.' : '') + (v.location ? ' Seen at ' + v.location + '.' : ''); | |||
return { title: 'Vehicle:' + v.plate, body: OB + 'Vehicle page\n| identifier = ' + esc(v.plate) + '\n| plate = ' + esc(v.plate) + '\n| type = ' + esc(v.make_model || '') + '\n| agency = ' + esc(v.agency || '') + '\n| state = ' + esc(v.state || '') + '\n| status = Active\n| verification = Added via quick-add, pending review.\n| summary = ' + esc(vsum) + '\n| source_1 = ' + esc(v.source || '') + '\n}}\n' }; | |||
} | |||
if (kind === 'death') { | |||
if (!v.name) { return { err: 'Name is required.' }; } | |||
var meta = [v.age, v.nationality].filter(Boolean).join(', '), sk = esc(v.date) + ' ' + esc(v.name); | |||
return { title: 'Death of ' + v.name + ' in ICE custody', body: OB + 'Infobox incident\n| title = ' + esc(v.name) + '\n| date = ' + esc(v.date) + '\n| state = ' + esc(v.state) + '\n| country = United States\n| location_detail = ' + esc(v.location) + '\n| agencies = ' + esc(v.agency || 'ICE') + '\n| operation_type = Death in immigration detention / custody\n| status = Autopsy pending.\n| people_affected = 1\n| deaths = 1' + (v.cause ? ' — ' + esc(v.cause) : '') + '\n| verification = Added via quick-add, pending review.\n| key_sources =\n* ' + esc(v.source || '-') + '\n}}\n\n' + "'''" + esc(v.name) + "'''" + (meta ? ' (' + meta + ')' : '') + ' died' + (v.date ? ' on ' + esc(v.date) : '') + ' while in the custody of ICE' + (v.location ? ', at ' + esc(v.location) : '') + '.' + (v.cause ? ' Reported cause: ' + esc(v.cause) + '.' : '') + '\n\n' + esc(v.description) + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (v.state ? CAT('Incidents in ' + esc(v.state)) : '') + CAT('Deaths by immigration enforcement|' + sk) + CAT('Deaths in immigration detention|' + sk) + yearCat(v.date) + '\n' }; | |||
} | |||
} | |||
function openForm(kind) { | |||
formEl.innerHTML = ''; var inputs = {}; | |||
TYPES[kind].forEach(function (f) { | |||
var w = document.createElement('label'); w.className = 'ic-rec-field'; | |||
var s = document.createElement('span'); s.textContent = f[1]; w.appendChild(s); | |||
var el = f[2] ? document.createElement('textarea') : document.createElement('input'); | |||
if (f[2]) { el.rows = 3; } else { el.type = 'text'; } | |||
w.appendChild(el); formEl.appendChild(w); inputs[f[0]] = el; | |||
}); | |||
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-inbox-add'; btn.textContent = 'Create ' + kind + ' page'; | |||
var cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = 'Cancel'; | |||
var msg = document.createElement('div'); msg.className = 'ic-rec-msg'; | |||
var row = document.createElement('div'); row.className = 'ic-inbox-actions'; row.appendChild(btn); row.appendChild(cancel); | |||
formEl.appendChild(row); formEl.appendChild(msg); | |||
cancel.addEventListener('click', function () { formEl.innerHTML = ''; }); | |||
btn.addEventListener('click', function () { | |||
var v = {}; Object.keys(inputs).forEach(function (k) { v[k] = inputs[k].value; }); | |||
var pg = build(kind, v); | |||
if (pg.err) { msg.textContent = pg.err; return; } | |||
btn.disabled = true; msg.textContent = 'Creating…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
if (!api) { api = new mw.Api(); } | |||
api.get({ action: 'query', titles: pg.title, formatversion: 2 }).then(function (d) { | |||
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + pg.title + '" already exists.'); } | |||
return api.postWithToken('csrf', { action: 'edit', title: pg.title, createonly: 1, text: pg.body, summary: 'Quick-add ' + kind }); | |||
}).then(function () { | |||
msg.innerHTML = '✓ Created <a href="/index.php/' + encodeURIComponent(pg.title.replace(/ /g, '_')) + '" target="_blank" rel="noopener">' + esc(pg.title) + '</a>.'; | |||
setTimeout(function () { formEl.innerHTML = ''; }, 3000); | |||
}).catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); }); | |||
}); | |||
}); | |||
} | |||
}); /* Add a confined submission volunteer: account + submitter group + profile */ | |||
ready(function () { | |||
var box = document.getElementById('ic-addvol'); | |||
if (!box) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { box.innerHTML = '<em>This tool is restricted.</em>'; return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function fld(label, el) { var w = document.createElement('label'); w.className = 'ic-rec-field'; var s = document.createElement('span'); s.textContent = label; w.appendChild(s); w.appendChild(el); return w; } | |||
function gen() { var c = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; var p = ''; for (var i = 0; i < 14; i++) { p += c.charAt(Math.floor(Math.random() * c.length)); } return p; } | |||
var uname = document.createElement('input'); uname.type = 'text'; uname.placeholder = 'Username for the new volunteer'; | |||
var pass = document.createElement('input'); pass.type = 'text'; pass.value = gen(); | |||
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-inbox-add'; btn.textContent = 'Create volunteer'; | |||
var msg = document.createElement('div'); msg.className = 'ic-rec-msg'; | |||
box.appendChild(fld('Username', uname)); box.appendChild(fld('Password (auto-generated, editable)', pass)); box.appendChild(btn); box.appendChild(msg); | |||
function esc(x) { return (x || '').replace(/</g, '<'); } | |||
btn.addEventListener('click', function () { | |||
var u = uname.value.trim(), p = pass.value.trim(); | |||
if (!u || p.length < 8) { msg.textContent = 'Enter a username and a password of 8+ characters.'; return; } | |||
btn.disabled = true; | |||
var log = []; | |||
function show() { msg.innerHTML = log.join('<br>'); } | |||
// 1) create account | |||
log.push('Creating account…'); show(); | |||
api.get({ action: 'query', meta: 'tokens', type: 'createaccount', formatversion: 2 }).then(function (d) { | |||
var tok = d.query.tokens.createaccounttoken; | |||
return api.post({ action: 'createaccount', createtoken: tok, username: u, password: p, retype: p, createreturnurl: location.href, reason: 'Confined submission volunteer' }); | |||
}).then(function (r) { | |||
var st = r.createaccount && r.createaccount.status; | |||
log[log.length - 1] = (st === 'PASS') ? '✓ Account created.' : '✗ Account: ' + ((r.createaccount && r.createaccount.message) || st || 'failed — create it via Special:CreateAccount, then re-run to add group + profile.'); | |||
show(); | |||
}).catch(function (e) { | |||
log[log.length - 1] = '✗ Account: ' + (e && e.message ? e.message : e) + ' (try Special:CreateAccount, then re-run).'; show(); | |||
}).then(function () { | |||
// 2) profile page | |||
log.push('Creating profile…'); show(); | |||
var prof = "''Volunteer contributor to the ICE List.''\n\n== Submit ==\nThanks for helping document immigration enforcement. Everything you send is reviewed before it is published, and your identity is not recorded:\n* [[ICE List:Submit incident (test)|Report an incident]]\n* [[ICE List:Submit agent (test)|Submit an agent]]\n* [[ICE List:Submit vehicle (test)|Submit a vehicle]]\n"; | |||
return api.postWithToken('csrf', { action: 'edit', title: 'User:' + u, text: prof, createonly: 1, summary: 'Create volunteer profile' }) | |||
.then(function () { log[log.length - 1] = '✓ Profile created.'; show(); }) | |||
.catch(function (e) { log[log.length - 1] = '• Profile: ' + (e && e.message ? e.message : e); show(); }); | |||
}).then(function () { | |||
// 3) submitter group | |||
log.push('Adding to submitter group…'); show(); | |||
return api.postWithToken('userrights', { action: 'userrights', user: u, add: 'submitter', reason: 'confined volunteer' }) | |||
.then(function () { log[log.length - 1] = '✓ Added to submitter group (confined to submissions).'; show(); }) | |||
.catch(function () { log[log.length - 1] = '• Submitter group not applied — paste the submitter LocalSettings block, then add the group in Special:UserRights.'; show(); }); | |||
}).then(function () { | |||
log.push('<strong>Hand these to the volunteer — Username: <code>' + esc(u) + '</code> · Password: <code>' + esc(p) + '</code></strong> · <a href="/index.php/User:' + encodeURIComponent(u) + '" target="_blank" rel="noopener">profile</a>'); | |||
show(); | |||
uname.value = ''; pass.value = gen(); btn.disabled = false; | |||
}); | |||
}); | |||
}); | |||
}); /* Submitter desk: submission console on a confined volunteer's own profile */ | |||
ready(function () { | |||
var user = mw.config.get('wgUserName'); | |||
if (!user) { return; } | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'User:' + user) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
var isAdmin = /^ICEListAdmin[0-9]+$/.test(user) || ['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; }); | |||
if (isAdmin) { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var panel = document.createElement('div'); panel.className = 'ic-desk'; | |||
panel.innerHTML = '<div class="ic-desk__kicker">Contribute</div>' + | |||
'<p class="ic-desk__hello">Thanks for helping document immigration enforcement. Everything you submit is reviewed before it is published, and your identity is not recorded. Pick what to send:</p>' + | |||
'<div class="ic-desk__tools"></div>'; | |||
content.insertBefore(panel, content.firstChild); | |||
var tools = panel.querySelector('.ic-desk__tools'); | |||
[['Report an incident', '/index.php/ICE_List:Submit_incident_(test)'], ['Submit an agent', '/index.php/ICE_List:Submit_agent_(test)'], ['Submit a vehicle', '/index.php/ICE_List:Submit_vehicle_(test)']].forEach(function (l) { | |||
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary'; | |||
var a = document.createElement('a'); a.href = l[1]; a.textContent = l[0]; sp.appendChild(a); tools.appendChild(sp); | |||
}); | |||
}); /* Account requests panel on ICEListAdmin6's profile (robust) */ | |||
ready(function () { | |||
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { return; } | |||
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:ICEListAdmin6') { return; } | |||
var content = document.querySelector('#mw-content-text .mw-parser-output'); | |||
if (!content) { return; } | |||
var panel = document.createElement('div'); panel.className = 'ic-desk ic-areq-panel'; | |||
panel.innerHTML = '<div class="ic-desk__kicker">Account requests</div><div class="ic-areq">Checking…</div>'; | |||
var desks = content.querySelectorAll('.ic-desk'); | |||
if (desks.length) { desks[desks.length - 1].parentNode.insertBefore(panel, desks[desks.length - 1].nextSibling); } | |||
else { content.insertBefore(panel, content.firstChild); } | |||
var box = panel.querySelector('.ic-areq'); | |||
var QUEUE = '/index.php/Special:ConfirmAccounts'; | |||
var openBtn = '<p style="margin-top:.6rem"><a class="ic-hero__btn ic-hero__btn--primary" style="display:inline-block" href="' + QUEUE + '"><span>Open the full queue →</span></a></p>'; | |||
fetch(QUEUE, { credentials: 'same-origin' }).then(function (r) { return r.text(); }).then(function (html) { | |||
var doc = new DOMParser().parseFromString(html, 'text/html'); | |||
var cont = doc.querySelector('#mw-content-text'); | |||
if (!cont) { box.innerHTML = 'Could not load the queue. ' + openBtn; return; } | |||
// broad: any link into ConfirmAccounts with a query (request rows) | |||
var links = [].slice.call(cont.querySelectorAll('a[href]')).filter(function (a) { | |||
var h = a.getAttribute('href') || ''; return /[?&]acrid=\d/.test(h) && a.textContent.trim().length > 1; | |||
}); | |||
var seen = {}, items = []; | |||
links.forEach(function (a) { var t = a.textContent.trim(); if (!seen[t]) { seen[t] = 1; items.push({ t: t, h: a.getAttribute('href') }); } }); | |||
if (items.length) { | |||
var html2 = '<p><b>' + items.length + '</b> pending request(s):</p><ul class="ic-areq-list">'; | |||
items.forEach(function (it) { html2 += '<li><a href="' + it.h + '">' + it.t + '</a></li>'; }); | |||
box.innerHTML = html2 + '</ul>' + openBtn; | |||
} else { | |||
// show the queue's own text so nothing is ever silently blank | |||
var txt = (cont.textContent || '').replace(/\s+/g, ' ').trim(); | |||
var empty = /no pending|there are no|no requests|no account requests/i.test(txt); | |||
box.innerHTML = '<p>' + (empty ? 'No pending requests found in the queue.' : 'Requests may be present but not auto-listed — open the queue to review.') + '</p>' + openBtn; | |||
} | |||
}).catch(function () { box.innerHTML = 'Could not reach the queue. ' + openBtn; }); | |||
}); /* Homepage: featured culture work of the day (rotates daily) */ | |||
ready(function () { | |||
var box = document.getElementById('ic-featured-media'); | |||
if (!box) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: 'ICE List:Culture', formatversion: 2 }).then(function (d) { | |||
var txt = d.query.pages[0].revisions[0].slots.main.content; | |||
var items = [], re = /\{\{Culture pick\|([^}]*)\}\}/g, m; | |||
while ((m = re.exec(txt))) { | |||
var o = {}; m[1].split('|').forEach(function (kv) { var i = kv.indexOf('='); if (i > 0) { o[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } }); | |||
if (o.title) { items.push(o); } | |||
} | |||
if (!items.length) { box.textContent = ''; return; } | |||
var p = items[Math.floor(Date.now() / 86400000) % items.length]; | |||
var COL = { documentary: '#E2231A', film: '#1A4FE2', 'film & tv': '#1A4FE2', music: '#0E8F4E', book: '#9A5A24', podcast: '#6A2FB8' }; | |||
var col = COL[(p.type || '').toLowerCase()] || '#E2231A'; | |||
function esc(x) { var e = document.createElement('div'); e.textContent = x || ''; return e.innerHTML; } | |||
var url = '/index.php/' + encodeURIComponent('Culture:' + p.title.replace(/ /g, '_')); | |||
box.innerHTML = '<a class="ic-fm" href="' + url + '">' + | |||
'<div class="ic-fm__cov" style="--pc:' + col + '"></div>' + | |||
'<div class="ic-fm__txt"><div class="ic-fm__kick">' + esc(p.type) + (p.year ? ' · ' + esc(p.year) : '') + '</div>' + | |||
'<div class="ic-fm__ttl">' + esc(p.title) + '</div>' + | |||
(p.creator ? '<div class="ic-fm__by">' + esc(p.creator) + '</div>' : '') + | |||
(p.blurb ? '<p class="ic-fm__desc">' + esc(p.blurb) + '</p>' : '') + | |||
'<span class="ic-fm__cta">Where to find it →</span></div></a>'; | |||
if (p.cover) { | |||
api.get({ action: 'query', titles: 'File:' + p.cover, prop: 'imageinfo', iiprop: 'url', iiurlwidth: 240, formatversion: 2 }).then(function (dd) { | |||
var pg = dd.query.pages[0], ii = pg && pg.imageinfo; | |||
if (ii && ii[0]) { var cov = box.querySelector('.ic-fm__cov'); cov.style.backgroundImage = 'url(' + ii[0].thumburl + ')'; cov.classList.add('ic-fm__cov--img'); } | |||
}); | |||
} | |||
}).catch(function () { box.textContent = ''; }); | |||
}); | |||
}); /* Mobile homepage: interleave columns + accordion (first half open, second half collapsed) */ | |||
ready(function () { | |||
if (mw.config.get('wgPageName') !== 'Main_Page') { return; } | |||
var cols = document.querySelectorAll('.icelist-homepage-columns > .icelist-homepage-column'); | |||
if (cols.length < 2) { return; } | |||
var n = cols.length, items = []; | |||
[].forEach.call(cols, function (col, ci) { | |||
var i = 0; | |||
[].forEach.call(col.children, function (ch) { | |||
if (ch.tagName === 'STYLE' || ch.tagName === 'LINK') { return; } | |||
var ord = i * n + ci; ch.style.order = String(ord); i++; | |||
if (ch.classList && ch.classList.contains('ic-card') && ch.querySelector('.ic-card__head') && ch.querySelector('.ic-card__body')) { | |||
items.push({ el: ch, ord: ord }); | |||
} | |||
}); | |||
}); | |||
items.sort(function (a, b) { return a.ord - b.ord; }); | |||
var half = Math.ceil(items.length / 2); | |||
items.forEach(function (it, idx) { | |||
var c = it.el, head = c.querySelector('.ic-card__head'); | |||
c.classList.add('ic-card--collap'); | |||
if (idx >= half) { c.classList.add('is-collapsed'); } | |||
head.setAttribute('role', 'button'); head.setAttribute('tabindex', '0'); | |||
function t() { c.classList.toggle('is-collapsed'); } | |||
head.addEventListener('click', t); | |||
head.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); t(); } }); | |||
}); | |||
}); | |||
/* Strip the "ICE List:" title-convention prefix from main-namespace headings */ | |||
ready(function () { | |||
if (mw.config.get('wgNamespaceNumber') !== 0) { return; } | |||
var el = document.querySelector('.mw-page-title-main') || document.querySelector('#firstHeading'); | |||
if (!el) { return; } | |||
['ICE List:', 'ICE List Wiki:'].forEach(function (p) { | |||
if (el.firstChild && el.firstChild.nodeType === 3 && el.firstChild.nodeValue.indexOf(p) === 0) { | |||
el.firstChild.nodeValue = el.firstChild.nodeValue.slice(p.length); | |||
} | |||
}); | |||
}); | |||
/* Language globe: toggle the dropdown menu */ | |||
ready(function () { | |||
var boxes = [].slice.call(document.querySelectorAll('.ic-lang')); | |||
if (!boxes.length) { return; } | |||
boxes.forEach(function (b) { | |||
var btn = b.querySelector('.ic-lang__btn'); | |||
if (!btn) { return; } | |||
function tog(e) { e.stopPropagation(); boxes.forEach(function (o) { if (o !== b) { o.classList.remove('is-open'); } }); b.classList.toggle('is-open'); } | |||
btn.addEventListener('click', tog); | |||
btn.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tog(e); } }); | |||
}); | |||
document.addEventListener('click', function () { boxes.forEach(function (b) { b.classList.remove('is-open'); }); }); | |||
}); | |||
/* Site-wide article header: prepend a category eyebrow above the page title */ | |||
ready(function () { | |||
var ns = mw.config.get('wgNamespaceNumber'); | |||
if ([0, 14, 4].indexOf(ns) < 0) { return; } | |||
if (mw.config.get('wgPageName') === 'Main_Page') { return; } | |||
if (document.querySelector('.ic-work')) { return; } | |||
var h = document.querySelector('.mw-first-heading') || document.getElementById('firstHeading'); | |||
if (!h || document.getElementById('ic-pagehead-eyebrow')) { return; } | |||
var label = ''; | |||
if (ns === 14) { label = 'Category'; } | |||
else if (ns === 4) { label = 'ICE List'; } | |||
else { | |||
var cats = mw.config.get('wgCategories') || []; | |||
var map = [['Agents', 'Agent'], ['Incidents', 'Incident'], ['Deaths', 'Death'], ['Vehicles', 'Vehicle'], ['Facilities', 'Facility'], ['Companies', 'Company'], ['Boycott', 'Boycott'], ['287(g)', '287(g) agreement']]; | |||
for (var i = 0; i < map.length; i++) { | |||
for (var j = 0; j < cats.length; j++) { if (cats[j].indexOf(map[i][0]) !== -1) { label = map[i][1]; break; } } | |||
if (label) { break; } | |||
} | |||
} | |||
if (!label) { return; } | |||
var eb = document.createElement('div'); eb.id = 'ic-pagehead-eyebrow'; eb.className = 'ic-pagehead-eyebrow'; eb.textContent = label; | |||
h.parentNode.insertBefore(eb, h); | |||
}); | |||
/* External links in content open in a new tab */ | |||
ready(function () { | |||
var ls = document.querySelectorAll('.mw-parser-output a.external'); | |||
for (var i = 0; i < ls.length; i++) { ls[i].target = '_blank'; ls[i].rel = 'noopener noreferrer'; } | |||
}); | |||
/* Volunteer profile desk (submitter / sorter / confirm) — approved scheme + submitter paste-and-go */ | |||
ready(function () { | |||
var user = mw.config.get('wgUserName'); | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'User:' + user) { return; } | |||
var groups = mw.config.get('wgUserGroups') || []; | |||
var role = null, ORDER = ['confirm', 'sorter', 'submitter']; | |||
for (var i = 0; i < ORDER.length; i++) { if (groups.indexOf(ORDER[i]) >= 0) { role = ORDER[i]; break; } } | |||
if (!role) { return; } | |||
var U = mw.util.getUrl; | |||
var CFG = { | |||
submitter: { cls: '', eye: 'Volunteer', title: 'Your volunteer desk', paste: true, | |||
job: 'Welcome. The fastest way to help is jumping into work that is <b>ready right now</b> — hundreds of links waiting to be sorted, and agents that need identifying. Or drop a new link below. Nothing is public until an admin publishes it.', | |||
acts: [['Sort a waiting link →', U('ICE List:Submission/Inbox'), 1], ['Identify an agent →', U('ICE List:Most wanted'), 0], ['Task board →', U('ICE List:Submission/Tasks'), 0]], | |||
chips: [['Submission inbox', 'Links in the inbox']], | |||
steps: ['Spot a post, article or video about ICE', 'Paste the link(s) and hit add', 'Sorters take it from there'], | |||
more: { eye: 'Want to do more?', text: 'Dropping links is just phase 1 — you can jump into <b>any</b> step whenever you feel like it. It is all optional, and you can stop any time.', | |||
acts: [['Try sorting a link →', U('ICE List:Submission/Inbox'), 1], ['Suggest a 287(g) photo →', U('ICE List:Submit a lead') + '?suggest=photo', 0], ['Suggest an officer name →', U('ICE List:Submit a lead') + '?suggest=name', 0]] } }, | |||
sorter: { cls: ' ic-vdesk--sorter', eye: 'Sorter · Phase 2', title: 'Your sorter desk', | |||
job: 'Take a submitted link and <b>pull out the facts</b> — agent photos, incidents with locations, license plates — into a clean record.', | |||
acts: [['Sort the next link', U('ICE List:Submission/Inbox'), 1], ['Open sorting', U('ICE List:Submission/Sorting'), 0]], | |||
chips: [['Submission inbox', 'Waiting in inbox'], ['Submission sorting', 'In sorting']], | |||
steps: ['Claim a link from the inbox', 'Fill the record; upload screenshots', 'Send it on to Confirm'] }, | |||
confirm: { cls: ' ic-vdesk--confirm', eye: 'Confirm · Phase 3', title: 'Your confirm desk', | |||
job: 'Check that each sorted record is <b>complete and correct</b> before it reaches the admins.', | |||
acts: [['Review the next record', U('ICE List:Submission/Sorting'), 1], ['Open confirm queue', U('ICE List:Submission/Confirm'), 0]], | |||
chips: [['Submission sorting', 'In sorting'], ['Submission confirm', 'Awaiting confirm']], | |||
steps: ['Open a completed record', 'Verify the details and sources', 'Confirm — it goes to the admins'] } | |||
}; | |||
var c = CFG[role]; | |||
var actHtml = c.acts.map(function (a) { return '<a class="ic-vbtn' + (a[2] ? '' : ' ic-vbtn--ghost') + '" href="' + a[1] + '">' + a[0] + '</a>'; }).join(''); | |||
var pasteHtml = c.paste ? '<div class="ic-vpaste"><textarea class="ic-vpaste__in" rows="3" placeholder="Paste one or more links (one per line)…"></textarea><input class="ic-vpaste__note" placeholder="Optional: what is it / where from"><select class="ic-vpaste__type"><option value="">What kind of link? (helps sorters prioritise)</option><option value="video">🎥 Video / footage</option><option value="photo">📷 Photo</option><option value="social">📱 Social post</option><option value="article">📰 News article</option><option value="document">📄 Document</option><option value="other">🔗 Other</option></select><div class="ic-vact"><a class="ic-vbtn ic-vpaste__go" href="#">Add link(s)</a><span class="ic-vpaste__msg"></span></div></div>' : ''; | |||
var chipHtml = c.chips.map(function (ch) { return '<a class="ic-vchip" data-cat="' + ch[0] + '" href="' + U('Category:' + ch[0]) + '"><b>—</b><span>' + ch[1] + '</span></a>'; }).join(''); | |||
var stepHtml = c.steps.map(function (st) { return '<div class="ic-vstep">' + st + '</div>'; }).join(''); | |||
var moreHtml = c.more ? '<div class="ic-vdesk__more"><div class="ic-veyebrow">' + c.more.eye + '</div><p class="ic-vjob">' + c.more.text + '</p><div class="ic-vact">' + c.more.acts.map(function (a) { return '<a class="ic-vbtn' + (a[2] ? '' : ' ic-vbtn--ghost') + '" href="' + a[1] + '">' + a[0] + '</a>'; }).join('') + '</div></div>' : ''; | |||
var box = document.createElement('div'); box.className = 'ic-vdesk' + c.cls; | |||
box.innerHTML = '<div class="ic-vdesk__head"><div class="ic-veyebrow">' + c.eye + '</div><div class="ic-vtitle">' + c.title + '</div><div class="ic-vjob">' + c.job + '</div></div>' | |||
+ '<div class="ic-vdesk__body">' + pasteHtml + '<div class="ic-vact">' + actHtml + '</div><div class="ic-vqueue">' + chipHtml + '</div>' | |||
+ '<div><div class="ic-vsteps__label">How it works</div><div class="ic-vsteps">' + stepHtml + '</div></div>' + moreHtml + '<div class="ic-vguide">New here? <a href="/index.php/ICE_List:Volunteer_guide">Read the full volunteer guide \u2192</a></div></div>'; | |||
var content = document.querySelector('.mw-parser-output') || document.getElementById('mw-content-text'); | |||
if (content) { content.insertBefore(box, content.firstChild); } | |||
if (role === 'sorter' || role === 'confirm') { | |||
var CAT = role === 'sorter' ? 'Submission sorting' : 'Submission confirm'; | |||
var PAT = role === 'sorter' ? /\/link-/ : /\/(agent|vehicle|incident|link)-/; | |||
var prim = box.querySelector('.ic-vbtn:not(.ic-vbtn--ghost)'); | |||
if (prim) { prim.addEventListener('click', function (e) { | |||
e.preventDefault(); var lbl = prim.textContent; prim.textContent = 'Finding next…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:' + CAT, cmlimit: 50, cmtype: 'page', formatversion: 2 }).then(function (d) { | |||
var m = (d.query.categorymembers || []).filter(function (p) { return PAT.test(p.title); }); | |||
if (m.length) { window.location.href = '/index.php/' + encodeURI(m[0].title.replace(/ /g, '_')); } | |||
else { prim.textContent = 'All clear \u2014 nothing to work'; setTimeout(function(){ prim.textContent = lbl; }, 2500); } | |||
}, function () { prim.textContent = lbl; }); | |||
}); | |||
}); } | |||
} | |||
// fill counts | |||
var cats = c.chips.map(function (ch) { return 'Category:' + ch[0]; }); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', titles: cats.join('|'), prop: 'categoryinfo', formatversion: 2 }).then(function (d) { | |||
var m = {}; (d.query.pages || []).forEach(function (p) { m[p.title] = (p.categoryinfo && p.categoryinfo.pages) || 0; }); | |||
var chips = box.querySelectorAll('.ic-vchip'); | |||
for (var i = 0; i < chips.length; i++) { var k = 'Category:' + chips[i].getAttribute('data-cat'); chips[i].querySelector('b').textContent = (m[k] != null ? m[k] : 0); } | |||
}); | |||
// wire the paste-and-go box | |||
var go = box.querySelector('.ic-vpaste__go'); | |||
if (go) { | |||
go.addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
var ta = box.querySelector('.ic-vpaste__in'), note = box.querySelector('.ic-vpaste__note'), msg = box.querySelector('.ic-vpaste__msg'); | |||
var links = (ta.value || '').split(/\s+/).filter(function (x) { return /^https?:\/\//i.test(x); }); | |||
if (!links.length) { msg.textContent = 'Paste at least one link (starting http…).'; return; } | |||
go.style.pointerEvents = 'none'; go.textContent = 'Adding…'; msg.textContent = ''; | |||
var ctx = (note.value || '').replace(/[{}|]/g, ''), ty = ((box.querySelector('.ic-vpaste__type') || {}).value || ''), done = 0, fail = 0; | |||
function next(i) { | |||
if (i >= links.length) { | |||
go.style.pointerEvents = ''; go.textContent = 'Add link(s)'; ta.value = ''; note.value = ''; | |||
msg.textContent = 'Added ' + done + (fail ? (', ' + fail + ' failed') : '') + '. Thank you!'; | |||
var chip = box.querySelector('.ic-vchip b'); if (chip) { chip.textContent = (parseInt(chip.textContent, 10) || 0) + done; } | |||
return; | |||
} | |||
var id = 'link-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 10000); | |||
var text = '{' + '{Submission link|url=' + links[i].replace(/[{}|]/g, '') + '|note=' + ctx + '|source=' + user + (ty ? '|type=' + ty : '') + '|status=new}}'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/' + id, text: text, createonly: 1, summary: 'Link submitted via desk' }) | |||
.then(function () { done++; }, function () { fail++; }).always(function () { next(i + 1); }); | |||
} | |||
next(0); | |||
}); | |||
} | |||
}); | |||
}); | |||
/* Suppress the recurring ULS "Language changed" popup */ | |||
ready(function () { | |||
function kill() { | |||
var ns = document.querySelectorAll('.mw-notification'); | |||
for (var i = 0; i < ns.length; i++) { if (/Language changed/i.test(ns[i].textContent || '')) { ns[i].style.display = 'none'; } } | |||
} | |||
kill(); | |||
if (window.MutationObserver) { | |||
var area = document.querySelector('.mw-notification-area') || document.body; | |||
new MutationObserver(kill).observe(area, { childList: true, subtree: true }); | |||
} | |||
}); | |||
/* Volunteer roles: pick one (captured into the request) */ | |||
ready(function () { | |||
if (mw.config.get('wgCanonicalSpecialPageName') !== 'RequestAccount') { return; } | |||
var content = document.querySelector('#mw-content-text .mw-body-content') || document.getElementById('mw-content-text'); | |||
if (!content || document.querySelector('.ic-roles')) { return; } | |||
var ROLES = [ | |||
['Submitter', '', 'Spot links about ICE on social media and drop them in. The easiest way to help.'], | |||
['Sorter', 'ic-role--b', 'Turn submitted links into structured records \u2014 agent photos, incidents with locations, license plates.'], | |||
['Confirm', 'ic-role--g', 'Quality-check finished records before they reach the admins.'], | |||
['General help', 'ic-role--n', 'Not sure where you fit? Pick this and we will place you.'] | |||
]; | |||
var box = document.createElement('div'); box.className = 'ic-roles'; | |||
var html = '<div class="ic-veyebrow">Choose your volunteer role</div>' | |||
+ '<p>Approved volunteers work only in private review queues \u2014 <b>nothing you submit is public until an administrator publishes it.</b> <b>Tap the role you would like</b> and we will note it on your request:</p>' | |||
+ '<div class="ic-roles__grid">'; | |||
ROLES.forEach(function (r) { html += '<div class="ic-role ' + r[1] + '" role="button" tabindex="0" data-role="' + r[0] + '"><b>' + r[0] + '</b><span>' + r[2] + '</span></div>'; }); | |||
html += '</div>'; | |||
box.innerHTML = html; | |||
var form = content.querySelector('form:not(#searchform)'); | |||
if (form) { form.parentNode.insertBefore(box, form); } else { content.insertBefore(box, content.firstChild); } | |||
var bio = document.getElementById('wpBio'); | |||
function pick(card) { | |||
var cards = box.querySelectorAll('.ic-role'); | |||
for (var j = 0; j < cards.length; j++) { cards[j].classList.remove('is-selected'); } | |||
card.classList.add('is-selected'); | |||
if (bio) { | |||
var line = 'Preferred volunteer role: ' + card.getAttribute('data-role') + '.'; | |||
var rest = bio.value.replace(/^Preferred volunteer role:[^\n]*\n?\n?/, ''); | |||
bio.value = line + '\n\n' + rest; | |||
} | |||
} | |||
var cards = box.querySelectorAll('.ic-role'); | |||
for (var j = 0; j < cards.length; j++) { | |||
cards[j].addEventListener('click', function () { pick(this); }); | |||
cards[j].addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(this); } }); | |||
} | |||
}); | |||
/* Submission pipeline: advance items with a Mark-done button (removes from the list) */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var STAGE = { | |||
'ICE List:Submission/Sorting': { to: 'sorted', label: 'Mark sorted ✓' }, | |||
'ICE List:Submission/Confirm': { to: 'confirmed', label: 'Confirm ✓' } | |||
}; | |||
var st = STAGE[pn]; | |||
if (!st) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
var items = document.querySelectorAll('.ic-sub-item[data-page]'); | |||
for (var i = 0; i < items.length; i++) { | |||
(function (it) { | |||
var page = it.getAttribute('data-page'), url = it.getAttribute('data-url'); | |||
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; } var sl = document.createElement('a'); sl.href = '/index.php/' + encodeURI(page.replace(/ /g, '_')); sl.className = 'ic-sub-sort'; sl.textContent = 'Sort this \u2192'; act.appendChild(sl); | |||
if (url) { var op = document.createElement('a'); op.href = url; op.target = '_blank'; op.rel = 'noopener noreferrer'; op.className = 'ic-sub-open'; op.textContent = 'Open link ↗'; act.appendChild(op); } | |||
var btn = document.createElement('a'); btn.href = '#'; btn.className = 'ic-sub-done'; btn.textContent = st.label; | |||
btn.addEventListener('click', function (e) { | |||
e.preventDefault(); btn.textContent = 'Saving…'; | |||
api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) { | |||
var c = d.query.pages[0].revisions[0].slots.main.content; | |||
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1' + st.to) : c.replace(/\}\}/, '|status=' + st.to + '}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc, summary: 'Pipeline: advance to ' + st.to }); | |||
}).then(function () { it.style.transition = 'opacity .3s'; it.style.opacity = '0'; setTimeout(function () { it.style.display = 'none'; }, 300); }, | |||
function (err) { btn.textContent = 'Failed — retry'; }); | |||
}); | |||
act.appendChild(btn); | |||
})(items[i]); | |||
} | |||
}); | |||
}); | |||
/* Inbox: turn every item into an inviting, whole-card-clickable "Work on this" affordance */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'ICE List:Submission/Inbox') { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; } | |||
var items = document.querySelectorAll('.ic-sub-item[data-page]'); | |||
for (var i = 0; i < items.length; i++) { | |||
(function (it) { | |||
var page = it.getAttribute('data-page'), url = it.getAttribute('data-url'); | |||
var act = it.querySelector('.ic-sub-actions'); if (!act || act.querySelector('.ic-sub-work')) { return; } | |||
var w = document.createElement('a'); w.href = '/index.php/' + encodeURI((page || '').replace(/ /g, '_')); w.className = 'ic-sub-work'; w.textContent = 'Work on this →'; act.appendChild(w); | |||
if (url) { var op = document.createElement('a'); op.href = url; op.target = '_blank'; op.rel = 'noopener noreferrer'; op.className = 'ic-sub-open'; op.textContent = 'Peek at the link ↗'; act.appendChild(op); } | |||
it.classList.add('ic-sub-item--live'); | |||
it.addEventListener('click', function (e) { if (e.target.closest('a') || (window.getSelection && window.getSelection().toString())) { return; } window.location.href = w.href; }); | |||
})(items[i]); | |||
} | |||
}); | |||
/* Sorter enrichment: open a link, pull agent/vehicle/incident records (with screenshots) into the review list */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (!/^ICE List:Submission\/(Inbox|Sorting)\/.+/.test(pn)) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; } | |||
var justSubmitter = g.indexOf('submitter') >= 0 && g.indexOf('sorter') < 0 && g.indexOf('confirm') < 0 && g.indexOf('sysop') < 0 && !/^ICEListAdmin/.test(u); | |||
var content = document.querySelector('.mw-parser-output'); if (!content) { return; } | |||
var urlEl = content.querySelector('.ic-sub-url a'); var srcUrl = urlEl ? urlEl.href : ''; | |||
var panel = document.createElement('div'); panel.className = 'ic-sortwork'; | |||
panel.innerHTML = '<div class="ic-veyebrow">' + (justSubmitter ? 'Want to do more? Sort this link' : 'Sort this link') + '</div>' | |||
+ (justSubmitter ? '<p class="ic-aa-alt">Optional — only if you fancy it. Pulling the facts out of a link is the next job up.</p>' : '') | |||
+ '<p>Pull structured records out of this link. They go to <b>Confirm</b> for review — they do <b>not</b> publish live.</p>'+ '<ul class="ic-sw-guide"><li><b>Agent</b> \u2014 grab a clear face / name screenshot from the footage.</li><li><b>Vehicle</b> \u2014 screenshot the plate, then note make, model and colour.</li><li><b>Incident</b> \u2014 note the date, location and what happened.</li><li><b>Note</b> \u2014 anything else useful.</li></ul>' | |||
+ (srcUrl ? '<p><a class="ic-vbtn" href="' + srcUrl + '" target="_blank" rel="noopener noreferrer">Open the link ↗</a></p>' : '') | |||
+ '<div class="ic-sw__btns"><a href="#" class="ic-vbtn ic-sw-add" data-t="agent">+ Agent</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="vehicle">+ Vehicle</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="incident">+ Incident</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="note">+ Note</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="lead">+ Lead</a></div>' | |||
+ '<div class="ic-sw-form"></div><div class="ic-sw-added"></div>' | |||
+ '<div class="ic-sw-finish"><a href="#" class="ic-vbtn ic-sw-finish-btn">Done with this link — send to Confirm ✓</a> <span class="ic-sw-finmsg"></span></div>'; | |||
content.insertBefore(panel, content.firstChild); | |||
var FIELDS = { | |||
agent: [['name', 'Name / alias'], ['agency', 'Agency'], ['role', 'Role / rank'], ['state', 'State'], ['notes', 'Notes', 1]], | |||
vehicle: [['plate', 'License plate'], ['vtype', 'Make / model / type'], ['agency', 'Agency'], ['state', 'State'], ['notes', 'Notes', 1]], | |||
incident: [['date', 'Date (YYYY-MM-DD)'], ['location', 'Location'], ['state', 'State'], ['notes', 'What happened', 1]], | |||
note: [['notes', 'Note / context', 1]], | |||
lead: [['subject', 'Who or what is this about?'], ['finding', 'What did you find? Your lead or idea', 1], ['sources', 'Sources & evidence links (one per line)', 1], ['needs', 'What still needs checking?', 1]] | |||
}; | |||
var formBox = panel.querySelector('.ic-sw-form'), added = panel.querySelector('.ic-sw-added'); | |||
function openForm(t) { | |||
var f = FIELDS[t], html = '<div class="ic-sw-card"><div class="ic-sw-card__h">New ' + t + '</div>'; | |||
f.forEach(function (fl) { html += '<label class="ic-sw-fld"><span>' + fl[1] + '</span>' + (fl[2] ? '<textarea rows="2" data-k="' + fl[0] + '"></textarea>' : '<input data-k="' + fl[0] + '">') + '</label>'; }); | |||
if (t !== 'note' && t !== 'lead') { html += '<label class="ic-sw-fld"><span>Screenshot ' + (t === 'incident' ? '(optional)' : '(from footage)') + '</span><input type="file" accept="image/*" class="ic-sw-file"></label>'; } | |||
html += '<div class="ic-vact"><a href="#" class="ic-vbtn ic-sw-save" data-t="' + t + '">Add to review list</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-cancel">Cancel</a> <span class="ic-sw-msg"></span></div></div>'; | |||
formBox.innerHTML = html; | |||
} | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); } | |||
panel.addEventListener('click', function (e) { | |||
var fin = e.target.closest('.ic-sw-finish-btn'); | |||
if (fin) { | |||
e.preventDefault(); | |||
var fm = panel.querySelector('.ic-sw-finmsg'); fm.textContent = 'Saving…'; fin.style.pointerEvents = 'none'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', titles: pn, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) { | |||
var c = d.query.pages[0].revisions[0].slots.main.content; | |||
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1sorted') : c.replace(/\}\}/, '|status=sorted}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: pn, text: nc, summary: 'Pipeline: link sorted, sent to Confirm' }); | |||
}).then(function () { | |||
fm.textContent = ''; fin.textContent = '✓ Sent to Confirm'; | |||
panel.insertAdjacentHTML('beforeend', '<p class="ic-sw-done">This link is done and out of the inbox. <a href="#" class="ic-sw-next">Sort the next link →</a></p>'); | |||
}, function () { fm.textContent = 'Failed — retry'; fin.style.pointerEvents = ''; }); | |||
}); | |||
return; | |||
} | |||
var nx = e.target.closest('.ic-sw-next'); | |||
if (nx) { | |||
e.preventDefault(); nx.textContent = 'Finding…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Submission inbox', cmlimit: '50', cmtype: 'page' }).then(function (d) { | |||
var m = (d.query.categorymembers || []).map(function (x) { return x.title; }).filter(function (t) { return /\/link-/.test(t) && t !== pn; }); | |||
if (m.length) { location.href = '/index.php/' + encodeURI(m[0].replace(/ /g, '_')); } else { nx.textContent = 'Inbox empty — nice work'; } | |||
}); | |||
}); | |||
return; | |||
} | |||
var a = e.target.closest('.ic-sw-add'); if (a) { e.preventDefault(); openForm(a.getAttribute('data-t')); return; } | |||
if (e.target.closest('.ic-sw-cancel')) { e.preventDefault(); formBox.innerHTML = ''; return; } | |||
var s = e.target.closest('.ic-sw-save'); if (!s) { return; } | |||
e.preventDefault(); | |||
var t = s.getAttribute('data-t'), card = s.closest('.ic-sw-card'), msg = card.querySelector('.ic-sw-msg'); | |||
var vals = {}; card.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; }); | |||
var fileEl = card.querySelector('.ic-sw-file'), file = fileEl && fileEl.files[0]; | |||
s.style.pointerEvents = 'none'; msg.textContent = 'Saving…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(), ts = Date.now().toString(36) + Math.floor(Math.random() * 1000); | |||
function createRecord(img) { | |||
var parts = ['{' + '{Sorting record|type=' + t.charAt(0).toUpperCase() + t.slice(1)]; | |||
Object.keys(vals).forEach(function (k) { if (vals[k]) { parts.push('|' + k + '=' + esc(vals[k])); } }); | |||
if (img) { parts.push('|image=' + img); } | |||
parts.push('|source=' + srcUrl + '|status=pending}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/' + t + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Sorter added ' + t }) | |||
.then(function () { added.insertAdjacentHTML('afterbegin', '<div class="ic-sw-done">✓ ' + t + ' added to the review list.</div>'); formBox.innerHTML = ''; }); | |||
} | |||
if (file) { | |||
var ext = (file.name.split('.').pop() || 'png').toLowerCase(); | |||
api.upload(file, { filename: 'Sub-' + t + '-' + ts + '.' + ext, comment: 'Sorter screenshot', ignorewarnings: true }) | |||
.then(function () { createRecord('Sub-' + t + '-' + ts + '.' + ext); }, function (err) { msg.textContent = 'Upload failed: ' + err; s.style.pointerEvents = ''; }); | |||
} else { createRecord('').then(null, function (err) { msg.textContent = 'Failed: ' + err; s.style.pointerEvents = ''; }); } | |||
}); | |||
}); | |||
}); | |||
/* Ask an admin: in-wiki question box → gated help-desk queue */ | |||
ready(function () { | |||
var host = document.getElementById('ic-askadmin'); if (!host) { return; } | |||
var u = mw.config.get('wgUserName'); | |||
if (!u) { | |||
host.innerHTML = '<div class="ic-aa"><p><b>Sign in to ask.</b> The ask box sends your question straight to the admins — it needs an account so they can reply to you.</p>' | |||
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Ask_an_admin">Log in</a> ' | |||
+ '<a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Ask_an_admin">Create an account</a></p>' | |||
+ '<p class="ic-aa-alt">Can’t create an account at all? Message <a href="https://bsky.app/profile/crustnews.com" target="_blank" rel="noopener noreferrer">@crustnews.com on Bluesky</a>.</p></div>'; | |||
return; | |||
} | |||
host.innerHTML = '<div class="ic-aa"><label class="ic-sw-fld"><span>Your question</span>' | |||
+ '<textarea rows="4" id="ic-aa-q" placeholder="What are you stuck on? Plain words are fine."></textarea></label>' | |||
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-aa-send">Send to the admins</a> <span id="ic-aa-msg"></span></div>' | |||
+ '<p class="ic-aa-alt">An admin will reply on <b>your talk page</b> — you’ll get a notification.</p></div>'; | |||
document.getElementById('ic-aa-send').addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
var q = document.getElementById('ic-aa-q').value.trim(); | |||
var msg = document.getElementById('ic-aa-msg'); | |||
if (!q) { msg.textContent = 'Type a question first.'; return; } | |||
this.style.pointerEvents = 'none'; msg.textContent = 'Sending…'; | |||
var btn = this; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
new mw.Api().postWithToken('csrf', { | |||
action: 'edit', title: 'ICE List:Submission/Inbox/Help desk', section: 'new', | |||
sectiontitle: 'Question from ' + u, | |||
text: q.replace(/~{3,}/g, '') + '\n\ncc [[User:ICEListAdmin6|ICEListAdmin6]] — [[User:AdminBot|AdminBot]] ([[User talk:AdminBot|talk]]) 14:21, 27 July 2026 (UTC)', | |||
summary: 'Volunteer question' | |||
}).then(function () { | |||
msg.textContent = ''; | |||
btn.textContent = '✓ Sent — an admin will reply on your talk page'; | |||
}, function (err) { | |||
msg.textContent = 'Failed (' + err + ') — try again'; | |||
btn.style.pointerEvents = ''; | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Community check (vetting): pod members confirm new links before sorters see them */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var isItem = /^ICE List:Submission\/Inbox\/link-/.test(pn); | |||
var isQueue = pn === 'ICE List:Submission/Vetting'; | |||
if (!isItem && !isQueue) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
var pod = null; for (var pi = 1; pi <= 4; pi++) { if (g.indexOf('pod' + pi) >= 0) { pod = pi; break; } } | |||
var isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''); | |||
if (isQueue) { | |||
var qits = document.querySelectorAll('.ic-sub-item[data-page]'); | |||
for (var qj = 0; qj < qits.length; qj++) (function (it) { | |||
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; } | |||
var a = document.createElement('a'); a.href = '/index.php/' + encodeURI(it.getAttribute('data-page').replace(/ /g, '_')); | |||
a.className = 'ic-sub-sort'; a.textContent = 'Check this link →'; act.appendChild(a); | |||
})(qits[qj]); | |||
return; | |||
} | |||
var it = document.querySelector('.ic-sub-item[data-page]'); if (!it) { return; } | |||
if (it.getAttribute('data-status') !== 'new' || it.getAttribute('data-arch') === '1') { return; } | |||
var VET_GOAL = 2; // TEMP for promo surge: lowered from 4 so new contributions flow while the crowd grows (matches Template:Submission link >=2) | |||
var vets = parseInt(it.getAttribute('data-vets') || '0', 10); | |||
if (vets >= VET_GOAL) { return; } | |||
var box = document.createElement('div'); box.className = 'ic-vetbox'; | |||
box.innerHTML = '<div class="ic-veyebrow">Community check</div>' | |||
+ '<p><b>' + vets + ' of ' + VET_GOAL + '</b> independent checks done. New links reach the sorters once ' + VET_GOAL + ' different volunteers confirm they’re real and in scope.</p>' | |||
+ '<div class="ic-vact"></div>'; | |||
var actv = box.querySelector('.ic-vact'); | |||
function vetBtn(label, fill) { | |||
var b = document.createElement('a'); b.href = '#'; b.className = 'ic-vbtn'; b.textContent = label; | |||
b.addEventListener('click', function (e) { | |||
e.preventDefault(); b.style.pointerEvents = 'none'; b.textContent = 'Saving…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', titles: pn, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) { | |||
var c = d.query.pages[0].revisions[0].slots.main.content; | |||
var src = /\|\s*source\s*=\s*([^|}\n]*)/.exec(c); | |||
if (!fill && src && src[1].trim() === u) { b.textContent = 'You can’t check your own link'; return null; } | |||
if (!fill && new RegExp('\\|\\s*v' + pod + '\\s*=\\s*[^|}\\n]*\\S').test(c)) { b.textContent = 'Your group already checked this one'; return null; } | |||
var slots = fill ? [1, 2, 3, 4] : [pod]; | |||
slots.forEach(function (n) { | |||
if (fill && new RegExp('\\|\\s*v' + n + '\\s*=\\s*[^|}\\n]*\\S').test(c)) { return; } | |||
var val = fill ? 'admin:' + u : u; | |||
var re = new RegExp('(\\|\\s*v' + n + '\\s*=\\s*)[^|}\\n]*'); | |||
if (re.test(c)) { c = c.replace(re, '$1' + val); } | |||
else { var k = c.lastIndexOf('}}'); c = c.slice(0, k) + '|v' + n + '=' + val + c.slice(k); } | |||
}); | |||
return api.postWithToken('csrf', { action: 'edit', title: pn, text: c, summary: fill ? 'Admin: mark fully checked' : 'Community check' }); | |||
}).then(function (r) { if (r) { location.reload(); } }, function () { b.textContent = 'Failed — retry'; b.style.pointerEvents = ''; }); | |||
}); | |||
}); | |||
actv.appendChild(b); | |||
} | |||
if (pod) { vetBtn('Confirm this link is real & in scope ✓', false); } | |||
if (isAdmin) { vetBtn('(Admin) Mark fully checked', true); } | |||
if (!pod && !isAdmin) { actv.innerHTML = '<span class="ic-aa-alt">Your account isn’t in a checking group yet — an admin adds you.</span>'; } | |||
var vcontent = document.querySelector('.mw-parser-output'); | |||
if (vcontent) { vcontent.insertBefore(box, vcontent.firstChild); } | |||
}); | |||
/* Admin: recent-signups panel on the admin's own profile */ | |||
ready(function () { | |||
var u = mw.config.get('wgUserName'), g = mw.config.get('wgUserGroups') || []; | |||
if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''))) { return; } | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn !== 'User:' + u) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', list: 'logevents', letype: 'newusers', lelimit: 12, formatversion: 2 }).then(function (d) { | |||
var evs = (d.query.logevents || []).filter(function (e) { return e.user; }); | |||
if (!evs.length) { return; } | |||
api.get({ action: 'query', list: 'users', ususers: evs.map(function (e) { return e.user; }).join('|'), usprop: 'editcount|groups', formatversion: 2 }).then(function (d2) { | |||
var info = {}; (d2.query.users || []).forEach(function (x) { info[x.name] = x; }); | |||
var box = document.createElement('div'); box.className = 'ic-newvols'; | |||
var h = '<div class="ic-veyebrow">New volunteers</div><table><tr><th>account</th><th>role</th><th>edits</th><th></th></tr>'; | |||
evs.forEach(function (e) { | |||
var x = info[e.user] || {}; | |||
var gr = (x.groups || []).filter(function (r) { return ['submitter', 'sorter', 'confirm'].indexOf(r) >= 0; }).join(', ') || '—'; | |||
var enc = encodeURIComponent(e.user.replace(/ /g, '_')); | |||
h += '<tr><td><a href="/index.php/User:' + enc + '">' + e.user + '</a> <span class="ic-nv-when">' + (e.timestamp || '').slice(0, 10) + '</span></td><td>' + gr + '</td><td>' + (x.editcount || 0) + '</td>' | |||
+ '<td><a href="/index.php/Special:Contributions/' + enc + '">activity</a> · <a href="/index.php/Special:UserRights/' + enc + '">rights</a> · <a href="/index.php?title=Special:Block/' + enc + '">block</a></td></tr>'; | |||
}); | |||
h += '</table>'; | |||
box.innerHTML = h; | |||
var vd = document.querySelector('.ic-vdesk'); | |||
var host = document.querySelector('.mw-parser-output'); | |||
if (vd && vd.parentNode) { vd.parentNode.insertBefore(box, vd.nextSibling); } | |||
else if (host) { host.insertBefore(box, host.firstChild); } | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Suggest an update / sighting — crowd-OSINT on agent & 287(g) agency pages; everything routes to approval */ | |||
ready(function () { | |||
if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgNamespaceNumber') !== 0) { return; } | |||
var cats = mw.config.get('wgCategories') || []; | |||
var isAgent = cats.indexOf('Agents') >= 0; | |||
var isAgency = cats.some(function (c) { return /287\(g\)/.test(c); }); | |||
if (!isAgent && !isAgency) { return; } | |||
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-suggest')) { return; } | |||
function esc(v) { return (v || '').replace(/[{}|\[\]<>]/g, '').trim(); } | |||
var subject = mw.config.get('wgTitle'); | |||
var wrap = document.createElement('div'); wrap.className = 'ic-suggest'; | |||
wrap.innerHTML = '<button type="button" class="ic-suggest__open">+ Suggest a photo, a name, or a sighting</button>' | |||
+ '<div class="ic-suggest__form" hidden>' | |||
+ '<div class="ic-suggest__kick">Know something about ' + esc(subject) + '? Suggest it — a checker reviews everything before it appears on the page, and you stay anonymous.</div>' | |||
+ '<label class="ic-sg-l"><span>What kind of tip?</span><select class="ic-sg-type">' | |||
+ '<option value="Sighting">I think I saw them / it somewhere</option>' | |||
+ '<option value="Photo">A photo (paste a link to it)</option>' | |||
+ '<option value="Name">Who they are — name, rank, unit</option>' | |||
+ '<option value="Correction">A correction to this page</option>' | |||
+ '<option value="Other">Something else</option></select></label>' | |||
+ '<label class="ic-sg-l ic-sg-whererow"><span>Where & when</span><input class="ic-sg-where" placeholder="City, state — and a date if you have one"></label>' | |||
+ '<label class="ic-sg-l"><span>Details</span><textarea class="ic-sg-details" rows="3" placeholder="What did you see, or what should change?"></textarea></label>' | |||
+ '<label class="ic-sg-l"><span>Link / source <span class="ic-sg-opt">(optional, but it helps a lot)</span></span><input class="ic-sg-src" placeholder="https://… a post, article, or photo"></label>' | |||
+ '<div class="ic-sg-cap" style="display:none"><label class="ic-sg-l"><span class="ic-sg-cap-q"></span><input class="ic-sg-cap-a"></label></div>' | |||
+ '<div class="ic-sg-act"><a href="#" class="ic-vbtn ic-sg-send">Send suggestion</a> <span class="ic-sg-msg"></span></div>' | |||
+ '<div class="ic-sg-priv">Not shown publicly until a checker approves it. First time? <a href="/index.php/ICE_List:Staying_safe">Staying safe →</a></div></div>'; | |||
content.insertBefore(wrap, content.firstChild); | |||
var openBtn = wrap.querySelector('.ic-suggest__open'), form = wrap.querySelector('.ic-suggest__form'), capId = null; | |||
openBtn.addEventListener('click', function () { if (form.hasAttribute('hidden')) { form.removeAttribute('hidden'); openBtn.textContent = '× Close'; } else { form.setAttribute('hidden', ''); openBtn.textContent = '+ Suggest a photo, a name, or a sighting'; } }); | |||
var typeSel = wrap.querySelector('.ic-sg-type'), whereRow = wrap.querySelector('.ic-sg-whererow'); | |||
function syncWhere() { whereRow.style.display = (typeSel.value === 'Sighting') ? '' : 'none'; } | |||
typeSel.addEventListener('change', syncWhere); syncWhere(); | |||
wrap.querySelector('.ic-sg-send').addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
var msg = wrap.querySelector('.ic-sg-msg'), btn = this; | |||
var type = typeSel.value, where = wrap.querySelector('.ic-sg-where').value, details = wrap.querySelector('.ic-sg-details').value, src = wrap.querySelector('.ic-sg-src').value; | |||
if (!esc(details) && !esc(src)) { msg.textContent = 'Add a detail or a link first.'; return; } | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…'; | |||
var user = mw.config.get('wgUserName'); | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function done() { form.innerHTML = '<div class="ic-suggest__kick">✓ Sent — thank you. A checker will review your suggestion about ' + esc(subject) + '.</div>'; } | |||
function fail(err) { btn.style.pointerEvents = ''; msg.textContent = 'Could not send: ' + err; } | |||
if (user) { | |||
var finding = type + (esc(where) ? ' — ' + esc(where) : '') + (esc(details) ? ': ' + esc(details) : ''); | |||
var rec = '{' + '{Sorting record|type=Lead|subject=' + esc(subject) + '|finding=' + finding + '|sources=' + esc(src) + '|needs=Verify, then update [' + '[' + esc(subject) + ']]|source=suggestion|status=pending}}'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: rec, createonly: 1, summary: 'Suggestion about ' + esc(subject) }).then(done, fail); | |||
} else { | |||
var note = 'Suggestion (' + esc(type) + ') about [' + '[' + esc(subject) + ']]' + (esc(where) ? ' — ' + esc(where) : '') + (esc(details) ? ': ' + esc(details) : ''); | |||
var body = '{' + '{Submission link|url=' + esc(src) + '|note=' + note + '|source=suggestion|status=new}}'; | |||
var params = { action: 'edit', title: 'ICE List:Submission/Inbox/link-suggest-anon-' + ts, text: body, createonly: 1, summary: 'Anonymous suggestion', formatversion: 2 }; | |||
var capA = wrap.querySelector('.ic-sg-cap-a'); | |||
if (capId && capA && capA.value) { params.captchaid = capId; params.captchaword = capA.value; } | |||
function nudge() { form.innerHTML = '<div class="ic-suggest__kick">Almost — anonymous suggestions need a free, 20-second account so we can weed out bad actors. <a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">Create an account →</a> then reopen this. Your tip was not lost.</div>'; } | |||
api.postWithToken('csrf', params).then(function (r) { | |||
var ed = r && r.edit; | |||
if (ed && ed.result === 'Success') { done(); return; } | |||
if (ed && ed.captcha) { capId = ed.captcha.id; wrap.querySelector('.ic-sg-cap').style.display = ''; wrap.querySelector('.ic-sg-cap-q').textContent = 'One quick check to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then tap Send suggestion again.'; btn.style.pointerEvents = ''; return; } | |||
nudge(); | |||
}, function () { nudge(); }); | |||
} | |||
}); | |||
}); | |||
}); | |||
/* Volunteer task board: claim / mark done, admin add-task */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var onBoard = (pn === 'ICE List:Submission/Tasks'); | |||
var onTask = /^ICE List:Submission\/Tasks\/.+/.test(pn); | |||
if (!onBoard && !onTask) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
if (!u) { return; } | |||
var isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); } | |||
function setFields(page, fields) { | |||
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) { | |||
var c = dd.query.pages[0].revisions[0].slots.main.content; | |||
Object.keys(fields).forEach(function (k) { | |||
var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^|}\\n]*'); | |||
if (re.test(c)) { c = c.replace(re, '$1' + fields[k]); } else { c = c.replace(/\}\}/, '|' + k + '=' + fields[k] + '}}'); } | |||
}); | |||
return api.postWithToken('csrf', { action: 'edit', title: page, text: c, summary: 'Task update' }); | |||
}); | |||
} | |||
var addHost = document.getElementById('ic-taskadd'); | |||
if (onBoard && isAdmin && addHost) { | |||
addHost.className = 'ic-taskadd'; | |||
addHost.innerHTML = '<div class="ic-veyebrow">Add a task</div>' | |||
+ '<label class="ic-sw-fld"><span>Task — what needs doing?</span><input id="ic-tk-title" placeholder="e.g. Add sources to this incident"></label>' | |||
+ '<label class="ic-sw-fld"><span>About which page? (optional — paste an exact incident/agent/facility title)</span><input id="ic-tk-target" placeholder="Exact page title"></label>' | |||
+ '<label class="ic-sw-fld"><span>Type</span><select id="ic-tk-type"><option value="sources">🔗 Sources</option><option value="photo">📷 Photo</option><option value="verify">✅ Verify</option><option value="research">🔎 Research</option><option value="translate">🌐 Translate</option><option value="cleanup">🧹 Cleanup</option><option value="task">📋 Other</option></select></label>' | |||
+ '<label class="ic-sw-fld"><span>Details (optional)</span><textarea rows="2" id="ic-tk-detail"></textarea></label>' | |||
+ '<label style="display:inline-flex;gap:.4em;align-items:center;font-size:.9em;margin:.2em 0 .5em"><input type="checkbox" id="ic-tk-pri"> Mark as priority</label>' | |||
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-tk-add">Add task</a> <span id="ic-tk-msg"></span></div>'; | |||
document.getElementById('ic-tk-add').addEventListener('click', function (e) { | |||
e.preventDefault(); var btn = this, msg = document.getElementById('ic-tk-msg'); | |||
var title = esc(document.getElementById('ic-tk-title').value); | |||
if (!title) { msg.textContent = 'Give the task a title.'; return; } | |||
var target = esc(document.getElementById('ic-tk-target').value), type = document.getElementById('ic-tk-type').value, detail = esc(document.getElementById('ic-tk-detail').value), pri = document.getElementById('ic-tk-pri').checked ? 'high' : ''; | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Adding…'; | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var body = '{' + '{Task|title=' + title + '|target=' + target + '|type=' + type + '|priority=' + pri + '|detail=' + detail + '|by=' + u + '|status=open}}'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Tasks/' + ts, text: body, createonly: 1, summary: 'New task' }).then(function () { msg.textContent = '✓ Added.'; setTimeout(function () { location.reload(); }, 600); }, function (err) { msg.textContent = 'Failed: ' + err; btn.style.pointerEvents = ''; }); | |||
}); | |||
} | |||
document.querySelectorAll('.ic-task[data-page]').forEach(function (t) { | |||
var page = t.getAttribute('data-page'), st = t.getAttribute('data-status'), cb = t.getAttribute('data-claimedby'); | |||
var bar = t.querySelector('.ic-task__actions'); if (!bar) { return; } | |||
function mk(label, cls, fn) { var a = document.createElement('a'); a.href = '#'; a.className = 'ic-vbtn ' + (cls || ''); a.textContent = label; a.addEventListener('click', function (e) { e.preventDefault(); fn(a); }); bar.appendChild(a); } | |||
function act(fields, label) { return function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setFields(page, fields).then(function () { a.textContent = label; setTimeout(function () { location.reload(); }, 500); }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); }; } | |||
if (st === 'open') { mk('Claim this →', '', act({ status: 'claimed', claimedby: u }, '✓ claimed')); } | |||
else if (st === 'claimed') { if (cb === u || isAdmin) { mk('Mark done ✓', '', act({ status: 'done' }, '✓ done')); } mk('Unclaim', 'ic-vbtn--ghost', act({ status: 'open', claimedby: '' }, 'released')); if (cb && cb !== u && !isAdmin) { var s = document.createElement('span'); s.className = 'ic-task__self'; s.textContent = 'claimed by ' + cb; bar.appendChild(s); } } | |||
else if (st === 'done' && isAdmin) { mk('Reopen', 'ic-vbtn--ghost', act({ status: 'open', claimedby: '' }, 'reopened')); } | |||
if (isAdmin) { mk('Delete', 'ic-vbtn--ghost', function (a) { if (!confirm('Delete this task?')) { return; } a.textContent = '…'; api.postWithToken('csrf', { action: 'delete', title: page, reason: 'Task removed' }).then(function () { t.style.display = 'none'; }, function () { a.textContent = 'retry'; }); }); } | |||
}); | |||
}); | |||
}); | |||
/* Admin: request volunteer work on this page → creates a targeted task */ | |||
ready(function () { | |||
if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgNamespaceNumber') !== 0) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''))) { return; } | |||
var cats = mw.config.get('wgCategories') || []; | |||
if (!(cats.indexOf('Incidents') >= 0 || cats.indexOf('Agents') >= 0 || cats.indexOf('Vehicles') >= 0 || cats.some(function (c) { return /287\(g\)/.test(c) || /Facilit/i.test(c) || /Concentration camp/i.test(c); }))) { return; } | |||
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-reqwork')) { return; } | |||
var target = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var box = document.createElement('div'); box.className = 'ic-reqwork'; | |||
box.innerHTML = '<button type="button" class="ic-reqwork__open">➕ Request volunteer work on this page</button>' | |||
+ '<div class="ic-reqwork__form" hidden><div class="ic-veyebrow">New task · on this page</div>' | |||
+ '<label class="ic-sw-fld"><span>What needs doing here?</span><input class="ic-rw-title" placeholder="e.g. Find sources / add a photo / verify the date"></label>' | |||
+ '<label class="ic-sw-fld"><span>Type</span><select class="ic-rw-type"><option value="sources">🔗 Sources</option><option value="photo">📷 Photo</option><option value="verify">✅ Verify</option><option value="research">🔎 Research</option><option value="cleanup">🧹 Cleanup</option><option value="task">📋 Other</option></select></label>' | |||
+ '<label class="ic-sw-fld"><span>Details (optional)</span><textarea rows="2" class="ic-rw-detail"></textarea></label>' | |||
+ '<label style="display:inline-flex;gap:.4em;align-items:center;font-size:.9em;margin:.2em 0 .5em"><input type="checkbox" class="ic-rw-pri"> Priority</label>' | |||
+ '<div class="ic-vact"><a href="#" class="ic-vbtn ic-rw-add">Post to task board</a> <span class="ic-rw-msg"></span></div></div>'; | |||
content.insertBefore(box, content.firstChild); | |||
var ob = box.querySelector('.ic-reqwork__open'), fm = box.querySelector('.ic-reqwork__form'); | |||
ob.addEventListener('click', function () { if (fm.hasAttribute('hidden')) { fm.removeAttribute('hidden'); ob.textContent = '× Close'; } else { fm.setAttribute('hidden', ''); ob.textContent = '➕ Request volunteer work on this page'; } }); | |||
box.querySelector('.ic-rw-add').addEventListener('click', function (e) { | |||
e.preventDefault(); var btn = this, msg = box.querySelector('.ic-rw-msg'); | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); } | |||
var title = esc(box.querySelector('.ic-rw-title').value); | |||
if (!title) { msg.textContent = 'Say what needs doing.'; return; } | |||
var type = box.querySelector('.ic-rw-type').value, detail = esc(box.querySelector('.ic-rw-detail').value), pri = box.querySelector('.ic-rw-pri').checked ? 'high' : ''; | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Posting…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var body = '{' + '{Task|title=' + title + '|target=' + target + '|type=' + type + '|priority=' + pri + '|detail=' + detail + '|by=' + u + '|status=open}}'; | |||
new mw.Api().postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Tasks/' + ts, text: body, createonly: 1, summary: 'Task requested from ' + target }).then(function () { fm.innerHTML = '<div class="ic-veyebrow">✓ Posted to the <a href="/index.php/ICE_List:Submission/Tasks">task board</a>.</div>'; }, function (err) { msg.textContent = 'Failed: ' + err; btn.style.pointerEvents = ''; }); | |||
}); | |||
}); | |||
}); | |||
/* Personal checklist: my tasks (in progress / done) on my own profile */ | |||
ready(function () { | |||
var u = mw.config.get('wgUserName'), pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (!u || pn !== 'User:' + u) { return; } | |||
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-mytasks')) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', list: 'usercontribs', ucuser: u, uclimit: 500, ucprop: 'title', formatversion: 2 }).then(function (d) { | |||
var seen = {}, pages = []; | |||
(d.query.usercontribs || []).forEach(function (x) { if (/^ICE List:Submission\/Tasks\/.+/.test(x.title) && !seen[x.title]) { seen[x.title] = 1; pages.push(x.title); } }); | |||
if (!pages.length) { return; } | |||
api.get({ action: 'query', titles: pages.slice(0, 50).join('|'), prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) { | |||
var prog = [], done = []; | |||
(dd.query.pages || []).forEach(function (p) { | |||
if (!p.revisions) { return; } | |||
var c = p.revisions[0].slots.main.content; | |||
function f(k) { var m = new RegExp('\\|\\s*' + k + '\\s*=([^|}\\n]*)').exec(c); return m ? m[1].trim() : ''; } | |||
var st = f('status').toLowerCase(), title = f('title') || p.title, cb = f('claimedby'); | |||
if (st === 'claimed' && cb === u) { prog.push({ p: p.title, t: title, tg: f('target') }); } | |||
else if (st === 'done') { done.push({ p: p.title, t: title, tg: f('target') }); } | |||
}); | |||
if (!prog.length && !done.length) { return; } | |||
function row(x, checked) { return '<div class="ic-mt-row"><span class="ic-mt-box' + (checked ? ' ic-mt-box--on' : '') + '">' + (checked ? '✓' : '') + '</span><a href="/index.php/' + encodeURI(x.p.replace(/ /g, '_')) + '">' + mw.html.escape(x.t) + '</a>' + (x.tg ? ' <span class="ic-mt-tg">— ' + mw.html.escape(x.tg) + '</span>' : '') + '</div>'; } | |||
var box = document.createElement('div'); box.className = 'ic-mytasks'; | |||
box.innerHTML = '<div class="ic-veyebrow">My tasks</div>' | |||
+ (prog.length ? '<div class="ic-mt-h">In progress (' + prog.length + ')</div>' + prog.map(function (x) { return row(x, false); }).join('') : '') | |||
+ (done.length ? '<div class="ic-mt-h">Done (' + done.length + ')</div>' + done.map(function (x) { return row(x, true); }).join('') : '') | |||
+ '<div class="ic-vact"><a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Tasks">Find more tasks →</a></div>'; | |||
var vd = document.querySelector('.ic-vdesk'); if (vd && vd.parentNode) { vd.parentNode.insertBefore(box, vd.nextSibling); } else { content.insertBefore(box, content.firstChild); } | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Footage-tips swipe deck (ICE List:Footage deck) — drag + confirmed-only */ | |||
ready(function () { | |||
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'ICE List:Footage deck') { return; } | |||
var mount = document.getElementById('ic-deck'); if (!mount) { return; } | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); } | |||
function shuffle(a) { for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; } | |||
function fileUrl(f) { return '/index.php?title=Special:FilePath/' + encodeURIComponent(f); } | |||
var allRows = [].slice.call(document.querySelectorAll('.ic-di')).map(function (d) { | |||
return { inc: d.getAttribute('data-incident'), state: icNormState(d.getAttribute('data-state')), cardimg: d.getAttribute('data-cardimg'), city: d.getAttribute('data-city'), date: d.getAttribute('data-date'), agency: d.getAttribute('data-agency') }; | |||
}).filter(function (r) { return r.inc; }); | |||
mount.innerHTML = '<div class="ic-fd-empty">Loading the deck…</div>'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
new mw.Api().get({ action: 'query', titles: 'ICE List:Footage deck/hidden', prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) { | |||
var pg = dd.query.pages[0]; var txt = (pg.revisions && pg.revisions[0].slots.main.content) || ''; | |||
var conf = icConf(txt); | |||
start(allRows.filter(function (r) { return !conf[r.inc]; })); | |||
}, function () { start([]); }); | |||
}); | |||
function start(rows) { | |||
if (!rows.length) { mount.innerHTML = '<div class="ic-fd-empty">No incidents to show right now.</div>'; return; } | |||
var byState = {}; rows.forEach(function (r) { (byState[r.state] = byState[r.state] || []).push(r); }); | |||
var states = Object.keys(byState).filter(Boolean).sort(); | |||
var cur = [], idx = 0, curState = ''; | |||
mount.innerHTML = '<div class="ic-fd"><div class="ic-fd-bar"><label class="ic-fd-pick">State <select class="ic-fd-state"></select></label><span class="ic-fd-count"></span></div><div class="ic-fd-stage"></div></div>'; | |||
var sel = mount.querySelector('.ic-fd-state'), stage = mount.querySelector('.ic-fd-stage'), cnt = mount.querySelector('.ic-fd-count'); | |||
sel.innerHTML = '<option value="">Pick a state…</option>' + states.map(function (s) { return '<option value="' + s + '">' + mw.html.escape(s) + ' (' + byState[s].length + ')</option>'; }).join(''); | |||
sel.addEventListener('change', function () { curState = this.value; cur = curState ? shuffle(byState[curState].slice()) : []; idx = 0; render(); }); | |||
function render() { | |||
if (!curState) { stage.innerHTML = '<div class="ic-fd-empty">Pick a state to start swiping.</div>'; cnt.textContent = ''; return; } | |||
if (idx >= cur.length) { stage.innerHTML = '<div class="ic-fd-empty">That is every incident in ' + mw.html.escape(curState) + '. Thank you.<br><a href="#" class="ic-fd-again">Go again →</a></div>'; cnt.textContent = ''; stage.querySelector('.ic-fd-again').onclick = function (e) { e.preventDefault(); cur = shuffle(byState[curState].slice()); idx = 0; render(); }; return; } | |||
var r = cur[idx]; cnt.textContent = (idx + 1) + ' / ' + cur.length; | |||
var face = r.cardimg ? '<img class="ic-fd-img" src="' + fileUrl(r.cardimg) + '" alt="">' : '<div class="ic-fd-ph"><span>' + mw.html.escape(r.state) + '</span></div>'; | |||
var chip = [r.city, r.date, r.agency].filter(Boolean).map(mw.html.escape).join(' · '); | |||
var url = '/index.php/' + encodeURI(r.inc.replace(/ /g, '_')); | |||
stage.innerHTML = '<div class="ic-fd-card"><span class="ic-fd-tag ic-fd-tag--no">Pass</span><span class="ic-fd-tag ic-fd-tag--yes">Tip</span>' | |||
+ '<a class="ic-fd-face" href="' + url + '" target="_blank" rel="noopener">' + face + '<span class="ic-fd-watch">▶ Watch on the record</span></a>' | |||
+ '<div class="ic-fd-body"><div class="ic-fd-title">' + mw.html.escape(r.inc) + '</div>' + (chip ? '<div class="ic-fd-chip">' + chip + '</div>' : '') + '</div>' | |||
+ '<div class="ic-fd-acts"><button class="ic-fd-pass" type="button">Pass</button><button class="ic-fd-tip" type="button">I have a tip →</button></div>' | |||
+ '<div class="ic-fd-tipsheet" hidden></div></div>'; | |||
var card = stage.querySelector('.ic-fd-card'); | |||
function fly(dir){ card.style.transition = 'transform .3s ease, opacity .3s'; card.style.transform = 'translateX(' + (dir*520) + 'px) rotate(' + (dir*18) + 'deg)'; card.style.opacity = '0'; } | |||
stage.querySelector('.ic-fd-pass').onclick = function () { fly(-1); setTimeout(function(){ idx++; render(); }, 220); }; | |||
stage.querySelector('.ic-fd-tip').onclick = function () { openTip(r, card); }; | |||
var sx = 0, dxx = 0, dragging = false, moved = false; | |||
card.addEventListener('pointerdown', function (e) { if (e.target.closest('.ic-fd-acts,.ic-fd-tipsheet')) { return; } dragging = true; moved = false; sx = e.clientX; card.style.transition = 'none'; try { card.setPointerCapture(e.pointerId); } catch (_) {} }); | |||
card.addEventListener('pointermove', function (e) { if (!dragging) { return; } dxx = e.clientX - sx; if (Math.abs(dxx) > 6) { moved = true; } card.style.transform = 'translateX(' + dxx + 'px) rotate(' + (dxx / 22) + 'deg)'; card.classList.toggle('ic-fd-yes', dxx > 55); card.classList.toggle('ic-fd-no', dxx < -55); }); | |||
card.addEventListener('pointerup', function () { if (!dragging) { return; } dragging = false; card.classList.remove('ic-fd-yes', 'ic-fd-no'); if (dxx > 105) { fly(1); openTip(r, card); } else if (dxx < -105) { fly(-1); setTimeout(function () { idx++; render(); }, 220); } else { card.style.transition = 'transform .2s'; card.style.transform = ''; } dxx = 0; }); | |||
card.querySelector('.ic-fd-face').addEventListener('click', function (e) { if (moved) { e.preventDefault(); } }); | |||
} | |||
function openTip(r, card) { | |||
if (card) { card.style.transition = 'transform .25s'; card.style.transform = ''; } | |||
var sheet = stage.querySelector('.ic-fd-tipsheet'); sheet.hidden = false; | |||
sheet.innerHTML = '<div class="ic-fd-tk">What can you tell us about this?</div>' | |||
+ '<div class="ic-fd-chips">' + ['I recognize an agent', 'I was there', 'I have more footage', 'Something is wrong'].map(function (x) { return '<button type="button" class="ic-fd-c" data-c="' + x + '">' + x + '</button>'; }).join('') + '</div>' | |||
+ '<textarea class="ic-fd-note" rows="2" placeholder="Optional short note. Do not include home addresses or personal contact details."></textarea>' | |||
+ '<input class="ic-fd-link" placeholder="Optional: a link to footage or a post">' | |||
+ '<div class="ic-fd-send-row"><button type="button" class="ic-fd-send">Send tip</button> <button type="button" class="ic-fd-skip">Skip</button> <span class="ic-fd-msg"></span></div>' | |||
+ '<div class="ic-fd-priv">Anonymous. Nothing appears publicly until a checker reviews it.</div>'; | |||
var chosen = ''; | |||
sheet.querySelectorAll('.ic-fd-c').forEach(function (b) { b.onclick = function () { sheet.querySelectorAll('.ic-fd-c').forEach(function (x) { x.classList.remove('on'); }); b.classList.add('on'); chosen = b.getAttribute('data-c'); }; }); | |||
sheet.querySelector('.ic-fd-skip').onclick = function () { idx++; render(); }; | |||
sheet.querySelector('.ic-fd-send').onclick = function () { sendTip(r, chosen, sheet); }; | |||
} | |||
function sendTip(r, chosen, sheet) { | |||
var msg = sheet.querySelector('.ic-fd-msg'), btn = sheet.querySelector('.ic-fd-send'); | |||
var note = sheet.querySelector('.ic-fd-note').value, link = sheet.querySelector('.ic-fd-link').value; | |||
if (!chosen && !note.trim() && !link.trim()) { msg.textContent = 'Pick one or add a note or link.'; return; } | |||
if (/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b|lives at|home address|\d+\s+\w+\s+(street|st|ave|avenue|road|rd|drive|dr|lane|ln)\b/i.test(note)) { msg.textContent = 'Please remove phone numbers or addresses first.'; return; } | |||
btn.disabled = true; msg.textContent = 'Sending…'; | |||
var finding = (chosen || 'Tip') + (esc(note) ? ': ' + esc(note) : ''); | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(), user = mw.config.get('wgUserName'); | |||
function done() { sheet.innerHTML = '<div class="ic-fd-tk">✓ Thank you. A checker will review your tip.</div>'; setTimeout(function () { idx++; render(); }, 850); } | |||
function fail(e) { btn.disabled = false; msg.textContent = 'Could not send: ' + e; } | |||
if (user) { | |||
var rec = '{' + '{Sorting record|type=Lead|subject=' + esc(r.inc) + '|finding=' + finding + '|sources=' + esc(link) + '|needs=Footage tip on [' + '[' + esc(r.inc) + ']]|source=footage-deck|status=pending}}'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: rec, createonly: 1, summary: 'Footage tip about ' + esc(r.inc) }).then(done, fail); | |||
} else { | |||
var body = '{' + '{Submission link|url=' + esc(link) + '|note=Footage tip (' + finding + ') on [' + '[' + esc(r.inc) + ']]|source=footage-deck|status=new}}'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/link-fdtip-' + ts, text: body, createonly: 1, summary: 'Anonymous footage tip' }).then(done, function () { sheet.innerHTML = '<div class="ic-fd-tk">Almost. Anonymous tips need a free, 20-second account. <a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Footage_deck">Create an account →</a></div>'; }); | |||
} | |||
}); | |||
} | |||
render(); | |||
} | |||
}); | |||
/* Footage deck: admin management panel on own profile */ | |||
ready(function () { | |||
var u = mw.config.get('wgUserName'); if (!u) { return; } | |||
var g = mw.config.get('wgUserGroups') || []; if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; } | |||
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; } | |||
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-dm-panel')) { return; } | |||
var box = document.createElement('div'); box.className = 'ic-dm-panel'; | |||
box.innerHTML = '<div class="ic-veyebrow">Footage deck · management</div><div class="ic-dm-load">Loading incidents…</div>'; | |||
content.insertBefore(box, content.firstChild); | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
Promise.all([ | |||
api.get({ action: 'parse', page: 'ICE List:Footage deck', prop: 'text', formatversion: 2 }), | |||
api.get({ action: 'query', titles: 'ICE List:Footage deck/hidden', prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }) | |||
]).then(function (res) { | |||
var tmp = document.createElement('div'); tmp.innerHTML = res[0].parse.text; | |||
var rows = [].slice.call(tmp.querySelectorAll('.ic-di')).map(function (d) { return { inc: d.getAttribute('data-incident'), state: icNormState(d.getAttribute('data-state')), city: d.getAttribute('data-city'), date: d.getAttribute('data-date') }; }).filter(function (r) { return r.inc; }); | |||
var pg = res[1].query.pages[0]; var conf = icConf((pg.revisions && pg.revisions[0].slots.main.content) || ''); | |||
renderPanel(rows, conf, api); | |||
}, function () { box.querySelector('.ic-dm-load').textContent = 'Could not load the deck data.'; }); | |||
}); | |||
function renderPanel(rows, conf, api) { | |||
var byState = {}; rows.forEach(function (r) { (byState[r.state] = byState[r.state] || []).push(r); }); | |||
var states = Object.keys(byState).filter(Boolean).sort(); | |||
var h = '<div class="ic-veyebrow">Footage deck · management</div><p class="ic-dm-help">Every documented incident is in the deck by default. Untick any you want to <b>remove</b>. Saves as you go.</p>'; | |||
states.forEach(function (st) { | |||
var list = byState[st]; var cn = list.filter(function (r) { return !conf[r.inc]; }).length; | |||
h += '<details class="ic-dm-st"><summary>' + mw.html.escape(st) + ' <span class="ic-dm-c">' + cn + ' / ' + list.length + ' in deck</span></summary>'; | |||
list.forEach(function (r) { | |||
h += '<label class="ic-dm-row"><input type="checkbox" data-inc="' + encodeURIComponent(r.inc) + '"' + (conf[r.inc] ? '' : ' checked') + '><span class="ic-dm-t">' + mw.html.escape(r.inc) + '</span>' + (conf[r.inc] ? '<span class="ic-dm-flag">removed</span>' : '') + '</label>'; | |||
}); | |||
h += '</details>'; | |||
}); | |||
h += '<div class="ic-dm-save"><span class="ic-dm-msg"></span></div>'; | |||
box.innerHTML = h; | |||
box.addEventListener('change', function (e) { | |||
var cb = e.target.closest('input[type=checkbox]'); if (!cb) { return; } | |||
var inc = decodeURIComponent(cb.getAttribute('data-inc')); | |||
if (cb.checked) { delete conf[inc]; } else { conf[inc] = 1; } | |||
var row = cb.closest('.ic-dm-row'); var fl = row.querySelector('.ic-dm-flag'); | |||
if (cb.checked && fl) { fl.remove(); } else if (!cb.checked && !fl) { row.insertAdjacentHTML('beforeend', '<span class="ic-dm-flag">removed</span>'); } | |||
saveConf(conf, api); | |||
}); | |||
} | |||
var saveT; | |||
function saveConf(conf, api) { | |||
var msg = box.querySelector('.ic-dm-msg'); if (msg) { msg.textContent = 'Saving…'; } | |||
clearTimeout(saveT); | |||
saveT = setTimeout(function () { | |||
var lines = Object.keys(conf).sort().map(function (x) { return '* ' + x; }).join('\n'); | |||
var page = '<!-- Incidents hidden from the public footage deck. Managed from admin profiles; edit via the panel, not by hand. -->\n' + lines + '\n[[Category:Volunteer resources]]'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Footage deck/hidden', text: page, summary: 'Update footage deck confirmed list' }).then(function () { if (msg) { msg.textContent = '✓ Saved · ' + Object.keys(conf).length + ' removed.'; } }, function (e) { if (msg) { msg.textContent = 'Save failed: ' + e; } }); | |||
}, 500); | |||
} | |||
}); | |||
/* Records: confirmer confirm / send-back, and admin publish-to-live-page */ | |||
/* Records: confirmer confirm / send-back, and admin publish-to-live-page */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var onQueue = (pn === 'ICE List:Submission/Confirm' || pn === 'ICE List:Submission/Overview'); | |||
var onRecord = /^ICE List:Submission\/Sorting\/(agent|vehicle|incident|note)-/.test(pn); | |||
if (!onQueue && !onRecord) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
var isConfirm = g.indexOf('confirm') >= 0, isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''); | |||
var isVolunteer = isAdmin || ['submitter', 'sorter', 'confirm', 'photographer'].some(function (x) { return g.indexOf(x) >= 0; }); | |||
if (!isVolunteer) { return; } | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function esc(v) { return (v || '').replace(/[\[\]{}|]/g, '').trim(); } | |||
function parseRec(c) { var m = /\{\{Sorting record\|([\s\S]*?)\}\}/.exec(c), d = {}; if (m) { m[1].split('|').forEach(function (kv) { var i = kv.indexOf('='); if (i > 0) { d[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } }); } return d; } | |||
function setStatus(page, st, reason) { | |||
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) { | |||
var c = dd.query.pages[0].revisions[0].slots.main.content; | |||
c = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1' + st) : c.replace(/\}\}/, '|status=' + st + '}}'); | |||
if (reason != null) { var r = reason.replace(/[|{}]/g, ' '); c = /\|\s*reason\s*=/.test(c) ? c.replace(/(\|\s*reason\s*=\s*)[^|}\n]*/, '$1' + r) : c.replace(/\}\}/, '|reason=' + r + '}}'); } | |||
return api.postWithToken('csrf', { action: 'edit', title: page, text: c, summary: 'Pipeline: ' + st }); | |||
}); | |||
} | |||
function buildAgent(d, id, pub) { | |||
var name = esc(d.name), title, nm; | |||
if (name) { var np = name.split(/\s+/); title = np.length >= 2 ? np[np.length - 1] + ', ' + np.slice(0, -1).join(' ') : name; nm = name; } else { title = 'ICE List:Unidentified agent - ' + id; nm = 'Unidentified agent ' + id; } | |||
var body = '{{Agent page\n|name=' + nm + '\n|agency=' + (esc(d.agency) || 'Unknown') + '\n|role=' + esc(d.role) + '\n|state=' + esc(d.state) + '\n|status=Active\n|image=' + (pub || 'nopfp.png') + '\n|verification=Submitted via pipeline\n|summary=' + (esc(d.notes) || (nm + ' was documented via the submission pipeline.')) + '\n}}\n\n== Documented Incidents ==\n{{IncidentsForAgent}}\n\n{{MediaForAgent}}\n\n' + (d.source ? '== Sources ==\n* ' + d.source + '\n\n' : '') + '[[Category:Agents]]' + (pub ? '' : '[[Category:Agents needing a photo]]'); | |||
return { title: title, body: body }; | |||
} | |||
function buildVehicle(d, id, pub) { | |||
var plate = esc(d.plate) || ('unknown-' + id); | |||
return { title: 'Vehicle:' + plate, body: "'''Vehicle''' plate " + plate + (d.state ? ' (' + esc(d.state) + ')' : '') + '.\n\n* Make/model: ' + esc(d.vtype) + '\n* Agency: ' + (esc(d.agency) || 'ICE') + '\n* Notes: ' + esc(d.notes) + '\n* Source: ' + (d.source || '-') + '\n' + (pub ? '\n[[File:' + pub + '|thumb|Vehicle ' + plate + ']]\n' : '') + '\n[[Category:Vehicles]]' }; | |||
} | |||
function buildIncident(d, id, pub) { | |||
var loc = esc(d.location), st = esc(d.state), date = esc(d.date); | |||
var title = ((esc(d.notes) || 'Incident').slice(0, 50) + ' (' + (loc || 'Unknown') + (st ? ', ' + st : '') + (date ? ' ' + date : '') + ')').replace(/[\[\]{}|#<>]/g, ''); | |||
var body = '{{Infobox incident\n | title = ' + title + '\n | date = ' + date + '\n | city = ' + loc + '\n | state = ' + st + '\n | country = United States\n | status = Submitted via pipeline\n | agents_involved = ' + esc(d.agency) + '\n}}\n\n' + esc(d.notes) + '\n\n' + (pub ? '[[File:' + pub + '|thumb]]\n\n' : '') + (d.source ? '== Sources ==\n* ' + d.source + '\n\n' : '') + '[[Category:Incidents]]'; | |||
return { title: title, body: body }; | |||
} | |||
function publish(page) { | |||
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) { | |||
var c = dd.query.pages[0].revisions[0].slots.main.content, d = parseRec(c); | |||
var id = page.split('/').pop().replace(/^[a-z]+-/, ''), type = (d.type || '').toLowerCase(); | |||
var img = d.image || '', ext = img ? (img.split('.').pop() || 'jpg') : '', pub = '', built; | |||
if (type === 'agent') { pub = img ? 'Agent-' + id + '.' + ext : ''; built = buildAgent(d, id, pub); } | |||
else if (type === 'vehicle') { pub = img ? 'Vehicle-' + id + '.' + ext : ''; built = buildVehicle(d, id, pub); } | |||
else if (type === 'incident') { pub = img ? 'Incident-' + id + '.' + ext : ''; built = buildIncident(d, id, pub); } | |||
else { return Promise.reject('notes are context only — confirm & archive, don’t publish'); } | |||
var chain = Promise.resolve(); | |||
if (img) { chain = api.postWithToken('csrf', { action: 'move', from: 'File:' + img, to: 'File:' + pub, reason: 'Publish from pipeline', movetalk: 1, ignorewarnings: 1, noredirect: 1 }).then(null, function () { built.body = built.body.split(pub).join(img); }); } | |||
return chain.then(function () { | |||
return api.postWithToken('csrf', { action: 'edit', title: built.title, text: built.body, createonly: 1, summary: 'Publish confirmed record from pipeline' }); | |||
}).then(function () { | |||
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1published') : c.replace(/\}\}/, '|status=published}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc + '\n<!-- published: ' + built.title + ' -->', summary: 'Pipeline: published to ' + built.title }).then(function () { return built.title; }); | |||
}); | |||
}); | |||
} | |||
var recs = [].slice.call(document.querySelectorAll('.ic-rec[data-page]')); | |||
var pend = recs.filter(function (r) { return r.getAttribute('data-status') === 'pending'; }).map(function (r) { return r.getAttribute('data-page'); }); | |||
var creators = {}; | |||
(pend.length ? api.get({ action: 'query', titles: pend.slice(0, 50).join('|'), prop: 'revisions', rvdir: 'newer', rvlimit: 1, rvprop: 'user', formatversion: 2 }).then(function (dd) { (dd.query.pages || []).forEach(function (p) { if (p.revisions && p.revisions[0]) { creators[p.title.replace(/_/g, ' ')] = p.revisions[0].user; } }); }, function () {}) : Promise.resolve()).then(function () { | |||
recs.forEach(function (rec) { | |||
var page = rec.getAttribute('data-page'), st = rec.getAttribute('data-status'), ty = rec.getAttribute('data-type'); | |||
var bar = rec.querySelector('.ic-rec__actions'); if (!bar) { return; } | |||
function mk(label, cls, fn) { var a = document.createElement('a'); a.href = '#'; a.className = 'ic-vbtn ' + (cls || ''); a.textContent = label; a.addEventListener('click', function (e) { e.preventDefault(); fn(a); }); bar.appendChild(a); } | |||
if (st === 'pending' && isVolunteer && (isAdmin || creators[page.replace(/_/g, ' ')] !== u)) { | |||
mk('Confirm ✓', '', function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'confirmed', null).then(function () { rec.style.opacity = '.4'; a.textContent = '✓ confirmed'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); }); | |||
mk('Send back to sorting ↩', 'ic-vbtn--ghost', function (a) { var why = prompt('What needs fixing? The sorter will see this:'); if (why == null) { return; } a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'rework', why || 'needs another look').then(function () { rec.style.opacity = '.4'; a.textContent = '↩ sent back'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); }); | |||
} | |||
if (st === 'pending' && isVolunteer && !isAdmin && creators[page.replace(/_/g, ' ')] === u) { | |||
var sn = document.createElement('span'); sn.className = 'ic-rec__self'; sn.textContent = 'You submitted this — another volunteer confirms it.'; bar.appendChild(sn); | |||
} | |||
if (st === 'confirmed' && isAdmin) { | |||
if (ty === 'note' || ty === 'lead') { | |||
mk('Mark actioned ✓', '', function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'published', null).then(function () { rec.style.opacity = '.5'; a.textContent = '✓ actioned'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); }); | |||
} else { | |||
mk('Publish →', '', function (a) { a.textContent = 'Publishing…'; a.style.pointerEvents = 'none'; publish(page).then(function (title) { a.textContent = '✓ published'; var l = document.createElement('a'); l.href = '/index.php/' + encodeURI(title.replace(/ /g, '_')); l.textContent = 'view page →'; l.className = 'ic-rec__pub'; bar.appendChild(l); rec.style.opacity = '.6'; }, function (err) { a.textContent = 'Failed: ' + err; a.style.pointerEvents = ''; }); }); | |||
} | |||
} | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Photographer on-ramp: upload a photo + context → a pipeline record */ | |||
ready(function () { | |||
var host = document.getElementById('ic-photoupload'); if (!host) { return; } | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
var canUp = g.indexOf('photographer') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''); | |||
if (!u) { | |||
host.innerHTML = '<div class="ic-aa"><p><b>Log in to upload.</b> Photo uploads need an account so your work is protected.</p><p><a class="ic-vbtn" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Submit_photos">Log in</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Submit_photos">Create an account</a></p></div>'; | |||
return; | |||
} | |||
if (!canUp) { host.innerHTML = '<div class="ic-aa"><p>Your account can’t upload yet — ask an admin to add you to the <b>photographer</b> role. <a href="/index.php/ICE_List:Ask_an_admin">Ask an admin →</a></p></div>'; return; } | |||
host.innerHTML = '<div class="ic-aa">' | |||
+ '<label class="ic-sw-fld"><span>What is it?</span><select id="ic-ph-type"><option value="incident">An incident / scene</option><option value="agent">An agent</option><option value="vehicle">A vehicle</option></select></label>' | |||
+ '<label class="ic-sw-fld"><span>Photo (strip location data first — see Staying safe)</span><input type="file" accept="image/*" id="ic-ph-file"></label>' | |||
+ '<label class="ic-sw-fld"><span>Where? City & state</span><input id="ic-ph-loc"></label>' | |||
+ '<label class="ic-sw-fld"><span>When? Date</span><input id="ic-ph-date" placeholder="YYYY-MM-DD"></label>' | |||
+ '<label class="ic-sw-fld ic-ph-plate" style="display:none"><span>License plate (if a vehicle)</span><input id="ic-ph-plate"></label>' | |||
+ '<label class="ic-sw-fld"><span>What’s happening? Any detail helps</span><textarea rows="3" id="ic-ph-notes"></textarea></label>' | |||
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-ph-send">Upload to the pipeline</a> <span id="ic-ph-msg"></span></div>' | |||
+ '<p class="ic-aa-alt">A checker verifies your photo before it ever appears publicly. Never put yourself at risk to get a shot.</p></div>'; | |||
var typeSel = document.getElementById('ic-ph-type'); | |||
typeSel.addEventListener('change', function () { host.querySelector('.ic-ph-plate').style.display = this.value === 'vehicle' ? '' : 'none'; }); | |||
document.getElementById('ic-ph-send').addEventListener('click', function (e) { | |||
e.preventDefault(); var btn = this, msg = document.getElementById('ic-ph-msg'); | |||
function val(id) { var el = document.getElementById(id); return el ? el.value : ''; } | |||
var file = document.getElementById('ic-ph-file').files[0]; | |||
if (!file) { msg.textContent = 'Choose a photo first.'; return; } | |||
var t = typeSel.value, vals = { location: val('ic-ph-loc'), date: val('ic-ph-date'), notes: val('ic-ph-notes'), plate: val('ic-ph-plate') }; | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Uploading…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(), ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var ext = (file.name.split('.').pop() || 'jpg').toLowerCase(), fn = 'Sub-photo-' + ts + '.' + ext; | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); } | |||
api.upload(file, { filename: fn, comment: 'Photographer upload', ignorewarnings: true }).then(function () { | |||
var parts = ['{' + '{Sorting record|type=' + t.charAt(0).toUpperCase() + t.slice(1) + '|image=' + fn]; | |||
['location', 'date', 'notes', 'plate'].forEach(function (k) { if (esc(vals[k])) { parts.push('|' + k + '=' + esc(vals[k])); } }); | |||
parts.push('|source=photographer|status=pending}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/' + t + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Photographer upload' }); | |||
}).then(function () { host.innerHTML = '<div class="ic-aa"><p><b>✓ Uploaded.</b> Thank you. A checker will verify it, and it may become a page on the site. <a href="#" onclick="location.reload();return false;">Upload another →</a></p></div>'; }, | |||
function (err) { msg.textContent = 'Failed: ' + err + ' — try again'; btn.style.pointerEvents = ''; }); | |||
}); | |||
}); | |||
}); | |||
/* Standalone "Submit a lead" (OSINT) → a Sorting record type=Lead in the review queue */ | |||
ready(function () { | |||
var host = document.getElementById('ic-leadform'); if (!host) { return; } | |||
var u = mw.config.get('wgUserName'); | |||
if (!u) { | |||
host.innerHTML = '<div class="ic-aa"><p><b>Log in to submit a lead.</b> OSINT leads go into the review queue, so they need an account — 30 seconds, alias only.</p>' | |||
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Submit_a_lead">Create an account →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Submit_a_lead">Log in</a></p></div>'; | |||
return; | |||
} | |||
var F = [['subject', 'Who or what is this about? (an agent, incident, facility, company)'], ['finding', 'What did you find? Your lead or idea', 1], ['sources', 'Sources & evidence links (one per line)', 1], ['needs', 'What still needs checking?', 1]]; | |||
var kind = ((mw.util && mw.util.getParamValue && mw.util.getParamValue('suggest')) || '').toLowerCase(); | |||
var CTX = { | |||
photo: { banner: 'Suggesting a <b>photo</b> for a 287(g) agency or officer', subj: 'Which agency or officer? Paste the page name', find: 'Paste a link to the photo, and say what it shows', src: 'Where the photo is from (link)' }, | |||
name: { banner: 'Suggesting <b>who runs</b> a 287(g) agency', subj: 'Which agency? Paste the page name', find: 'The sheriff / chief / officer name — and how you know', src: 'Sources for the name (link, one per line)' } | |||
}; | |||
var ctx = CTX[kind], banner = ''; | |||
if (ctx) { F[0] = ['subject', ctx.subj]; F[1] = ['finding', ctx.find, 1]; F[2] = ['sources', ctx.src, 1]; banner = '<div class="ic-sg-ctx">' + ctx.banner + ' — a checker reviews it before anything changes on the page.</div>'; } | |||
var h = '<div class="ic-aa">' + banner; | |||
F.forEach(function (f) { h += '<label class="ic-sw-fld"><span>' + f[1] + '</span>' + (f[2] ? '<textarea rows="3" data-k="' + f[0] + '"></textarea>' : '<input data-k="' + f[0] + '">') + '</label>'; }); | |||
h += '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-lead-send">Submit lead to the review queue</a> <span id="ic-lead-msg"></span></div>'; | |||
h += '<p class="ic-aa-alt">A checker reviews every lead. Never do anything unsafe or illegal to get information — see <a href="/index.php/ICE_List:Staying_safe">Staying safe</a>.</p></div>'; | |||
host.innerHTML = h; | |||
document.getElementById('ic-lead-send').addEventListener('click', function (e) { | |||
e.preventDefault(); var btn = this, msg = document.getElementById('ic-lead-msg'); | |||
var vals = {}; host.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; }); | |||
if (!(vals.finding || '').trim() && !(vals.subject || '').trim()) { msg.textContent = 'Add at least a subject and what you found.'; return; } | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); } | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var parts = ['{' + '{Sorting record|type=Lead']; | |||
F.forEach(function (f) { if ((vals[f[0]] || '').trim()) { parts.push('|' + f[0] + '=' + esc(vals[f[0]]).replace(/\n/g, '<br>')); } }); | |||
parts.push('|source=osint|status=pending}}'); | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: parts.join(''), createonly: 1, summary: 'OSINT lead' }).then(function () { | |||
host.innerHTML = '<div class="ic-aa"><p><b>✓ Lead submitted.</b> Thank you for the research — a checker will review it. <a href="#" onclick="location.reload();return false;">Submit another →</a></p></div>'; | |||
}, function (code) { | |||
if (/permission|denied|group|badaccess/i.test(String(code))) { msg.textContent = 'Your account can’t submit to the queue yet — ask an admin.'; btn.style.pointerEvents = ''; return; } | |||
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = ''; | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Public "drop a link" form — works for anyone (incl. logged-out once the anon hook is on) */ | |||
ready(function () { | |||
var host = document.getElementById('ic-replink'); if (!host) { return; } | |||
host.innerHTML = '<div class="ic-aa">' | |||
+ '<label class="ic-sw-fld"><span>Paste the link — a post, article, or video about ICE</span><input id="ic-lk-url" placeholder="https://..."></label>' | |||
+ '<label class="ic-sw-fld"><span>One line of context (optional)</span><textarea rows="2" id="ic-lk-note"></textarea></label>' | |||
+ '<label class="ic-sw-fld"><span>What kind of link? (helps us prioritise footage)</span><select id="ic-lk-type"><option value="">— choose —</option><option value="video">🎥 Video / footage</option><option value="photo">📷 Photo</option><option value="social">📱 Social post</option><option value="article">📰 News article</option><option value="document">📄 Document</option><option value="other">🔗 Other</option></select></label>' | |||
+ '<div class="ic-cap" style="display:none"><label class="ic-sw-fld"><span class="ic-cap-q"></span><input class="ic-cap-a"></label></div>' | |||
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-lk-send">Drop the link</a> <span id="ic-lk-msg"></span></div>' | |||
+ '<p class="ic-aa-alt">No account needed. A volunteer checks it before anything appears publicly. Never share a link that would put you at risk — see <a href="/index.php/ICE_List:Staying_safe">Staying safe</a>.</p></div>'; | |||
var capId = null; | |||
document.getElementById('ic-lk-send').addEventListener('click', function (e) { | |||
e.preventDefault(); var btn = this, msg = document.getElementById('ic-lk-msg'); | |||
var url = document.getElementById('ic-lk-url').value.trim(); | |||
if (!/^https?:\/\//i.test(url)) { msg.textContent = 'Paste a link that starts with http.'; return; } | |||
var note = document.getElementById('ic-lk-note').value; | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); } | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var ty = (document.getElementById('ic-lk-type') || {}).value || ''; | |||
var body = '{' + '{Submission link|url=' + url.replace(/[{}|]/g, '') + '|note=' + esc(note) + '|source=anonymous' + (ty ? '|type=' + ty : '') + '|status=new}}'; | |||
var params = { action: 'edit', title: 'ICE List:Submission/Inbox/link-anon-' + ts, text: body, createonly: 1, summary: 'Anonymous link drop', formatversion: 2 }; | |||
var capA = host.querySelector('.ic-cap-a'); | |||
if (capId && capA.value) { params.captchaid = capId; params.captchaword = capA.value; } | |||
api.postWithToken('csrf', params).then(function (r) { | |||
var ed = r && r.edit; | |||
if (ed && ed.result === 'Success') { host.innerHTML = '<div class="ic-aa"><p><b>✓ Thank you.</b> Your link is in the queue — a volunteer will check it. <a href="#" onclick="location.reload();return false;">Drop another →</a></p></div>'; return; } | |||
if (ed && ed.captcha) { capId = ed.captcha.id; host.querySelector('.ic-cap').style.display = ''; host.querySelector('.ic-cap-q').textContent = 'One quick check to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then tap Drop the link again.'; btn.style.pointerEvents = ''; return; } | |||
msg.textContent = 'Could not send — try again.'; btn.style.pointerEvents = ''; | |||
}, function (code) { | |||
if (!mw.config.get('wgUserName') && /permission|denied|group|badaccess/i.test(String(code))) { | |||
msg.textContent = 'Anonymous submitting isn’t switched on yet — an admin needs to enable it.'; btn.style.pointerEvents = ''; return; | |||
} | |||
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = ''; | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Public agent-report form + admin handled buttons */ | |||
ready(function () { | |||
var pnR = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var QUEUES = { 'ICE List:Submission/Reports/Agents': 1, 'ICE List:Submission/Reports/Incidents': 1, 'ICE List:Submission/Reports/Companies': 1 }; | |||
if (QUEUES[pnR]) { | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
document.querySelectorAll('.ic-rep[data-page]').forEach(function (it) { | |||
if (it.getAttribute('data-status') === 'handled') { return; } | |||
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; } | |||
var b = document.createElement('a'); b.href = '#'; b.className = 'ic-sub-done'; b.textContent = 'Mark handled ✓'; | |||
b.addEventListener('click', function (e) { | |||
e.preventDefault(); b.textContent = 'Saving…'; | |||
var page = it.getAttribute('data-page'); | |||
api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) { | |||
var c = d.query.pages[0].revisions[0].slots.main.content; | |||
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1handled') : c.replace(/\}\}$/, '|status=handled}}'); | |||
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc, summary: 'Report handled' }); | |||
}).then(function () { it.style.opacity = '.35'; b.textContent = '✓ handled'; }, function () { b.textContent = 'Failed — retry'; }); | |||
}); | |||
act.appendChild(b); | |||
}); | |||
}); | |||
return; | |||
} | |||
var KINDS = { | |||
'ic-repagent': { tpl: 'Agent report', label: 'agent report', pref: 'agent', fields: [['name', 'Who? Name, badge number, or description'], ['agency', 'Agency (ICE, CBP, Border Patrol, unknown)'], ['where', 'Where? City and state'], ['when', 'When? Date or rough time frame'], ['what', 'What happened?', 1], ['links', 'Links to footage or posts (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] }, | |||
'ic-repincident': { tpl: 'Incident report', label: 'incident report', pref: 'incident', fields: [['what', 'What happened? One line'], ['when', 'When? Date'], ['where', 'Where? City and state'], ['agents', 'Agents involved (names / description)'], ['vehicles', 'Vehicles (plates / description)'], ['details', 'Full account — what you saw', 1], ['links', 'Links to footage, posts, or news (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] }, | |||
'ic-repcompany': { tpl: 'Company report', label: 'company report', pref: 'company', fields: [['company', 'Company name'], ['what', 'How are they involved with ICE? (contracts, cooperation, profiteering)', 1], ['where', 'Where? Locations / states'], ['links', 'Sources / evidence links (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] } | |||
}; | |||
var mountId = null, cfg = null; | |||
for (var kk in KINDS) { if (document.getElementById(kk)) { mountId = kk; cfg = KINDS[kk]; break; } } | |||
if (!cfg) { return; } | |||
var host = document.getElementById(mountId); | |||
var h = '<div class="ic-aa">'; | |||
cfg.fields.forEach(function (f) { h += '<label class="ic-sw-fld"><span>' + f[1] + '</span>' + (f[2] ? '<textarea rows="3" data-k="' + f[0] + '"></textarea>' : '<input data-k="' + f[0] + '">') + '</label>'; }); | |||
h += '<div class="ic-cap" style="display:none"><label class="ic-sw-fld"><span class="ic-cap-q"></span><input class="ic-cap-a"></label></div>'; | |||
h += '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-rep-send">Send report to the admins</a> <span id="ic-rep-msg"></span></div>'; | |||
h += '<p class="ic-aa-alt">Only ICE List admins can see reports. Nothing you send is published.</p></div>'; | |||
host.innerHTML = h; | |||
var capId = null; | |||
document.getElementById('ic-rep-send').addEventListener('click', function (e) { | |||
e.preventDefault(); | |||
var btn = this, msg = document.getElementById('ic-rep-msg'); | |||
var vals = {}; host.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; }); | |||
if (!cfg.fields.some(function (f) { return (vals[f[0]] || '').trim(); })) { msg.textContent = 'Fill in at least one field.'; return; } | |||
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
function resc(v) { return (v || '').replace(/[{}|]/g, ' '); } | |||
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4); | |||
var parts = ['{' + '{' + cfg.tpl]; | |||
cfg.fields.forEach(function (f) { if ((vals[f[0]] || '').trim()) { parts.push('|' + f[0] + '=' + resc(vals[f[0]]).replace(/\n/g, '<br>')); } }); | |||
parts.push('|status=new}}'); | |||
var params = { action: 'edit', title: 'ICE List:Submission/Reports/' + cfg.pref + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Public ' + cfg.label, formatversion: 2 }; | |||
var capA = host.querySelector('.ic-cap-a'); | |||
if (capId && capA.value) { params.captchaid = capId; params.captchaword = capA.value; } | |||
api.postWithToken('csrf', params).then(function (r) { | |||
var ed = r && r.edit; | |||
if (ed && ed.result === 'Success') { host.innerHTML = '<div class="ic-aa"><p><b>✓ Report sent.</b> Thank you. Only the admins can see it. If you left contact details they may follow up.</p></div>'; return; } | |||
if (ed && ed.captcha) { capId = ed.captcha.id; host.querySelector('.ic-cap').style.display = ''; host.querySelector('.ic-cap-q').textContent = 'One check, to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then send again.'; btn.style.pointerEvents = ''; return; } | |||
msg.textContent = 'Could not send. Try once more.'; btn.style.pointerEvents = ''; | |||
}, function (code) { | |||
if (!mw.config.get('wgUserName') && (code === 'permissiondenied' || code === 'protectedpage' || /permission|denied|group/i.test(String(code)))) { | |||
var back = encodeURIComponent(mw.config.get('wgPageName')); | |||
host.innerHTML = '<div class="ic-aa"><p><b>Almost there — you need a free account.</b> It takes about 30 seconds and you pick an alias (never your real name). Then you can submit this straight away.</p>' | |||
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=' + back + '">Create an account →</a> ' | |||
+ '<a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:UserLogin&returnto=' + back + '">Log in</a></p></div>'; | |||
return; | |||
} | |||
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = ''; | |||
}); | |||
}); | |||
}); | |||
}); | |||
/* Volunteer ladder: progress + trusted-status request (own profile only) */ | |||
ready(function () { | |||
var u = mw.config.get('wgUserName'); if (!u) { return; } | |||
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; } | |||
var g = mw.config.get('wgUserGroups') || []; | |||
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0)) { return; } | |||
var trusted = g.indexOf('trusted-volunteer') >= 0 || g.indexOf('incident-editor') >= 0 || g.indexOf('sysop') >= 0; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
var api = new mw.Api(); | |||
api.get({ action: 'query', list: 'usercontribs', ucuser: u, uclimit: 500, ucshow: 'new', ucprop: 'title', formatversion: 2 }).then(function (d) { | |||
var cs = d.query.usercontribs || [], c = { links: 0, agents: 0, vehicles: 0, incidents: 0, leads: 0, notes: 0 }; | |||
cs.forEach(function (x) { | |||
var t = x.title; | |||
if (/\/Sorting\/agent-/.test(t)) { c.agents++; } | |||
else if (/\/Sorting\/vehicle-/.test(t)) { c.vehicles++; } | |||
else if (/\/Sorting\/incident-/.test(t)) { c.incidents++; } | |||
else if (/\/Sorting\/lead-/.test(t)) { c.leads++; } | |||
else if (/\/Sorting\/note-/.test(t)) { c.notes++; } | |||
else if (/\/Inbox\/link-/.test(t)) { c.links++; } | |||
}); | |||
var total = c.links + c.agents + c.vehicles + c.incidents + c.leads + c.notes, goal = 50; | |||
function brk() { | |||
var p = []; | |||
if (c.links) { p.push(c.links + ' link' + (c.links > 1 ? 's' : '')); } | |||
if (c.agents) { p.push(c.agents + ' agent' + (c.agents > 1 ? 's' : '')); } | |||
if (c.vehicles) { p.push(c.vehicles + ' vehicle' + (c.vehicles > 1 ? 's' : '')); } | |||
if (c.incidents) { p.push(c.incidents + ' incident' + (c.incidents > 1 ? 's' : '')); } | |||
if (c.leads) { p.push(c.leads + ' lead' + (c.leads > 1 ? 's' : '')); } | |||
if (c.notes) { p.push(c.notes + ' note' + (c.notes > 1 ? 's' : '')); } | |||
return p.length ? p.join(' · ') : 'Nothing yet — open a link on your desk to start.'; | |||
} | |||
var box = document.createElement('div'); box.className = 'ic-ladder'; | |||
var h = '<div class="ic-veyebrow">Your progress</div>'; | |||
if (trusted) { | |||
h += '<p><b>You’re a trusted volunteer.</b> Thank you. The next steps up are by invitation from the community.</p><p class="ic-ladder-break">' + brk() + '</p>'; | |||
} else { | |||
var pct = Math.min(100, Math.round(total / goal * 100)); | |||
h += '<p><b>' + total + ' of ' + goal + '</b> contributions. At ' + goal + ' you can ask the team to review you for trusted-volunteer status.</p>' | |||
+ '<div class="ic-ladder-bar"><span style="width:' + pct + '%"></span></div>' | |||
+ '<p class="ic-ladder-break">' + brk() + '</p>'; | |||
h += (total >= goal) | |||
? '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-ladder-req">Request a trusted-volunteer review</a> <span id="ic-ladder-msg"></span></div>' | |||
: '<p class="ic-aa-alt">' + (goal - total) + ' to go before you can request a review.</p>'; | |||
} | |||
h += '<p class="ic-aa-alt"><a href="/index.php/ICE_List:Volunteer/Becoming_a_trusted_volunteer">How the volunteer ladder works →</a></p>'; | |||
box.innerHTML = h; | |||
var host = document.querySelector('.mw-parser-output'); if (host) { host.insertBefore(box, host.firstChild); } | |||
var rq = document.getElementById('ic-ladder-req'); | |||
if (rq) { rq.addEventListener('click', function (e) { | |||
e.preventDefault(); rq.style.pointerEvents = 'none'; | |||
var msg = document.getElementById('ic-ladder-msg'); msg.textContent = 'Sending…'; | |||
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/Promotions', section: 'new', sectiontitle: 'Trusted-volunteer request: ' + u, text: '[[User:' + u + ']] (' + total + ' contributions: ' + brk() + ') requests a trusted-volunteer review. [[Special:Contributions/' + u + '|see contributions]] — cc [[User:ICEListAdmin6|ICEListAdmin6]] [[User:AdminBot|AdminBot]] ([[User talk:AdminBot|talk]]) 14:21, 27 July 2026 (UTC)', summary: 'Trusted-volunteer request' }) | |||
.then(function () { msg.textContent = ''; rq.textContent = '✓ Request sent'; }, function () { msg.textContent = 'Failed — retry'; rq.style.pointerEvents = ''; }); | |||
}); } | |||
}); | |||
}); | |||
}); | |||
/* Confirm/sort desks: when your own queue is empty, offer upstream work */ | |||
ready(function () { | |||
var u = mw.config.get('wgUserName'); if (!u) { return; } | |||
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; } | |||
var g = mw.config.get('wgUserGroups') || []; | |||
var isConfirm = g.indexOf('confirm') >= 0, isSorter = g.indexOf('sorter') >= 0; | |||
if (!isConfirm && !isSorter) { return; } | |||
var myCat = isConfirm ? 'Category:Submission confirm' : 'Category:Submission sorting'; | |||
mw.loader.using(['mediawiki.api']).then(function () { | |||
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: myCat, cmlimit: '2', cmtype: 'page' }).then(function (d) { | |||
if ((d.query.categorymembers || []).length > 0) { return; } | |||
var box = document.createElement('div'); box.className = 'ic-emptyq'; | |||
var links = isConfirm | |||
? '<a class="ic-vbtn" href="/index.php/ICE_List:Submission/Sorting">Help sort links →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Vetting">Check new links →</a>' | |||
: '<a class="ic-vbtn" href="/index.php/ICE_List:Submission/Vetting">Check new links →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Inbox">Browse the inbox →</a>'; | |||
box.innerHTML = '<div class="ic-veyebrow">Your queue is clear</div><p>Nothing waiting for you right now. Nice work. Want to lend a hand upstream?</p><div class="ic-vact">' + links + '</div>'; | |||
var host = document.querySelector('.mw-parser-output'); if (host) { host.insertBefore(box, host.firstChild); } | |||
}); | |||
}); | |||
}); | |||
/* Logged-in volunteers: annotate the profile link + a "get to work" nudge */ | |||
ready(function () { | |||
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName'); | |||
var isVol = g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0; | |||
if (!u || !isVol) { return; } | |||
// annotate the personal-tools profile link with a task dot | |||
var pl = document.querySelector('#pt-userpage a, li#pt-userpage a, #p-personal #pt-userpage a'); | |||
if (pl && !pl.querySelector('.ic-pt-dot')) { | |||
pl.title = 'Your desk. Your tasks are here.'; | |||
var dot = document.createElement('span'); dot.className = 'ic-pt-dot'; pl.appendChild(dot); | |||
} | |||
// the get-to-work bar — skip it while already on your own profile | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn === 'User:' + u) { return; } | |||
try { if (sessionStorage.getItem('ic-gtw-x')) { return; } } catch (e) {} | |||
var b = document.createElement('div'); b.className = 'ic-gtw'; | |||
b.innerHTML = '<span class="ic-gtw-t"><b>Ready to add to the record?</b> Your tasks are waiting on your desk.</span>' | |||
+ '<a class="ic-gtw-go" href="/index.php/Special:MyPage">Get to work →</a>' | |||
+ '<a href="#" class="ic-gtw-x" aria-label="Dismiss">×</a>'; | |||
var host = document.querySelector('#mw-content-text'); | |||
if (host) { host.insertBefore(b, host.firstChild); } | |||
b.querySelector('.ic-gtw-x').addEventListener('click', function (e) { e.preventDefault(); b.style.display = 'none'; try { sessionStorage.setItem('ic-gtw-x', '1'); } catch (e) {} }); | |||
}); | |||
/* Anonymous readers: one gentle, non-modal "drop a link" nudge after they've read a bit */ | |||
ready(function () { | |||
if (mw.config.get('wgUserName')) { return; } // logged-out only | |||
if (mw.config.get('wgNamespaceNumber') < 0) { return; } // not on Special pages | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (/^Special:|Submit a link|Submit incident|Submit Agent|Submit photos|CreateAccount|UserLogin/.test(pn)) { return; } | |||
try { if (sessionStorage.getItem('ic-nudge-x')) { return; } } catch (e) {} | |||
var shown = false, t; | |||
function show() { | |||
if (shown) { return; } | |||
var bar = document.querySelector('.ic-support, .ic-anonbar'); | |||
if (bar && bar.offsetParent !== null) { return; } // donation/top banner still up → don't double-ask | |||
shown = true; clearTimeout(t); | |||
var n = document.createElement('div'); n.className = 'ic-nudge'; | |||
n.innerHTML = '<button class="ic-nudge__x" aria-label="Dismiss">×</button>' | |||
+ '<div class="ic-nudge__t"><b>Seen something about ICE?</b> This record is built by all of us — add what you see. No account needed.</div>' | |||
+ '<a class="ic-nudge__btn" href="/index.php/ICE_List:Submit_a_link">Drop a link →</a>'; | |||
document.body.appendChild(n); | |||
requestAnimationFrame(function () { n.classList.add('ic-nudge--in'); }); | |||
n.querySelector('.ic-nudge__x').addEventListener('click', function () { | |||
n.classList.remove('ic-nudge--in'); setTimeout(function () { n.remove(); }, 300); | |||
try { sessionStorage.setItem('ic-nudge-x', '1'); } catch (e) {} | |||
}); | |||
} | |||
t = setTimeout(show, 14000); // after ~14s of reading | |||
window.addEventListener('scroll', function ons() { // ...or 45% down the page | |||
if ((window.scrollY + window.innerHeight) / document.body.scrollHeight > 0.45) { | |||
window.removeEventListener('scroll', ons); show(); | |||
} | |||
}, { passive: true }); | |||
}); | |||
/* Share bar on public content pages (agents, incidents, deaths, vehicles, media, maps) */ | |||
ready(function () { | |||
if (mw.config.get('wgAction') !== 'view') { return; } | |||
var ns = mw.config.get('wgNamespaceNumber'); | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
if (pn === 'Main Page') { return; } | |||
var isMain = ns === 0; | |||
var isPublicProject = /^ICE List(?: Wiki)?:/.test(pn) && !/submission|volunteer|ask an admin|staying safe|poster|submit|culture inbox|help desk|report an agent|agent reports|becoming|donate|start here/i.test(pn); | |||
if (!(isMain || isPublicProject)) { return; } | |||
var host = document.querySelector('.mw-parser-output'); if (!host) { return; } | |||
var title = mw.config.get('wgTitle'); | |||
var url = (location.protocol + '//' + location.host + mw.util.getUrl()); | |||
var canon = document.querySelector('link[rel=canonical]'); if (canon && canon.href) { url = canon.href; } | |||
var text = title + ' — documented on the ICE List, a public record of U.S. immigration enforcement.'; | |||
var eU = encodeURIComponent(url), eT = encodeURIComponent(text), eTU = encodeURIComponent(text + ' ' + url); | |||
var targets = [ | |||
['Bluesky', 'https://bsky.app/intent/compose?text=' + eTU], | |||
['X', 'https://twitter.com/intent/tweet?text=' + eT + '&url=' + eU], | |||
['Facebook', 'https://www.facebook.com/sharer/sharer.php?u=' + eU], | |||
['Threads', 'https://www.threads.net/intent/post?text=' + eTU], | |||
['Reddit', 'https://www.reddit.com/submit?url=' + eU + '&title=' + encodeURIComponent(title)] | |||
]; | |||
var bar = document.createElement('div'); bar.className = 'ic-share'; | |||
var h = '<span class="ic-share__lbl">Share this record</span>' | |||
+ '<a href="#" class="ic-share__b ic-share__copy">Copy link</a>'; | |||
targets.forEach(function (t) { h += '<a class="ic-share__b" target="_blank" rel="noopener noreferrer" href="' + t[1] + '">' + t[0] + '</a>'; }); | |||
if (navigator.share) { h += '<a href="#" class="ic-share__b ic-share__native">More…</a>'; } | |||
bar.innerHTML = h; | |||
host.insertBefore(bar, host.firstChild); | |||
bar.querySelector('.ic-share__copy').addEventListener('click', function (e) { | |||
e.preventDefault(); var self = this; | |||
navigator.clipboard.writeText(url).then(function () { var o = self.textContent; self.textContent = 'Copied ✓'; setTimeout(function () { self.textContent = o; }, 1400); }); | |||
}); | |||
var nat = bar.querySelector('.ic-share__native'); | |||
if (nat) { nat.addEventListener('click', function (e) { e.preventDefault(); navigator.share({ title: title, text: text, url: url }).catch(function () {}); }); } | |||
}); | |||
/* Auto-link key terms to their maps (first mention per page, non-destructive) */ | |||
ready(function () { | |||
var pn = mw.config.get('wgPageName').replace(/_/g, ' '); | |||
var content = document.querySelector('.mw-parser-output'); if (!content) { return; } | |||
var LINKS = [ | |||
{ re: /287\(g\)/, href: '/index.php/ICE_List:Map_of_287(g)_agreements', title: '287(g) agreements map', skip: /Map of 287\(g\)/ }, | |||
{ re: /\b(detention cente?r|concentration camp)\b/i, href: '/index.php/ICE_List:Map_of_concentration_camps_and_ICE_facilities', title: 'Map of detention sites', skip: /concentration camps and ICE facilities/ } | |||
]; | |||
LINKS.forEach(function (cfg) { | |||
if (cfg.skip.test(pn)) { return; } // don't self-link on its own map page | |||
var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, { | |||
acceptNode: function (n) { | |||
if (!cfg.re.test(n.nodeValue)) { return NodeFilter.FILTER_SKIP; } | |||
var p = n.parentNode; | |||
while (p && p !== content) { | |||
if (p.tagName === 'A' || /^H[1-6]$/.test(p.tagName || '') || (p.classList && (p.classList.contains('mw-editsection') || p.classList.contains('ic-anonbar')))) { return NodeFilter.FILTER_SKIP; } | |||
p = p.parentNode; | |||
} | |||
return NodeFilter.FILTER_ACCEPT; | |||
} | |||
}); | |||
var node = walker.nextNode(); if (!node) { return; } | |||
var m = cfg.re.exec(node.nodeValue); if (!m) { return; } | |||
var before = node.nodeValue.slice(0, m.index), after = node.nodeValue.slice(m.index + m[0].length); | |||
var a = document.createElement('a'); a.href = cfg.href; a.textContent = m[0]; a.title = cfg.title; | |||
var frag = document.createDocumentFragment(); | |||
if (before) { frag.appendChild(document.createTextNode(before)); } | |||
frag.appendChild(a); | |||
if (after) { frag.appendChild(document.createTextNode(after)); } | |||
node.parentNode.replaceChild(frag, node); | |||
}); | |||
}); | |||
/* Dismissable volunteer-guide banner (pipeline volunteers) */ | |||
ready(function () { | |||
var g = mw.config.get('wgUserGroups') || []; | |||
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0)) { return; } | |||
try { if (localStorage.getItem('ic-guide-dismissed')) { return; } } catch (e) {} | |||
var b = document.createElement('div'); b.className = 'ic-guidebanner'; | |||
b.innerHTML = '<span>New to the ICE List? <a href="/index.php/ICE_List:Volunteer_guide">Read the volunteer guide \u2192</a></span><a href="#" class="ic-guidebanner__x" aria-label="Dismiss">\u00d7</a>'; | |||
var host = document.querySelector('#mw-content-text'); | |||
if (host) { host.insertBefore(b, host.firstChild); } | |||
b.querySelector('.ic-guidebanner__x').addEventListener('click', function (e) { e.preventDefault(); b.style.display = 'none'; try { localStorage.setItem('ic-guide-dismissed', '1'); } catch (e) {} }); | |||
}); | |||
})(); | |||
/* ============ end ICE List client tools ============ */ | |||
Latest revision as of 14:21, 27 July 2026
/* Load CRUST brand fonts — ResourceLoader strips @import in Common.css, so inject the link here */
(function(){
var l=document.createElement('link'); l.rel='stylesheet';
l.href='https://fonts.googleapis.com/css2?family=Anton&family=Libre+Franklin:wght@400;500;600;700;800;900&family=Spline+Sans+Mono:wght@400;500;600&display=swap';
document.head.appendChild(l);
})();
(function () {
'use strict';
var KEY = 'icelist_anon_donate_dismiss_until';
var DAYS = 14;
if ((mw.config.get('wgUserName')!==null)) return;
var until = parseInt(localStorage.getItem(KEY) || '0', 10);
if (Date.now() < until) return;
function mount() {
var siteNotice = document.getElementById('siteNotice');
if (!siteNotice) return;
// If empty, do nothing
var txt = (siteNotice.textContent || '').replace(/\s+/g, ' ').trim();
if (!txt) return;
siteNotice.classList.add('icelist-anon-donate');
// Add close button once
if (!siteNotice.querySelector('.icelist-notice-close')) {
var btn = document.createElement('button');
btn.type = 'button';
btn.className = 'icelist-notice-close';
btn.setAttribute('aria-label', 'Close');
btn.textContent = '×';
siteNotice.insertBefore(btn, siteNotice.firstChild);
btn.addEventListener('click', function (e) {
e.preventDefault();
siteNotice.style.display = 'none';
localStorage.setItem(KEY, String(Date.now() + DAYS * 24 * 60 * 60 * 1000));
});
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', mount);
} else {
mount();
}
})();
mw.loader.using(['jquery']).then(function () {
var s = document.createElement('script');
s.src = 'https://cdnjs.buymeacoffee.com/1.0.0/widget.prod.min.js';
s.setAttribute('data-name', 'BMC-Widget');
s.setAttribute('data-cfasync', 'false');
s.setAttribute('data-id', 'crustnews');
s.setAttribute('data-description', 'Support me on Buy me a coffee!');
s.setAttribute('data-message', '');
s.setAttribute('data-color', '#FF5F5F');
s.setAttribute('data-position', 'Right');
s.setAttribute('data-x_margin', '18');
s.setAttribute('data-y_margin', '18');
document.body.appendChild(s);
});
// Tag agent pages (any page showing an agent card) so the duplicate title/heading can be hidden
if ( document.querySelector( '.ic-agent-card' ) ) {
document.body.classList.add( 'ic-agent-page' );
}
/* ---- Mark anonymous users so anon-only banners can show (fail-safe: hidden by default) ---- */
if ((mw.config.get('wgUserName')===null)) { document.documentElement.classList.add('vol-anon'); }
/* ---- Incident candidates page: logged-in-only + per-row dismiss ---- */
if (/Incident_candidates$/.test(mw.config.get('wgPageName'))) {
mw.loader.using(['mediawiki.api', 'mediawiki.util']).then(function () {
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) return;
if ((mw.config.get('wgUserName')===null)) {
content.innerHTML = '<div class="vol-login">This page is for logged-in ICE List volunteers. ' +
'<a href="' + mw.util.getUrl('Special:UserLogin', {returnto: mw.config.get('wgPageName')}) +
'">Log in</a> to review incident candidates.</div>';
return;
}
var api = new mw.Api();
content.querySelectorAll('li').forEach(function (li) {
var create = Array.prototype.find.call(li.querySelectorAll('a'), function (a) {
return /FormEdit\/Incident/.test(a.getAttribute('href') || '');
});
if (!create) return;
var art = Array.prototype.find.call(li.querySelectorAll('a[href^="http"]'), function (a) {
return a !== create && !/FormEdit\/Incident/.test(a.getAttribute('href') || '');
});
var url = art ? art.href : null;
var btn = document.createElement('button');
btn.textContent = '✕ dismiss';
btn.className = 'vol-dismiss';
btn.onclick = function () {
if (!url) { li.remove(); return; }
btn.disabled = true; btn.textContent = '…';
api.get({action: 'parse', page: mw.config.get('wgPageName'), prop: 'wikitext', formatversion: 2})
.then(function (r) {
var lines = r.parse.wikitext.split('\n'), out = [], removed = false;
for (var i = 0; i < lines.length; i++) {
if (!removed && lines[i].indexOf(url) !== -1) {
if (out.length && /^<!-- cand:/.test(out[out.length - 1])) out.pop();
removed = true; continue;
}
out.push(lines[i]);
}
return api.postWithToken('csrf', {action: 'edit', title: mw.config.get('wgPageName'),
text: out.join('\n'), summary: 'Dismiss incident candidate'});
})
.then(function () { li.remove(); })
.catch(function () { btn.disabled = false; btn.textContent = '✕ dismiss'; });
};
li.appendChild(document.createTextNode(' '));
li.appendChild(btn);
});
});
}
/* ============ ICE List client tools (vanilla; robust login detection) ============ */
(function () {
function ready(fn) { if (document.readyState !== 'loading') { fn(); } else { document.addEventListener('DOMContentLoaded', fn); } }
var IC_ST = { AL:'Alabama',AK:'Alaska',AZ:'Arizona',AR:'Arkansas',CA:'California',CO:'Colorado',CT:'Connecticut',DE:'Delaware',FL:'Florida',GA:'Georgia',HI:'Hawaii',ID:'Idaho',IL:'Illinois',IN:'Indiana',IA:'Iowa',KS:'Kansas',KY:'Kentucky',LA:'Louisiana',ME:'Maine',MD:'Maryland',MA:'Massachusetts',MI:'Michigan',MN:'Minnesota',MS:'Mississippi',MO:'Missouri',MT:'Montana',NE:'Nebraska',NV:'Nevada',NH:'New Hampshire',NJ:'New Jersey',NM:'New Mexico',NY:'New York',NC:'North Carolina',ND:'North Dakota',OH:'Ohio',OK:'Oklahoma',OR:'Oregon',PA:'Pennsylvania',RI:'Rhode Island',SC:'South Carolina',SD:'South Dakota',TN:'Tennessee',TX:'Texas',UT:'Utah',VT:'Vermont',VA:'Virginia',WA:'Washington',WV:'West Virginia',WI:'Wisconsin',WY:'Wyoming',DC:'District of Columbia' };
function icNormState(x){ x=(x||'').trim(); var u=x.toUpperCase(); if(x.length<=2 && IC_ST[u]){ return IC_ST[u]; } return x; }
function icConf(txt){ var c={}; (txt||'').split('\n').forEach(function(l){ l=l.replace(/^[*#\s]+/,'').trim(); if(l && l.indexOf('<')!==0){ c[l]=1; } }); return c; }
/* Essential information: fully gadget-rendered interactive accordion (no wiki-parser fragility) */
ready(function () {
var mount = document.getElementById('ic-essential'); if (!mount) { return; }
function U(t) { return '/index.php/' + encodeURI(t.replace(/ /g, '_')); }
var G = [
['Your rights', 'If ICE shows up', [['Your Rights at Your Door', 'At your door'], ['Your Rights on the Street', 'On the street'], ['Your Rights in Your Car', 'In your car'], ['Your Rights at Work', 'At work'], ['Courthouses, Schools and Hospitals', 'Courthouses, schools & hospitals']]],
['Your rights', 'Protest & bear witness', [['Protest Rights', 'Protest rights'], ['If You Witness a Raid', 'If you witness a raid'], ['Recording Incidents Safely', 'Recording incidents safely'], ['Information to Document', 'What to document']]],
['Family', 'For immigrants & families', [['Family Preparedness Plan', 'Family preparedness plan'], ['Red Cards and Rights Cards', 'Red cards: your rights without speaking'], ['If You Are Detained', 'If you are detained'], ['If a Loved One Is Detained', 'If a loved one is detained']]],
['Support', 'Get help', [['Finding an Immigration Lawyer', 'Find a real lawyer, free if needed'], ['Emergency Hotlines and Rapid Response', 'Hotlines: save these numbers'], ['How to Report an Incident', 'Report an incident']]],
['Field guide', 'Spotting ICE', [['How to Spot ICE', 'How to spot ICE'], ['Plainclothes and Gear Patterns', 'Plainclothes & gear patterns'], ['ICE Vehicle Identification', 'Vehicle identification'], ['Joint Operations With Local Police', 'Joint ops with local police']]],
['Explainer', 'Understanding ICE', [['ICE vs CBP vs HSI vs ERO', 'ICE vs CBP vs HSI vs ERO'], ['287g Agreements Explained', '287(g) explained'], ['How Field Offices Operate', 'How field offices operate']]]
];
function esc(s) { return s.replace(/&/g, '&').replace(/</g, '<'); }
var html = '<div class="ic-ess">';
G.forEach(function (g, i) {
var rows = g[2].map(function (it) { return '<a class="ic-ess-row" href="' + U(it[0]) + '"><span>' + esc(it[1]) + '</span><span class="ic-ess-arr">→</span></a>'; }).join('');
html += '<div class="ic-ess-g' + (i === 0 ? ' ic-ess-g--open' : '') + '">'
+ '<button type="button" class="ic-ess-h"><span class="ic-ess-k">' + esc(g[0]) + '</span><span class="ic-ess-t">' + esc(g[1]) + '</span><span class="ic-ess-tog"></span></button>'
+ '<div class="ic-ess-b">' + rows + '</div></div>';
});
html += '<a class="ic-ess-all" href="' + U('ICE List:Essential information') + '">All guides in one place →</a></div>';
mount.innerHTML = html;
mount.addEventListener('click', function (e) { var h = e.target.closest('.ic-ess-h'); if (h) { h.parentNode.classList.toggle('ic-ess-g--open'); } });
});
/* DPL page normalizer — MUST run first. {{FULLPAGENAME}} inside a DPL include wrongly
resolves to the listing page, so every card's data-page is wrong. The DPL wrapper
.ic-sub-dpl carries the true per-item page (via %PAGE%); copy it onto the inner card
so every downstream gadget reads the correct page unchanged. */
ready(function () {
var ws = document.querySelectorAll('.ic-sub-dpl[data-page]');
for (var i = 0; i < ws.length; i++) {
var p = ws[i].getAttribute('data-page');
if (!p) { continue; }
var inner = ws[i].querySelectorAll('[data-page]');
for (var j = 0; j < inner.length; j++) { inner[j].setAttribute('data-page', p); }
}
});
/* Detect logged-in state robustly: mw.user first, then the DOM logout link
(wgUserName can be null in cached RLCONF, so trust the personal-tools chrome). */
function markMember() {
var el = document.documentElement;
if (el.classList.contains('vol-member')) { return true; }
var inn = false;
try { if (window.mw && mw.user && (mw.config.get('wgUserName')!==null)) { inn = true; } } catch (e) {}
if (!inn && (document.getElementById('pt-logout') ||
document.querySelector('a[href*="Special:UserLogout"], a[href*="UserLogout"], #pt-userpage'))) { inn = true; }
if (inn) { el.classList.add('vol-member'); }
return inn;
}
markMember();
ready(markMember);
/* Incident pages: footage + agents front and centre, volunteer add-forms */
ready(function () {
try {
var ns = mw.config.get('wgNamespaceNumber');
var cats = mw.config.get('wgCategories') || [];
var pn = mw.config.get('wgPageName');
var pnText = pn.replace(/_/g, ' ');
var isIncident = (ns === 0 && cats.indexOf('Incidents') !== -1);
var isAgent = (ns === 0 && cats.indexOf('Agents') !== -1) || (ns === 4 && /^ICE List:Unidentified agent - /.test(pnText));
if (!isIncident && !isAgent) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var isVol = (typeof markMember === 'function') && markMember();
var hero = document.createElement('div'); hero.className = 'ic-incident-hero';
var panels = {};
var box = content.querySelector('.ic-incident-box');
var incState = '';
if (box) {
[].slice.call(box.querySelectorAll('.ic-incident-meta > div')).forEach(function (r) {
var dt = r.querySelector('dt'), dd = r.querySelector('dd');
if (dt && dd && /^state$/i.test(dt.textContent.trim())) { incState = dd.textContent.trim(); }
});
}
if (isVol) {
var bar = document.createElement('div'); bar.className = 'ic-incident-actions';
function mkBtn(label, key, cls) {
var b = document.createElement('button'); b.className = 'ic-incident-actbtn' + (cls ? ' ' + cls : '');
b.innerHTML = label;
b.addEventListener('click', function () {
var open = panels[key] && panels[key].style.display === 'block';
Object.keys(panels).forEach(function (k) { if (panels[k]) { panels[k].style.display = 'none'; } });
if (panels[key] && !open) { panels[key].style.display = 'block'; }
});
bar.appendChild(b); return b;
}
mkBtn('✎ Add information', 'info', 'primary');
mkBtn('▶ Add footage / media', 'media');
if (isIncident) { mkBtn('+ Add an agent', 'agent'); }
hero.appendChild(bar);
}
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); }
function panel(title) {
var p = document.createElement('div'); p.className = 'ic-incident-panel'; p.style.display = 'none';
var h = document.createElement('div'); h.className = 'ic-incident-panel__h'; h.textContent = title; p.appendChild(h);
return p;
}
function inp(ph, val) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph || ''; i.className = 'ic-incident-input'; if (val) { i.value = val; } return i; }
if (isVol) {
/* INFO */
var pInfo = panel('Add information');
var infoP = document.createElement('p'); infoP.textContent = 'Open the editor to add or correct information on this incident.';
var infoA = document.createElement('a'); infoA.className = 'ic-incident-actbtn primary'; infoA.textContent = 'Open editor'; infoA.href = '/index.php/' + pn + '?action=edit';
pInfo.appendChild(infoP); pInfo.appendChild(infoA); hero.appendChild(pInfo); panels.info = pInfo;
/* MEDIA */
var pMedia = panel('Add footage / media');
var urlI = inp('Paste any social/video link (YouTube, X, TikTok, Instagram, Threads, Facebook, Vimeo)');
var capI = inp('Caption (optional)');
var mBtn = document.createElement('button'); mBtn.className = 'ic-incident-actbtn primary'; mBtn.textContent = 'Embed on this page';
var mMsg = document.createElement('div'); mMsg.className = 'ic-incident-msg';
pMedia.appendChild(urlI); pMedia.appendChild(capI); pMedia.appendChild(mBtn); pMedia.appendChild(mMsg);
hero.appendChild(pMedia); panels.media = pMedia;
mBtn.addEventListener('click', function () {
var u = urlI.value.trim();
if (!/^https?:\/\//.test(u)) { mMsg.textContent = 'Enter a valid https link.'; return; }
mBtn.disabled = true; mMsg.textContent = 'Embedding…';
var emb = '{{Embed|url=' + esc(u) + (capI.value.trim() ? '|caption=' + esc(capI.value.trim()) : '') + '}}';
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) {
var pg = d.query.pages[0]; var txt = (pg.revisions && pg.revisions[0].slots.main.content) || '';
var marker = '<div class="ic-incident-footage">';
if (txt.indexOf(marker) !== -1) { txt = txt.replace(marker, marker + '\n' + emb); }
else { var ci = txt.search(/\n\[\[Category:/); var blk = '\n' + marker + '\n' + emb + '\n</div>\n'; txt = (ci !== -1) ? txt.slice(0, ci) + blk + txt.slice(ci) : txt + blk; }
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Add footage/media via incident form' });
}).then(function () { mMsg.textContent = '✓ Added — reloading…'; setTimeout(function () { location.reload(); }, 900); })
.catch(function (e) { mBtn.disabled = false; mMsg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
if (isIncident) {
/* AGENT */
var pAgent = panel('Add an agent');
var help = document.createElement('div'); help.className = 'ic-incident-help';
help.innerHTML = 'Leave <b>name</b> blank for an <b>unidentified</b> agent (goes to the Most Wanted board). Enter a name to create an <b>identified</b> agent page. Either way it links to this incident.';
pAgent.appendChild(help);
var nameI = inp('Name — blank if unidentified');
var codeI = inp('Code for unidentified (e.g. ' + (incState ? incState.replace(/[^A-Za-z]/g, '').slice(0, 2).toUpperCase() : 'XX') + '-0001)');
var agencyI = inp('Agency (ICE / CBP / Unknown)');
var stateI = inp('State', incState || '');
var descI = inp('What they did / notes');
[nameI, codeI, agencyI, stateI, descI].forEach(function (e) { pAgent.appendChild(e); });
var mwRow = document.createElement('label'); mwRow.className = 'ic-incident-check';
var mwCb = document.createElement('input'); mwCb.type = 'checkbox'; mwRow.appendChild(mwCb);
mwRow.appendChild(document.createTextNode(' Flag as Most Wanted (unidentified only)')); pAgent.appendChild(mwRow);
var aBtn = document.createElement('button'); aBtn.className = 'ic-incident-actbtn primary'; aBtn.textContent = 'Create & link to this incident';
var aMsg = document.createElement('div'); aMsg.className = 'ic-incident-msg';
pAgent.appendChild(aBtn); pAgent.appendChild(aMsg); hero.appendChild(pAgent); panels.agent = pAgent;
aBtn.addEventListener('click', function () {
var name = nameI.value.trim(), code = codeI.value.trim().replace(/[^A-Za-z0-9 .\-]/g, '');
var agency = agencyI.value.trim() || 'Unknown', st = stateI.value.trim(), desc = descI.value.trim();
var named = !!name;
if (!named && !code) { aMsg.textContent = 'Enter a name, or a code for an unidentified agent.'; return; }
aBtn.disabled = true; aMsg.textContent = 'Creating…';
var title, body, agentLink, stcat = st || 'No State Given';
if (named) {
title = name; agentLink = '[[' + name + ']]';
body = '{{Agent page\n|name=' + esc(name) + '\n|agency=' + esc(agency) + '\n|state=' + esc(st) + '\n|status=Active\n|image=Agent-placeholder.jpg\n|summary=' + esc(name + ' was documented in connection with ' + pnText + '.') + '\n}}\n\n== Documented Incidents ==\n{{IncidentsForAgent}}\n\n{{MediaForAgent}}\n\n[[Category:Agents]][[Category:Agents in ' + stcat + ']][[Category:Agents needing a photo]]\n';
} else {
title = 'ICE List:Unidentified agent - ' + code; agentLink = '[[' + title + ']]';
var wanted = mwCb.checked ? 'Most wanted' : 'Standard';
var wcat = mwCb.checked ? 'Most wanted unidentified agents' : 'Standard unidentified agents';
body = '{{UnknownAgent\n | code = ' + esc(code) + '\n | image = Agent-placeholder.jpg\n | description = ' + esc(desc || ('Recorded during ' + pnText)) + '. Source: [[' + pnText + ']]\n | agency = ' + esc(agency) + '\n | state = ' + esc(st) + '\n | seen = \n | status = Open\n | wanted = ' + wanted + '\n}}\n<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n{{MediaForAgent}}\n\n[[Category:Unidentified agents]][[Category:' + wcat + ']][[Category:Unidentified agents in ' + stcat + ']][[Category:Agents in ' + stcat + ']]\n';
}
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('That agent page already exists: ' + title); }
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create agent via incident form' });
}).then(function () {
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 });
}).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
if (txt.indexOf(agentLink) === -1) {
txt = txt.replace(/(\|\s*agents_involved\s*=\s*)([^\n]*)/, function (m, p, val) { return p + (val.trim() ? val.trim() + '; ' : '') + agentLink; });
}
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Link agent ' + title + ' to incident' });
}).then(function () {
aMsg.innerHTML = '✓ Created ' + (named ? 'identified agent' : 'unidentified agent (now on the Most Wanted board)') + '. Reloading…';
setTimeout(function () { location.reload(); }, 1300);
}).catch(function (e) { aBtn.disabled = false; aMsg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
}
}
/* FOOTAGE hoist */
var fWrap = document.createElement('div');
var media = content.querySelector('.ic-media-block');
var inFoot = content.querySelector('.ic-incident-footage');
if (media || inFoot) { var fh = document.createElement('div'); fh.className = 'ic-incident-hero__h'; fh.textContent = 'Footage & media'; fWrap.appendChild(fh); }
if (inFoot) { fWrap.appendChild(inFoot); }
if (media) { fWrap.appendChild(media); }
hero.appendChild(fWrap);
/* AGENTS strip */
var seen = {}, codes = [];
if (box) {
[].slice.call(box.querySelectorAll('a[href]')).forEach(function (a) {
var m = decodeURIComponent(a.getAttribute('href') || '').replace(/_/g, ' ').match(/Unidentified agent - ([^"#?|\]&]+)/);
if (m) { var c = m[1].trim(); if (!seen[c]) { seen[c] = 1; codes.push(c); } }
});
}
if (codes.length) {
var aw = document.createElement('div');
var ah = document.createElement('div'); ah.className = 'ic-incident-hero__h'; ah.textContent = 'Agents at the scene'; aw.appendChild(ah);
var strip = document.createElement('div'); strip.className = 'ic-incident-agents';
codes.forEach(function (c) {
var src = '/index.php/Special:Redirect/file/' + encodeURIComponent('Wanted-' + c + '.png');
var card = document.createElement('div'); card.className = 'ic-incident-agent';
card.innerHTML = '<a href="/index.php/ICE_List:Unidentified_agent_-_' + encodeURIComponent(c.replace(/ /g, '_')) + '"><img alt="Agent ' + c + '" loading="lazy"></a><div class="ic-incident-agent__cap">Agent ' + c + ' · unidentified</div>';
var img = card.querySelector('img'); img.onerror = function () { card.style.display = 'none'; }; img.src = src;
strip.appendChild(card);
});
aw.appendChild(strip); hero.appendChild(aw);
}
var firstH = content.querySelector('.mw-heading2, h2');
if (firstH) { content.insertBefore(hero, firstH); } else { content.appendChild(hero); }
});
} catch (e) {}
});
/* Identify: convert an unidentified-agent page into a named agent page */
ready(function () {
try {
if (typeof markMember !== 'function' || !markMember()) { return; }
if (mw.config.get('wgAction') !== 'view') { return; }
if ((mw.config.get('wgCategories') || []).indexOf('Unidentified agents') === -1) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var code = pn.replace('ICE List:Unidentified agent - ', '').trim();
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); }
function inp(ph) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph; i.className = 'ic-incident-input'; return i; }
var box = document.createElement('div'); box.className = 'ic-identify';
var h = document.createElement('div'); h.className = 'ic-identify__h'; h.textContent = 'Identified this agent? Convert to a named agent page';
box.appendChild(h);
var nameI = inp('Full name (required)'), agencyI = inp('Agency — ICE, CBP, HSI…'), roleI = inp('Role / title (optional)'), officeI = inp('Field office (optional)');
[nameI, agencyI, roleI, officeI].forEach(function (e) { box.appendChild(e); });
var btn = document.createElement('button'); btn.className = 'ic-incident-actbtn primary'; btn.textContent = 'Convert & remove from Most Wanted';
var msg = document.createElement('div'); msg.className = 'ic-incident-msg';
box.appendChild(btn); box.appendChild(msg);
content.insertBefore(box, content.firstChild);
btn.addEventListener('click', function () {
var name = nameI.value.trim();
if (!name) { msg.textContent = 'Enter the agent’s name.'; return; }
var agency = agencyI.value.trim() || 'Unknown', role = roleI.value.trim(), office = officeI.value.trim();
btn.disabled = true; msg.textContent = 'Working…';
var image = '', state = '', desc = '';
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
var mi = txt.match(/\|\s*image\s*=\s*([^\n|]*)/); image = mi ? mi[1].trim() : '';
var ms = txt.match(/\|\s*state\s*=\s*([^\n|]*)/); state = ms ? ms[1].trim() : '';
var md = txt.match(/\|\s*description\s*=\s*([^\n|]*)/); desc = md ? md[1].trim() : '';
return api.get({ action: 'query', titles: name, formatversion: 2 });
}).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + name + '" already exists — merge manually.'); }
return api.postWithToken('csrf', { action: 'move', from: pn, to: name, reason: 'Agent identified as ' + name, movetalk: 1 });
}).then(function () {
var ph = !image || /placeholder|nopfp|no-pfp|noimage/i.test(image);
var stcat = state || 'No State Given';
var body = '{{Agent page\n|name=' + esc(name) + '\n|agency=' + esc(agency) +
'\n|role=' + esc(role) + '\n|field_office=' + esc(office) + '\n|state=' + esc(state) +
'\n|status=Active\n|image=' + (image || 'nopfp.png') + '\n|verification=Verified\n|summary=' +
esc(name + ' was documented as the federal agent previously recorded as unidentified agent ' + code + '. ' + desc) +
'\n}}\n\n== Documented Incidents ==\n\'\'This list updates automatically when incident pages link to this agent.\'\'\n\n{{IncidentsForAgent}}\n\n' +
'== Evidence and Sources ==\n* Formerly documented as unidentified agent \'\'\'' + code + '\'\'\'.\n* Here extra evidence can be provided, including social media links\n\n' +
'{{MediaForAgent}}\n\n[[Category:Agents]][[Category:Agents in ' + stcat + ']]' +
(ph ? '[[Category:Agents needing a photo]]' : '[[Category:Agents with photo]]') + '[[Category:Formerly unidentified agents]]\n';
return api.postWithToken('csrf', { action: 'edit', title: name, text: body, summary: 'Convert unidentified agent ' + code + ' to identified agent page' });
}).then(function () {
return api.get({ action: 'query', list: 'backlinks', bltitle: pn, bllimit: 50, blnamespace: 0 });
}).then(function (d) {
var links = (d.query.backlinks || []).map(function (b) { return b.title; }).filter(function (t) { return t !== name; });
var chain = Promise.resolve();
var resrc = '\\[\\[\\s*' + pn.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + '\\s*(\\|[^\\]]*)?\\]\\]';
links.forEach(function (t) {
chain = chain.then(function () {
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: t, formatversion: 2 }).then(function (dd) {
var pg = dd.query.pages[0]; if (!pg.revisions) { return; }
var tx = pg.revisions[0].slots.main.content;
var nx = tx.replace(new RegExp(resrc, 'g'), function (m, p1) { return '[[' + name + (p1 || '') + ']]'; });
if (nx !== tx) { return api.postWithToken('csrf', { action: 'edit', title: t, text: nx, summary: 'Attribute identified agent ' + name }); }
});
});
});
return chain;
}).then(function () {
var url = '/index.php/' + encodeURIComponent(name.replace(/ /g, '_'));
msg.innerHTML = '✓ Converted. Redirecting to <a href="' + url + '">' + name + '</a>…';
setTimeout(function () { location.href = url; }, 1200);
}).catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
});
} catch (e) {}
});
/* Submissions hub: filter chips */
ready(function () {
var tb = document.getElementById('ic-sub-toolbar');
if (!tb) { return; }
var cards = [].slice.call(document.querySelectorAll('.ic-sub-card'));
if (!cards.length) { return; }
var defs = [['all', 'All'], ['matched', 'Matched to a page'], ['unmatched', 'Unmatched'], ['file', 'Has file'], ['link', 'Has link'], ['video', 'Video']];
var chips = [], cnt = document.createElement('span'); cnt.className = 'ic-sub-count';
function match(c, f) { return f === 'all' ? true : c.getAttribute('data-' + f) === '1'; }
function apply(f) {
var n = 0;
cards.forEach(function (c) { var ok = match(c, f); c.style.display = ok ? '' : 'none'; if (ok) { n++; } });
chips.forEach(function (ch) { ch.classList.toggle('is-active', ch.getAttribute('data-f') === f); });
cnt.textContent = n + ' shown';
}
defs.forEach(function (d) {
var b = document.createElement('button'); b.className = 'ic-sub-chip'; b.setAttribute('data-f', d[0]); b.textContent = d[1];
b.addEventListener('click', function () { apply(d[0]); }); tb.appendChild(b); chips.push(b);
});
tb.appendChild(cnt); apply('all');
});
/* Submission review queue: route each submission; routing removes the options */
ready(function () {
var q = document.querySelector('.ic-subq');
if (!q) { return; }
if (!(typeof markMember === 'function' && markMember())) {
var g = q.querySelector('.ic-subq__buttons'); if (g) { g.innerHTML = '<em>Log in as a volunteer to route this submission.</em>'; }
return;
}
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
var pn = mw.config.get('wgPageName');
var file = q.getAttribute('data-file') || '', link = q.getAttribute('data-link') || '', suggested = q.getAttribute('data-suggested') || '';
var VIDEO = /(youtube\.com|youtu\.be|tiktok\.com|instagram\.com|threads\.(com|net)|facebook\.com|fb\.watch|vimeo\.com|x\.com|twitter\.com)/i;
function esc(x) { return (x || '').replace(/[|]/g, '|').replace(/[\[\]{}]/g, ''); }
var wrap = q.querySelector('.ic-subq__buttons'); wrap.innerHTML = '';
var msg = document.createElement('div'); msg.className = 'ic-subq__msg';
function attachTo(target, done) {
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: target, formatversion: 2 }).then(function (d) {
var p = d.query.pages[0];
if (!p || p.missing !== undefined) { throw new Error('Page not found: ' + target); }
var txt = p.revisions[0].slots.main.content, media = '';
if (file) { media += '[[File:' + file + '|thumb|300px|Submitted media]]'; }
if (link && VIDEO.test(link)) { media += (media ? '\n' : '') + '{{Embed|url=' + esc(link) + '}}'; }
if (media) { txt = (txt.indexOf('{{MediaForAgent}}') !== -1) ? txt.replace('{{MediaForAgent}}', media + '\n{{MediaForAgent}}') : (txt.trimEnd() + '\n\n' + media + '\n'); }
if (link && !VIDEO.test(link)) {
var ev = '* ' + esc(link) + ' — submitted via form';
if (/==\s*Evidence and Sources\s*==/.test(txt)) { txt = txt.replace(/(==\s*Evidence and Sources\s*==[^\n]*\n)/, '$1' + ev + '\n'); }
else { txt = txt.trimEnd() + '\n\n== Evidence and Sources ==\n' + ev + '\n'; }
}
return api.postWithToken('csrf', { action: 'edit', title: target, text: txt, summary: 'Add submitted media (review queue)' });
}).then(function () { return resolve('routed to [[' + target + ']]'); }).then(done).catch(function (e) { fail(e); });
}
function resolve(note) {
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: pn, formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
txt = txt.replace(/\{\{Submission entry[^}]*\}\}/, '').replace('[[Category:Submission review]]', '');
txt = "'''Resolved:''' " + note + '\n\n' + txt + '\n[[Category:Resolved submissions]]\n';
return api.postWithToken('csrf', { action: 'edit', title: pn, text: txt, summary: 'Resolve submission: ' + note });
});
}
function fail(e) { msg.textContent = 'Error: ' + (e && e.message ? e.message : e); [].forEach.call(wrap.querySelectorAll('button,input'), function (x) { x.disabled = false; }); }
function done() { msg.innerHTML = '✓ Routed — reloading…'; setTimeout(function () { location.reload(); }, 900); }
function busy() { [].forEach.call(wrap.querySelectorAll('button,input'), function (x) { x.disabled = true; }); msg.textContent = 'Working…'; }
function btn(label, cls, fn) { var b = document.createElement('button'); b.className = 'ic-incident-actbtn' + (cls ? ' ' + cls : ''); b.textContent = label; b.addEventListener('click', fn); wrap.appendChild(b); return b; }
if (suggested) {
btn('Attach to ' + suggested, 'primary', function () { busy(); attachTo(suggested, done); });
}
var row = document.createElement('span'); row.className = 'ic-subq__row';
var tin = document.createElement('input'); tin.type = 'text'; tin.className = 'ic-incident-input ic-subq__input'; tin.placeholder = 'Attach to another page (exact title)';
var tbtn = document.createElement('button'); tbtn.className = 'ic-incident-actbtn'; tbtn.textContent = 'Attach';
tbtn.addEventListener('click', function () { var t = tin.value.trim(); if (!t) { msg.textContent = 'Enter a page title.'; return; } busy(); attachTo(t, done); });
row.appendChild(tin); row.appendChild(tbtn); wrap.appendChild(row);
btn('Create unidentified agent', '', function () {
var code = prompt('Code for the new unidentified agent (e.g. NY-0007):', ''); if (!code) { return; }
code = code.replace(/[^A-Za-z0-9 .\-]/g, ''); var st = prompt('State (optional):', '') || '';
busy();
var title = 'ICE List:Unidentified agent - ' + code, stcat = st.trim() || 'No State Given';
var body = '{{UnknownAgent\n | code = ' + esc(code) + '\n | image = ' + (file ? file : 'Agent-placeholder.jpg') + '\n | description = Submitted via media form.' + (link ? ' Source: ' + esc(link) : '') + '\n | agency = Unknown\n | state = ' + esc(st) + '\n | seen = \n | status = Open\n | wanted = Standard\n}}\n<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n' + (link && VIDEO.test(link) ? '{{Embed|url=' + esc(link) + '}}\n' : '') + '{{MediaForAgent}}\n\n[[Category:Unidentified agents]][[Category:Standard unidentified agents]][[Category:Unidentified agents in ' + stcat + ']][[Category:Agents in ' + stcat + ']]' + (file ? '[[Category:Agents with photo]]' : '') + '\n';
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('Already exists: ' + title); }
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create unidentified agent from submission' });
}).then(function () { return resolve('created [[' + title + ']]'); }).then(done).catch(fail);
});
btn('Promote to incident', '', function () {
var title = prompt('New incident page title (include date + location):', '');
if (!title) { return; }
var corr = prompt('Corroborating source URL (independent evidence \u2014 required to promote a tip):', '');
if (!corr || !/^https?:\/\//.test(corr)) { msg.textContent = 'A corroborating source URL is required to promote a tip to an incident.'; return; }
busy();
var media = file ? '[[File:' + file + '|thumb|360px|Submitted media]]\n' : '';
if (link && VIDEO.test(link)) { media += '{{Embed|url=' + esc(link) + '}}\n'; }
var body = '{{Infobox incident\n| title = ' + esc(title) + '\n| status = Developing (promoted from a community tip)\n| verification = Developing. Originated as a community tip, corroborated by an independent source (below).\n| key_sources =\n* Tip: ' + esc(link || file || 'submitted media') + '\n* Corroboration: ' + esc(corr) + '\n}}\n\n' + "''Promoted from a community tip after independent corroboration. Records only what is sourced; will be updated as verified.''\n\n" + media + '\n{{MediaForIncident}}\n\n[[Category:Incidents]][[Category:Incidents involving ICE]]\n';
api.get({ action: 'query', titles: title, formatversion: 2 }).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + title + '" already exists.'); }
return api.postWithToken('csrf', { action: 'edit', title: title, createonly: 1, text: body, summary: 'Create incident from corroborated tip' });
}).then(function () { return resolve('promoted to [[' + title + ']]'); }).then(function () {
msg.innerHTML = '\u2713 Created <a href="/index.php/' + encodeURIComponent(title.replace(/ /g, '_')) + '">' + title + '</a> \u2014 reloading\u2026'; setTimeout(function () { location.reload(); }, 1200);
}).catch(fail);
});
btn('Discard', '', function () { if (!confirm('Discard this submission as not usable?')) { return; } busy(); resolve('discarded (not usable)').then(done).catch(fail); });
wrap.appendChild(msg);
});
});
/* Wanted posters: with/without photo filter (per-state pages) */
ready(function () {
var bar = document.getElementById('ic-photo-filter');
if (!bar) { return; }
var secs = [].slice.call(document.querySelectorAll('.ic-poster-section[data-photo]'));
if (!secs.length) { return; }
function hasTiles(sec) { return sec && sec.querySelector('.ic-poster-tile'); }
secs.forEach(function (s) { if (!hasTiles(s)) { s.style.display = 'none'; } });
var live = secs.filter(hasTiles);
var hasW = live.some(function (s) { return s.getAttribute('data-photo') === 'yes'; });
var hasN = live.some(function (s) { return s.getAttribute('data-photo') === 'no'; });
if (!(hasW && hasN)) { return; }
var lbl = document.createElement('span'); lbl.className = 'ic-photo-lbl'; lbl.textContent = 'Show:'; bar.appendChild(lbl);
var defs = [['all', 'All'], ['with', 'With photo'], ['needed', 'Photo needed']];
var btns = [];
function apply(mode) {
live.forEach(function (s) { var p = s.getAttribute('data-photo'); s.style.display = (mode === 'all' || (mode === 'with' && p === 'yes') || (mode === 'needed' && p === 'no')) ? '' : 'none'; });
btns.forEach(function (b) { b.classList.toggle('is-active', b.getAttribute('data-m') === mode); });
}
defs.forEach(function (d) {
var b = document.createElement('button'); b.className = 'ic-photo-btn'; b.setAttribute('data-m', d[0]); b.textContent = d[1];
b.addEventListener('click', function () { apply(d[0]); }); bar.appendChild(b); btns.push(b);
});
apply('all');
});
/* Homepage: strip native title tooltips on interactive tiles */
ready(function () {
if (mw.config.get('wgPageName') === 'Main_Page') {
document.querySelectorAll('.ic-scenarios a, .ic-stat a, .ic-hero__actions a').forEach(function (a) { a.removeAttribute('title'); });
}
});
/* Render video / social embeds (all users, client-side for privacy) */
ready(function () {
var nodes = document.querySelectorAll('.ic-embed:not([data-done])');
if (!nodes.length) { return; }
var needX = false, needIG = false, needTh = false, needTT = false;
function ytId(u){ var m=u.match(/[?&]v=([\w-]{6,})/)||u.match(/youtu\.be\/([\w-]{6,})/)||u.match(/\/(?:shorts|embed|live)\/([\w-]{6,})/); return m?m[1]:''; }
function vimeoId(u){ var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/); return m?m[1]:''; }
function detect(u){ var h; try{ h=new URL(u).hostname.replace(/^www\./,'').toLowerCase(); }catch(e){ return ''; }
if(h.indexOf('youtube')>-1||h==='youtu.be') return 'youtube';
if(h.indexOf('vimeo')>-1) return 'vimeo';
if(h.indexOf('instagram')>-1) return 'instagram';
if(h.indexOf('tiktok')>-1) return 'tiktok';
if(h.indexOf('facebook')>-1||h==='fb.watch') return 'facebook';
if(h==='x.com'||h.indexOf('twitter')>-1) return 'x';
if(h.indexOf('threads')>-1) return 'threads';
return ''; }
nodes.forEach(function (el) {
el.setAttribute('data-done', '1');
var id = el.getAttribute('data-id'), url = el.getAttribute('data-url');
var plat = (el.getAttribute('data-platform') || '').toLowerCase().trim();
if (!plat) {
if (el.classList.contains('ic-embed-youtube')) { plat = 'youtube'; }
else if (el.classList.contains('ic-embed-vimeo')) { plat = 'vimeo'; }
else if (el.classList.contains('ic-embed-x')) { plat = 'x'; }
else if (el.classList.contains('ic-embed-instagram')) { plat = 'instagram'; }
else if (el.classList.contains('ic-embed-threads')) { plat = 'threads'; }
}
if (!plat && url) { plat = detect(url); }
if (plat === 'youtube' && !id && url) { id = ytId(url); }
if (plat === 'vimeo' && !id && url) { id = vimeoId(url); }
if (plat === 'youtube' && id) {
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen allow="encrypted-media; picture-in-picture" src="https://www.youtube-nocookie.com/embed/' + encodeURIComponent(id) + '"></iframe>';
} else if (plat === 'vimeo' && id) {
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen src="https://player.vimeo.com/video/' + encodeURIComponent(id) + '"></iframe>';
} else if ((plat === 'x' || plat === 'twitter') && url) {
el.innerHTML = '<blockquote class="twitter-tweet"><a href="' + url + '"></a></blockquote>'; needX = true;
} else if (plat === 'instagram' && url) {
el.innerHTML = '<blockquote class="instagram-media" data-instgrm-permalink="' + url + '" data-instgrm-version="14"></blockquote>'; needIG = true;
} else if (plat === 'threads' && url) {
el.innerHTML = '<blockquote class="text-post-media" data-text-post-permalink="' + url + '"></blockquote>'; needTh = true;
} else if (plat === 'tiktok' && url) {
el.innerHTML = '<blockquote class="tiktok-embed" cite="' + url + '" style="max-width:605px;min-width:325px;"><a href="' + url + '"></a></blockquote>'; needTT = true;
} else if (plat === 'facebook' && url) {
el.className = 'ic-embed ic-embed--ratio'; el.innerHTML = '<iframe loading="lazy" allowfullscreen scrolling="no" frameborder="0" src="https://www.facebook.com/plugins/video.php?show_text=false&href=' + encodeURIComponent(url) + '"></iframe>';
} else if (url) {
el.innerHTML = '<a class="ic-embed-link" href="' + url + '" target="_blank" rel="noopener">' + (el.textContent || 'View media') + ' ↗</a>';
}
});
function load(src) { var sc = document.createElement('script'); sc.async = true; sc.src = src; document.body.appendChild(sc); }
if (needX) { load('https://platform.twitter.com/widgets.js'); }
if (needIG) { load('https://www.instagram.com/embed.js'); }
if (needTh) { load('https://www.threads.net/embed.js'); }
if (needTT) { load('https://www.tiktok.com/embed.js'); }
});
/* Show the shareable accountability card on named-agent pages */
ready(function () {
try {
if (mw.config.get('wgNamespaceNumber') !== 0) { return; }
if ((mw.config.get('wgCategories') || []).indexOf('Agents') === -1) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var infoImg = content.querySelector('.ic-agent-card__photo img, .infobox img');
var noPhoto = !infoImg || /placeholder|nopfp|no[-_]?pfp|noimage/i.test((infoImg.getAttribute('src') || '') + '|' + (infoImg.getAttribute('alt') || ''));
if (noPhoto) {
var pn = document.createElement('div'); pn.className = 'ic-photo-needed';
pn.innerHTML = '📷 <strong>We don\'t have a photo of this agent yet.</strong> Do you? <a href="/index.php/ICE_List:Submit_media_form">Submit a photo</a> to help identify and document them — your identity is not recorded.';
content.insertBefore(pn, content.firstChild);
}
var fn = 'Card-' + mw.config.get('wgPageName') + '.png';
var src = mw.util ? mw.util.getUrl('Special:Redirect/file/' + fn) : '/index.php/Special:Redirect/file/' + encodeURIComponent(fn);
var wrap = document.createElement('div'); wrap.className = 'ic-agent-share';
wrap.innerHTML = '<div class="ic-agent-share-h">Share this agent</div>'
+ '<a class="ic-agent-share-img" href="' + src + '" target="_blank" rel="noopener"><img alt="Accountability share card" loading="lazy"></a>'
+ '<div class="ic-agent-share-c">Download and share this card. Recognize them, or have a clearer photo? <a href="/index.php/ICE_List:Submit_media_form">Submit info</a> — your identity is not recorded.</div>';
var img = wrap.querySelector('img');
img.onerror = function () { wrap.style.display = 'none'; };
img.src = src;
content.appendChild(wrap);
} catch (e) {}
});
/* Wanted posters gallery: filter by state */
ready(function () {
var bar = document.getElementById('ic-poster-filter');
if (!bar) { return; }
var tiles = [].slice.call(document.querySelectorAll('.ic-poster-tile[data-state]'));
if (!tiles.length) { return; }
var counts = {};
tiles.forEach(function (t) { var st = (t.getAttribute('data-state') || '').trim(); if (st) { counts[st] = (counts[st] || 0) + 1; } });
var sel = document.createElement('select'); sel.className = 'ic-wanted-statesel';
var all = document.createElement('option'); all.value = ''; all.textContent = 'All states'; sel.appendChild(all);
Object.keys(counts).sort().forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st + ' (' + counts[st] + ')'; sel.appendChild(o); });
var lab = document.createElement('span'); lab.className = 'ic-wanted-filter-label'; lab.textContent = 'Filter by state: ';
bar.appendChild(lab); bar.appendChild(sel);
sel.addEventListener('change', function () {
var v = sel.value;
tiles.forEach(function (t) { t.style.display = (!v || (t.getAttribute('data-state') || '').trim() === v) ? '' : 'none'; });
});
});
/* Most Wanted board: filter by state */
ready(function () {
var bar = document.getElementById('ic-wanted-filter');
if (!bar) { return; }
var cards = [].slice.call(document.querySelectorAll('.ic-wanted-card[data-state]'));
if (!cards.length) { return; }
var counts = {};
cards.forEach(function (c) { var st = (c.getAttribute('data-state') || '').trim(); if (st) { counts[st] = (counts[st] || 0) + 1; } });
var sel = document.createElement('select'); sel.className = 'ic-wanted-statesel';
var all = document.createElement('option'); all.value = ''; all.textContent = 'All states'; sel.appendChild(all);
Object.keys(counts).sort().forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st + ' (' + counts[st] + ')'; sel.appendChild(o); });
var lab = document.createElement('span'); lab.className = 'ic-wanted-filter-label'; lab.textContent = 'Filter by state: ';
bar.appendChild(lab); bar.appendChild(sel);
function apply() {
var v = sel.value;
cards.forEach(function (c) {
var wrap = c.closest('.ic-wanted-entry') || c;
wrap.style.display = (!v || (c.getAttribute('data-state') || '').trim() === v) ? '' : 'none';
});
document.querySelectorAll('.ic-wanted-grid').forEach(function (g) {
var any = [].slice.call(g.querySelectorAll('.ic-wanted-entry')).some(function (e) { return e.style.display !== 'none'; });
var h = g.previousElementSibling;
});
}
sel.addEventListener('change', apply);
});
/* Add unidentified agent form (Most Wanted board, logged-in only) */
ready(function () {
try {
var box = document.getElementById('ic-addagent');
if (!box || !markMember()) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(); box.innerHTML = '';
var form = document.createElement('div'); form.className = 'ic-addform';
var drop = document.createElement('div'); drop.className = 'ic-drop';
var hint = document.createElement('span'); hint.textContent = 'Drag & drop a photo here, or click to choose';
var fileInput = document.createElement('input'); fileInput.type = 'file'; fileInput.accept = 'image/*'; fileInput.style.display = 'none';
var preview = document.createElement('img'); preview.className = 'ic-drop-preview'; preview.style.display = 'none';
drop.appendChild(hint); drop.appendChild(fileInput); drop.appendChild(preview);
var chosenFile = null;
function setFile(f) { if (!f || f.type.indexOf('image/') !== 0) { return; } chosenFile = f; var r = new FileReader(); r.onload = function (e) { preview.src = e.target.result; preview.style.display = 'block'; hint.textContent = f.name; }; r.readAsDataURL(f); }
drop.addEventListener('click', function () { fileInput.click(); });
fileInput.addEventListener('change', function () { setFile(fileInput.files[0]); });
['dragover', 'dragenter'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.add('drag'); }); });
['dragleave', 'drop'].forEach(function (ev) { drop.addEventListener(ev, function (e) { e.preventDefault(); drop.classList.remove('drag'); }); });
drop.addEventListener('drop', function (e) { if (e.dataTransfer.files[0]) { setFile(e.dataTransfer.files[0]); } });
form.appendChild(drop);
function field(label, ph, ta) {
var w = document.createElement('label'); w.className = 'ic-addrow'; w.appendChild(document.createTextNode(label));
var i = document.createElement(ta ? 'textarea' : 'input'); if (!ta) { i.type = 'text'; } i.placeholder = ph || ''; w.appendChild(i); form.appendChild(w); return i;
}
var codeI = field('Code (e.g. IL-0008)', 'required'); var descI = field('Description / what they did', '', true);
var agencyI = field('Agency', 'ICE / CBP / Unknown'); var stateI = field('Location / state', '');
var srcI = field('Source or incident (optional)', 'link or [[Incident page]]');
var vidI = field('Video link (optional)', 'paste any social video link');
var mwRow = document.createElement('label'); mwRow.className = 'ic-addrow ic-addcheck';
var mwCb = document.createElement('input'); mwCb.type = 'checkbox'; mwRow.appendChild(mwCb); mwRow.appendChild(document.createTextNode(' Add to Most wanted')); form.appendChild(mwRow);
var btn = document.createElement('button'); btn.className = 'uk-flag'; btn.textContent = 'Create entry';
var msg = document.createElement('div'); msg.className = 'ic-addmsg'; form.appendChild(btn); form.appendChild(msg); box.appendChild(form);
btn.addEventListener('click', function () {
var code = codeI.value.trim().replace(/[^A-Za-z0-9 .\-]/g, '');
if (!code) { msg.textContent = 'Enter a code.'; return; }
var title = 'ICE List:Unidentified agent - ' + code, wanted = mwCb.checked ? 'Most wanted' : 'Standard';
btn.disabled = true; msg.textContent = 'Working…';
function esc(x) { return x.replace(/[|]/g, '|'); }
function createPage(img) {
var desc = descI.value.trim(); if (srcI.value.trim()) { desc += (desc ? '. ' : '') + 'Source: ' + srcI.value.trim(); }
var body = '{{UnknownAgent\n | code = ' + code + '\n | image = ' + (img || 'Agent-placeholder.jpg') +
'\n | description = ' + esc(desc) + '\n | agency = ' + (esc(agencyI.value.trim()) || 'Unknown') +
'\n | state = ' + esc(stateI.value.trim()) + '\n | seen = \n | status = Open\n | wanted = ' + wanted + '\n}}\n' +
'<div class="ic-wanted-submit">[[ICE List:Submit media form|Submit info on this agent →]]</div>\n' + (vidI.value.trim() ? '{{Embed|url=' + vidI.value.trim() + '}}\n' : '') + '{{MediaForAgent}}\n' +
'[[Category:Unidentified agents]][[Category:' + wanted + ' unidentified agents]]\n';
api.create(title, { summary: 'Add unidentified agent (board form)' }, body).then(function () {
msg.innerHTML = 'Created. <a href="/index.php/' + encodeURIComponent(title.replace(/ /g, '_')) + '">Open ' + code + ' →</a>'; btn.disabled = false;
form.querySelectorAll('input,textarea').forEach(function (el) { if (el.type !== 'file') { el.value = ''; } el.checked = false; });
preview.style.display = 'none'; chosenFile = null; hint.textContent = 'Drag & drop a photo here, or click to choose';
}).catch(function (e) { msg.textContent = (e === 'articleexists') ? 'An entry with that code already exists.' : ('Create failed: ' + e); btn.disabled = false; });
}
if (chosenFile) {
var ext = (chosenFile.name.split('.').pop() || 'jpg').toLowerCase(); if (ext.length > 4 || !ext) { ext = 'jpg'; }
api.upload(chosenFile, { filename: code + '.' + ext, comment: 'Unidentified agent photo', text: '[[Category:Unidentified agents]]', ignorewarnings: true })
.then(function () { createPage(code + '.' + ext); })
.catch(function (e) { msg.textContent = 'Photo upload unavailable (' + e + ') — creating entry without it…'; createPage(null); });
} else { createPage(null); }
});
});
} catch (e) {}
});
/* Unidentified agent marking panel (Most Wanted board) */
ready(function () {
try {
if (!markMember()) { return; }
if (mw.config.get('wgAction') !== 'view') { return; }
if ((mw.config.get('wgCategories') || []).indexOf('Unidentified agents') === -1) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(), page = mw.config.get('wgPageName');
var FIELDS = [['description', 'Description'], ['agency', 'Agency'], ['state', 'Location'], ['status', 'Status']];
var panel = document.createElement('div'); panel.className = 'vol-panel uk-panel';
panel.innerHTML = '<h4>Unidentified agent <span class="vol-status">loading…</span></h4>';
var status = panel.querySelector('.vol-status'), inputs = {};
var wl = document.createElement('label'); wl.className = 'uk-row';
var wcb = document.createElement('input'); wcb.type = 'checkbox'; wcb.disabled = true;
wl.appendChild(wcb); wl.appendChild(document.createTextNode(' Most wanted (feature at top of board)')); panel.appendChild(wl);
FIELDS.forEach(function (f) {
var r = document.createElement('label'); r.className = 'uk-row'; r.appendChild(document.createTextNode(f[1] + ': '));
var i = document.createElement('input'); i.type = 'text'; i.disabled = true; r.appendChild(i); panel.appendChild(r); inputs[f[0]] = i;
});
var btn = document.createElement('button'); btn.className = 'uk-flag'; btn.textContent = 'Save'; btn.disabled = true; panel.appendChild(btn);
var vrow = document.createElement('div'); vrow.className = 'uk-row'; vrow.style.marginTop = '.5rem';
vrow.appendChild(document.createTextNode('Add video (paste any social link): '));
var vin = document.createElement('input'); vin.type = 'text'; vin.placeholder = 'https://…'; vin.disabled = true; vin.style.minWidth = '16rem';
var vbtn = document.createElement('button'); vbtn.className = 'uk-flag'; vbtn.textContent = 'Add video'; vbtn.disabled = true; vbtn.style.marginLeft = '.3rem';
vrow.appendChild(vin); vrow.appendChild(vbtn); panel.appendChild(vrow);
vbtn.addEventListener('click', function () {
var u = vin.value.trim();
if (!/^https?:\/\//.test(u)) { status.textContent = 'enter a full URL'; return; }
status.textContent = 'adding…'; vbtn.disabled = true;
var w = wt, emb = '{{Embed|url=' + u + '}}', idx = w.search(/\n\[\[Category:/);
w = idx >= 0 ? (w.slice(0, idx) + '\n' + emb + w.slice(idx)) : (w.replace(/\s*$/, '') + '\n' + emb + '\n');
api.postWithToken('csrf', { action: 'edit', title: page, text: w, summary: 'Add video embed' })
.then(function () { wt = w; vin.value = ''; status.textContent = 'video added ✓ — reload to view'; vbtn.disabled = false; })
.catch(function () { status.textContent = 'add failed (blocked link?)'; vbtn.disabled = false; });
});
content.insertBefore(panel, content.firstChild);
var wt = '';
function gv(k) { var m = wt.match(new RegExp('\\|\\s*' + k + '\\s*=\\s*([^\\n|}]*)')); return m ? m[1].trim() : ''; }
function setP(w, k, v) { var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^\\n|}]*'); if (re.test(w)) { return w.replace(re, '$1 ' + v.replace(/\$/g, '$$$$')); } return w.replace(/(\{\{UnknownAgent[\s\S]*?)(\n\}\})/, '$1\n | ' + k + ' = ' + v + '$2'); }
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) {
try { wt = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wt = ''; }
wcb.checked = /Category:Most wanted unidentified agents/.test(wt) || gv('wanted') === 'Most wanted';
FIELDS.forEach(function (f) { inputs[f[0]].value = gv(f[0]); inputs[f[0]].disabled = false; });
wcb.disabled = false; btn.disabled = false; vin.disabled = false; vbtn.disabled = false; status.textContent = '';
});
btn.addEventListener('click', function () {
status.textContent = 'saving…'; btn.disabled = true;
var w = wt, wanted = wcb.checked ? 'Most wanted' : 'Standard';
w = setP(w, 'wanted', wanted);
FIELDS.forEach(function (f) { w = setP(w, f[0], inputs[f[0]].value.trim()); });
if (/Category:(?:Most wanted|Standard) unidentified agents/.test(w)) {
w = w.replace(/\[\[Category:(?:Most wanted|Standard) unidentified agents\]\]/, '[[Category:' + wanted + ' unidentified agents]]');
} else {
w = w.replace(/(\[\[Category:Unidentified agents\]\])/, '$1[[Category:' + wanted + ' unidentified agents]]');
}
api.postWithToken('csrf', { action: 'edit', title: page, text: w, summary: 'Mark unidentified agent (' + wanted + ')' })
.then(function () { wt = w; status.textContent = 'saved ✓ — reload to update'; btn.disabled = false; })
.catch(function () { status.textContent = 'save failed'; btn.disabled = false; });
});
});
} catch (e) {}
});
/* Volunteer media tagging panel (Cargo Media items) */
ready(function () {
try {
if (!markMember()) { return; }
if (mw.config.get('wgAction') !== 'view') { return; }
if ((mw.config.get('wgCategories') || []).indexOf('Media') === -1) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(), page = mw.config.get('wgPageName');
var FIELDS = [['agents','Agents (separate with ;)'],['incidents','Incidents (separate with ;)'],['state','State'],['agency','Agency']];
var STATUS = ['Unverified','Pending','Verified'];
var panel = document.createElement('div'); panel.className = 'vol-panel';
panel.innerHTML = '<h4>Tag media <span class="vol-status">loading…</span></h4>';
var status = panel.querySelector('.vol-status'), inputs = {};
FIELDS.forEach(function (f) {
var row = document.createElement('label'); row.className = 'vol-tag-row';
row.appendChild(document.createTextNode(f[1] + ': '));
var inp = document.createElement('input'); inp.type = 'text'; inp.disabled = true;
row.appendChild(inp); panel.appendChild(row); inputs[f[0]] = inp;
});
var srow = document.createElement('label'); srow.className = 'vol-tag-row';
srow.appendChild(document.createTextNode('Status: '));
var sel = document.createElement('select'); sel.disabled = true;
STATUS.forEach(function (v) { var o = document.createElement('option'); o.value = v; o.textContent = v; sel.appendChild(o); });
srow.appendChild(sel); panel.appendChild(srow); inputs.status = sel;
var btn = document.createElement('button'); btn.textContent = 'Save tags'; btn.disabled = true; btn.className = 'vol-tag-save';
panel.appendChild(btn);
content.insertBefore(panel, content.firstChild);
var wikitext = '';
function getVal(k) { var m = wikitext.match(new RegExp('\\|\\s*' + k + '\\s*=\\s*([^\\n|}]*)')); return m ? m[1].trim() : ''; }
function setParam(wt, k, v) {
var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^\\n|}]*');
if (re.test(wt)) { return wt.replace(re, '$1 ' + v.replace(/\$/g, '$$$$')); }
return wt.replace(/(\{\{Media[\s\S]*?)(\n\}\})/, '$1\n | ' + k + ' = ' + v + '$2');
}
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) {
try { wikitext = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wikitext = ''; }
if (wikitext.indexOf('{{Media') === -1) { panel.parentNode.removeChild(panel); return; }
FIELDS.forEach(function (f) { inputs[f[0]].value = getVal(f[0]); inputs[f[0]].disabled = false; });
sel.value = getVal('status') || 'Unverified'; sel.disabled = false; btn.disabled = false; status.textContent = '';
});
btn.addEventListener('click', function () {
status.textContent = 'saving…'; btn.disabled = true;
var wt = wikitext;
FIELDS.forEach(function (f) { wt = setParam(wt, f[0], inputs[f[0]].value.trim()); });
wt = setParam(wt, 'status', sel.value);
api.postWithToken('csrf', { action: 'edit', title: page, text: wt, summary: 'Tag media (agents/incidents/state/status)' }).then(function () {
wikitext = wt; status.textContent = 'saved ✓ — reload to update embeds'; btn.disabled = false;
}).catch(function () { status.textContent = 'save failed'; btn.disabled = false; });
});
});
} catch (e) {}
});
/* Volunteer incident marking + stage tool */
ready(function () {
if (!markMember()) { return; }
if (mw.config.get('wgAction') !== 'view') { return; }
var cats = mw.config.get('wgCategories') || [];
if (cats.indexOf('Incidents') === -1) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var STAGES = ['', 'Intake', 'OSINT', 'Detailing', 'Verification', 'Writing', 'Published'];
var MARKS = ['Agents recorded', 'Plates recorded', 'Faces captured', 'Video secured', 'Location confirmed', 'Witnesses identified', 'Needs review', 'Reviewed'];
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
var page = mw.config.get('wgPageName');
var panel = document.createElement('div'); panel.className = 'vol-panel';
panel.innerHTML = '<h4>Volunteer triage <span class="vol-status">loading…</span></h4>';
var status = panel.querySelector('.vol-status');
var stageRow = document.createElement('div'); stageRow.className = 'vol-stage-row';
stageRow.appendChild(document.createTextNode('Stage: '));
var stageSel = document.createElement('select'); stageSel.disabled = true;
STAGES.forEach(function (st) { var o = document.createElement('option'); o.value = st; o.textContent = st || '(unstaged)'; stageSel.appendChild(o); });
stageRow.appendChild(stageSel); panel.appendChild(stageRow);
var boxes = {};
MARKS.forEach(function (m) {
var lab = document.createElement('label'); var cb = document.createElement('input'); cb.type = 'checkbox'; cb.disabled = true;
lab.appendChild(cb); lab.appendChild(document.createTextNode(' ' + m)); panel.appendChild(lab); boxes[m] = cb;
});
content.insertBefore(panel, content.firstChild);
var wikitext = '';
function parseMarks(wt) { var re = /\{\{\s*Mark\s*\|\s*([^}|]+?)\s*\}\}/g, mm, set = {}; while ((mm = re.exec(wt))) { set[mm[1].trim()] = true; } return set; }
function parseStage(wt) { var m = wt.match(/\{\{\s*Stage\s*\|\s*([^}|]+?)\s*\}\}/); return m ? m[1].trim() : ''; }
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: page, formatversion: 2 }).then(function (r) {
try { wikitext = r.query.pages[0].revisions[0].slots.main.content; } catch (e) { wikitext = ''; }
var set = parseMarks(wikitext);
MARKS.forEach(function (m) { boxes[m].checked = !!set[m]; boxes[m].disabled = false; });
stageSel.value = parseStage(wikitext); stageSel.disabled = false;
status.textContent = '';
});
function save() {
status.textContent = 'saving…';
stageSel.disabled = true; MARKS.forEach(function (m) { boxes[m].disabled = true; });
var stg = stageSel.value;
var chosen = MARKS.filter(function (m) { return boxes[m].checked; });
var inner = (stg ? '{{Stage|' + stg + '}}' : '') + chosen.map(function (m) { return '{{Mark|' + m + '}}'; }).join('');
var region = '<div class="vol-only vol-marks"><!--VOLMARKS-->' + inner + '<!--/VOLMARKS--></div>';
var done = false, wt = wikitext;
wt = wikitext.replace(/<div class="vol-only vol-marks"><!--VOLMARKS-->[\s\S]*?<!--\/VOLMARKS--><\/div>/, function () { done = true; return region; });
if (!done) { wt = wikitext.replace(/<!--VOLMARKS-->[\s\S]*?<!--\/VOLMARKS-->/, function () { done = true; return '<!--VOLMARKS-->' + inner + '<!--/VOLMARKS-->'; }); }
if (!done) { var idx = wt.search(/\n\[\[Category:/); if (idx >= 0) { wt = wt.slice(0, idx) + '\n' + region + wt.slice(idx); } else { wt = wt.replace(/\s*$/, '') + '\n\n' + region + '\n'; } }
api.postWithToken('csrf', { action: 'edit', title: page, text: wt, summary: 'Update volunteer triage / stage' }).then(function () {
wikitext = wt; status.textContent = 'saved ✓';
stageSel.disabled = false; MARKS.forEach(function (m) { boxes[m].disabled = false; });
setTimeout(function () { status.textContent = ''; }, 1500);
}).catch(function () { status.textContent = 'save failed'; stageSel.disabled = false; MARKS.forEach(function (m) { boxes[m].disabled = false; }); });
}
stageSel.addEventListener('change', save);
MARKS.forEach(function (m) { boxes[m].addEventListener('change', save); });
});
});
/* Culture index: featured spotlight + tabs + search over poster tiles */
ready(function () {
var app = document.getElementById('ic-cult-app');
if (!app) { return; }
var grid = app.querySelector('.ic-cult-grid');
if (!grid) { return; }
var cards = [].slice.call(grid.querySelectorAll('.ic-cult-card'));
if (!cards.length) { return; }
var LABEL = { documentary: 'Documentaries', film: 'Film & TV', music: 'Music', book: 'Books', podcast: 'Podcasts' };
var GROUP = { books: ['book', 'podcast'] };
var byType = {};
function tx(el, sel) { var n = el.querySelector(sel); return n ? n.textContent.trim() : ''; }
cards.forEach(function (c) {
var ty = c.getAttribute('data-type') || 'other';
byType[ty] = (byType[ty] || 0) + 1;
c.setAttribute('data-s', (tx(c, '.ic-cult-mtitle') + ' ' + tx(c, '.ic-cult-mby') + ' ' + tx(c, '.ic-cult-ov')).toLowerCase());
var lk = c.getAttribute('data-link');
if (lk) { c.addEventListener('click', function (e) { if (e.target.closest('a')) { return; } window.open(lk, '_blank', 'noopener'); }); }
});
var fc = grid.querySelector('.ic-cult-card[data-featured="1"]') || cards[0];
var fty = fc.getAttribute('data-type') || '', flk = fc.getAttribute('data-link') || '';
var fEl = document.createElement('section'); fEl.className = 'ic-cult-featured';
var wrap = document.createElement('div'); wrap.className = 'ic-cult-fcovwrap';
var cov = fc.querySelector('.ic-cult-cov'); if (cov) { wrap.appendChild(cov.cloneNode(true)); }
var fx = document.createElement('div'); fx.className = 'ic-cult-fx';
fx.innerHTML = '<div class="ic-cult-feyebrow">Featured</div><div class="ic-cult-ftitle"></div><div class="ic-cult-fmeta"></div><p class="ic-cult-fblurb"></p>';
fx.querySelector('.ic-cult-ftitle').textContent = tx(fc, '.ic-cult-mtitle');
fx.querySelector('.ic-cult-fmeta').textContent = (LABEL[fty] || '') + ' \u00b7 ' + (fc.getAttribute('data-year') || '') + ' \u00b7 ' + tx(fc, '.ic-cult-mby');
fx.querySelector('.ic-cult-fblurb').textContent = tx(fc, '.ic-cult-ov');
if (flk) { var a = document.createElement('a'); a.className = 'ic-cult-fcta'; a.href = flk; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'Where to find it \u2192'; fx.appendChild(a); }
fEl.appendChild(wrap); fEl.appendChild(fx);
var bar = document.createElement('div'); bar.className = 'ic-cult-bar';
var tabsEl = document.createElement('div'); tabsEl.className = 'ic-cult-tabs';
var search = document.createElement('input'); search.type = 'search'; search.className = 'ic-cult-search'; search.placeholder = 'Search titles, creators\u2026';
bar.appendChild(tabsEl); bar.appendChild(search);
var TABS = [['all', 'All'], ['documentary', 'Documentaries'], ['film', 'Film & TV'], ['music', 'Music'], ['books', 'Books & Podcasts']]
.filter(function (x) { return x[0] === 'all' || (GROUP[x[0]] ? GROUP[x[0]].some(function (g) { return byType[g]; }) : byType[x[0]]); });
var active = 'all', query = '', chips = [];
function inTab(ty2) { return active === 'all' || (GROUP[active] ? GROUP[active].indexOf(ty2) >= 0 : ty2 === active); }
function cnt(k) { return cards.filter(function (c) { var ty2 = c.getAttribute('data-type'); return k === 'all' ? true : (GROUP[k] ? GROUP[k].indexOf(ty2) >= 0 : ty2 === k); }).length; }
function apply() {
cards.forEach(function (c) { var ok = inTab(c.getAttribute('data-type')) && (!query || (c.getAttribute('data-s') || '').indexOf(query) >= 0); c.style.display = ok ? '' : 'none'; });
chips.forEach(function (ch) { ch.setAttribute('aria-selected', ch.getAttribute('data-f') === active); });
}
TABS.forEach(function (x) {
var b = document.createElement('button'); b.className = 'ic-cult-tab'; b.setAttribute('data-f', x[0]); b.setAttribute('aria-selected', x[0] === active);
b.appendChild(document.createTextNode(x[1] + ' '));
var n = document.createElement('span'); n.className = 'ic-cult-tn'; n.textContent = cnt(x[0]); b.appendChild(n);
b.addEventListener('click', function () { active = x[0]; apply(); });
tabsEl.appendChild(b); chips.push(b);
});
search.addEventListener('input', function () { query = search.value.trim().toLowerCase(); apply(); });
app.insertBefore(fEl, grid); app.insertBefore(bar, grid); apply();
});
/* Culture: recommend-a-title form -> gated review queue */
ready(function () {
var box = document.getElementById('ic-rec-form');
if (!box) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
var user = mw.config.get('wgUserName');
if (!user) {
box.innerHTML = '<p><em>Please <a href="/index.php?title=Special:UserLogin&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">log in</a> to recommend a title. New here? See the <a href="/index.php/ICE_List:Contribute">Contribute</a> page.</em></p>';
return;
}
function fld(label, el) { var w = document.createElement('label'); w.className = 'ic-rec-field'; var s = document.createElement('span'); s.textContent = label; w.appendChild(s); w.appendChild(el); return w; }
function inp(ph) { var i = document.createElement('input'); i.type = 'text'; i.placeholder = ph; return i; }
var sel = document.createElement('select');
['Documentary', 'Film & TV', 'Music', 'Book', 'Podcast'].forEach(function (o) { var op = document.createElement('option'); op.textContent = o; sel.appendChild(op); });
var title = inp('e.g. El Norte'), year = inp('e.g. 1983'), creator = inp('Director / artist / author'), link = inp('Where to watch / listen / read (URL)');
var blurb = document.createElement('textarea'); blurb.rows = 3; blurb.placeholder = 'One sentence: what it is and why it belongs.';
var btn = document.createElement('button'); btn.className = 'ic-cult-fcta'; btn.textContent = 'Submit recommendation';
var msg = document.createElement('div'); msg.className = 'ic-rec-msg';
[fld('Type', sel), fld('Title', title), fld('Year', year), fld('Creator', creator), fld('Link', link), fld('Why it belongs', blurb)].forEach(function (f) { box.appendChild(f); });
box.appendChild(btn); box.appendChild(msg);
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); }
btn.addEventListener('click', function () {
if (!title.value.trim()) { msg.textContent = 'Please enter a title.'; return; }
btn.disabled = true; msg.textContent = 'Submitting…';
var id = 'cult-' + Date.now().toString(36) + Math.floor(Math.random() * 10000);
var pn = 'ICE List:Submission - ' + id;
var body = "'''Culture recommendation''' (submitted by [[User:" + user + "]]) — review and, if suitable, add to [[ICE List:Culture]].\n\n" +
"* '''Type:''' " + esc(sel.value) + "\n* '''Title:''' " + esc(title.value) + "\n* '''Year:''' " + esc(year.value) +
"\n* '''Creator:''' " + esc(creator.value) + "\n* '''Link:''' <nowiki>" + esc(link.value) + "</nowiki>\n* '''Why it belongs:''' " + esc(blurb.value) + "\n\n" +
"[[Cat" + "egory:Culture recommendations]]\n";
api.postWithToken('csrf', { action: 'edit', title: pn, text: body, createonly: 1, summary: 'Culture recommendation via form' })
.then(function () { box.innerHTML = '<p><strong>✓ Thank you.</strong> Your recommendation has gone to the review queue and will appear on the library once an admin approves it.</p>'; })
.catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
});
});
/* Profile "My desk": dashboard — stats, tools, pull-a-task buttons */
ready(function () {
var user = mw.config.get('wgUserName');
if (!user) { return; }
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'User:' + user) { return; }
var groups = mw.config.get('wgUserGroups') || [];
if (!/^ICEListAdmin[0-9]+$/.test(user) && !['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var panel = document.createElement('div'); panel.className = 'ic-desk';
panel.innerHTML =
'<div class="ic-desk__kicker">My desk</div>' +
'<div class="ic-desk__stats"></div>' +
'<div class="ic-desk__tools"></div>' +
'<div class="ic-desk__pulls"><div class="ic-desk__pullkick">Pull something to work on</div><div class="ic-desk__pullbtns"><span class="ic-desk__loading">Loading your queues…</span></div><div class="ic-desk__pullresult"></div></div>';
content.insertBefore(panel, content.firstChild);
if (user === 'ICEListAdmin6') {
var cs = document.createElement('div'); cs.className = 'ic-desk__culture';
cs.innerHTML = '<div class="ic-desk__kicker">Culture recommendations</div><div id="ic-culture-inbox">Loading…</div>';
panel.appendChild(cs);
}
var toolsEl = panel.querySelector('.ic-desk__tools');
[['Review hub', '/index.php/ICE_List:Review'], ['Pending changes', '/index.php/Special:PendingChanges'], ['Unreviewed pages', '/index.php/Special:UnreviewedPages'], ['Submissions queue', '/index.php/ICE_List:Submissions']].forEach(function (l) {
var sp = document.createElement('span'); sp.className = 'ic-hero__btn'; var a = document.createElement('a'); a.href = l[1]; a.target = '_blank'; a.rel = 'noopener'; a.textContent = l[0]; sp.appendChild(a); toolsEl.appendChild(sp);
});
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function unrevList() { return api.get({ action: 'query', list: 'unreviewedpages', urnamespace: 0, urlimit: 500, formatversion: 2 }).then(function (d) { return ((d.query && d.query.unreviewedpages) || []).map(function (p) { return p.title; }); }).catch(function () { return []; }); }
function catList(cat) { return api.get({ action: 'query', list: 'categorymembers', cmtitle: cat, cmnamespace: 0, cmlimit: 200, formatversion: 2 }).then(function (d) { return ((d.query && d.query.categorymembers) || []).map(function (p) { return p.title; }); }).catch(function () { return []; }); }
var statsEl = panel.querySelector('.ic-desk__stats'), btnsEl = panel.querySelector('.ic-desk__pullbtns'), resEl = panel.querySelector('.ic-desk__pullresult');
function stat(n, label, href, red) { var d = document.createElement('div'); d.className = 'ic-stat' + (red ? ' ic-stat--red' : ''); d.innerHTML = '<a href="' + href + '" target="_blank" rel="noopener"><span class="n">' + n + '</span><span class="l">' + label + '</span></a>'; statsEl.appendChild(d); }
function rnd(a) { return a[Math.floor(Math.random() * a.length)]; }
function esc(x) { return x.replace(/</g, '<'); }
function url(t) { return '/index.php/' + encodeURIComponent(t.replace(/ /g, '_')); }
function render(label, t, href, red, titles, review) {
resEl.innerHTML = '<div class="ic-desk__task' + (red ? ' ic-desk__task--red' : '') + '">' +
'<div class="ic-desk__taskkick">' + label + '</div>' +
'<a class="ic-desk__title" href="' + href + '" target="_blank" rel="noopener">' + esc(t) + '</a>' +
'<div class="ic-desk__act"><span class="ic-hero__btn ic-hero__btn--primary"><a href="' + href + '" target="_blank" rel="noopener">' + (review ? 'Review it →' : 'Open →') + '</a></span>' +
'<button class="ic-desk__another" type="button">Pull another</button></div></div>';
resEl.querySelector('.ic-desk__another').addEventListener('click', function () { showResult(label, titles, red, review); });
}
function showResult(label, titles, red, review) {
if (!titles.length) { resEl.innerHTML = '<div class="ic-desk__task"><div class="ic-desk__taskkick">' + label + '</div>Nothing in this queue right now.</div>'; return; }
var t = rnd(titles);
if (review) {
api.get({ action: 'query', prop: 'flagged', titles: t, formatversion: 2 }).then(function (d) {
var p = d.query.pages[0], st = p.flagged && p.flagged.stable_revid;
var href = st ? ('/index.php?title=' + encodeURIComponent(t.replace(/ /g, '_')) + '&diff=cur&oldid=' + st) : (url(t) + '?stable=0');
render(label, t, href, red, titles, review);
}).catch(function () { render(label, t, url(t), red, titles, review); });
} else {
render(label, t, url(t), red, titles, review);
}
}
function addBtn(label, titles, red, review) {
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary';
var a = document.createElement('a'); a.href = '#'; a.textContent = label + ' →';
a.addEventListener('click', function (e) { e.preventDefault(); showResult(label, titles, red, review); });
sp.appendChild(a); btnsEl.appendChild(sp);
}
Promise.all([unrevList(), catList('Category:Submission review'), catList('Category:Agents needing a photo')]).then(function (res) {
var unrev = res[0], subs = res[1].filter(function (t) { return t.indexOf('ICE List:Submission - cult-') !== 0; }), photos = res[2];
stat(unrev.length >= 500 ? '500+' : unrev.length, 'Pending review', '/index.php/Special:UnreviewedPages', true);
stat(subs.length, 'Submissions to route', '/index.php/ICE_List:Submissions', false);
stat(photos.length, 'Photos needed', '/index.php/Category:Agents_needing_a_photo', false);
btnsEl.innerHTML = '';
addBtn('Review a page', unrev, true, true);
addBtn('Route a submission', subs, false, false);
addBtn('Photo an agent', photos, false, false);
}).catch(function () { btnsEl.innerHTML = '<em>Could not load your queues.</em>'; });
});
});
/* Culture inbox: one-click review -> add to library (ICEListAdmin6) */
ready(function () {
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { return; }
var box = document.getElementById('ic-culture-inbox');
if (!box) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function field(txt, label) {
var lines = txt.split('\n'), pre = "* '''" + label + ":'''";
for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf(pre) === 0) { return lines[i].slice(pre.length).replace(/<\/?nowiki>/g, '').trim(); } }
return '';
}
function esc(x) { return (x || '').replace(/[|\[\]{}]/g, '').trim(); }
api.get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Culture recommendations', cmlimit: 100, formatversion: 2 }).then(function (d) {
var mems = ((d.query && d.query.categorymembers) || []).filter(function (p) { return p.ns === 0 && p.title.indexOf('ICE List:Submission - cult-') === 0; });
box.innerHTML = '';
if (!mems.length) { box.innerHTML = '<p><em>No pending recommendations.</em></p>'; return; }
mems.forEach(function (p) { renderCard(p.title); });
});
function renderCard(title) {
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) {
var pg = d.query.pages[0]; if (!pg.revisions) { return; }
var txt = pg.revisions[0].slots.main.content;
var sm = txt.indexOf('[[User:'), sub = '';
if (sm >= 0) { sub = txt.slice(sm + 7, txt.indexOf(']]', sm)).split('|')[0]; }
var rec = { type: field(txt, 'Type'), title: field(txt, 'Title'), year: field(txt, 'Year'), creator: field(txt, 'Creator'), link: field(txt, 'Link'), blurb: field(txt, 'Why it belongs'), sub: sub };
var card = document.createElement('div'); card.className = 'ic-inbox-card';
card.innerHTML = '<div class="ic-inbox-h"><strong>' + esc(rec.title) + '</strong> <span class="ic-inbox-meta">' + esc(rec.type) + ' · ' + esc(rec.year) + ' · ' + esc(rec.creator) + '</span></div>' +
'<div class="ic-inbox-blurb">' + esc(rec.blurb) + '</div>' +
'<div class="ic-inbox-link">' + (rec.link ? '<a href="' + esc(rec.link) + '" target="_blank" rel="noopener">' + esc(rec.link) + '</a>' : '(no link)') + '</div>' +
'<div class="ic-inbox-sub">from ' + esc(rec.sub) + '</div>';
var actions = document.createElement('div'); actions.className = 'ic-inbox-actions';
var msg = document.createElement('span'); msg.className = 'ic-inbox-msg';
var addb = document.createElement('button'); addb.className = 'ic-inbox-add'; addb.textContent = 'Add to library';
var disb = document.createElement('button'); disb.className = 'ic-inbox-discard'; disb.textContent = 'Discard';
addb.addEventListener('click', function () {
addb.disabled = true; disb.disabled = true; msg.textContent = 'Adding…';
addToLibrary(rec).then(function () { return resolve(title, 'added to [[ICE List:Culture]]'); }).then(function () {
msg.textContent = '✓ Added.'; card.style.opacity = 0.5; setTimeout(function () { card.remove(); }, 700);
}).catch(function (e) { addb.disabled = false; disb.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
disb.addEventListener('click', function () {
if (!confirm('Discard this recommendation?')) { return; }
addb.disabled = true; disb.disabled = true; msg.textContent = '…';
resolve(title, 'discarded').then(function () { card.style.opacity = 0.5; setTimeout(function () { card.remove(); }, 500); }).catch(function () { msg.textContent = 'Error'; });
});
actions.appendChild(addb); actions.appendChild(disb); actions.appendChild(msg); card.appendChild(actions);
box.appendChild(card);
});
}
function addToLibrary(rec) {
var pick = '{{Culture pick|type=' + esc(rec.type) + '|title=' + esc(rec.title) + '|year=' + esc(rec.year) + '|creator=' + esc(rec.creator) + '|blurb=' + esc(rec.blurb) + '|link=' + esc(rec.link) + '}}';
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: 'ICE List:Culture', formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
var anchor = '\n</div>\n</div>', i = txt.indexOf(anchor);
if (i < 0) { throw new Error('library grid not found'); }
var nt = txt.slice(0, i) + '\n' + pick + txt.slice(i);
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Culture', text: nt, summary: 'Add culture pick from inbox: ' + esc(rec.title) });
});
}
function resolve(title, note) {
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
txt = txt.replace('[[Cat' + 'egory:Culture recommendations]]', '[[Cat' + 'egory:Resolved culture recommendations]]');
txt = "'''Resolved:''' " + note + '\n\n' + txt;
return api.postWithToken('csrf', { action: 'edit', title: title, text: txt, summary: 'Resolve culture recommendation: ' + note });
});
}
});
}); /* On-wiki submission forms (incident / agent / vehicle) -> gated review queue */
ready(function () {
var box = document.getElementById('ic-submit-form');
if (!box) { return; }
var kind = (box.getAttribute('data-kind') || '').toLowerCase();
var FIELDS = {
incident: [['date', 'Date (YYYY-MM-DD)'], ['city', 'City'], ['state', 'State'], ['agencies', 'Agencies involved (ICE, CBP…)'], ['description', 'What happened', 1], ['source', 'Source link(s)']],
agent: [['name', 'Agent name (leave blank if unidentified)'], ['agency', 'Agency (ICE, CBP, HSI…)'], ['role', 'Role / title'], ['field_office', 'Field office'], ['state', 'State'], ['description', 'Description / distinguishing details', 1], ['source', 'Source link(s)']],
vehicle: [['plate', 'License plate'], ['state', 'Plate state'], ['make_model', 'Make / model'], ['color', 'Color'], ['agency', 'Agency (if known)'], ['location', 'Where seen'], ['description', 'Notes', 1], ['source', 'Source link(s)']]
};
var defs = FIELDS[kind];
if (!defs) { box.innerHTML = '<em>Unknown submission type.</em>'; return; }
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
var user = mw.config.get('wgUserName');
if (!user) { box.innerHTML = '<p><em>Please <a href="/index.php?title=Special:UserLogin&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">log in</a> to submit.</em></p>'; return; }
var inputs = {};
defs.forEach(function (f) {
var w = document.createElement('label'); w.className = 'ic-rec-field';
var s = document.createElement('span'); s.textContent = f[1]; w.appendChild(s);
var el = f[2] ? document.createElement('textarea') : document.createElement('input');
if (f[2]) { el.rows = 3; } else { el.type = 'text'; }
w.appendChild(el); box.appendChild(w); inputs[f[0]] = el;
});
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-cult-fcta'; btn.textContent = 'Submit for review';
var msg = document.createElement('div'); msg.className = 'ic-rec-msg';
box.appendChild(btn); box.appendChild(msg);
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); }
btn.addEventListener('click', function () {
if (!inputs.description.value.trim() && !inputs[defs[0][0]].value.trim()) { msg.textContent = 'Please fill in the details.'; return; }
btn.disabled = true; msg.textContent = 'Submitting…';
var id = kind + '-' + Date.now().toString(36) + Math.floor(Math.random() * 10000);
var pn = 'ICE List:Submission - ' + id;
var body = "'''" + kind.charAt(0).toUpperCase() + kind.slice(1) + " submission''' (submitted by [[User:" + user + "]]) - review and route.\n\n* '''Kind:''' " + kind + "\n";
defs.forEach(function (f) {
var v = inputs[f[0]].value;
if (v && v.trim()) {
var lbl = f[0].charAt(0).toUpperCase() + f[0].slice(1).replace(/_/g, ' ');
body += (f[0] === 'source') ? "* '''Source:''' <nowiki>" + esc(v) + "</nowiki>\n" : "* '''" + lbl + ":''' " + esc(v) + "\n";
}
});
body += "\n" + CAT('Submission review') + CAT('Pending submissions') + "\n";
api.postWithToken('csrf', { action: 'edit', title: pn, text: body, createonly: 1, summary: 'On-wiki ' + kind + ' submission' })
.then(function () { box.innerHTML = '<p><strong>✓ Thank you.</strong> Your ' + kind + ' submission has gone to the review queue. A reviewer will check it before anything is published — and your identity is not recorded.</p>'; })
.catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
});
});
/* Submission review inbox: list on-wiki submissions, promote to a page or discard */
ready(function () {
var box = document.getElementById('ic-sub-inbox');
if (!box) { return; }
var groups = mw.config.get('wgUserGroups') || [];
if (!['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { box.innerHTML = '<em>Reviewer access required.</em>'; return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function field(txt, label) { var lines = txt.split('\n'), pre = "* '''" + label + ":'''"; for (var i = 0; i < lines.length; i++) { if (lines[i].indexOf(pre) === 0) { return lines[i].slice(pre.length).replace(/<\/?nowiki>/g, '').trim(); } } return ''; }
function esc(x) { return (x || '').replace(/[|\[\]{}]/g, '').trim(); }
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; }
var OB = '{' + '{';
function yearCat(date) { var m = (date || '').match(/20\d\d/); return m ? '[[Cat' + 'egory:Incidents in ' + m[0] + ']]' : ''; }
var ALL = ['Date', 'City', 'County', 'State', 'Agencies', 'Name', 'Agency', 'Role', 'Field office', 'Plate', 'Make model', 'Color', 'Location', 'Description', 'Source'];
api.get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Pending submissions', cmnamespace: 0, cmlimit: 100, formatversion: 2 }).then(function (d) {
var mems = ((d.query && d.query.categorymembers) || []).filter(function (p) { return p.title.indexOf('ICE List:Submission - ') === 0; });
box.innerHTML = ''; if (!mems.length) { box.innerHTML = '<p><em>No pending submissions.</em></p>'; return; }
mems.forEach(function (p) { card(p.title); });
});
function card(title) {
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) {
var pg = d.query.pages[0]; if (!pg.revisions) { return; }
var txt = pg.revisions[0].slots.main.content;
var kind = field(txt, 'Kind') || 'submission';
var sm = txt.indexOf('[[User:'), sub = ''; if (sm >= 0) { sub = txt.slice(sm + 7, txt.indexOf(']]', sm)).split('|')[0]; }
var rec = { kind: kind, sub: sub }; ALL.forEach(function (l) { var v = field(txt, l); if (v) { rec[l.toLowerCase().replace(/ /g, '_')] = v; } });
var c = document.createElement('div'); c.className = 'ic-inbox-card';
var rows = ALL.map(function (l) { var k = l.toLowerCase().replace(/ /g, '_'); return rec[k] ? ('<div class="ic-subrow"><span>' + l + '</span> ' + esc(rec[k]) + '</div>') : ''; }).join('');
c.innerHTML = '<div class="ic-inbox-h"><strong>' + kind.toUpperCase() + ' submission</strong> <span class="ic-inbox-meta">from ' + esc(sub) + '</span></div>' + rows;
var act = document.createElement('div'); act.className = 'ic-inbox-actions';
var msg = document.createElement('span'); msg.className = 'ic-inbox-msg';
var pb = document.createElement('button'); pb.className = 'ic-inbox-add'; pb.type = 'button'; pb.textContent = 'Promote to page';
var db = document.createElement('button'); db.type = 'button'; db.textContent = 'Discard';
pb.addEventListener('click', function () { promote(title, kind, rec, c, msg, pb, db); });
db.addEventListener('click', function () { if (!confirm('Discard this submission?')) { return; } pb.disabled = db.disabled = true; msg.textContent = '…'; resolve(title, 'discarded').then(function () { c.style.opacity = 0.5; setTimeout(function () { c.remove(); }, 500); }).catch(function () { msg.textContent = 'Error'; }); });
act.appendChild(pb); act.appendChild(db); act.appendChild(msg); c.appendChild(act); box.appendChild(c);
});
}
function buildPage(kind, rec) {
if (kind === 'incident') {
var loc = [rec.city, rec.state].filter(Boolean).join(', ');
var title = ((rec.date ? rec.date + ' ' : '') + 'Incident' + (loc ? ' in ' + loc : '')).trim();
var body = OB + 'Infobox incident\n| title = ' + esc(title) + '\n| date = ' + esc(rec.date || '') + '\n| city = ' + esc(rec.city || '') + '\n| state = ' + esc(rec.state || '') + '\n| agencies = ' + esc(rec.agencies || '') + '\n| status = Developing (from a volunteer submission).\n| verification = Developing - submitted for review.\n| key_sources =\n* ' + esc(rec.source || '-') + '\n}}\n\n' + esc(rec.description || '') + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (rec.state ? CAT('Incidents in ' + esc(rec.state)) : '') + yearCat(rec.date) + '\n';
return { title: title, body: body };
}
if (kind === 'agent') {
if (!rec.name) { return null; }
var b = OB + 'Agent page\n|name=' + esc(rec.name) + '\n|agency=' + esc(rec.agency || 'Unknown') + '\n|role=' + esc(rec.role || '') + '\n|field_office=' + esc(rec.field_office || '') + '\n|state=' + esc(rec.state || '') + '\n|status=Active\n|image=nopfp.png\n|verification=Submitted for review\n|summary=' + esc(rec.description || '') + '\n}}\n\n== Documented Incidents ==\n' + OB + 'IncidentsForAgent}}\n\n' + OB + 'MediaForAgent}}\n\n' + CAT('Agents') + CAT('Agents needing a photo') + '\n';
return { title: rec.name, body: b };
}
if (kind === 'vehicle') {
var vt = rec.plate ? ('Vehicle:' + rec.plate) : ('Vehicle submission ' + (rec.state || ''));
var vb = "'''Vehicle'''" + (rec.plate ? ' plate ' + esc(rec.plate) : '') + (rec.state ? ' (' + esc(rec.state) + ')' : '') + '.\n\n* Make/model: ' + esc(rec.make_model || '-') + '\n* Color: ' + esc(rec.color || '-') + '\n* Agency: ' + esc(rec.agency || '-') + '\n* Where seen: ' + esc(rec.location || '-') + '\n* Notes: ' + esc(rec.description || '-') + '\n* Source: ' + esc(rec.source || '-') + '\n\n' + CAT('Vehicles') + '\n';
return { title: vt, body: vb };
}
return null;
}
function promote(title, kind, rec, c, msg, pb, db) {
var pg = buildPage(kind, rec);
if (!pg) { msg.textContent = 'Unidentified agent / unknown kind — open the submission to build it manually.'; return; }
pb.disabled = db.disabled = true; msg.textContent = 'Creating ' + pg.title + '…';
api.get({ action: 'query', titles: pg.title, formatversion: 2 }).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + pg.title + '" already exists.'); }
return api.postWithToken('csrf', { action: 'edit', title: pg.title, createonly: 1, text: pg.body, summary: 'Create from reviewed submission' });
}).then(function () { return resolve(title, 'promoted to [[' + pg.title + ']]'); }).then(function () {
msg.innerHTML = '✓ Created <a href="/index.php/' + encodeURIComponent(pg.title.replace(/ /g, '_')) + '" target="_blank" rel="noopener">' + esc(pg.title) + '</a> (now pending review).';
c.style.opacity = 0.6; setTimeout(function () { c.remove(); }, 1600);
}).catch(function (e) { pb.disabled = db.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
}
function resolve(title, note) {
return api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: title, formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
txt = txt.replace(CAT('Pending submissions'), CAT('Resolved submissions'));
txt = "'''Resolved:''' " + note + '\n\n' + txt;
return api.postWithToken('csrf', { action: 'edit', title: title, text: txt, summary: 'Resolve submission: ' + note });
});
}
});
}); /* Quick add: create incident / agent / vehicle / death directly from your profile */
ready(function () {
var user = mw.config.get('wgUserName');
if (!user) { return; }
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'User:' + user) { return; }
var groups = mw.config.get('wgUserGroups') || [];
if (!/^ICEListAdmin[0-9]+$/.test(user) && !['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; })) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var panel = document.createElement('div'); panel.className = 'ic-desk ic-quickadd';
panel.innerHTML = '<div class="ic-desk__kicker">Quick add</div><div class="ic-qa__btns"></div><div class="ic-qa__form"></div>';
var desk = content.querySelector('.ic-desk');
if (desk) { content.insertBefore(panel, desk.nextSibling); } else { content.insertBefore(panel, content.firstChild); }
var TYPES = {
incident: [['date', 'Date (YYYY-MM-DD)'], ['city', 'City'], ['state', 'State'], ['agencies', 'Agencies (ICE, CBP…)'], ['description', 'What happened', 1], ['source', 'Source link']],
agent: [['name', 'Full name (required)'], ['agency', 'Agency'], ['role', 'Role / title'], ['field_office', 'Field office'], ['state', 'State'], ['description', 'Description / details', 1], ['source', 'Source link']],
vehicle: [['plate', 'License plate (required)'], ['state', 'Plate state'], ['make_model', 'Make / model'], ['color', 'Color'], ['agency', 'Agency'], ['location', 'Where seen'], ['description', 'Notes', 1], ['source', 'Source link']],
death: [['name', 'Name of the deceased (required)'], ['age', 'Age'], ['nationality', 'Nationality'], ['date', 'Date of death (YYYY-MM-DD)'], ['location', 'Location / facility'], ['state', 'State'], ['agency', 'Agency'], ['cause', 'Reported cause'], ['description', 'What happened', 1], ['source', 'Source link']]
};
var btnsEl = panel.querySelector('.ic-qa__btns'), formEl = panel.querySelector('.ic-qa__form'), api = null;
function esc(x) { return (x || '').replace(/[|\[\]{}<>]/g, ' ').trim(); }
function CAT(n) { return '[[Cat' + 'egory:' + n + ']]'; }
var OB = '{' + '{';
function yearCat(date) { var m = (date || '').match(/20\d\d/); return m ? CAT('Incidents in ' + m[0]) : ''; }
Object.keys(TYPES).forEach(function (k) {
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary';
var a = document.createElement('a'); a.href = '#'; a.textContent = 'Add ' + k;
a.addEventListener('click', function (e) { e.preventDefault(); openForm(k); });
sp.appendChild(a); btnsEl.appendChild(sp);
});
if (mw.config.get('wgUserName') === 'ICEListAdmin6') { var mv = document.createElement('div'); mv.className = 'ic-qa__admin'; mv.innerHTML = '<a href="/index.php/ICE_List:Add_volunteer">+ Add a submission volunteer →</a>'; panel.appendChild(mv); }
function build(kind, v) {
if (kind === 'incident') {
var loc = [v.city, v.state].filter(Boolean).join(', ');
var title = ((v.date ? v.date + ' ' : '') + 'Incident' + (loc ? ' in ' + loc : '')).trim();
return { title: title, body: OB + 'Infobox incident\n| title = ' + esc(title) + '\n| date = ' + esc(v.date) + '\n| city = ' + esc(v.city) + '\n| state = ' + esc(v.state) + '\n| agencies = ' + esc(v.agencies) + '\n| status = Developing.\n| verification = Added via quick-add.\n| key_sources =\n* ' + esc(v.source || '-') + '\n}}\n\n' + esc(v.description) + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (v.state ? CAT('Incidents in ' + esc(v.state)) : '') + yearCat(v.date) + '\n' };
}
if (kind === 'agent') {
if (!v.name) { return { err: 'Name is required.' }; }
return { title: v.name, body: OB + 'Agent page\n|name=' + esc(v.name) + '\n|agency=' + esc(v.agency || 'Unknown') + '\n|role=' + esc(v.role) + '\n|field_office=' + esc(v.field_office) + '\n|state=' + esc(v.state) + '\n|status=Active\n|image=nopfp.png\n|verification=Added via quick-add\n|summary=' + esc(v.description) + '\n}}\n\n== Documented Incidents ==\n' + OB + 'IncidentsForAgent}}\n\n' + OB + 'MediaForAgent}}\n\n' + CAT('Agents') + CAT('Agents needing a photo') + '\n' };
}
if (kind === 'vehicle') {
if (!v.plate) { return { err: 'Plate is required.' }; }
var vsum = (v.description || ('Vehicle plate ' + v.plate)) + (v.color ? ' Colour: ' + v.color + '.' : '') + (v.location ? ' Seen at ' + v.location + '.' : '');
return { title: 'Vehicle:' + v.plate, body: OB + 'Vehicle page\n| identifier = ' + esc(v.plate) + '\n| plate = ' + esc(v.plate) + '\n| type = ' + esc(v.make_model || '') + '\n| agency = ' + esc(v.agency || '') + '\n| state = ' + esc(v.state || '') + '\n| status = Active\n| verification = Added via quick-add, pending review.\n| summary = ' + esc(vsum) + '\n| source_1 = ' + esc(v.source || '') + '\n}}\n' };
}
if (kind === 'death') {
if (!v.name) { return { err: 'Name is required.' }; }
var meta = [v.age, v.nationality].filter(Boolean).join(', '), sk = esc(v.date) + ' ' + esc(v.name);
return { title: 'Death of ' + v.name + ' in ICE custody', body: OB + 'Infobox incident\n| title = ' + esc(v.name) + '\n| date = ' + esc(v.date) + '\n| state = ' + esc(v.state) + '\n| country = United States\n| location_detail = ' + esc(v.location) + '\n| agencies = ' + esc(v.agency || 'ICE') + '\n| operation_type = Death in immigration detention / custody\n| status = Autopsy pending.\n| people_affected = 1\n| deaths = 1' + (v.cause ? ' — ' + esc(v.cause) : '') + '\n| verification = Added via quick-add, pending review.\n| key_sources =\n* ' + esc(v.source || '-') + '\n}}\n\n' + "'''" + esc(v.name) + "'''" + (meta ? ' (' + meta + ')' : '') + ' died' + (v.date ? ' on ' + esc(v.date) : '') + ' while in the custody of ICE' + (v.location ? ', at ' + esc(v.location) : '') + '.' + (v.cause ? ' Reported cause: ' + esc(v.cause) + '.' : '') + '\n\n' + esc(v.description) + '\n\n' + OB + 'MediaForIncident}}\n\n' + CAT('Incidents') + CAT('Incidents involving ICE') + (v.state ? CAT('Incidents in ' + esc(v.state)) : '') + CAT('Deaths by immigration enforcement|' + sk) + CAT('Deaths in immigration detention|' + sk) + yearCat(v.date) + '\n' };
}
}
function openForm(kind) {
formEl.innerHTML = ''; var inputs = {};
TYPES[kind].forEach(function (f) {
var w = document.createElement('label'); w.className = 'ic-rec-field';
var s = document.createElement('span'); s.textContent = f[1]; w.appendChild(s);
var el = f[2] ? document.createElement('textarea') : document.createElement('input');
if (f[2]) { el.rows = 3; } else { el.type = 'text'; }
w.appendChild(el); formEl.appendChild(w); inputs[f[0]] = el;
});
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-inbox-add'; btn.textContent = 'Create ' + kind + ' page';
var cancel = document.createElement('button'); cancel.type = 'button'; cancel.textContent = 'Cancel';
var msg = document.createElement('div'); msg.className = 'ic-rec-msg';
var row = document.createElement('div'); row.className = 'ic-inbox-actions'; row.appendChild(btn); row.appendChild(cancel);
formEl.appendChild(row); formEl.appendChild(msg);
cancel.addEventListener('click', function () { formEl.innerHTML = ''; });
btn.addEventListener('click', function () {
var v = {}; Object.keys(inputs).forEach(function (k) { v[k] = inputs[k].value; });
var pg = build(kind, v);
if (pg.err) { msg.textContent = pg.err; return; }
btn.disabled = true; msg.textContent = 'Creating…';
mw.loader.using(['mediawiki.api']).then(function () {
if (!api) { api = new mw.Api(); }
api.get({ action: 'query', titles: pg.title, formatversion: 2 }).then(function (d) {
if (d.query.pages[0] && d.query.pages[0].missing === undefined) { throw new Error('A page named "' + pg.title + '" already exists.'); }
return api.postWithToken('csrf', { action: 'edit', title: pg.title, createonly: 1, text: pg.body, summary: 'Quick-add ' + kind });
}).then(function () {
msg.innerHTML = '✓ Created <a href="/index.php/' + encodeURIComponent(pg.title.replace(/ /g, '_')) + '" target="_blank" rel="noopener">' + esc(pg.title) + '</a>.';
setTimeout(function () { formEl.innerHTML = ''; }, 3000);
}).catch(function (e) { btn.disabled = false; msg.textContent = 'Error: ' + (e && e.message ? e.message : e); });
});
});
}
}); /* Add a confined submission volunteer: account + submitter group + profile */
ready(function () {
var box = document.getElementById('ic-addvol');
if (!box) { return; }
var groups = mw.config.get('wgUserGroups') || [];
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { box.innerHTML = '<em>This tool is restricted.</em>'; return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function fld(label, el) { var w = document.createElement('label'); w.className = 'ic-rec-field'; var s = document.createElement('span'); s.textContent = label; w.appendChild(s); w.appendChild(el); return w; }
function gen() { var c = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'; var p = ''; for (var i = 0; i < 14; i++) { p += c.charAt(Math.floor(Math.random() * c.length)); } return p; }
var uname = document.createElement('input'); uname.type = 'text'; uname.placeholder = 'Username for the new volunteer';
var pass = document.createElement('input'); pass.type = 'text'; pass.value = gen();
var btn = document.createElement('button'); btn.type = 'button'; btn.className = 'ic-inbox-add'; btn.textContent = 'Create volunteer';
var msg = document.createElement('div'); msg.className = 'ic-rec-msg';
box.appendChild(fld('Username', uname)); box.appendChild(fld('Password (auto-generated, editable)', pass)); box.appendChild(btn); box.appendChild(msg);
function esc(x) { return (x || '').replace(/</g, '<'); }
btn.addEventListener('click', function () {
var u = uname.value.trim(), p = pass.value.trim();
if (!u || p.length < 8) { msg.textContent = 'Enter a username and a password of 8+ characters.'; return; }
btn.disabled = true;
var log = [];
function show() { msg.innerHTML = log.join('<br>'); }
// 1) create account
log.push('Creating account…'); show();
api.get({ action: 'query', meta: 'tokens', type: 'createaccount', formatversion: 2 }).then(function (d) {
var tok = d.query.tokens.createaccounttoken;
return api.post({ action: 'createaccount', createtoken: tok, username: u, password: p, retype: p, createreturnurl: location.href, reason: 'Confined submission volunteer' });
}).then(function (r) {
var st = r.createaccount && r.createaccount.status;
log[log.length - 1] = (st === 'PASS') ? '✓ Account created.' : '✗ Account: ' + ((r.createaccount && r.createaccount.message) || st || 'failed — create it via Special:CreateAccount, then re-run to add group + profile.');
show();
}).catch(function (e) {
log[log.length - 1] = '✗ Account: ' + (e && e.message ? e.message : e) + ' (try Special:CreateAccount, then re-run).'; show();
}).then(function () {
// 2) profile page
log.push('Creating profile…'); show();
var prof = "''Volunteer contributor to the ICE List.''\n\n== Submit ==\nThanks for helping document immigration enforcement. Everything you send is reviewed before it is published, and your identity is not recorded:\n* [[ICE List:Submit incident (test)|Report an incident]]\n* [[ICE List:Submit agent (test)|Submit an agent]]\n* [[ICE List:Submit vehicle (test)|Submit a vehicle]]\n";
return api.postWithToken('csrf', { action: 'edit', title: 'User:' + u, text: prof, createonly: 1, summary: 'Create volunteer profile' })
.then(function () { log[log.length - 1] = '✓ Profile created.'; show(); })
.catch(function (e) { log[log.length - 1] = '• Profile: ' + (e && e.message ? e.message : e); show(); });
}).then(function () {
// 3) submitter group
log.push('Adding to submitter group…'); show();
return api.postWithToken('userrights', { action: 'userrights', user: u, add: 'submitter', reason: 'confined volunteer' })
.then(function () { log[log.length - 1] = '✓ Added to submitter group (confined to submissions).'; show(); })
.catch(function () { log[log.length - 1] = '• Submitter group not applied — paste the submitter LocalSettings block, then add the group in Special:UserRights.'; show(); });
}).then(function () {
log.push('<strong>Hand these to the volunteer — Username: <code>' + esc(u) + '</code> · Password: <code>' + esc(p) + '</code></strong> · <a href="/index.php/User:' + encodeURIComponent(u) + '" target="_blank" rel="noopener">profile</a>');
show();
uname.value = ''; pass.value = gen(); btn.disabled = false;
});
});
});
}); /* Submitter desk: submission console on a confined volunteer's own profile */
ready(function () {
var user = mw.config.get('wgUserName');
if (!user) { return; }
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'User:' + user) { return; }
var groups = mw.config.get('wgUserGroups') || [];
var isAdmin = /^ICEListAdmin[0-9]+$/.test(user) || ['sysop', 'bureaucrat', 'reviewer', 'editor'].some(function (g) { return groups.indexOf(g) >= 0; });
if (isAdmin) { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var panel = document.createElement('div'); panel.className = 'ic-desk';
panel.innerHTML = '<div class="ic-desk__kicker">Contribute</div>' +
'<p class="ic-desk__hello">Thanks for helping document immigration enforcement. Everything you submit is reviewed before it is published, and your identity is not recorded. Pick what to send:</p>' +
'<div class="ic-desk__tools"></div>';
content.insertBefore(panel, content.firstChild);
var tools = panel.querySelector('.ic-desk__tools');
[['Report an incident', '/index.php/ICE_List:Submit_incident_(test)'], ['Submit an agent', '/index.php/ICE_List:Submit_agent_(test)'], ['Submit a vehicle', '/index.php/ICE_List:Submit_vehicle_(test)']].forEach(function (l) {
var sp = document.createElement('span'); sp.className = 'ic-hero__btn ic-hero__btn--primary';
var a = document.createElement('a'); a.href = l[1]; a.textContent = l[0]; sp.appendChild(a); tools.appendChild(sp);
});
}); /* Account requests panel on ICEListAdmin6's profile (robust) */
ready(function () {
if (mw.config.get('wgUserName') !== 'ICEListAdmin6') { return; }
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:ICEListAdmin6') { return; }
var content = document.querySelector('#mw-content-text .mw-parser-output');
if (!content) { return; }
var panel = document.createElement('div'); panel.className = 'ic-desk ic-areq-panel';
panel.innerHTML = '<div class="ic-desk__kicker">Account requests</div><div class="ic-areq">Checking…</div>';
var desks = content.querySelectorAll('.ic-desk');
if (desks.length) { desks[desks.length - 1].parentNode.insertBefore(panel, desks[desks.length - 1].nextSibling); }
else { content.insertBefore(panel, content.firstChild); }
var box = panel.querySelector('.ic-areq');
var QUEUE = '/index.php/Special:ConfirmAccounts';
var openBtn = '<p style="margin-top:.6rem"><a class="ic-hero__btn ic-hero__btn--primary" style="display:inline-block" href="' + QUEUE + '"><span>Open the full queue →</span></a></p>';
fetch(QUEUE, { credentials: 'same-origin' }).then(function (r) { return r.text(); }).then(function (html) {
var doc = new DOMParser().parseFromString(html, 'text/html');
var cont = doc.querySelector('#mw-content-text');
if (!cont) { box.innerHTML = 'Could not load the queue. ' + openBtn; return; }
// broad: any link into ConfirmAccounts with a query (request rows)
var links = [].slice.call(cont.querySelectorAll('a[href]')).filter(function (a) {
var h = a.getAttribute('href') || ''; return /[?&]acrid=\d/.test(h) && a.textContent.trim().length > 1;
});
var seen = {}, items = [];
links.forEach(function (a) { var t = a.textContent.trim(); if (!seen[t]) { seen[t] = 1; items.push({ t: t, h: a.getAttribute('href') }); } });
if (items.length) {
var html2 = '<p><b>' + items.length + '</b> pending request(s):</p><ul class="ic-areq-list">';
items.forEach(function (it) { html2 += '<li><a href="' + it.h + '">' + it.t + '</a></li>'; });
box.innerHTML = html2 + '</ul>' + openBtn;
} else {
// show the queue's own text so nothing is ever silently blank
var txt = (cont.textContent || '').replace(/\s+/g, ' ').trim();
var empty = /no pending|there are no|no requests|no account requests/i.test(txt);
box.innerHTML = '<p>' + (empty ? 'No pending requests found in the queue.' : 'Requests may be present but not auto-listed — open the queue to review.') + '</p>' + openBtn;
}
}).catch(function () { box.innerHTML = 'Could not reach the queue. ' + openBtn; });
}); /* Homepage: featured culture work of the day (rotates daily) */
ready(function () {
var box = document.getElementById('ic-featured-media');
if (!box) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', prop: 'revisions', rvprop: 'content', rvslots: 'main', titles: 'ICE List:Culture', formatversion: 2 }).then(function (d) {
var txt = d.query.pages[0].revisions[0].slots.main.content;
var items = [], re = /\{\{Culture pick\|([^}]*)\}\}/g, m;
while ((m = re.exec(txt))) {
var o = {}; m[1].split('|').forEach(function (kv) { var i = kv.indexOf('='); if (i > 0) { o[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } });
if (o.title) { items.push(o); }
}
if (!items.length) { box.textContent = ''; return; }
var p = items[Math.floor(Date.now() / 86400000) % items.length];
var COL = { documentary: '#E2231A', film: '#1A4FE2', 'film & tv': '#1A4FE2', music: '#0E8F4E', book: '#9A5A24', podcast: '#6A2FB8' };
var col = COL[(p.type || '').toLowerCase()] || '#E2231A';
function esc(x) { var e = document.createElement('div'); e.textContent = x || ''; return e.innerHTML; }
var url = '/index.php/' + encodeURIComponent('Culture:' + p.title.replace(/ /g, '_'));
box.innerHTML = '<a class="ic-fm" href="' + url + '">' +
'<div class="ic-fm__cov" style="--pc:' + col + '"></div>' +
'<div class="ic-fm__txt"><div class="ic-fm__kick">' + esc(p.type) + (p.year ? ' · ' + esc(p.year) : '') + '</div>' +
'<div class="ic-fm__ttl">' + esc(p.title) + '</div>' +
(p.creator ? '<div class="ic-fm__by">' + esc(p.creator) + '</div>' : '') +
(p.blurb ? '<p class="ic-fm__desc">' + esc(p.blurb) + '</p>' : '') +
'<span class="ic-fm__cta">Where to find it →</span></div></a>';
if (p.cover) {
api.get({ action: 'query', titles: 'File:' + p.cover, prop: 'imageinfo', iiprop: 'url', iiurlwidth: 240, formatversion: 2 }).then(function (dd) {
var pg = dd.query.pages[0], ii = pg && pg.imageinfo;
if (ii && ii[0]) { var cov = box.querySelector('.ic-fm__cov'); cov.style.backgroundImage = 'url(' + ii[0].thumburl + ')'; cov.classList.add('ic-fm__cov--img'); }
});
}
}).catch(function () { box.textContent = ''; });
});
}); /* Mobile homepage: interleave columns + accordion (first half open, second half collapsed) */
ready(function () {
if (mw.config.get('wgPageName') !== 'Main_Page') { return; }
var cols = document.querySelectorAll('.icelist-homepage-columns > .icelist-homepage-column');
if (cols.length < 2) { return; }
var n = cols.length, items = [];
[].forEach.call(cols, function (col, ci) {
var i = 0;
[].forEach.call(col.children, function (ch) {
if (ch.tagName === 'STYLE' || ch.tagName === 'LINK') { return; }
var ord = i * n + ci; ch.style.order = String(ord); i++;
if (ch.classList && ch.classList.contains('ic-card') && ch.querySelector('.ic-card__head') && ch.querySelector('.ic-card__body')) {
items.push({ el: ch, ord: ord });
}
});
});
items.sort(function (a, b) { return a.ord - b.ord; });
var half = Math.ceil(items.length / 2);
items.forEach(function (it, idx) {
var c = it.el, head = c.querySelector('.ic-card__head');
c.classList.add('ic-card--collap');
if (idx >= half) { c.classList.add('is-collapsed'); }
head.setAttribute('role', 'button'); head.setAttribute('tabindex', '0');
function t() { c.classList.toggle('is-collapsed'); }
head.addEventListener('click', t);
head.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); t(); } });
});
});
/* Strip the "ICE List:" title-convention prefix from main-namespace headings */
ready(function () {
if (mw.config.get('wgNamespaceNumber') !== 0) { return; }
var el = document.querySelector('.mw-page-title-main') || document.querySelector('#firstHeading');
if (!el) { return; }
['ICE List:', 'ICE List Wiki:'].forEach(function (p) {
if (el.firstChild && el.firstChild.nodeType === 3 && el.firstChild.nodeValue.indexOf(p) === 0) {
el.firstChild.nodeValue = el.firstChild.nodeValue.slice(p.length);
}
});
});
/* Language globe: toggle the dropdown menu */
ready(function () {
var boxes = [].slice.call(document.querySelectorAll('.ic-lang'));
if (!boxes.length) { return; }
boxes.forEach(function (b) {
var btn = b.querySelector('.ic-lang__btn');
if (!btn) { return; }
function tog(e) { e.stopPropagation(); boxes.forEach(function (o) { if (o !== b) { o.classList.remove('is-open'); } }); b.classList.toggle('is-open'); }
btn.addEventListener('click', tog);
btn.addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); tog(e); } });
});
document.addEventListener('click', function () { boxes.forEach(function (b) { b.classList.remove('is-open'); }); });
});
/* Site-wide article header: prepend a category eyebrow above the page title */
ready(function () {
var ns = mw.config.get('wgNamespaceNumber');
if ([0, 14, 4].indexOf(ns) < 0) { return; }
if (mw.config.get('wgPageName') === 'Main_Page') { return; }
if (document.querySelector('.ic-work')) { return; }
var h = document.querySelector('.mw-first-heading') || document.getElementById('firstHeading');
if (!h || document.getElementById('ic-pagehead-eyebrow')) { return; }
var label = '';
if (ns === 14) { label = 'Category'; }
else if (ns === 4) { label = 'ICE List'; }
else {
var cats = mw.config.get('wgCategories') || [];
var map = [['Agents', 'Agent'], ['Incidents', 'Incident'], ['Deaths', 'Death'], ['Vehicles', 'Vehicle'], ['Facilities', 'Facility'], ['Companies', 'Company'], ['Boycott', 'Boycott'], ['287(g)', '287(g) agreement']];
for (var i = 0; i < map.length; i++) {
for (var j = 0; j < cats.length; j++) { if (cats[j].indexOf(map[i][0]) !== -1) { label = map[i][1]; break; } }
if (label) { break; }
}
}
if (!label) { return; }
var eb = document.createElement('div'); eb.id = 'ic-pagehead-eyebrow'; eb.className = 'ic-pagehead-eyebrow'; eb.textContent = label;
h.parentNode.insertBefore(eb, h);
});
/* External links in content open in a new tab */
ready(function () {
var ls = document.querySelectorAll('.mw-parser-output a.external');
for (var i = 0; i < ls.length; i++) { ls[i].target = '_blank'; ls[i].rel = 'noopener noreferrer'; }
});
/* Volunteer profile desk (submitter / sorter / confirm) — approved scheme + submitter paste-and-go */
ready(function () {
var user = mw.config.get('wgUserName');
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'User:' + user) { return; }
var groups = mw.config.get('wgUserGroups') || [];
var role = null, ORDER = ['confirm', 'sorter', 'submitter'];
for (var i = 0; i < ORDER.length; i++) { if (groups.indexOf(ORDER[i]) >= 0) { role = ORDER[i]; break; } }
if (!role) { return; }
var U = mw.util.getUrl;
var CFG = {
submitter: { cls: '', eye: 'Volunteer', title: 'Your volunteer desk', paste: true,
job: 'Welcome. The fastest way to help is jumping into work that is <b>ready right now</b> — hundreds of links waiting to be sorted, and agents that need identifying. Or drop a new link below. Nothing is public until an admin publishes it.',
acts: [['Sort a waiting link →', U('ICE List:Submission/Inbox'), 1], ['Identify an agent →', U('ICE List:Most wanted'), 0], ['Task board →', U('ICE List:Submission/Tasks'), 0]],
chips: [['Submission inbox', 'Links in the inbox']],
steps: ['Spot a post, article or video about ICE', 'Paste the link(s) and hit add', 'Sorters take it from there'],
more: { eye: 'Want to do more?', text: 'Dropping links is just phase 1 — you can jump into <b>any</b> step whenever you feel like it. It is all optional, and you can stop any time.',
acts: [['Try sorting a link →', U('ICE List:Submission/Inbox'), 1], ['Suggest a 287(g) photo →', U('ICE List:Submit a lead') + '?suggest=photo', 0], ['Suggest an officer name →', U('ICE List:Submit a lead') + '?suggest=name', 0]] } },
sorter: { cls: ' ic-vdesk--sorter', eye: 'Sorter · Phase 2', title: 'Your sorter desk',
job: 'Take a submitted link and <b>pull out the facts</b> — agent photos, incidents with locations, license plates — into a clean record.',
acts: [['Sort the next link', U('ICE List:Submission/Inbox'), 1], ['Open sorting', U('ICE List:Submission/Sorting'), 0]],
chips: [['Submission inbox', 'Waiting in inbox'], ['Submission sorting', 'In sorting']],
steps: ['Claim a link from the inbox', 'Fill the record; upload screenshots', 'Send it on to Confirm'] },
confirm: { cls: ' ic-vdesk--confirm', eye: 'Confirm · Phase 3', title: 'Your confirm desk',
job: 'Check that each sorted record is <b>complete and correct</b> before it reaches the admins.',
acts: [['Review the next record', U('ICE List:Submission/Sorting'), 1], ['Open confirm queue', U('ICE List:Submission/Confirm'), 0]],
chips: [['Submission sorting', 'In sorting'], ['Submission confirm', 'Awaiting confirm']],
steps: ['Open a completed record', 'Verify the details and sources', 'Confirm — it goes to the admins'] }
};
var c = CFG[role];
var actHtml = c.acts.map(function (a) { return '<a class="ic-vbtn' + (a[2] ? '' : ' ic-vbtn--ghost') + '" href="' + a[1] + '">' + a[0] + '</a>'; }).join('');
var pasteHtml = c.paste ? '<div class="ic-vpaste"><textarea class="ic-vpaste__in" rows="3" placeholder="Paste one or more links (one per line)…"></textarea><input class="ic-vpaste__note" placeholder="Optional: what is it / where from"><select class="ic-vpaste__type"><option value="">What kind of link? (helps sorters prioritise)</option><option value="video">🎥 Video / footage</option><option value="photo">📷 Photo</option><option value="social">📱 Social post</option><option value="article">📰 News article</option><option value="document">📄 Document</option><option value="other">🔗 Other</option></select><div class="ic-vact"><a class="ic-vbtn ic-vpaste__go" href="#">Add link(s)</a><span class="ic-vpaste__msg"></span></div></div>' : '';
var chipHtml = c.chips.map(function (ch) { return '<a class="ic-vchip" data-cat="' + ch[0] + '" href="' + U('Category:' + ch[0]) + '"><b>—</b><span>' + ch[1] + '</span></a>'; }).join('');
var stepHtml = c.steps.map(function (st) { return '<div class="ic-vstep">' + st + '</div>'; }).join('');
var moreHtml = c.more ? '<div class="ic-vdesk__more"><div class="ic-veyebrow">' + c.more.eye + '</div><p class="ic-vjob">' + c.more.text + '</p><div class="ic-vact">' + c.more.acts.map(function (a) { return '<a class="ic-vbtn' + (a[2] ? '' : ' ic-vbtn--ghost') + '" href="' + a[1] + '">' + a[0] + '</a>'; }).join('') + '</div></div>' : '';
var box = document.createElement('div'); box.className = 'ic-vdesk' + c.cls;
box.innerHTML = '<div class="ic-vdesk__head"><div class="ic-veyebrow">' + c.eye + '</div><div class="ic-vtitle">' + c.title + '</div><div class="ic-vjob">' + c.job + '</div></div>'
+ '<div class="ic-vdesk__body">' + pasteHtml + '<div class="ic-vact">' + actHtml + '</div><div class="ic-vqueue">' + chipHtml + '</div>'
+ '<div><div class="ic-vsteps__label">How it works</div><div class="ic-vsteps">' + stepHtml + '</div></div>' + moreHtml + '<div class="ic-vguide">New here? <a href="/index.php/ICE_List:Volunteer_guide">Read the full volunteer guide \u2192</a></div></div>';
var content = document.querySelector('.mw-parser-output') || document.getElementById('mw-content-text');
if (content) { content.insertBefore(box, content.firstChild); }
if (role === 'sorter' || role === 'confirm') {
var CAT = role === 'sorter' ? 'Submission sorting' : 'Submission confirm';
var PAT = role === 'sorter' ? /\/link-/ : /\/(agent|vehicle|incident|link)-/;
var prim = box.querySelector('.ic-vbtn:not(.ic-vbtn--ghost)');
if (prim) { prim.addEventListener('click', function (e) {
e.preventDefault(); var lbl = prim.textContent; prim.textContent = 'Finding next…';
mw.loader.using(['mediawiki.api']).then(function () {
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:' + CAT, cmlimit: 50, cmtype: 'page', formatversion: 2 }).then(function (d) {
var m = (d.query.categorymembers || []).filter(function (p) { return PAT.test(p.title); });
if (m.length) { window.location.href = '/index.php/' + encodeURI(m[0].title.replace(/ /g, '_')); }
else { prim.textContent = 'All clear \u2014 nothing to work'; setTimeout(function(){ prim.textContent = lbl; }, 2500); }
}, function () { prim.textContent = lbl; });
});
}); }
}
// fill counts
var cats = c.chips.map(function (ch) { return 'Category:' + ch[0]; });
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', titles: cats.join('|'), prop: 'categoryinfo', formatversion: 2 }).then(function (d) {
var m = {}; (d.query.pages || []).forEach(function (p) { m[p.title] = (p.categoryinfo && p.categoryinfo.pages) || 0; });
var chips = box.querySelectorAll('.ic-vchip');
for (var i = 0; i < chips.length; i++) { var k = 'Category:' + chips[i].getAttribute('data-cat'); chips[i].querySelector('b').textContent = (m[k] != null ? m[k] : 0); }
});
// wire the paste-and-go box
var go = box.querySelector('.ic-vpaste__go');
if (go) {
go.addEventListener('click', function (e) {
e.preventDefault();
var ta = box.querySelector('.ic-vpaste__in'), note = box.querySelector('.ic-vpaste__note'), msg = box.querySelector('.ic-vpaste__msg');
var links = (ta.value || '').split(/\s+/).filter(function (x) { return /^https?:\/\//i.test(x); });
if (!links.length) { msg.textContent = 'Paste at least one link (starting http…).'; return; }
go.style.pointerEvents = 'none'; go.textContent = 'Adding…'; msg.textContent = '';
var ctx = (note.value || '').replace(/[{}|]/g, ''), ty = ((box.querySelector('.ic-vpaste__type') || {}).value || ''), done = 0, fail = 0;
function next(i) {
if (i >= links.length) {
go.style.pointerEvents = ''; go.textContent = 'Add link(s)'; ta.value = ''; note.value = '';
msg.textContent = 'Added ' + done + (fail ? (', ' + fail + ' failed') : '') + '. Thank you!';
var chip = box.querySelector('.ic-vchip b'); if (chip) { chip.textContent = (parseInt(chip.textContent, 10) || 0) + done; }
return;
}
var id = 'link-' + Date.now().toString(36) + '-' + Math.floor(Math.random() * 10000);
var text = '{' + '{Submission link|url=' + links[i].replace(/[{}|]/g, '') + '|note=' + ctx + '|source=' + user + (ty ? '|type=' + ty : '') + '|status=new}}';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/' + id, text: text, createonly: 1, summary: 'Link submitted via desk' })
.then(function () { done++; }, function () { fail++; }).always(function () { next(i + 1); });
}
next(0);
});
}
});
});
/* Suppress the recurring ULS "Language changed" popup */
ready(function () {
function kill() {
var ns = document.querySelectorAll('.mw-notification');
for (var i = 0; i < ns.length; i++) { if (/Language changed/i.test(ns[i].textContent || '')) { ns[i].style.display = 'none'; } }
}
kill();
if (window.MutationObserver) {
var area = document.querySelector('.mw-notification-area') || document.body;
new MutationObserver(kill).observe(area, { childList: true, subtree: true });
}
});
/* Volunteer roles: pick one (captured into the request) */
ready(function () {
if (mw.config.get('wgCanonicalSpecialPageName') !== 'RequestAccount') { return; }
var content = document.querySelector('#mw-content-text .mw-body-content') || document.getElementById('mw-content-text');
if (!content || document.querySelector('.ic-roles')) { return; }
var ROLES = [
['Submitter', '', 'Spot links about ICE on social media and drop them in. The easiest way to help.'],
['Sorter', 'ic-role--b', 'Turn submitted links into structured records \u2014 agent photos, incidents with locations, license plates.'],
['Confirm', 'ic-role--g', 'Quality-check finished records before they reach the admins.'],
['General help', 'ic-role--n', 'Not sure where you fit? Pick this and we will place you.']
];
var box = document.createElement('div'); box.className = 'ic-roles';
var html = '<div class="ic-veyebrow">Choose your volunteer role</div>'
+ '<p>Approved volunteers work only in private review queues \u2014 <b>nothing you submit is public until an administrator publishes it.</b> <b>Tap the role you would like</b> and we will note it on your request:</p>'
+ '<div class="ic-roles__grid">';
ROLES.forEach(function (r) { html += '<div class="ic-role ' + r[1] + '" role="button" tabindex="0" data-role="' + r[0] + '"><b>' + r[0] + '</b><span>' + r[2] + '</span></div>'; });
html += '</div>';
box.innerHTML = html;
var form = content.querySelector('form:not(#searchform)');
if (form) { form.parentNode.insertBefore(box, form); } else { content.insertBefore(box, content.firstChild); }
var bio = document.getElementById('wpBio');
function pick(card) {
var cards = box.querySelectorAll('.ic-role');
for (var j = 0; j < cards.length; j++) { cards[j].classList.remove('is-selected'); }
card.classList.add('is-selected');
if (bio) {
var line = 'Preferred volunteer role: ' + card.getAttribute('data-role') + '.';
var rest = bio.value.replace(/^Preferred volunteer role:[^\n]*\n?\n?/, '');
bio.value = line + '\n\n' + rest;
}
}
var cards = box.querySelectorAll('.ic-role');
for (var j = 0; j < cards.length; j++) {
cards[j].addEventListener('click', function () { pick(this); });
cards[j].addEventListener('keydown', function (e) { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); pick(this); } });
}
});
/* Submission pipeline: advance items with a Mark-done button (removes from the list) */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var STAGE = {
'ICE List:Submission/Sorting': { to: 'sorted', label: 'Mark sorted ✓' },
'ICE List:Submission/Confirm': { to: 'confirmed', label: 'Confirm ✓' }
};
var st = STAGE[pn];
if (!st) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
var items = document.querySelectorAll('.ic-sub-item[data-page]');
for (var i = 0; i < items.length; i++) {
(function (it) {
var page = it.getAttribute('data-page'), url = it.getAttribute('data-url');
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; } var sl = document.createElement('a'); sl.href = '/index.php/' + encodeURI(page.replace(/ /g, '_')); sl.className = 'ic-sub-sort'; sl.textContent = 'Sort this \u2192'; act.appendChild(sl);
if (url) { var op = document.createElement('a'); op.href = url; op.target = '_blank'; op.rel = 'noopener noreferrer'; op.className = 'ic-sub-open'; op.textContent = 'Open link ↗'; act.appendChild(op); }
var btn = document.createElement('a'); btn.href = '#'; btn.className = 'ic-sub-done'; btn.textContent = st.label;
btn.addEventListener('click', function (e) {
e.preventDefault(); btn.textContent = 'Saving…';
api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) {
var c = d.query.pages[0].revisions[0].slots.main.content;
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1' + st.to) : c.replace(/\}\}/, '|status=' + st.to + '}}');
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc, summary: 'Pipeline: advance to ' + st.to });
}).then(function () { it.style.transition = 'opacity .3s'; it.style.opacity = '0'; setTimeout(function () { it.style.display = 'none'; }, 300); },
function (err) { btn.textContent = 'Failed — retry'; });
});
act.appendChild(btn);
})(items[i]);
}
});
});
/* Inbox: turn every item into an inviting, whole-card-clickable "Work on this" affordance */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'ICE List:Submission/Inbox') { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; }
var items = document.querySelectorAll('.ic-sub-item[data-page]');
for (var i = 0; i < items.length; i++) {
(function (it) {
var page = it.getAttribute('data-page'), url = it.getAttribute('data-url');
var act = it.querySelector('.ic-sub-actions'); if (!act || act.querySelector('.ic-sub-work')) { return; }
var w = document.createElement('a'); w.href = '/index.php/' + encodeURI((page || '').replace(/ /g, '_')); w.className = 'ic-sub-work'; w.textContent = 'Work on this →'; act.appendChild(w);
if (url) { var op = document.createElement('a'); op.href = url; op.target = '_blank'; op.rel = 'noopener noreferrer'; op.className = 'ic-sub-open'; op.textContent = 'Peek at the link ↗'; act.appendChild(op); }
it.classList.add('ic-sub-item--live');
it.addEventListener('click', function (e) { if (e.target.closest('a') || (window.getSelection && window.getSelection().toString())) { return; } window.location.href = w.href; });
})(items[i]);
}
});
/* Sorter enrichment: open a link, pull agent/vehicle/incident records (with screenshots) into the review list */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (!/^ICE List:Submission\/(Inbox|Sorting)\/.+/.test(pn)) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; }
var justSubmitter = g.indexOf('submitter') >= 0 && g.indexOf('sorter') < 0 && g.indexOf('confirm') < 0 && g.indexOf('sysop') < 0 && !/^ICEListAdmin/.test(u);
var content = document.querySelector('.mw-parser-output'); if (!content) { return; }
var urlEl = content.querySelector('.ic-sub-url a'); var srcUrl = urlEl ? urlEl.href : '';
var panel = document.createElement('div'); panel.className = 'ic-sortwork';
panel.innerHTML = '<div class="ic-veyebrow">' + (justSubmitter ? 'Want to do more? Sort this link' : 'Sort this link') + '</div>'
+ (justSubmitter ? '<p class="ic-aa-alt">Optional — only if you fancy it. Pulling the facts out of a link is the next job up.</p>' : '')
+ '<p>Pull structured records out of this link. They go to <b>Confirm</b> for review — they do <b>not</b> publish live.</p>'+ '<ul class="ic-sw-guide"><li><b>Agent</b> \u2014 grab a clear face / name screenshot from the footage.</li><li><b>Vehicle</b> \u2014 screenshot the plate, then note make, model and colour.</li><li><b>Incident</b> \u2014 note the date, location and what happened.</li><li><b>Note</b> \u2014 anything else useful.</li></ul>'
+ (srcUrl ? '<p><a class="ic-vbtn" href="' + srcUrl + '" target="_blank" rel="noopener noreferrer">Open the link ↗</a></p>' : '')
+ '<div class="ic-sw__btns"><a href="#" class="ic-vbtn ic-sw-add" data-t="agent">+ Agent</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="vehicle">+ Vehicle</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="incident">+ Incident</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="note">+ Note</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-add" data-t="lead">+ Lead</a></div>'
+ '<div class="ic-sw-form"></div><div class="ic-sw-added"></div>'
+ '<div class="ic-sw-finish"><a href="#" class="ic-vbtn ic-sw-finish-btn">Done with this link — send to Confirm ✓</a> <span class="ic-sw-finmsg"></span></div>';
content.insertBefore(panel, content.firstChild);
var FIELDS = {
agent: [['name', 'Name / alias'], ['agency', 'Agency'], ['role', 'Role / rank'], ['state', 'State'], ['notes', 'Notes', 1]],
vehicle: [['plate', 'License plate'], ['vtype', 'Make / model / type'], ['agency', 'Agency'], ['state', 'State'], ['notes', 'Notes', 1]],
incident: [['date', 'Date (YYYY-MM-DD)'], ['location', 'Location'], ['state', 'State'], ['notes', 'What happened', 1]],
note: [['notes', 'Note / context', 1]],
lead: [['subject', 'Who or what is this about?'], ['finding', 'What did you find? Your lead or idea', 1], ['sources', 'Sources & evidence links (one per line)', 1], ['needs', 'What still needs checking?', 1]]
};
var formBox = panel.querySelector('.ic-sw-form'), added = panel.querySelector('.ic-sw-added');
function openForm(t) {
var f = FIELDS[t], html = '<div class="ic-sw-card"><div class="ic-sw-card__h">New ' + t + '</div>';
f.forEach(function (fl) { html += '<label class="ic-sw-fld"><span>' + fl[1] + '</span>' + (fl[2] ? '<textarea rows="2" data-k="' + fl[0] + '"></textarea>' : '<input data-k="' + fl[0] + '">') + '</label>'; });
if (t !== 'note' && t !== 'lead') { html += '<label class="ic-sw-fld"><span>Screenshot ' + (t === 'incident' ? '(optional)' : '(from footage)') + '</span><input type="file" accept="image/*" class="ic-sw-file"></label>'; }
html += '<div class="ic-vact"><a href="#" class="ic-vbtn ic-sw-save" data-t="' + t + '">Add to review list</a> <a href="#" class="ic-vbtn ic-vbtn--ghost ic-sw-cancel">Cancel</a> <span class="ic-sw-msg"></span></div></div>';
formBox.innerHTML = html;
}
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); }
panel.addEventListener('click', function (e) {
var fin = e.target.closest('.ic-sw-finish-btn');
if (fin) {
e.preventDefault();
var fm = panel.querySelector('.ic-sw-finmsg'); fm.textContent = 'Saving…'; fin.style.pointerEvents = 'none';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', titles: pn, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) {
var c = d.query.pages[0].revisions[0].slots.main.content;
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1sorted') : c.replace(/\}\}/, '|status=sorted}}');
return api.postWithToken('csrf', { action: 'edit', title: pn, text: nc, summary: 'Pipeline: link sorted, sent to Confirm' });
}).then(function () {
fm.textContent = ''; fin.textContent = '✓ Sent to Confirm';
panel.insertAdjacentHTML('beforeend', '<p class="ic-sw-done">This link is done and out of the inbox. <a href="#" class="ic-sw-next">Sort the next link →</a></p>');
}, function () { fm.textContent = 'Failed — retry'; fin.style.pointerEvents = ''; });
});
return;
}
var nx = e.target.closest('.ic-sw-next');
if (nx) {
e.preventDefault(); nx.textContent = 'Finding…';
mw.loader.using(['mediawiki.api']).then(function () {
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: 'Category:Submission inbox', cmlimit: '50', cmtype: 'page' }).then(function (d) {
var m = (d.query.categorymembers || []).map(function (x) { return x.title; }).filter(function (t) { return /\/link-/.test(t) && t !== pn; });
if (m.length) { location.href = '/index.php/' + encodeURI(m[0].replace(/ /g, '_')); } else { nx.textContent = 'Inbox empty — nice work'; }
});
});
return;
}
var a = e.target.closest('.ic-sw-add'); if (a) { e.preventDefault(); openForm(a.getAttribute('data-t')); return; }
if (e.target.closest('.ic-sw-cancel')) { e.preventDefault(); formBox.innerHTML = ''; return; }
var s = e.target.closest('.ic-sw-save'); if (!s) { return; }
e.preventDefault();
var t = s.getAttribute('data-t'), card = s.closest('.ic-sw-card'), msg = card.querySelector('.ic-sw-msg');
var vals = {}; card.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; });
var fileEl = card.querySelector('.ic-sw-file'), file = fileEl && fileEl.files[0];
s.style.pointerEvents = 'none'; msg.textContent = 'Saving…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(), ts = Date.now().toString(36) + Math.floor(Math.random() * 1000);
function createRecord(img) {
var parts = ['{' + '{Sorting record|type=' + t.charAt(0).toUpperCase() + t.slice(1)];
Object.keys(vals).forEach(function (k) { if (vals[k]) { parts.push('|' + k + '=' + esc(vals[k])); } });
if (img) { parts.push('|image=' + img); }
parts.push('|source=' + srcUrl + '|status=pending}}');
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/' + t + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Sorter added ' + t })
.then(function () { added.insertAdjacentHTML('afterbegin', '<div class="ic-sw-done">✓ ' + t + ' added to the review list.</div>'); formBox.innerHTML = ''; });
}
if (file) {
var ext = (file.name.split('.').pop() || 'png').toLowerCase();
api.upload(file, { filename: 'Sub-' + t + '-' + ts + '.' + ext, comment: 'Sorter screenshot', ignorewarnings: true })
.then(function () { createRecord('Sub-' + t + '-' + ts + '.' + ext); }, function (err) { msg.textContent = 'Upload failed: ' + err; s.style.pointerEvents = ''; });
} else { createRecord('').then(null, function (err) { msg.textContent = 'Failed: ' + err; s.style.pointerEvents = ''; }); }
});
});
});
/* Ask an admin: in-wiki question box → gated help-desk queue */
ready(function () {
var host = document.getElementById('ic-askadmin'); if (!host) { return; }
var u = mw.config.get('wgUserName');
if (!u) {
host.innerHTML = '<div class="ic-aa"><p><b>Sign in to ask.</b> The ask box sends your question straight to the admins — it needs an account so they can reply to you.</p>'
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Ask_an_admin">Log in</a> '
+ '<a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Ask_an_admin">Create an account</a></p>'
+ '<p class="ic-aa-alt">Can’t create an account at all? Message <a href="https://bsky.app/profile/crustnews.com" target="_blank" rel="noopener noreferrer">@crustnews.com on Bluesky</a>.</p></div>';
return;
}
host.innerHTML = '<div class="ic-aa"><label class="ic-sw-fld"><span>Your question</span>'
+ '<textarea rows="4" id="ic-aa-q" placeholder="What are you stuck on? Plain words are fine."></textarea></label>'
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-aa-send">Send to the admins</a> <span id="ic-aa-msg"></span></div>'
+ '<p class="ic-aa-alt">An admin will reply on <b>your talk page</b> — you’ll get a notification.</p></div>';
document.getElementById('ic-aa-send').addEventListener('click', function (e) {
e.preventDefault();
var q = document.getElementById('ic-aa-q').value.trim();
var msg = document.getElementById('ic-aa-msg');
if (!q) { msg.textContent = 'Type a question first.'; return; }
this.style.pointerEvents = 'none'; msg.textContent = 'Sending…';
var btn = this;
mw.loader.using(['mediawiki.api']).then(function () {
new mw.Api().postWithToken('csrf', {
action: 'edit', title: 'ICE List:Submission/Inbox/Help desk', section: 'new',
sectiontitle: 'Question from ' + u,
text: q.replace(/~{3,}/g, '') + '\n\ncc [[User:ICEListAdmin6|ICEListAdmin6]] — [[User:AdminBot|AdminBot]] ([[User talk:AdminBot|talk]]) 14:21, 27 July 2026 (UTC)',
summary: 'Volunteer question'
}).then(function () {
msg.textContent = '';
btn.textContent = '✓ Sent — an admin will reply on your talk page';
}, function (err) {
msg.textContent = 'Failed (' + err + ') — try again';
btn.style.pointerEvents = '';
});
});
});
});
/* Community check (vetting): pod members confirm new links before sorters see them */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var isItem = /^ICE List:Submission\/Inbox\/link-/.test(pn);
var isQueue = pn === 'ICE List:Submission/Vetting';
if (!isItem && !isQueue) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
var pod = null; for (var pi = 1; pi <= 4; pi++) { if (g.indexOf('pod' + pi) >= 0) { pod = pi; break; } }
var isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || '');
if (isQueue) {
var qits = document.querySelectorAll('.ic-sub-item[data-page]');
for (var qj = 0; qj < qits.length; qj++) (function (it) {
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; }
var a = document.createElement('a'); a.href = '/index.php/' + encodeURI(it.getAttribute('data-page').replace(/ /g, '_'));
a.className = 'ic-sub-sort'; a.textContent = 'Check this link →'; act.appendChild(a);
})(qits[qj]);
return;
}
var it = document.querySelector('.ic-sub-item[data-page]'); if (!it) { return; }
if (it.getAttribute('data-status') !== 'new' || it.getAttribute('data-arch') === '1') { return; }
var VET_GOAL = 2; // TEMP for promo surge: lowered from 4 so new contributions flow while the crowd grows (matches Template:Submission link >=2)
var vets = parseInt(it.getAttribute('data-vets') || '0', 10);
if (vets >= VET_GOAL) { return; }
var box = document.createElement('div'); box.className = 'ic-vetbox';
box.innerHTML = '<div class="ic-veyebrow">Community check</div>'
+ '<p><b>' + vets + ' of ' + VET_GOAL + '</b> independent checks done. New links reach the sorters once ' + VET_GOAL + ' different volunteers confirm they’re real and in scope.</p>'
+ '<div class="ic-vact"></div>';
var actv = box.querySelector('.ic-vact');
function vetBtn(label, fill) {
var b = document.createElement('a'); b.href = '#'; b.className = 'ic-vbtn'; b.textContent = label;
b.addEventListener('click', function (e) {
e.preventDefault(); b.style.pointerEvents = 'none'; b.textContent = 'Saving…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', titles: pn, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) {
var c = d.query.pages[0].revisions[0].slots.main.content;
var src = /\|\s*source\s*=\s*([^|}\n]*)/.exec(c);
if (!fill && src && src[1].trim() === u) { b.textContent = 'You can’t check your own link'; return null; }
if (!fill && new RegExp('\\|\\s*v' + pod + '\\s*=\\s*[^|}\\n]*\\S').test(c)) { b.textContent = 'Your group already checked this one'; return null; }
var slots = fill ? [1, 2, 3, 4] : [pod];
slots.forEach(function (n) {
if (fill && new RegExp('\\|\\s*v' + n + '\\s*=\\s*[^|}\\n]*\\S').test(c)) { return; }
var val = fill ? 'admin:' + u : u;
var re = new RegExp('(\\|\\s*v' + n + '\\s*=\\s*)[^|}\\n]*');
if (re.test(c)) { c = c.replace(re, '$1' + val); }
else { var k = c.lastIndexOf('}}'); c = c.slice(0, k) + '|v' + n + '=' + val + c.slice(k); }
});
return api.postWithToken('csrf', { action: 'edit', title: pn, text: c, summary: fill ? 'Admin: mark fully checked' : 'Community check' });
}).then(function (r) { if (r) { location.reload(); } }, function () { b.textContent = 'Failed — retry'; b.style.pointerEvents = ''; });
});
});
actv.appendChild(b);
}
if (pod) { vetBtn('Confirm this link is real & in scope ✓', false); }
if (isAdmin) { vetBtn('(Admin) Mark fully checked', true); }
if (!pod && !isAdmin) { actv.innerHTML = '<span class="ic-aa-alt">Your account isn’t in a checking group yet — an admin adds you.</span>'; }
var vcontent = document.querySelector('.mw-parser-output');
if (vcontent) { vcontent.insertBefore(box, vcontent.firstChild); }
});
/* Admin: recent-signups panel on the admin's own profile */
ready(function () {
var u = mw.config.get('wgUserName'), g = mw.config.get('wgUserGroups') || [];
if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''))) { return; }
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn !== 'User:' + u) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', list: 'logevents', letype: 'newusers', lelimit: 12, formatversion: 2 }).then(function (d) {
var evs = (d.query.logevents || []).filter(function (e) { return e.user; });
if (!evs.length) { return; }
api.get({ action: 'query', list: 'users', ususers: evs.map(function (e) { return e.user; }).join('|'), usprop: 'editcount|groups', formatversion: 2 }).then(function (d2) {
var info = {}; (d2.query.users || []).forEach(function (x) { info[x.name] = x; });
var box = document.createElement('div'); box.className = 'ic-newvols';
var h = '<div class="ic-veyebrow">New volunteers</div><table><tr><th>account</th><th>role</th><th>edits</th><th></th></tr>';
evs.forEach(function (e) {
var x = info[e.user] || {};
var gr = (x.groups || []).filter(function (r) { return ['submitter', 'sorter', 'confirm'].indexOf(r) >= 0; }).join(', ') || '—';
var enc = encodeURIComponent(e.user.replace(/ /g, '_'));
h += '<tr><td><a href="/index.php/User:' + enc + '">' + e.user + '</a> <span class="ic-nv-when">' + (e.timestamp || '').slice(0, 10) + '</span></td><td>' + gr + '</td><td>' + (x.editcount || 0) + '</td>'
+ '<td><a href="/index.php/Special:Contributions/' + enc + '">activity</a> · <a href="/index.php/Special:UserRights/' + enc + '">rights</a> · <a href="/index.php?title=Special:Block/' + enc + '">block</a></td></tr>';
});
h += '</table>';
box.innerHTML = h;
var vd = document.querySelector('.ic-vdesk');
var host = document.querySelector('.mw-parser-output');
if (vd && vd.parentNode) { vd.parentNode.insertBefore(box, vd.nextSibling); }
else if (host) { host.insertBefore(box, host.firstChild); }
});
});
});
});
/* Suggest an update / sighting — crowd-OSINT on agent & 287(g) agency pages; everything routes to approval */
ready(function () {
if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgNamespaceNumber') !== 0) { return; }
var cats = mw.config.get('wgCategories') || [];
var isAgent = cats.indexOf('Agents') >= 0;
var isAgency = cats.some(function (c) { return /287\(g\)/.test(c); });
if (!isAgent && !isAgency) { return; }
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-suggest')) { return; }
function esc(v) { return (v || '').replace(/[{}|\[\]<>]/g, '').trim(); }
var subject = mw.config.get('wgTitle');
var wrap = document.createElement('div'); wrap.className = 'ic-suggest';
wrap.innerHTML = '<button type="button" class="ic-suggest__open">+ Suggest a photo, a name, or a sighting</button>'
+ '<div class="ic-suggest__form" hidden>'
+ '<div class="ic-suggest__kick">Know something about ' + esc(subject) + '? Suggest it — a checker reviews everything before it appears on the page, and you stay anonymous.</div>'
+ '<label class="ic-sg-l"><span>What kind of tip?</span><select class="ic-sg-type">'
+ '<option value="Sighting">I think I saw them / it somewhere</option>'
+ '<option value="Photo">A photo (paste a link to it)</option>'
+ '<option value="Name">Who they are — name, rank, unit</option>'
+ '<option value="Correction">A correction to this page</option>'
+ '<option value="Other">Something else</option></select></label>'
+ '<label class="ic-sg-l ic-sg-whererow"><span>Where & when</span><input class="ic-sg-where" placeholder="City, state — and a date if you have one"></label>'
+ '<label class="ic-sg-l"><span>Details</span><textarea class="ic-sg-details" rows="3" placeholder="What did you see, or what should change?"></textarea></label>'
+ '<label class="ic-sg-l"><span>Link / source <span class="ic-sg-opt">(optional, but it helps a lot)</span></span><input class="ic-sg-src" placeholder="https://… a post, article, or photo"></label>'
+ '<div class="ic-sg-cap" style="display:none"><label class="ic-sg-l"><span class="ic-sg-cap-q"></span><input class="ic-sg-cap-a"></label></div>'
+ '<div class="ic-sg-act"><a href="#" class="ic-vbtn ic-sg-send">Send suggestion</a> <span class="ic-sg-msg"></span></div>'
+ '<div class="ic-sg-priv">Not shown publicly until a checker approves it. First time? <a href="/index.php/ICE_List:Staying_safe">Staying safe →</a></div></div>';
content.insertBefore(wrap, content.firstChild);
var openBtn = wrap.querySelector('.ic-suggest__open'), form = wrap.querySelector('.ic-suggest__form'), capId = null;
openBtn.addEventListener('click', function () { if (form.hasAttribute('hidden')) { form.removeAttribute('hidden'); openBtn.textContent = '× Close'; } else { form.setAttribute('hidden', ''); openBtn.textContent = '+ Suggest a photo, a name, or a sighting'; } });
var typeSel = wrap.querySelector('.ic-sg-type'), whereRow = wrap.querySelector('.ic-sg-whererow');
function syncWhere() { whereRow.style.display = (typeSel.value === 'Sighting') ? '' : 'none'; }
typeSel.addEventListener('change', syncWhere); syncWhere();
wrap.querySelector('.ic-sg-send').addEventListener('click', function (e) {
e.preventDefault();
var msg = wrap.querySelector('.ic-sg-msg'), btn = this;
var type = typeSel.value, where = wrap.querySelector('.ic-sg-where').value, details = wrap.querySelector('.ic-sg-details').value, src = wrap.querySelector('.ic-sg-src').value;
if (!esc(details) && !esc(src)) { msg.textContent = 'Add a detail or a link first.'; return; }
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…';
var user = mw.config.get('wgUserName');
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function done() { form.innerHTML = '<div class="ic-suggest__kick">✓ Sent — thank you. A checker will review your suggestion about ' + esc(subject) + '.</div>'; }
function fail(err) { btn.style.pointerEvents = ''; msg.textContent = 'Could not send: ' + err; }
if (user) {
var finding = type + (esc(where) ? ' — ' + esc(where) : '') + (esc(details) ? ': ' + esc(details) : '');
var rec = '{' + '{Sorting record|type=Lead|subject=' + esc(subject) + '|finding=' + finding + '|sources=' + esc(src) + '|needs=Verify, then update [' + '[' + esc(subject) + ']]|source=suggestion|status=pending}}';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: rec, createonly: 1, summary: 'Suggestion about ' + esc(subject) }).then(done, fail);
} else {
var note = 'Suggestion (' + esc(type) + ') about [' + '[' + esc(subject) + ']]' + (esc(where) ? ' — ' + esc(where) : '') + (esc(details) ? ': ' + esc(details) : '');
var body = '{' + '{Submission link|url=' + esc(src) + '|note=' + note + '|source=suggestion|status=new}}';
var params = { action: 'edit', title: 'ICE List:Submission/Inbox/link-suggest-anon-' + ts, text: body, createonly: 1, summary: 'Anonymous suggestion', formatversion: 2 };
var capA = wrap.querySelector('.ic-sg-cap-a');
if (capId && capA && capA.value) { params.captchaid = capId; params.captchaword = capA.value; }
function nudge() { form.innerHTML = '<div class="ic-suggest__kick">Almost — anonymous suggestions need a free, 20-second account so we can weed out bad actors. <a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=' + encodeURIComponent(mw.config.get('wgPageName')) + '">Create an account →</a> then reopen this. Your tip was not lost.</div>'; }
api.postWithToken('csrf', params).then(function (r) {
var ed = r && r.edit;
if (ed && ed.result === 'Success') { done(); return; }
if (ed && ed.captcha) { capId = ed.captcha.id; wrap.querySelector('.ic-sg-cap').style.display = ''; wrap.querySelector('.ic-sg-cap-q').textContent = 'One quick check to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then tap Send suggestion again.'; btn.style.pointerEvents = ''; return; }
nudge();
}, function () { nudge(); });
}
});
});
});
/* Volunteer task board: claim / mark done, admin add-task */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var onBoard = (pn === 'ICE List:Submission/Tasks');
var onTask = /^ICE List:Submission\/Tasks\/.+/.test(pn);
if (!onBoard && !onTask) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
if (!u) { return; }
var isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u);
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); }
function setFields(page, fields) {
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) {
var c = dd.query.pages[0].revisions[0].slots.main.content;
Object.keys(fields).forEach(function (k) {
var re = new RegExp('(\\|\\s*' + k + '\\s*=)[^|}\\n]*');
if (re.test(c)) { c = c.replace(re, '$1' + fields[k]); } else { c = c.replace(/\}\}/, '|' + k + '=' + fields[k] + '}}'); }
});
return api.postWithToken('csrf', { action: 'edit', title: page, text: c, summary: 'Task update' });
});
}
var addHost = document.getElementById('ic-taskadd');
if (onBoard && isAdmin && addHost) {
addHost.className = 'ic-taskadd';
addHost.innerHTML = '<div class="ic-veyebrow">Add a task</div>'
+ '<label class="ic-sw-fld"><span>Task — what needs doing?</span><input id="ic-tk-title" placeholder="e.g. Add sources to this incident"></label>'
+ '<label class="ic-sw-fld"><span>About which page? (optional — paste an exact incident/agent/facility title)</span><input id="ic-tk-target" placeholder="Exact page title"></label>'
+ '<label class="ic-sw-fld"><span>Type</span><select id="ic-tk-type"><option value="sources">🔗 Sources</option><option value="photo">📷 Photo</option><option value="verify">✅ Verify</option><option value="research">🔎 Research</option><option value="translate">🌐 Translate</option><option value="cleanup">🧹 Cleanup</option><option value="task">📋 Other</option></select></label>'
+ '<label class="ic-sw-fld"><span>Details (optional)</span><textarea rows="2" id="ic-tk-detail"></textarea></label>'
+ '<label style="display:inline-flex;gap:.4em;align-items:center;font-size:.9em;margin:.2em 0 .5em"><input type="checkbox" id="ic-tk-pri"> Mark as priority</label>'
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-tk-add">Add task</a> <span id="ic-tk-msg"></span></div>';
document.getElementById('ic-tk-add').addEventListener('click', function (e) {
e.preventDefault(); var btn = this, msg = document.getElementById('ic-tk-msg');
var title = esc(document.getElementById('ic-tk-title').value);
if (!title) { msg.textContent = 'Give the task a title.'; return; }
var target = esc(document.getElementById('ic-tk-target').value), type = document.getElementById('ic-tk-type').value, detail = esc(document.getElementById('ic-tk-detail').value), pri = document.getElementById('ic-tk-pri').checked ? 'high' : '';
btn.style.pointerEvents = 'none'; msg.textContent = 'Adding…';
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var body = '{' + '{Task|title=' + title + '|target=' + target + '|type=' + type + '|priority=' + pri + '|detail=' + detail + '|by=' + u + '|status=open}}';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Tasks/' + ts, text: body, createonly: 1, summary: 'New task' }).then(function () { msg.textContent = '✓ Added.'; setTimeout(function () { location.reload(); }, 600); }, function (err) { msg.textContent = 'Failed: ' + err; btn.style.pointerEvents = ''; });
});
}
document.querySelectorAll('.ic-task[data-page]').forEach(function (t) {
var page = t.getAttribute('data-page'), st = t.getAttribute('data-status'), cb = t.getAttribute('data-claimedby');
var bar = t.querySelector('.ic-task__actions'); if (!bar) { return; }
function mk(label, cls, fn) { var a = document.createElement('a'); a.href = '#'; a.className = 'ic-vbtn ' + (cls || ''); a.textContent = label; a.addEventListener('click', function (e) { e.preventDefault(); fn(a); }); bar.appendChild(a); }
function act(fields, label) { return function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setFields(page, fields).then(function () { a.textContent = label; setTimeout(function () { location.reload(); }, 500); }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); }; }
if (st === 'open') { mk('Claim this →', '', act({ status: 'claimed', claimedby: u }, '✓ claimed')); }
else if (st === 'claimed') { if (cb === u || isAdmin) { mk('Mark done ✓', '', act({ status: 'done' }, '✓ done')); } mk('Unclaim', 'ic-vbtn--ghost', act({ status: 'open', claimedby: '' }, 'released')); if (cb && cb !== u && !isAdmin) { var s = document.createElement('span'); s.className = 'ic-task__self'; s.textContent = 'claimed by ' + cb; bar.appendChild(s); } }
else if (st === 'done' && isAdmin) { mk('Reopen', 'ic-vbtn--ghost', act({ status: 'open', claimedby: '' }, 'reopened')); }
if (isAdmin) { mk('Delete', 'ic-vbtn--ghost', function (a) { if (!confirm('Delete this task?')) { return; } a.textContent = '…'; api.postWithToken('csrf', { action: 'delete', title: page, reason: 'Task removed' }).then(function () { t.style.display = 'none'; }, function () { a.textContent = 'retry'; }); }); }
});
});
});
/* Admin: request volunteer work on this page → creates a targeted task */
ready(function () {
if (mw.config.get('wgAction') !== 'view' || mw.config.get('wgNamespaceNumber') !== 0) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || ''))) { return; }
var cats = mw.config.get('wgCategories') || [];
if (!(cats.indexOf('Incidents') >= 0 || cats.indexOf('Agents') >= 0 || cats.indexOf('Vehicles') >= 0 || cats.some(function (c) { return /287\(g\)/.test(c) || /Facilit/i.test(c) || /Concentration camp/i.test(c); }))) { return; }
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-reqwork')) { return; }
var target = mw.config.get('wgPageName').replace(/_/g, ' ');
var box = document.createElement('div'); box.className = 'ic-reqwork';
box.innerHTML = '<button type="button" class="ic-reqwork__open">➕ Request volunteer work on this page</button>'
+ '<div class="ic-reqwork__form" hidden><div class="ic-veyebrow">New task · on this page</div>'
+ '<label class="ic-sw-fld"><span>What needs doing here?</span><input class="ic-rw-title" placeholder="e.g. Find sources / add a photo / verify the date"></label>'
+ '<label class="ic-sw-fld"><span>Type</span><select class="ic-rw-type"><option value="sources">🔗 Sources</option><option value="photo">📷 Photo</option><option value="verify">✅ Verify</option><option value="research">🔎 Research</option><option value="cleanup">🧹 Cleanup</option><option value="task">📋 Other</option></select></label>'
+ '<label class="ic-sw-fld"><span>Details (optional)</span><textarea rows="2" class="ic-rw-detail"></textarea></label>'
+ '<label style="display:inline-flex;gap:.4em;align-items:center;font-size:.9em;margin:.2em 0 .5em"><input type="checkbox" class="ic-rw-pri"> Priority</label>'
+ '<div class="ic-vact"><a href="#" class="ic-vbtn ic-rw-add">Post to task board</a> <span class="ic-rw-msg"></span></div></div>';
content.insertBefore(box, content.firstChild);
var ob = box.querySelector('.ic-reqwork__open'), fm = box.querySelector('.ic-reqwork__form');
ob.addEventListener('click', function () { if (fm.hasAttribute('hidden')) { fm.removeAttribute('hidden'); ob.textContent = '× Close'; } else { fm.setAttribute('hidden', ''); ob.textContent = '➕ Request volunteer work on this page'; } });
box.querySelector('.ic-rw-add').addEventListener('click', function (e) {
e.preventDefault(); var btn = this, msg = box.querySelector('.ic-rw-msg');
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); }
var title = esc(box.querySelector('.ic-rw-title').value);
if (!title) { msg.textContent = 'Say what needs doing.'; return; }
var type = box.querySelector('.ic-rw-type').value, detail = esc(box.querySelector('.ic-rw-detail').value), pri = box.querySelector('.ic-rw-pri').checked ? 'high' : '';
btn.style.pointerEvents = 'none'; msg.textContent = 'Posting…';
mw.loader.using(['mediawiki.api']).then(function () {
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var body = '{' + '{Task|title=' + title + '|target=' + target + '|type=' + type + '|priority=' + pri + '|detail=' + detail + '|by=' + u + '|status=open}}';
new mw.Api().postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Tasks/' + ts, text: body, createonly: 1, summary: 'Task requested from ' + target }).then(function () { fm.innerHTML = '<div class="ic-veyebrow">✓ Posted to the <a href="/index.php/ICE_List:Submission/Tasks">task board</a>.</div>'; }, function (err) { msg.textContent = 'Failed: ' + err; btn.style.pointerEvents = ''; });
});
});
});
/* Personal checklist: my tasks (in progress / done) on my own profile */
ready(function () {
var u = mw.config.get('wgUserName'), pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (!u || pn !== 'User:' + u) { return; }
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-mytasks')) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', list: 'usercontribs', ucuser: u, uclimit: 500, ucprop: 'title', formatversion: 2 }).then(function (d) {
var seen = {}, pages = [];
(d.query.usercontribs || []).forEach(function (x) { if (/^ICE List:Submission\/Tasks\/.+/.test(x.title) && !seen[x.title]) { seen[x.title] = 1; pages.push(x.title); } });
if (!pages.length) { return; }
api.get({ action: 'query', titles: pages.slice(0, 50).join('|'), prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) {
var prog = [], done = [];
(dd.query.pages || []).forEach(function (p) {
if (!p.revisions) { return; }
var c = p.revisions[0].slots.main.content;
function f(k) { var m = new RegExp('\\|\\s*' + k + '\\s*=([^|}\\n]*)').exec(c); return m ? m[1].trim() : ''; }
var st = f('status').toLowerCase(), title = f('title') || p.title, cb = f('claimedby');
if (st === 'claimed' && cb === u) { prog.push({ p: p.title, t: title, tg: f('target') }); }
else if (st === 'done') { done.push({ p: p.title, t: title, tg: f('target') }); }
});
if (!prog.length && !done.length) { return; }
function row(x, checked) { return '<div class="ic-mt-row"><span class="ic-mt-box' + (checked ? ' ic-mt-box--on' : '') + '">' + (checked ? '✓' : '') + '</span><a href="/index.php/' + encodeURI(x.p.replace(/ /g, '_')) + '">' + mw.html.escape(x.t) + '</a>' + (x.tg ? ' <span class="ic-mt-tg">— ' + mw.html.escape(x.tg) + '</span>' : '') + '</div>'; }
var box = document.createElement('div'); box.className = 'ic-mytasks';
box.innerHTML = '<div class="ic-veyebrow">My tasks</div>'
+ (prog.length ? '<div class="ic-mt-h">In progress (' + prog.length + ')</div>' + prog.map(function (x) { return row(x, false); }).join('') : '')
+ (done.length ? '<div class="ic-mt-h">Done (' + done.length + ')</div>' + done.map(function (x) { return row(x, true); }).join('') : '')
+ '<div class="ic-vact"><a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Tasks">Find more tasks →</a></div>';
var vd = document.querySelector('.ic-vdesk'); if (vd && vd.parentNode) { vd.parentNode.insertBefore(box, vd.nextSibling); } else { content.insertBefore(box, content.firstChild); }
});
});
});
});
/* Footage-tips swipe deck (ICE List:Footage deck) — drag + confirmed-only */
ready(function () {
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'ICE List:Footage deck') { return; }
var mount = document.getElementById('ic-deck'); if (!mount) { return; }
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, '').trim(); }
function shuffle(a) { for (var i = a.length - 1; i > 0; i--) { var j = Math.floor(Math.random() * (i + 1)); var t = a[i]; a[i] = a[j]; a[j] = t; } return a; }
function fileUrl(f) { return '/index.php?title=Special:FilePath/' + encodeURIComponent(f); }
var allRows = [].slice.call(document.querySelectorAll('.ic-di')).map(function (d) {
return { inc: d.getAttribute('data-incident'), state: icNormState(d.getAttribute('data-state')), cardimg: d.getAttribute('data-cardimg'), city: d.getAttribute('data-city'), date: d.getAttribute('data-date'), agency: d.getAttribute('data-agency') };
}).filter(function (r) { return r.inc; });
mount.innerHTML = '<div class="ic-fd-empty">Loading the deck…</div>';
mw.loader.using(['mediawiki.api']).then(function () {
new mw.Api().get({ action: 'query', titles: 'ICE List:Footage deck/hidden', prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) {
var pg = dd.query.pages[0]; var txt = (pg.revisions && pg.revisions[0].slots.main.content) || '';
var conf = icConf(txt);
start(allRows.filter(function (r) { return !conf[r.inc]; }));
}, function () { start([]); });
});
function start(rows) {
if (!rows.length) { mount.innerHTML = '<div class="ic-fd-empty">No incidents to show right now.</div>'; return; }
var byState = {}; rows.forEach(function (r) { (byState[r.state] = byState[r.state] || []).push(r); });
var states = Object.keys(byState).filter(Boolean).sort();
var cur = [], idx = 0, curState = '';
mount.innerHTML = '<div class="ic-fd"><div class="ic-fd-bar"><label class="ic-fd-pick">State <select class="ic-fd-state"></select></label><span class="ic-fd-count"></span></div><div class="ic-fd-stage"></div></div>';
var sel = mount.querySelector('.ic-fd-state'), stage = mount.querySelector('.ic-fd-stage'), cnt = mount.querySelector('.ic-fd-count');
sel.innerHTML = '<option value="">Pick a state…</option>' + states.map(function (s) { return '<option value="' + s + '">' + mw.html.escape(s) + ' (' + byState[s].length + ')</option>'; }).join('');
sel.addEventListener('change', function () { curState = this.value; cur = curState ? shuffle(byState[curState].slice()) : []; idx = 0; render(); });
function render() {
if (!curState) { stage.innerHTML = '<div class="ic-fd-empty">Pick a state to start swiping.</div>'; cnt.textContent = ''; return; }
if (idx >= cur.length) { stage.innerHTML = '<div class="ic-fd-empty">That is every incident in ' + mw.html.escape(curState) + '. Thank you.<br><a href="#" class="ic-fd-again">Go again →</a></div>'; cnt.textContent = ''; stage.querySelector('.ic-fd-again').onclick = function (e) { e.preventDefault(); cur = shuffle(byState[curState].slice()); idx = 0; render(); }; return; }
var r = cur[idx]; cnt.textContent = (idx + 1) + ' / ' + cur.length;
var face = r.cardimg ? '<img class="ic-fd-img" src="' + fileUrl(r.cardimg) + '" alt="">' : '<div class="ic-fd-ph"><span>' + mw.html.escape(r.state) + '</span></div>';
var chip = [r.city, r.date, r.agency].filter(Boolean).map(mw.html.escape).join(' · ');
var url = '/index.php/' + encodeURI(r.inc.replace(/ /g, '_'));
stage.innerHTML = '<div class="ic-fd-card"><span class="ic-fd-tag ic-fd-tag--no">Pass</span><span class="ic-fd-tag ic-fd-tag--yes">Tip</span>'
+ '<a class="ic-fd-face" href="' + url + '" target="_blank" rel="noopener">' + face + '<span class="ic-fd-watch">▶ Watch on the record</span></a>'
+ '<div class="ic-fd-body"><div class="ic-fd-title">' + mw.html.escape(r.inc) + '</div>' + (chip ? '<div class="ic-fd-chip">' + chip + '</div>' : '') + '</div>'
+ '<div class="ic-fd-acts"><button class="ic-fd-pass" type="button">Pass</button><button class="ic-fd-tip" type="button">I have a tip →</button></div>'
+ '<div class="ic-fd-tipsheet" hidden></div></div>';
var card = stage.querySelector('.ic-fd-card');
function fly(dir){ card.style.transition = 'transform .3s ease, opacity .3s'; card.style.transform = 'translateX(' + (dir*520) + 'px) rotate(' + (dir*18) + 'deg)'; card.style.opacity = '0'; }
stage.querySelector('.ic-fd-pass').onclick = function () { fly(-1); setTimeout(function(){ idx++; render(); }, 220); };
stage.querySelector('.ic-fd-tip').onclick = function () { openTip(r, card); };
var sx = 0, dxx = 0, dragging = false, moved = false;
card.addEventListener('pointerdown', function (e) { if (e.target.closest('.ic-fd-acts,.ic-fd-tipsheet')) { return; } dragging = true; moved = false; sx = e.clientX; card.style.transition = 'none'; try { card.setPointerCapture(e.pointerId); } catch (_) {} });
card.addEventListener('pointermove', function (e) { if (!dragging) { return; } dxx = e.clientX - sx; if (Math.abs(dxx) > 6) { moved = true; } card.style.transform = 'translateX(' + dxx + 'px) rotate(' + (dxx / 22) + 'deg)'; card.classList.toggle('ic-fd-yes', dxx > 55); card.classList.toggle('ic-fd-no', dxx < -55); });
card.addEventListener('pointerup', function () { if (!dragging) { return; } dragging = false; card.classList.remove('ic-fd-yes', 'ic-fd-no'); if (dxx > 105) { fly(1); openTip(r, card); } else if (dxx < -105) { fly(-1); setTimeout(function () { idx++; render(); }, 220); } else { card.style.transition = 'transform .2s'; card.style.transform = ''; } dxx = 0; });
card.querySelector('.ic-fd-face').addEventListener('click', function (e) { if (moved) { e.preventDefault(); } });
}
function openTip(r, card) {
if (card) { card.style.transition = 'transform .25s'; card.style.transform = ''; }
var sheet = stage.querySelector('.ic-fd-tipsheet'); sheet.hidden = false;
sheet.innerHTML = '<div class="ic-fd-tk">What can you tell us about this?</div>'
+ '<div class="ic-fd-chips">' + ['I recognize an agent', 'I was there', 'I have more footage', 'Something is wrong'].map(function (x) { return '<button type="button" class="ic-fd-c" data-c="' + x + '">' + x + '</button>'; }).join('') + '</div>'
+ '<textarea class="ic-fd-note" rows="2" placeholder="Optional short note. Do not include home addresses or personal contact details."></textarea>'
+ '<input class="ic-fd-link" placeholder="Optional: a link to footage or a post">'
+ '<div class="ic-fd-send-row"><button type="button" class="ic-fd-send">Send tip</button> <button type="button" class="ic-fd-skip">Skip</button> <span class="ic-fd-msg"></span></div>'
+ '<div class="ic-fd-priv">Anonymous. Nothing appears publicly until a checker reviews it.</div>';
var chosen = '';
sheet.querySelectorAll('.ic-fd-c').forEach(function (b) { b.onclick = function () { sheet.querySelectorAll('.ic-fd-c').forEach(function (x) { x.classList.remove('on'); }); b.classList.add('on'); chosen = b.getAttribute('data-c'); }; });
sheet.querySelector('.ic-fd-skip').onclick = function () { idx++; render(); };
sheet.querySelector('.ic-fd-send').onclick = function () { sendTip(r, chosen, sheet); };
}
function sendTip(r, chosen, sheet) {
var msg = sheet.querySelector('.ic-fd-msg'), btn = sheet.querySelector('.ic-fd-send');
var note = sheet.querySelector('.ic-fd-note').value, link = sheet.querySelector('.ic-fd-link').value;
if (!chosen && !note.trim() && !link.trim()) { msg.textContent = 'Pick one or add a note or link.'; return; }
if (/\b\d{3}[-.\s]?\d{3}[-.\s]?\d{4}\b|lives at|home address|\d+\s+\w+\s+(street|st|ave|avenue|road|rd|drive|dr|lane|ln)\b/i.test(note)) { msg.textContent = 'Please remove phone numbers or addresses first.'; return; }
btn.disabled = true; msg.textContent = 'Sending…';
var finding = (chosen || 'Tip') + (esc(note) ? ': ' + esc(note) : '');
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(), user = mw.config.get('wgUserName');
function done() { sheet.innerHTML = '<div class="ic-fd-tk">✓ Thank you. A checker will review your tip.</div>'; setTimeout(function () { idx++; render(); }, 850); }
function fail(e) { btn.disabled = false; msg.textContent = 'Could not send: ' + e; }
if (user) {
var rec = '{' + '{Sorting record|type=Lead|subject=' + esc(r.inc) + '|finding=' + finding + '|sources=' + esc(link) + '|needs=Footage tip on [' + '[' + esc(r.inc) + ']]|source=footage-deck|status=pending}}';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: rec, createonly: 1, summary: 'Footage tip about ' + esc(r.inc) }).then(done, fail);
} else {
var body = '{' + '{Submission link|url=' + esc(link) + '|note=Footage tip (' + finding + ') on [' + '[' + esc(r.inc) + ']]|source=footage-deck|status=new}}';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/link-fdtip-' + ts, text: body, createonly: 1, summary: 'Anonymous footage tip' }).then(done, function () { sheet.innerHTML = '<div class="ic-fd-tk">Almost. Anonymous tips need a free, 20-second account. <a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Footage_deck">Create an account →</a></div>'; });
}
});
}
render();
}
});
/* Footage deck: admin management panel on own profile */
ready(function () {
var u = mw.config.get('wgUserName'); if (!u) { return; }
var g = mw.config.get('wgUserGroups') || []; if (!(g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u))) { return; }
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; }
var content = document.querySelector('.mw-parser-output'); if (!content || document.querySelector('.ic-dm-panel')) { return; }
var box = document.createElement('div'); box.className = 'ic-dm-panel';
box.innerHTML = '<div class="ic-veyebrow">Footage deck · management</div><div class="ic-dm-load">Loading incidents…</div>';
content.insertBefore(box, content.firstChild);
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
Promise.all([
api.get({ action: 'parse', page: 'ICE List:Footage deck', prop: 'text', formatversion: 2 }),
api.get({ action: 'query', titles: 'ICE List:Footage deck/hidden', prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 })
]).then(function (res) {
var tmp = document.createElement('div'); tmp.innerHTML = res[0].parse.text;
var rows = [].slice.call(tmp.querySelectorAll('.ic-di')).map(function (d) { return { inc: d.getAttribute('data-incident'), state: icNormState(d.getAttribute('data-state')), city: d.getAttribute('data-city'), date: d.getAttribute('data-date') }; }).filter(function (r) { return r.inc; });
var pg = res[1].query.pages[0]; var conf = icConf((pg.revisions && pg.revisions[0].slots.main.content) || '');
renderPanel(rows, conf, api);
}, function () { box.querySelector('.ic-dm-load').textContent = 'Could not load the deck data.'; });
});
function renderPanel(rows, conf, api) {
var byState = {}; rows.forEach(function (r) { (byState[r.state] = byState[r.state] || []).push(r); });
var states = Object.keys(byState).filter(Boolean).sort();
var h = '<div class="ic-veyebrow">Footage deck · management</div><p class="ic-dm-help">Every documented incident is in the deck by default. Untick any you want to <b>remove</b>. Saves as you go.</p>';
states.forEach(function (st) {
var list = byState[st]; var cn = list.filter(function (r) { return !conf[r.inc]; }).length;
h += '<details class="ic-dm-st"><summary>' + mw.html.escape(st) + ' <span class="ic-dm-c">' + cn + ' / ' + list.length + ' in deck</span></summary>';
list.forEach(function (r) {
h += '<label class="ic-dm-row"><input type="checkbox" data-inc="' + encodeURIComponent(r.inc) + '"' + (conf[r.inc] ? '' : ' checked') + '><span class="ic-dm-t">' + mw.html.escape(r.inc) + '</span>' + (conf[r.inc] ? '<span class="ic-dm-flag">removed</span>' : '') + '</label>';
});
h += '</details>';
});
h += '<div class="ic-dm-save"><span class="ic-dm-msg"></span></div>';
box.innerHTML = h;
box.addEventListener('change', function (e) {
var cb = e.target.closest('input[type=checkbox]'); if (!cb) { return; }
var inc = decodeURIComponent(cb.getAttribute('data-inc'));
if (cb.checked) { delete conf[inc]; } else { conf[inc] = 1; }
var row = cb.closest('.ic-dm-row'); var fl = row.querySelector('.ic-dm-flag');
if (cb.checked && fl) { fl.remove(); } else if (!cb.checked && !fl) { row.insertAdjacentHTML('beforeend', '<span class="ic-dm-flag">removed</span>'); }
saveConf(conf, api);
});
}
var saveT;
function saveConf(conf, api) {
var msg = box.querySelector('.ic-dm-msg'); if (msg) { msg.textContent = 'Saving…'; }
clearTimeout(saveT);
saveT = setTimeout(function () {
var lines = Object.keys(conf).sort().map(function (x) { return '* ' + x; }).join('\n');
var page = '<!-- Incidents hidden from the public footage deck. Managed from admin profiles; edit via the panel, not by hand. -->\n' + lines + '\n[[Category:Volunteer resources]]';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Footage deck/hidden', text: page, summary: 'Update footage deck confirmed list' }).then(function () { if (msg) { msg.textContent = '✓ Saved · ' + Object.keys(conf).length + ' removed.'; } }, function (e) { if (msg) { msg.textContent = 'Save failed: ' + e; } });
}, 500);
}
});
/* Records: confirmer confirm / send-back, and admin publish-to-live-page */
/* Records: confirmer confirm / send-back, and admin publish-to-live-page */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var onQueue = (pn === 'ICE List:Submission/Confirm' || pn === 'ICE List:Submission/Overview');
var onRecord = /^ICE List:Submission\/Sorting\/(agent|vehicle|incident|note)-/.test(pn);
if (!onQueue && !onRecord) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
var isConfirm = g.indexOf('confirm') >= 0, isAdmin = g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || '');
var isVolunteer = isAdmin || ['submitter', 'sorter', 'confirm', 'photographer'].some(function (x) { return g.indexOf(x) >= 0; });
if (!isVolunteer) { return; }
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function esc(v) { return (v || '').replace(/[\[\]{}|]/g, '').trim(); }
function parseRec(c) { var m = /\{\{Sorting record\|([\s\S]*?)\}\}/.exec(c), d = {}; if (m) { m[1].split('|').forEach(function (kv) { var i = kv.indexOf('='); if (i > 0) { d[kv.slice(0, i).trim()] = kv.slice(i + 1).trim(); } }); } return d; }
function setStatus(page, st, reason) {
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) {
var c = dd.query.pages[0].revisions[0].slots.main.content;
c = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1' + st) : c.replace(/\}\}/, '|status=' + st + '}}');
if (reason != null) { var r = reason.replace(/[|{}]/g, ' '); c = /\|\s*reason\s*=/.test(c) ? c.replace(/(\|\s*reason\s*=\s*)[^|}\n]*/, '$1' + r) : c.replace(/\}\}/, '|reason=' + r + '}}'); }
return api.postWithToken('csrf', { action: 'edit', title: page, text: c, summary: 'Pipeline: ' + st });
});
}
function buildAgent(d, id, pub) {
var name = esc(d.name), title, nm;
if (name) { var np = name.split(/\s+/); title = np.length >= 2 ? np[np.length - 1] + ', ' + np.slice(0, -1).join(' ') : name; nm = name; } else { title = 'ICE List:Unidentified agent - ' + id; nm = 'Unidentified agent ' + id; }
var body = '{{Agent page\n|name=' + nm + '\n|agency=' + (esc(d.agency) || 'Unknown') + '\n|role=' + esc(d.role) + '\n|state=' + esc(d.state) + '\n|status=Active\n|image=' + (pub || 'nopfp.png') + '\n|verification=Submitted via pipeline\n|summary=' + (esc(d.notes) || (nm + ' was documented via the submission pipeline.')) + '\n}}\n\n== Documented Incidents ==\n{{IncidentsForAgent}}\n\n{{MediaForAgent}}\n\n' + (d.source ? '== Sources ==\n* ' + d.source + '\n\n' : '') + '[[Category:Agents]]' + (pub ? '' : '[[Category:Agents needing a photo]]');
return { title: title, body: body };
}
function buildVehicle(d, id, pub) {
var plate = esc(d.plate) || ('unknown-' + id);
return { title: 'Vehicle:' + plate, body: "'''Vehicle''' plate " + plate + (d.state ? ' (' + esc(d.state) + ')' : '') + '.\n\n* Make/model: ' + esc(d.vtype) + '\n* Agency: ' + (esc(d.agency) || 'ICE') + '\n* Notes: ' + esc(d.notes) + '\n* Source: ' + (d.source || '-') + '\n' + (pub ? '\n[[File:' + pub + '|thumb|Vehicle ' + plate + ']]\n' : '') + '\n[[Category:Vehicles]]' };
}
function buildIncident(d, id, pub) {
var loc = esc(d.location), st = esc(d.state), date = esc(d.date);
var title = ((esc(d.notes) || 'Incident').slice(0, 50) + ' (' + (loc || 'Unknown') + (st ? ', ' + st : '') + (date ? ' ' + date : '') + ')').replace(/[\[\]{}|#<>]/g, '');
var body = '{{Infobox incident\n | title = ' + title + '\n | date = ' + date + '\n | city = ' + loc + '\n | state = ' + st + '\n | country = United States\n | status = Submitted via pipeline\n | agents_involved = ' + esc(d.agency) + '\n}}\n\n' + esc(d.notes) + '\n\n' + (pub ? '[[File:' + pub + '|thumb]]\n\n' : '') + (d.source ? '== Sources ==\n* ' + d.source + '\n\n' : '') + '[[Category:Incidents]]';
return { title: title, body: body };
}
function publish(page) {
return api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (dd) {
var c = dd.query.pages[0].revisions[0].slots.main.content, d = parseRec(c);
var id = page.split('/').pop().replace(/^[a-z]+-/, ''), type = (d.type || '').toLowerCase();
var img = d.image || '', ext = img ? (img.split('.').pop() || 'jpg') : '', pub = '', built;
if (type === 'agent') { pub = img ? 'Agent-' + id + '.' + ext : ''; built = buildAgent(d, id, pub); }
else if (type === 'vehicle') { pub = img ? 'Vehicle-' + id + '.' + ext : ''; built = buildVehicle(d, id, pub); }
else if (type === 'incident') { pub = img ? 'Incident-' + id + '.' + ext : ''; built = buildIncident(d, id, pub); }
else { return Promise.reject('notes are context only — confirm & archive, don’t publish'); }
var chain = Promise.resolve();
if (img) { chain = api.postWithToken('csrf', { action: 'move', from: 'File:' + img, to: 'File:' + pub, reason: 'Publish from pipeline', movetalk: 1, ignorewarnings: 1, noredirect: 1 }).then(null, function () { built.body = built.body.split(pub).join(img); }); }
return chain.then(function () {
return api.postWithToken('csrf', { action: 'edit', title: built.title, text: built.body, createonly: 1, summary: 'Publish confirmed record from pipeline' });
}).then(function () {
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1published') : c.replace(/\}\}/, '|status=published}}');
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc + '\n<!-- published: ' + built.title + ' -->', summary: 'Pipeline: published to ' + built.title }).then(function () { return built.title; });
});
});
}
var recs = [].slice.call(document.querySelectorAll('.ic-rec[data-page]'));
var pend = recs.filter(function (r) { return r.getAttribute('data-status') === 'pending'; }).map(function (r) { return r.getAttribute('data-page'); });
var creators = {};
(pend.length ? api.get({ action: 'query', titles: pend.slice(0, 50).join('|'), prop: 'revisions', rvdir: 'newer', rvlimit: 1, rvprop: 'user', formatversion: 2 }).then(function (dd) { (dd.query.pages || []).forEach(function (p) { if (p.revisions && p.revisions[0]) { creators[p.title.replace(/_/g, ' ')] = p.revisions[0].user; } }); }, function () {}) : Promise.resolve()).then(function () {
recs.forEach(function (rec) {
var page = rec.getAttribute('data-page'), st = rec.getAttribute('data-status'), ty = rec.getAttribute('data-type');
var bar = rec.querySelector('.ic-rec__actions'); if (!bar) { return; }
function mk(label, cls, fn) { var a = document.createElement('a'); a.href = '#'; a.className = 'ic-vbtn ' + (cls || ''); a.textContent = label; a.addEventListener('click', function (e) { e.preventDefault(); fn(a); }); bar.appendChild(a); }
if (st === 'pending' && isVolunteer && (isAdmin || creators[page.replace(/_/g, ' ')] !== u)) {
mk('Confirm ✓', '', function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'confirmed', null).then(function () { rec.style.opacity = '.4'; a.textContent = '✓ confirmed'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); });
mk('Send back to sorting ↩', 'ic-vbtn--ghost', function (a) { var why = prompt('What needs fixing? The sorter will see this:'); if (why == null) { return; } a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'rework', why || 'needs another look').then(function () { rec.style.opacity = '.4'; a.textContent = '↩ sent back'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); });
}
if (st === 'pending' && isVolunteer && !isAdmin && creators[page.replace(/_/g, ' ')] === u) {
var sn = document.createElement('span'); sn.className = 'ic-rec__self'; sn.textContent = 'You submitted this — another volunteer confirms it.'; bar.appendChild(sn);
}
if (st === 'confirmed' && isAdmin) {
if (ty === 'note' || ty === 'lead') {
mk('Mark actioned ✓', '', function (a) { a.textContent = 'Saving…'; a.style.pointerEvents = 'none'; setStatus(page, 'published', null).then(function () { rec.style.opacity = '.5'; a.textContent = '✓ actioned'; }, function () { a.textContent = 'retry'; a.style.pointerEvents = ''; }); });
} else {
mk('Publish →', '', function (a) { a.textContent = 'Publishing…'; a.style.pointerEvents = 'none'; publish(page).then(function (title) { a.textContent = '✓ published'; var l = document.createElement('a'); l.href = '/index.php/' + encodeURI(title.replace(/ /g, '_')); l.textContent = 'view page →'; l.className = 'ic-rec__pub'; bar.appendChild(l); rec.style.opacity = '.6'; }, function (err) { a.textContent = 'Failed: ' + err; a.style.pointerEvents = ''; }); });
}
}
});
});
});
});
/* Photographer on-ramp: upload a photo + context → a pipeline record */
ready(function () {
var host = document.getElementById('ic-photoupload'); if (!host) { return; }
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
var canUp = g.indexOf('photographer') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('sysop') >= 0 || /^ICEListAdmin/.test(u || '');
if (!u) {
host.innerHTML = '<div class="ic-aa"><p><b>Log in to upload.</b> Photo uploads need an account so your work is protected.</p><p><a class="ic-vbtn" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Submit_photos">Log in</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Submit_photos">Create an account</a></p></div>';
return;
}
if (!canUp) { host.innerHTML = '<div class="ic-aa"><p>Your account can’t upload yet — ask an admin to add you to the <b>photographer</b> role. <a href="/index.php/ICE_List:Ask_an_admin">Ask an admin →</a></p></div>'; return; }
host.innerHTML = '<div class="ic-aa">'
+ '<label class="ic-sw-fld"><span>What is it?</span><select id="ic-ph-type"><option value="incident">An incident / scene</option><option value="agent">An agent</option><option value="vehicle">A vehicle</option></select></label>'
+ '<label class="ic-sw-fld"><span>Photo (strip location data first — see Staying safe)</span><input type="file" accept="image/*" id="ic-ph-file"></label>'
+ '<label class="ic-sw-fld"><span>Where? City & state</span><input id="ic-ph-loc"></label>'
+ '<label class="ic-sw-fld"><span>When? Date</span><input id="ic-ph-date" placeholder="YYYY-MM-DD"></label>'
+ '<label class="ic-sw-fld ic-ph-plate" style="display:none"><span>License plate (if a vehicle)</span><input id="ic-ph-plate"></label>'
+ '<label class="ic-sw-fld"><span>What’s happening? Any detail helps</span><textarea rows="3" id="ic-ph-notes"></textarea></label>'
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-ph-send">Upload to the pipeline</a> <span id="ic-ph-msg"></span></div>'
+ '<p class="ic-aa-alt">A checker verifies your photo before it ever appears publicly. Never put yourself at risk to get a shot.</p></div>';
var typeSel = document.getElementById('ic-ph-type');
typeSel.addEventListener('change', function () { host.querySelector('.ic-ph-plate').style.display = this.value === 'vehicle' ? '' : 'none'; });
document.getElementById('ic-ph-send').addEventListener('click', function (e) {
e.preventDefault(); var btn = this, msg = document.getElementById('ic-ph-msg');
function val(id) { var el = document.getElementById(id); return el ? el.value : ''; }
var file = document.getElementById('ic-ph-file').files[0];
if (!file) { msg.textContent = 'Choose a photo first.'; return; }
var t = typeSel.value, vals = { location: val('ic-ph-loc'), date: val('ic-ph-date'), notes: val('ic-ph-notes'), plate: val('ic-ph-plate') };
btn.style.pointerEvents = 'none'; msg.textContent = 'Uploading…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(), ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var ext = (file.name.split('.').pop() || 'jpg').toLowerCase(), fn = 'Sub-photo-' + ts + '.' + ext;
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); }
api.upload(file, { filename: fn, comment: 'Photographer upload', ignorewarnings: true }).then(function () {
var parts = ['{' + '{Sorting record|type=' + t.charAt(0).toUpperCase() + t.slice(1) + '|image=' + fn];
['location', 'date', 'notes', 'plate'].forEach(function (k) { if (esc(vals[k])) { parts.push('|' + k + '=' + esc(vals[k])); } });
parts.push('|source=photographer|status=pending}}');
return api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/' + t + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Photographer upload' });
}).then(function () { host.innerHTML = '<div class="ic-aa"><p><b>✓ Uploaded.</b> Thank you. A checker will verify it, and it may become a page on the site. <a href="#" onclick="location.reload();return false;">Upload another →</a></p></div>'; },
function (err) { msg.textContent = 'Failed: ' + err + ' — try again'; btn.style.pointerEvents = ''; });
});
});
});
/* Standalone "Submit a lead" (OSINT) → a Sorting record type=Lead in the review queue */
ready(function () {
var host = document.getElementById('ic-leadform'); if (!host) { return; }
var u = mw.config.get('wgUserName');
if (!u) {
host.innerHTML = '<div class="ic-aa"><p><b>Log in to submit a lead.</b> OSINT leads go into the review queue, so they need an account — 30 seconds, alias only.</p>'
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=ICE_List:Submit_a_lead">Create an account →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:UserLogin&returnto=ICE_List:Submit_a_lead">Log in</a></p></div>';
return;
}
var F = [['subject', 'Who or what is this about? (an agent, incident, facility, company)'], ['finding', 'What did you find? Your lead or idea', 1], ['sources', 'Sources & evidence links (one per line)', 1], ['needs', 'What still needs checking?', 1]];
var kind = ((mw.util && mw.util.getParamValue && mw.util.getParamValue('suggest')) || '').toLowerCase();
var CTX = {
photo: { banner: 'Suggesting a <b>photo</b> for a 287(g) agency or officer', subj: 'Which agency or officer? Paste the page name', find: 'Paste a link to the photo, and say what it shows', src: 'Where the photo is from (link)' },
name: { banner: 'Suggesting <b>who runs</b> a 287(g) agency', subj: 'Which agency? Paste the page name', find: 'The sheriff / chief / officer name — and how you know', src: 'Sources for the name (link, one per line)' }
};
var ctx = CTX[kind], banner = '';
if (ctx) { F[0] = ['subject', ctx.subj]; F[1] = ['finding', ctx.find, 1]; F[2] = ['sources', ctx.src, 1]; banner = '<div class="ic-sg-ctx">' + ctx.banner + ' — a checker reviews it before anything changes on the page.</div>'; }
var h = '<div class="ic-aa">' + banner;
F.forEach(function (f) { h += '<label class="ic-sw-fld"><span>' + f[1] + '</span>' + (f[2] ? '<textarea rows="3" data-k="' + f[0] + '"></textarea>' : '<input data-k="' + f[0] + '">') + '</label>'; });
h += '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-lead-send">Submit lead to the review queue</a> <span id="ic-lead-msg"></span></div>';
h += '<p class="ic-aa-alt">A checker reviews every lead. Never do anything unsafe or illegal to get information — see <a href="/index.php/ICE_List:Staying_safe">Staying safe</a>.</p></div>';
host.innerHTML = h;
document.getElementById('ic-lead-send').addEventListener('click', function (e) {
e.preventDefault(); var btn = this, msg = document.getElementById('ic-lead-msg');
var vals = {}; host.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; });
if (!(vals.finding || '').trim() && !(vals.subject || '').trim()) { msg.textContent = 'Add at least a subject and what you found.'; return; }
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api(); function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); }
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var parts = ['{' + '{Sorting record|type=Lead'];
F.forEach(function (f) { if ((vals[f[0]] || '').trim()) { parts.push('|' + f[0] + '=' + esc(vals[f[0]]).replace(/\n/g, '<br>')); } });
parts.push('|source=osint|status=pending}}');
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Sorting/lead-' + ts, text: parts.join(''), createonly: 1, summary: 'OSINT lead' }).then(function () {
host.innerHTML = '<div class="ic-aa"><p><b>✓ Lead submitted.</b> Thank you for the research — a checker will review it. <a href="#" onclick="location.reload();return false;">Submit another →</a></p></div>';
}, function (code) {
if (/permission|denied|group|badaccess/i.test(String(code))) { msg.textContent = 'Your account can’t submit to the queue yet — ask an admin.'; btn.style.pointerEvents = ''; return; }
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = '';
});
});
});
});
/* Public "drop a link" form — works for anyone (incl. logged-out once the anon hook is on) */
ready(function () {
var host = document.getElementById('ic-replink'); if (!host) { return; }
host.innerHTML = '<div class="ic-aa">'
+ '<label class="ic-sw-fld"><span>Paste the link — a post, article, or video about ICE</span><input id="ic-lk-url" placeholder="https://..."></label>'
+ '<label class="ic-sw-fld"><span>One line of context (optional)</span><textarea rows="2" id="ic-lk-note"></textarea></label>'
+ '<label class="ic-sw-fld"><span>What kind of link? (helps us prioritise footage)</span><select id="ic-lk-type"><option value="">— choose —</option><option value="video">🎥 Video / footage</option><option value="photo">📷 Photo</option><option value="social">📱 Social post</option><option value="article">📰 News article</option><option value="document">📄 Document</option><option value="other">🔗 Other</option></select></label>'
+ '<div class="ic-cap" style="display:none"><label class="ic-sw-fld"><span class="ic-cap-q"></span><input class="ic-cap-a"></label></div>'
+ '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-lk-send">Drop the link</a> <span id="ic-lk-msg"></span></div>'
+ '<p class="ic-aa-alt">No account needed. A volunteer checks it before anything appears publicly. Never share a link that would put you at risk — see <a href="/index.php/ICE_List:Staying_safe">Staying safe</a>.</p></div>';
var capId = null;
document.getElementById('ic-lk-send').addEventListener('click', function (e) {
e.preventDefault(); var btn = this, msg = document.getElementById('ic-lk-msg');
var url = document.getElementById('ic-lk-url').value.trim();
if (!/^https?:\/\//i.test(url)) { msg.textContent = 'Paste a link that starts with http.'; return; }
var note = document.getElementById('ic-lk-note').value;
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function esc(v) { return (v || '').replace(/[{}|\[\]]/g, ''); }
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var ty = (document.getElementById('ic-lk-type') || {}).value || '';
var body = '{' + '{Submission link|url=' + url.replace(/[{}|]/g, '') + '|note=' + esc(note) + '|source=anonymous' + (ty ? '|type=' + ty : '') + '|status=new}}';
var params = { action: 'edit', title: 'ICE List:Submission/Inbox/link-anon-' + ts, text: body, createonly: 1, summary: 'Anonymous link drop', formatversion: 2 };
var capA = host.querySelector('.ic-cap-a');
if (capId && capA.value) { params.captchaid = capId; params.captchaword = capA.value; }
api.postWithToken('csrf', params).then(function (r) {
var ed = r && r.edit;
if (ed && ed.result === 'Success') { host.innerHTML = '<div class="ic-aa"><p><b>✓ Thank you.</b> Your link is in the queue — a volunteer will check it. <a href="#" onclick="location.reload();return false;">Drop another →</a></p></div>'; return; }
if (ed && ed.captcha) { capId = ed.captcha.id; host.querySelector('.ic-cap').style.display = ''; host.querySelector('.ic-cap-q').textContent = 'One quick check to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then tap Drop the link again.'; btn.style.pointerEvents = ''; return; }
msg.textContent = 'Could not send — try again.'; btn.style.pointerEvents = '';
}, function (code) {
if (!mw.config.get('wgUserName') && /permission|denied|group|badaccess/i.test(String(code))) {
msg.textContent = 'Anonymous submitting isn’t switched on yet — an admin needs to enable it.'; btn.style.pointerEvents = ''; return;
}
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = '';
});
});
});
});
/* Public agent-report form + admin handled buttons */
ready(function () {
var pnR = mw.config.get('wgPageName').replace(/_/g, ' ');
var QUEUES = { 'ICE List:Submission/Reports/Agents': 1, 'ICE List:Submission/Reports/Incidents': 1, 'ICE List:Submission/Reports/Companies': 1 };
if (QUEUES[pnR]) {
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
document.querySelectorAll('.ic-rep[data-page]').forEach(function (it) {
if (it.getAttribute('data-status') === 'handled') { return; }
var act = it.querySelector('.ic-sub-actions'); if (!act) { return; }
var b = document.createElement('a'); b.href = '#'; b.className = 'ic-sub-done'; b.textContent = 'Mark handled ✓';
b.addEventListener('click', function (e) {
e.preventDefault(); b.textContent = 'Saving…';
var page = it.getAttribute('data-page');
api.get({ action: 'query', titles: page, prop: 'revisions', rvprop: 'content', rvslots: 'main', formatversion: 2 }).then(function (d) {
var c = d.query.pages[0].revisions[0].slots.main.content;
var nc = /\|\s*status\s*=/.test(c) ? c.replace(/(\|\s*status\s*=\s*)[^|}\n]*/, '$1handled') : c.replace(/\}\}$/, '|status=handled}}');
return api.postWithToken('csrf', { action: 'edit', title: page, text: nc, summary: 'Report handled' });
}).then(function () { it.style.opacity = '.35'; b.textContent = '✓ handled'; }, function () { b.textContent = 'Failed — retry'; });
});
act.appendChild(b);
});
});
return;
}
var KINDS = {
'ic-repagent': { tpl: 'Agent report', label: 'agent report', pref: 'agent', fields: [['name', 'Who? Name, badge number, or description'], ['agency', 'Agency (ICE, CBP, Border Patrol, unknown)'], ['where', 'Where? City and state'], ['when', 'When? Date or rough time frame'], ['what', 'What happened?', 1], ['links', 'Links to footage or posts (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] },
'ic-repincident': { tpl: 'Incident report', label: 'incident report', pref: 'incident', fields: [['what', 'What happened? One line'], ['when', 'When? Date'], ['where', 'Where? City and state'], ['agents', 'Agents involved (names / description)'], ['vehicles', 'Vehicles (plates / description)'], ['details', 'Full account — what you saw', 1], ['links', 'Links to footage, posts, or news (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] },
'ic-repcompany': { tpl: 'Company report', label: 'company report', pref: 'company', fields: [['company', 'Company name'], ['what', 'How are they involved with ICE? (contracts, cooperation, profiteering)', 1], ['where', 'Where? Locations / states'], ['links', 'Sources / evidence links (one per line)', 1], ['contact', 'Your contact (optional, only admins see it)']] }
};
var mountId = null, cfg = null;
for (var kk in KINDS) { if (document.getElementById(kk)) { mountId = kk; cfg = KINDS[kk]; break; } }
if (!cfg) { return; }
var host = document.getElementById(mountId);
var h = '<div class="ic-aa">';
cfg.fields.forEach(function (f) { h += '<label class="ic-sw-fld"><span>' + f[1] + '</span>' + (f[2] ? '<textarea rows="3" data-k="' + f[0] + '"></textarea>' : '<input data-k="' + f[0] + '">') + '</label>'; });
h += '<div class="ic-cap" style="display:none"><label class="ic-sw-fld"><span class="ic-cap-q"></span><input class="ic-cap-a"></label></div>';
h += '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-rep-send">Send report to the admins</a> <span id="ic-rep-msg"></span></div>';
h += '<p class="ic-aa-alt">Only ICE List admins can see reports. Nothing you send is published.</p></div>';
host.innerHTML = h;
var capId = null;
document.getElementById('ic-rep-send').addEventListener('click', function (e) {
e.preventDefault();
var btn = this, msg = document.getElementById('ic-rep-msg');
var vals = {}; host.querySelectorAll('[data-k]').forEach(function (el) { vals[el.getAttribute('data-k')] = el.value; });
if (!cfg.fields.some(function (f) { return (vals[f[0]] || '').trim(); })) { msg.textContent = 'Fill in at least one field.'; return; }
btn.style.pointerEvents = 'none'; msg.textContent = 'Sending…';
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
function resc(v) { return (v || '').replace(/[{}|]/g, ' '); }
var ts = Date.now().toString(36) + Math.floor(Math.random() * 1e4);
var parts = ['{' + '{' + cfg.tpl];
cfg.fields.forEach(function (f) { if ((vals[f[0]] || '').trim()) { parts.push('|' + f[0] + '=' + resc(vals[f[0]]).replace(/\n/g, '<br>')); } });
parts.push('|status=new}}');
var params = { action: 'edit', title: 'ICE List:Submission/Reports/' + cfg.pref + '-' + ts, text: parts.join(''), createonly: 1, summary: 'Public ' + cfg.label, formatversion: 2 };
var capA = host.querySelector('.ic-cap-a');
if (capId && capA.value) { params.captchaid = capId; params.captchaword = capA.value; }
api.postWithToken('csrf', params).then(function (r) {
var ed = r && r.edit;
if (ed && ed.result === 'Success') { host.innerHTML = '<div class="ic-aa"><p><b>✓ Report sent.</b> Thank you. Only the admins can see it. If you left contact details they may follow up.</p></div>'; return; }
if (ed && ed.captcha) { capId = ed.captcha.id; host.querySelector('.ic-cap').style.display = ''; host.querySelector('.ic-cap-q').textContent = 'One check, to stop bots: ' + (ed.captcha.question || 'answer required'); msg.textContent = 'Answer the question, then send again.'; btn.style.pointerEvents = ''; return; }
msg.textContent = 'Could not send. Try once more.'; btn.style.pointerEvents = '';
}, function (code) {
if (!mw.config.get('wgUserName') && (code === 'permissiondenied' || code === 'protectedpage' || /permission|denied|group/i.test(String(code)))) {
var back = encodeURIComponent(mw.config.get('wgPageName'));
host.innerHTML = '<div class="ic-aa"><p><b>Almost there — you need a free account.</b> It takes about 30 seconds and you pick an alias (never your real name). Then you can submit this straight away.</p>'
+ '<p><a class="ic-vbtn" href="/index.php?title=Special:CreateAccount&returnto=' + back + '">Create an account →</a> '
+ '<a class="ic-vbtn ic-vbtn--ghost" href="/index.php?title=Special:UserLogin&returnto=' + back + '">Log in</a></p></div>';
return;
}
msg.textContent = 'Failed: ' + (code || 'error') + ' — try again'; btn.style.pointerEvents = '';
});
});
});
});
/* Volunteer ladder: progress + trusted-status request (own profile only) */
ready(function () {
var u = mw.config.get('wgUserName'); if (!u) { return; }
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; }
var g = mw.config.get('wgUserGroups') || [];
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0)) { return; }
var trusted = g.indexOf('trusted-volunteer') >= 0 || g.indexOf('incident-editor') >= 0 || g.indexOf('sysop') >= 0;
mw.loader.using(['mediawiki.api']).then(function () {
var api = new mw.Api();
api.get({ action: 'query', list: 'usercontribs', ucuser: u, uclimit: 500, ucshow: 'new', ucprop: 'title', formatversion: 2 }).then(function (d) {
var cs = d.query.usercontribs || [], c = { links: 0, agents: 0, vehicles: 0, incidents: 0, leads: 0, notes: 0 };
cs.forEach(function (x) {
var t = x.title;
if (/\/Sorting\/agent-/.test(t)) { c.agents++; }
else if (/\/Sorting\/vehicle-/.test(t)) { c.vehicles++; }
else if (/\/Sorting\/incident-/.test(t)) { c.incidents++; }
else if (/\/Sorting\/lead-/.test(t)) { c.leads++; }
else if (/\/Sorting\/note-/.test(t)) { c.notes++; }
else if (/\/Inbox\/link-/.test(t)) { c.links++; }
});
var total = c.links + c.agents + c.vehicles + c.incidents + c.leads + c.notes, goal = 50;
function brk() {
var p = [];
if (c.links) { p.push(c.links + ' link' + (c.links > 1 ? 's' : '')); }
if (c.agents) { p.push(c.agents + ' agent' + (c.agents > 1 ? 's' : '')); }
if (c.vehicles) { p.push(c.vehicles + ' vehicle' + (c.vehicles > 1 ? 's' : '')); }
if (c.incidents) { p.push(c.incidents + ' incident' + (c.incidents > 1 ? 's' : '')); }
if (c.leads) { p.push(c.leads + ' lead' + (c.leads > 1 ? 's' : '')); }
if (c.notes) { p.push(c.notes + ' note' + (c.notes > 1 ? 's' : '')); }
return p.length ? p.join(' · ') : 'Nothing yet — open a link on your desk to start.';
}
var box = document.createElement('div'); box.className = 'ic-ladder';
var h = '<div class="ic-veyebrow">Your progress</div>';
if (trusted) {
h += '<p><b>You’re a trusted volunteer.</b> Thank you. The next steps up are by invitation from the community.</p><p class="ic-ladder-break">' + brk() + '</p>';
} else {
var pct = Math.min(100, Math.round(total / goal * 100));
h += '<p><b>' + total + ' of ' + goal + '</b> contributions. At ' + goal + ' you can ask the team to review you for trusted-volunteer status.</p>'
+ '<div class="ic-ladder-bar"><span style="width:' + pct + '%"></span></div>'
+ '<p class="ic-ladder-break">' + brk() + '</p>';
h += (total >= goal)
? '<div class="ic-vact"><a href="#" class="ic-vbtn" id="ic-ladder-req">Request a trusted-volunteer review</a> <span id="ic-ladder-msg"></span></div>'
: '<p class="ic-aa-alt">' + (goal - total) + ' to go before you can request a review.</p>';
}
h += '<p class="ic-aa-alt"><a href="/index.php/ICE_List:Volunteer/Becoming_a_trusted_volunteer">How the volunteer ladder works →</a></p>';
box.innerHTML = h;
var host = document.querySelector('.mw-parser-output'); if (host) { host.insertBefore(box, host.firstChild); }
var rq = document.getElementById('ic-ladder-req');
if (rq) { rq.addEventListener('click', function (e) {
e.preventDefault(); rq.style.pointerEvents = 'none';
var msg = document.getElementById('ic-ladder-msg'); msg.textContent = 'Sending…';
api.postWithToken('csrf', { action: 'edit', title: 'ICE List:Submission/Inbox/Promotions', section: 'new', sectiontitle: 'Trusted-volunteer request: ' + u, text: '[[User:' + u + ']] (' + total + ' contributions: ' + brk() + ') requests a trusted-volunteer review. [[Special:Contributions/' + u + '|see contributions]] — cc [[User:ICEListAdmin6|ICEListAdmin6]] [[User:AdminBot|AdminBot]] ([[User talk:AdminBot|talk]]) 14:21, 27 July 2026 (UTC)', summary: 'Trusted-volunteer request' })
.then(function () { msg.textContent = ''; rq.textContent = '✓ Request sent'; }, function () { msg.textContent = 'Failed — retry'; rq.style.pointerEvents = ''; });
}); }
});
});
});
/* Confirm/sort desks: when your own queue is empty, offer upstream work */
ready(function () {
var u = mw.config.get('wgUserName'); if (!u) { return; }
if (mw.config.get('wgPageName').replace(/_/g, ' ') !== 'User:' + u) { return; }
var g = mw.config.get('wgUserGroups') || [];
var isConfirm = g.indexOf('confirm') >= 0, isSorter = g.indexOf('sorter') >= 0;
if (!isConfirm && !isSorter) { return; }
var myCat = isConfirm ? 'Category:Submission confirm' : 'Category:Submission sorting';
mw.loader.using(['mediawiki.api']).then(function () {
new mw.Api().get({ action: 'query', list: 'categorymembers', cmtitle: myCat, cmlimit: '2', cmtype: 'page' }).then(function (d) {
if ((d.query.categorymembers || []).length > 0) { return; }
var box = document.createElement('div'); box.className = 'ic-emptyq';
var links = isConfirm
? '<a class="ic-vbtn" href="/index.php/ICE_List:Submission/Sorting">Help sort links →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Vetting">Check new links →</a>'
: '<a class="ic-vbtn" href="/index.php/ICE_List:Submission/Vetting">Check new links →</a> <a class="ic-vbtn ic-vbtn--ghost" href="/index.php/ICE_List:Submission/Inbox">Browse the inbox →</a>';
box.innerHTML = '<div class="ic-veyebrow">Your queue is clear</div><p>Nothing waiting for you right now. Nice work. Want to lend a hand upstream?</p><div class="ic-vact">' + links + '</div>';
var host = document.querySelector('.mw-parser-output'); if (host) { host.insertBefore(box, host.firstChild); }
});
});
});
/* Logged-in volunteers: annotate the profile link + a "get to work" nudge */
ready(function () {
var g = mw.config.get('wgUserGroups') || [], u = mw.config.get('wgUserName');
var isVol = g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0;
if (!u || !isVol) { return; }
// annotate the personal-tools profile link with a task dot
var pl = document.querySelector('#pt-userpage a, li#pt-userpage a, #p-personal #pt-userpage a');
if (pl && !pl.querySelector('.ic-pt-dot')) {
pl.title = 'Your desk. Your tasks are here.';
var dot = document.createElement('span'); dot.className = 'ic-pt-dot'; pl.appendChild(dot);
}
// the get-to-work bar — skip it while already on your own profile
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn === 'User:' + u) { return; }
try { if (sessionStorage.getItem('ic-gtw-x')) { return; } } catch (e) {}
var b = document.createElement('div'); b.className = 'ic-gtw';
b.innerHTML = '<span class="ic-gtw-t"><b>Ready to add to the record?</b> Your tasks are waiting on your desk.</span>'
+ '<a class="ic-gtw-go" href="/index.php/Special:MyPage">Get to work →</a>'
+ '<a href="#" class="ic-gtw-x" aria-label="Dismiss">×</a>';
var host = document.querySelector('#mw-content-text');
if (host) { host.insertBefore(b, host.firstChild); }
b.querySelector('.ic-gtw-x').addEventListener('click', function (e) { e.preventDefault(); b.style.display = 'none'; try { sessionStorage.setItem('ic-gtw-x', '1'); } catch (e) {} });
});
/* Anonymous readers: one gentle, non-modal "drop a link" nudge after they've read a bit */
ready(function () {
if (mw.config.get('wgUserName')) { return; } // logged-out only
if (mw.config.get('wgNamespaceNumber') < 0) { return; } // not on Special pages
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (/^Special:|Submit a link|Submit incident|Submit Agent|Submit photos|CreateAccount|UserLogin/.test(pn)) { return; }
try { if (sessionStorage.getItem('ic-nudge-x')) { return; } } catch (e) {}
var shown = false, t;
function show() {
if (shown) { return; }
var bar = document.querySelector('.ic-support, .ic-anonbar');
if (bar && bar.offsetParent !== null) { return; } // donation/top banner still up → don't double-ask
shown = true; clearTimeout(t);
var n = document.createElement('div'); n.className = 'ic-nudge';
n.innerHTML = '<button class="ic-nudge__x" aria-label="Dismiss">×</button>'
+ '<div class="ic-nudge__t"><b>Seen something about ICE?</b> This record is built by all of us — add what you see. No account needed.</div>'
+ '<a class="ic-nudge__btn" href="/index.php/ICE_List:Submit_a_link">Drop a link →</a>';
document.body.appendChild(n);
requestAnimationFrame(function () { n.classList.add('ic-nudge--in'); });
n.querySelector('.ic-nudge__x').addEventListener('click', function () {
n.classList.remove('ic-nudge--in'); setTimeout(function () { n.remove(); }, 300);
try { sessionStorage.setItem('ic-nudge-x', '1'); } catch (e) {}
});
}
t = setTimeout(show, 14000); // after ~14s of reading
window.addEventListener('scroll', function ons() { // ...or 45% down the page
if ((window.scrollY + window.innerHeight) / document.body.scrollHeight > 0.45) {
window.removeEventListener('scroll', ons); show();
}
}, { passive: true });
});
/* Share bar on public content pages (agents, incidents, deaths, vehicles, media, maps) */
ready(function () {
if (mw.config.get('wgAction') !== 'view') { return; }
var ns = mw.config.get('wgNamespaceNumber');
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
if (pn === 'Main Page') { return; }
var isMain = ns === 0;
var isPublicProject = /^ICE List(?: Wiki)?:/.test(pn) && !/submission|volunteer|ask an admin|staying safe|poster|submit|culture inbox|help desk|report an agent|agent reports|becoming|donate|start here/i.test(pn);
if (!(isMain || isPublicProject)) { return; }
var host = document.querySelector('.mw-parser-output'); if (!host) { return; }
var title = mw.config.get('wgTitle');
var url = (location.protocol + '//' + location.host + mw.util.getUrl());
var canon = document.querySelector('link[rel=canonical]'); if (canon && canon.href) { url = canon.href; }
var text = title + ' — documented on the ICE List, a public record of U.S. immigration enforcement.';
var eU = encodeURIComponent(url), eT = encodeURIComponent(text), eTU = encodeURIComponent(text + ' ' + url);
var targets = [
['Bluesky', 'https://bsky.app/intent/compose?text=' + eTU],
['X', 'https://twitter.com/intent/tweet?text=' + eT + '&url=' + eU],
['Facebook', 'https://www.facebook.com/sharer/sharer.php?u=' + eU],
['Threads', 'https://www.threads.net/intent/post?text=' + eTU],
['Reddit', 'https://www.reddit.com/submit?url=' + eU + '&title=' + encodeURIComponent(title)]
];
var bar = document.createElement('div'); bar.className = 'ic-share';
var h = '<span class="ic-share__lbl">Share this record</span>'
+ '<a href="#" class="ic-share__b ic-share__copy">Copy link</a>';
targets.forEach(function (t) { h += '<a class="ic-share__b" target="_blank" rel="noopener noreferrer" href="' + t[1] + '">' + t[0] + '</a>'; });
if (navigator.share) { h += '<a href="#" class="ic-share__b ic-share__native">More…</a>'; }
bar.innerHTML = h;
host.insertBefore(bar, host.firstChild);
bar.querySelector('.ic-share__copy').addEventListener('click', function (e) {
e.preventDefault(); var self = this;
navigator.clipboard.writeText(url).then(function () { var o = self.textContent; self.textContent = 'Copied ✓'; setTimeout(function () { self.textContent = o; }, 1400); });
});
var nat = bar.querySelector('.ic-share__native');
if (nat) { nat.addEventListener('click', function (e) { e.preventDefault(); navigator.share({ title: title, text: text, url: url }).catch(function () {}); }); }
});
/* Auto-link key terms to their maps (first mention per page, non-destructive) */
ready(function () {
var pn = mw.config.get('wgPageName').replace(/_/g, ' ');
var content = document.querySelector('.mw-parser-output'); if (!content) { return; }
var LINKS = [
{ re: /287\(g\)/, href: '/index.php/ICE_List:Map_of_287(g)_agreements', title: '287(g) agreements map', skip: /Map of 287\(g\)/ },
{ re: /\b(detention cente?r|concentration camp)\b/i, href: '/index.php/ICE_List:Map_of_concentration_camps_and_ICE_facilities', title: 'Map of detention sites', skip: /concentration camps and ICE facilities/ }
];
LINKS.forEach(function (cfg) {
if (cfg.skip.test(pn)) { return; } // don't self-link on its own map page
var walker = document.createTreeWalker(content, NodeFilter.SHOW_TEXT, {
acceptNode: function (n) {
if (!cfg.re.test(n.nodeValue)) { return NodeFilter.FILTER_SKIP; }
var p = n.parentNode;
while (p && p !== content) {
if (p.tagName === 'A' || /^H[1-6]$/.test(p.tagName || '') || (p.classList && (p.classList.contains('mw-editsection') || p.classList.contains('ic-anonbar')))) { return NodeFilter.FILTER_SKIP; }
p = p.parentNode;
}
return NodeFilter.FILTER_ACCEPT;
}
});
var node = walker.nextNode(); if (!node) { return; }
var m = cfg.re.exec(node.nodeValue); if (!m) { return; }
var before = node.nodeValue.slice(0, m.index), after = node.nodeValue.slice(m.index + m[0].length);
var a = document.createElement('a'); a.href = cfg.href; a.textContent = m[0]; a.title = cfg.title;
var frag = document.createDocumentFragment();
if (before) { frag.appendChild(document.createTextNode(before)); }
frag.appendChild(a);
if (after) { frag.appendChild(document.createTextNode(after)); }
node.parentNode.replaceChild(frag, node);
});
});
/* Dismissable volunteer-guide banner (pipeline volunteers) */
ready(function () {
var g = mw.config.get('wgUserGroups') || [];
if (!(g.indexOf('submitter') >= 0 || g.indexOf('sorter') >= 0 || g.indexOf('confirm') >= 0)) { return; }
try { if (localStorage.getItem('ic-guide-dismissed')) { return; } } catch (e) {}
var b = document.createElement('div'); b.className = 'ic-guidebanner';
b.innerHTML = '<span>New to the ICE List? <a href="/index.php/ICE_List:Volunteer_guide">Read the volunteer guide \u2192</a></span><a href="#" class="ic-guidebanner__x" aria-label="Dismiss">\u00d7</a>';
var host = document.querySelector('#mw-content-text');
if (host) { host.insertBefore(b, host.firstChild); }
b.querySelector('.ic-guidebanner__x').addEventListener('click', function (e) { e.preventDefault(); b.style.display = 'none'; try { localStorage.setItem('ic-guide-dismissed', '1'); } catch (e) {} });
});
})();
/* ============ end ICE List client tools ============ */