/*· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · 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 · · Menus currently support four row types: divider, functions, links and profile-prefs-toggles · 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 getMenu(menuArray) { if(typeof getMenu == 'undefined' || getMenu === false) { return false; } var menuHTML = buildMenuTop(); $.each(menuArray,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, 'data-function-arguments': JSON.stringify(this.functionArguments) }); } else if(this.type == 'link') { menuHTML = menuHTML + buildMenuRowFullwidth(this.label, { class: 'row-type-' + this.type, 'href': this.href }); } else if(this.type == 'profile-prefs-toggle') { // only prefs in the qvitter namespace is supported if(this.namespace == 'qvitter') { // enabled? var prefEnabledOrDisabled = 'disabled'; if(isQvitterProfilePrefEnabled(this.topic)) { prefEnabledOrDisabled = 'enabled'; } // sometimes we want another label when the toggle is enabled var labelToUse = this.label; if(isQvitterProfilePrefEnabled(this.topic) && typeof this.enabledLabel != 'undefined') { labelToUse = this.enabledLabel; } // the tick can be disabled var disableTickClass = ''; if(typeof this.tickDisabled != 'undefined' && this.tickDisabled === true) { var disableTickClass = ' tick-disabled'; } // get row html menuHTML = menuHTML + buildMenuRowFullwidth(labelToUse, { id: this.topic, class: 'row-type-' + this.type + ' ' + prefEnabledOrDisabled + disableTickClass, 'data-menu-row-type': this.type, 'data-profile-prefs-topic': this.topic, 'data-profile-prefs-namespace': this.namespace, 'data-profile-prefs-label': replaceHtmlSpecialChars(this.label), 'data-profile-prefs-enabled-label': replaceHtmlSpecialChars(this.enabledLabel), 'data-profile-pref-state': prefEnabledOrDisabled, 'data-profile-pref-callback': this.callback }); } } }); return menuHTML + buildMenuBottom(); } /* · · · Menu from streamObject · · · · · · · · · · */ function streamObjectGetMenu(streamObject) { if(typeof streamObject == 'undefined') { return false; } if(streamObject.menu === false) { return false; } return getMenu(streamObject.menu); } /* · · · Menu components · · · · · · · · · · */ function buildMenuTop() { return ''; } function buildMenuDivider() { return ''; } function buildMenuRowFullwidth(label, attributes) { var attributesHTML = ''; $.each(attributes,function(k,v){ attributesHTML = attributesHTML + ' ' + k + '=\'' + v + '\''; }); return '
  • ' + replaceHtmlSpecialChars(label) + '
  • '; } function alignMenuToParent(menu, parent, offsetLeft) { if(typeof offsetLeft == 'undefined') { var offsetLeft = 0; } var menuLeft = parent.innerWidth()/2 - menu.width() + 15 + offsetLeft; var menuTop = parent.height()+5; menu.css('left', menuLeft + 'px'); menu.css('top', menuTop + 'px'); menu.css('display','block'); // show menu } /* · · · Show error message · · @param message: error message · · · · · · · · · · */ function showErrorMessage(message, after) { if(typeof after == 'undefined') { var after = $('#user-container,#login-register-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('
  • ' + favLabel + ' ' + data.favs.length + '
  • '); } } // add repeats if(data.repeats.length > 0) { if(data.repeats.length == 1) { var repeatsLabel = window.sL.requeetNoun; } else if(data.repeats.length > 1) { var repeatsLabel = window.sL.requeetsNoun; } if(q.children('.queet').find('.rq-count').length>0) { q.children('.queet').find('.rq-count').children('strong').html(data.repeats.length); } else { q.children('.queet').find('li.avatar-row').before('
  • ' + repeatsLabel + ' ' + data.repeats.length + '
  • '); } } // 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('' + favsAndRepeatsByTime[this].fullname + ''); if(avatarnum > 15) { return false; } avatarnum++; }); } /* · · · Build a follow/block button · · @param obj: an object with a user array · · · · · · · · · · */ function buildFollowBlockbutton(obj) { // following? var followingClass = ''; if(obj.following) { followingClass = ' following'; } // blocking? var blockingClass = ''; if(obj.statusnet_blocking) { blockingClass = ' blocking'; } 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 if(obj.blocks_you) { var followButton = ''; } else { var followButton = ''; } } } return followButton; } /* · · · 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 + ''; } // me? var is_me = ''; if(window.loggedIn !== false && window.loggedIn.id == data.id) { var is_me = ' is-me'; } // logged in? var logged_in = ''; if(window.loggedIn !== false) { var logged_in = ' logged-in'; } // silenced? var is_silenced = ''; if(data.is_silenced === true) { is_silenced = ' silenced'; } // sandboxed? var is_sandboxed = ''; if(data.is_sandboxed === true) { is_sandboxed = ' sandboxed'; } // muted? var is_muted = ''; if(isUserMuted(data.id)) { is_muted = ' user-muted'; } var followButton = ''; // follow from external instance if logged out and the user is local if(window.loggedIn === false && data.is_local == true) { followButton = ''; } // edit profile button if it's me else if(window.loggedIn !== false && window.loggedIn.id == data.id) { followButton = ''; } // follow button for logged in users else if(window.loggedIn !== false) { followButton = buildFollowBlockbutton(data); } // is webpage empty? var emptyWebpage = ''; if(data.url.length<1) { emptyWebpage = ' empty'; } // full card html data.profileCardHtml = '\
    \
    \
    \ \ \ \
    \

    ' + data.name + '

    \ ' + window.sL.silenced + '\ ' + window.sL.sandboxed + '\

    \ @' + data.screen_name + '\ ' + follows_you + '\

    \

    ' + data.description + '

    \

    \ ' + data.location + '\ \ · \ ' + data.url.replace('http://','').replace('https://','') + '\ \

    \
    \
    \ \
    \ '; return data; } /* · · · Build external profile card HTML · · @param data: an object containing data.external user array, · and maybe (hopefully) also a data.local user array · · · · · · · · · · */ function buildExternalProfileCard(data) { // follows me? var follows_you = ''; if(data.local !== null && data.local.follows_you === true && window.loggedIn.id != data.local.id) { var follows_you = '' + window.sL.followsYou + ''; } // follow button var followButton = ''; if(window.loggedIn !== false && typeof data.local != 'undefined' && data.local !== null) { var followButton = buildFollowBlockbutton(data.local); } // me? var is_me = ''; if(window.loggedIn !== false && window.loggedIn.id == data.local.id) { var is_me = ' is-me'; } // logged in? var logged_in = ''; if(window.loggedIn !== false) { var logged_in = ' logged-in'; } // silenced? var is_silenced = ''; if(data.local.is_silenced === true) { is_silenced = ' silenced'; } // sandboxed? var is_sandboxed = ''; if(data.local.is_sandboxed === true) { is_sandboxed = ' sandboxed'; } // muted? var is_muted = ''; if(isUserMuted(data.local.id)) { is_muted = ' user-muted'; } // local id/screen_name var localUserId = data.local.id; var localUserScreenName = data.local.screen_name; // empty strings and zeros instead of null data = cleanUpUserObject(data.external); // old statusnet-versions might not have full avatar urls in their api response if(typeof data.profile_image_url_original == 'undefined' || data.profile_image_url_original === null || data.profile_image_url_original.length == 0) { data.profile_image_url_original = data.profile_image_url; } if(typeof data.profile_image_url_profile_size == 'undefined' || data.profile_image_url_profile_size === null || data.profile_image_url_profile_size.length == 0) { data.profile_image_url_profile_size = data.profile_image_url; } // we might have a cover photo if(typeof data.cover_photo != 'undefined' && data.cover_photo !== false) { var cover_photo = data.cover_photo; } else { var cover_photo = data.profile_image_url_original; } // is webpage empty? var emptyWebpage = ''; if(data.url.length<1) { emptyWebpage = ' empty'; } var serverUrl = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.statusnet_profile_url, data.screen_name); data.screenNameWithServer = '@' + data.screen_name + '@' + serverUrl; data.profileCardHtml = '\ \
    '; return data; } /* · · · Adds a profile card before feed element · · @param data: an object with a user array · · · · · · · · · · */ function addProfileCardToDOM(data) { // change design changeDesign({backgroundimage:data.background_image, backgroundcolor:data.backgroundcolor, linkcolor:data.linkcolor}); // remove any old profile card and show profile card $('#feed').siblings('.profile-card').remove(); $('#feed').before(data.profileCardHtml); } /* · · · Adds a group profile card before feed element · · @param data: an object with one or more queet objects · · · · · · · · · · */ function groupProfileCard(groupAlias) { getFromAPI('statusnet/groups/show/' + groupAlias + '.json', function(data){ if(data){ data.nickname = data.nickname || ''; data.fullname = data.fullname || ''; data.stream_logo = data.stream_logo || window.defaultAvatarStreamSize; data.homepage_logo = data.homepage_logo || window.defaultAvatarProfileSize; data.original_logo = data.original_logo || window.defaultAvatarProfileSize; data.description = data.description || ''; data.homepage = data.homepage || ''; data.url = data.url || ''; data.member_count = data.member_count || 0; data.admin_count = data.admin_count || 0; // show user actions if logged in var memberClass = ''; if(data.member) { memberClass = 'member'; } var memberButton = ''; if(typeof window.loggedIn.screen_name != 'undefined') { var memberButton = ''; } // follow from external instance if logged out if(typeof window.loggedIn.screen_name == 'undefined') { var memberButton = ''; } // change design changeDesign({backgroundimage:false, backgroundcolor:false, linkcolor:false}); // add card to DOM $('#feed').siblings('.profile-card').remove(); // remove any old profile card $('#feed').before(' '); }}); } /* · · · 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; } // update the cache for the old stream rememberStreamStateInLocalStorage(); // remove any old error messages $('.error-message').remove(); // halt interval that checks for new queets window.clearInterval(checkForNewQueetsInterval); // scroll to top $(window).scrollTop(0); $('body').addClass('androidFix').scrollTop(0).removeClass('androidFix'); // blur any selected links $('a').blur(); // unset metadata for the old stream saved in attributes $('#feed-body').removeAttr('data-search-page-number'); $('#feed-body').removeAttr('data-end-reached'); // hide new queets bar and reload stream button $('#new-queets-bar-container').addClass('hidden'); $('.reload-stream').hide(); display_spinner('#feed-header-inner'); // are we just reloading? var weAreReloading = false; if(typeof window.currentStreamObject != 'undefined' && window.currentStreamObject.stream == streamObject.stream) { weAreReloading = true; } // show hidden items when we reload if(weAreReloading) { $('#feed-body').children('.stream-item').removeClass('hidden'); } // remember the most recent stream object window.currentStreamObject = streamObject; // set the new streams header and description if(streamObject.streamSubHeader) { $('#feed-header-inner h2').html(streamObject.streamSubHeader); } else { $('#feed-header-inner h2').html(streamObject.streamHeader); } if(streamObject.streamDescription !== false) { $('#feed-header-description').html(streamObject.streamDescription); } else { $('#feed-header-description').empty(); } // add menu cog if this stream has a menu if(streamObject.menu && window.loggedIn) { $('#feed-header-inner h2').append('
    '); } // subtle animation to show somethings happening $('#feed-header-inner h2').css('opacity','0.2'); $('#feed-header-inner h2').animate({opacity:'1'},1000); // if we're just reloading, we dont need to: // (1) check if we have a cached version of this stream // (2) remove the stream if we don't // (3) change design if(weAreReloading === false) { // (1) check if we have a cached version of the stream var haveOldStreamState = localStorageObjectCache_GET('streamState',window.currentStreamObject.path); // discard and remove any cached data that is not in this (new) format if(typeof haveOldStreamState.card == 'undefined' || typeof haveOldStreamState.feed == 'undefined') { localStorageObjectCache_STORE('streamState',window.currentStreamObject.path, false); haveOldStreamState = false; } // show cached version immediately if(haveOldStreamState) { $('.profile-card,.hover-card,.hover-card-caret').remove(); $('#feed').before(haveOldStreamState.card); var oldStreamState = $('
    ').html(haveOldStreamState.feed); // if the cached items has data-quitter-id-in-stream attributes, sort them before adding them if(oldStreamState.children('.stream-item[data-quitter-id-in-stream]').length>0) { oldStreamState.sortDivsByAttrDesc('data-quitter-id-in-stream'); } // hide any removed notices that we know of if(typeof window.knownDeletedNotices != 'undefined') { $.each(window.knownDeletedNotices,function(delededURI,v){ oldStreamState.children('.stream-item[data-uri="' + delededURI + '"]').addClass('deleted always-hidden'); }); } // hide all notices from blocked users (not for user lists) if(window.currentStreamObject.type != 'users' && typeof window.allBlocking != 'undefined') { markAllNoticesFromBlockedUsersAsBlockedInJQueryObject(oldStreamState); } // mark all notices and profile cards from muted users as muted markAllNoticesFromMutedUsersAsMutedInJQueryObject(oldStreamState); markAllProfileCardsFromMutedUsersAsMutedInDOM(); // hide dublicate repeats, only show the first/oldest instance of a notice oldStreamState = hideAllButOldestInstanceOfStreamItem(oldStreamState); // show full notice text for all cached notices, if we have it in cache $.each(oldStreamState.children('.stream-item'),function(){ getFullUnshortenedHtmlForQueet($(this),true); }); // if this is notidfications we have seen obviously seen them before oldStreamState.children('.stream-item').removeClass('not-seen'); $('#feed-body').html(oldStreamState.html()); // set location bar from stream if(setLocation) { setUrlFromStream(streamObject); setLocation = false; // don't set location twice if we've already set it here } // update all queets times updateAllQueetsTimes(); // also mark this stream as the current stream immediately, if a saved copy exists addStreamToHistoryMenuAndMarkAsCurrent(streamObject); // make sure page-container and feed is visible $('#page-container').css('opacity','1'); $('#feed').css('opacity','1'); // run callbacks for this stream if(streamObject.callbacks !== false) { $.each(streamObject.callbacks, function(){ if(typeof window[this] == 'function') { window[this](); } }); } // maybe do something if(typeof actionOnSuccess == 'function') { actionOnSuccess(); // don't invoke actionOnSuccess later if we already invoked it here actionOnSuccess = false; } } // (2) if we don't have a cached version we remove and hide the old stream and wait for the new one to load else { $('.profile-card,.hover-card,.hover-card-caret').remove(); $('#feed').css('opacity',0); $('#feed-body').html(''); remove_spinner(); display_spinner(); // display spinner in page header instead feed header } // (3) change design immediately to either cached design or logged in user's if(typeof window.oldStreamsDesigns[window.currentStreamObject.nickname] != 'undefined') { changeDesign(window.oldStreamsDesigns[window.currentStreamObject.nickname]); } 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.currentStreamObject.stream != 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) { // e.g. 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 if(streamObject.name == 'list notice stream' || streamObject.name == 'list members' || streamObject.name == 'list subscribers') { showErrorMessage(window.sL.ERRORcouldNotFindList); } else { showErrorMessage(window.sL.ERRORcouldNotFindPage + '

    url: ' + url); } emptyRememberAndHideFeed(); } else if(error.status == 410 && streamObject.name == 'notice') { showErrorMessage(window.sL.ERRORnoticeRemoved); emptyRememberAndHideFeed(); } else if(error.status == 0 || (error.status == 200 && error.responseText == 'An error occurred.') ) { showErrorMessage(window.sL.ERRORnoContactWithServer + ' (' + replaceHtmlSpecialChars(error.statusText) + ')'); // don't hide feed for these errors } else if (typeof error.responseJSON != 'undefined' && typeof error.responseJSON.error != 'undefined' && error.responseJSON.error.length > 0) { showErrorMessage(error.responseJSON.error); emptyRememberAndHideFeed(); } else { showErrorMessage(window.sL.ERRORsomethingWentWrong + '

    \ url: ' + url + '

    \ jQuery ajax() error:
    ' + replaceHtmlSpecialChars(JSON.stringify(error, null, ' ')) + '
    \ streamObject:
    ' + replaceHtmlSpecialChars(JSON.stringify(streamObject, null, ' ')) + '
    \ '); } // make sure page-container is visible $('#page-container').css('opacity','1'); } // 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(); // some streams, e.g. /statuses/show/1234.json is not enclosed in an array, make sure it is if(!$.isArray(queet_data)) { queet_data = [queet_data]; } // empty feed-body if this is a // (1) notice page // (2) if we got an empty result // (3) it's not a stream of notices or notifications if(window.currentStreamObject.name == 'notice' || queet_data.length==0 || (window.currentStreamObject.type != 'notices' && window.currentStreamObject.type != 'notifications')) { $('#feed-body').html(''); } // if the last item in the stream doesn't exists in the feed-body, we can't // just prepend the new items, since it will create a gap in the middle of the // feed. in that case we just empty the body and start from scratch else if($('#feed-body').children('.stream-item[data-quitter-id-in-stream="' + queet_data.slice(-1)[0].id + '"]').length == 0) { $('#feed-body').html(''); } // if the stream is slow to load, the user might have expanded a notice, or scrolled down // and started reading. in that case we add the new items _hidden_ if($('#feed-body').children('.stream-item.expanded').length>0 || $(window).scrollTop() > 50) { addToFeed(queet_data, false,'hidden'); maybeShowTheNewQueetsBar(); } else { addToFeed(queet_data, false,'visible'); } // make sure page-container is visible $('#page-container').css('opacity','1'); $('#feed').css('opacity','1'); // run callbacks for this stream if(streamObject.callbacks !== false) { $.each(streamObject.callbacks, function(){ if(typeof window[this] == 'function') { window[this](); } }); } $('.reload-stream').show(); $('body').removeClass('loading-older');$('body').removeClass('loading-newer'); // maybe do something if(typeof actionOnSuccess == 'function') { actionOnSuccess(); } } }); } /* · · · Empties the feed body, remembers the new empty state in localstorage and hide the feed and any profile card · and mark this stream as current · · · · · · · · · · */ function emptyRememberAndHideFeed() { $('#feed').css('opacity','0'); $('#feed-body').empty(); $('.profile-card').remove(); rememberStreamStateInLocalStorage(); } /* · · · 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 popup if(q.closest('.modal-container').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'); // get full html, if shortened getFullUnshortenedHtmlForQueet(q); // 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('
    '); // "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 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.children('.queet').children('script.attachment-json').length > 0 && q.children('.queet').children('script.attachment-json').text() != 'undefined') { try { var attachmentsParsed = JSON.parse(q.children('.queet').children('script.attachment-json').text()); } catch(e) { var attachmentsParsed = false; console.log('could not parse attachment json when expanding the notice: ' + e); console.log("attachment-json: " + q.children('.queet').children('script.attachment-json').text()); } if(attachmentsParsed !== false) { $.each(attachmentsParsed, 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') { // 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 getConversation(q, qid); // show inline reply form if logged in if(typeof window.loggedIn.screen_name != 'undefined') { q.children('.queet').append(replyFormHtml(q,qid)); maybePrefillQueetBoxWithCachedText(q.children('.queet').find('.queet-box')); } } } } } function cleanUpAfterCollapseQueet(streamItem) { var queet = streamItem.children('.queet'); streamItem.css('height','auto'); queet.css('margin-top','0'); streamItem.removeClass('expanded'); streamItem.prev().removeClass('next-expanded'); streamItem.removeClass('collapsing'); streamItem.find('.expanded-content').remove(); streamItem.find('.view-more-container-top').remove(); streamItem.find('.view-more-container-bottom').remove(); streamItem.find('.inline-reply-queetbox').remove(); streamItem.children('.stream-item.conversation').remove(); streamItem.find('.show-full-conversation').remove(); streamItem.removeAttr('style'); queet.removeAttr('style'); queet.find('.queet-thumbs.thumb-num-1').removeAttr('style'); queet.find('.queet-thumbs.thumb-num-1 .thumb-container').css('max-height',''); } /* · · · Get a popup queet box · · @return the html for the queet box · · · · · · · · · · */ function queetBoxPopUpHtml() { // if we have cached text in localstorage var data = localStorageObjectCache_GET('queetBoxInput','pop-up-queet-box'); if(data) { var cachedText = encodeURIComponent(data); } 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(streamItem,qid) { var q = streamItem.children('.queet'); // if we have cached text in localstorage var data = localStorageObjectCache_GET('queetBoxInput','queet-box-' + qid); if(data) { var cachedText = encodeURIComponent(data); } // object with ostatus-uri as key to avoid duplicates var screenNamesToAdd = {}; // add the screen name to the one we're replying to first (if it's not me) if(!thisIsALinkToMyProfile(streamItem.attr('data-user-profile-url')) && typeof streamItem.attr('data-user-ostatus-uri') != 'undefined') { screenNamesToAdd[streamItem.attr('data-user-ostatus-uri')] = streamItem.attr('data-user-screen-name'); } // old style notice (probably cached, this can be removed later) else if (typeof streamItem.attr('data-user-ostatus-uri') == 'undefined') { screenNamesToAdd[q.find('.account-group').attr('href')] = q.find('.screen-name').text().replace('@',''); } // add the rest of the attentions (not me) if(q.children('script.attentions-json').length > 0 && q.children('script.attentions-json').text() != 'undefined') { try { var attentionsParsed = JSON.parse(q.children('script.attentions-json').text()); } catch(e) { var attentionsParsed = false; console.log('could not parse attentions json: ' + e); console.log("attentions-json: " + q.children('script.attentions-json').text()); } if(attentionsParsed !== false) { $.each(attentionsParsed, function() { if(!thisIsALinkToMyProfile(this.profileurl) && typeof screenNamesToAdd[this.ostatus_uri] == 'undefined') { screenNamesToAdd[this.ostatus_uri] = this.screen_name; } }); } } // build reply/rant strings var repliesText = ''; if(Object.keys(screenNamesToAdd).length < 1 && q.find('strong.name').attr('data-user-id') == window.loggedIn.id) { if(streamItem.attr('data-in-reply-to-status-id') == 'null' || streamItem.attr('data-in-reply-to-status-id') == 'false' || streamItem.attr('data-in-reply-to-status-id') == 'undefined' || streamItem.attr('data-in-reply-to-status-id') == '') { var startText = window.sL.startRant + ' '; } else { var startText = window.sL.continueRant + ' '; } } else { var startText = window.sL.replyTo + ' '; var repliesArray = []; $.each(screenNamesToAdd, function(url,screenName){ repliesArray.push(screenName); }); if(repliesArray.length>0) { startText = '@' + repliesArray.join(' @') + ' 
    '; repliesText = '@' + repliesArray.join(' @') + ' '; } } 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(''); 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, 8); } // 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, 0); }}); } function showConversation(q, qid, data, offsetScroll) { 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); var queetHtml = buildQueetHtml(obj, obj.id, 'hidden-conversation', false, true); if(q.hasClass('expanded')) { // add queet to conversation only if still expanded // don't add if already exist in conversation if(q.children('.stream-item.conversation[data-quitter-id="' + obj.id + '"]').length > 0) { // the data in the existing notice is updated automatically in searchForUpdatedNoticeData invoked from getFromAPI } else if(before_or_after == 'before') { q.children('.queet').before(queetHtml); } else { if(q.children('.queet').nextAll('.conversation').length < 1) { q.children('.queet').after(queetHtml); } else { q.children('.queet').nextAll('.conversation').last().after(queetHtml); } } } } convertAttachmentMoreHref(); }); } else { remove_spinner(); } // loop trough this stream items conversation and show the "strict" line of replies rememberMyScrollPos(q.children('.queet'),qid,offsetScroll); findInReplyToStatusAndShow(q,qid,q.attr('data-in-reply-to-status-id'),true,false); backToMyScrollPos(q.children('.queet'),qid,false); findAndMarkLastVisibleInConversation(q); } else { remove_spinner(); } } /* · · · Add last&first visible class, since that's not possible to select in pure css · · · · · · · · · · · · · · */ function findAndMarkLastVisibleInConversation(streamItem) { streamItem.children().removeClass('last-visible'); streamItem.children().removeClass('first-visible-after-parent'); streamItem.children().not('.hidden-conversation').not('.always-hidden').last().addClass('last-visible'); streamItem.children('.queet').nextAll().not('.hidden-conversation').not('.always-hidden').first().addClass('first-visible-after-parent'); streamItem.children().removeClass('first-visible'); streamItem.children().not('.hidden-conversation').not('.always-hidden').first().addClass('first-visible'); } /* · · · Recursive walker functions to view only reyplies to replies, not full conversation · · · · · · · · · · · · · · */ function findInReplyToStatusAndShow(q, qid,reply,only_first,onlyINreplyto) { var reply_found = q.find('.stream-item[data-quitter-id="' + reply + '"]'); var reply_found_reply_to = q.find('.stream-item[data-quitter-id="' + reply_found.attr('data-in-reply-to-status-id') + '"]'); if(reply_found.length>0) { reply_found.removeClass('hidden-conversation'); reply_found.css('opacity','1'); if(only_first && reply_found_reply_to.length>0) { if(q.children('.view-more-container-top').length < 1) { if(q.children('.queet').prevAll('.hidden-conversation:not(.always-hidden)').length>0) { q.prepend(''); } } findReplyToStatusAndShow(q, qid,qid,true); } else { findInReplyToStatusAndShow(q, qid,reply_found.attr('data-in-reply-to-status-id'),false,onlyINreplyto); } } else if(!onlyINreplyto) { findReplyToStatusAndShow(q, qid,qid,true); } else { checkForHiddenConversationQueets(q, qid); } } // recursive function to find the replies to a status function findReplyToStatusAndShow(q, qid,this_id,only_first) { var replies_found = q.find('.stream-item[data-in-reply-to-status-id="' + this_id + '"]'); $.each(replies_found, function(k,reply_found){ var reply_founds_reply = q.find('.stream-item[data-in-reply-to-status-id="' + $(reply_found).attr('data-quitter-id') + '"]'); $(reply_found).removeClass('hidden-conversation'); $(reply_found).css('opacity','1'); if(!only_first) { findReplyToStatusAndShow(q, qid,$(this).attr('data-quitter-id'),false); } if(only_first && reply_founds_reply.length>0) { if(q.children('.view-more-container-bottom').length < 1) { if(q.children('.queet').nextAll('.hidden-conversation:not(.always-hidden)').length>0) { q.append(''); } } } }); 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:not(.always-hidden)').length>0) { if(q.children('.queet').find('.show-full-conversation').length == 0) { q.children('.queet').find('.stream-item-footer').append('' + window.sL.expandFullConversation + ''); } } 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) { // some streams, e.g. /statuses/show/1234.json is not enclosed in an array, make sure it is if(!$.isArray(feed)) { feed = [feed]; } var addedToTopOfFeedBodyNum = 0; $.each(feed.reverse(), function (key,obj) { var extraClassesThisRun = extraClasses; // if this is the notifications feed if(window.currentStreamObject.name == 'notifications') { // don't show any notices with object_type "activity" if(typeof obj.notice != 'undefined' && obj.notice !== null && obj.notice.is_post_verb === false) { 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 += ' not-seen'; } if(isUserMuted(obj.from_profile.id)) { extraClassesThisRun += ' user-muted'; } // external var ostatusHtml = ''; if(obj.from_profile.is_local === false) { ostatusHtml = ''; } if(obj.ntype == 'like') { var noticeTime = parseTwitterDate(obj.notice.created_at); var notificationHtml = ''; } else if(obj.ntype == 'repeat') { var noticeTime = parseTwitterDate(obj.notice.created_at); var notificationHtml = '
    \
    \
    \ ' + ostatusHtml + '\
    \
    \ \ ' + window.sL.xRepeatedYourQueet + ' ' + notificationTime + '\
    \
    \ \ ' + noticeTime + '\ : \ ' + $.trim(obj.notice.statusnet_html) + '\
    \
    \
    \
    '; } else if(obj.ntype == 'mention') { var notificationHtml = buildQueetHtml(obj.notice, obj.id, extraClassesThisRun + ' notification mention'); } else if(obj.ntype == 'reply') { var notificationHtml = buildQueetHtml(obj.notice, obj.id, extraClassesThisRun + ' notification reply'); } else if(obj.ntype == 'follow') { var notificationHtml = ''; } if(after) { $('#' + after).after(notificationHtml); } else { $('#feed-body').prepend(notificationHtml); addedToTopOfFeedBodyNum++; } } } // if this is a user feed else if(window.currentStreamObject.type == 'users') { // only if not user is already in stream if($('#stream-item-' + obj.id).length == 0) { var userHtml = buildUserStreamItemHtml(obj); if(after) { $('#' + after).after(userHtml); } else { $('#feed-body').prepend(userHtml); addedToTopOfFeedBodyNum++; } } } // if this is a list of groups else if(window.currentStreamObject.type == 'groups') { // 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 = '
    ' + memberButton + '
    '; if(after) { $('#' + after).after(groupHtml); } else { $('#feed-body').prepend(groupHtml); addedToTopOfFeedBodyNum++; } } } // if this is a retweet // (note the difference between "the repeat-notice" and "the repeated notice") // but the unrepeat delete activity notices have retweeted_status added to them, so check this is not a delete notice else if(typeof obj.retweeted_status != 'undefined' && (typeof obj.qvitter_delete_notice == 'undefined' || obj.qvitter_delete_notice === false)) { // if repeat-notice doesn't already exist in feed if($('#stream-item-' + obj.id).length == 0) { // if the repeated notice already exist in feed, we add this, but hidden if($('.stream-item[data-quitter-id="' + obj.retweeted_status.id + '"]').length > 0) { extraClassesThisRun += ' hidden-repeat'; } var queetHtml = buildQueetHtml(obj.retweeted_status, obj.id, extraClassesThisRun, obj); if(after) { $('#' + after).after(queetHtml); } else { $('#feed-body').prepend(queetHtml); addedToTopOfFeedBodyNum++; } } } // ordinary tweet else { // only if not already exist if($('#stream-item-' + obj.id).length == 0) { // remove any matching temp post if(typeof obj.user != 'undefined' && obj.user.id == window.loggedIn.id && $('#feed-body > .temp-post').length > 0 && after === false) { $('#feed-body > .temp-post').each(function (){ if($(this).children('.queet').find('.queet-text').text() == obj.text) { $(this).remove(); extraClassesThisRun = 'visible'; } }); } var queetHtml = buildQueetHtml(obj, obj.id, extraClassesThisRun); if(after) { $('#' + after).after(queetHtml); } else { $('#feed-body').prepend(queetHtml); addedToTopOfFeedBodyNum++; // if this is a single notice, we expand it if(window.currentStreamObject.name == 'notice') { expand_queet($('#stream-item-' + obj.id)); } } } } }); convertAttachmentMoreHref(); // if we've added stuff to the top of feed-body, we update our stream cache if(addedToTopOfFeedBodyNum>0) { rememberStreamStateInLocalStorage(); } $('.stream-selection').removeAttr('data-current-user-stream-name'); // don't remeber user feeds } /* · · · Build HTML for a user stream item from an object · · @param obj: a user object · · · · · · · · · · · · · · */ function buildUserStreamItemHtml(obj) { obj.description = obj.description || ''; // external var ostatusHtml = ''; if(obj.is_local === false) { ostatusHtml = ''; } // rtl or not var rtlOrNot = ''; if($('body').hasClass('rtl')) { rtlOrNot = 'rtl'; } // following? var followingClass = ''; if(obj.following) { followingClass = ' following'; } // blocking? var blockingClass = ''; if(obj.statusnet_blocking) { blockingClass = ' blocking'; } // silenced? var silencedClass = ''; if(obj.is_silenced === true) { silencedClass = ' silenced'; } // sandboxed? var sandboxedClass = ''; if(obj.is_sandboxed === true) { sandboxedClass = ' sandboxed'; } // logged in? var loggedInClass = ''; if(window.loggedIn !== false) { loggedInClass = ' logged-in'; } // muted? var mutedClass = ''; if(isUserMuted(obj.id)) { mutedClass = ' user-muted'; } 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 = buildFollowBlockbutton(obj); } } return '
    \
    \ ' + followButton + '\
    \ \
    \
    '; } /* · · · 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, extraClasses, requeeted_by, isConversation) { // if we've blocked this user, but it has slipped through anyway var blockingTooltip = ''; if(typeof window.allBlocking != 'undefined') { $.each(window.allBlocking,function(){ if(this == obj.user.id){ extraClasses += ' profile-blocked-by-me'; blockingTooltip = ' data-tooltip="' + window.sL.thisIsANoticeFromABlockedUser + '"'; return false; // break } }); } // muted? (if class is not already added) if(isUserMuted(obj.user.id) && extraClasses.indexOf(' user-muted') == -1) { extraClasses += ' user-muted'; blockingTooltip = ' data-tooltip="' + window.sL.thisIsANoticeFromAMutedUser + '"'; } // silenced? if(obj.user.is_silenced === true) { extraClasses += ' silenced'; } // sandboxed? if(obj.user.is_sandboxed === true) { extraClasses += ' sandboxed'; } // deleted? if(typeof window.knownDeletedNotices[obj.uri] != 'undefined') { extraClasses += ' deleted always-hidden'; } // unrepeated? if(typeof requeeted_by != 'undefined' && requeeted_by !== false && typeof window.knownDeletedNotices[requeeted_by.uri] != 'undefined') { extraClasses += ' unrepeated always-hidden'; } // activity? (hidden with css) if(obj.source == 'activity' || obj.is_post_verb === false) { extraClasses += ' activity always-hidden'; // because we had an xss issue with activities, the obj.statusnet_html of qvitter-deleted-activity-notices can contain unwanted html, so we escape, they are hidden anyway obj.statusnet_html = replaceHtmlSpecialChars(obj.statusnet_html); } // 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-'; extraClasses += ' 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 if(obj.repeated) { var requeetHtml = '
  • '; extraClasses += ' requeeted'; } else { var requeetHtml = '
  • '; } // favorite html if(obj.favorited) { var favoriteHtml = ''; extraClasses += ' 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_profileurl != obj.user.statusnet_profile_url) { var replyToProfileurl = obj.in_reply_to_profileurl; var replyToScreenName = obj.in_reply_to_screen_name; } // if we don't have a reply-to, we might have attentions, in that case use the first one as reply else if(typeof obj.attentions != 'undefined' && typeof obj.attentions[0] != 'undefined') { var replyToProfileurl = obj.attentions[0].profileurl; var replyToScreenName = obj.attentions[0].screen_name; } if(typeof replyToProfileurl != 'undefined' && typeof replyToScreenName != 'undefined') { reply_to_html = '@' + replyToScreenName + ' '; } // 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 + ''; }); } // requeets get's a context element and a identifying class // uri used is the repeate-notice's uri for repeats, not the repetED notice's uri (necessary if someone deletes a repeat) var URItoUse = obj.uri; var requeetHtml = ''; if(typeof requeeted_by != 'undefined' && requeeted_by !== false) { var requeetedByHtml = ' ' + requeeted_by.user.name + ''; requeetHtml = '
    ' + window.sL.requeetedBy.replace('{requeeted-by}',requeetedByHtml) + '
    '; var URItoUse = requeeted_by.uri; extraClasses += ' is-requeet'; } // the URI for delete activity notices are the same as the notice that is to be deleted // so we make the URI for the (hidden) actitity notice unique, otherwise we might remove // the activity notice from DOM when we remove the deleted notice if(typeof obj.qvitter_delete_notice != 'undefined' && obj.qvitter_delete_notice == true) { URItoUse += '-activity-notice'; } // attachment html and attachment url's to hide var attachmentBuild = buildAttachmentHTML(obj.attachments); var statusnetHTML = $('
    ').html(obj.statusnet_html); $.each(statusnetHTML.find('a'),function(k,aElement){ $.each(attachmentBuild.urlsToHide,function(k,urlToHide){ var urlToHideWithoutProtocol = removeProtocolFromUrl(urlToHide); if(urlToHideWithoutProtocol == removeProtocolFromUrl($(aElement).attr('href')) || urlToHideWithoutProtocol == removeProtocolFromUrl($(aElement).text())) { $(aElement).addClass('hidden-embedded-link-in-queet-text') } }); }); // if statusnetHTML is contains

    :s, unwrap those (diaspora..) statusnetHTML.children('p').each(function(){ $(this).contents().unwrap(); }); // find a place in the queet-text for the quoted notices statusnetHTML = placeQuotedNoticesInQueetText(attachmentBuild.quotedNotices, statusnetHTML); statusnetHTML = statusnetHTML.html(); // remove trailing
    s from queet text while (statusnetHTML.slice(-4) == '
    ') { statusnetHTML = statusnetHTML.slice(0,-4); } // external var ostatusHtml = ''; if(obj.user.is_local === false) { ostatusHtml = ''; var qSource = '' + getHost(obj.external_url) + ''; } else { var qSource = obj.source; } var queetTime = parseTwitterDate(obj.created_at); var queetHtml = '

    \
    \ \ \ ' + requeetHtml + '\ ' + ostatusHtml + '\
    \ \
    ' + $.trim(statusnetHTML) + '
    \ ' + attachmentBuild.html + '\ \
    \
    \
    '; // detect rtl queetHtml = detectRTL(queetHtml); return queetHtml; } /* · · · Place or update quoted notices in the queet text · · @param quotedNotices: object returned by buildAttachmentHTML() · @param queetText: jQuery object for queet text · · · · · · · · · · */ function placeQuotedNoticesInQueetText(quotedNotices,queetText) { $.each(quotedNotices,function(k,qoutedNotice){ if(typeof qoutedNotice.url != 'undefined') { var quoteLinkFound = queetText.find('a[href*="' + removeProtocolFromUrl(qoutedNotice.url) + '"]:not(.quote-link-container)'); // if we can't found it in a href, we might find it in data-quote-url attribute! if(quoteLinkFound.length==0) { quoteLinkFound = queetText.find('a[data-quote-url*="' + removeProtocolFromUrl(qoutedNotice.url) + '"]:not(.quote-link-container)'); } if(quoteLinkFound.length>0) { $.each(quoteLinkFound,function(){ // restore old style links (this can be removed later) if($(this).children('.quoted-notice-header, .oembed-item-header').length>0) { $(this).html($(this).attr('href')); $(this).removeAttr('style'); $(this).removeClass('quoted-notice'); $(this).removeClass('oembed-item'); } // place a container if we don't have it if(!$(this).next().is('.quote-link-container')) { $(this).after(''); } // update the link and the (maybe newly added) quote container after the link if($(this).next().is('.quote-link-container')) { $(this).addClass('hidden-quote-link-in-queet-text'); $(this).attr('data-quote-url',qoutedNotice.url); $(this).next().attr('data-quote-url',qoutedNotice.url); $(this).next().addClass(qoutedNotice.class); $(this).next().attr('href',qoutedNotice.href); $(this).next().html(qoutedNotice.html); // remove unnecessary line breaks, i.e. remove br between two quoted notices if($(this).prev().is('br')) { $(this).prev().remove(); } if(!$(this).next().next().is('br')) { $(this).next().after('
    '); } } }); } } }); return queetText; } /* · · · Build HTML for the attachments to a queet · · @param attachments: attachment object returned by the api · · · · · · · · · · · · · · */ function buildAttachmentHTML(attachments){ var attachmentHTML = ''; var oembedHTML = ''; var quotedNotices = []; var attachmentNum = 0; var oembedNum = 0; var urlsToHide = []; if(typeof attachments != "undefined") { $.each(attachments, function(){ // quoted notices if(typeof this.quoted_notice != 'undefined') { var quotedContent = this.quoted_notice.content; // quoted notice's attachments' thumb urls var quotedAttachmentsHTML = ''; var quotedAttachmentsHTMLbefore = ''; var quotedAttachmentsHTMLafter = ''; if(typeof this.quoted_notice.attachments != 'undefined' && this.quoted_notice.attachments.length > 0) { quotedAttachmentsHTML += '
    ' $.each(this.quoted_notice.attachments,function(k,qAttach){ quotedAttachmentsHTML += '
    '; // remove attachment string from content quotedContent = quotedContent.split(window.siteInstanceURL + 'attachment/' + qAttach.attachment_id).join(''); }); quotedAttachmentsHTML += '
    '; // if there is only one attachment, it goes before, otherwise after if(this.quoted_notice.attachments.length == 1) { quotedAttachmentsHTMLbefore = quotedAttachmentsHTML; } else { quotedAttachmentsHTMLafter = quotedAttachmentsHTML; } } var quotedNoticeHTML = quotedAttachmentsHTMLbefore + '\
    \ ' + this.quoted_notice.fullname + '\ ' + this.quoted_notice.nickname + '\
    \
    ' + $.trim(quotedContent) + '
    \ ' + quotedAttachmentsHTMLafter; quotedNotices.push({ url: this.url, html: quotedNoticeHTML, href: window.siteInstanceURL + 'notice/' + this.quoted_notice.id, class:'quoted-notice' }); } // if we have Twitter oembed data, we add is as quotes else if(typeof this.oembed != 'undefined' && this.oembed !== false && this.oembed.provider == 'Twitter') { var twitterHTML = '
    \ ' + this.oembed.author_name + '\ ' + this.oembed.title + '\
    \
    ' + this.oembed.oembedHTML + '
    \ '; quotedNotices.push({ url: this.url, html: twitterHTML, href: this.url, class:'oembed-item' }); } // if we have other oembed data (but not for photos and youtube, we handle those later) else if(typeof this.oembed != 'undefined' && this.oembed !== false && this.oembed.title !== null && this.oembed.provider != 'YouTube' && this.oembed.type != 'photo') { var oembedImage = ''; // only local images if(typeof this.thumb_url != 'undefined' && this.thumb_url !== null && isLocalURL(this.thumb_url)) { oembedImage = '
    '; } var oembedBody = ''; // don't add body if html it's too similar (80%) to the title (wordpress does this..) if(this.oembed.oembedHTML !== null && this.oembed.oembedHTML.length > 0) { if(this.oembed.oembedHTML.length > 200) { this.oembed.oembedHTML = this.oembed.oembedHTML.substring(0,200) + '…'; } if(stringSimilarity(this.oembed.oembedHTML,this.oembed.title.substring(0,200)) < 80) { oembedBody = this.oembed.oembedHTML; } } if(this.oembed.provider === null) { var oembedProvider = this.url; var oembedProviderURL = ''; } else { var oembedProvider = this.oembed.provider; var oembedProviderURL = removeProtocolFromUrl(this.oembed.provider_url); // remove trailing / if(oembedProviderURL.slice(-1) == '/') { oembedProviderURL = oembedProviderURL.slice(0,-1); } } // If the oembed data is generated by Qvitter, we know a better way of showing the title var oembedTitle = this.oembed.title; var oembedTitleHTML = '' + oembedTitle + ''; if(oembedTitle.slice(-10) == ' (Qvitter)') { var oembedTimePosted = parseTwitterLongDate(oembedTitle.slice(0,-10)); var oembedGNUsocialUsername = this.oembed.author_name.substring(this.oembed.author_name.lastIndexOf('(')+1,this.oembed.author_name.lastIndexOf(')')); var oembedGNUsocialFullname = this.oembed.author_name.slice(0,-(oembedGNUsocialUsername.length+3)); oembedTitleHTML = '' + oembedGNUsocialFullname + '\ @' + oembedGNUsocialUsername + ''; } oembedHTML += '\ ' + oembedImage + '\
    \ ' + oembedTitleHTML + '\
    \
    ' + oembedBody + '
    \ \
    '; oembedNum++; } // if there's a local thumb_url we assume this is a image or video else if(typeof this.thumb_url != 'undefined' && this.thumb_url !== null && isLocalURL(this.thumb_url)) { 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'; } // gif-class var animatedGifClass = ''; if(typeof this.animated != 'undefined' && this.animated === true) { var animatedGifClass = ' animated-gif'; } // 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 local images else if (this.url.indexOf(window.siteInstanceURL) === 0) { var img_url = this.url; } // small thumbnail for small remote images else { var img_url = this.thumb_url; } var urlToHide = window.siteInstanceURL + 'attachment/' + this.id; attachmentHTML += ''; urlsToHide.push(urlToHide); // hide this attachment url from the queet text attachmentNum++; } else if (this.mimetype == 'image/svg+xml') { var urlToHide = window.siteInstanceURL + 'attachment/' + this.id; attachmentHTML += ''; urlsToHide.push(urlToHide); // hide this attachment url from the queet text attachmentNum++; } }); } return { html: '
    ' + oembedHTML + '
    ' + attachmentHTML + '
    ', urlsToHide: urlsToHide, quotedNotices: quotedNotices }; }