blob: 010d2f84cd05aac50b6c9e19ff787f3ea60f1f8b [file] [log] [blame]
/*
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// THIS IS AN AUTOMATICALLY COMBINED FILE. PLEASE EDIT source/*.js!!
/******************************************
Fetched from source/base-http-extensions.js
******************************************/
// URL calls currently 'in escrow'. This controls the spinny wheel animation
var async_escrow = {}
var async_maxwait = 250; // ms to wait before displaying spinner
var async_status = 'clear';
var async_cache = {}
// Escrow spinner check
async function escrow_check() {
let now = new Date();
let show_spinner = false;
for (var k in async_escrow) {
if ( (now - async_escrow[k]) > async_maxwait ) {
show_spinner = true;
break;
}
}
// Fetch or create the spinner
let spinner = document.getElementById('spinner');
if (!spinner) {
spinner = new HTML('div', { id: 'spinner', class: 'spinner'});
spinwheel = new HTML('div', {id: 'spinwheel', class: 'spinwheel'});
spinner.inject(spinwheel);
spinner.inject(new HTML('h2', {}, "Loading, please wait.."));
document.body.appendChild(spinner);
}
// Show or don't show spinner?
if (show_spinner) {
spinner.style.display = 'block';
if (async_status === 'clear') {
console.log("Waiting for JSON resource, deploying spinner");
async_status = 'waiting';
}
} else {
spinner.style.display = 'none';
if (async_status === 'waiting') {
console.log("All URLs out of escrow, dropping spinner");
async_status = 'clear';
}
}
}
async function async_snap(error) {
msg = await error.text();
msg = msg.replace(/<.*?>/g, ""); // strip HTML tags
modal("An error occured", "An error code %u occured while trying to fetch %s:\n%s".format(error.status, error.url, msg), "error");
}
// Asynchronous GET call
async function GET(url, callback, state) {
console.log("Fetching JSON resource at %s".format(url))
let pkey = "GET-%s-%s".format(callback, url);
let res = undefined;
let res_json = undefined;
state = state || {};
state.url = url;
if (state && state.cached === true && async_cache[url]) {
console.log("Fetching %s from cache".format(url));
res_json = async_cache[url];
}
else {
try {
console.log("putting %s in escrow...".format(url));
async_escrow[pkey] = new Date(); // Log start of request in escrow dict
const rv = await fetch(url, {credentials: 'same-origin'}); // Wait for resource...
// Since this is an async request, the request may have been canceled
// by the time we get a response. Only do callback if not.
if (async_escrow[pkey] !== undefined) {
delete async_escrow[pkey]; // move out of escrow when fetched
res = rv;
}
}
catch (e) {
delete async_escrow[pkey]; // move out of escrow if failed
console.log("The URL %s could not be fetched: %s".format(url, e));
modal("An error occured", "An error occured while trying to fetch %s:\n%s".format(url, e), "error");
}
}
if (res !== undefined || res_json !== undefined) {
// We expect a 2xx return code (usually 200 or 201), snap otherwise
if ((res_json) || (res.status >= 200 && res.status < 300)) {
console.log("Successfully fetched %s".format(url))
if (res_json) {
js = res_json;
} else {
js = await res.json();
async_cache[url] = js;
}
if (callback) {
callback(state, js);
} else {
console.log("No callback function was registered for %s, ignoring result.".format(url));
}
} else {
console.log("URL %s returned HTTP code %u, snapping!".format(url, res.status));
async_snap(res);
}
}
}
/******************************************
Fetched from source/base-js-extensions.js
******************************************/
/**
* String formatting prototype
* A'la printf
*/
String.prototype.format = function() {
let args = arguments;
let n = 0;
let t = this;
let rtn = this.replace(/(?!%)?%([-+]*)([0-9.]*)([a-zA-Z])/g, function(m, pm, len, fmt) {
len = parseInt(len || '1');
// We need the correct number of args, balk otherwise, using ourselves to format the error!
if (args.length <= n) {
let err = "Error interpolating string '%s': Expected at least %u argments, only got %u!".format(t, n+1, args.length);
console.log(err);
throw err;
}
let varg = args[n];
n++;
switch (fmt) {
case 's':
if (typeof(varg) == 'function') {
varg = '(function)';
}
return varg;
// For now, let u, d and i do the same thing
case 'd':
case 'i':
case 'u':
varg = parseInt(varg).pad(len); // truncate to Integer, pad if needed
return varg;
}
});
return rtn;
}
/**
* Number prettification prototype:
* Converts 1234567 into 1,234,567 etc
*/
Number.prototype.pretty = function(fix) {
if (fix) {
return String(this.toFixed(fix)).replace(/(\d)(?=(\d{3})+\.)/g, '$1,');
}
return String(this.toFixed(0)).replace(/(\d)(?=(\d{3})+$)/g, '$1,');
};
/**
* Number padding
* usage: 123.pad(6) -> 000123
*/
Number.prototype.pad = function(n) {
var str;
str = String(this);
/* Do we need to pad? if so, do it using String.repeat */
if (str.length < n) {
str = "0".repeat(n - str.length) + str;
}
return str;
};
/* Func for converting a date to YYYY-MM-DD HH:MM */
Date.prototype.ISOBare = function() {
var M, d, h, m, y;
y = this.getFullYear();
m = (this.getMonth() + 1).pad(2);
d = this.getDate().pad(2);
h = this.getHours().pad(2);
M = this.getMinutes().pad(2);
return y + "-" + m + "-" + d + " " + h + ":" + M;
};
/* isArray: function to detect if an object is an array */
isArray = function(value) {
return value && typeof value === 'object' && value instanceof Array && typeof value.length === 'number' && typeof value.splice === 'function' && !(value.propertyIsEnumerable('length'));
};
/* isHash: function to detect if an object is a hash */
isHash = function(value) {
return value && typeof value === 'object' && !isArray(value);
};
/* Remove an array element by value */
Array.prototype.remove = function(val) {
var i, item, j, len;
for (i = j = 0, len = this.length; j < len; i = ++j) {
item = this[i];
if (item === val) {
this.splice(i, 1);
return this;
}
}
return this;
};
/* Check if array has value */
Array.prototype.has = function(val) {
var i, item, j, len;
for (i = j = 0, len = this.length; j < len; i = ++j) {
item = this[i];
if (item === val) {
return true;
}
}
return false;
};
/******************************************
Fetched from source/body-fixups.js
******************************************/
ponymail_url_regex = new RegExp("(" + "(?:(?:[a-z]+)://)" + "(?:\\S+(?::\\S*)?@)?" + "(?:" + "([01][0-9][0-9]|2[0-4][0-9]|25[0-5])" + "|" + "(?:(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)" + "(?:\\.(?:[a-z\\u00a1-\\uffff0-9]-*)*[a-z\\u00a1-\\uffff0-9]+)*" + "(?:\\.(?:[a-z\\u00a1-\\uffff]{2,}))" + "\\.?" + ")" + "(?::\\d{2,5})?" + "(?:[/?#]([^,<>()\\[\\] \\t\\r\\n]|(<[^:\\s]*?>|\\([^:\\s]*?\\)|\\[[^:\\s]*?\\]))*)?" + ")\\.?", "mi");
// Regex for things to potentially put inside quote objects:
// - quotes
// - forwarded emails
// - inline quoting
// - top posting with original email following
ponymail_quote_regex = new RegExp("(" +
// Typical encapsulation of context by ticketing systems and/or bug trackers
"(---\\r?\\n([^\\r\\n]*?\\r?\\n)*?---$)|" +
"(" +
"(?:\\r?\\n|^)" + // start of line or after a colon
// Classic method; a signal line and the quote with '>' starting each line quoted
"(" +
"(" + // Initial line signalling a quote
"((\\d+-\\d+-\\d+)\\s+.+<\\S+@\\S+>:[ \t\r\n]+)|" + // "01-02-1982 Foo Bar <foo@bar.baz>:" OR
"(on\\s+(.+|.+\\n.+)\\s+wrote:[\r?\n]+)|" + // "On $somedate, $someone wrote:", OR...
"(le\\s+(.+|.+\\n.+)\\s+écrit:[\r?\n]+)|" + // French version of the above, OR...
"(.?am .+? schrieb\\s+.+:[\r?\n]+)|" + // German version of the above, OR...
"(envoy[ée] de mon .+|sent from my .+|von meinem .+ gesendet)[ \t\r\n]+" + // "sent from my iphone/ipad/android phone/whatever", usually means the next part is a quote.
")" + // End initial signal line
"(^$)*" + // Accept blank newlines following it...
"(((^(?!>)[\\s\\S]+$)*)|(^\\s*>[\\s\\S]+$)*)" + // Either text that follows immediately after with no '>' first, OR text with '>' first, but NOT both...
")|" +
"(" +
// Lines after the signal line; comes in one shape, generally speaking...
"(^\\s*>+[ \\t]*[^\r\n]*\r*\n+)+" + // Lines beginning with one or more '>' after the initial signal line
")" +
")+|" + //OR...
"(" +
"^(-{5,10}).+?\\1[\r\n]+" + // ----- Forwarded Message -----
"(^\\w+:\\s+.+[\r\n]+){3,10}[\r\n]+" + // Between three and ten header fields (we ask for at least 3, so as to not quote PGP blocks)
"[\\S\\s]+" + // Whatever comes next...
")+" +
")", "mi");
// Somewhat simplified method for catching email footers/trailers that we don't need
ponymail_trailer_regex = new RegExp("^--[\r\n]+.*", "mi");//(--\r?\n([^\r\n]*?\r?\n){1,6}$)|[\r\n.]+^((--+ \r?\n|--+\r?\n|__+\r?\n|--+\\s*[^\r\n]+\\s*--+\r?\n)(.*\r?\n)+)+$", "m");
// This is a regex for capturing code diff blocks in an email
ponymail_diff_regex = new RegExp(
"(" +
"^-{3} .+?[\r\n]+" + // Starts with a "--- /foo/bar/baz"
"^\\+{3} .+?[\r\n]+" + // Then a "+++ /foo/bar/baz"
"(" + // Then one or more of...
"^@@ .+[\r\n]+" + // positioning
"(^ .*[\r\n]*$){0,3}" + // diff header
"(^[-+ ].*[\r\n]*)+" + // actual diff
"(^ .*[\r\n]*$){0,3}" + // diff trailer
")+" +
")", "mi");
// Function for turning URLs into <a> tags
function fixup_urls(splicer) {
if (typeof splicer == 'object') {
return splicer;
//splicer = splicer.innerText;
}
/* Array holding text and links */
var i, m, t, textbits, url, urls;
textbits = [];
/* Find the first link, if any */
i = splicer.search(ponymail_url_regex);
urls = 0;
/* While we have more links, ... */
while (i !== -1) {
urls++;
/* Only parse the first 250 URLs... srsly */
if (urls > 250) {
break;
}
/* Text preceding the link? add it to textbits frst */
if (i > 0) {
t = splicer.substr(0, i);
textbits.push(t);
splicer = splicer.substr(i);
}
/* Find the URL and cut it out as a link */
m = splicer.match(ponymail_url_regex);
if (m) {
url = m[1];
i = url.length;
t = splicer.substr(0, i);
textbits.push(new HTML('a', {
href: url
}, url));
splicer = splicer.substr(i);
}
/* Find the next link */
i = splicer.search(ponymail_url_regex);
}
/* push the remaining text into textbits */
textbits.push(splicer);
return textbits;
}
// Simple check to (attempt to) assess whether a trailer should
// remain or get cut out.
function legit_trailer(a) {
let lines = a.split(/\s*\r?\n/);
let first_line = lines.shift();
while (first_line.length == 0 && lines.length) first_line = lines.shift(); // get first meaningful line
if (!lines.length || first_line == '--') return ''; // likely a simple trailer
let last_line = lines.pop();
while (last_line.length == 0 && lines.length) last_line = lines.pop(); // get last meaningful line
// Check if first and last line are similar, which is usually indictive of a ticket system
if (last_line == first_line) {
return a;
}
// Otherwise, check if first line has two or more dashes, and it occurs again later (also tix)
if (first_line.match(/^---+/) && lines.has(first_line)) {
return "|||" + a + "|||";
}
// Lastly, if there is "sufficient" length to the dashes, allow (JIRA etc)
if (first_line.match(/^-{6,72}$/)) return a;
return '';
}
// Function for cutting away trailers
function cut_trailer(splicer) {
if (!chatty_layout) return splicer; // only invoke in social rendering mode
if (typeof splicer == 'object') {
splicer.innerText = splicer.innerText.replace(ponymail_trailer_regex, legit_trailer, 3);
} else {
splicer = splicer.replace(ponymail_trailer_regex, legit_trailer);
}
return splicer;
}
function color_diff_lines(diff) {
let lines = diff.split(/[\r\n]+/);
let ret = [];
for (var z = 0; z < lines.length;z++) {
let line = lines[z];
let color = 'grey';
if (line[0] == '@') color = 'blue';
if (line[0] == '-') color = 'red';
if (line[0] == '+') color = 'green';
if (line[0] == ' ') color = 'black';
let el = new HTML('span', {style: {color: color}}, line);
ret.push(el);
ret.push(new HTML('br'));
}
return ret;
}
// Function for coloring diffs
function fixup_diffs(splicer) {
if (!chatty_layout) return splicer; // only invoke in social rendering mode
if (typeof splicer == 'object') {
splicer = splicer.innerText;
}
/* Array holding text and links */
var i, m, t, diff, diffs;
let textbits = [];
/* Find the first link, if any */
i = splicer.search(ponymail_diff_regex);
diffs = 0;
/* While we have more links, ... */
while (i !== -1) {
diffs++;
/* Only parse the first 20 diffs... srsly */
if (diffs > 25) {
break;
}
console.log(i);
/* Text preceding the diff? add it to textbits frst */
if (i > 0) {
t = splicer.substr(0, i);
textbits.push(t);
splicer = splicer.substr(i);
}
/* Find the URL and cut it out as a link */
m = splicer.match(ponymail_diff_regex);
if (m) {
diff = m[1];
i = diff.length;
t = splicer.substr(0, i);
textbits.push(new HTML('pre', {class: 'diff'}, color_diff_lines(diff)));
splicer = splicer.substr(i);
}
/* Find the next link */
i = splicer.search(ponymail_diff_regex);
}
/* push the remaining text into textbits */
textbits.push(splicer);
return textbits;
}
// Function for turning quotes into quote segments
function fixup_quotes(splicer) {
if (splicer[splicer.length-1] !== "\n") splicer += "\n"; //tweak to make quotes match the last line if no newline on it.
var hideQuotes, i, m, qdiv, quote, quotes, t, textbits;
hideQuotes = true;
if (prefs.compactQuotes === false && !chatty_layout) {
hideQuotes = false;
}
if (!hideQuotes) return splicer; // We'll bail here for now. Dunno why not.
/* Array holding text and quotes */
textbits = [];
/* Find the first quote, if any */
i = splicer.search(ponymail_quote_regex);
quotes = 0;
/* While we have more quotes, ... */
while (i !== -1) {
quotes++;
/* Only parse the first 50 quotes... srsly */
if (quotes > 50) {
break;
}
/* Text preceding the quote? add it to textbits first */
if (i > 0) {
t = splicer.substr(0, i);
let diffed = fixup_diffs(cut_trailer(t));
if (isArray(diffed)) { for(var z = 0; z < diffed.length; z++) textbits.push(fixup_urls(diffed[z]));}
else textbits.push(fixup_urls(diffed));
splicer = splicer.substr(i);
}
/* Find the quote and cut it out as a div */
m = splicer.match(ponymail_quote_regex);
if (m) {
quote = m[0];
i = quote.length;
t = splicer.substr(0, i);
quote = quote.replace(/(>*\s*\r?\n)+$/g, "");
qdiv = new HTML('div', {
"class": "email_quote_parent"
}, [
new HTML('button', {
title: "Toggle quote",
onclick: "toggle_quote(this)"
}, new HTML('span', {class:'glyphicon glyphicon-comment'}, " ")), new HTML('br'), new HTML('blockquote', {
"class": "email_quote",
style: {
display: hideQuotes ? 'none' : 'block'
}
}, fixup_urls(quote))
]);
textbits.push(qdiv);
splicer = splicer.substr(i);
}
/* Find the next quotes */
i = splicer.search(ponymail_quote_regex);
}
/* push the remaining text into textbits */
let diffed = fixup_diffs(cut_trailer(splicer));
if (isArray(diffed)) { for(var z = 0; z < diffed.length; z++) diffed[z] = fixup_urls(diffed[z]);}
else diffed = fixup_urls(diffed);
textbits.push(new HTML('span', {}, diffed));
return textbits;
}
function toggle_quote(el) {
let quote = el.parentNode.childNodes[2];
if (quote.style.display != 'block') {
quote.style.display = 'block';
} else {
quote.style.display = 'none';
}
}
/******************************************
Fetched from source/composer.js
******************************************/
let mua_trigger = 'mailto:';
let mua_list = null;
let mua_mid = null;
let mua_headers = {};
function compose_send() {
let of = [];
for (let k in mua_headers) {
of.push(k + "=" + encodeURIComponent(mua_headers[k]));
}
// Push the subject and email body into the form data
of.push("subject=" + encodeURIComponent(document.getElementById('composer_subject').value));
of.push("body=" + encodeURIComponent(document.getElementById('composer_body').value));
if (ponymail_preferences.login && ponymail_preferences.login.alternates && document.getElementById('composer_alt')) {
of.push("alt=" + encodeURIComponent(document.getElementById('composer_alt').options[document.getElementById('composer_alt').selectedIndex].value));
}
let request = new XMLHttpRequest();
request.open("POST", "/api/compose.lua");
request.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
request.send(of.join("&")); // send email as a POST string
document.getElementById('composer_modal').style.display = 'none';
modal("Message dispatched!", "Your email has been sent. Depending on moderation rules, it may take a while before it shows up in the archives.", "help");
}
function compose_email(replyto, list) {
let email = null;
let loggedIn = (ponymail_preferences.login && ponymail_preferences.login.credentials) ? true : false;
if (replyto) email = full_emails[replyto||''];
let listname = list;
mua_headers = {};
if (email) {
listname = email.list_raw.replace(/[<>]/g, '').replace('.', '@', 1);
mua_trigger = mua_link(email);
if (email['message-id'] && email['message-id'].length > 0) mua_headers['in-reply-to'] = email['message-id'];
if (email['message-id'] && email['message-id'].length > 0) mua_headers.references = email['message-id'];
mua_headers.eid = email.mid;
} else {
mua_trigger = mua_link(null, listname);
}
mua_list = listname;
mua_headers.to = listname;
mua_mid = email ? email['message-id'] : null;
// Not logged in? MUA it is, then!
if (!loggedIn) {
if (email) {
let a = new HTML('a', {href: mua_trigger}, "Reply via your own email client");
let p = new HTML('p', {}, [
"In order to reply to emails using the web interface, you need to be ",
new HTML('a', {href: '/oauth.html'}, "logged in first"),
". You can however still reply to this email using your own email client: ",
a
]
);
composer("Reply to thread:", p);
return;
}
}
// Replying to an email and logged in?
let eml_subject = "";
let eml_body = "";
let eml_title = `Start a new thread on ${listname}:`;
if (email) {
eml_subject = "Re: " + email.subject;
eml_body = composer_re(email);
eml_title = `Reply to email on ${listname}:`;
}
let form = [];
form.push(new HTML('b', {}, "Sending as:"));
let s = new HTML('select', { id: 'composer_alt' });
s.inject(new HTML('option', {}, ponymail_preferences.login.credentials.email));
if (ponymail_preferences.login && ponymail_preferences.login.alternates) {
for (let z = 0; z < ponymail_preferences.login.alternates.length; z++) {
s.inject(new HTML('option', {}, ponymail_preferences.login.alternates[z]));
}
}
form.push(new HTML('br'));
form.push(s);
form.push(new HTML('br'));
form.push(new HTML('b', {}, "Subject:"));
form.push(new HTML('br'));
form.push(new HTML('input', {style: { width: '90%'}, id: 'composer_subject', type: 'text', value: eml_subject}));
form.push(new HTML('br'));
form.push(new HTML('b', {}, "Reply:"));
form.push(new HTML('br'));
let body = new HTML('textarea', {style: { width: '90%', minHeight: '400px'}, id: 'composer_body'}, eml_body);
form.push(body);
let btn = new HTML('button', { onclick: 'compose_send();'}, "Send reply");
form.push(btn);
form.push(" ");
form.push(new HTML('a', {href: mua_trigger, style: { marginLeft: '10px'}}, "Or compose via your own email client"));
composer(eml_title, form);
if (email) document.getElementById('composer_body').focus();
}
// Generic modal function
function composer(title, contents) {
let modal = document.getElementById('composer_modal');
if (modal == undefined) {
modal = new HTML('div', { id: 'composer_modal'}, [
new HTML('div', {id: 'composer_modal_content'}, [
new HTML('span', {id: 'composer_modal_close', onclick: 'document.getElementById("composer_modal").style.display = "none";'}, 'X'),
new HTML('h2', {id: 'composer_modal_title'}, title),
new HTML('div', {id: 'composer_modal_contents'}, contents)
])
]);
document.body.appendChild(modal);
} else {
document.getElementById('composer_modal_title').innerText = title;
document.getElementById('composer_modal_contents').innerHTML = '';
document.getElementById('composer_modal_contents').inject(contents||'');
}
modal.style.display = 'block';
}
// Constructor for email body in replies...
function composer_re(email) {
let lines = email.body.split(/\r?\n/);
for (let i = 0; i < lines.length; i++) {
lines[i] = '> ' + lines[i];
}
let re = `\n\nOn ${email.date} ${email.from.replace(/\s*<.+?>/, '')} wrote:\n`;
re += lines.join("\n");
return re;
}
// MUA mailto: link generator
function mua_link(email, xlist) {
if (!email && xlist) {
return `mailto:${xlist}?subject=Subject+goes+here`;
}
let eml_raw_short = composer_re(email);
let subject = "RE: " + email.subject||'';
let truncated = false;
let N = 16000; // Anything above 16K can cause namespace issues with links.
if (eml_raw_short.length > N) {
truncated = true;
eml_raw_short = eml_raw_short.substring(0, N) + "\n[message truncated...]";
}
let listname = email.list_raw.replace(/[<>]/g, '').replace('.', '@', 1);
let xlink = 'mailto:' + listname + "?subject=" + encodeURIComponent(subject) + "&amp;In-Reply-To=" + encodeURIComponent(email['message-id']) + "&body=" + encodeURIComponent(eml_raw_short);
return xlink;
}
/******************************************
Fetched from source/construct-thread.js
******************************************/
let current_open_email = null;
function expand_email_threaded(idx, flat) {
let placeholder = document.getElementById('email_%u'.format(idx));
if (placeholder) {
// Check if email is already visible - if so, hide it!
if (placeholder.style.display == 'block') {
console.log("Collapsing thread at index %u".format(idx));
placeholder.style.display = 'none';
current_email_idx = undefined;
return false;
}
current_email_idx = idx;
console.log("Expanding thread at index %u".format(idx));
placeholder.style.display = 'block';
// Check if we've already filled out the structure here
if (placeholder.getAttribute('data-filled') == 'yes') {
console.log("Already constructed this thread, bailing!");
} else {
// Construct the base scaffolding for all emails
let eml = flat ? current_json.emails[idx] : current_json.thread_struct[idx];
if (eml) {
current_open_email = eml.tid || eml.mid;
}
let thread = construct_thread(eml);
placeholder.inject(thread);
placeholder.setAttribute("data-filled", 'yes');
}
}
return false;
}
function construct_thread(thread, cid, nestlevel, included) {
// First call on a thread/email, this is indef.
// Use this to plop a scroll call when loaded
// to prevent weird cache-scrolling
let doScroll = false;
if (cid === undefined) {
doScroll = true;
}
included = included||[];
cid = (cid || 0) + 1;
nestlevel = (nestlevel||0) + 1;
let mw = calc_email_width();
let max_nesting = ponymail_max_nesting;
if (mw < 700) {
max_nesting = Math.floor(mw/250);
}
cid %= 5;
let color = ['286090', 'ccab0a', 'c04331', '169e4e', '6d4ca5'][cid];
let email = undefined;
if (nestlevel < max_nesting) {
email = new HTML('div', { class: 'email_wrapper', id: 'email_%s'.format(thread.tid||thread.id)});
if (chatty_layout) {
email.style.border = "none !important";
} else {
email.style.borderLeft = '3px solid #%s'.format(color);
}
} else {
email = new HTML('div', { class: 'email_wrapper_nonest', id: 'email_%s'.format(thread.tid||thread.id)});
}
let wrapper = new HTML('div', { class: 'email_inner_wrapper', id: 'email_contents_%s'.format(thread.tid||thread.id)});
email.inject(wrapper);
if (isArray(thread.children)) {
thread.children.sort((a,b) => a.epoch - b.epoch);
for (var i = 0; i < thread.children.length; i++) {
let reply = construct_thread(thread.children[i], cid, nestlevel, included);
cid++;
if (reply) {
email.inject(reply);
}
}
}
let tid = thread.tid||thread.id;
if (!included.includes(tid)) {
included.push(tid);
console.log("Loading email %s".format(tid));
GET("%sapi/email.lua?id=%s".format(apiURL, tid), render_email, {cached: true, scroll: doScroll, id: tid, div: wrapper});
}
return email;
}
// Singular thread construction via permalinks
function construct_single_thread(state, json) {
current_json = json;
if (json && json.error) {
modal("An error occured", "Sorry, we hit a snag while trying to load the email(s): \n\n%s".format(json.error), "error");
return;
}
let div = document.getElementById('emails');
div.innerHTML = "";
if (chatty_layout) {
let listname = json.thread.list_raw.replace(/[<>]/g, '').replace('.', '@', 1);
div.setAttribute("class", "email_placeholder_chatty");
div.inject(new HTML('h4', {class: 'chatty_title'}, json.emails[0].subject));
div.inject(new HTML('a', {href: 'list.html?%s'.format(listname), class: 'chatty_title'}, 'Posted to %s'.format(listname)));
} else {
div.setAttribute("class", "email_placeholder");
}
div.style.display = "block";
let thread = json.thread;
let email = construct_thread(thread);
div.inject(email);
}
/******************************************
Fetched from source/datepicker.js
******************************************/
var months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
var datepicker_spawner = null
var calendarpicker_spawner = null
var units = {
w: 'week',
d: 'day',
M: 'month',
y: 'year'
}
function fixupPicker(obj) {
obj.addEventListener("focus", function(event){
$('html').on('hide.bs.dropdown', function (e) {
return false;
});
});
obj.addEventListener("blur", function(event){
$('html').unbind('hide.bs.dropdown')
});
}
// makeSelect: Creates a <select> object with options
function makeSelect(options, id, selval) {
var sel = document.createElement('select')
sel.addEventListener("focus", function(event){
$('html').on('hide.bs.dropdown', function (e) {
return false;
});
});
sel.addEventListener("blur", function(event){
$('html').unbind('hide.bs.dropdown')
});
sel.setAttribute("name", id)
sel.setAttribute("id", id)
// For each options element, create it in the DOM
for (var key in options) {
var opt = document.createElement('option')
// Hash or array?
if (typeof key == "string") {
opt.setAttribute("value", key)
// Option is selected by default?
if (key == selval) {
opt.setAttribute("selected", "selected")
}
} else {
// Option is selected by default?
if (options[key] == selval) {
opt.setAttribute("selected", "selected")
}
}
opt.text = options[key]
sel.appendChild(opt)
}
return sel
}
// splitDiv: Makes a split div with 2 elements,
// and puts div2 into the right column,
// and 'name' as text in the left one.
function splitDiv(id, name, div2) {
var div = document.createElement('div')
var subdiv = document.createElement('div')
var radio = document.createElement('input')
radio.setAttribute("type", "radio")
radio.setAttribute("name", "datepicker_radio")
radio.setAttribute("value", name)
radio.setAttribute("id", "datepicker_radio_" + id)
radio.setAttribute("onclick", "calcTimespan('"+ id + "')")
var label = document.createElement('label')
label.innerHTML = "&nbsp; " + name + ": "
label.setAttribute("for", "datepicker_radio_" + id)
subdiv.appendChild(radio)
subdiv.appendChild(label)
subdiv.style.float = "left"
div2.style.float = "left"
subdiv.style.width = "120px"
subdiv.style.height = "48px"
div2.style.height = "48px"
div2.style.width = "250px"
div.appendChild(subdiv)
div.appendChild(div2)
return div
}
// calcTimespan: Calculates the value and representational text
// for the datepicker choice and puts it in the datepicker's
// spawning input/select element.
function calcTimespan(what) {
var wat = ""
var tval = ""
// Less than N units ago?
if (what == 'lt') {
// Get unit and how many units
var N = document.getElementById('datepicker_lti').value
var unit = document.getElementById('datepicker_lts').value
var unitt = units[unit]
if (parseInt(N) != 1) {
unitt += "s"
}
// If this makes sense, construct a humanly readable and a computer version
// of the timespan
if (N.length > 0) {
wat = "Less than " + N + " " + unitt + " ago"
tval = "lte=" + N + unit
}
}
// More than N units ago?
if (what == 'mt') {
// As above, get unit and no of units.
var N = document.getElementById('datepicker_mti').value
var unit = document.getElementById('datepicker_mts').value
var unitt = units[unit]
if (parseInt(N) != 1) {
unitt += "s"
}
// construct timespan val + description
if (N.length > 0) {
wat = "More than " + N + " " + unitt + " ago"
tval = "gte=" + N + unit
}
}
// Date range?
if (what == 'cd') {
// Get From and To values
var f = document.getElementById('datepicker_cfrom').value
var t = document.getElementById('datepicker_cto').value
// construct timespan val + description if both from and to are valid
if (f.length > 0 && t.length > 0) {
wat = "From " + f + " to " + t
tval = "dfr=" + f + "|dto=" + t
}
}
// If we calc'ed a value and spawner exists, update its key/val
if (datepicker_spawner && what && wat.length > 0) {
document.getElementById('datepicker_radio_' + what).checked = true
if (datepicker_spawner.options) {
datepicker_spawner.options[0].value = tval
datepicker_spawner.options[0].text = wat
} else if (datepicker_spawner.value) {
datepicker_spawner.value = wat
datepicker_spawner.setAttribute("data", tval)
}
}
}
// datePicker: spawns a date picker with various
// timespan options right next to the parent caller.
function datePicker(parent, seedPeriod) {
datepicker_spawner = parent
var div = document.getElementById('datepicker_popup')
// If the datepicker object doesn't exist, spawn it
if (!div) {
div = document.createElement('div')
var id = parseInt(Math.random() * 10000).toString(16)
div.setAttribute("id", "datepicker_popup")
div.setAttribute("class", "datepicker")
}
// Reset the contents of the datepicker object
div.innerHTML = ""
div.style.display = "block"
// Position the datepicker next to whatever called it
var bb = parent.getBoundingClientRect()
div.style.top = (bb.bottom + 8) + "px"
div.style.left = (bb.left + 32) + "px"
// -- Less than N $units ago
var ltdiv = document.createElement('div')
var lti = document.createElement('input')
lti.setAttribute("id", "datepicker_lti")
lti.style.width = "48px"
lti.setAttribute("onkeyup", "calcTimespan('lt')")
lti.setAttribute("onblur", "calcTimespan('lt')")
ltdiv.appendChild(lti)
var lts = makeSelect({
'd': "Day(s)",
'w': 'Week(s)',
'M': "Month(s)",
'y': "Year(s)"
}, 'datepicker_lts', 'm')
lts.setAttribute("onchange", "calcTimespan('lt')")
ltdiv.appendChild(lts)
ltdiv.appendChild(document.createTextNode(' ago'))
div.appendChild(splitDiv('lt', 'Less than', ltdiv))
// -- More than N $units ago
var mtdiv = document.createElement('div')
var mti = document.createElement('input')
mti.style.width = "48px"
mti.setAttribute("id", "datepicker_mti")
mti.setAttribute("onkeyup", "calcTimespan('mt')")
mti.setAttribute("onblur", "calcTimespan('mt')")
mtdiv.appendChild(mti)
var mts = makeSelect({
'd': "Day(s)",
'w': 'Week(s)',
'M': "Month(s)",
'y': "Year(s)"
}, 'datepicker_mts', 'm')
mtdiv.appendChild(mts)
mts.setAttribute("onchange", "calcTimespan('mt')")
mtdiv.appendChild(document.createTextNode(' ago'))
div.appendChild(splitDiv('mt', 'More than', mtdiv))
// -- Calendar timespan
// This is just two text fields, the calendarPicker sub-plugin populates them
var cdiv = document.createElement('div')
var cfrom = document.createElement('input')
cfrom.style.width = "90px"
cfrom.setAttribute("id", "datepicker_cfrom")
cfrom.setAttribute("onfocus", "showCalendarPicker(this)")
cfrom.setAttribute("onchange", "calcTimespan('cd')")
cdiv.appendChild(document.createTextNode('From: '))
cdiv.appendChild(cfrom)
var cto = document.createElement('input')
cto.style.width = "90px"
cto.setAttribute("id", "datepicker_cto")
cto.setAttribute("onfocus", "showCalendarPicker(this)")
cto.setAttribute("onchange", "calcTimespan('cd')")
cdiv.appendChild(document.createTextNode('To: '))
cdiv.appendChild(cto)
div.appendChild(splitDiv('cd', 'Date range', cdiv))
// -- Magic button that sends the timespan back to the caller
var okay = document.createElement('input')
okay.setAttribute("type", "button")
okay.setAttribute("value", "Okay")
okay.setAttribute("onclick", "setDatepickerDate()")
div.appendChild(okay)
parent.parentNode.appendChild(div)
document.body.setAttribute("onclick", "")
window.setTimeout(function() { document.body.setAttribute("onclick", "blurDatePicker(event)") }, 200)
lti.focus()
// This is for recalcing the set options if spawned from a
// select/input box with an existing value derived from an
// earlier call to datePicker
var ptype = ""
var pvalue = parent.hasAttribute("data") ? parent.getAttribute("data") : parent.value
if (pvalue.search(/=|-/) != -1) {
// Less than N units ago?
if (pvalue.match(/lte/)) {
var m = pvalue.match(/lte=(\d+)([dMyw])/)
ptype = 'lt'
if (m) {
document.getElementById('datepicker_lti').value = m[1]
var sel = document.getElementById('datepicker_lts')
for (var i in sel.options) {
if (parseInt(i) >= 0) {
if (sel.options[i].value == m[2]) {
sel.options[i].selected = "selected"
} else {
sel.options[i].selected = null
}
}
}
}
}
// More than N units ago?
if (pvalue.match(/gte/)) {
ptype = 'mt'
var m = pvalue.match(/gte=(\d+)([dMyw])/)
if (m) {
document.getElementById('datepicker_mti').value = m[1]
var sel = document.getElementById('datepicker_mts')
// Go through the unit values, select the one we use
for (var i in sel.options) {
if (parseInt(i) >= 0) {
if (sel.options[i].value == m[2]) {
sel.options[i].selected = "selected"
} else {
sel.options[i].selected = null
}
}
}
}
}
// Date range?
if (pvalue.match(/dfr/)) {
ptype = 'cd'
// Make sure we have both a dfr and a dto here, catch them
var mf = pvalue.match(/dfr=(\d+-\d+-\d+)/)
var mt = pvalue.match(/dto=(\d+-\d+-\d+)/)
if (mf && mt) {
// easy peasy, just set two text fields!
document.getElementById('datepicker_cfrom').value = mf[1]
document.getElementById('datepicker_cto').value = mt[1]
}
}
// Month??
if (pvalue.match(/(\d{4})-(\d+)/)) {
ptype = 'cd'
// Make sure we have both a dfr and a dto here, catch them
var m = pvalue.match(/(\d{4})-(\d+)/)
if (m.length == 3) {
// easy peasy, just set two text fields!
var dfrom = new Date(parseInt(m[1]),parseInt(m[2])-1,1, 0, 0, 0)
var dto = new Date(parseInt(m[1]),parseInt(m[2]),0, 23, 59, 59)
document.getElementById('datepicker_cfrom').value = m[0] + "-" + dfrom.getDate()
document.getElementById('datepicker_cto').value = m[0] + "-" + dto.getDate()
}
}
calcTimespan(ptype)
}
}
function datePickerValue(seedPeriod) {
// This is for recalcing the set options if spawned from a
// select/input box with an existing value derived from an
// earlier call to datePicker
var ptype = ""
var rv = seedPeriod
if (seedPeriod && seedPeriod.search && seedPeriod.search(/=|-/) != -1) {
// Less than N units ago?
if (seedPeriod.match(/lte/)) {
var m = seedPeriod.match(/lte=(\d+)([dMyw])/)
ptype = 'lt'
var unitt = units[m[2]]
if (parseInt(m[1]) != 1) {
unitt += "s"
}
rv = "Less than " + m[1] + " " + unitt + " ago"
}
// More than N units ago?
if (seedPeriod.match(/gte/)) {
ptype = 'mt'
var m = seedPeriod.match(/gte=(\d+)([dMyw])/)
var unitt = units[m[2]]
if (parseInt(m[1]) != 1) {
unitt += "s"
}
rv = "More than " + m[1] + " " + unitt + " ago"
}
// Date range?
if (seedPeriod.match(/dfr/)) {
ptype = 'cd'
var mf = seedPeriod.match(/dfr=(\d+-\d+-\d+)/)
var mt = seedPeriod.match(/dto=(\d+-\d+-\d+)/)
if (mf && mt) {
rv = "From " + mf[1] + " to " + mt[1]
}
}
// Month??
if (seedPeriod.match(/^(\d+)-(\d+)$/)) {
ptype = 'mr' // just a made up thing...(month range)
var mr = seedPeriod.match(/(\d+)-(\d+)/)
if (mr) {
dfrom = new Date(parseInt(mr[1]),parseInt(mr[2])-1,1, 0, 0, 0)
rv = months[dfrom.getMonth()] + ', ' + mr[1]
}
}
}
return rv
}
function datePickerDouble(seedPeriod) {
// This basically takes a date-arg and doubles it backwards
// so >=3M becomes =>6M etc. Also returns the cutoff for
// the original date and the span in days of the original
var ptype = ""
var rv = seedPeriod
var dbl = seedPeriod
var tspan = 1
var dfrom = new Date()
var dto = new Date()
// datepicker range?
if (seedPeriod && seedPeriod.search && seedPeriod.search(/=/) != -1) {
// Less than N units ago?
if (seedPeriod.match(/lte/)) {
var m = seedPeriod.match(/lte=(\d+)([dMyw])/)
ptype = 'lt'
rv = "<" + m[1] + m[2] + " ago"
dbl = "lte=" + (parseInt(m[1])*2) + m[2]
// N months ago
if (m[2] == "M") {
dfrom.setMonth(dfrom.getMonth()-parseInt(m[1]), dfrom.getDate())
}
// N days ago
if (m[2] == "d") {
dfrom.setDate(dfrom.getDate()-parseInt(m[1]))
}
// N years ago
if (m[2] == "y") {
dfrom.setYear(dfrom.getFullYear()-parseInt(m[1]))
}
// N weeks ago
if (m[2] == "w") {
dfrom.setDate(dfrom.getDate()-(parseInt(m[1])*7))
}
// Calc total duration in days for this time span
tspan = parseInt((dto.getTime() - dfrom.getTime() + 5000) / (1000*86400))
}
// More than N units ago?
if (seedPeriod.match(/gte/)) {
ptype = 'mt'
var m = seedPeriod.match(/gte=(\d+)([dMyw])/)
rv = ">" + m[1] + m[2] + " ago"
dbl = "gte=" + (parseInt(m[1])*2) + m[2]
tspan = parseInt(parseInt(m[1]) * 30.4)
dfrom = null
// Months
if (m[2] == "M") {
dto.setMonth(dto.getMonth()-parseInt(m[1]), dto.getDate())
}
// Days
if (m[2] == "d") {
dto.setDate(dto.getDate()-parseInt(m[1]))
}
// Years
if (m[2] == "y") {
dto.setYear(dto.getFullYear()-parseInt(m[1]))
}
// Weeks
if (m[2] == "w") {
dto.setDate(dto.getDate()-(parseInt(m[1])*7))
}
// Can't really figure out a timespan for this, so...null!
// This also sort of invalidates use on the trend page, but meh..
tspan = null
}
// Date range?
if (seedPeriod.match(/dfr/)) {
ptype = 'cd'
// Find from and to
var mf = seedPeriod.match(/dfr=(\d+)-(\d+)-(\d+)/)
var mt = seedPeriod.match(/dto=(\d+)-(\d+)-(\d+)/)
if (mf && mt) {
rv = "from " + mf[1] + " to " + mt[1]
// Starts at 00:00:00 on from date
dfrom = new Date(parseInt(mf[1]),parseInt(mf[2])-1,parseInt(mf[3]), 0, 0, 0)
// Ends at 23:59:59 on to date
dto = new Date(parseInt(mt[1]),parseInt(mt[2])-1,parseInt(mt[3]), 23, 59, 59)
// Get duration in days, add 5 seconds to we can floor the value and get an integer
tspan = parseInt((dto.getTime() - dfrom.getTime() + 5000) / (1000*86400))
// double the distance
var dpast = new Date(dfrom)
dpast.setDate(dpast.getDate() - tspan)
dbl = seedPeriod.replace(/dfr=[^|]+/, "dfr=" + (dpast.getFullYear()) + '-' + (dpast.getMonth()+1) + '-' + dpast.getDate())
} else {
tspan = 0
}
}
}
// just N days?
else if (parseInt(seedPeriod).toString() == seedPeriod.toString()) {
tspan = parseInt(seedPeriod)
dfrom.setDate(dfrom.getDate() - tspan)
dbl = "lte=" + (tspan*2) + "d"
}
// Specific month?
else if (seedPeriod.match(/^(\d+)-(\d+)$/)) {
// just a made up thing...(month range)
ptype = 'mr'
var mr = seedPeriod.match(/(\d+)-(\d+)/)
if (mr) {
rv = seedPeriod
// Same as before, start at 00:00:00
dfrom = new Date(parseInt(mr[1]),parseInt(mr[2])-1,1, 0, 0, 0)
// end at 23:59:59
dto = new Date(parseInt(mr[1]),parseInt(mr[2]),0, 23, 59, 59)
// B-A, add 5 seconds so we can floor the no. of days into an integer neatly
tspan = parseInt((dto.getTime() - dfrom.getTime() + 5000) / (1000*86400))
// Double timespan
var dpast = new Date(dfrom)
dpast.setDate(dpast.getDate() - tspan)
dbl = "dfr=" + (dpast.getFullYear()) + '-' + (dpast.getMonth()+1) + '-' + dpast.getDate() + "|dto=" + (dto.getFullYear()) + '-' + (dto.getMonth()+1) + '-' + dto.getDate()
} else {
tspan = 0
}
}
return [dbl, dfrom, dto, tspan]
}
// set date in caller and hide datepicker again.
function setDatepickerDate() {
calcTimespan()
blurDatePicker()
}
// findParent: traverse DOM and see if we can find a parent to 'el'
// called 'name'. This is used for figuring out whether 'el' has
// lost focus or not.
function findParent(el, name) {
if (el.getAttribute && el.getAttribute("id") == name) {
return true
}
if (el.parentNode && el.parentNode.getAttribute) {
if (el.parentNode.getAttribute("id") != name) {
return findParent(el.parentNode, name)
} else {
return true
}
} else {
return false;
}
}
// function for hiding the date picker
function blurDatePicker(evt) {
var es = evt ? (evt.target || evt.srcElement) : null;
if ((!es || !es.parentNode || (!findParent(es, "datepicker_popup") && !findParent(es, "calendarpicker_popup"))) && !(es ? es : "null").toString().match(/javascript:void/)) {
document.getElementById('datepicker_popup').style.display = "none"
$('html').trigger('hide.bs.dropdown')
}
}
// draws the actual calendar inside a calendarPicker object
function drawCalendarPicker(obj, date) {
obj.focus()
// Default to NOW for calendar.
var now = new Date()
// if called with an existing date (YYYY-MM-DD),
// convert it to a JS date object and use that for
// rendering the calendar
if (date) {
var ar = date.split(/-/)
now = new Date(ar[0],parseInt(ar[1])-1,ar[2])
}
var days = ['Mon','Tue','Wed','Thu','Fri','Sat','Sun']
var mat = now
// Go to first day of the month
mat.setDate(1)
obj.innerHTML = "<h3>" + months[mat.getMonth()] + ", " + mat.getFullYear() + ":</h3>"
var tm = mat.getMonth()
// -- Nav buttons --
// back-a-year button
var a = document.createElement('a')
fixupPicker(a)
a.setAttribute("onclick", "drawCalendarPicker(this.parentNode, '" + (mat.getFullYear()-1) + '-' + (mat.getMonth()+1) + '-' + mat.getDate() + "');")
a.setAttribute("href", "javascript:void(0);")
a.innerHTML = "≪"
obj.appendChild(a)
// back-a-month button
a = document.createElement('a')
fixupPicker(a)
a.setAttribute("onclick", "drawCalendarPicker(this.parentNode, '" + mat.getFullYear() + '-' + (mat.getMonth()) + '-' + mat.getDate() + "');")
a.setAttribute("href", "javascript:void(0);")
a.innerHTML = "&lt;"
obj.appendChild(a)
// forward-a-month button
a = document.createElement('a')
fixupPicker(a)
a.setAttribute("onclick", "drawCalendarPicker(this.parentNode, '" + mat.getFullYear() + '-' + (mat.getMonth()+2) + '-' + mat.getDate() + "');")
a.setAttribute("href", "javascript:void(0);")
a.innerHTML = "&gt;"
obj.appendChild(a)
// forward-a-year button
a = document.createElement('a')
fixupPicker(a)
a.setAttribute("onclick", "drawCalendarPicker(this.parentNode, '" + (mat.getFullYear()+1) + '-' + (mat.getMonth()+1) + '-' + mat.getDate() + "');")
a.setAttribute("href", "javascript:void(0);")
a.innerHTML = "≫"
obj.appendChild(a)
obj.appendChild(document.createElement('br'))
// Table containing the dates of the selected month
var table = document.createElement('table')
table.setAttribute("border", "1")
table.style.margin = "0 auto"
// Add header day names
var tr = document.createElement('tr');
for (var m = 0; m < 7; m++) {
var td = document.createElement('th')
td.innerHTML = days[m]
tr.appendChild(td)
}
table.appendChild(tr)
// Until we hit the first day in a month, add blank days
tr = document.createElement('tr');
var weekday = mat.getDay()
if (weekday == 0) {
weekday = 7
}
weekday--;
for (var i = 0; i < weekday; i++) {
var td = document.createElement('td')
tr.appendChild(td)
}
// While still in this month, add day then increment date by 1 day.
while (mat.getMonth() == tm) {
weekday = mat.getDay()
if (weekday == 0) {
weekday = 7
}
weekday--;
if (weekday == 0) {
table.appendChild(tr)
tr = document.createElement('tr');
}
td = document.createElement('td')
// onclick for setting the calendarPicker's parent to this val.
td.setAttribute("onclick", "setCalendarDate('" + mat.getFullYear() + '-' + (mat.getMonth()+1) + '-' + mat.getDate() + "');")
td.innerHTML = mat.getDate()
mat.setDate(mat.getDate()+1)
tr.appendChild(td)
}
table.appendChild(tr)
obj.appendChild(table)
}
// callback for datePicker; sets the cd value to what date was picked
function setCalendarDate(what) {
$('html').on('hide.bs.dropdown', function (e) {
return false;
});
setTimeout(function() { $('html').unbind('hide.bs.dropdown');}, 250);
calendarpicker_spawner.value = what
var div = document.getElementById('calendarpicker_popup')
div.parentNode.focus()
div.style.display = "none"
calcTimespan('cd')
}
// caller for when someone clicks on a calendarPicker enabled field
function showCalendarPicker(parent, seedDate) {
calendarpicker_spawner = parent
// If supplied with a YYYY-MM-DD date, use this to seed the calendar
if (!seedDate) {
var m = parent.value.match(/(\d+-\d+(-\d+)?)/)
if (m) {
seedDate = m[1]
}
}
// Show or create the calendar object
var div = document.getElementById('calendarpicker_popup')
if (!div) {
div = document.createElement('div')
div.setAttribute("id", "calendarpicker_popup")
div.setAttribute("class", "calendarpicker")
document.getElementById('datepicker_popup').appendChild(div)
div.innerHTML = "Calendar goes here..."
}
div.style.display = "block"
var bb = parent.getBoundingClientRect()
// Align with the calling object, slightly below
div.style.top = (bb.bottom + 8) + "px"
div.style.left = (bb.right - 32) + "px"
drawCalendarPicker(div, seedDate)
}
/******************************************
Fetched from source/init.js
******************************************/
var ponymail_version = "1.0.1-Foal" // Current version of Pony Mail
var ponymail_name = "Pony Mail" // name of archive (set to "Foo's mail archive" or whatever)
var hits_per_page = 10;
var apiURL = ''; // external API URL. Usually left blank.
// Stuff regarding what we're doing right now
var current_json = {};
var current_state = {};
var current_list = '';
var current_domain = '';
var current_year = 0;
var current_month = 0;
var current_quick_search = '';
var select_primed = false;
var ponymail_preferences = {};
var ponymail_search_list = 'this';
var current_listmode = 'threaded';
var ponymail_max_nesting = 10; // max nesting level before unthreading to save space
// thread state
var current_email_idx = undefined;
var chatty_layout = true;
var ponymail_date_format = { weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'};
var virtual_inbox_loading = false;
var collated_json = {};
// List auto-picker criteria
var boring_lists = ['commits', 'cvs', 'site-cvs', 'security', 'notifications']; // we'd rather not default to these, noisy!
var favorite_list = 'dev'; // if we have this, default to it
var long_tabs = false; // tab name format (long or short)
console.log("/******* Apache Pony Mail (Foal v/%s) Initializing ********/".format(ponymail_version))
// Adjust titles:
document.title = ponymail_name;
let titles = document.getElementsByClassName("title");
for (var i in titles) {
titles[i].innerText = ponymail_name;
}
// check local storage for settings
console.log("Checking localStorage availability");
var can_store = false;
if (window.localStorage && window.localStorage.setItem) {
try {
window.localStorage.setItem("ponymail_test", "foo");
can_store = true;
console.log("localStorage available!");
} catch (e) {
console.log("no localStorage available!");
}
}
console.log("Initializing escrow checks");
window.setInterval(escrow_check, 250);
console.log("Initializing key command logger");
window.addEventListener('keyup', keyCommands);
if (pm_config && pm_config.apiURL) {
apiURL = pm_config.apiURL;
console.log("Setting API URL to %s".format(apiURL));
}
window.addEventListener('load',function() {
document.body.appendChild(new HTML('footer', {class: 'footer'}, [
new HTML('div', { class: 'container'}, [
new HTML('p', {class: 'muted'}, "Powered by Apache Pony Mail (Foal v/%s)".format(ponymail_version))
]
)
])
);
}
);
console.log("initializing pop state checker");
window.onpopstate = function(event) {
console.log("Popping state");
return parseURL({cached: true});
};
/******************************************
Fetched from source/key-commands.js
******************************************/
// Generic modal function
function modal(title, msg, type, isHTML) {
let modal = document.getElementById('modal');
let text = document.getElementById('modal_text');
if (modal == undefined) {
text = new HTML('p', {id: 'modal_text'}, "");
modal = new HTML('div', { id: 'modal'}, [
new HTML('div', {id: 'modal_content'}, [
new HTML('span', {id: 'modal_close', onclick: 'document.getElementById("modal").style.display = "none";'}, 'X'),
new HTML('h2', {id: 'modal_title'}, title),
new HTML('div', {}, text)
])
]);
document.body.appendChild(modal);
}
if (type) {
modal.setAttribute("class", "modal_" + type);
} else {
modal.setAttribute("class", undefined);
}
modal.style.display = 'block';
document.getElementById('modal_title').innerText = title;
// If we trust HTML, use it. Otherwise only show as textNode.
if (isHTML) {
text.innerHTML = msg;
} else {
msg = msg.replace(/<.*?>/g, ""); // strip HTML tags
text.innerText = msg;
}
}
// Helper for determining if an email is open or not...
function anyOpen() {
let open = (current_email_idx !== undefined) ? true: false;
console.log("Emails open? " + open);
return open;
}
// Helper function for hiding windows and open tabs
// Hide previous action on first escape, hide everything on second escape
function hideWindows(force_all) {
// First, check if we want to hide a modal
let modal = document.getElementById('modal');
if (modal && modal.style.display == 'block') {
modal.style.display = 'none';
if (force_all !== true) return;
}
// RThen, check if we want to hide a composer modal
let cmodal = document.getElementById('composer_modal');
if (cmodal && cmodal.style.display == 'block') {
cmodal.style.display = 'none';
if (force_all !== true) return;
}
// Check Advanced Search
let as = document.getElementById('advanced_search');
if (as && as.style.display == 'block') {
as.style.display = 'none';
if (force_all !== true) return;
}
// Check for individually opened email
if (current_email_idx !== undefined) {
console.log("Hiding placeholder at index %u".format(current_email_idx));
let placeholder = document.getElementById('email_%u'.format(current_email_idx));
if (placeholder) {
placeholder.style.display = 'none';
}
current_email_idx = undefined; // undef this even if we can't find the email
if (force_all !== true) return;
}
// if viewing a single thread, disregard the collapses below - the won't make sense!
if (location.href.match(/thread\.html/)) return;
// Finally, check for other opened emails, close 'em all
let placeholders = document.getElementsByClassName('email_placeholder');
for (var i = 0; i < placeholders.length; i++) {
if (placeholders[i].style.display == 'block') {
console.log("Hiding placeholder %s".format(placeholders[i].getAttribute('id')));
placeholders[i].style.display = 'none';
// Reset scroll cache
try { window.scrollTo(0,0);} catch (e) {}
}
}
placeholders = document.getElementsByClassName('email_placeholder_chatty');
for (var i = 0; i < placeholders.length; i++) {
if (placeholders[i].style.display == 'block') {
console.log("Hiding placeholder %s".format(placeholders[i].getAttribute('id')));
placeholders[i].style.display = 'none';
// Reset scroll cache
try { window.scrollTo(0,0);} catch (e) {}
}
}
}
// Show keyboard commands
function showHelp() {
modal("Keyboard shortcuts:", "<pre><kbd>H</kbd>: Show this help window.\n<kbd>C</kbd>: Compose a new email to this list.\n<kbd>R</kbd>: Reply to the currently active thread.\n<kbd>S</kbd>: Go to the search bar.\n<kbd>Escape</kbd>: Hide modals or collapse threads.\n<kbd>RightArrow</kbd>: Go to next bunch of emails in list view.\n<kbd>LeftArrow</kbd>: Go to previous bunch of emails in list view.</pre>", "help", true);
}
// Function for capturing and evaluating key strokes
// If it matches a known shortcut, execute that then..
function keyCommands(e) {
if (!e.ctrlKey) {
// Get calling element and its type
let target = e.target || e.srcElement;
let type = target.tagName;
// We won't jump out of an input field!
if (['INPUT', 'TEXTAREA', 'SELECT'].has(type)) {
return;
}
switch (e.key) {
case 's':
document.getElementById('q').focus();
return;
case 'h':
showHelp();
return;
case 'c':
compose_email(null, `${current_list}@${current_domain}`);
return;
case 'r':
console.log(current_open_email);
if (current_open_email && full_emails[current_open_email]) {
compose_email(current_open_email);
}
return;
case 'Escape':
hideWindows();
return;
case 'ArrowRight': // quick-next
if (current_json) { // IF list view...
let blobs = current_json.emails;
if (current_listmode == 'threaded') blobs = current_json.thread_struct;
let no_emails = blobs.length;
if (current_email_idx == undefined && current_json && (current_index_pos + current_per_page) < no_emails) {
listview_header({pos: current_index_pos + current_per_page}, current_json);
}
}
return;
case 'ArrowLeft': // quick previous
if (current_json) { // IF list view...
let blobs = current_json.emails;
if (current_listmode == 'threaded') blobs = current_json.thread_struct;
let no_emails = blobs.length;
if (current_email_idx == undefined && current_json && (current_index_pos - current_per_page) >= 0) {
listview_header({pos: current_index_pos - current_per_page}, current_json);
}
}
return;
}
}
}
// swipe left/right for next/previous page on mobile
function ponymail_swipe(event) {
// Only accept "big" swipes
let len = Math.abs(event.swipestart.coords[0] - event.swipestop.coords[ 0 ]);
let direction = event.swipestart.coords[0] > event.swipestop.coords[ 0 ] ? 'left' : 'right';
console.log("swipe %s of %u pixels detected".format(direction, len));
if (len < 20) return false;
if (direction == 'right') {
if (current_json) { // IF list view...
let blobs = current_json.emails;
if (current_listmode == 'threaded') blobs = current_json.thread_struct;
let no_emails = blobs.length;
if (current_email_idx == undefined && current_json && (current_index_pos - current_per_page) >= 0) {
listview_header({pos: current_index_pos - current_per_page}, current_json);
}
}
}
else if (direction == 'left') {
if (current_json) { // IF list view...
let blobs = current_json.emails;
if (current_listmode == 'threaded') blobs = current_json.thread_struct;
let no_emails = blobs.length;
if (current_email_idx == undefined && current_json && (current_index_pos + current_per_page) < no_emails) {
listview_header({pos: current_index_pos + current_per_page}, current_json);
}
}
}
return false;
}
/******************************************
Fetched from source/list-index.js
******************************************/
let list_json = {}
function list_index(state, json) {
if (json) {
list_json = json;
}
let lists = document.getElementById('list_picker_ul');
if (state && state.letter) {
let tab = undefined;
let tabs = lists.childNodes;
for (var i = 0; i < tabs.length; i++) {
let xtab = tabs[i];
if ((xtab.innerText == state.to || xtab.getAttribute('data-list') == state.to)) {
tab = xtab;
tab.setAttribute("class", 'active');
} else {
xtab.setAttribute("class", "");
}
}
return;
}
else {
let letters = 'abcdefghijklmnopqrstuvwxyz*';
for (var i = 0; i < letters.length; i++) {
let letter = letters[i].toUpperCase();
let li = new HTML('li', {onclick: 'switch_index({letter: "%s"});'.format(letter), class: (letter == 'A') ? 'active' :null}, letter);
lists.inject(li);
}
}
}
let preferred_lists = ['dev', 'users'];
let preferred_no_lists = ['security'];
function best_list(entries) {
let x = 0;
let pick = 'dev';
for (var key in entries) {
if (preferred_lists.has(key)) return key;
if (preferred_no_lists.has(key) && Object.keys(entries).length > 1) continue;
if (entries[key] > x) {
x = entries[key];
pick = key;
}
}
return pick;
}
function list_index_onepage(state, json) {
let obj = document.getElementById('list_index_child');
obj.style.padding = '8px';
let domains = Object.keys(json.lists);
domains.sort();
let letter = '';
for (var i = 0; i < domains.length; i++) {
let domain = domains[i];
let l = domain[0];
if (l != letter) {
letter = l;
let lhtml = new HTML('h2', {}, l.toUpperCase());
obj.inject(lhtml);
}
let a = new HTML('a', {href: 'list.html?%s@%s'.format(best_list(json.lists[domain]), domain)}, domain);
obj.inject(['- ', a]);
obj.inject(new HTML('br'));
}
}
function prime_list_index() {
GET('%sapi/preferences.lua'.format(apiURL), list_index_onepage, {});
}
/******************************************
Fetched from source/listview-flat.js
******************************************/
function calc_per_page() {
// Figure out how many emails per page
let body = document.body;
let html = document.documentElement;
let height = Math.max( body.scrollHeight,
html.clientHeight, html.scrollHeight);
let width = Math.max( body.scrollWidth,
html.clientWidth, html.scrollWidth);
let email_h = 40;
console.log("window area: %ux%u".format(width, height));
if (width < 600) { console.log("Using narrow view, halving emails per page..."); email_h = 80;}
height -= 180;
let per_page = Math.max(5, Math.floor(height / email_h));
per_page -= per_page % 5;
return per_page;
}
function listview_flat(json, start) {
let list = document.getElementById('emails');
list.innerHTML = "";
let per_page = calc_per_page();
let s = start || 0;
if (json.emails && json.emails.length) {
for (n = s; n < (s+per_page); n++ ) {
let z = json.emails.length - n - 1; // reverse order by default
if (json.emails[z]) {
let item = listview_flat_element(json.emails[z], z);
list.inject(item);
// Hidden placeholder for expanding email(s)
let placeholder = new HTML('div', {class: chatty_layout ? 'email_placeholder_chatty' : 'email_placeholder', id: 'email_%u'.format(z)});
list.inject(placeholder);
}
}
} else {
list.inject(txt("No emails found..."));
}
}
function listview_flat_element(eml, idx) {
let link_wrapper = new HTML('a', {href:'thread/%s'.format(eml.id), onclick:'return(expand_email_threaded(%u, true));'.format(idx)});
let element = new HTML('div', { class: "listview_email_flat"}, " ");
let date = new Date(eml.epoch*1000.0);
let now = new Date();
// Add gravatar
let gravatar = new HTML('img', { class:"gravatar", src: "https://secure.gravatar.com/avatar/%s.jpg?s=96&r=g&d=mm".format(eml.gravatar)});
element.inject(gravatar);
// Add author
let authorName = eml.from.replace(/\s*<.+>/, "").replace(/"/g, '');
let authorEmail = eml.from.match(/\s*<(.+@.+)>\s*/);
if (authorName.length == 0) authorName = authorEmail ? authorEmail[1] : "(No author?)";
let author = new HTML('span', { class: "listview_email_author"}, authorName);
element.inject(author);
// Combined space for subject + body teaser
let as = new HTML('div', {class: 'listview_email_as'});
let suba = new HTML('a', {}, eml.subject === '' ? '(No subject)' : eml.subject);
let subject = new HTML('div', {class: 'listview_email_subject email_unread'}, suba);
as.inject(subject);
let body = new HTML('div', {class: 'listview_email_body'}, eml.body);
as.inject(body);
element.inject(as);
// Labels
let labels = new HTML('div', {class: 'listview_email_labels'});
let dl = new HTML('span', { class: 'label label-default'}, date.ISOBare());
if (now - date < 86400000) {
dl.setAttribute("class", "label label-primary");
}
labels.inject(dl);
element.inject(labels);
link_wrapper.inject(element);
return link_wrapper;
}
/******************************************
Fetched from source/listview-header.js
******************************************/
let prev_listview_json = {};
let prev_listview_state = {};
let current_index_pos = 0;
let current_per_page = 0;
function listview_header(state, json) {
let list_title = json.list;
prev_listview_json = json;
prev_listview_state = state;
if (current_list == 'virtual' && current_domain == 'inbox') {
list_title = "Virtual inbox, past 30 days";
}
let blobs = json.emails;
if (current_listmode == 'threaded') blobs = json.thread_struct;
if (current_year && current_month) {
list_title += ", %s %u".format(months[current_month-1], current_year);
} else {
list_title += ", past month";
}
if (json.searchParams.q && json.searchParams.q.length || (json.searchParams.d||"").match(/=/)) {
list_title = "Custom search";
}
document.getElementById('listview_title').innerText = list_title + ":";
let download = new HTML('button', { title: 'Download as mbox archive', download: 'true'}, new HTML('span', {class: 'glyphicon glyphicon-save'}, " "));
document.getElementById('listview_title').inject(download);
download.addEventListener('click', () => {
dl_url = pm_config.apiURL + 'api/mbox.lua?';
for (let key in json.searchParams || {}) {
dl_url += key + "=" + encodeURIComponent(json.searchParams[key]) + "&";
}
location.href = dl_url;
});
let chevrons = document.getElementById('listview_chevrons');
let per_page = calc_per_page();
current_per_page = per_page;
current_index_pos = state.pos || 0;
let first = 1;
if (state && state.pos) {
first = 1 + state.pos;
}
chevrons.innerHTML = "Showing <b>%u through %u</b> of <b>%u</b> topics&nbsp;".format(first, Math.min(first + per_page - 1, blobs.length), blobs.length||0);
let pprev = Math.max(0, first - per_page - 1);
let cback = new HTML('button', { onclick: 'listview_header({pos: %u}, current_json);'.format(pprev), disabled: (first == 1) ? 'true': null}, new HTML('span', {class: 'glyphicon glyphicon-chevron-left'}, " "));
chevrons.inject(cback);
let pnext = first + per_page - 1;
let cforward = new HTML('button', { onclick: 'listview_header({pos: %u}, current_json);'.format(pnext), disabled: (first+per_page-1 >= blobs.length) ? 'true': null}, new HTML('span', {class: 'glyphicon glyphicon-chevron-right'}, " "));
chevrons.inject(cforward);
let crefresh = new HTML('button', { onclick: 'parseURL();', title: 'Refresh results', style: {marginLeft: '8px'}}, new HTML('span', {class: 'glyphicon glyphicon-refresh'}, " "));
chevrons.inject(crefresh);
if (state && state.pos != undefined) {
if (current_listmode == 'threaded') {
listview_threaded(json, state.pos);
} else {
listview_flat(json, state.pos);
}
}
let tm = document.getElementById('threaded_mobile_img');
if (tm) {
if (current_listmode == 'threaded') tm.setAttribute("src", "images/threading_enabled.png");
else tm.setAttribute("src", "images/threading_disabled.png");
}
}
function listview_list_lists(state, json) {
let lists = document.getElementById('list_picker_ul');
let searching = (state && state.search === true) ? true : false;
if (state && state.to) {
let tab = undefined;
let tabs = lists.childNodes;
for (var i = 0; i < tabs.length; i++) {
let xtab = tabs[i];
if ((state.to == 'search' && xtab.getAttribute('id') == 'tab_search') || (xtab.innerText == state.to || xtab.getAttribute('data-list') == state.to)) {
tab = xtab;
tab.setAttribute("class", state.to == 'search' ? 'search' : 'active');
} else if (xtab.getAttribute("class") != 'list_all_narrow' && xtab.getAttribute("class") != 'others') {
xtab.setAttribute("class", "");
}
}
return;
}
if (!json) {
json = ponymail_preferences;
}
if (lists) {
lists.innerHTML = "";
if (isHash(json.lists) && json.lists[current_domain]) {
lists_sorted = [];
for (var list in json.lists[current_domain]) {
lists_sorted.push([list, json.lists[current_domain][list]]);
}
lists_sorted.sort((a,b) => b[1] - a[1]);
let alists = [];
for (var i = 0; i < lists_sorted.length; i++) alists.push(lists_sorted[i][0]);
if (current_list != '*' && current_domain != '*') {
alists.remove(current_list);
alists.unshift(current_list);
}
let maxlists = (searching && 3 || 4);
for (var i = 0; i < alists.length; i++) {
if (i >= maxlists) break;
let listname = alists[i];
let listnametxt = listname;
if (long_tabs) { listnametxt = '%s@%s'.format(listname, current_domain);}
let li = new HTML('li', {onclick: 'switch_list(this, "tab");', class: (listname == current_list && !searching) ? 'active' :null}, listnametxt);
li.setAttribute("data-list", '%s@%s'.format(listname, current_domain));
lists.inject(li);
}
if (alists.length > maxlists) {
let other_lists_sorted = [];
for (var i = maxlists; i < alists.length; i++) {
other_lists_sorted.push(alists[i]);
}
other_lists_sorted.sort();
let li = new HTML('li', {class: 'others'});
let otherlists = new HTML('select', {class: 'listview_others', onchange: 'switch_list(this.value);'});
otherlists.inject(new HTML('option', { disabled: 'disabled', selected: 'selected'}, 'Other lists (%u):'.format(other_lists_sorted.length)));
li.inject(otherlists);
for (var i = 0; i < other_lists_sorted.length; i++) {
let listname = other_lists_sorted[i];
let opt = new HTML('option', {value: "%s@%s".format(listname, current_domain)}, listname);
otherlists.inject(opt);
}
lists.inject(li);
}
// All lists, for narrow UI
let all_lists_narrow = [];
for (var i = 0; i < alists.length; i++) {
all_lists_narrow.push(alists[i]);
}
all_lists_narrow.sort();
let li = new HTML('li', {class: 'list_all_narrow'});
let otherlists = new HTML('select', {class: 'listview_others', onchange: 'switch_list(this.value);'});
otherlists.inject(new HTML('option', { disabled: 'disabled', selected: 'selected'}, "%s@%s".format(current_list, current_domain)));
li.inject(otherlists);
for (var i = 0; i < all_lists_narrow.length; i++) {
let listname = all_lists_narrow[i];
let opt = new HTML('option', {value: "%s@%s".format(listname, current_domain)}, listname);
otherlists.inject(opt);
}
lists.inject(li);
}
}
if (searching) {
let li = new HTML('li', {onclick: 'switch_list(this, "tab");', id: 'tab_search', class: 'search'}, "Search: %s".format(state.query));
li.setAttribute("data-url", state.url);
li.setAttribute("data-href", location.href);
lists.inject(li);
}
// Populate the project selector
if (isHash(json.lists)) {
let no_projects = 0;
let select = document.getElementById('project_select');
if (!select || select_primed) return;
let opts = {}
let doms = [];
for (var domain in json.lists) {
let option = new HTML('option', { value: domain }, domain);
opts[domain] = option;
doms.push(domain);
no_projects++;
}
if (no_projects > 1 || current_domain == '*') {
select.innerHTML = "";
let title = new HTML('option', { disabled: 'disabled', selected: 'true', value: ''}, "Available projects (%u):".format(no_projects));
select.inject(title);
doms.sort();
for (var i = 0; i < doms.length; i++) {
select.inject(opts[doms[i]]);
}
select.style.display = "inline-block";
select_primed = true; // mark it primed so we don't generate it again later
}
}
}
function switch_project(domain) {
// TODO: improve this
if (ponymail_preferences && ponymail_preferences.lists[domain]) {
// Switch to the most populous, but not commits/cvs
let lists_sorted = [];
for (var list in ponymail_preferences.lists[domain]) {
lists_sorted.push([list, ponymail_preferences.lists[domain][list]]);
}
lists_sorted.sort((a,b) => b[1] - a[1]);
let lists = [];
for (var i = 0; i < lists_sorted.length; i++) lists.push(lists_sorted[i][0]);
let listname = lists[0];
let n = 1;
if (lists.length > n) {
while (boring_lists.has(listname) && lists.length > n) {
listname = lists[n];
n++;
}
if (lists.has(favorite_list)) {
listname = favorite_list;
}
}
switch_list('%s@%s'.format(listname, domain));
} else {
switch_list('dev@%s'.format(domain));
}
}
function switch_list(list, from) {
let listid = list;
if (typeof list == 'object') {
let dataURL = list.getAttribute('data-url');
if (dataURL) {
GET(dataURL, renderListView, {search: true, cached: true});
let newhref = list.getAttribute('data-href');
if (location.href !== newhref) {
window.history.pushState({}, null, newhref);
}
listview_list_lists({to: 'search'});
return;
}
listid = list.getAttribute("data-list") || list.innerText;
}
let bits = listid.split("@");
current_list = bits[0];
current_domain = bits[1];
current_year = 0;
current_month = 0;
let newhref = "list.html?%s".format(listid);
if (location.href !== newhref) {
window.history.pushState({}, null, newhref);
}
console.log("Switching list to %s...".format(listid));
listview_list_lists({to: from ? listid : undefined});
post_prime({cached: true, from: from});
}
window.addEventListener('orientationchange', function() {
window.setTimeout(function() {
if (anyOpen() == false) {
listview_header(prev_listview_state, prev_listview_json);
}
}, 100);
}, false);
/******************************************
Fetched from source/listview-threaded.js
******************************************/
function calc_email_width() {
// Figure out how many emails per page
let body = document.body;
let html = document.documentElement;
let width = Math.max( body.scrollWidth, body.offsetWidth,
html.clientWidth, html.scrollWidth, html.offsetWidth );
return width;
}
function listview_threaded(json, start) {
let list = document.getElementById('emails');
list.innerHTML = "";
let per_page = calc_per_page();
let s = start || 0;
if (json.thread_struct && json.thread_struct.length) {
for (n = s; n < (s+per_page); n++ ) {
let z = json.thread_struct.length - n - 1; // reverse order by default
if (json.thread_struct[z]) {
let item = listview_threaded_element(json.thread_struct[z], z);
list.inject(item);
// Hidden placeholder for expanding email(s)
let placeholder = new HTML('div', {class: chatty_layout ? 'email_placeholder_chatty' : 'email_placeholder', id: 'email_%u'.format(z)});
list.inject(placeholder);
}
}
} else {
list.inject(txt("No emails found..."));
}
}
function find_email(id) {
let json = current_json;
for (var i = 0; i < json.emails.length; i++) {
if (json.emails[i].id == id) return json.emails[i];
}
return null;
}
function count_replies(thread) {
let reps = 0;
if (isArray(thread.children)) {
for (let i = 0; i < thread.children.length; i++) {
if (thread.children[i].tid == thread.tid) reps--;
if (true) {
reps++;
reps += count_replies(thread.children[i]);
}
}
}
return reps;
}
function count_people(thread, hash) {
let ppl = hash || {};
let eml = find_email(thread.tid);
if (eml) ppl[eml.from] = true;
if (isArray(thread.children)) {
for (let i = 0; i < thread.children.length; i++) {
if (true) {
count_people(thread.children[i], ppl);
}
}
}
let n = 0;
for (var _ in ppl) n++;
return n;
}
function last_email(thread) {
let newest = thread.epoch;
if (isArray(thread.children)) {
for (let i = 0; i < thread.children.length; i++) {
newest = Math.max(newest, last_email(thread.children[i]));
}
}
return newest;
}
function listview_threaded_element(thread, idx) {
let eml = find_email(thread.tid);
if (!eml) { return; }
let link_wrapper = new HTML('a', {href:'thread/%s'.format(eml.id), onclick:'return(expand_email_threaded(%u));'.format(idx)});
let element = new HTML('div', { class: "listview_email_flat"}, " ");
let date = new Date(eml.epoch*1000.0);
let now = new Date();
// Add gravatar
let gravatar = new HTML('img', { class:"gravatar", src: "https://secure.gravatar.com/avatar/%s.jpg?s=96&r=g&d=mm".format(eml.gravatar)});
element.inject(gravatar);
// Add author
let authorName = eml.from.replace(/\s*<.+>/, "").replace(/"/g, '');
let authorEmail = eml.from.match(/\s*<(.+@.+)>\s*/);
if (authorName.length == 0) authorName = authorEmail ? authorEmail[1] : "(No author?)";
let author = new HTML('span', { class: "listview_email_author"}, authorName);
element.inject(author);
// If needed, inject ML name
if (current_domain == 'inbox' || current_list == '*') {
author.style.lineHeight = '16px';
author.inject(new HTML('br'));
author.inject(new HTML('span', { class: "label label-primary", style: "font-style: italic; font-size: 1rem;"}, eml.list_raw.replace(/[<>]/g, '').replace('.','@',1)));
}
// Combined space for subject + body teaser
let as = new HTML('div', {class: 'listview_email_as'});
let suba = new HTML('a', {}, eml.subject === '' ? '(No subject)' : eml.subject);
if (current_json.name == '*' || current_json.domain == '*') {
let kbd = new HTML('kbd', {class: 'listview_kbd'}, eml.list_raw.replace(/[<>]/g, '').replace('.','@',1))
suba = [kbd, suba];
}
let subject = new HTML('div', {class: 'listview_email_subject email_unread'}, suba);
as.inject(subject);
let body = new HTML('div', {class: 'listview_email_body'}, eml.body);
as.inject(body);
element.inject(as);
// Labels
let labels = new HTML('div', {class: 'listview_email_labels'});
// Participants
let ppl = count_people(thread);
let ptitle = (ppl == 1) ? "one participant" : "%u participants".format(ppl);
let people = new HTML('span', { class: 'label label-default', title: ptitle}, [
new HTML('span', { class: 'glyphicon glyphicon-user'}, ' '),
" %u".format(ppl)
]);
labels.inject(people);
// Replies
let reps = count_replies(thread);
let rtitle = (reps == 1) ? "one reply" : "%u replies".format(reps);
let repl = new HTML('span', { class: 'label label-default', title: rtitle}, [
new HTML('span', { class: 'glyphicon glyphicon-envelope'}, ' '),
" %u".format(reps)
]);
labels.inject(repl);
// Date
date = new Date(last_email(thread) * 1000.0);
let dl = new HTML('span', { class: 'label label-default'}, date.ISOBare());
if (now - date < 86400000) {
dl.setAttribute("class", "label label-primary");
}
labels.inject(dl);
element.inject(labels);
link_wrapper.inject(element);
return link_wrapper;
}
/******************************************
Fetched from source/preferences.js
******************************************/
// logout: log out a user
// call the logout URL, then refresh this page - much simple!
function logout() {
GET("/api/preferences.lua?logout=true", () => location.href = document.location);
}
function init_preferences(state, json) {
ponymail_preferences = json || {};
// First, load session local settings, if possible
if (can_store) {
let local_preferences = window.localStorage.getItem('ponymail_preferences');
if (local_preferences) {
ljson = JSON.parse(local_preferences);
if (ljson.chatty_layout !== undefined) {
chatty_layout = ljson.chatty_layout;
}
}
}
// color some links
let cl = document.getElementById('chatty_link');
if (cl) {
cl.setAttribute("class", chatty_layout ? "enabled" : "disabled");
}
if (ponymail_preferences.login && ponymail_preferences.login.credentials) {
let prefsmenu = document.getElementById('prefs_dropdown');
let uimg = document.getElementById('uimg');
uimg.setAttribute("src", "images/user.png");
uimg.setAttribute("title", "Logged in as %s".format(ponymail_preferences.login.credentials.fullname));
// Generate user menu
prefsmenu.innerHTML = "";
let logout = new HTML('a', { href: "javascript:void(logout());"}, "Log out");
let li = new HTML('li', {}, logout)
prefsmenu.inject(li);
} else {
let prefsmenu = document.getElementById('prefs_dropdown');
if (prefsmenu) {
prefsmenu.innerHTML = "";
let login = new HTML('a', { href: "javascript:location.href='oauth.html';"}, "Log In");
let li = new HTML('li', {}, login)
prefsmenu.inject(li);
}
}
if (json) {
listview_list_lists(state, json);
if (state && state.prime) {
// If lists is accessible, show it
if (json.lists[current_domain] && json.lists[current_domain][current_list] != undefined) {
post_prime(state);
} else {// otherwise, bork
if (current_list.length > 0 && (!json.lists[current_domain] || Object.keys(json.lists[current_domain]).length > 0)) {
let eml = document.getElementById('emails');
eml.innerText = "We couldn't find this list. It may not exist or require you to be logged in with specific credentials.";
eml.inject(new HTML('br'));
eml.inject(new HTML('a', {href: 'oauth.html', onclick:'location.href="oauth.html";'}, "Click here to log in via OAuth"));
} else {
console.log(current_domain);
let first_list = Object.keys(json.lists[current_domain])[0];
location.href = `?${first_list}@${current_domain}`;
}
}
}
}
}
function save_preferences() {
if (can_store) {
let ljson = {
chatty_layout: chatty_layout
};
let lstring = JSON.stringify(ljson);
window.localStorage.setItem('ponymail_preferences', lstring);
console.log("Saved local preferences");
}
}
function set_theme(theme) {
current_listmode = theme;
renderListView(current_state, current_json);
save_preferences();
}
function set_skin(skin) {
chatty_layout = !chatty_layout;
let cl = document.getElementById('chatty_link');
if (cl) {
cl.setAttribute("class", chatty_layout ? "enabled" : "disabled");
}
hideWindows(true);
renderListView(current_state, current_json);
save_preferences();
}
// set_skin, but for permalinks
function set_skin_permalink(skin) {
chatty_layout = !chatty_layout;
let cl = document.getElementById('chatty_link');
if (cl) {
cl.setAttribute("class", chatty_layout ? "enabled" : "disabled");
}
hideWindows(true);
save_preferences();
parse_permalink();
}
/******************************************
Fetched from source/primer.js
******************************************/
/* List View Rendering main func */
function renderListView(state, json) {
if (json) {
current_json = json;
}
current_state = state;
async_escrow['rendering'] = new Date();
if (!state || state.update_calendar !== false) {
renderCalendar(json.firstYear,json.firstMonth,json.lastYear, json.lastMonth);
}
// sort threads by date
if (isArray(json.thread_struct)) {
current_json.thread_struct.sort((a,b) => last_email(a) - last_email(b));
}
listview_header(state, json);
if (current_listmode == 'threaded') { listview_threaded(json, 0); }
else { listview_flat(json, 0);}
sidebar_stats(json); // This comes last, takes the longest with WC enabled.
delete async_escrow['rendering'];
if (state && state.to) {
listview_list_lists(state);
}
}
/* Primer function for List View
* Fetches the following:
* - user preferences (api/preferences.lua)
* - pony mail list DB (api/pminfo.lua)
* - emails from this list (api/stats.lua)
* When done, we create the scaffolding and list view
*/
function primeListView(state) {
console.log("Priming user interface for List View..");
state = state || {};
state.prime = true;
GET('%sapi/preferences.lua'.format(apiURL), init_preferences, state);
}
// callback from when prefs have loaded
function post_prime(state) {
let sURL = '%sapi/stats.lua?list=%s&domain=%s'.format(apiURL, current_list, current_domain);
if (current_year && current_month) {
sURL += "&d=%u-%u".format(current_year, current_month);
}
if (! (state && state.search)) {
if (state && state.array) {
collated_json = {};
virtual_inbox_loading = true;
for (var i = 0; i < state.array.length; i++) {
let list = state.array[i].split('@');
sURL = '%sapi/stats.lua?list=%s&domain=%s'.format(apiURL, list[0], list[1]);
GET(sURL, render_virtual_inbox, state);
}
} else {
GET(sURL, renderListView, state);
}
} else {
search(state.query, state.date);
}
}
function parseURL(state) {
let bits = window.location.search.substr(1).split(":", 3);
let list = bits[0];
let month = bits[1];
let query = bits[2];
let list_array = null;
state = state || {};
current_query = query || "";
current_month = 0;
current_year = 0;
// If "month" (year-month) is specified,
// we should set the current vars
if (month) {
try {
let dbits = month.split("-");
current_year = dbits[0];
current_month = dbits[1];
} catch (e) {}
}
// Is this a valid list?
if (list !== '') {
// multi-list??
if (list.match(/,/)) {
state.array = list.split(',');
current_domain = 'inbox';
current_list = 'virtual';
} else {
let lbits = list.split("@");
if (lbits.length > 1) {
current_list = lbits[0];
current_domain = lbits[1];
} else {
current_domain = lbits;
current_list = '';
}
}
}
// Are we initiating a search?
if (query) {
state.search = true;
state.query = query;
state.date = month;
}
primeListView(state);
};
// Parse a permalink and fetch the thread
function parse_permalink() {
// message id is the bit after the last /
let mid = location.href.split('/').pop();
init_preferences(); // blank call to load defaults like social rendering
GET('%sapi/preferences.lua'.format(apiURL), init_preferences, null);
// Fetch the thread data and pass to build_single_thread
GET('%sapi/thread.lua?id=%s'.format(apiURL, mid), construct_single_thread, {cached: true});
}
// Virtual inbox Å•endering
function render_virtual_inbox(state, json) {
if (json) {
collated_json.emails = collated_json.emails || [];
collated_json.thread_struct = collated_json.thread_struct || [];
for (var i = 0; i < json.emails.length; i++) {
collated_json.emails.push(json.emails[i]);
}
for (var i = 0; i < json.thread_struct.length; i++) {
collated_json.thread_struct.push(json.thread_struct[i]);
}
}
for (var k in async_escrow) {
return;
}
if (true) {
console.log("Rendering multi-list")
current_json = collated_json;
current_json.participants = [];
async_escrow['rendering'] = new Date();
if (!state || state.update_calendar !== false) {
renderCalendar(json.firstYear,json.firstMonth,json.lastYear, json.lastMonth);
}
// sort threads by date
if (isArray(json.thread_struct)) {
current_json.thread_struct.sort((a,b) => last_email(a) - last_email(b));
}
listview_header(state, current_json);
if (current_listmode == 'threaded') { listview_threaded(current_json, 0); }
else { listview_flat(current_json, 0);}
sidebar_stats(current_json); // This comes last, takes the longest with WC enabled.
delete async_escrow['rendering'];
}
}
/******************************************
Fetched from source/render-email.js
******************************************/
let full_emails = {};
async function render_email(state, json) {
let div = state.div;
full_emails[json.mid] = json; // Save for composer if replying later...
if (state.scroll) {
let rect = div.getBoundingClientRect();
try {
window.setTimeout(function() { window.scrollTo(0, rect.top - 48);}, 200);
console.log("Scrolled to %u".format(rect.top - 48));
} catch (e) {}
}
if (chatty_layout) {
return render_email_chatty(state, json);
}
// Author
let author_field = new HTML('div', {class: 'email_kv'});
let author_key = new HTML('div', {class: 'email_key'}, "From: ");
let author_value = new HTML('div', {class: 'email_value'}, json.from);
author_field.inject([author_key, author_value]);
div.inject(author_field);
// Subject
let subject_field = new HTML('div', {class: 'email_kv'});
let subject_key = new HTML('div', {class: 'email_key'}, "Subject: ");
let subject_value = new HTML('div', {class: 'email_value'}, json.subject == '' ? '(No subject)' : json.subject);
subject_field.inject([subject_key, subject_value]);
div.inject(subject_field);
// Date
let date_field = new HTML('div', {class: 'email_kv'});
let date_key = new HTML('div', {class: 'email_key'}, "Date: ");
let date_value = new HTML('div', {class: 'email_value'}, new Date(json.epoch * 1000.0).ISOBare());
date_field.inject([date_key, date_value]);
div.inject(date_field);
// List
let listname = json.list_raw.replace(".", "@", 1).replace(/[<>]/g, "");
let list_field = new HTML('div', {class: 'email_kv'});
let list_key = new HTML('div', {class: 'email_key'}, "List: ");
let list_value = new HTML('div', {class: 'email_value'},
new HTML('a', {href: 'list?%s'.format(listname)}, listname)
);
list_field.inject([list_key, list_value]);
div.inject(list_field);
// Private email??
if (json.private === true) {
let priv_field = new HTML('div', {class: 'email_kv'});
let priv_key = new HTML('div', {class: 'email_key'}, "Private: ");
let priv_value = new HTML('div', {class: 'email_value_emphasis'}, "YES");
priv_field.inject([priv_key, priv_value]);
div.inject(priv_field);
}
// Attachments?
if (json.attachments && json.attachments.length > 0) {
let attach_field = new HTML('div', {class: 'email_kv'});
let attach_key = new HTML('div', {class: 'email_key'}, "Attachment(s): ");
let alinks = [];
for (let n = 0; n < json.attachments.length; n++) {
let attachment = json.attachments[n];
let link = `${pm_config.apiURL}api/email.lua?attachment=true&id=${json.mid}&file=${attachment.hash}`;
let a = new HTML('a', {href: link, target: '_blank'}, attachment.filename);
alinks.push(a);
let fs = ` ${attachment.size} bytes`;
if (attachment.size >= 1024) fs = ` ${Math.floor(attachment.size/1024)} KB`;
if (attachment.size >= 1024*1024) fs = ` ${Math.floor(attachment.size/(1024*10.24))/100} MB`;
alinks.push (fs);
alinks.push(new HTML('br'));
}
let attach_value = new HTML('div', {class: 'email_value'}, alinks);
attach_field.inject([attach_key, attach_value]);
div.inject(attach_field);
}
let text = new HTML('pre', {}, fixup_quotes(json.body));
div.inject(text);
let toolbar = new HTML('div', {class: 'toolbar'});
// reply to email button
let replybutton = new HTML('button', { title: "Reply to this email", onclick: `compose_email('${json.mid}');`, class: 'btn toolbar_btn toolbar_button_reply'}, new HTML('span', { class: 'glyphicon glyphicon-pencil'}, ' '));
toolbar.inject(replybutton);
// permalink button
let linkbutton = new HTML('a', { href: 'thread/%s'.format(json.mid), title: "Permanent link to this email", class: 'btn toolbar_btn toolbar_button_link'}, new HTML('span', { class: 'glyphicon glyphicon-link'}, ' '));
toolbar.inject(linkbutton);
// Source-view button
let sourcebutton = new HTML('a', { href: '%sapi/source.lua?id=%s'.format(apiURL, json.mid), title: "View raw source", class: 'btn toolbar_btn toolbar_button_source'}, new HTML('span', { class: 'glyphicon glyphicon-file'}, ' '));
toolbar.inject(sourcebutton);
text.inject(toolbar);
}
async function render_email_chatty(state, json) {
let div = state.div;
div.parentNode.style.border = 'none';
// Author
let when = new Date(json.epoch * 1000.0);
let ldate = when.toISOString();
try {
ldate = "%s %s".format(when.toLocaleDateString('en-US', ponymail_date_format), when.toLocaleTimeString());
} catch (e) {
}
let author_field = new HTML('div', {class: 'chatty_author'});
let gravatar = new HTML('img', { class:"chatty_gravatar", src: "https://secure.gravatar.com/avatar/%s.jpg?s=96&r=g&d=mm".format(json.gravatar)});
let author_name = json.from.replace(/\s*<.+>/, "").replace(/"/g, '');
let author_email = json.from.match(/\s*<(.+@.+)>\s*/);
if (author_name.length == 0) author_name = author_email ? author_email[1] : "(No author?)";
let author_nametag = new HTML('div', {class: 'chatty_author_name'}, [
new HTML('b', {}, author_name),
" - %s".format(ldate)
]);
author_field.inject([gravatar, author_nametag]);
div.inject(author_field);
let chatty_body = fixup_quotes(json.body);
if (json.mid == current_open_email) {
let header = new HTML('h4', {class: 'chatty_title_inline'}, json.subject);
chatty_body.unshift(header);
}
let text = new HTML('pre', {class: 'chatty_body'}, chatty_body);
div.inject(text);
// Private text?
if (json.private === true) {
text.style.backgroundImage = "url(images/private.png)";
}
// Attachments?
if (json.attachments && json.attachments.length > 0) {
let attach_field = new HTML('div', {class: 'email_kv'});
let attach_key = new HTML('div', {class: 'email_key'}, "Attachment(s):");
let alinks = [];
for (let n = 0; n < json.attachments.length; n++) {
let attachment = json.attachments[n];
let link = `${pm_config.apiURL}api/email.lua?attachment=true&id=${json.mid}&file=${attachment.hash}`;
let a = new HTML('a', {href: link, target: '_blank'}, attachment.filename);
alinks.push(a);
let fs = ` ${attachment.size} bytes`;
if (attachment.size >= 1024) fs = ` ${Math.floor(attachment.size/1024)} KB`;
if (attachment.size >= 1024*1024) fs = ` ${Math.floor(attachment.size/(1024*10.24))/100} MB`;
alinks.push (fs);
alinks.push(new HTML('br'));
}
let attach_value = new HTML('div', {class: 'email_value'}, alinks);
attach_field.inject([attach_key, attach_value]);
text.inject(attach_field);
}
let toolbar = new HTML('div', {class: 'toolbar_chatty'});
// reply to email button
let replybutton = new HTML('button', { title: "Reply to this email", onclick: `compose_email('${json.mid}');`, class: 'btn toolbar_btn toolbar_button_reply'}, new HTML('span', { class: 'glyphicon glyphicon-pencil'}, ' '));
toolbar.inject(replybutton);
// permalink button
let linkbutton = new HTML('a', { href: 'thread/%s'.format(json.mid), title: "Permanent link to this email", class: 'btn toolbar_btn toolbar_button_link'}, new HTML('span', { class: 'glyphicon glyphicon-link'}, ' '));
toolbar.inject(linkbutton);
// Source-view button
let sourcebutton = new HTML('a', { href: '%sapi/source.lua?id=%s'.format(apiURL, json.mid), title: "View raw source", class: 'btn toolbar_btn toolbar_button_source'}, new HTML('span', { class: 'glyphicon glyphicon-file'}, ' '));
toolbar.inject(sourcebutton);
// Admin button?
if (ponymail_preferences.login && ponymail_preferences.login.credentials && ponymail_preferences.login.credentials.admin) {
let adminbutton = new HTML('a', { href: '#', title: "Administrative control", class: 'btn toolbar_btn toolbar_button_admin'}, new HTML('span', { class: 'glyphicon glyphicon-cog'}, ' '));
toolbar.inject(adminbutton);
}
text.inject(toolbar);
}
/******************************************
Fetched from source/scaffolding-html.js
******************************************/
/**
* HTML: DOM creator class
* args:
* - type: HTML element type (div, table, p etc) to produce
* - params: hash of element params to add (class, style etc)
* - children: optional child or children objects to insert into the new element
* Example:
* div = new HTML('div', {
* class: "footer",
* style: {
* fontWeight: "bold"
* }
#}, "Some text inside a div")
*/
var txt = (msg) => document.createTextNode(msg);
var HTML = (function() {
function HTML(type, params, children) {
/* create the raw element, or clone if passed an existing element */
var child, j, len, val;
if (typeof type === 'object') {
this.element = type.cloneNode();
} else {
this.element = document.createElement(type);
}
/* If params have been passed, set them */
if (isHash(params)) {
for (var key in params) {
val = params[key];
/* Standard string value? */
if (typeof val === "string" || typeof val === 'number') {
this.element.setAttribute(key, val);
} else if (isArray(val)) {
/* Are we passing a list of data to set? concatenate then */
this.element.setAttribute(key, val.join(" "));
} else if (isHash(val)) {
/* Are we trying to set multiple sub elements, like a style? */
for (var subkey in val) {
let subval = val[subkey];
if (!this.element[key]) {
throw "No such attribute, " + key + "!";
}
this.element[key][subkey] = subval;
}
}
}
}
/* If any children have been passed, add them to the element */
if (children) {
/* If string, convert to textNode using txt() */
if (typeof children === "string") {
this.element.inject(txt(children));
} else {
/* If children is an array of elems, iterate and add */
if (isArray(children)) {
for (j = 0, len = children.length; j < len; j++) {
child = children[j];
/* String? Convert via txt() then */
if (typeof child === "string") {
this.element.inject(txt(child));
} else {
/* Plain element, add normally */
this.element.inject(child);
}
}
} else {
/* Just a single element, add it */
this.element.inject(children);
}
}
}
return this.element;
}
return HTML;
})();
/**
* prototype injector for HTML elements:
* Example: mydiv.inject(otherdiv)
*/
HTMLElement.prototype.inject = function(child) {
var item, j, len;
if (isArray(child)) {
for (j = 0, len = child.length; j < len; j++) {
item = child[j];
if (typeof item === 'string') {
item = txt(item);
}
this.appendChild(item);
}
} else {
if (typeof child === 'string') {
child = txt(child);
}
this.appendChild(child);
}
return child;
};
/**
* prototype for emptying an html element
*/
HTMLElement.prototype.empty = function() {
var ndiv;
ndiv = this.cloneNode();
this.parentNode.replaceChild(ndiv, this);
return ndiv;
};
function toggleView(id) {
let obj = document.getElementById(id);
if (obj) {
obj.style.display = (obj.style.display == 'block') ? 'none' : 'block';
}
}
/******************************************
Fetched from source/search.js
******************************************/
function search(query, date) {
let list = current_list;
let global = false;
let domain = current_domain;
if (ponymail_search_list == 'global') {
list = '*';
domain = '*';
global = true;
}
if (ponymail_search_list == 'domain') {
list = '*';
global = true;
}
let sURL = '%sapi/stats.lua?d=%s&list=%s&domain=%s&q=%s'.format(apiURL, date, list, domain, query);
GET(sURL, renderListView, {search: true, global: global});
let listid = '%s@%s'.format(list, domain);
let newhref = "list?%s:%s:%s".format(listid, date, query);
if (location.href !== newhref) {
window.history.pushState({}, null, newhref);
}
listview_list_lists({url: sURL, search: true, query: query});
hideWindows(true);
document.getElementById('q').value = query;
return false;
}
// set the list(s) to search, update links
function search_set_list(what) {
ponymail_search_list = what;
let links = document.getElementsByClassName('searchlistoption');
let whatxt = "this list"
for (var i = 0; i < links.length; i++) {
let el = links[i];
if (el.getAttribute("id").match(what)) {
el.setAttribute("class", "searchlistoption checked");
whatxt = el.innerText.toLowerCase();
} else {
el.setAttribute("class", "searchlistoption");
}
}
document.getElementById('q').setAttribute("placeholder", "Search %s...".format(whatxt));
}
/******************************************
Fetched from source/sidebar-calendar.js
******************************************/
var months_shortened = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
var default_start_year = 1990;
var default_end_year = 2100;
var calendar_index = 0;
var calendar_years_shown = 4;
function renderCalendar(FY, FM, LY, LM) {
calendar_index = 0;
// Only render if calendar div is present
let cal = document.getElementById('sidebar_calendar');
if (!cal) {return;}
let now = new Date();
let CY = now.getFullYear();
let CM = now.getMonth();
let SY = Math.min(LY, CY); // last year in calendar, considering current date
let cdiv = new HTML('div', { class: 'sidebar_calendar' })
let N = 0;
// Chevron for moving to later years
let chevron = new HTML('div', { class: 'sidebar_calendar_chevron'});
chevron.inject(new HTML('span', { onclick: 'calendar_scroll(this, -4);', style: {display: 'none'}, id: 'sidebar_calendar_up' , class: 'glyphicon glyphicon-collapse-up', title: "Show later years"}, " "));
cdiv.inject(chevron);
// Create divs for each year, assign all visible
for (var Y = SY; Y >= FY; Y--) {
let ydiv = new HTML('div', { class: 'sidebar_calendar_year', id: 'sidebar_calendar_'+N++ });
ydiv.inject(txt(Y));
ydiv.inject(new HTML('br'));
for (var i = 0; i < months_shortened.length; i++) {
let mon = months_shortened[i];
let mdiv = new HTML('div', { onclick: 'calendar_click(%u, %u);'.format(Y, i+1), class: 'sidebar_calendar_month'}, mon);
// Mark out-of-bounds segments
if ( (Y == SY && i >= LM) || (Y == CY && i > CM) ) {
mdiv.setAttribute("class", "sidebar_calendar_month_nothing");
}
if (Y == FY && ((i+1) < FM)) {
mdiv.setAttribute("class", "sidebar_calendar_month_nothing");
}
ydiv.inject(mdiv);
}
cdiv.inject(ydiv);
}
cal.innerHTML = "<p style='text-align: center;'>Archives (%u - %u):</p>".format(FY, SY);
cal.inject(cdiv);
chevron = new HTML('div', { class: 'sidebar_calendar_chevron'});
chevron.inject(new HTML('span', { onclick: 'calendar_scroll(this, 4);', style: {display: 'none'}, id: 'sidebar_calendar_down', class: 'glyphicon glyphicon-collapse-down', title: "Show earlier years"}, " "));
cdiv.inject(chevron);
// If we have > 4 years, hide the rest
if (N > calendar_years_shown) {
for (var i = calendar_years_shown; i < N; i++) {
let obj = document.getElementById('sidebar_calendar_'+i);
if (obj) {
obj.style.display = "none";
}
}
document.getElementById('sidebar_calendar_down').style.display = 'block';
}
}
function calendar_scroll(me, x) {
let years = document.getElementsByClassName('sidebar_calendar_year');
calendar_index = Math.max(Math.min(years.length-x, calendar_index + x), 0);
if (calendar_index > 0) {
document.getElementById('sidebar_calendar_up').style.display = 'block';
} else {
document.getElementById('sidebar_calendar_up').style.display = 'none';
}
if (calendar_index < (years.length-x)) {
document.getElementById('sidebar_calendar_down').style.display = 'block';
} else {
document.getElementById('sidebar_calendar_down').style.display = 'none';
}
for (var i = 0; i < years.length; i++) {
let year = years[i];
if (typeof(year) == 'object') {
if (i >= calendar_index && i < (calendar_index+Math.abs(x))) {
year.style.display = "block";
} else {
year.style.display = "none";
}
}
}
}
function calendar_click(year, month) {
current_year = year;
current_month = month;
searching = false;
let newhref = "list.html?%s@%s:%u-%u".format(current_list, current_domain, year, month);
if (location.href !== newhref) {
window.history.pushState({}, null, newhref);
}
GET('%sapi/stats.lua?list=%s&domain=%s&d=%u-%u'.format(apiURL, current_list, current_domain, year, month), renderListView, {to: '%s@%s'.format(current_list, current_domain), update_calendar: false});
}
/******************************************
Fetched from source/sidebar-stats.js
******************************************/
async function sidebar_stats(json) {
let obj = document.getElementById('sidebar_stats');
if (!obj) { return; }
obj.innerHTML = ""; // clear stats bar
if (!json.emails || isHash(json.emails) || json.emails.length == 0) {
obj.innerText = "No emails found...";
return;
}
// Top 10 participants
obj.inject("Found %u emails by %u authors, divided into %u topics.".format(json.emails.length, json.numparts, json.no_threads));
obj.inject(new HTML('h5', {}, "Most active authors:"));
for (var i=0; i < json.participants.length; i++) {
if (i >= 5) {
break;
}
let par = json.participants[i];
if (par.name.length > 24) {
par.name = par.name.substr(0, 23) + "...";
}
if (par.name.length == 0) {
par.name = par.email;
}
pdiv = new HTML('div', {class:"sidebar_stats_participant"});
pimg = new HTML('img', { class:"gravatar_sm", src: "https://secure.gravatar.com/avatar/%s.jpg?s=64&r=g&d=mm".format(par.gravatar)})
pdiv.inject(pimg);
pdiv.inject(new HTML('b', {}, par.name+": "));
pdiv.inject(new HTML('br'));
pdiv.inject("%u emails sent".format(par.count));
obj.inject(pdiv);
}
// Word cloud, if applicable
let wc = document.getElementById('sidebar_wordcloud');
if (wc && json.cloud) {
wc.innerHTML = "";
wc.inject(new HTML('h5', {}, "Popular topics:"));
// word cloud is delayed by 50ms to let the rest render first
// this is a chrome-specific slowdown we're addressing.
window.setTimeout(function() {wordCloud(json.cloud, 220, 100, wc);}, 50);
}
}