| /* |
| 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. |
| */ |
| |
| // displayEmail: Shows an email inside a thread |
| function displayEmail(json, id, level) { |
| // Level indicates the nestedness if threaded view (indentation) |
| level = level ? level : 1 |
| if (!json.mid && !json.tid) { |
| alert("404: Could not find this email!") |
| return |
| } |
| if (current_thread_mids[json.mid]) { |
| return |
| } else { |
| current_thread_mids[json.mid] = true |
| current_email_msgs.push(json) |
| } |
| |
| // Save the JSON in our JS array so we don't have to fetch it again later |
| saved_emails[json.mid] = json |
| var estyle = "" |
| last_opened_email = json.mid |
| |
| // color based on view before or not?? |
| if (localStorageAvailable) { |
| if (! window.localStorage.getItem("viewed_" + json.mid) ){ |
| //estyle = "linear-gradient(to bottom, rgba(252,255,244,1) 0%,rgba(233,233,206,1) 100%)" |
| |
| try { |
| window.localStorage.setItem("viewed_" + json.mid, json.epoch) |
| } catch(e) { |
| |
| } |
| } |
| if (window.localStorage.getItem("viewed_" + json.mid) && window.localStorage.getItem("viewed_" + json.mid).search("!") == 10){ |
| //estyle = "linear-gradient(to bottom, rgba(252,255,244,1) 0%,rgba(233,233,206,1) 100%)" |
| var epoch = parseInt(window.localStorage.getItem("viewed_" + json.mid)) |
| try { |
| window.localStorage.setItem("viewed_" + json.mid, epoch + ":") |
| } catch(e) { |
| |
| } |
| } |
| } |
| // Coloring for nested emails |
| var cols = ['primary', 'success', 'info', 'warning', 'danger'] |
| |
| // Sanitise email ID and find the <div> object it's supposed to go into |
| var id_sanitised = id.toString().replace(/@<.+>/, "") |
| var thread = document.getElementById('thread_' + id_sanitised) |
| if (thread) { |
| // transform <foo.bar.tld> to foo@bar.tld |
| var lid = json.list.replace(/[<>]/g, "").replace(/^([^.]+)\./, "$1@") |
| |
| // Escape email body, convert < to < |
| var ebody = json.body |
| if (ebody == null) {ebody = '(null body)'} // temporary hack to deal with broken bodies |
| ebody = ebody.replace(/</mg, "<") |
| ebody = "\n" + ebody // add a newline at top |
| // If we're compacting quotes in the email, let's...do so with some fuzzy logic |
| if (prefs.compactQuotes == 'yes') { |
| ebody = ebody.replace(/((?:\r?\n)((on .+ wrote:[\r\n]+)|(sent from my .+)|(>+([ \t]*[^\r\n]*)?\r?\n)+)+)+/mgi, function(inner) { |
| var rnd = (Math.random() * 100).toString() |
| inner = inner.replace(/>/g, ">") |
| var html = "<div class='bs-callout bs-callout-default' style='margin: 3px; padding: 2px;' id='parent_" + rnd + "'>" + |
| "<img src='" + URL_BASE + "/images/quote.png' title='show/hide original text' onclick='toggleView(\"quote_" + rnd + "\")'/><br/>" + |
| "<div style='display: none;' id='quote_" + rnd + "'>" + inner + "</div></div>" |
| return html |
| }) |
| } |
| |
| // Turn URLs into <a> tags |
| ebody = ebody.replace(re_weburl, "<a href='$1'>$1</a>") |
| |
| // Get theme (social, default etc) if set locally in browser |
| if (localStorageAvailable) { |
| var th = window.localStorage.getItem("pm_theme") |
| if (th) { |
| prefs.theme = th |
| } |
| } |
| |
| // Social theme rendering |
| if (prefs.theme && prefs.theme == "social") { |
| |
| // Date and sender formatting |
| var sdate = new Date(json.epoch*1000).toLocaleString('en-US', { timeZone: 'UTC', weekday: 'long', year: 'numeric', month: 'long', day: 'numeric', hour: 'numeric', minute: 'numeric' }) |
| var fr = json['from'].replace(/"/g, "").replace(/<.+>/, "").replace(/</g, "<") |
| thread.style.background = estyle |
| |
| // Don't indent if we're too deeply nested, it gets weird looking |
| if (level <= 6) { |
| thread.style.marginLeft = "40px" |
| } |
| |
| thread.style.marginTop = "40px" |
| thread.innerHTML = "<img src='https://secure.gravatar.com/avatar/" + json['gravatar'] + ".jpg?s=48&r=g&d=mm' style='vertical-align:middle'/> <b>" + fr + "</b> - " + sdate |
| thread.innerHTML += ' <label class="label label-success" onclick="compose(\'' + json.mid + '\');" style="cursor: pointer; float: right; margin-left: 10px;">Reply</label>' |
| if (level > 1) { |
| thread.innerHTML += ' <a href="javascript:void(0);" onclick="rollup(\'' + id_sanitised + '\');"><label class="label label-primary" title="roll up" style="cursor: pointer; float: right; margin-right: 10px;"><span id="rollup_' + id_sanitised + '" class="glyphicon glyphicon-chevron-up"> </span></label></a> ' |
| } |
| thread.innerHTML += "<br/><br/>" |
| |
| // Make the colored bar to the left that indicates nest level |
| var bclass = "bubble-" + cols[parseInt(Math.random() * cols.length - 0.01)] |
| // append body |
| thread.innerHTML += "<div class='" + bclass + "' style='padding: 8px; font-family: Hack; word-wrap: normal; white-space: pre-wrap; word-break: normal;'>" + ebody + '</div>' |
| |
| // Do we have attachments in this email? |
| if (json.attachments && json.attachments.length > 0) { |
| thread.innerHTML += "<b>Attachments: </b>" |
| for (var a in json.attachments) { |
| // figure out name and size in kb (or bytes if < 1024) |
| var fd = json.attachments[a] |
| var size = parseInt(fd.size/1024) |
| if (size > 0) { |
| size = size.toLocaleString() + " kb" |
| } else { |
| size = fd.size.toLocaleString() + " bytes" |
| } |
| thread.innerHTML += "<a href='" + URL_BASE + "/api/email.lua?attachment=true&id=" + json.tid + "&file=" + fd.hash + "'>" + fd.filename.replace(/</g, "<") + "</a> (" + size + ") " |
| } |
| thread.innerHTML += "<br/>" |
| } |
| // This is for the 'highlight new emails' feature |
| if (thread.hasAttribute("meme")) { |
| thread.scrollIntoView() |
| thread.style.background = "rgba(200,200,255, 0.25)" |
| } |
| } |
| // Default theme |
| else { |
| thread.setAttribute("class", "reply bs-callout bs-callout-" + cols[parseInt(Math.random() * cols.length - 0.01)]) |
| thread.style.background = estyle |
| thread.style.marginTop = "30px" |
| thread.innerHTML += ' <label class="label label-success" onclick="compose(\'' + json.mid + '\');" style="cursor: pointer; float: right; margin-left: 10px;">Reply</label>' |
| thread.innerHTML += ' <a href="' + URL_BASE + '/thread.html/'+(pm_config.shortLinks ? shortenID(json.mid) : encodeURIComponent(json.mid))+'"><label class="label label-warning" style="cursor: pointer; float: right;">Permalink</label></a>' |
| thread.innerHTML += ' <a href="' + URL_BASE + '/api/source.lua/'+json.mid+'"><label class="label label-danger" style="cursor: pointer; float: right; margin-right: 10px;">View Source</label></a> ' |
| if (level > 1) { |
| thread.innerHTML += ' <a href="javascript:void(0);" onclick="rollup(\'' + id_sanitised + '\');"><label class="label label-primary" title="roll up" style="cursor: pointer; float: right; margin-right: 10px;"><span id="rollup_' + id_sanitised + '" class="glyphicon glyphicon-chevron-up"> </span></label></a> ' |
| } |
| |
| |
| thread.innerHTML += "<br/>" |
| //thread.style.border = "1px dotted #666" |
| thread.style.padding = "0px" |
| thread.style.leftpadding = "10px" |
| thread.style.rightpadding = "0px" |
| thread.style.fontFamily = "Hack" |
| |
| var fields = ['From', 'To', 'CC', 'Subject', 'Date'] |
| for (var i in fields) { |
| var key = fields[i] |
| if (json[key.toLowerCase()] != undefined && json[key.toLowerCase()].length > 0) { |
| thread.innerHTML += "<b>" + key + ": </b>" + json[key.toLowerCase()].replace(/</g, "<") + "<br/>" |
| } |
| } |
| if (json.private) { |
| thread.innerHTML += "<font color='#C00'><b>Private: </b> YES</font><br/>" |
| if (level == 1) { |
| thread.style.backgroundImage = "url(/images/private.png)" |
| } |
| } |
| |
| thread.innerHTML += "<b>List: </b><a href='" + URL_BASE + "/list.html?" + lid + "'>" + lid + "</a><br/>" |
| if (json.attachments && json.attachments.length > 0) { |
| thread.innerHTML += "<b>Attachments: </b>" |
| for (var a in json.attachments) { |
| var fd = json.attachments[a] |
| var size = parseInt(fd.size/1024) |
| if (size > 0) { |
| size = size.toLocaleString() + " kb" |
| } else { |
| size = fd.size.toLocaleString() + " bytes" |
| } |
| thread.innerHTML += "<a href='" + URL_BASE + "/api/email.lua?attachment=true&id=" + json.tid + "&file=" + fd.hash + "'>" + fd.filename.replace(/</g, "<") + "</a> (" + size + ") " |
| } |
| thread.innerHTML += "<br/>" |
| } |
| |
| var pv = "" |
| if (json.private) { |
| pv = "background: none !important;" |
| } |
| thread.innerHTML += "<pre style='color: inherit; padding: 8px; font-family: Hack; word-wrap: normal; white-space: pre-wrap; word-break: normal; " + pv + "'>" + ebody + '</pre>' |
| |
| // Same as with social theme - "highlight new emails" |
| if (thread.hasAttribute("meme")) { |
| thread.scrollIntoView() |
| thread.style.background = "rgba(200,200,255, 0.25)" |
| } |
| } |
| } else { |
| alert("Error, " + id + " not found :(") |
| } |
| } |
| |
| |
| // displaySingleEmail: shows a single email. Used for permalinks |
| function displaySingleEmail(json, id) { |
| |
| var thread = document.getElementById('email') |
| if (thread) { |
| if (localStorageAvailable) { |
| if (! window.localStorage.getItem("viewed_" + json.id) ){ |
| estyle = "background: background: linear-gradient(to bottom, rgba(252,255,244,1) 0%,rgba(233,233,206,1) 100%);" |
| try { |
| window.localStorage.setItem("viewed_" + json.id, latestEmailInThread + "!") |
| } catch(e) { |
| |
| } |
| } |
| } |
| thread.setAttribute("class", "reply bs-callout bs-callout-info") |
| thread.innerHTML = '' |
| thread.style.padding = "5px" |
| thread.style.fontFamily = "Hack" |
| if (json.error) { |
| thread.innerHTML = "<h4>Error: " + json.error + "</h4>" |
| return; |
| } |
| var fields = ['From', 'To', 'Subject', 'Date'] |
| var fields = ['From', 'To', 'CC', 'Subject', 'Date'] |
| for (var i in fields) { |
| var key = fields[i] |
| if (json[key.toLowerCase()] != undefined) { |
| thread.innerHTML += "<b>" + key + ": </b>" + json[key.toLowerCase()].replace(/</g, "<") + "<br/>" |
| } |
| } |
| if (json.private) { |
| thread.innerHTML += "<font color='#C00'><b>Private list: </b> YES</font><br/>" |
| } |
| var lid = json.list.replace(/[<>]/g, "").replace(/^([^.]+)\./, "$1@") |
| |
| var ebody = json.body |
| if (ebody == null) {ebody = '(null body)'} // temporary hack to deal with broken bodies |
| ebody = ebody.replace(/</, "<") |
| ebody = "\n" + ebody |
| if (true) { |
| ebody = ebody.replace(/(?:\r?\n)((>+[ \t]+[^\r\n]*\r?\n+)+)/mg, function(inner) { |
| var rnd = (Math.random() * 100).toString() |
| var html = "<div class='bs-callout bs-callout-default' style='padding: 2px;' id='parent_" + rnd + "'>" + |
| "<img src='" + URL_BASE + "/images/quote.png' title='show/hide original text' onclick='toggleView(\"quote_" + rnd + "\")'/><br/>" + |
| "<div style='display: none;' id='quote_" + rnd + "'>" + inner + "</div></div>" |
| return html |
| }) |
| } |
| |
| ebody = ebody.replace(re_weburl, "<a href=\"$1\">$1</a>") |
| |
| thread.innerHTML += "<b>List ID: </b><a href='" + URL_BASE + "/list.html?" + lid + "'>" + lid + "</a><br/>" |
| thread.innerHTML += "<br/><pre style='font-family: Hack;'>" + ebody + '</pre>' |
| } else { |
| alert("Error, " + id + " not found :(") |
| } |
| } |
| |
| |
| |
| |
| // displayEmailThreaded: Appends an email to a threaded display of a topic |
| function displayEmailThreaded(json, state, threadobj) { |
| var level = state.level ? state.level : 1 |
| var b = state.before |
| var cobj = document.getElementById("thread_" + b.toString().replace(/@<.+>/, "")) |
| var obj = (threadobj && (typeof threadobj).match(/object/i)) ? threadobj : ((cobj && (typeof cobj).match(/object/i)) ? cobj : document.getElementById("thread_" + state.main)) |
| if (!json.mid && !json.tid) { |
| if (obj) { |
| obj.innerHTML = "<h2>404!</h2><p>Sorry, we couldn't find this email :(" |
| } |
| return |
| } |
| if (state.main == json.mid || state.main == json.tid) { |
| return |
| } |
| saved_emails[json.mid] = json |
| if (obj) { |
| var eobj = document.getElementById("thread_" + (json.mid ? json.mid : json.tid).toString().replace(/@<.+>/, "")) |
| var node = eobj ? eobj : document.createElement('div') |
| node.setAttribute("epoch", json.epoch.toString()) |
| node.style.marginBottom = "20px"; |
| node.setAttribute("id", "thread_" + (json.mid ? json.mid : json.tid).toString().replace(/@<.+>/, "")) |
| node.style.display = "block" // hack so openEmail will state that there's an email open. |
| if (json.mid != b) { |
| |
| if (state.pchild && document.getElementById("thread_" + state.pchild.toString().replace(/@<.+>/, ""))) { |
| var pc = document.getElementById("thread_" + state.pchild.toString().replace(/@<.+>/, "")) |
| try { |
| if (prefs.sortOrder == 'forward') { |
| obj.insertAfter(pc, node) |
| } else { |
| obj.insertBefore(pc, node) |
| } |
| } catch (e) { |
| obj.appendChild(node) |
| } |
| } else { |
| if (prefs.sortOrder == 'forward') { |
| obj.appendChild(node) |
| } else { |
| obj.insertBefore(node, obj.firstChild) |
| } |
| } |
| displayEmail(json, (json.tid ? json.tid : json.mid), level) |
| } else { |
| document.getElementById("thread_" + state.main).appendChild(node) |
| } |
| if (state.child && state.child.children && state.child.children.length > 0) { |
| getChildren(state.main, state.child, level, node) |
| } |
| } else { |
| alert("Could not find parent object, thread_" + state.main) |
| } |
| } |
| |
| |
| |
| // toggleEmails_threaded: Open up a threaded display of a topic |
| function toggleEmails_threaded(id, close, toverride, threadobj) { |
| current_thread_mids = {} |
| current_email_msgs = [] |
| var thread = threadobj ? threadobj : document.getElementById('thread_' + id.toString().replace(/@<.+>/, "")) |
| if (thread) { |
| current_thread = id |
| if (localStorageAvailable) { |
| var epoch = latestEmailInThread + "!" |
| if (current_thread_json[id]) { |
| var xx = window.localStorage.getItem("viewed_" + current_thread_json[id].tid) |
| if (xx) { |
| var yy = parseInt(xx) |
| if (yy >= parseInt(latestEmailInThread)) { |
| epoch = yy |
| } |
| } |
| try { |
| window.localStorage.setItem("viewed_" + current_thread_json[id].tid, epoch) |
| } catch(e) { |
| |
| } |
| } |
| } |
| |
| thread.style.display = (thread.style.display == 'none') ? 'block' : 'none'; |
| // Bail if we can't find the thread struct |
| if (!current_thread_json[id]) { |
| return; |
| } |
| var helper = document.getElementById('helper_' + id) |
| if (!helper) { |
| helper = document.createElement('div') |
| helper.setAttribute("id", "helper_" + id) |
| helper.style.padding = "10px" |
| thread.parentNode.insertBefore(helper, thread) |
| } |
| |
| if (prefs.groupBy == 'thread' && !(toverride == true)) { |
| // View as flat |
| helper.innerHTML = '<label style="padding: 4px; font-size: 10pt; cursor: pointer; float: right;" class="label label-info" onclick="prefs.groupBy=\'date\'; toggleEmails_threaded(' + id + ', true); toggleEmails_threaded(' + id + ', false, true); sortByDate(' + id + ');" style="cursor: pointer; float: right;">Click to view as flat thread, sort by date</label> ' |
| |
| // Highlight new emails since last view |
| helper.innerHTML += '<label style="margin-right: 10px; padding: 4px; font-size: 10pt; cursor: pointer; float: right;" class="label label-success" onclick="highlightNewEmails('+id+');" style="cursor: pointer; float: right;">Highlight new messages</label> ' |
| } else { |
| helper.innerHTML = '<label style="padding: 4px; font-size: 10pt; cursor: pointer; float: right;" class="label label-info" onclick="prefs.groupBy=\'thread\'; toggleEmails_threaded(' + id + ', true);toggleEmails_threaded(' + id + ');" style="cursor: pointer; float: right;">Click to view as nested thread</label> ' |
| } |
| // time travel magic! |
| var ml = findEml(current_thread_json[id].tid) |
| if (!current_thread_json[id].magic && ml.irt && ml.irt.length > 0) { |
| helper.innerHTML += "<p id='magic_"+id+"'><i><b>Note:</b> You are viewing a search result/aggregation in threaded mode. Only results matching your keywords or dates are shown, which may distort the thread. For the best result, go to the specific list and view the full thread there, or view your search results in flat mode. Or we can <a href='javascript:void(0);' onclick='timeTravelList("+id+")'>do some magic for you</a>.</i></p>" |
| // Why was this here?? |
| /* |
| var btn = document.createElement('a') |
| btn.setAttribute("href", "javascript:void(0);") |
| btn.setAttribute("class", "btn btn-success") |
| btn.setAttribute("onclick", "prefs.displayMode='flat'; buildPage();") |
| btn.style.marginRight = "10px" |
| btn.innerHTML = "View results in flat mode instead" |
| helper.appendChild(btn) |
| */ |
| } else if (!current_thread_json[id].magic) { |
| helper.innerHTML += "<p id='magic_"+id+"'></p>" |
| } |
| |
| if (close == true) { |
| thread.style.display = 'none' |
| } |
| if (thread.style.display == 'none') { |
| helper.style.display = 'none' |
| prefs.groupBy = 'thread' // hack for now |
| thread.innerHTML = "" |
| if (document.getElementById('bubble_' + id)) { |
| document.getElementById('bubble_' + id).style.display = 'block' |
| } |
| return |
| } else { |
| helper.style.display = 'block' |
| if (document.getElementById('bubble_' + id)) { |
| document.getElementById('bubble_' + id).style.display = 'none' |
| } |
| } |
| if (!open_emails[id]) { |
| open_emails[id] = true |
| |
| } |
| var eml = saved_emails[current_thread_json[id].tid] |
| if (!eml || !eml.from) { |
| GetAsync("/api/email.lua?id=" + current_thread_json[id].tid, { |
| blockid: id, |
| thread: current_thread_json[id], |
| object: threadobj, |
| }, loadEmails_threaded) |
| } else { |
| loadEmails_threaded(eml, { |
| blockid: id, |
| thread: current_thread_json[id], |
| object: threadobj |
| }) |
| } |
| } |
| } |
| |
| // func for highlighting emails that have shown up during a recent page build, that we haven't |
| // actually viewed before. |
| function highlightNewEmails(id) { |
| // This currently requires localStorage to store the view data |
| if (localStorageAvailable) { |
| kiddos = [] |
| var t = document.getElementById("thread_" + id) |
| if (t) { |
| traverseThread(t, 'thread') // find all child elements called 'thread*' |
| // For each email in this thread, check (or set) when it was first viewed |
| for (var i in kiddos) { |
| var mid = kiddos[i].getAttribute("id") |
| var epoch = window.localStorage.getItem("first_view_" + mid) |
| if (epoch && epoch != pb_refresh) { // did we view this before the last page build? |
| kiddos[i].style.color = "#AAA" |
| } else { // never seen it before, have it at normal color and set the first-view-date |
| |
| try { |
| window.localStorage.setItem("first_view_" + mid, pb_refresh) |
| } catch(e) { |
| |
| } |
| kiddos[i].style.color = "#000" |
| } |
| } |
| } |
| } |
| } |
| |
| function displaySingleThread(json) { |
| if (json && json.emails && json.emails[0]) { |
| current_thread_json = [json.emails[0]] |
| current_flat_json = json.emails |
| } |
| var thread = document.getElementById('thread_0') |
| thread.innerHTML = "" |
| var helper = document.createElement('div') |
| helper.setAttribute("id", "helper_0") |
| thread.appendChild(helper) |
| |
| // Sometimes emails are hidden for anonymous users, let's make 'em know... |
| if (!current_thread_json[0]) { |
| popup("Email not found!", |
| ["Sorry, it seems like we couldn't find this email for you.", |
| "It may be private and hidden for non-authenticated users.", |
| "In which case you could <a href='" + URL_BASE + "/oauth.html'>Log in</a> and see if that helps."], |
| 60) |
| } |
| var mid = current_thread_json[0].mid.replace(/[<>]/g, "") |
| if (mid.length > 40) { |
| mid = mid.substring(0,40) + "..." |
| } |
| // set tab title |
| document.title = current_thread_json[0].subject + " - Pony Mail" |
| |
| // Set up for reply-to pane if not present already (for permalink view) |
| last_opened_email = current_thread_json[0].eid |
| if (!saved_emails[last_opened_email]) { |
| saved_emails[last_opened_email] = current_thread_json[0] |
| xlist = current_thread_json[0].list |
| } |
| |
| |
| helper.innerHTML = "<h4 style='margin: 0px; padding: 5px;'>Viewing email #" + mid + " (and replies):</h4>" |
| if (prefs.groupBy == 'thread') { |
| helper.innerHTML += '<label style="padding: 4px; font-size: 10pt; cursor: pointer; float: right;" class="label label-info" onclick="prefs.groupBy=\'date\'; displaySingleThread();" style="cursor: pointer; float: right;">Click to view as flat thread, sort by date</label> ' |
| } else { |
| helper.innerHTML += '<label style="padding: 4px; font-size: 10pt; cursor: pointer; float: right;" class="label label-info" onclick="prefs.groupBy=\'thread\'; displaySingleThread();" style="cursor: pointer; float: right;">Click to view as nested thread</label> ' |
| } |
| if (current_thread_json[0]['in-reply-to']) { |
| helper.innerHTML += '<div class="alert alert-warning" style="margin-top: 10px;"><p><b>Notice!</b><br>This appears to not be the first email in this thread (it has <q><b>in-reply-to</b></q> set).<br/>If you like, we can try to find the first email in the thread for you:<br/><a href="javascript:void(0);" style="font-size: 10pt; cursor: pointer;" onclick="timeTravelSingleThread();" style="cursor: pointer; " class="btn btn-success">Go to the first email in this thread</a> </p></div>' |
| } |
| |
| loadEmails_threaded(current_thread_json[0], { |
| blockid: 0, |
| thread: current_thread_json[0] |
| }) |
| if (prefs.groupBy != 'thread') { |
| sortByDate(0) |
| } |
| } |
| |
| |
| // getSingleThread: fetch a thread from ES and go to callback |
| function getSingleThread(id) { |
| GetAsync("/api/thread.lua?id=" + id, null, displaySingleThread) |
| } |