/*· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·
· ·
· ·
· Q V I T T E R ·
· ·
· ·
· \\\\_\ ·
· \\) \____) ·
· ·
· ·
· @licstart The following is the entire license notice for the ·
· JavaScript code in this page. ·
· ·
· Copyright (C) 2015 Hannes Mannerheim and other contributors ·
· ·
· ·
· This program is free software: you can redistribute it and/or modify ·
· it under the terms of the GNU Affero General Public License as ·
· published by the Free Software Foundation, either version 3 of the ·
· License, or (at your option) any later version. ·
· ·
· This program is distributed in the hope that it will be useful, ·
· but WITHOUT ANY WARRANTY; without even the implied warranty of ·
· MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ·
· GNU Affero General Public License for more details. ·
· ·
· You should have received a copy of the GNU Affero General Public License ·
· along with this program. If not, see . ·
· ·
· @licend The above is the entire license notice ·
· for the JavaScript code in this page. ·
· ·
· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */
/* ·
·
· Build a menu for a stream, if there is any to build
·
· Stream menus currently support three row types: divider, functions and profile-prefs-toggles
· They are defined in stream-router.js. Function rows run the function in
· the function attribute when clicked. Profile-prefs-toggle rows toggles the
· preference in the attribute when clicked.
·
· @param streamObject: stream object returned by pathToStreamRouter()
·
· · · · · · · · · */
function streamObjectGetMenu(streamObject) {
if(typeof streamObject == 'undefined') {
return false;
}
if(streamObject.menu === false) {
return false;
}
var menuHTML = buildMenuTop();
$.each(streamObject.menu,function(){
if(this.type == 'divider') {
menuHTML = menuHTML + buildMenuDivider();
}
else if(this.type == 'function') {
menuHTML = menuHTML + buildMenuRowFullwidth(this.label, {
class: 'row-type-' + this.type,
'data-menu-row-type': this.type,
'data-function-name': this.functionName
});
}
else if(this.type == 'profile-prefs-toggle') {
// only prefs in the qvitter namespace is supported
if(this.namespace == 'qvitter') {
// enabled?
var prefEnabledOrDisabled = 'disabled';
if(typeof window.qvitterProfilePrefs[this.topic] != 'undefined'
&& window.qvitterProfilePrefs[this.topic] !== null
&& window.qvitterProfilePrefs[this.topic] != ''
&& window.qvitterProfilePrefs[this.topic] !== false
&& window.qvitterProfilePrefs[this.topic] != 0
&& window.qvitterProfilePrefs[this.topic] != '0') {
prefEnabledOrDisabled = 'enabled';
}
// get row html
menuHTML = menuHTML + buildMenuRowFullwidth(this.label, {
id: this.topic,
class: 'row-type-' + this.type + ' ' + prefEnabledOrDisabled,
'data-menu-row-type': this.type,
'data-profile-prefs-topic': this.topic,
'data-profile-prefs-namespace': this.namespace,
'data-profile-pref-state': prefEnabledOrDisabled
});
}
}
});
return menuHTML + buildMenuBottom();
}
/* ·
·
· Menu components
·
· · · · · · · · · */
function buildMenuTop() {
return '
\
\
\
\
';
}
function buildMenuBottom() {
return '
';
}
function buildMenuDivider() {
return '';
}
function buildMenuRowFullwidth(label, attributes) {
var attributesHTML = '';
$.each(attributes,function(k,v){
attributesHTML = attributesHTML + ' ' + k + '="' + v + '"';
});
return '
';
}
function alignMenuToParent(menu, parent) {
var menuLeft = parent.width()/2 - menu.width() + 15;
var menuTop = parent.height()+5;
menu.css('left', menuLeft + 'px');
menu.css('top', menuTop + 'px');
}
/* ·
·
· Show error message
·
· @param message: error message
·
· · · · · · · · · */
function showErrorMessage(message, after) {
if(typeof after == 'undefined') {
var after = $('#user-container');
}
after.after('
' + message + '
');
}
/* ·
·
· Show favs and requeets in queet element
·
· @param q: queet jQuery object
· @param data: object with users that has faved and requeeted
·
· · · · · · · · · */
function showFavsAndRequeetsInQueet(q,data) {
// set the non-expanded fav- and rq-count
q.children('.queet').find('.action-fav-num').html(data.favs.length);
q.children('.queet').find('.action-fav-num').attr('data-fav-num',data.favs.length);
q.children('.queet').find('.action-rq-num').html(data.repeats.length);
q.children('.queet').find('.action-rq-num').attr('data-rq-num',data.repeats.length);
// don't proceed if queet is not expanded
if(!q.hasClass('expanded') || q.hasClass('collapsing')) {
return;
}
// don't proceed and remove expanded stats if all favs and repeats are removed
if(data.favs.length < 1 && data.repeats.length < 1) {
q.children('.queet').find('.stats').remove();
return;
}
// remove any existing stats container and add a new empty one
if(q.children('.queet').find('ul.stats').length > 0) {
q.children('.queet').find('ul.stats').remove();
}
q.children('.queet').find('.queet-stats-container').prepend('
');
// set the expanded fav-count number
if(data.favs.length > 0) {
if(data.favs.length == 1) {
var favLabel = window.sL.favoriteNoun;
}
else if(data.favs.length > 1) {
var favLabel = window.sL.favoritesNoun;
}
if(q.children('.queet').find('.fav-count').length>0) {
q.children('.queet').find('.fav-count').children('strong').html(data.favs.length);
}
else {
q.children('.queet').find('li.avatar-row').before('
');
}
}
// merge favs and repeats objects by user_id (removes duplicate users)
var favsAndRepeats = {};
$.each(data.repeats,function(){
favsAndRepeats[this.user_id] = this;
});
$.each(data.favs,function(){
favsAndRepeats[this.user_id] = this;
});
// make an object with time the key
var favsAndRepeatsByTime = {};
$.each(favsAndRepeats,function(){
favsAndRepeatsByTime[this.time] = this;
});
// create an array with times and sort it
var timeSorted = [];
$.each(favsAndRepeats,function(){
timeSorted.push(this.time);
});
timeSorted.sort();
// display avatars in chronological order, max 7
var avatarnum = 1;
$.each(timeSorted,function(){
q.children('.queet').find('.avatar-row').append('');
if(avatarnum > 15) {
return false;
}
avatarnum++;
});
}
/* ·
·
· Build profile card HTML
·
· @param data: an object with a user array
·
· · · · · · · · · */
function buildProfileCard(data) {
data = cleanUpUserObject(data);
// use avatar if no cover photo
var coverPhotoHtml = '';
if(data.cover_photo !== false) {
coverPhotoHtml = 'background-image:url(\'' + data.cover_photo + '\')';
}
// follows me?
var follows_you = '';
if(data.follows_you === true && window.loggedIn.id != data.id) {
var follows_you = '' + window.sL.followsYou + '';
}
// show user actions if logged in
var followingClass = '';
if(data.following) {
followingClass = 'following';
}
var followButton = '';
// only add follow button if this is a local user
if(data.is_local == true) {
if(typeof window.loggedIn.screen_name != 'undefined' && window.loggedIn.id != data.id) {
var followButton = '';
}
// follow from external instance if logged out
if(typeof window.loggedIn.screen_name == 'undefined') {
var followButton = '';
}
// edit profile button if me
if(typeof window.loggedIn.screen_name != 'undefined' && window.loggedIn.id == data.id) {
var followButton = '';
}
}
// is webpage empty?
var emptyWebpage = '';
if(data.url.length<1) {
emptyWebpage = ' empty';
}
// full card html
data.profileCardHtml = '\
');
}});
}
/* ·
·
· Change stream
·
· @param streamObject: object returned by pathToStreamRouter()
· @param setLocation: whether we should update the browsers location bar when we set the new stream
· @param fallbackId: if we fail to get the stream, it can be due to a bad/changed user/group nickname,
· in that case this parameter can contain a user/group id that we can use to retrieve the correct nickname
· @param actionOnSuccess: callback function on success
·
· · · · · · · · · */
function setNewCurrentStream(streamObject,setLocation,fallbackId,actionOnSuccess) {
if(!streamObject && !streamObject.stream) {
console.log('invalid streamObject, no stream to set!');
return;
}
// remove any old error messages
$('.error-message').remove();
// remember state of old stream (including profile card)
if(typeof window.currentStreamObject != 'undefined') {
localStorageObjectCache_STORE('streamState',window.currentStreamObject.path, $('#feed').siblings('.profile-card').outerHTML() + $('#feed').outerHTML());
}
// halt interval that checks for new queets
window.clearInterval(checkForNewQueetsInterval);
display_spinner();
// scroll to top
$(window).scrollTop(0);
$('body').addClass('androidFix').scrollTop(0).removeClass('androidFix');
// blur any selected links
$('a').blur();
// null any searches
$('#feed-body').removeAttr('data-search-page-number');
// remember the most recent stream
window.currentStream = streamObject.stream;
window.currentStreamObject = streamObject;
if(streamObject.streamSubHeader) {
var h2FeedHeader = streamObject.streamSubHeader;
}
else {
var h2FeedHeader = streamObject.streamHeader;
}
// if we have a saved copy of this stream, show it immediately (but it is replaced when stream finishes to load later)
var haveOldStreamState = localStorageObjectCache_GET('streamState',window.currentStreamObject.path);
if(haveOldStreamState) {
$('.profile-card,.hover-card,.hover-card-caret').remove();
$('#feed').remove();
$('#user-container').after(haveOldStreamState);
$('.profile-card').css('display','none');
$('#feed').css('display','none');
$('.profile-card').show();
$('#feed').show();
$('#feed-body').removeAttr('data-end-reached');
$('#feed-header-inner h2').css('opacity','0.2');
$('#feed-header-inner h2').html(h2FeedHeader); // update header (could be wrong in cache)
if(streamObject.menu && window.loggedIn) {
$('#feed-header-inner h2').append('');
}
$('#feed-header-inner h2').animate({opacity:'1'},1000);
// set location bar from stream
if(setLocation) {
setUrlFromStream(streamObject);
setLocation = false; // don't set location twice if we've already set it here
}
// also mark this stream as the current stream immediately, if a saved copy exists
addStreamToHistoryMenuAndMarkAsCurrent(streamObject);
// maybe do something
if(typeof actionOnSuccess == 'function') {
actionOnSuccess();
// don't invoke actionOnSuccess later if we already invoked it here
actionOnSuccess = false;
}
}
// otherwise we fade out and wait for stream to load
else {
// fade out
$('#feed,.profile-card').animate({opacity:'0'},150,function(){
// when fade out finishes, remove any profile cards and set new header
$('.profile-card,.hover-card,.hover-card-caret').remove();
$('#feed-body').html('');
$('#feed-header-inner h2').html(h2FeedHeader);
if(streamObject.menu && window.loggedIn) {
$('#feed-header-inner h2').append('');
}
});
}
// change design immediately to either cached design or logged in user's
if(typeof window.oldStreamsDesigns[theUserOrGroupThisStreamBelongsTo(window.currentStream)] != 'undefined') {
changeDesign(window.oldStreamsDesigns[theUserOrGroupThisStreamBelongsTo(window.currentStream)]);
}
else {
changeDesign({backgroundimage:window.loggedIn.background_image, backgroundcolor:window.loggedIn.backgroundcolor, linkcolor:window.loggedIn.linkcolor});
}
// get stream
getFromAPI(streamObject.stream, function(queet_data, userArray, error, url){
// while waiting for this data user might have changed stream, so only proceed if current stream still is this one
if(window.currentStream != streamObject.stream) {
console.log('stream has changed, aborting');
return;
}
// if we have a fallbackId and a userArray, and the userArray's id is not equal to
// the fallackId, this is the wrong stream! we need to re-invoke setNewCurrentStream()
// with the correct and up-to-date nickname (maybe best not to send a fallbackId here not
// to risk getting into an infinite loop caused by bad data)
// also, we do the same thing if getting the stream fails, but we have a fallback id
if((userArray && fallbackId && userArray.id != fallbackId)
|| (queet_data === false && fallbackId)) {
if(streamObject.name == 'profile') {
getNicknameByUserIdFromAPI(fallbackId,function(nickname) {
if(nickname) {
setNewCurrentStream(pathToStreamRouter(nickname),true,false,actionOnSuccess);
}
else {
// redirect to front page if everything fails
setNewCurrentStream(pathToStreamRouter('/'),true,false,actionOnSuccess);
}
});
}
else if(streamObject.name == 'group notice stream') {
getNicknameByGroupIdFromAPI(fallbackId,function(nickname) {
if(nickname) {
setNewCurrentStream(pathToStreamRouter('group/' + nickname),true,false,actionOnSuccess);
}
else {
// redirect to front page if everything fails
setNewCurrentStream(pathToStreamRouter('/'),true,false,actionOnSuccess);
}
});
}
}
// getting stream failed, and we don't have a fallback id
else if(queet_data === false) {
// maybe fade in user-container here, ("success" was a badly chosen name...)
if(typeof actionOnSuccess == 'function') {
actionOnSuccess();
}
if(error.status == 401) {
showErrorMessage(window.sL.ERRORmustBeLoggedIn);
}
else if(error.status == 404) {
if(streamObject.name == 'profile'
|| streamObject.name == 'friends timeline'
|| streamObject.name == 'mentions'
|| streamObject.name == 'favorites'
|| streamObject.name == 'subscribers'
|| streamObject.name == 'subscriptions'
|| streamObject.name == 'user group list') {
showErrorMessage(window.sL.ERRORcouldNotFindUserWithNickname.replace('{nickname}',replaceHtmlSpecialChars(streamObject.nickname)));
}
else if(streamObject.name == 'group notice stream'
|| streamObject.name == 'group member list'
|| streamObject.name == 'group admin list') {
showErrorMessage(window.sL.ERRORcouldNotFindGroupWithNickname.replace('{nickname}',replaceHtmlSpecialChars(streamObject.nickname)));
}
else {
showErrorMessage(window.sL.ERRORcouldNotFindPage + '
\
');
}
}
// everything seems fine, show the new stream
else if(queet_data) {
// set location bar from stream
if(setLocation) {
setUrlFromStream(streamObject);
}
// profile card from user array
if(userArray) {
addProfileCardToDOM(buildProfileCard(userArray));
}
// remove any trailing profile cards
else {
$('.profile-card').remove();
}
// show group profile card if this is a group stream
if(streamObject.name == 'group notice stream'
|| streamObject.name == 'group member list'
|| streamObject.name == 'group admin list') {
groupProfileCard(streamObject.nickname);
}
// say hello to the api if this is notifications stream, to
// get correct unread notifcation count
if(window.currentStreamObject.name == 'notifications') {
helloAPI();
}
// start checking for new queets again
window.clearInterval(checkForNewQueetsInterval);
checkForNewQueetsInterval=window.setInterval(function(){checkForNewQueets()},window.timeBetweenPolling);
// add this stream to the history menu
addStreamToHistoryMenuAndMarkAsCurrent(streamObject);
remove_spinner();
$('#feed-body').html(''); // empty feed body
$('#new-queets-bar').parent().addClass('hidden'); document.title = window.siteTitle; // hide new queets bar if it's visible there
addToFeed(queet_data, false,'visible'); // add stream items to feed element
$('#feed').animate({opacity:'1'},150); // fade in
$('.reload-stream').show();
$('#feed-body').removeAttr('data-end-reached');
$('body').removeClass('loading-older');$('body').removeClass('loading-newer');
$('html,body').scrollTop(0); // scroll to top
// maybe do something
if(typeof actionOnSuccess == 'function') {
actionOnSuccess();
}
}
});
}
/* ·
·
· Add this stream to history menu if it doesn't exist in stream selection menus (if we're logged in)
· and mark this stream as current
·
· @param streamObject: stream object returned by pathToStreamRouter()
·
· · · · · · · · · */
function addStreamToHistoryMenuAndMarkAsCurrent(streamObject) {
if(streamObject.parentPath) {
var urlToMarkAsCurrent = window.siteInstanceURL + streamObject.parentPath;
}
else {
var urlToMarkAsCurrent = window.siteInstanceURL + streamObject.path;
}
if($('.stream-selection[href="' + urlToMarkAsCurrent + '"]').length==0
&& typeof window.loggedIn.screen_name != 'undefined') {
$('#history-container').prepend('' + streamObject.streamHeader + '');
updateHistoryLocalStorage();
// max 10 in history container
var historyNum = $('#history-container').children('.stream-selection').length;
if(historyNum > 10) {
$('#history-container').children('.stream-selection').slice(-(historyNum-10)).remove();
}
}
$('.stream-selection').removeClass('current');
$('.stream-selection[href="' + urlToMarkAsCurrent + '"]').addClass('current');
}
/* ·
·
· Expand/de-expand queet
·
· @param q: the stream item to expand
· @param doScrolling: if we should scroll back to position or not
·
· · · · · · · · · */
function expand_queet(q,doScrolling) {
// don't do anything if this is a queet being posted
if(q.hasClass('temp-post')) {
return;
}
// don't expand if this is a remote profile popup
if(q.closest('#popup-external-profile, #popup-local-profile').length>0) {
return;
}
doScrolling = typeof doScrolling !== 'undefined' ? doScrolling : true;
var qid = q.attr('data-quitter-id');
// de-expand if expanded
if(q.hasClass('expanded') && !q.hasClass('collapsing')) {
var sel = getSelection().toString();
if(!sel
&& !q.find('.queet-button').children('button').hasClass('enabled')
&& !q.find('.queet-button').children('button').hasClass('too-long')) { // don't collapse if text is selected, or if queet has an active queet button, or if queet text is too long
// remove some things right away
q.find('.inline-reply-caret').remove();
// "unplay" gif image on collapse if there's only one attachment (switch to thumb)
var gifToUnPlay = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button').children('.attachment-thumb[data-mime-type="image/gif"]');
if(gifToUnPlay.length > 0) {
gifToUnPlay.attr('src',gifToUnPlay.attr('data-thumb-url'));
gifToUnPlay.parent('.thumb-container').css('background-image','url(\'' + gifToUnPlay.attr('data-thumb-url') + '\')');
}
// show thumbs (if hidden) and remove any iframe video immediately
q.children('.queet').find('.queet-thumbs').removeClass('hide-thumbs');
q.children('.queet').find('iframe').remove();
q.addClass('collapsing');
if(q.hasClass('conversation')) {
q.removeClass('expanded');
q.removeClass('collapsing');
q.find('.view-more-container-top').remove();
q.find('.view-more-container-bottom').remove();
q.find('.stream-item.conversation').remove();
q.find('.inline-reply-queetbox').remove();
q.find('.expanded-content').remove();
}
else {
rememberMyScrollPos(q.children('.queet'),qid,0);
// give stream item a height
q.css('height',q.height() + 'px');
q.children('.queet').find('.expanded-content').css('height',q.find('.expanded-content').height() + 'px');
q.children('.queet').find('.queet-thumbs.thumb-num-1').css('max-height',q.find('.queet-thumbs.thumb-num-1').height() + 'px');
q.children('.queet').find('.queet-thumbs.thumb-num-1 .thumb-container').css('max-height',q.find('.queet-thumbs.thumb-num-1').height() + 'px');
q.children('div').not('.queet').children('a').css('opacity','0.5');
q.children('div').not('.queet').children().children().css('opacity','0.5');
var collapseTime = 100 + q.find('.stream-item.conversation:not(.hidden-conversation)').length*50;
// set transition time (needs to be delayed, otherwise webkit animates the height-setting above)
setTimeout(function() {
q.children('.queet').css('-moz-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.children('.queet').css('-o-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.children('.queet').css('-webkit-transition-duration',Math.round( collapseTime * 1000 * 10) / 10 + 's');
q.children('.queet').css('transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.children('.queet').find('.expanded-content, .queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('-moz-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.children('.queet').find('.expanded-content, .queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('-o-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.children('.queet').find('.expanded-content, .queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('-webkit-transition-duration',Math.round( collapseTime * 1000 * 10) / 10 + 's');
q.children('.queet').find('.expanded-content, .queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.css('-moz-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.css('-o-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
q.css('-webkit-transition-duration',Math.round( collapseTime * 1000 * 10) / 10 + 's');
q.css('transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
// set new heights and margins to animate to
var animateToHeight = q.children('.queet').outerHeight() - q.find('.inline-reply-queetbox').outerHeight() - q.children('.queet').find('.expanded-content').outerHeight() - Math.max(0,q.children('.queet').find('.queet-thumbs.thumb-num-1').outerHeight()-250) - 2;
if(animateToHeight < 73) { // no less than this
animateToHeight = 73;
}
q.css('height',animateToHeight + 'px');
q.children('.queet').css('margin-top', '-' + (q.children('.queet').offset().top - q.offset().top) + 'px');
q.children('.queet').find('.expanded-content').css('height','0');
q.children('.queet').find('.queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('max-height','250px');
if(doScrolling) {
setTimeout(function() {
backToMyScrollPos(q,qid,500,function(){
cleanUpAfterCollapseQueet(q);
});
}, collapseTime);
}
else {
setTimeout(function() {
cleanUpAfterCollapseQueet(q);
}, collapseTime);
}
}, 50);
}
}
}
else if(!q.hasClass('collapsing')) {
// not for acitivity or notifications
if(!q.hasClass('activity') && !q.hasClass('repeat') && !q.hasClass('like') && !q.hasClass('follow')) {
q.addClass('expanded');
q.prev().addClass('next-expanded');
// if shortened queet, get full text
if(q.children('.queet').find('span.attachment.more').length>0 && q.data('attachments') != 'undefined') {
// get full html for queet, first try localstorage cache
var cacheData = localStorageObjectCache_GET('fullQueetHtml',qid);
if(cacheData) {
q.children('.queet').find('.queet-text').html(cacheData);
q.children('.queet').outerHTML(detectRTL(q.children('.queet').outerHTML()));
}
else {
var attachmentId = q.children('.queet').find('span.attachment.more').attr('data-attachment-id');
// the url to the text/html attachment is in an array in an attribute
$.each(q.data('attachments'), function(k,attachment) {
if(attachment.id == attachmentId) {
$.get(attachment.url,function(data){
if(data) {
// get body and store in localStorage
var bodyHtml = $('').html(data).find('body').html();
localStorageObjectCache_STORE('fullQueetHtml',qid,bodyHtml);
q.children('.queet').find('.queet-text').html($.trim(bodyHtml));
q.children('.queet').outerHTML(detectRTL(q.children('.queet').outerHTML()));
}
});
return false;
}
});
}
}
// add expanded container
var longdate = parseTwitterLongDate(q.find('.created-at').attr('data-created-at'));
var qurl = q.find('.created-at').find('a').attr('href');
var metadata = '' + longdate + ' · ' + unescape(q.attr('data-source')) + ' · ' + window.sL.details + '';
// show expanded content
q.find('.stream-item-footer').before('
' + metadata + '
');
// "play" gif image on expand if there's only one attachment (switch to full gif from thumb)
var gifToPlay = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button').children('.attachment-thumb[data-mime-type="image/gif"]');
if(gifToPlay.length > 0) {
gifToPlay.attr('src',gifToPlay.attr('data-full-image-url'));
gifToPlay.parent('.thumb-container').css('background-image','url(\'' + gifToPlay.attr('data-full-image-url') + '\')');
}
// if there's only one thumb and it's a youtube video, show it inline
if(q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.youtube').length == 1) {
var youtubeURL = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.youtube').children('.attachment-thumb').attr('data-full-image-url');
if(q.children('.queet').find('.expanded-content').children('.media').children('iframe[src="' + youTubeEmbedLinkFromURL(youtubeURL) + '"]').length < 1) { // not if already showed
// hide video thumbnail if it's the only one
if(q.children('.queet').find('.queet-thumbs').children('.thumb-container').length < 2) {
q.children('.queet').find('.queet-thumbs').addClass('hide-thumbs');
}
// show video
q.children('.queet').find('.expanded-content').prepend('');
}
}
// show certain attachments in expanded content
if(q.data('attachments') != 'undefined') {
$.each(q.data('attachments'), function() {
var attachment_mimetype = this.mimetype;
var attachment_title = this.url;
// filename extension
var attachment_title_extension = attachment_title.substr((~-attachment_title.lastIndexOf(".") >>> 0) + 2);
// attachments in the content link to /attachment/etc url and not direct to image/video, link is in title
if(typeof attachment_title != 'undefined') {
// hack to make remote webm-movies load
if(attachment_title_extension == 'webm') {
attachment_mimetype = 'video/webm';
}
// videos
if($.inArray(attachment_mimetype, ['video/mp4', 'video/ogg', 'video/quicktime', 'video/webm']) >=0) {
if(q.children('.queet').find('.expanded-content').children('.media').children('video').children('source[href="' + attachment_title + '"]').length < 1) { // not if already showed
// local attachment with a thumbnail
var attachment_poster = '';
if(typeof this.thumb_url != 'undefined') {
attachment_poster = ' poster="' + this.thumb_url + '"';
}
if(q.children('.queet').find('.expanded-content').children('.media').length > 0) {
q.children('.queet').find('.media').last().after('');
}
else {
q.children('.queet').find('.expanded-content').prepend('');
}
}
}
else {
// other plugins, e.g. gotabulo, can check for other attachment file formats to expand
window.currentlyExpanding = {
"attachment_title":attachment_title,
"attachment_mimetype":attachment_mimetype,
"attachment_title_extension":attachment_title_extension,
"streamItem":q,
"thisAttachmentLink":$(this)
};
$(document).trigger('qvitterExpandOtherAttachments');
}
}
});
}
// get and show favs and repeats
getFavsAndRequeetsForQueet(q, qid);
// show conversation and reply form (but not if already in conversation)
if(!q.hasClass('conversation')) {
// show conversation (wait for css to animate the margin 50ms)
setTimeout(function(){
getConversation(q, qid);
},50);
// show inline reply form if logged in
if(typeof window.loggedIn.screen_name != 'undefined') {
q.children('.queet').append(replyFormHtml(q,qid));
// if we have cached text, expand the reply form and add that
var queetBox = q.children('.queet').find('.queet-box');
var cachedText = decodeURIComponent(queetBox.attr('data-cached-text'));
var cachedTextText = $('').html(cachedText).text();
if(cachedText != 'undefined') {
queetBox.click();
queetBox.html(cachedText);
setSelectionRange(queetBox[0], cachedTextText.length, cachedTextText.length);
queetBox.trigger('input');
}
}
}
}
}
}
function cleanUpAfterCollapseQueet(q) {
q.css('height','auto');
q.children('.queet').css('margin-top','0');
q.removeClass('expanded');
q.prev().removeClass('next-expanded');
q.removeClass('collapsing');
q.find('.expanded-content').remove();
q.find('.view-more-container-top').remove();
q.find('.view-more-container-bottom').remove();
q.find('.inline-reply-queetbox').remove();
q.find('.stream-item.conversation').remove();
q.find('.show-full-conversation').remove();
q.removeAttr('style');
q.children('.queet').removeAttr('style');
q.children('.queet').find('.queet-thumbs.thumb-num-1').removeAttr('style');
q.children('.queet').find('.queet-thumbs.thumb-num-1 .thumb-container').css('max-height','');
}
/* ·
·
· Get a queet box, mainly for popups
·
· @return the html for the queet box
·
· · · · · · · · · */
function queetBoxHtml() {
var startText = encodeURIComponent(window.sL.compose);
return '
' + decodeURIComponent(startText) + '
';
}
/* ·
·
· Get a reply form
·
· @param q: the stream item to open reply form in
· @param qid: queet id
· @return the html for the reply form
·
· · · · · · · · · */
function replyFormHtml(q,qid) {
// if we have cached text in localstorage
var data = localStorageObjectCache_GET('queetBoxInput','queet-box-' + qid);
if(data) {
var cachedText = encodeURIComponent(data);
}
// get all @:s
var user_screen_name = q.children('.queet').find('.screen-name').html().substring(1);
var user_screen_name_html = '@' + user_screen_name + '';
var user_screen_name_text = '@' + user_screen_name;
var reply_to_screen_name = '';
var reply_to_screen_name_html = '';
var reply_to_screen_name_text = '';
if(q.attr('data-in-reply-to-screen-name').length>0 // not if not a reply
&& q.attr('data-in-reply-to-screen-name') != $('#user-screen-name').html() // not if it's me
&& q.attr('data-in-reply-to-screen-name') != user_screen_name // not same screen name twice
) {
reply_to_screen_name = q.attr('data-in-reply-to-screen-name');
reply_to_screen_name_html = ' @' + reply_to_screen_name + '';
reply_to_screen_name_text = ' @' + reply_to_screen_name;
}
var more_reply_tos = '';
var more_reply_tos_text = '';
$.each(q.children('.queet').find('.queet-text').find('.mention'),function(key,obj){
var thisMention = $(obj).html().replace('@','');
if(thisMention != user_screen_name && thisMention != reply_to_screen_name && thisMention != $('#user-screen-name').html()) {
more_reply_tos = more_reply_tos + ' @' + thisMention + '';
more_reply_tos_text = more_reply_tos_text + ' @' + thisMention;
}
});
var startText = window.sL.replyTo + ' ' + user_screen_name_html + reply_to_screen_name_html + more_reply_tos + ' ';
var repliesText = user_screen_name_text + reply_to_screen_name_text + more_reply_tos_text + ' ';
startText = encodeURIComponent(startText);
repliesText = encodeURIComponent(repliesText);
return '
' + decodeURIComponent(startText) + '
';
}
/* ·
·
· Popup for replies, deletes, etc
·
· @param popupId: popups id
· @param heading: popops header
· @param bodyHtml: popups body html
· @param footerHtml: popups footer html
·
· · · · · · · · · · · · · */
function popUpAction(popupId, heading, bodyHtml, footerHtml, popUpWidth){
$('.modal-container').remove(); // remove any open popups
var allFooterHtml = '';
if(footerHtml) {
allFooterHtml = '';
}
$('body').prepend('
' + heading + '
' + bodyHtml + '
' + allFooterHtml + '
');
var thisPopUp = $('#' + popupId).children('.modal-draggable');
if(typeof popUpWidth != 'undefined') {
thisPopUp.width(popUpWidth);
}
centerPopUp(thisPopUp);
}
function centerPopUp(thisPopUp) {
thisPopUp.css('margin-top','');
thisPopUp.css('margin-left','');
var this_modal_height = thisPopUp.height();
var this_modal_width = thisPopUp.width();
var popupPos = thisPopUp.offset().top - $(window).scrollTop();
if((popupPos-(this_modal_height/2))<5) {
var marginTop = 5-popupPos;
}
else {
var marginTop = 0-this_modal_height/2;
}
thisPopUp.css('margin-top', marginTop + 'px');
thisPopUp.css('margin-left', '-' + (this_modal_width/2) + 'px');
thisPopUp.draggable({ handle: ".modal-header" });
}
/* ·
·
· Get and show conversation
·
· This function has grown into a monster, needs fixing
·
· · · · · · · · · · · · · */
function getConversation(q, qid) {
// check if we have a conversation for this notice cached in localstorage
var cacheData = localStorageObjectCache_GET('conversation',q.attr('data-conversation-id'));
if(cacheData) {
showConversation(q, qid, cacheData);
}
// always get most recent conversation from server
getFromAPI('statusnet/conversation/' + q.attr('data-conversation-id') + '.json?count=100', function(data){ if(data) {
// cache in localstorage
localStorageObjectCache_STORE('conversation',q.attr('data-conversation-id'), data);
showConversation(q, qid,data);
}});
}
function showConversation(q, qid, data) {
rememberMyScrollPos(q.children('.queet'),qid,0);
if(data && !q.hasClass('collapsing')){
if(data.length>1) {
var before_or_after = 'before';
$.each(data.reverse(), function (key,obj) {
// switch to append after original queet
if(obj.id == qid) {
before_or_after = 'after';
}
// don't add clicked queet to DOM, but all else
// note: first we add the full conversation, but hidden
if(obj.id != qid) {
var queetTime = parseTwitterDate(obj.created_at);
if(obj.source == 'activity') {
// because we had an xss issue, the obj.statusnet_html of qvitter-deleted-activity-notices can contain unwanted html, so we escape..
obj.statusnet_html = replaceHtmlSpecialChars(obj.statusnet_html);
var queetHtml = '
');
}
}
}
});
checkForHiddenConversationQueets(q, qid);
}
// helper function for the above recursive functions
function checkForHiddenConversationQueets(q, qid) {
// here we check if there are any remaining hidden queets in conversation, if there are, we put a "show full conversation"-link
if(q.find('.hidden-conversation').length>0) {
if(q.children('.queet').find('.show-full-conversation').length == 0) {
q.children('.queet').find('.stream-item-footer').append('' + window.sL.expandFullConversation + '');
// if this is a single notice, we show conversation
if(window.currentStream.substring(0,14) == 'statuses/show/') {
q.children('.queet').find('.show-full-conversation').trigger('click');
}
}
}
else {
q.children('.queet').find('.show-full-conversation').remove();
}
}
/* ·
·
· Build stream items and add them to feed
·
· Also a function that has grown out of control... Needs total makeover
·
· · · · · · · · · · · · · */
function addToFeed(feed, after, extraClasses, isReply) {
// some streams, e.g. /statuses/show/1234.json is not enclosed in an array, make sure it is
if(!$.isArray(feed)) {
feed = [feed];
}
$.each(feed.reverse(), function (key,obj) {
var extraClassesThisRun = extraClasses;
// is this a temp-post-placeholder?
var isTempPost = false;
if(after) {
if(after.indexOf('stream-item-temp-post') > -1) {
isTempPost = true;
}
}
// if this is the notifications feed, but not if it is a reply
if(window.currentStream.substring(0,35) == 'qvitter/statuses/notifications.json'
&& !isReply) {
// don't show any notices with object_type "activity"
if(typeof obj.notice != 'undefined' && obj.notice !== null && obj.notice.is_activity === true) {
return true;
}
// only if this notification isn't already in stream
if($('#feed-body > .stream-item[data-quitter-id-in-stream="' + obj.id + '"]').length == 0) {
obj.from_profile.description = obj.from_profile.description || '';
var notificationTime = parseTwitterDate(obj.created_at);
if(obj.is_seen == '0') {
extraClassesThisRun = extraClassesThisRun + ' not-seen'
}
// external
var ostatusHtml = '';
if(obj.from_profile.is_local === false) {
ostatusHtml = '';
}
if(obj.ntype == 'like') {
var noticeTime = parseTwitterDate(obj.notice.created_at);
var notificationHtml = '
';
}
if(after) {
$('#' + after).after(notificationHtml);
}
else {
$('#feed-body').prepend(notificationHtml);
}
// add not seen notification circle
$.each($('.notification.not-seen .queet'),function(){
if($(this).children('.not-seen').length<1) {
$(this).prepend('');
}
});
}
}
// if this is a user feed
else if((window.currentStream.substring(0,21) == 'statuses/friends.json'
|| window.currentStream.substring(0,18) == 'statuses/followers'
|| window.currentStream.substring(0,28) == 'statusnet/groups/membership/'
|| window.currentStream.substring(0,24) == 'statusnet/groups/admins/')
&& isTempPost === false // not if we're posting queet
) {
// only if not user is already in stream
if($('#stream-item-' + obj.id).length == 0) {
obj.description = obj.description || '';
// external
var ostatusHtml = '';
if(obj.is_local === false) {
ostatusHtml = '';
}
// rtl or not
var rtlOrNot = '';
if($('body').hasClass('rtl')) {
rtlOrNot = 'rtl';
}
// show user actions
var followingClass = '';
if(obj.following) {
followingClass = 'following';
}
var followButton = '';
if(typeof window.loggedIn.screen_name != 'undefined' // if logged in
&& window.loggedIn.id != obj.id) { // not if this is me
if(!(obj.statusnet_profile_url.indexOf('/twitter.com/')>-1 && obj.following === false)) { // only unfollow twitter users
var followButton = '';
}
}
var userHtml = '
';
if(after) {
$('#' + after).after(userHtml);
}
else {
$('#feed-body').prepend(userHtml);
}
}
}
// if this is a list of groups
else if(window.currentStream.substring(0,26) == 'statusnet/groups/list.json'
&& isTempPost === false // not if we're posting queet
) {
// only if not group is already in stream
if($('#stream-item-' + obj.id).length == 0) {
obj.description = obj.description || '';
obj.stream_logo = obj.stream_logo || window.defaultAvatarStreamSize;
// rtl or not
var rtlOrNot = '';
if($('body').hasClass('rtl')) {
rtlOrNot = 'rtl';
}
// show group actions if logged in
var memberClass = '';
if(obj.member) {
memberClass = 'member';
}
var memberButton = '';
if(typeof window.loggedIn.screen_name != 'undefined') {
var memberButton = '';
}
var groupAvatar = obj.stream_logo;
if(obj.homepage_logo != null) {
groupAvatar = obj.homepage_logo;
}
var groupHtml = '
');
}
}
}
// retweeted object don't exist in feed
else {
var queetHtml = buildQueetHtml(obj.retweeted_status, obj.id, extraClassesThisRun, obj);
if(after) {
$('#' + after).after(queetHtml);
}
else {
$('#feed-body').prepend(queetHtml);
}
}
}
// ordinary tweet
else {
// if this is a special qvitter-delete-notice activity notice it means we try to hide
// the deleted notice from our stream
// the uri is in the obj.text var, between the double curly brackets
if(typeof obj.qvitter_delete_notice != 'undefined' && obj.qvitter_delete_notice == true) {
var uriToHide = obj.text.substring(obj.text.indexOf('{{')+2,obj.text.indexOf('}}'));
var streamItemToHide = $('.stream-item[data-uri="' + uriToHide + '"]');
streamItemToHide.animate({opacity:'0.2'},1000,'linear',function(){
$(this).css('height',$(this).height() + 'px');
$(this).animate({height:'0px'},500,'linear',function(){
$(this).remove();
});
});
}
// only if not already exist
if($('#q-' + obj.id).length == 0) {
// activity get special design
if(obj.source == 'activity' || obj.is_activity === true) {
// because we had an xss issue, the obj.statusnet_html of qvitter-deleted-activity-notices can contain unwanted html, so we escape..
obj.statusnet_html = replaceHtmlSpecialChars(obj.statusnet_html);
var queetTime = parseTwitterDate(obj.created_at);
var queetHtml = '
';
// detect rtl
queetHtml = detectRTL(queetHtml);
if(after) {
$('#' + after).after(queetHtml);
}
else {
$('#feed-body').prepend(queetHtml);
}
}
else {
// if this is my queet, remove any temp-queets
if(typeof obj.user != 'undefined') {
if(obj.user.screen_name == $('#user-screen-name').html()) {
if($('.temp-post').length > 0) {
$('.temp-post').each(function (){
// remove temp duplicate
$(this).css('display','none');
// we do this so this queet gets added after correct temp-queet in expanded conversations
if($(this).find('.queet-text').text() == obj.text) {
after = $(this).attr('id');
}
// but don't hide my new queet
extraClassesThisRun = 'visible';
});
}
}
}
var queetHtml = buildQueetHtml(obj, obj.id, extraClassesThisRun);
if(after) {
if($('#' + after).hasClass('conversation')) { // if this is a reply, give stream item some conversation formatting
if($('#conversation-q-' + obj.id).length == 0) { // only if it's not already there
$('#' + after).after(queetHtml.replace('id="stream-item','id="conversation-stream-item').replace('class="stream-item','class="stream-item conversation').replace('id="q','id="conversation-q'));
$('#' + after).remove();
}
}
else {
$('#' + after).after(queetHtml);
}
}
else {
$('#feed-body').prepend(queetHtml);
// if this is a single notice, we expand it
if(window.currentStream.substring(0,14) == 'statuses/show/') {
expand_queet($('#stream-item-' + obj.id));
}
}
// fadeout any posting-popups
setTimeout(function(){
$('#popup-sending').fadeOut(1000, function(){
$('#popup-sending').remove();
});
},100);
}
}
}
convertAttachmentMoreHref();
});
$('.stream-selection').removeAttr('data-current-user-stream-name'); // don't remeber user feeds
}
/* ·
·
· Build HTML for a queet from an object
·
· @param obj: a queet object
· @param requeeted_by: if this is a requeet
·
· · · · · · · · · · · · · */
function buildQueetHtml(obj, idInStream, extraClassesThisRun, requeeted_by, isConversation) {
// if we have the full html for a truncated notice cached in localstorage, we use that
var cacheData = localStorageObjectCache_GET('fullQueetHtml',obj.id);
if(cacheData) {
obj.statusnet_html = cacheData;
}
// we don't want to print 'null' in in_reply_to_screen_name-attribute, someone might have that username!
var in_reply_to_screen_name = '';
if(obj.in_reply_to_screen_name != null) {
in_reply_to_screen_name = obj.in_reply_to_screen_name;
}
// conversations has some slightly different id's
var idPrepend = '';
if(typeof isConversation != 'undefined' && isConversation === true) {
var idPrepend = 'conversation-';
}
// is this mine?
var isThisMine = 'not-mine';
if(obj.user.id == window.loggedIn.id) {
var isThisMine = 'is-mine';
}
// requeeted by me?
var requeetedByMe = '';
if(obj.repeated_id) {
requeetedByMe = ' data-requeeted-by-me-id="' + obj.repeated_id + '" ';
}
// requeet html
var requeetedClass = '';
if(obj.repeated) {
var requeetHtml = '
';
var requeetedClass = 'requeeted';
}
else {
var requeetHtml = '
';
}
// favorite html
var favoritedClass = '';
if(obj.favorited) {
var favoriteHtml = '';
favoritedClass = 'favorited';
}
else {
var favoriteHtml = '';
}
// actions only for logged in users
var queetActions = '';
if(typeof window.loggedIn.screen_name != 'undefined') {
queetActions = '
' + requeetHtml + '
' + obj.repeat_num + '
' + favoriteHtml + '
' + obj.fave_num + '
';
}
// reply-to html
var reply_to_html = '';
if(obj.in_reply_to_screen_name !== null && obj.in_reply_to_profileurl !== null && obj.in_reply_to_screen_name != obj.user.screen_name) {
reply_to_html = '@' + obj.in_reply_to_screen_name + ' ';
}
// in-groups html
var in_groups_html = '';
if(typeof obj.statusnet_in_groups != 'undefined' && obj.statusnet_in_groups !== false && typeof obj.statusnet_in_groups === 'object') {
$.each(obj.statusnet_in_groups,function(){
in_groups_html = in_groups_html + ' !' + this.nickname + '';
});
}
// image attachment thumbnails
var attachment_html = '';
var attachmentNum = 0;
if(typeof obj.attachments != "undefined") {
$.each(obj.attachments, function(){
if(typeof this.thumb_url != 'undefined' && this.thumb_url !== null) { // if there's a thumb_url we assume this is a image or video
var bigThumbW = 1000;
var bigThumbH = 3000;
if(bigThumbW > window.siteMaxThumbnailSize) {
bigThumbW = window.siteMaxThumbnailSize;
}
if(bigThumbH > window.siteMaxThumbnailSize) {
bigThumbH = window.siteMaxThumbnailSize;
}
// very long landscape images should not have background-size:cover
var noCoverClass='';
if(this.width/this.height > 2) {
noCoverClass=' no-cover';
}
// play button for videos and animated gifs
var playButtonClass = '';
if((this.url.indexOf('://www.youtube.com') > -1 || this.url.indexOf('://youtu.be') > -1)
|| (typeof this.animated != 'undefined' && this.animated === true)) {
var playButtonClass = ' play-button';
}
// youtube class
var youTubeClass = '';
if(this.url.indexOf('://www.youtube.com') > -1 || this.url.indexOf('://youtu.be') > -1) {
youTubeClass = ' youtube';
}
// animated gifs always get default small non-animated thumbnail
if(this.animated === true) {
var img_url = this.thumb_url;
}
// if no dimensions are set, go with default thumb
else if(this.width === null && this.height === null) {
var img_url = this.thumb_url;
}
// large images get large thumbnail
else if(this.width > 1000) {
var img_url = this.large_thumb_url;
}
// no thumbnails for small images
else {
var img_url = this.url;
}
attachment_html = attachment_html + '';
attachmentNum++;
}
else if (this.mimetype == 'image/svg+xml') {
attachment_html = attachment_html + '';
attachmentNum++;
}
});
}
// requeets
var requeetHtml = '';
if(typeof requeeted_by != 'undefined' && requeeted_by !== false) {
var requeetedByHtml = '' + requeeted_by.user.name + '';
requeetHtml = '