| // Generated by CoffeeScript 1.9.3 |
| |
| /* |
| 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 coffee/*.coffee!! */ |
| |
| /* |
| ****************************************** |
| Fetched from coffee/calendar.coffee |
| ****************************************** |
| */ |
| var BasicEmailDisplay, BasicListView, Calendar, DateEmailDisplay, HTML, HTTPRequest, SingleListView, ThreadedEmailDisplay, calendar_months, cog, dbRead, dbWrite, dealWithKeyboard, e, footerScaffolding, genColors, get, hasRead, headerScaffolding, hsl2rgb, isArray, isHash, listView, listviewScaffolding, markRead, maxLists, parseURL, pendingURLStatus, pending_spinner_at, pending_url_operations, pm_snap, pm_storage_available, pm_storage_globvar, ponymail_current_email, ponymail_current_listview, ponymail_display_models, ponymail_domain, ponymail_email_open, ponymail_list, ponymail_list_json, ponymail_listname, ponymail_lists, ponymail_listview_models, ponymail_month, ponymail_preferences, ponymail_query, ponymail_quote_regex, ponymail_register_display, ponymail_register_listview, ponymail_stored_email, ponymail_url_regex, ponymail_version, quickSearch, quickSearchBar, readEmail, renderListView, scaffoldingEmailCallback, set, setupAccount, shortBits, shortenURL, spinCheck, testCoffee, testToggle, threadScaffolding, toggleMonth, toggleQuote, toggleYear, txt, unshortenURL, |
| extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, |
| hasProp = {}.hasOwnProperty; |
| |
| calendar_months = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December']; |
| |
| |
| /** |
| * Calendar: Make a HTML calendar with years and months |
| * that expands/contracts. For the left side view. |
| * Usage: cal = new Calendar('2001-2', '2016-9', 2010) |
| * Would make a calendar going from 2001 to 2016 with 2010 expanded. |
| */ |
| |
| Calendar = (function() { |
| function Calendar(start, end, jumpTo) { |
| var div, eMonth, eYear, extra, j, jMonth, jYear, month, monthDiv, monthsDiv, now, o, ref, ref1, ref2, ref3, ref4, ref5, ref6, sMonth, sYear, uid, yDiv, year, years; |
| now = new Date(); |
| uid = parseInt(Math.random() * 100000000).toString(16); |
| |
| /* Split start and end into years and months */ |
| ref = String(start).split("-"), sYear = ref[0], sMonth = ref[1]; |
| ref1 = [now.getFullYear(), now.getMonth() + 1], eYear = ref1[0], eMonth = ref1[1]; |
| ref2 = [0, 0], jYear = ref2[0], jMonth = ref2[1]; |
| if (jumpTo) { |
| ref3 = String(jumpTo).split("-", 2), jYear = ref3[0], jMonth = ref3[1]; |
| jYear = parseInt(jYear); |
| jMonth = parseInt(jMonth); |
| } |
| |
| /* If end year+month given, use it */ |
| if (end) { |
| ref4 = String(end).split("-"), eYear = ref4[0], eMonth = ref4[1]; |
| |
| /* If end year is this year, restrict months to those that have passed */ |
| if (parseInt(eYear) === now.getFullYear()) { |
| eMonth = now.getMonth() + 1; |
| } |
| } |
| |
| /* Make sure months are there, otherwise set them */ |
| if (!sMonth) { |
| sMonth = 1; |
| } |
| if (!eMonth) { |
| eMonth = 12; |
| } |
| |
| /* For each year, construct the year div to hold months */ |
| years = []; |
| for (year = j = ref5 = parseInt(sYear), ref6 = parseInt(eYear); ref5 <= ref6 ? j <= ref6 : j >= ref6; year = ref5 <= ref6 ? ++j : --j) { |
| yDiv = new HTML('div', { |
| id: ("calendar_year_" + uid + "_") + year, |
| data: String(year), |
| "class": "calendar_year", |
| onclick: "toggleYear(this);" |
| }, String(year)); |
| |
| /* Construct the placeholder for months */ |
| |
| /* Hide unless active year */ |
| monthsDiv = new HTML('div', { |
| "class": (jumpTo && jYear === year) || (!jumpTo && year === parseInt(eYear)) ? "calendar_months" : "calendar_months_hidden", |
| id: ("calendar_months_" + uid + "_") + year |
| }); |
| |
| /* For each month, make a div */ |
| for (month = o = 12; o >= 1; month = --o) { |
| |
| /* Make sure this is within the start<->end range */ |
| if ((year > sYear || month >= sMonth) && (year < eYear || month <= eMonth)) { |
| extra = ""; |
| if (jumpTo && jYear === year && jMonth === month) { |
| extra = "calendar_month_selected"; |
| } |
| monthDiv = new HTML('div', { |
| "class": "calendar_month " + extra, |
| id: "calendar_month_" + uid + "_" + year + "-" + month, |
| data: year + "-" + month, |
| onclick: "toggleMonth(this)" |
| }, calendar_months[month - 1]); |
| monthsDiv.inject(monthDiv); |
| } |
| } |
| |
| /* unshift year into the div list (thus reverse order) */ |
| years.unshift(monthsDiv); |
| years.unshift(yDiv); |
| } |
| |
| /* Return a combined div */ |
| div = new HTML('div', { |
| "class": "calendar", |
| id: uid, |
| data: sYear + "-" + eYear |
| }, years); |
| return div; |
| } |
| |
| return Calendar; |
| |
| })(); |
| |
| toggleYear = function(div) { |
| |
| /* Get the start and end year from the parent div */ |
| var eYear, j, month, ref, ref1, ref2, ref3, results, sYear, uid, y, year; |
| ref = div.parentNode.getAttribute('data').split("-"), sYear = ref[0], eYear = ref[1]; |
| |
| /* Get the year we clicked on */ |
| ref1 = div.getAttribute("data").split("-"), year = ref1[0], month = ref1[1]; |
| year = parseInt(year); |
| month = parseInt(month); |
| uid = div.parentNode.getAttribute("id"); |
| |
| /* For each year, hide if not this year, else show */ |
| results = []; |
| for (y = j = ref2 = parseInt(sYear), ref3 = parseInt(eYear); ref2 <= ref3 ? j <= ref3 : j >= ref3; y = ref2 <= ref3 ? ++j : --j) { |
| if (y === year) { |
| results.push(get("calendar_months_" + uid + "_" + y).setAttribute("class", "calendar_months")); |
| } else { |
| results.push(get("calendar_months_" + uid + "_" + y).setAttribute("class", "calendar_months calendar_months_hidden")); |
| } |
| } |
| return results; |
| }; |
| |
| toggleMonth = function(div) { |
| var m, month, ref, uid, year; |
| uid = div.parentNode.parentNode.getAttribute("id"); |
| m = div.getAttribute("data"); |
| ref = m.split("-"), year = ref[0], month = ref[1]; |
| |
| /* Update the list view using the new month */ |
| return listView({ |
| month: year + "-" + parseInt(month).pad(2) |
| }); |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/colors.coffee |
| ****************************************** |
| */ |
| |
| hsl2rgb = function(h, s, l) { |
| var fract, min, sh, sv, switcher, v, vsf; |
| h = h % 1; |
| if (s > 1) { |
| s = 1; |
| } |
| if (l > 1) { |
| l = 1; |
| } |
| if (l <= 0.5) { |
| v = l * (1 + s); |
| } else { |
| v = l + s - l * s; |
| } |
| if (v === 0) { |
| return { |
| r: 0, |
| g: 0, |
| b: 0 |
| }; |
| } |
| min = 2 * l - v; |
| sv = (v - min) / v; |
| sh = (6 * h) % 6; |
| switcher = Math.floor(sh); |
| fract = sh - switcher; |
| vsf = v * sv * fract; |
| switch (switcher) { |
| case 0: |
| return { |
| r: v, |
| g: min + vsf, |
| b: min |
| }; |
| case 1: |
| return { |
| r: v - vsf, |
| g: v, |
| b: min |
| }; |
| case 2: |
| return { |
| r: min, |
| g: v, |
| b: min + vsf |
| }; |
| case 3: |
| return { |
| r: min, |
| g: v - vsf, |
| b: v |
| }; |
| case 4: |
| return { |
| r: min + vsf, |
| g: min, |
| b: v |
| }; |
| case 5: |
| return { |
| r: v, |
| g: min, |
| b: v - vsf |
| }; |
| } |
| return { |
| r: 0, |
| g: 0, |
| b: 0 |
| }; |
| }; |
| |
| genColors = function(numColors, saturation, lightness, hex) { |
| var baseHue, c, cls, h, i, j, ref; |
| cls = []; |
| baseHue = 1.34; |
| for (i = j = 1, ref = numColors; 1 <= ref ? j <= ref : j >= ref; i = 1 <= ref ? ++j : --j) { |
| c = hsl2rgb(baseHue, saturation, lightness); |
| if (hex) { |
| h = (Math.round(c.r * 255 * 255 * 255) + Math.round(c.g * 255 * 255) + Math.round(c.b * 255)).toString(16); |
| while (h.length < 6) { |
| h = '0' + h; |
| } |
| h = '#' + h; |
| cls.push(h); |
| } else { |
| cls.push({ |
| r: parseInt(c.r * 255), |
| g: parseInt(c.g * 255), |
| b: parseInt(c.b * 255) |
| }); |
| } |
| baseHue -= 0.23; |
| if (baseHue < 0) { |
| baseHue += 1; |
| } |
| } |
| return cls; |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/defaults.coffee |
| ****************************************** |
| */ |
| |
| |
| /* Pony Mail defaults */ |
| |
| ponymail_version = "0.10 (Coffee and Cake)"; |
| |
| ponymail_lists = {}; |
| |
| ponymail_list = ""; |
| |
| ponymail_month = ""; |
| |
| ponymail_query = ""; |
| |
| ponymail_listname = ""; |
| |
| ponymail_domain = ""; |
| |
| ponymail_list_json = {}; |
| |
| ponymail_current_listview = null; |
| |
| ponymail_email_open = []; |
| |
| ponymail_current_email = null; |
| |
| ponymail_stored_email = {}; |
| |
| ponymail_preferences = {}; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/defaults_register.coffee |
| ****************************************** |
| */ |
| |
| |
| /* Various register functions */ |
| |
| ponymail_listview_models = {}; |
| |
| ponymail_display_models = {}; |
| |
| ponymail_register_listview = function(name, title, cl) { |
| return ponymail_listview_models[name] = { |
| title: title, |
| "class": cl |
| }; |
| }; |
| |
| ponymail_register_display = function(name, title, cl) { |
| return ponymail_display_models[name] = { |
| title: title, |
| "class": cl |
| }; |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/dom_utils.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * 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") |
| */ |
| |
| HTML = (function() { |
| function HTML(type, params, children) { |
| |
| /* create the raw element, or clone if passed an existing element */ |
| var child, j, key, len, subkey, subval, 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 (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 (subkey in val) { |
| 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; |
| |
| })(); |
| |
| |
| /* Set: shortcut for a.setAttribute(b,c) */ |
| |
| set = function(a, b, c) { |
| return a.setAttribute(b, c); |
| }; |
| |
| |
| /* txt: shortcut for creating a text node */ |
| |
| txt = function(a) { |
| return document.createTextNode(a); |
| }; |
| |
| |
| /* Get: Shortcut for doc.getElementById */ |
| |
| get = function(a) { |
| return document.getElementById(a); |
| }; |
| |
| |
| /** |
| * 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 show/hide function for HTML elements: |
| * If called with a bool, show if True, hide if False. |
| * If no bool, toggle show/hide based on current state. |
| */ |
| |
| HTMLElement.prototype.show = function(bool) { |
| var b, d; |
| d = 'block'; |
| if (typeof bool === 'undefined') { |
| d = this.style && this.style.display === 'none' ? 'block' : 'none'; |
| } else if (bool === false) { |
| d = 'none'; |
| } else if (bool === true) { |
| b = 'block'; |
| } |
| this.style.display = d; |
| return d; |
| }; |
| |
| |
| /** |
| * prototype for emptying an html element |
| */ |
| |
| HTMLElement.prototype.empty = function() { |
| var ndiv; |
| ndiv = this.cloneNode(); |
| this.parentNode.replaceChild(ndiv, this); |
| return ndiv; |
| }; |
| |
| |
| /* Cog: Loading panel for when waiting for a response */ |
| |
| cog = function(div, size) { |
| var i, idiv; |
| if (size == null) { |
| size = 200; |
| } |
| idiv = mk('div', { |
| "class": "icon", |
| style: { |
| texAlign: 'center', |
| verticalAlign: 'middle', |
| height: '500px' |
| } |
| }); |
| i = mk('i', { |
| "class": 'fa fa-spin fa-cog', |
| style: { |
| fontSize: size + 'pt !important', |
| color: '#AAB' |
| } |
| }); |
| idiv.inject([i, mk('br'), "Loading data, please wait..."]); |
| div.innerHTML = ""; |
| return div.appendChild(idiv); |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/email_display_basic.coffee |
| ****************************************** |
| */ |
| |
| |
| /* readMail: figure out how to display an email/thread */ |
| |
| readEmail = function(obj) { |
| |
| /* find the original email ID and point of origin */ |
| var closedOne, email, index, j, len, mid, parent; |
| mid = null; |
| parent = null; |
| if (typeof obj === 'string') { |
| mid = obj; |
| parent = document.body; |
| } else if (typeof obj === 'object') { |
| mid = obj.getAttribute("data"); |
| parent = obj; |
| } |
| |
| /* We good to go? */ |
| if ((!mid) || (!parent)) { |
| alert("Couldn't find the email or insertion point!"); |
| return; |
| } |
| |
| /* First check if the MID is already open |
| * If so, close it instead |
| */ |
| closedOne = false; |
| for (j = 0, len = ponymail_email_open.length; j < len; j++) { |
| email = ponymail_email_open[j]; |
| if (mid === email.mid) { |
| email.hide(); |
| closedOne = true; |
| break; |
| } |
| } |
| if (!closedOne) { |
| |
| /* Get thread index value if set, for threads */ |
| index = parent.getAttribute("data-index"); |
| |
| /* We have an(other) email open now */ |
| ponymail_current_email = new ThreadedEmailDisplay(parent, mid, index); |
| return ponymail_email_open.push(ponymail_current_email); |
| } |
| }; |
| |
| |
| /* Basic email display class */ |
| |
| BasicEmailDisplay = (function() { |
| function BasicEmailDisplay(parent1, mid1) { |
| var me, r; |
| this.parent = parent1; |
| this.mid = mid1; |
| this.placeholder = get("placeholder_" + this.mid) || new HTML('div', { |
| "class": "email_placeholder", |
| id: "placeholder_" + this.mid |
| }); |
| |
| /* Inject into listview or body */ |
| this.parent.inject(this.placeholder); |
| |
| /* Make sure it's empty, may have been used before! */ |
| this.placeholder = this.placeholder.empty(); |
| this.placeholder.show(true); |
| me = this; |
| |
| /* Do we have this email in cache? */ |
| if (ponymail_stored_email[this.mid]) { |
| this.render(ponymail_stored_email[this.mid]); |
| } else { |
| |
| /* Not stored, fetch the email first */ |
| r = new HTTPRequest("api/email.lua?", { |
| get: { |
| id: this.mid |
| }, |
| callback: function(json, state) { |
| return me.render(json, state); |
| } |
| }); |
| } |
| } |
| |
| BasicEmailDisplay.prototype.render = function(json, state) { |
| |
| /* Store email in cache if not there already */ |
| var at, att_line, b, buttons, date_line, file, from_line, fsize, headers, htmlbody, j, len, link, list_line, pbutton, placeholder, rbutton, ref, sbutton, shortID, subject_line; |
| if (!ponymail_stored_email[json.mid]) { |
| ponymail_stored_email[json.mid] = json; |
| } |
| |
| /* Mark as read */ |
| markRead(json.mid); |
| placeholder = get('placeholder_' + this.mid + "_" + json.mid) || get('placeholder_' + json.mid); |
| |
| /* Display email headers */ |
| headers = new HTML('div', { |
| "class": "email_header" |
| }); |
| from_line = new HTML('div', {}, [ |
| new HTML('div', { |
| "class": "header_key" |
| }, "From: "), new HTML('div', { |
| "class": "header_value" |
| }, json.from) |
| ]); |
| headers.inject(from_line); |
| subject_line = new HTML('div', {}, [ |
| new HTML('div', { |
| "class": "header_key" |
| }, "Subject: "), new HTML('div', { |
| "class": "header_value" |
| }, json.subject) |
| ]); |
| headers.inject(subject_line); |
| date_line = new HTML('div', {}, [ |
| new HTML('div', { |
| "class": "header_key" |
| }, "Date: "), new HTML('div', { |
| "class": "header_value" |
| }, new Date(json.epoch * 1000).ISOBare()) |
| ]); |
| headers.inject(date_line); |
| |
| /* <a.b.c> -> a@b.c */ |
| this.list = json.list_raw.replace(/<([^.]+)\.(.+)>/, (function(_this) { |
| return function(a, b, c) { |
| return b + "@" + c; |
| }; |
| })(this)); |
| list_line = new HTML('div', {}, [ |
| new HTML('div', { |
| "class": "header_key" |
| }, "List: "), new HTML('div', { |
| "class": "header_value" |
| }, new HTML('a', { |
| href: "list.html?" + this.list |
| }, this.list)) |
| ]); |
| headers.inject(list_line); |
| |
| /* Attachments, if any */ |
| if (isArray(json.attachments) && json.attachments.length > 0) { |
| at = []; |
| ref = json.attachments; |
| for (j = 0, len = ref.length; j < len; j++) { |
| file = ref[j]; |
| fsize = file.size; |
| |
| /* Compact size to MB, KB or bytes */ |
| if (fsize > (1024 * 1024)) { |
| fsize = (fsize / (1024 * 1024)).toFixed(2) + "MB"; |
| } else if (fsize > 1024) { |
| fsize = (fsize / 1024.).toFixed(2) + "KB"; |
| } else { |
| fsize = fsize + " bytes"; |
| } |
| |
| /* Make a link with the filename and size */ |
| link = new HTML('a', { |
| href: "api/email.lua?attachment=true&file=" + file.hash + "&id=" + json.mid, |
| style: { |
| marginRight: "8px" |
| } |
| }, file.filename + " (" + fsize + ")"); |
| at.push(link); |
| } |
| att_line = new HTML('div', {}, [ |
| new HTML('div', { |
| "class": "header_key" |
| }, "Attachments: "), new HTML('div', { |
| "class": "header_value" |
| }, at) |
| ]); |
| headers.inject(att_line); |
| } |
| |
| /* Action buttons */ |
| |
| /* Permalink */ |
| shortID = shortenURL(json.mid); |
| pbutton = new HTML('a', { |
| "class": "label_yellow", |
| href: "thread.html/" + shortID |
| }, "Permalink"); |
| |
| /* Source */ |
| sbutton = new HTML('a', { |
| "class": "label_red", |
| href: "api/source.lua/" + json.mid |
| }, "View source"); |
| |
| /* Reply */ |
| rbutton = new HTML('a', { |
| "class": "label_green", |
| href: "javascript:void(0);" |
| }, "Reply"); |
| buttons = new HTML('div', { |
| "class": "email_header_buttons" |
| }, [pbutton, sbutton, rbutton]); |
| headers.inject(buttons); |
| placeholder.inject(headers); |
| |
| /* parse body, convert quotes */ |
| htmlbody = this.quotify(json.body); |
| |
| /* Now inject the body */ |
| b = new HTML('pre', { |
| "class": "email_body" |
| }, htmlbody); |
| return placeholder.inject(b); |
| }; |
| |
| |
| /* quotify: put quotes inside quote blocks */ |
| |
| BasicEmailDisplay.prototype.quotify = function(splicer) { |
| var hideQuotes, i, m, qdiv, quote, quotes, t, textbits; |
| hideQuotes = true; |
| if (ponymail_preferences['hideQuotes'] && ponymail_preferences['hideQuotes'] === false) { |
| hideQuotes = false; |
| } |
| |
| /* Array holding text and quotes */ |
| textbits = []; |
| |
| /* Find the first quote, if any */ |
| i = splicer.search(ponymail_quote_regex); |
| quotes = 0; |
| |
| /* While we have more links, ... */ |
| 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); |
| textbits.push(this.URLify(t)); |
| splicer = splicer.substr(i); |
| } |
| |
| /* Find the quote and cut it out as a div */ |
| m = splicer.match(ponymail_quote_regex); |
| if (m) { |
| quote = m[1]; |
| 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('img', { |
| src: 'images/quote.png', |
| width: "24", |
| height: "26", |
| title: "Toggle quote", |
| onclick: "toggleQuote(this)" |
| }), new HTML('br'), new HTML('blockquote', { |
| "class": "email_quote", |
| style: { |
| display: hideQuotes ? 'none' : 'block' |
| } |
| }, this.URLify(quote)) |
| ]); |
| textbits.push(qdiv); |
| splicer = splicer.substr(i); |
| } |
| |
| /* Find the next link */ |
| i = splicer.search(ponymail_quote_regex); |
| } |
| |
| /* push the remaining text into textbits */ |
| textbits.push(this.URLify(splicer)); |
| return textbits; |
| }; |
| |
| |
| /* URLify: find links and HTML'ify them */ |
| |
| BasicEmailDisplay.prototype.URLify = function(splicer) { |
| |
| /* 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 50 URLs... srsly */ |
| if (urls > 50) { |
| 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; |
| }; |
| |
| BasicEmailDisplay.prototype.hide = function() { |
| this.placeholder.show(false); |
| ponymail_email_open.remove(this); |
| return ponymail_current_email = null; |
| }; |
| |
| return BasicEmailDisplay; |
| |
| })(); |
| |
| ponymail_register_display('default', "Single email view", BasicEmailDisplay); |
| |
| |
| /* toggleQuote: show/hide a quote */ |
| |
| toggleQuote = function(div) { |
| return div.parentNode.childNodes[2].show(); |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/email_display_bydate.coffee |
| ****************************************** |
| */ |
| |
| |
| /* date-sorted multi email display class - extends BasicEmail Display */ |
| |
| DateEmailDisplay = (function(superClass) { |
| extend(DateEmailDisplay, superClass); |
| |
| function DateEmailDisplay(parent1, mid1, index) { |
| var email, emails, item, j, len, len1, me, o, ref, thread; |
| this.parent = parent1; |
| this.mid = mid1; |
| this.placeholder = get("placeholder_" + this.mid) || new HTML('div', { |
| "class": "email_placeholder", |
| id: "placeholder_" + this.mid |
| }); |
| |
| /* Inject into listview or body */ |
| this.parent.inject(this.placeholder); |
| |
| /* Make sure it's empty, may have been used before! */ |
| this.placeholder = this.placeholder.empty(); |
| this.placeholder.show(true); |
| me = this; |
| |
| /* Find the thread or fake one */ |
| thread = { |
| tid: this.mid |
| }; |
| if (index && ponymail_current_listview && ponymail_current_listview.json.thread_struct[index]) { |
| thread = ponymail_current_listview.json.thread_struct[index]; |
| } |
| emails = [[this.mid, 0]]; |
| ref = this.dateSort(thread); |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| emails.push(item); |
| } |
| for (o = 0, len1 = emails.length; o < len1; o++) { |
| email = emails[o]; |
| this.dateFetch(this.placeholder, email[0]); |
| } |
| return this; |
| } |
| |
| DateEmailDisplay.prototype.dateSort = function(thread) { |
| var citem, item, j, len, len1, list, o, ref, ref1; |
| list = []; |
| if (thread.children && isArray(thread.children)) { |
| ref = thread.children; |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| list.push([item.tid, item.epoch]); |
| ref1 = this.dateSort(item); |
| for (o = 0, len1 = ref1.length; o < len1; o++) { |
| citem = ref1[o]; |
| list.push(citem); |
| } |
| } |
| } |
| list.sort((function(_this) { |
| return function(a, b) { |
| return a[1] > b[1]; |
| }; |
| })(this)); |
| return list; |
| }; |
| |
| DateEmailDisplay.prototype.dateFetch = function(parent, thread) { |
| |
| /* Make the thread item placeholder */ |
| var bcolor, bcolors, bodyplace, me, place, r, replyplace; |
| bodyplace = new HTML('div', { |
| id: "placeholder_" + this.mid + "_" + thread, |
| "class": "email_boxed" |
| }); |
| |
| /* Assign a random color to the left */ |
| this.prevColor = this.prevColor || ""; |
| bcolors = ['#C93F20', '#20C94A', '#2063C9', '#C9AA20', '#AD20C9', '#99C920', '#20C9C3']; |
| bcolor = bcolors[Math.round(Math.random() * bcolors.length)]; |
| |
| /* ensure we don't get the same color twice in a row */ |
| while (bcolor === this.prevColor) { |
| bcolor = bcolors[Math.round(Math.random() * bcolors.length)]; |
| } |
| this.prevColor = bcolor; |
| bodyplace.style.borderLeft = "4px solid " + bcolor; |
| replyplace = new HTML('div', { |
| id: "thread_replies_" + this.mid + "_" + thread, |
| style: { |
| marginLeft: "20px" |
| } |
| }); |
| place = new HTML('div', { |
| id: "thread_parent_" + this.mid + "_" + thread, |
| style: { |
| float: "left", |
| width: "100%" |
| } |
| }, [bodyplace, replyplace]); |
| parent.inject(place); |
| |
| /* Do we have this email in cache? */ |
| if (ponymail_stored_email[thread]) { |
| return this.render(ponymail_stored_email[thread]); |
| } else { |
| me = this; |
| |
| /* Not stored, fetch the email first */ |
| return r = new HTTPRequest("api/email.lua?", { |
| get: { |
| id: thread |
| }, |
| callback: function(json, state) { |
| return me.render(json, state); |
| } |
| }); |
| } |
| }; |
| |
| return DateEmailDisplay; |
| |
| })(BasicEmailDisplay); |
| |
| ponymail_register_display('date', "Stacked view", DateEmailDisplay); |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/email_display_threaded.coffee |
| ****************************************** |
| */ |
| |
| |
| /* threaded email display class - extends BasicEmail Display */ |
| |
| ThreadedEmailDisplay = (function(superClass) { |
| extend(ThreadedEmailDisplay, superClass); |
| |
| function ThreadedEmailDisplay(parent1, mid1, index, tjson) { |
| var me, thread; |
| this.parent = parent1; |
| this.mid = mid1; |
| if (tjson == null) { |
| tjson = null; |
| } |
| this.placeholder = get("placeholder_" + this.mid) || new HTML('div', { |
| "class": "email_placeholder", |
| id: "placeholder_" + this.mid |
| }); |
| this.shown = {}; |
| me = this; |
| |
| /* Find the thread or fake one */ |
| thread = { |
| tid: this.mid |
| }; |
| if (tjson) { |
| thread = tjson; |
| this.mid = tjson.mid; |
| this.parent = get('email_placeholder'); |
| } else if (index && ponymail_current_listview && ponymail_current_listview.json.thread_struct[index]) { |
| thread = ponymail_current_listview.json.thread_struct[index]; |
| } |
| |
| /* Inject into listview or body */ |
| this.parent.inject(this.placeholder); |
| |
| /* Make sure it's empty, may have been used before! */ |
| this.placeholder = this.placeholder.empty(); |
| this.placeholder.show(true); |
| this.threadedFetch(this.placeholder, thread, 1); |
| return this; |
| } |
| |
| ThreadedEmailDisplay.prototype.threadedFetch = function(parent, thread, nestedness) { |
| |
| /* First off, we don't want duplicates due to whatever bug, so bug out if we've already rendered this email */ |
| var bcolor, bcolors, bodyplace, item, j, len, me, place, r, ref, replyplace; |
| if (this.shown[thread.tid]) { |
| return; |
| } |
| this.shown[thread.tid] = true; |
| |
| /* Make the thread item placeholder */ |
| bodyplace = new HTML('div', { |
| id: "placeholder_" + this.mid + "_" + thread.tid, |
| "class": "email_boxed" |
| }); |
| |
| /* Assign a random color to the left */ |
| this.prevColor = this.prevColor || ""; |
| bcolors = ['#C93F20', '#20C94A', '#2063C9', '#C9AA20', '#AD20C9', '#99C920', '#20C9C3']; |
| bcolor = bcolors[Math.round(Math.random() * bcolors.length)]; |
| |
| /* ensure we don't get the same color twice in a row */ |
| while (bcolor === this.prevColor) { |
| bcolor = bcolors[Math.round(Math.random() * bcolors.length)]; |
| } |
| this.prevColor = bcolor; |
| bodyplace.style.borderLeft = "4px solid " + bcolor; |
| replyplace = new HTML('div', { |
| id: "thread_replies_" + this.mid + "_" + thread.tid, |
| style: { |
| marginLeft: "20px" |
| } |
| }); |
| place = new HTML('div', { |
| id: "thread_parent_" + this.mid + "_" + thread.tid, |
| style: { |
| float: "left", |
| width: "100%" |
| } |
| }, [bodyplace, replyplace]); |
| parent.inject(place); |
| |
| /* Do we have this email in cache? */ |
| if (ponymail_stored_email[thread.tid]) { |
| this.render(ponymail_stored_email[thread.tid]); |
| } else { |
| me = this; |
| |
| /* Not stored, fetch the email first */ |
| r = new HTTPRequest("api/email.lua?", { |
| get: { |
| id: thread.tid |
| }, |
| callback: function(json, state) { |
| return me.render(json, state); |
| }, |
| state: { |
| nest: Math.min(nestedness + 1, 5) |
| } |
| }); |
| } |
| |
| /* Now do the same for each child item */ |
| if (thread.children && isArray(thread.children) && thread.children.length > 0) { |
| ref = thread.children; |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| this.threadedFetch(replyplace, item, Math.min(nestedness + 1, 5)); |
| } |
| } |
| return this; |
| }; |
| |
| return ThreadedEmailDisplay; |
| |
| })(BasicEmailDisplay); |
| |
| ponymail_register_display('threaded', "Threaded view", ThreadedEmailDisplay); |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/http_utils.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * Pending URLs watcher: |
| * Watches which URLs have been pending a result for a while |
| * and shows the spinner if things are taking too long. |
| */ |
| |
| pending_url_operations = {}; |
| |
| pending_spinner_at = 0; |
| |
| spinCheck = function(div, reset) { |
| var ndiv, spnow; |
| if (div.style.display === "block") { |
| spnow = new Date().getTime(); |
| if (reset || (spnow - pending_spinner_at) >= 4000) { |
| pending_spinner_at = spnow; |
| ndiv = div.cloneNode(true); |
| return div.parentNode.replaceChild(ndiv, div); |
| } |
| } else { |
| return pending_spinner_at = 0; |
| } |
| }; |
| |
| pendingURLStatus = function() { |
| var div, pending, spnow, time, url; |
| pending = 0; |
| spnow = new Date().getTime(); |
| div = get('loading'); |
| for (url in pending_url_operations) { |
| time = pending_url_operations[url]; |
| |
| /* Is something taking too long?? */ |
| if ((spnow - time) > 1500) { |
| pending++; |
| if (!div) { |
| div = new HTML('div', { |
| id: 'loading', |
| "class": "spinner" |
| }, [ |
| new HTML('div', { |
| "class": "spinwheel" |
| }, new HTML('div', { |
| "class": "spinwheel_md" |
| }, new HTML('div', { |
| "class": "spinwheel_sm" |
| }))), new HTML('br'), "Loading, please wait..." |
| ]); |
| document.body.inject(div); |
| pending_spinner_at = spnow; |
| div.addEventListener('animationend', function(e) { |
| return spinCheck(div); |
| }); |
| } |
| } |
| } |
| |
| /* If no pending operations, hide the spnner */ |
| if (pending === 0) { |
| div = get('loading'); |
| if (div) { |
| return div.style.display = "none"; |
| } |
| } else if (div && div.style.display === "none") { |
| div.style.display = "block"; |
| if (pending_spinner_at === 0) { |
| pending_spinner_at = spnow; |
| return spinCheck(div, true); |
| } |
| } |
| }; |
| |
| window.setInterval(pendingURLStatus, 500); |
| |
| |
| /** |
| * HTTPRequest: Fire off a HTTP request. |
| * Args: |
| * - url: The URL to request (may be relative or absolute) |
| * - args: |
| * - - state: A callback stateful object |
| * - - data: Any form/JSON data to send along if POST (method is derived |
| * from whether data is attached or not) |
| * - - getdata: Any form vars to append to the URL as URI-encoded formdata |
| * - - datatype: 'form' or 'json' data? |
| * - - callback: function to call when request has returned a response |
| * - - snap: snap function in case of internal server error or similar |
| * - - nocreds: don't pass on cookies? |
| |
| * Example POST request: |
| * HTTPRequest("/api/foo.lua", { |
| * state: { |
| * ponies: true |
| * }, |
| * callback: foofunc, |
| * data: { |
| * list: "foo.bar" |
| * } |
| * }) |
| */ |
| |
| HTTPRequest = (function() { |
| function HTTPRequest(url1, args1) { |
| var r, tmp; |
| this.url = url1; |
| this.args = args1; |
| |
| /* Set internal class data, determine request type */ |
| this.state = this.args.state; |
| this.method = this.args.data ? 'POST' : 'GET'; |
| this.data = this.args.data; |
| this.getdata = this.args.get; |
| this.datatype = this.args.datatype || 'form'; |
| this.callback = this.args.callback; |
| this.snap = this.args.snap || pm_snap; |
| this.nocreds = this.args.nocreds || false; |
| this.uid = parseInt(Math.random() * 10000000).toString(16); |
| |
| /* Construct request object */ |
| if (window.XMLHttpRequest) { |
| this.request = new XMLHttpRequest(); |
| } else { |
| this.request = new ActiveXObject("Microsoft.XMLHTTP"); |
| } |
| |
| /* Default to sending credentials */ |
| if (!this.nocreds) { |
| this.request.withCredentials = true; |
| } |
| |
| /* Determine what to send as data (if anything) */ |
| this.rdata = null; |
| if (this.method === 'POST') { |
| if (this.datatype === 'json') { |
| this.rdata = JSON.stringify(this.data); |
| } else { |
| this.rdata = this.formdata(this.data); |
| } |
| } |
| |
| /* If tasked with appending data to the URL, do so */ |
| if (isHash(this.getdata)) { |
| tmp = this.formdata(this.getdata); |
| if (tmp.length > 0) { |
| |
| /* Do we have form data here aleady? if so, append the new */ |
| |
| /* by adding an ampersand first */ |
| if (this.url.match(/\?/)) { |
| this.url += "&" + tmp; |
| } else { |
| this.url += "?" + tmp; |
| } |
| } |
| } |
| |
| /* Mark operation as pending result */ |
| pending_url_operations[this.uid] = new Date().getTime(); |
| |
| /* Use @method on URL */ |
| this.request.open(this.method, this.url, true); |
| |
| /* Send data */ |
| this.request.send(this.rdata); |
| |
| /* Set onChange behavior */ |
| r = this; |
| this.request.onreadystatechange = function() { |
| return r.onchange(); |
| }; |
| } |
| |
| HTTPRequest.prototype.onchange = function() { |
| |
| /* Mark operation as done */ |
| var e; |
| if (this.request.readyState === 4) { |
| delete pending_url_operations[this.uid]; |
| } |
| |
| /* Internal Server Error: Try to call snap */ |
| if (this.request.readyState === 4 && this.request.status === 500) { |
| if (this.snap) { |
| this.snap(this.state); |
| } |
| } |
| |
| /* 200 OK, everything is okay, try to parse JSON response */ |
| if (this.request.readyState === 4 && this.request.status === 200) { |
| if (this.callback) { |
| |
| /* Try to parse as JSON and deal with cache objects, fall back to old style parse-and-pass */ |
| try { |
| |
| /* Parse JSON response */ |
| this.response = JSON.parse(this.request.responseText); |
| |
| /* If loginRequired (rare!), redirect to oauth page */ |
| if (this.response && this.response.loginRequired) { |
| location.href = "/oauth.html"; |
| return; |
| } |
| |
| /* Otherwise, call the callback function */ |
| return this.callback(this.response, this.state); |
| } catch (_error) { |
| e = _error; |
| console.log("Callback failed: " + e); |
| return this.callback(JSON.parse(this.request.responseText), this.state); |
| } |
| } |
| } |
| }; |
| |
| |
| /* Standard form data joiner for POST data */ |
| |
| HTTPRequest.prototype.formdata = function(kv) { |
| var ar, k, v; |
| ar = []; |
| |
| /* For each key/value pair (assuming this is a hash) */ |
| if (isHash(kv)) { |
| for (k in kv) { |
| v = kv[k]; |
| |
| /* Only append if the value is non-empty */ |
| if (v && v !== "") { |
| |
| /* URI-Encode value and add to an array */ |
| ar.push(k + "=" + encodeURIComponent(v)); |
| } |
| } |
| } |
| |
| /* Join the array with ampersands, so we get "foo=bar&foo2=baz" */ |
| return ar.join("&"); |
| }; |
| |
| return HTTPRequest; |
| |
| })(); |
| |
| pm_snap = null; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/keyboard_shortcuts.coffee |
| ****************************************** |
| */ |
| |
| |
| /* dealWithKeyboard: Handles what happens when you hit the escape key */ |
| |
| dealWithKeyboard = function(e) { |
| var dp, splash; |
| splash = get('splash'); |
| |
| /* escape key: hide composer/settings/thread */ |
| if (e.keyCode === 27) { |
| if (splash && splash.style.display === 'block') { |
| splash.style.display = "none"; |
| } else if (location.href.search(/list\d?\.html/) !== -1) { |
| |
| /* should only work for the list view */ |
| |
| /* If datepicker popup is shown, hide it on escape */ |
| dp = get('datepicker_popup'); |
| if (dp && dp.style.display === "block") { |
| dp.show(false); |
| } else if (ponymail_email_open.length > 0) { |
| |
| /* Close the currently open email? */ |
| if (ponymail_current_email) { |
| ponymail_current_email.hide(); |
| } else { |
| |
| /* Close all email ? */ |
| while (ponymail_email_open.length > 0) { |
| ponymail_email_open[0].hide(); |
| } |
| } |
| } |
| } |
| } |
| |
| /* Make sure the below shortcuts don't interfere with normal operations */ |
| if (splash && splash.style.display !== 'block' && document.activeElement.nodeName !== 'INPUT' && !e.ctrlKey) { |
| |
| /* H key: show help */ |
| if (e.keyCode === 72) { |
| popup("Keyboard shortcuts", "<pre><b>H:</b>Show this help menu<br/><b>C:</b>Compose a new email to the current list<br/><b>R:</b>Reply to the last opened email<br/><b>S:</b>Go to the quick search bar<br/><b>Esc:</b>Hide/collapse current email or thread<br/></pre>You can also, in some cases, use the mouse wheel to scroll up/down the list view", 10); |
| } else if (e.keyCode === 67) { |
| |
| /* C key: compose */ |
| compose(null, ponymail_list, 'new'); |
| } else if (e.keyCode === 82) { |
| |
| /* R key: reply */ |
| if (ponymail_current_email && ponymail_email_open.length > 0) { |
| compose(last_opened_email, null, 'reply'); |
| } |
| } else if (e.keyCode === 83) { |
| |
| /* S key: quick search */ |
| if (get('q')) { |
| get('q').focus(); |
| } |
| } |
| } |
| |
| /* Page Up - scroll list view if possible */ |
| if (e.keyCode === 33 && ponymail_current_listview) { |
| ponymail_current_listview.swipe('up'); |
| } |
| |
| /* Page Down - scroll list view if possible */ |
| if (e.keyCode === 34 && ponymail_current_listview) { |
| return ponymail_current_listview.swipe('down'); |
| } |
| }; |
| |
| |
| /* Add listener for keys (mostly for escape key for hiding stuff) */ |
| |
| window.addEventListener("keyup", dealWithKeyboard, false); |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/listview_basic.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * Basic listview class, to be extended by other designs |
| */ |
| |
| BasicListView = (function() { |
| |
| /* json: from stats.lua, rpp = results per page, pos = starting position (from 0) */ |
| function BasicListView(json1, rpp1, pos1) { |
| var date, hd, m, ref, y; |
| this.json = json1; |
| this.rpp = rpp1 != null ? rpp1 : 0; |
| this.pos = pos1 != null ? pos1 : 0; |
| |
| /* @rpp == 0 == auto-compute num of items */ |
| if (this.rpp === 0) { |
| this.rpp = Math.max(parseInt((window.innerHeight - 300) / 40), 5); |
| this.rpp = this.rpp - (this.rpp % 5); |
| } |
| |
| /* Set the header first */ |
| hd = get('header'); |
| if (this.json.list) { |
| if (ponymail_month.length > 0) { |
| ref = ponymail_month.split("-", 2), y = ref[0], m = ref[1]; |
| date = calendar_months[parseInt(m) - 1] + (", " + y); |
| hd.empty().inject([ |
| this.json.list + " (" + date + "):", new HTML('a', { |
| href: "api/mbox.lua?list=" + ponymail_list + "&date=" + ponymail_month, |
| title: "Download as mbox archive" |
| }, new HTML('img', { |
| src: 'images/floppy.svg', |
| style: { |
| marginLeft: "10px", |
| width: "20px", |
| height: "20px", |
| verticalAlign: 'middle' |
| } |
| })) |
| ]); |
| } else { |
| hd.empty().inject(this.json.list + ", past 30 days:"); |
| } |
| } |
| |
| /* Get and clear the list view */ |
| this.lv = get('listview'); |
| this.lv = this.lv.empty(); |
| |
| /* Set some internal vars */ |
| this.listsize = 0; |
| |
| /* If we got results, use scroll() to display from result 0 on */ |
| if (isArray(this.json.thread_struct) && this.json.thread_struct.length > 0) { |
| |
| /* Set some internal vars */ |
| this.listsize = this.json.thread_struct.length; |
| |
| /* Reverse thread struct, but only if we're not using an |
| * already reversed cache |
| */ |
| if (!this.json.cached) { |
| this.json.thread_struct.reverse(); |
| } |
| this.scroll(this.rpp, this.pos); |
| } else { |
| |
| /* No results, just say...that */ |
| this.lv.inject("No emails found matching this criterion."); |
| } |
| ponymail_current_listview = this; |
| return this; |
| } |
| |
| |
| /* scroll: scroll to a position and show N emails/threads */ |
| |
| BasicListView.prototype.scroll = function(rpp, pos) { |
| var bj, dStat, diff, f, l, lastitem, nbutton, nno, now, np, pbutton, pno, pp, tmpthis, topButtons; |
| this.lastScroll = new Date().getTime(); |
| |
| /* Clear the list view */ |
| this.lv = this.lv.empty(); |
| topButtons = null; |
| this.rpp = rpp; |
| this.pos = pos; |
| |
| /* Show how many threads out of how many we are showing */ |
| f = pos + 1; |
| l = Math.min(this.listsize, pos + rpp); |
| dStat = new HTML('div', { |
| style: { |
| float: "left", |
| width: "100%", |
| fontSize: "80%", |
| textAlign: "center" |
| } |
| }, "Showing items " + f + " through " + l + " out of " + this.listsize + " results."); |
| this.lv.inject(dStat); |
| |
| /* First, build the prev/next buttons if needed */ |
| if (pos > 0 || (pos + rpp) < this.listsize) { |
| topButtons = new HTML('div', { |
| style: { |
| float: "left", |
| width: "100%" |
| } |
| }); |
| if (pos > 0) { |
| pno = Math.min(rpp, pos); |
| pp = Math.max(0, pos - rpp); |
| pbutton = new HTML('input', { |
| type: 'button', |
| value: 'Previous ' + pno + " message" + (pno === 1 ? '' : 's'), |
| onclick: "ponymail_current_listview.scroll(" + rpp + ", " + pp + ");", |
| "class": "listview_button_green", |
| style: { |
| float: "left" |
| } |
| }); |
| topButtons.inject(pbutton); |
| } |
| |
| /* Next button */ |
| if ((pos + rpp) < this.listsize) { |
| nno = Math.min(rpp, this.listsize - pos - rpp); |
| np = pos + rpp; |
| nbutton = new HTML('input', { |
| type: 'button', |
| value: 'Next ' + nno + " message" + (nno === 1 ? '' : 's'), |
| onclick: "ponymail_current_listview.scroll(" + rpp + ", " + np + ");", |
| "class": "listview_button_green", |
| style: { |
| float: "right" |
| } |
| }); |
| topButtons.inject(nbutton); |
| } |
| this.lv.inject(topButtons); |
| } |
| lastitem = this.renderItems(); |
| if (lastitem) { |
| bj = lastitem.getBoundingClientRect(); |
| this.lvitems.style.minHeight = (this.rpp * (bj.height + 1)) + "px"; |
| } |
| |
| /* If we made buttons, clone them at the bottom */ |
| if (topButtons) { |
| this.lv.inject(topButtons.cloneNode(true)); |
| } |
| now = new Date().getTime(); |
| diff = now - this.lastScroll; |
| if (this.json.cached) { |
| this.lv.inject("Fetched from cache (no updates detected), rendered in " + parseInt(diff) + "ms."); |
| } else { |
| this.lv.inject("Fetched in " + parseInt(this.json.took / 1000) + "ms, rendered in " + parseInt(diff) + "ms."); |
| } |
| tmpthis = this; |
| |
| /* Finally, enable scrolling */ |
| this.lv.addEventListener("mousewheel", function(e) { |
| return tmpthis.swipe(e); |
| }, false); |
| return this.lv.addEventListener("DOMMouseScroll", function(e) { |
| return tmpthis.swipe(e); |
| }, false); |
| }; |
| |
| |
| /* renderItems: render the list view emails/theads */ |
| |
| BasicListView.prototype.renderItems = function() { |
| |
| /* For each email result,... */ |
| var i, item, j, lastitem, len, lvitem, original, ref; |
| this.lvitems = new HTML('div', { |
| "class": "listview_table" |
| }); |
| lastitem = null; |
| ref = this.json.thread_struct.slice(this.pos, this.pos + this.rpp); |
| for (i = j = 0, len = ref.length; j < len; i = ++j) { |
| item = ref[i]; |
| original = this.findEmail(item.tid); |
| |
| /* Be sure we actually have an email here */ |
| if (original) { |
| |
| /* Call listViewItem to compile a list view HTML element */ |
| lvitem = this.listViewItem(original, item, i + this.pos); |
| lastitem = lvitem; |
| |
| /* Inject new item into the list view */ |
| this.lvitems.inject(lvitem); |
| } |
| } |
| this.lv.inject(this.lvitems); |
| return lastitem; |
| }; |
| |
| |
| /* findEmail: find an email given an ID */ |
| |
| BasicListView.prototype.findEmail = function(id) { |
| var email, j, len, ref; |
| ref = this.json.emails; |
| for (j = 0, len = ref.length; j < len; j++) { |
| email = ref[j]; |
| if (email.id === id) { |
| return email; |
| } |
| } |
| return null; |
| }; |
| |
| |
| /* countEmail: func for counting how many emails are in a thread */ |
| |
| BasicListView.prototype.countEmail = function(thread) { |
| var item, j, len, nc, ref; |
| nc = 0; |
| if (isArray(thread.children)) { |
| ref = thread.children; |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| nc++; |
| if (item.children && isArray(item.children) && item.children.length > 0) { |
| nc += this.countEmail(item); |
| } |
| } |
| } |
| return nc; |
| }; |
| |
| |
| /* countPeople: func for counting how many people are in a thread */ |
| |
| BasicListView.prototype.countPeople = function(thread, p) { |
| var email, eml, item, j, k, len, n, np, ref, t, v; |
| np = p || {}; |
| n = 0; |
| if (thread.tid) { |
| eml = this.findEmail(thread.tid); |
| if (eml) { |
| np[eml.from] = true; |
| ref = (isArray(thread.children) ? thread.children : []); |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| t = item.tid; |
| email = this.findEmail(t); |
| if (email) { |
| np[email.from] = true; |
| } |
| if (isArray(item.children) && item.children.length > 0) { |
| np = this.countPeople(item.children, np); |
| } |
| } |
| } |
| } |
| if (p) { |
| return np; |
| } else { |
| for (k in np) { |
| v = np[k]; |
| n++; |
| } |
| return n; |
| } |
| }; |
| |
| BasicListView.prototype.listViewItem = function(original, thread, index) { |
| |
| /* Be sure we actually have an email here */ |
| var avatar, date, date_style, envelopeimg, item, noeml, now, people, peopleimg, readStyle, sender, sid, stats, subject, uid; |
| if (original && thread) { |
| now = new Date().getTime() / 1000; |
| |
| /* Gather stats */ |
| people = this.countPeople(thread); |
| noeml = this.countEmail(thread); |
| |
| /* Render the email in the LV */ |
| |
| /* First set some data points for later */ |
| uid = parseInt(Math.random() * 999999999999).toString(16); |
| |
| /* Gravatar */ |
| avatar = new HTML('img', { |
| "class": "gravatar", |
| src: "https://secure.gravatar.com/avatar/" + original.gravatar + ".png?s=24&r=g&d=mm" |
| }); |
| |
| /* Make sure subject and author is...something */ |
| if (original.subject.length === 0) { |
| original.subject = "(No subject)"; |
| } |
| if (original.from.length === 0) { |
| original.from = "(No author?)"; |
| } |
| |
| /* Sender, without the <foo@bar> part - just the name */ |
| sender = new HTML('div', { |
| style: { |
| fontWeight: "bold" |
| } |
| }, original.from.replace(/\s*<.+>/, "").replace(/"/g, '')); |
| |
| /* readStyle: bold if new email, normal if read before */ |
| readStyle = "bold"; |
| if (hasRead(thread.tid)) { |
| readStyle = "normal"; |
| } |
| |
| /* Subject, PLUS a bit of the body with a break before */ |
| sid = shortenURL(thread.tid); |
| subject = new HTML('div', {}, [ |
| new HTML('a', { |
| style: { |
| fontWeight: readStyle |
| }, |
| href: "thread.html/" + sid, |
| onclick: "readEmail(this.parentNode.parentNode.parentNode); this.style.fontWeight = 'normal'; return false;" |
| }, original.subject), new HTML('br'), new HTML('span', { |
| "class": "listview_item_body" |
| }, thread.body) |
| ]); |
| |
| /* show number of replies and participants */ |
| peopleimg = new HTML('img', { |
| src: 'images/avatar.png', |
| style: { |
| verticalAlign: 'middle', |
| width: "12px", |
| height: "12px" |
| } |
| }); |
| envelopeimg = new HTML('img', { |
| src: 'images/envelope.png', |
| style: { |
| verticalAlign: 'middle', |
| width: "16px", |
| height: "12px" |
| } |
| }); |
| stats = new HTML('div', { |
| "class": "listview_right" |
| }, [peopleimg, " " + people + " ", envelopeimg, " " + noeml]); |
| |
| /* Add date; yellow if <= 1day, grey otherwise */ |
| date_style = "listview_grey"; |
| if ((now - 86400 * 4) < thread.epoch) { |
| date_style = "listview_yellow"; |
| } |
| date = new HTML('div', { |
| "class": "listview_right " + date_style |
| }, new Date(thread.epoch * 1000).ISOBare()); |
| |
| /* Finally, pull it all together in a div and add that to the listview */ |
| item = new HTML('div', { |
| id: uid, |
| data: thread.tid, |
| 'data-index': index, |
| "class": "listview_item" |
| }, new HTML('div', { |
| "class": "listview_summary" |
| }, [avatar, sender, subject, date, stats])); |
| return item; |
| } |
| }; |
| |
| |
| /* swipe: go to next or previous page of emails, depending on mouse wheel direction */ |
| |
| BasicListView.prototype.swipe = function(e) { |
| var direction, now, obj, scrollBar, style; |
| this.lastSwipe = this.lastSwipe || 0; |
| direction = ""; |
| if (typeof e === 'string') { |
| direction = e; |
| } else { |
| direction = (e.wheelDelta || -e.detail) < 0 ? 'down' : 'up'; |
| } |
| style = document.body.currentStyle || window.getComputedStyle(document.body, ""); |
| |
| /* Use the footer to determine whether scrollbar is present or not */ |
| obj = get('footer').getBoundingClientRect(); |
| scrollBar = window.innerHeight < obj.bottom; |
| |
| /* Abort swiping if an email is open or scrollbar is present */ |
| if (ponymail_email_open.length > 0 || scrollBar) { |
| return; |
| } |
| |
| /* Make sure we don't swipe too fast! */ |
| now = new Date().getTime(); |
| if (now - this.lastSwipe < 300) { |
| return; |
| } |
| this.lastSwipe = now; |
| if (direction === 'down') { |
| |
| /* Next page? */ |
| if (this.listsize > (this.pos + this.rpp + 1)) { |
| return this.scroll(this.rpp, this.pos + this.rpp); |
| } |
| } else if (direction === 'up') { |
| |
| /* Previous page? */ |
| if (this.pos > 0) { |
| return this.scroll(this.rpp, Math.max(0, this.pos - this.rpp)); |
| } |
| } |
| }; |
| |
| return BasicListView; |
| |
| })(); |
| |
| ponymail_register_listview('default', 'Compact (threaded) theme', BasicListView); |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/listview_basiclib.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * This is the listview basic library |
| */ |
| |
| |
| /* Generally, popping a window state should run a listView update */ |
| |
| window.onpopstate = function(event) { |
| return listView(null, true); |
| }; |
| |
| parseURL = function() { |
| var list, month, query, ref, ref1; |
| ref = window.location.search.substr(1).split(":", 3), list = ref[0], month = ref[1], query = ref[2]; |
| ponymail_list = list; |
| ponymail_month = month || ""; |
| ponymail_query = query || ""; |
| return ref1 = list.split("@"), ponymail_listname = ref1[0], ponymail_domain = ref1[1], ref1; |
| }; |
| |
| listView = function(hash, reParse) { |
| |
| /* Get the HTML filename */ |
| var args, d, domain, etc, htmlfile, k, l, list, max, newhref, pargs, r, ref, ref1, ref2, ref3, since, v; |
| ref = location.href.split("?"), htmlfile = ref[0], etc = ref[1]; |
| |
| /* Do we need to call the URL parser here? */ |
| if (reParse) { |
| parseURL(); |
| } |
| |
| /* Any new settings passed along? */ |
| if (isHash(hash)) { |
| if (typeof hash.month !== 'undefined') { |
| ponymail_month = hash.month; |
| } |
| if (typeof hash.list !== 'undefined') { |
| ponymail_list = hash.list; |
| } |
| if (typeof hash.query !== 'undefined') { |
| ponymail_query = hash.query; |
| } |
| } |
| |
| /* First, check that we have a list to view! */ |
| if (!(ponymail_list && ponymail_list.match(/.+?@.+/))) { |
| |
| /* Do we at least have a domain part? */ |
| if (ponymail_list && ponymail_list.match(/.+?\..+/)) { |
| |
| /* Check if there's a $default list in this domain */ |
| ref1 = ponymail_list.split("@", 2), l = ref1[0], d = ref1[1]; |
| if (!d) { |
| d = l; |
| } |
| |
| /* Do we have this domain listed? If not, redirect to front page */ |
| if (!d || !ponymail_lists[d]) { |
| location.href = "./"; |
| return; |
| } |
| if (ponymail_lists[d] && !ponymail_lists[d][l]) { |
| |
| /* we don't have a matching list, check for the default (or pick busiest) */ |
| l = pm_config.default_list; |
| |
| /*Â does the default exist ? */ |
| if (!ponymail_lists[d][l]) { |
| |
| /* If not, pick busiest */ |
| max = -1; |
| ref2 = ponymail_lists[d]; |
| for (k in ref2) { |
| v = ref2[k]; |
| if (v > max) { |
| max = v; |
| l = k; |
| } |
| } |
| } |
| |
| /* Redirect to the list */ |
| location.href = htmlfile + "?" + l + "@" + d; |
| return; |
| } else { |
| |
| /* No list specified, redirect to front page */ |
| location.href = "./"; |
| return; |
| } |
| } else { |
| |
| /* No domain specified, redirect to front page */ |
| location.href = "./"; |
| return; |
| } |
| } |
| |
| /* Set window title */ |
| document.title = ponymail_list + " - Pony Mail!"; |
| |
| /* Construct arg list for URL */ |
| args = ""; |
| if (ponymail_list && ponymail_list.length > 0) { |
| args += ponymail_list; |
| } |
| if (ponymail_month && ponymail_month.length > 0) { |
| args += ":" + ponymail_month; |
| } |
| if (ponymail_query && ponymail_query.length > 0) { |
| args += ":" + ponymail_query; |
| } |
| |
| /* Push a new history state using new args */ |
| newhref = htmlfile + "?" + args; |
| if (location.href !== newhref) { |
| window.history.pushState({}, null, newhref); |
| } |
| ref3 = ponymail_list.split("@", 2), list = ref3[0], domain = ref3[1]; |
| |
| /* Request month view from API, send to list view callback */ |
| pargs = "d=30"; |
| if (ponymail_month && ponymail_month.length > 0) { |
| pargs = "s=" + ponymail_month + "&e=" + ponymail_month; |
| } |
| |
| /* If we already fetched this URL once, only do an update check */ |
| if (ponymail_list_json[newhref] && ponymail_list_json[newhref].unixtime > 0) { |
| since = ponymail_list_json[newhref].unixtime; |
| return r = new HTTPRequest("api/stats.lua?list=" + list + "&domain=" + domain + "&" + pargs + "&since=" + since, { |
| callback: renderListView, |
| state: { |
| href: newhref |
| } |
| }); |
| } else { |
| return r = new HTTPRequest("api/stats.lua?list=" + list + "&domain=" + domain + "&" + pargs, { |
| callback: renderListView, |
| state: { |
| href: newhref |
| } |
| }); |
| } |
| }; |
| |
| renderListView = function(json, state) { |
| |
| /* If this is a cache check callback, and nothing has changed, use the old JSON */ |
| var cal, lv; |
| if (state && state.href && typeof json.changed !== 'undefined' && json.changed === false) { |
| json = ponymail_list_json[state.href]; |
| json.cached = true; |
| } else if (state && state.href) { |
| |
| /* Save JSON in cache if new */ |
| ponymail_list_json[state.href] = json; |
| } |
| |
| /* Start by adding the calendar */ |
| if (json.firstYear && json.lastYear) { |
| cal = new Calendar(json.firstYear, json.lastYear, ponymail_month); |
| get('calendar').empty().inject(cal); |
| } |
| return lv = new BasicListView(json); |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/listview_single_email.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * Single email list view - extends BasicListView |
| */ |
| |
| SingleListView = (function(superClass) { |
| extend(SingleListView, superClass); |
| |
| |
| /* json: from stats.lua, rpp = results per page, pos = starting position (from 0) */ |
| |
| function SingleListView(json1, rpp1, pos1) { |
| var date, hd, m, ref, y; |
| this.json = json1; |
| this.rpp = rpp1 != null ? rpp1 : 15; |
| this.pos = pos1 != null ? pos1 : 0; |
| |
| /* Set the header first */ |
| hd = get('header'); |
| if (this.json.list) { |
| if (ponymail_month.length > 0) { |
| ref = ponymail_month.split("-", 2), y = ref[0], m = ref[1]; |
| date = calendar_months[parseInt(m) - 1] + (", " + y); |
| hd.empty().inject([ |
| this.json.list + " (" + date + "):", new HTML('a', { |
| href: "api/mbox.lua?list=" + ponymail_list + "&date=" + ponymail_month, |
| title: "Download as mbox archive" |
| }, new HTML('img', { |
| src: 'images/floppy.svg', |
| style: { |
| marginLeft: "10px", |
| width: "20px", |
| height: "20px", |
| verticalAlign: 'middle' |
| } |
| })) |
| ]); |
| } else { |
| hd.empty().inject(this.json.list + ", past 30 days:"); |
| } |
| } |
| |
| /* Get and clear the list view */ |
| this.lv = get('listview'); |
| this.lv = this.lv.empty(); |
| |
| /* Set some internal vars */ |
| this.listsize = 0; |
| |
| /* If we got results, use scroll() to display from result 0 on */ |
| if (isArray(this.json.emails) && this.json.emails.length > 0) { |
| |
| /* Set some internal vars */ |
| this.listsize = this.json.emails.length; |
| |
| /* Reverse thread struct, but only if we're not using an |
| * already reversed cache |
| */ |
| if (!this.json.cached) { |
| this.json.emails.reverse(); |
| } |
| this.scroll(this.rpp, this.pos); |
| } else { |
| |
| /* No results, just say...that */ |
| this.lv.inject("No emails found matching this criterion."); |
| } |
| ponymail_current_listview = this; |
| return this; |
| } |
| |
| SingleListView.prototype.renderItems = function() { |
| |
| /* For each email result,... */ |
| var item, j, lastitem, len, original, ref; |
| this.lvitems = new HTML('div', { |
| "class": "listview_table" |
| }); |
| lastitem = null; |
| ref = this.json.emails.slice(this.pos, this.pos + this.rpp); |
| for (j = 0, len = ref.length; j < len; j++) { |
| original = ref[j]; |
| |
| /* Be sure we actually have an email here */ |
| if (original) { |
| |
| /* Call listViewItem to compile a list view HTML element */ |
| item = this.listViewItem(original, null); |
| lastitem = item; |
| |
| /* Inject new item into the list view */ |
| this.lvitems.inject(item); |
| } |
| } |
| this.lv.inject(this.lvitems); |
| return lastitem; |
| }; |
| |
| SingleListView.prototype.listViewItem = function(original, thread) { |
| |
| /* Be sure we actually have an email here */ |
| var avatar, date, date_style, item, now, readStyle, sender, sid, subject, uid; |
| if (original) { |
| now = new Date().getTime() / 1000; |
| |
| /* Render the email in the LV */ |
| |
| /* First set some data points for later */ |
| uid = parseInt(Math.random() * 999999999999).toString(16); |
| |
| /* Gravatar */ |
| avatar = new HTML('img', { |
| "class": "gravatar", |
| src: "https://secure.gravatar.com/avatar/" + original.gravatar + ".png?s=24&r=g&d=mm" |
| }); |
| |
| /* Sender, without the <foo@bar> part - just the name */ |
| sender = new HTML('div', { |
| style: { |
| fontWeight: "bold" |
| } |
| }, original.from.replace(/\s*<.+>/, "").replace(/"/g, '')); |
| |
| /* readStyle: bold if new email, normal if read before */ |
| readStyle = "bold"; |
| if (hasRead(original.id)) { |
| readStyle = "normal"; |
| } |
| |
| /* Subject, PLUS a bit of the body with a break before */ |
| sid = shortenURL(original.id); |
| subject = new HTML('div', {}, [ |
| new HTML('a', { |
| style: { |
| fontWeight: readStyle |
| }, |
| href: "thread.html/" + sid, |
| onclick: "readEmail(this.parentNode.parentNode.parentNode); this.style.fontWeight = 'normal'; return false;" |
| }, original.subject) |
| ]); |
| |
| /* Add date; yellow if <= 1day, grey otherwise */ |
| date_style = "listview_grey"; |
| if ((now - 86400 * 4) < original.epoch) { |
| date_style = "listview_yellow"; |
| } |
| date = new HTML('div', { |
| "class": "listview_right " + date_style |
| }, new Date(original.epoch * 1000).ISOBare()); |
| |
| /* Finally, pull it all together in a div and add that to the listview */ |
| item = new HTML('div', { |
| id: uid, |
| data: original.id, |
| "class": "listview_item" |
| }, new HTML('div', { |
| "class": "listview_summary" |
| }, [avatar, sender, subject, date])); |
| return item; |
| } |
| }; |
| |
| return SingleListView; |
| |
| })(BasicListView); |
| |
| ponymail_register_listview('single', 'Single email theme', SingleListView); |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/localstorage.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * Init: Test if localStorage is available or not |
| * If not, fall back to plain global var storage (not effective, but meh) |
| */ |
| |
| pm_storage_available = false; |
| |
| pm_storage_globvar = {}; |
| |
| try { |
| if (typeof window.localStorage !== "undefined") { |
| window.localStorage.setItem("pm_test", "1"); |
| pm_storage_available = true; |
| } |
| } catch (_error) { |
| e = _error; |
| pm_storage_available = false; |
| } |
| |
| |
| /** |
| * dbWrite: Store a key/val pair |
| * Example: dbWrite("ponies", "They are awesome!") |
| */ |
| |
| dbWrite = function(key, value) { |
| |
| /* Can we use localStorage? */ |
| var rv; |
| if (pm_storage_available) { |
| try { |
| rv = window.localStorage.setItem(key, value); |
| return rv; |
| } catch (_error) { |
| e = _error; |
| console.log("Could not save data to DB: " + e); |
| return null; |
| } |
| } else { |
| |
| /* Guess not, fall back to (ineffective) global var */ |
| pm_storage_globvar[key] = value; |
| return true; |
| } |
| }; |
| |
| |
| /* dbRead: Given a key, read the corresponding value from storage */ |
| |
| dbRead = function(key) { |
| |
| /* Do we have localStorage? */ |
| if (pm_storage_available) { |
| return window.localStorage.getItem(key); |
| } else { |
| |
| /* Nope, try global var */ |
| return pm_storage_globvar[key]; |
| } |
| }; |
| |
| hasRead = function(mid) { |
| if (dbRead("ponymail_read_" + mid)) { |
| return true; |
| } else { |
| return false; |
| } |
| }; |
| |
| markRead = function(mid) { |
| dbWrite("ponymail_read_" + mid, new Date().getTime()); |
| return true; |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/misc.coffee |
| ****************************************** |
| */ |
| |
| |
| /** |
| * 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; |
| }; |
| |
| 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"); |
| |
| ponymail_quote_regex = new RegExp("((?:\r?\n)((on .+ wrote:[\r\n]+)|(sent from my .+)|(>+[ \t]+[^\r\n]*\r?\n[^\n]*\n*)+)+)+", "mi"); |
| |
| |
| /** |
| * How many bits (of 7 chars each) do we want in our shortLink? |
| * The more bits, the more precise, the fewer bits, the shorter the link. |
| */ |
| |
| shortBits = 3; |
| |
| |
| /* Shortener: cut MID into pieces, convert to base36 to save 3-4 bytes */ |
| |
| shortenURL = function(mid) { |
| var a, arr, i, j, num, out, ref, res; |
| arr = mid.split("@"); |
| |
| /* IF arr is 2 bits, it's fine to shorten it (meduim/long generator). if 3, then potentially not (short generator) */ |
| if (arr.length === 2 && (pm_config && pm_config.shortLinks)) { |
| out = ""; |
| |
| /* For each bit in $howlongdowewantthis ... */ |
| for (i = j = 0, ref = shortBits - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { |
| |
| /* Cut off 8 chars, convert from base16 to base36 */ |
| a = arr[0].substr(i * 8, 8); |
| num = parseInt(a, 16); |
| res = num.toString(36); |
| |
| /* Padding for small numbers */ |
| while (res.length < 7) { |
| res = '-' + res; |
| } |
| out += res; |
| } |
| return "PZ" + out; |
| } |
| return mid; |
| }; |
| |
| unshortenURL = function(mid) { |
| |
| /* If new format ... */ |
| var i, j, num, o, out, ref, res; |
| if (mid.substr(0, 2) === 'PZ') { |
| out = ""; |
| |
| /* For each 7-char bit, convert from base36 to base16, remove padding */ |
| for (i = j = 0, ref = shortBits - 1; 0 <= ref ? j <= ref : j >= ref; i = 0 <= ref ? ++j : --j) { |
| num = parseInt(mid.substr(2 + (i * 7), 7).replace('-', ''), 36); |
| res = num.toString(16); |
| |
| /* 0-padding for smaller numbers (<8 chars) */ |
| while (res.length < 8) { |
| res = '0' + res; |
| } |
| out += res; |
| } |
| return out; |
| } else if (mid[0] === 'Z' || mid[0] === 'B') { |
| |
| /* Old format from 0.9 and before */ |
| out = ""; |
| |
| /* For each 7-char bit, convert from base36 to base16, remove padding */ |
| for (i = o = 0; o <= 1; i = ++o) { |
| num = parseInt(mid.substr(1 + (i * 7), 7).replace('-', ''), 36); |
| res = num.toString(16); |
| |
| /* 0-padding for smaller numbers (<9 chars) */ |
| while (res.length < 9) { |
| res = '0' + res; |
| } |
| out += res; |
| } |
| return out; |
| } else { |
| return mid; |
| } |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/preferences.coffee |
| ****************************************** |
| */ |
| |
| |
| /* maxLists: default max lists to show in top menu before resorting to 'more lists' */ |
| |
| maxLists = 2; |
| |
| setupAccount = function(json, state) { |
| var domain, j, len, li, list, lists, lmenu, myDomain, number, ref, ref1, sortedList; |
| myDomain = []; |
| |
| /* run parseURL for fetch list and domain */ |
| parseURL(); |
| |
| /* set up the list of...lists */ |
| if (json && isHash(json.lists)) { |
| ref = json.lists; |
| for (domain in ref) { |
| lists = ref[domain]; |
| ponymail_lists[domain] = lists; |
| |
| /* if current domain, set up the top menu to use it */ |
| if (domain === ponymail_domain) { |
| myDomain = lists; |
| } |
| } |
| } |
| |
| /* Are we on list.html? */ |
| if (state.listview) { |
| |
| /* Generate the lists part of the top menu */ |
| lmenu = get('listmenu'); |
| if (lmenu) { |
| |
| /* Make an array of mailing lists */ |
| sortedList = []; |
| for (list in myDomain) { |
| number = myDomain[list]; |
| sortedList.push(list); |
| } |
| |
| /* Sort descending by business */ |
| sortedList.sort((function(_this) { |
| return function(a, b) { |
| if (myDomain[a] < myDomain[b]) { |
| return 1; |
| } else { |
| return -1; |
| } |
| }; |
| })(this)); |
| ref1 = sortedList.slice(0, +(maxLists - 1) + 1 || 9e9); |
| for (j = 0, len = ref1.length; j < len; j++) { |
| list = ref1[j]; |
| li = new HTML('li', {}, new HTML('a', { |
| href: "?" + list + "@" + ponymail_domain, |
| onclick: "listView({month: '', list: '" + list + "@" + ponymail_domain + "'}); return false;" |
| }, list + '@')); |
| lmenu.inject(li); |
| } |
| |
| /* Do we have more lists?? */ |
| if (sortedList.length > maxLists) { |
| |
| /* Remove the first N lists, sort the rest by name */ |
| sortedList.splice(0, maxLists); |
| sortedList.sort(); |
| li = new HTML('li', {}, "More lists ⌄"); |
| lmenu.inject(li); |
| } |
| } |
| |
| /* Call listView to fetch email */ |
| return listView(null, true); |
| } |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/scaffolding.coffee |
| ****************************************** |
| */ |
| |
| |
| /* This is the basic scaffolding for all pages */ |
| |
| headerScaffolding = function() { |
| |
| /* Start off by making the top menu */ |
| var logo, menu, ul; |
| menu = new HTML('div', { |
| id: "topMenu" |
| }); |
| document.body.inject(menu); |
| ul = new HTML('ul', { |
| id: 'listmenu' |
| }); |
| logo = new HTML('li', { |
| "class": 'logo' |
| }, new HTML('a', { |
| href: "./" |
| }, new HTML('img', { |
| src: "images/logo.png", |
| style: { |
| paddingRight: "10px", |
| height: "38px", |
| width: "auto" |
| } |
| }))); |
| ul.inject(logo); |
| return menu.inject(ul); |
| }; |
| |
| footerScaffolding = function() { |
| |
| /* Add a footer */ |
| var footer; |
| footer = new HTML('div', { |
| id: "footer" |
| }); |
| document.body.inject(footer); |
| return footer.inject([ |
| "Powered by ", new HTML('a', { |
| href: 'https://ponymail.incubator.apache.org/' |
| }, "Apache Pony Mail (Incubating) v/" + ponymail_version), ". Copyright 2016, the Apache Software Foundation." |
| ]); |
| }; |
| |
| listviewScaffolding = function() { |
| var calHolder, header, listDiv, mainDiv, qs, r; |
| parseURL(true); |
| |
| /* Header scaffolding */ |
| headerScaffolding(); |
| |
| /* Now, make the base div */ |
| mainDiv = new HTML('div', { |
| id: "contents" |
| }); |
| document.body.inject(mainDiv); |
| |
| /* Quick Search Bar */ |
| qs = quickSearchBar(); |
| mainDiv.inject(qs); |
| |
| /* Make the title */ |
| header = new HTML('h2', { |
| id: "header" |
| }, "Loading list data..."); |
| mainDiv.inject(header); |
| |
| /* Then make the calendar placeholder */ |
| calHolder = new HTML('div', { |
| id: "calendar" |
| }); |
| mainDiv.inject(calHolder); |
| calHolder.inject(new HTML('h3', {}, "Archive:")); |
| |
| /* Finally, make the list view placeholder */ |
| listDiv = new HTML('div', { |
| id: "listview", |
| "class": "sbox" |
| }); |
| mainDiv.inject(listDiv); |
| |
| /* Footer */ |
| footerScaffolding(); |
| |
| /* Make an API call to the preferences script, have it call back to listView once done */ |
| return r = new HTTPRequest("api/preferences.lua", { |
| callback: setupAccount, |
| state: { |
| listview: true |
| } |
| }); |
| }; |
| |
| |
| /* Permalink view callback */ |
| |
| scaffoldingEmailCallback = function(json, state) { |
| e = new ThreadedEmailDisplay(null, null, null, json.thread); |
| }; |
| |
| |
| /* Permalink view */ |
| |
| threadScaffolding = function() { |
| |
| /* Header scaffolding */ |
| var mainDiv, mid, r; |
| headerScaffolding(); |
| |
| /* Now, make the base div */ |
| mainDiv = new HTML('div', { |
| id: "email_placeholder", |
| style: { |
| width: "90%" |
| } |
| }); |
| document.body.inject(mainDiv); |
| |
| /* Footer */ |
| footerScaffolding(); |
| |
| /* Make an API call to the preferences script, have it call back to listView once done */ |
| mid = location.href.match(/thread\.html\/(.+)/)[1]; |
| return r = new HTTPRequest("api/thread.lua?id=" + unshortenURL(mid), { |
| callback: scaffoldingEmailCallback |
| }); |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/search.coffee |
| ****************************************** |
| */ |
| |
| |
| /* Quick Search bar creation */ |
| |
| quickSearchBar = function() { |
| var advanced, button, datedata, input, list, listdata, listname, options, qs, span; |
| qs = new HTML('form', { |
| "class": "quicksearch", |
| onsubmit: 'quickSearch(); return false;' |
| }); |
| |
| /* Cog */ |
| |
| /* The blue search button */ |
| cog = new HTML('input', { |
| type: 'submit', |
| "class": 'qs_cog', |
| title: "Search settings" |
| }); |
| |
| /* Options area */ |
| options = new HTML('div', { |
| "class": 'qs_options' |
| }); |
| |
| /* Timespan to search within */ |
| datedata = "lte=1M"; |
| span = new HTML('a', { |
| id: 'qs_span', |
| data: datedata, |
| href: 'javascript:void(0);' |
| }, "Less than 1 month ago"); |
| |
| /* Lists(s) to search */ |
| listname = 'this list'; |
| listdata = ponymail_listname; |
| if (ponymail_listname.length === 0) { |
| listname = 'all lists'; |
| listdata = "*@*"; |
| } |
| list = new HTML('a', { |
| id: 'qs_list', |
| href: 'javascript:void(0);', |
| data: listdata |
| }, listname); |
| options.inject([span, new HTML('br'), list]); |
| |
| /* Input field for text search */ |
| input = new HTML('input', { |
| type: "text", |
| id: 'qs_input', |
| "class": "qs_input", |
| placeholder: "Search " + listname + "..." |
| }); |
| |
| /* The blue search button */ |
| button = new HTML('input', { |
| type: 'submit', |
| "class": 'qs_button' |
| }); |
| |
| /* Link to advanced search */ |
| advanced = new HTML('a', { |
| href: 'javascript:void(advancedSearch());', |
| "class": "qs_link" |
| }, new HTML('img', { |
| src: 'images/advanced.png', |
| style: { |
| verticalAlign: 'middle', |
| height: "24px", |
| marginTop: "-1px" |
| } |
| })); |
| |
| /* Add it all to the form */ |
| qs.inject(cog); |
| qs.inject(input); |
| qs.inject(button); |
| qs.inject(advanced); |
| return qs; |
| }; |
| |
| |
| /* Quick Search function */ |
| |
| quickSearch = function() { |
| |
| /* Get the QS input */ |
| }; |
| |
| |
| /* |
| ****************************************** |
| Fetched from coffee/test.coffee |
| ****************************************** |
| */ |
| |
| testCoffee = function() { |
| |
| /* Get main div from HTML */ |
| var cal, div, hider, item, j, len, li, logo, menu, p, parent, ref, ul; |
| parent = get('testdiv'); |
| menu = new HTML('div', { |
| id: "topMenu" |
| }); |
| parent.inject(menu); |
| ul = new HTML('ul'); |
| logo = new HTML('li', { |
| "class": 'logo' |
| }, new HTML('a', { |
| href: "./" |
| }, new HTML('img', { |
| src: "images/logo.png", |
| style: { |
| paddingRight: "10px", |
| height: "38px", |
| width: "auto" |
| } |
| }))); |
| ul.inject(logo); |
| ref = ['Home', 'Lists', 'Third item']; |
| for (j = 0, len = ref.length; j < len; j++) { |
| item = ref[j]; |
| li = new HTML('li', {}, item); |
| ul.inject(li); |
| } |
| menu.inject(ul); |
| div = new HTML('div', { |
| "class": "sbox" |
| }); |
| parent.inject(div); |
| cal = new Calendar('2010-5', '2016-9'); |
| div.inject(cal); |
| p = new HTML('p', { |
| "class": "foo", |
| style: { |
| textAlign: 'center' |
| } |
| }, "Text goes here"); |
| div.inject(p); |
| p.inject([". Here's a textNode added afterwards", new HTML('br')]); |
| hider = new HTML('b', { |
| onclick: 'testToggle(this);' |
| }, "Click here to hide this text for a second!"); |
| return p.inject(hider); |
| }; |
| |
| testToggle = function(div) { |
| div.show(); |
| return window.setTimeout(function() { |
| return div.show(); |
| }, 1000); |
| }; |