Last.fm - Shoutbox Quick Reply
By kepp
—
Last update Oct 21, 2007
—
Installed
3,375 times.
// ==UserScript==
// @name Last.fm - Shoutbox Quick Reply
// @namespace http://lastfm.shoutbox.quick.reply/kepp/
// @description Allows you to reply to posts in your shoutbox from your shoutbox
// @include http://www.last.fm/user/*
// @include http://www.last.fm/dashboard/*
// ==/UserScript==
/**
* v1.1.3 (10/20/2007)
* - Fix for breakage on some profiles??
* - Moved reply note toggle into page
* - Updated version history with the correct numbers...
* - Fix for change of script location/getting user's resid
*
* v1.1.2 (3/04/2007)
* - Fix so it handles users with ? in their username
*
* v1.1.1 (1/26/2007)
* - Fix so it works on hosts other than "www.last.fm"
*
* v1.1 (11/16/2006)
* - Copy checkbox checked state is persisted
* - Copied replies are no longer truncated too short
*
* v1.0.7 (11/16/2006)
* - Site update fixes, no more image reply button :(
*
* v1.0.6 (10/31/2006)
* - Added support for the shoutbox popup
*
* v1.0.5 (10/23/2006)
* - Update for site changes
*
* v1.0.4 (9/19/2006)
* - More cosmetic changes
* - Fixed for cached and new data not being handled the same
*
* v1.0.3 (9/16/2006)
* - Cosmetic changes
*
* v1.0.2 (9/15/2006)
* - Woops, fix for dashboard page
*
* v1.0.1 (9/15/2006)
* - Updated for site changes
*
* v1.0 (9/04/2006)
* - Tf the copy checkbox is checked the reply is copied back to your shoutbox
* - There's a pref in the menu so a note is added to copied replies
* - Clicking the reply button toggles the reply box
* - Clicking the cancel button toggles the reply box and resets it
* - In the textarea,
* esc: resets and hides the reply box, like pressing the cancel button
* shift + esc: hides the reply box
* ctrl + enter: sends the reply
*
*
* bugs? -> kepp on Last.fm
**/
const PREFIX = "gm_shoutbox_quick_reply_";
const MAX_LENGTH = 400;
const SENDING_SRC = "http://static.last.fm/tageditor/progress_active.gif";
const TOGGLE_SRC = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAA4AAAALCAMAAAB4W0xQAAAAElBMVEXX19eMjIxwcHDx8fH///////+2TxzQAAAABnRSTlP//////wCzv6S/AAAAP0lEQVR42lXLQQ6AUAwC0dLf3v/KDmiMzoLkLaj9FUr6UDMvrTOi0DrjJIiefKmuG0VgMegEMdoEPWaCuGG6ALeMAh6VC6SsAAAAAElFTkSuQmCC";
const LOCALE = {en: ["Reply to %#", "reply", "Saving...", "%# characters used",
"Copy", "Reply", "Cancel", "Reply note: ", "on", "off"]};
var s = getStrings();
function getStrings() {
var lang = document.documentElement.getAttribute("lang");
if (lang in LOCALE) {
return LOCALE[lang];
}
return LOCALE["en"];
}
function $x(query, context, array) {
var result = document.evaluate(query, (context || document), null,
XPathResult.ANY_TYPE, null);
if (array) {
var nodes = new Array(), node;
while (node = result.iterateNext()) {
nodes.push(node);
}
return nodes;
}
switch (result.resultType) {
case result.STRING_TYPE: return result.stringValue;
case result.NUMBER_TYPE: return result.numberValue;
case result.BOOLEAN_TYPE: return result.booleanValue;
}
return result.iterateNext();
}
function cacheResId(user, resId) {
GM_setValue(user + "_resId", resId);
}
function getCachedResId(user) {
return GM_getValue(user + "_resId");
}
// get the resid of a user from their profile data feed
function getResId(user, element) {
try {
var resId = getCachedResId(user);
if (!resId) {
GM_xmlhttpRequest({
method: "GET",
url: "http://ws.audioscrobbler.com/1.0/user/" +
encodeURIComponent(user) + "/profile.xml",
onload: function(details) {
var profile = new XML(details.responseText
.replace(/^<\?xml version[^]+?>/, ""));
resId = Number(profile.@id);
cacheResId(user, resId);
element.setAttribute("resid", user + "=" + resId);
}
});
} else {
element.setAttribute("resid", user + "=" + resId);
}
} catch (e) {
GM_log("getResId:" + e);
}
}
// used to get your own resid
function getUserResId(user, pageType, element) {
try {
var resId = getCachedResId(user);
if (resId) {
element.setAttribute("resid", resId);
} else {
if (pageType == "dashboard") {
getResId(user, element);
} else {
var scriptsPath = "/html/body//*[@id='shoutboxPanel']/following-sibling::script";
var scripts = $x(scriptsPath);
resId = scripts.innerHTML.match(/resid=(\d+)/)[1];
GM_setValue(user + "_resId", resId);
element.setAttribute("resid", user + "=" + resId);
}
}
} catch (e) {
GM_log("getUserResId:" + e);
}
}
// image link to toggle the display of the reply form
function addReplyLink(shoutboxMsg) {
try {
addReplyForm(shoutboxMsg);
var poster = $x("a/strong/span", shoutboxMsg).textContent;
var replyInfo = s[0].replace("%#", poster);
var replyLink = document.createElement("a");
replyLink.innerHTML = s[1];
replyLink.href = "javascript:;";
replyLink.title = replyInfo;
replyLink.className = PREFIX + "reply_link";
replyLink.addEventListener("click", toggleReplyForm, true);
var date = $x("small[last()]", shoutboxMsg);
date.innerHTML = date.innerHTML.replace(/\s+$/, " - ")
date.appendChild(replyLink);
} catch (e) {
GM_log("addReplyLink:" + e);
}
}
// add the reply forms to the shoutboxes, initially hidden
function addReplyForm(shoutboxMsg) {
try {
var replyForm = createReplyForm();
var replyStatus = createReplyStatus();
var textArea = createTextArea(replyForm);
var commands = createCommands(replyForm);
replyForm.addEventListener("DOMAttrModified", function(event) {
updateStatus(event, replyForm); }, false);
textArea.addEventListener("keyup", function(event) {
setTimeout(function(event, replyForm) { // lag reducer
updateStatus(event, replyForm); }, 0, event, replyForm)
}, false);
replyForm.appendChild(replyStatus);
replyForm.appendChild(textArea);
replyForm.appendChild(commands);
shoutboxMsg.appendChild(replyForm);
} catch (e) {
GM_log("addReplyForm:" + e);
}
}
// image used to indicate the message is being sent
function addSendingImage(shoutboxMsg) {
try {
var sendingBox = document.createElement("div");
sendingBox.className = PREFIX + "sending";
sendingBox.style.display = "none";
var sendingImage = document.createElement("img");
sendingImage.src = SENDING_SRC;
sendingImage.alt = "Sending image";
sendingBox.appendChild(sendingImage);
sendingBox.appendChild(document.createTextNode(s[2]));
shoutboxMsg.appendChild(sendingBox);
} catch (e) {
GM_log("addSendingImage:" + e);
}
}
// create the container form for all the stuff to send a reply
function createReplyForm(boxClass) {
var replyForm = document.createElement("form");
replyForm.className = PREFIX + "reply_form";
replyForm.style.display = "none";
return replyForm;
}
// create a status label for the status of your reply, this includes a
// character count and a loading icon
function createReplyStatus() {
var replyStatus = document.createElement("div");
replyStatus.className = PREFIX + "status";
var loadStatus = document.createElement("img");
loadStatus.src = SENDING_SRC;
var charCount = document.createElement("span");
charCount.innerHTML = s[3].replace("%#", "0/" + MAX_LENGTH);
replyStatus.appendChild(loadStatus);
replyStatus.appendChild(charCount);
return replyStatus;
}
// create the text box to type in
function createTextArea(replyForm) {
var textArea = document.createElement("textarea");
textArea.addEventListener("keyup", function(event) {
if (event.ctrlKey && event.keyCode == 13) {
processReply(replyForm);
} else if (event.shiftKey && event.keyCode == 27) {
replyForm.style.display = "none";
} else if (event.keyCode == 27) {
resetReplyForm(replyForm);
}
}, false);
return textArea;
}
// create a form input field, used to make the cancel and reply buttons
function createInput(type, value) {
var input = document.createElement("input");
input.type = type;
if (value) { input.value = value; }
return input;
}
// make a checkbox to select if you want to copy the reply to yourself
function createCopySelect() {
var copySelect = document.createElement("span");
var checkbox = createCopyCheckbox();
var copyNote = createCopyNote(checkbox);
copySelect.insertBefore(checkbox, copySelect.firstChild);
copySelect.appendChild(copyNote);
return copySelect;
}
function createCopyCheckbox() {
var checkbox = document.createElement("input");
checkbox.type = "checkbox";
checkbox.addEventListener("change", function(event) {
GM_setValue("copy", Boolean(event.target.checked));
}, false);
checkbox.addEventListener("keypress", function(event) {
if (event.charCode == 43) {
checkbox.checked = true;
} else if (event.charCode == 45) {
checkbox.checked = false;
}}, false);
return checkbox;
}
function createCopyNote(checkbox) {
var noteToggle = document.createElement("a");
noteToggle.href = "javascript:;";
noteToggle.innerHTML = s[4];
noteToggle.addEventListener("click", function(event) {
var checked = !checkbox.checked;
checkbox.checked = checked;
GM_setValue("copy", checked);
}, true);
return noteToggle;
}
function createCommands(replyForm) {
var copySelect = createCopySelect();
var replyButton = createInput("button", s[5]);
replyButton.disabled = true;
var cancelButton = createInput("button", s[6]);
replyButton.addEventListener("click", function(event) {
processReply(replyForm); }, false);
cancelButton.addEventListener("click", function(event) {
resetReplyForm(replyForm); }, false);
var commands = document.createElement("div");
commands.appendChild(copySelect);
var buttons = document.createElement("span");
buttons.appendChild(cancelButton);
buttons.appendChild(replyButton);
commands.appendChild(buttons);
return commands;
}
// update the status label and reply button according to the current state
function updateStatus(event, replyForm) {
try {
var textArea = $x("textarea", replyForm);
var label = $x("div[1]/span", replyForm);
var replyButton = $x("div[2]/span[2]/input[2]", replyForm);
var hasIds = hasResIds(textArea.parentNode);
// color the textarea and label
if (textArea.value.length > MAX_LENGTH) {
textArea.style.backgroundColor = "#D01F3C";
replyButton.disabled = true;
} else if (textArea.value.length > 0) {
textArea.style.backgroundColor = "white";
replyButton.disabled = !hasIds;
} else {
// need this for the initial showing of a fresh form
replyButton.disabled = !hasIds;
}
if (hasIds) { // hide the loading image
$x("div/img", replyForm).style.visibility = "hidden";
}
// update the character count as stuff is typed
if (event.target.localName == "TEXTAREA") {
label.innerHTML = textArea.value.length + "/" + MAX_LENGTH;
}
} catch (e) {
GM_log("updateStatus:" + e);
}
}
// test if the resids have been retrieved
function hasResIds(replyForm) {
try {
var hasPosterResId = replyForm.hasAttribute("resid");
var hasUserResId = true;
if ($x("div[2]/span/input", replyForm).checked) {
hasUserResId = $x("../../..", replyForm).hasAttribute("resid");
}
return (hasPosterResId && hasUserResId);
} catch (e) {
GM_log("hasResIds:" + e);
}
}
// send your reply to the other user's shoutbox, and your own if selected
function processReply(replyForm) {
var message = $x("textarea", replyForm).value;
var copyCheckbox = $x("div[2]/span/input", replyForm);
var poster = replyForm.getAttribute("resid").split("=");
if (copyCheckbox.checked) {
sendReply(message, poster[1], replyForm, false);
// add the note indicating it's a reply
// auto-truncate the message if needed
var me = $x("../../../@resid", replyForm).value;
var note = "\u25BA " + poster[0] + ": ";
if (GM_getValue("add_reply_note", false)) {
message = note + message.slice(0, MAX_LENGTH - note.length);
}
setTimeout(function() { // there's some kind of flood protection?
sendReply(message, me, replyForm, true); }, 5000);
} else {
sendReply(message, poster[1], replyForm, true);
}
}
function sendReply(message, resId, replyForm, last) {
setStatusSending(true, replyForm);
message = encodeURIComponent(message);
GM_xmlhttpRequest({
method: "POST",
headers:{ "Content-type": "application/x-www-form-urlencoded" },
url: "http://" + location.host + "/shoutbox/",
data: "message=" + message + "&restype=4&resid=" + resId + "&lang=en",
onload: function(details) {
if (last) { // assume that it's done
setStatusSending(false, replyForm);
if ($x("div[2]/span/input", replyForm).checked) {
prependShout(replyForm, details.responseText);
}
}
}
});
}
function setStatusSending(show, replyForm) {
if (show) {
replyForm.style.display = "none";
replyForm.nextSibling.style.display = "block";
} else {
replyForm.nextSibling.style.display = "none";
resetReplyForm(replyForm);
}
}
function prependShout(replyForm, html) {
var firstShout = $x("../../li", replyForm);
var newShout = document.createElement("div");
newShout.innerHTML = html;
if (firstShout.id == "insertShoutBeforeHere") { // insert new copied shout
newInsertBefore = document.createElement("div");
newInsertBefore.id = "insertShoutBeforeHere";
firstShout.id = "shoutmsgMeow23";
firstShout.parentNode.insertBefore(newInsertBefore, firstShout);
firstShout.parentNode.insertBefore(newShout, firstShout);
} else {
firstShout.parentNode.replaceChild(newShout, firstShout);
}
}
// cancel the reply you were typing and reset the reply form
// this is used when the cancel button is clicked and on the succcessful
// posting of a reply
function resetReplyForm(replyForm) {
replyForm.style.display = "none";
var charCount = $x("div/span", replyForm);
var textArea = $x("textarea", replyForm);
var copyCheckbox = $x("div[2]/span/input", replyForm);
// reset the character count, textarea, and checkbox out of sight
window.setTimeout(function() {
copyCheckbox.checked = false;
textArea.style.backgroundColor = "";
textArea.value = "";
// changing the above calls updateStatus, etc, so keep this after
charCount.style.color = "";
charCount.innerHTML = s[3].replace("%#", "0/" + MAX_LENGTH);
}, 0);
}
// toggle whether the reply form is shown or hidden
function toggleReplyForm(event) {
event.stopPropagation();
event.preventDefault();
var shoutboxMsg = this.parentNode.parentNode; // <_<
// toggle display
var formClass = PREFIX + "reply_form";
var replyForm = $x("form[@class='" + formClass + "']", shoutboxMsg);
if (replyForm.style.display == "none") {
// get the id of the person who posted
var poster = $x("a/strong/span", shoutboxMsg).textContent;
getResId(poster, replyForm);
var copy = $x("div[2]/span/input", replyForm);
copy.checked = GM_getValue("copy", false);
replyForm.style.display = "block";
$x("textarea", replyForm).focus();
} else {
replyForm.style.display = "none";
}
}
function addToggle() {
var toggle = document.createElement("div");
toggle.innerHTML = s[7];
var status = document.createElement("a");
status.href = "";
status.textContent = GM_getValue("add_reply_note", true) ? s[8] : s[9];
status.addEventListener("click", function(event) {
event.preventDefault();
var setting = GM_getValue("add_reply_note", false);
GM_setValue("add_reply_note", !setting);
this.textContent = setting ? s[9] : s[8];
}, false);
toggle.appendChild(status);
$x("id('shoutPostToggler')|//dd[@id='shoutboxPanel']").appendChild(toggle);
}
(function() {
var css =
"." + PREFIX + "status span {" +
" float: left; }" +
"." + PREFIX + "status {" +
" padding-top: 5px; }" +
"." + PREFIX + "status img {" +
" float: right;" +
" height: 16px; }" +
"." + PREFIX + "sending {" +
" text-align: center;" +
" height: 75px;" +
" padding: 5px;" +
" margin: 10px 0 0 -5px; }" +
"." + PREFIX + "sending img {" +
" vertical-align: middle;" +
" padding: 4px; }" +
"." + PREFIX + "reply_form textarea {" +
" width: 160px;" +
" height: 64px; }" +
"." + PREFIX + "reply_form input[type=button] {" +
" font-size: 9px;" +
" height: 18px;" +
" width: 45px;" +
" padding: 0 0 15px 0;" +
" margin: 1px 1px 0 0;}" +
"." + PREFIX + "reply_form div:last-child span:first-child {" +
" float: left; }" +
"." + PREFIX + "reply_form div:last-child {" +
" height: 18px; }" +
"." + PREFIX + "reply_form div:last-child span:last-child {" +
" float: right; }" +
"." + PREFIX + "links {" +
" width: 27px;" +
" clear: both;" +
" float: right; }";
GM_addStyle(css);
// get your username from the panel in the upper right, if it's there
try {
var user = $x("id('profileImage')/@href").value.split("/")[2];
} catch (e) {}
// get the username of the page owner, and the page type, from the url
var url = location.href.split("/");
var owner = url[4].match(/[^?]+/);
var pageType = url[3];
if (user == owner || pageType == "dashboard") {
try {
// get the shoutbox messages or the most recent shout
var shoutbox = $x("id('c_shoutboxPanel')|//dd[@id='shoutboxPanel']");
var messages = $x("ul/li[a]", shoutbox, true);
getUserResId(user, pageType, shoutbox); // get your user id
for each (var message in messages) {
addReplyLink(message); // toggle for the reply boxes
addSendingImage(message); // sending status image
}
addToggle();
} catch (e) {
GM_log("anon:" + e);
}
}
})();