dom-functions.js 105 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672
  1. /*· · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · ·
  2. · ·
  3. · ·
  4. · Q V I T T E R ·
  5. · ·
  6. · ·
  7. · <o) ·
  8. · /_//// ·
  9. · (____/ ·
  10. · (o< ·
  11. · o> \\\\_\ ·
  12. · \\) \____) ·
  13. · ·
  14. · ·
  15. · @licstart The following is the entire license notice for the ·
  16. · JavaScript code in this page. ·
  17. · ·
  18. · Copyright (C) 2015 Hannes Mannerheim and other contributors ·
  19. · ·
  20. · ·
  21. · This program is free software: you can redistribute it and/or modify ·
  22. · it under the terms of the GNU Affero General Public License as ·
  23. · published by the Free Software Foundation, either version 3 of the ·
  24. · License, or (at your option) any later version. ·
  25. · ·
  26. · This program is distributed in the hope that it will be useful, ·
  27. · but WITHOUT ANY WARRANTY; without even the implied warranty of ·
  28. · MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the ·
  29. · GNU Affero General Public License for more details. ·
  30. · ·
  31. · You should have received a copy of the GNU Affero General Public License ·
  32. · along with this program. If not, see <http://www.gnu.org/licenses/>. ·
  33. · ·
  34. · @licend The above is the entire license notice ·
  35. · for the JavaScript code in this page. ·
  36. · ·
  37. · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · · */
  38. /* ·
  39. ·
  40. · Build a menu
  41. ·
  42. · Menus currently support four row types: divider, functions, links and profile-prefs-toggles
  43. · Function rows run the function in the function attribute when clicked.
  44. · Profile-prefs-toggle rows toggles the preference in the attribute when clicked.
  45. ·
  46. · @param streamObject: stream object returned by pathToStreamRouter()
  47. ·
  48. · · · · · · · · · */
  49. function getMenu(menuArray) {
  50. if(typeof getMenu == 'undefined' || getMenu === false) {
  51. return false;
  52. }
  53. var menuHTML = buildMenuTop();
  54. $.each(menuArray,function(){
  55. if(this.type == 'divider') {
  56. menuHTML = menuHTML + buildMenuDivider();
  57. }
  58. else if(this.type == 'function') {
  59. menuHTML = menuHTML + buildMenuRowFullwidth(this.label, {
  60. class: 'row-type-' + this.type,
  61. 'data-menu-row-type': this.type,
  62. 'data-function-name': this.functionName,
  63. 'data-function-arguments': JSON.stringify(this.functionArguments)
  64. });
  65. }
  66. else if(this.type == 'link') {
  67. menuHTML = menuHTML + buildMenuRowFullwidth(this.label, {
  68. class: 'row-type-' + this.type,
  69. 'href': this.href
  70. });
  71. }
  72. else if(this.type == 'profile-prefs-toggle') {
  73. // only prefs in the qvitter namespace is supported
  74. if(this.namespace == 'qvitter') {
  75. // enabled?
  76. var prefEnabledOrDisabled = 'disabled';
  77. if(isQvitterProfilePrefEnabled(this.topic)) {
  78. prefEnabledOrDisabled = 'enabled';
  79. }
  80. // sometimes we want another label when the toggle is enabled
  81. var labelToUse = this.label;
  82. if(isQvitterProfilePrefEnabled(this.topic) && typeof this.enabledLabel != 'undefined') {
  83. labelToUse = this.enabledLabel;
  84. }
  85. // the tick can be disabled
  86. var disableTickClass = '';
  87. if(typeof this.tickDisabled != 'undefined' && this.tickDisabled === true) {
  88. var disableTickClass = ' tick-disabled';
  89. }
  90. // get row html
  91. menuHTML = menuHTML + buildMenuRowFullwidth(labelToUse, {
  92. id: this.topic,
  93. class: 'row-type-' + this.type + ' ' + prefEnabledOrDisabled + disableTickClass,
  94. 'data-menu-row-type': this.type,
  95. 'data-profile-prefs-topic': this.topic,
  96. 'data-profile-prefs-namespace': this.namespace,
  97. 'data-profile-prefs-label': replaceHtmlSpecialChars(this.label),
  98. 'data-profile-prefs-enabled-label': replaceHtmlSpecialChars(this.enabledLabel),
  99. 'data-profile-pref-state': prefEnabledOrDisabled,
  100. 'data-profile-pref-callback': this.callback
  101. });
  102. }
  103. }
  104. });
  105. return menuHTML + buildMenuBottom();
  106. }
  107. /* ·
  108. ·
  109. · Menu from streamObject
  110. ·
  111. · · · · · · · · · */
  112. function streamObjectGetMenu(streamObject) {
  113. if(typeof streamObject == 'undefined') {
  114. return false;
  115. }
  116. if(streamObject.menu === false) {
  117. return false;
  118. }
  119. return getMenu(streamObject.menu);
  120. }
  121. /* ·
  122. ·
  123. · Menu components
  124. ·
  125. · · · · · · · · · */
  126. function buildMenuTop() {
  127. return '<ul class="dropdown-menu">\
  128. <li class="dropdown-caret right">\
  129. <span class="caret-outer"></span>\
  130. <span class="caret-inner"></span>\
  131. </li>';
  132. }
  133. function buildMenuBottom() {
  134. return '</ul>';
  135. }
  136. function buildMenuDivider() {
  137. return '<li class="fullwidth dropdown-divider"></li>';
  138. }
  139. function buildMenuRowFullwidth(label, attributes) {
  140. var attributesHTML = '';
  141. $.each(attributes,function(k,v){
  142. attributesHTML = attributesHTML + ' ' + k + '=\'' + v + '\'';
  143. });
  144. return '<li class="fullwidth"><a' + attributesHTML + '>' + replaceHtmlSpecialChars(label) + '</a></li>';
  145. }
  146. function alignMenuToParent(menu, parent, offsetLeft) {
  147. if(typeof offsetLeft == 'undefined') {
  148. var offsetLeft = 0;
  149. }
  150. var menuLeft = parent.innerWidth()/2 - menu.width() + 15 + offsetLeft;
  151. var menuTop = parent.height()+5;
  152. menu.css('left', menuLeft + 'px');
  153. menu.css('top', menuTop + 'px');
  154. menu.css('display','block'); // show menu
  155. }
  156. /* ·
  157. ·
  158. · Show error message
  159. ·
  160. · @param message: error message
  161. ·
  162. · · · · · · · · · */
  163. function showErrorMessage(message, after) {
  164. if(typeof after == 'undefined') {
  165. var after = $('#user-container,#login-register-container');
  166. }
  167. after.after('<div class="error-message">' + message + '<span class="discard-error-message"></span></div>');
  168. }
  169. /* ·
  170. ·
  171. · Show favs and requeets in queet element
  172. ·
  173. · @param q: queet jQuery object
  174. · @param data: object with users that has faved and requeeted
  175. ·
  176. · · · · · · · · · */
  177. function showFavsAndRequeetsInQueet(q,data) {
  178. // set the non-expanded fav- and rq-count
  179. q.children('.queet').find('.action-fav-num').html(data.favs.length);
  180. q.children('.queet').find('.action-fav-num').attr('data-fav-num',data.favs.length);
  181. q.children('.queet').find('.action-rq-num').html(data.repeats.length);
  182. q.children('.queet').find('.action-rq-num').attr('data-rq-num',data.repeats.length);
  183. // don't proceed if queet is not expanded
  184. if(!q.hasClass('expanded') || q.hasClass('collapsing')) {
  185. return;
  186. }
  187. // don't proceed and remove expanded stats if all favs and repeats are removed
  188. if(data.favs.length < 1 && data.repeats.length < 1) {
  189. q.children('.queet').find('.stats').remove();
  190. return;
  191. }
  192. // remove any existing stats container and add a new empty one
  193. if(q.children('.queet').find('ul.stats').length > 0) {
  194. q.children('.queet').find('ul.stats').remove();
  195. }
  196. q.children('.queet').find('.queet-stats-container').prepend('<ul class="stats"><li class="avatar-row"></li></ul><div class="clearfix"></div>');
  197. // set the expanded fav-count number
  198. if(data.favs.length > 0) {
  199. if(data.favs.length == 1) {
  200. var favLabel = window.sL.favoriteNoun;
  201. }
  202. else if(data.favs.length > 1) {
  203. var favLabel = window.sL.favoritesNoun;
  204. }
  205. if(q.children('.queet').find('.fav-count').length>0) {
  206. q.children('.queet').find('.fav-count').children('strong').html(data.favs.length);
  207. }
  208. else {
  209. q.children('.queet').find('li.avatar-row').before('<li class="fav-count"><a>' + favLabel + ' </a><strong>' + data.favs.length + '</strong></li>');
  210. }
  211. }
  212. // add repeats
  213. if(data.repeats.length > 0) {
  214. if(data.repeats.length == 1) {
  215. var repeatsLabel = window.sL.requeetNoun;
  216. }
  217. else if(data.repeats.length > 1) {
  218. var repeatsLabel = window.sL.requeetsNoun;
  219. }
  220. if(q.children('.queet').find('.rq-count').length>0) {
  221. q.children('.queet').find('.rq-count').children('strong').html(data.repeats.length);
  222. }
  223. else {
  224. q.children('.queet').find('li.avatar-row').before('<li class="rq-count"><a>' + repeatsLabel + ' </a><strong>' + data.repeats.length + '</strong></li>');
  225. }
  226. }
  227. // merge favs and repeats objects by user_id (removes duplicate users)
  228. var favsAndRepeats = {};
  229. $.each(data.repeats,function(){
  230. favsAndRepeats[this.user_id] = this;
  231. });
  232. $.each(data.favs,function(){
  233. favsAndRepeats[this.user_id] = this;
  234. });
  235. // make an object with time the key
  236. var favsAndRepeatsByTime = {};
  237. $.each(favsAndRepeats,function(){
  238. favsAndRepeatsByTime[this.time] = this;
  239. });
  240. // create an array with times and sort it
  241. var timeSorted = [];
  242. $.each(favsAndRepeats,function(){
  243. timeSorted.push(this.time);
  244. });
  245. timeSorted.sort();
  246. // display avatars in chronological order, max 7
  247. var avatarnum = 1;
  248. $.each(timeSorted,function(){
  249. q.children('.queet').find('.avatar-row').append('<a title="' + favsAndRepeatsByTime[this].fullname + '" data-user-id="' + favsAndRepeatsByTime[this].user_id + '" href="' + favsAndRepeatsByTime[this].profileurl + '"><img alt="' + favsAndRepeatsByTime[this].fullname + '" src="' + favsAndRepeatsByTime[this].avatarurl + '" class="avatar size24" id="av-' + favsAndRepeatsByTime[this].user_id + '"></a>');
  250. if(avatarnum > 15) {
  251. return false;
  252. }
  253. avatarnum++;
  254. });
  255. }
  256. /* ·
  257. ·
  258. · Build a follow/block button
  259. ·
  260. · @param obj: an object with a user array
  261. ·
  262. · · · · · · · · · */
  263. function buildFollowBlockbutton(obj) {
  264. // following?
  265. var followingClass = '';
  266. if(obj.following) {
  267. followingClass = ' following';
  268. }
  269. // blocking?
  270. var blockingClass = '';
  271. if(obj.statusnet_blocking) {
  272. blockingClass = ' blocking';
  273. }
  274. var followButton = '';
  275. if(typeof window.loggedIn.screen_name != 'undefined' // if logged in
  276. && window.loggedIn.id != obj.id) { // not if this is me
  277. if(!(obj.statusnet_profile_url.indexOf('/twitter.com/')>-1 && obj.following === false)) { // only unfollow twitter users
  278. if(obj.blocks_you) {
  279. var followButton = '<div class="user-actions">\
  280. <div class="blocks-you" data-tooltip="' + window.sL.tooltipBlocksYou.replace('{username}','@' + obj.screen_name) + '"></div>\
  281. </div>';
  282. }
  283. else {
  284. var followButton = '<div class="user-actions">\
  285. <button data-follow-user-id="' + obj.id + '" data-follow-user="' + obj.statusnet_profile_url + '" type="button" class="qvitter-follow-button' + followingClass + blockingClass + '">\
  286. <span class="button-text follow-text"><i class="follow"></i>' + window.sL.userFollow + '</span>\
  287. <span class="button-text following-text">' + window.sL.userFollowing + '</span>\
  288. <span class="button-text unfollow-text">' + window.sL.userUnfollow + '</span>\
  289. <span class="button-text blocking-text">' + window.sL.buttonBlocked + '</span>\
  290. <span class="button-text unblock-text">' + window.sL.buttonUnblock + '</span>\
  291. </button>\
  292. </div>';
  293. }
  294. }
  295. }
  296. return followButton;
  297. }
  298. /* ·
  299. ·
  300. · Build profile card HTML
  301. ·
  302. · @param data: an object with a user array
  303. ·
  304. · · · · · · · · · */
  305. function buildProfileCard(data) {
  306. data = cleanUpUserObject(data);
  307. // use avatar if no cover photo
  308. var coverPhotoHtml = '';
  309. if(data.cover_photo !== false) {
  310. coverPhotoHtml = 'background-image:url(\'' + data.cover_photo + '\')';
  311. }
  312. // follows me?
  313. var follows_you = '';
  314. if(data.follows_you === true && window.loggedIn.id != data.id) {
  315. var follows_you = '<span class="follows-you">' + window.sL.followsYou + '</span>';
  316. }
  317. // me?
  318. var is_me = '';
  319. if(window.loggedIn !== false && window.loggedIn.id == data.id) {
  320. var is_me = ' is-me';
  321. }
  322. // logged in?
  323. var logged_in = '';
  324. if(window.loggedIn !== false) {
  325. var logged_in = ' logged-in';
  326. }
  327. // silenced?
  328. var is_silenced = '';
  329. if(data.is_silenced === true) {
  330. is_silenced = ' silenced';
  331. }
  332. // sandboxed?
  333. var is_sandboxed = '';
  334. if(data.is_sandboxed === true) {
  335. is_sandboxed = ' sandboxed';
  336. }
  337. // muted?
  338. var is_muted = '';
  339. if(isUserMuted(data.id)) {
  340. is_muted = ' user-muted';
  341. }
  342. // local or remote user?
  343. var local_or_remote = '';
  344. var remote_user_info = '';
  345. var serverUrl = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.statusnet_profile_url, data.screen_name);
  346. data.screenNameWithServer = '@' + data.screen_name + '@' + serverUrl;
  347. if(data.is_local !== true) {
  348. remote_user_info = '<div class="remote-user-info">' + window.sL.thisIsARemoteUser.replace('{remote-profile-url}',data.statusnet_profile_url) + '</div>'
  349. local_or_remote = ' remote-user';
  350. }
  351. else {
  352. local_or_remote = ' local-user';
  353. }
  354. var followButton = '';
  355. // follow from external instance if logged out and the user is local
  356. if(window.loggedIn === false && data.is_local == true) {
  357. followButton = '<div class="user-actions"><button type="button" class="external-follow-button"><span class="button-text follow-text"><i class="follow"></i>' + window.sL.userExternalFollow + '</span></button></div>';
  358. }
  359. // edit profile button if it's me
  360. else if(window.loggedIn !== false && window.loggedIn.id == data.id) {
  361. followButton = '<div class="user-actions"><button type="button" class="edit-profile-button"><span class="button-text edit-profile-text">' + window.sL.editMyProfile + '</span></button></div>';
  362. }
  363. // follow button for logged in users
  364. else if(window.loggedIn !== false) {
  365. followButton = buildFollowBlockbutton(data);
  366. }
  367. // is webpage empty?
  368. var emptyWebpage = '';
  369. if(data.url.length<1) {
  370. emptyWebpage = ' empty';
  371. }
  372. // full card html
  373. var profileCardHtml = '\
  374. <div class="profile-card' + is_me + logged_in + is_muted + local_or_remote + '">\
  375. <script class="profile-json" type="application/json">' + JSON.stringify(data) + '</script>\
  376. <a href="' + data.statusnet_profile_url + '" class="ostatus-link" data-tooltip="' + window.sL.goToTheUsersRemoteProfile + '" donthijack></a>\
  377. <div class="profile-header-inner' + is_silenced + is_sandboxed + '" style="' + coverPhotoHtml + '" data-user-id="' + data.id + '" data-screen-name="' + data.screen_name + '">\
  378. <div class="profile-header-inner-overlay"></div>\
  379. <a class="profile-picture" href="' + data.profile_image_url_original + '">\
  380. <img class="avatar profile-size" src="' + data.profile_image_url_profile_size + '" data-user-id="' + data.id + '" />\
  381. </a>\
  382. <div class="profile-card-inner">\
  383. <h1 class="fullname" data-user-id="' + data.id + '">' + data.name + '</h1>\
  384. <span class="silenced-flag" data-tooltip="' + window.sL.silencedStreamDescription + '">' + window.sL.silenced + '</span>\
  385. <span class="sandboxed-flag" data-tooltip="' + window.sL.sandboxedStreamDescription + '">' + window.sL.sandboxed + '</span>\
  386. <h2 class="username">\
  387. <span class="screen-name-with-server" data-user-id="' + data.id + '">' + data.screenNameWithServer + '</span>\
  388. <span class="screen-name" data-user-id="' + data.id + '">@' + data.screen_name + '</span>\
  389. ' + follows_you + '\
  390. </h2>\
  391. <div class="bio-container"><p>' + data.description + '</p></div>\
  392. <p class="location-and-url">\
  393. <span class="location">' + data.location + '</span>\
  394. <span class="url' + emptyWebpage + '">\
  395. <span class="divider"> · </span>\
  396. <a href="' + data.url + '">' + data.url.replace('http://','').replace('https://','') + '</a>\
  397. </span>\
  398. </p>\
  399. </div>\
  400. </div>\
  401. <div class="profile-banner-footer">\
  402. ' + remote_user_info + '\
  403. <ul class="stats">\
  404. <li class="tweet-num"><a href="' + data.statusnet_profile_url + '" class="tweet-stats">' + window.sL.notices + '<strong>' + data.statuses_count + '</strong></a></li>\
  405. <li class="following-num"><a href="' + data.statusnet_profile_url + '/subscriptions" class="following-stats">' + window.sL.following + '<strong>' + data.friends_count + '</strong></a></li>\
  406. <li class="follower-num"><a href="' + data.statusnet_profile_url + '/subscribers" class="follower-stats">' + window.sL.followers + '<strong>' + data.followers_count + '</strong></a></li>\
  407. <li class="groups-num"><a href="' + data.statusnet_profile_url + '/groups" class="groups-stats">' + window.sL.groups + '<strong>' + data.groups_count + '</strong></a></li>\
  408. </ul>\
  409. ' + followButton + '\
  410. <div class="user-menu-cog' + is_silenced + is_sandboxed + logged_in + '" data-tooltip="' + window.sL.userOptions + '" data-user-id="' + data.id + '" data-screen-name="' + data.screen_name + '"></div>\
  411. <div class="clearfix"></div>\
  412. </div>\
  413. </div>\
  414. ';
  415. return { userArray: data, profileCardHtml: profileCardHtml };
  416. }
  417. /* ·
  418. ·
  419. · Build external profile card HTML
  420. ·
  421. · @param data: an object containing data.external user array,
  422. · and maybe (hopefully) also a data.local user array
  423. ·
  424. · · · · · · · · · */
  425. function buildExternalProfileCard(data) {
  426. // follows me?
  427. var follows_you = '';
  428. if(data.local !== null && data.local.follows_you === true && window.loggedIn.id != data.local.id) {
  429. var follows_you = '<span class="follows-you">' + window.sL.followsYou + '</span>';
  430. }
  431. // follow button
  432. var followButton = '';
  433. if(window.loggedIn !== false && typeof data.local != 'undefined' && data.local) {
  434. var followButton = buildFollowBlockbutton(data.local);
  435. }
  436. // me?
  437. var is_me = '';
  438. if(window.loggedIn !== false && window.loggedIn.id == data.local.id) {
  439. var is_me = ' is-me';
  440. }
  441. // logged in?
  442. var logged_in = '';
  443. if(window.loggedIn !== false) {
  444. var logged_in = ' logged-in';
  445. }
  446. // silenced?
  447. var is_silenced = '';
  448. if(data.local.is_silenced === true) {
  449. is_silenced = ' silenced';
  450. }
  451. // sandboxed?
  452. var is_sandboxed = '';
  453. if(data.local.is_sandboxed === true) {
  454. is_sandboxed = ' sandboxed';
  455. }
  456. // muted?
  457. var is_muted = '';
  458. if(isUserMuted(data.local.id)) {
  459. is_muted = ' user-muted';
  460. }
  461. // local id/screen_name
  462. var localUserId = data.local.id;
  463. var localUserScreenName = data.local.screen_name;
  464. // empty strings and zeros instead of null
  465. data = cleanUpUserObject(data.external);
  466. // old statusnet-versions might not have full avatar urls in their api response
  467. if(typeof data.profile_image_url_original == 'undefined'
  468. || data.profile_image_url_original === null
  469. || data.profile_image_url_original.length == 0) {
  470. data.profile_image_url_original = data.profile_image_url;
  471. }
  472. if(typeof data.profile_image_url_profile_size == 'undefined'
  473. || data.profile_image_url_profile_size === null
  474. || data.profile_image_url_profile_size.length == 0) {
  475. data.profile_image_url_profile_size = data.profile_image_url;
  476. }
  477. // we might have a cover photo
  478. if(typeof data.cover_photo != 'undefined' && data.cover_photo !== false) {
  479. var cover_photo = data.cover_photo;
  480. }
  481. else {
  482. var cover_photo = data.profile_image_url_original;
  483. }
  484. // is webpage empty?
  485. var emptyWebpage = '';
  486. if(data.url.length<1) {
  487. emptyWebpage = ' empty';
  488. }
  489. var serverUrl = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(data.statusnet_profile_url, data.screen_name);
  490. data.screenNameWithServer = '@' + data.screen_name + '@' + serverUrl;
  491. var profileCardHtml = '\
  492. <div class="profile-card' + is_me + logged_in + is_muted + '">\
  493. <div class="profile-header-inner' + is_silenced + is_sandboxed + '" style="background-image:url(\'' + cover_photo + '\')" data-user-id="' + localUserId + '" data-screen-name="' + localUserScreenName + '">\
  494. <div class="profile-header-inner-overlay"></div>\
  495. <a class="profile-picture"><img src="' + data.profile_image_url_profile_size + '" /></a>\
  496. <div class="profile-card-inner">\
  497. <a target="_blank" href="' + data.statusnet_profile_url + '">\
  498. <h1 class="fullname">' + data.name + '</h1>\
  499. <span class="silenced-flag" data-tooltip="' + window.sL.silencedStreamDescription + '">' + window.sL.silenced + '</span>\
  500. <span class="sandboxed-flag" data-tooltip="' + window.sL.sandboxedStreamDescription + '">' + window.sL.sandboxed + '</span>\
  501. <h2 class="username">\
  502. <span class="screen-name">' + data.screenNameWithServer + '</span>\
  503. <span class="ostatus-link" data-tooltip="' + window.sL.goToTheUsersRemoteProfile + '" donthijack>' + window.sL.goToTheUsersRemoteProfile + '</span>\
  504. ' + follows_you + '\
  505. </h2>\
  506. </a>\
  507. <div class="bio-container"><p>' + data.description + '</p></div>\
  508. <p class="location-and-url">\
  509. <span class="location">' + data.location + '</span>\
  510. <span class="url' + emptyWebpage + '">\
  511. <span class="divider"> · </span>\
  512. <a target="_blank" href="' + data.url + '">' + data.url.replace('http://','').replace('https://','') + '</a>\
  513. </span>\
  514. </p>\
  515. </div>\
  516. </div>\
  517. <div class="profile-banner-footer">\
  518. <ul class="stats">\
  519. <li class="tweet-num"><a class="tweet-stats" target="_blank" href="' + data.statusnet_profile_url + '">' + window.sL.notices + '<strong>' + data.statuses_count + '</strong></a></li>\
  520. <li class="following-num"><a class="following-stats" target="_blank" href="' + data.statusnet_profile_url + '/subscriptions">' + window.sL.following + '<strong>' + data.friends_count + '</strong></a></li>\
  521. <li class="follower-num"><a class="follower-stats" target="_blank" href="' + data.statusnet_profile_url + '/subscribers">' + window.sL.followers + '<strong>' + data.followers_count + '</strong></a></li>\
  522. </ul>\
  523. ' + followButton + '\
  524. <div class="user-menu-cog' + is_silenced + is_sandboxed + logged_in + '" data-tooltip="' + window.sL.userOptions + '" data-user-id="' + localUserId + '" data-screen-name="' + localUserScreenName + '"></div>\
  525. <div class="clearfix"></div>\
  526. </div>\
  527. </div>\
  528. <div class="clearfix"></div>';
  529. return { userArray: data, profileCardHtml: profileCardHtml };
  530. }
  531. /* ·
  532. ·
  533. · Adds a profile card before feed element
  534. ·
  535. · @param data: an object with a user array
  536. ·
  537. · · · · · · · · · */
  538. function addProfileCardToDOM(data) {
  539. // change design
  540. changeDesign({backgroundimage:data.userArray.background_image, backgroundcolor:data.userArray.backgroundcolor, linkcolor:data.userArray.linkcolor});
  541. // remove any old profile card and show profile card
  542. $('#feed').siblings('.profile-card').remove();
  543. $('#feed').before(data.profileCardHtml);
  544. }
  545. /* ·
  546. ·
  547. · Adds a group profile card before feed element
  548. ·
  549. · @param data: an object with one or more queet objects
  550. ·
  551. · · · · · · · · · */
  552. function groupProfileCard(groupAlias) {
  553. getFromAPI('statusnet/groups/show/' + groupAlias + '.json', function(data){ if(data){
  554. data.nickname = data.nickname || '';
  555. data.fullname = data.fullname || '';
  556. data.stream_logo = data.stream_logo || window.defaultAvatarStreamSize;
  557. data.homepage_logo = data.homepage_logo || window.defaultAvatarProfileSize;
  558. data.original_logo = data.original_logo || window.defaultAvatarProfileSize;
  559. data.description = data.description || '';
  560. data.homepage = data.homepage || '';
  561. data.url = data.url || '';
  562. data.member_count = data.member_count || 0;
  563. data.admin_count = data.admin_count || 0;
  564. // show user actions if logged in
  565. var memberClass = '';
  566. if(data.member) {
  567. memberClass = 'member';
  568. }
  569. var memberButton = '';
  570. if(typeof window.loggedIn.screen_name != 'undefined') {
  571. var memberButton = '<div class="user-actions"><button data-group-id="' + data.id + '" type="button" class="member-button ' + memberClass + '"><span class="button-text join-text"><i class="join"></i>' + window.sL.joinGroup + '</span><span class="button-text ismember-text">' + window.sL.isMemberOfGroup + '</span><span class="button-text leave-text">' + window.sL.leaveGroup + '</span></button></div>';
  572. }
  573. // follow from external instance if logged out
  574. if(typeof window.loggedIn.screen_name == 'undefined') {
  575. var memberButton = '<div class="user-actions"><button type="button" class="external-member-button"><span class="button-text join-text"><i class="join"></i>' + window.sL.joinExternalGroup + '</span></button></div>';
  576. }
  577. // change design
  578. changeDesign({backgroundimage:false, backgroundcolor:false, linkcolor:false});
  579. // add card to DOM
  580. $('#feed').siblings('.profile-card').remove(); // remove any old profile card
  581. $('#feed').before(' <div class="profile-card group">\
  582. <div class="profile-header-inner" style="background-image:url(' + data.original_logo + ')">\
  583. <div class="profile-header-inner-overlay"></div>\
  584. <a class="profile-picture" href="' + data.original_logo + '">\
  585. <img src="' + data.homepage_logo + '" />\
  586. </a>\
  587. <div class="profile-card-inner">\
  588. <a href="' + window.siteInstanceURL + 'group/' + data.nickname + '">\
  589. <h1 class="fullname">\
  590. ' + data.fullname + '\
  591. <span></span>\
  592. </h1>\
  593. <h2 class="username">\
  594. <span class="screen-name">!' + data.nickname + '</span>\
  595. </h2>\
  596. </a>\
  597. <div class="bio-container">\
  598. <p>' + data.description + '</p>\
  599. </div>\
  600. <p class="location-and-url">\
  601. <span class="url">\
  602. <a href="' + data.homepage + '">' + data.homepage.replace('http://','').replace('https://','') + '</a>\
  603. </span>\
  604. </p>\
  605. </div>\
  606. </div>\
  607. <div class="profile-banner-footer">\
  608. <ul class="stats">\
  609. <li>\
  610. <a href="' + window.siteInstanceURL + 'group/' + data.nickname + '/members" class="member-stats">\
  611. ' + window.sL.memberCount + '\
  612. <strong>' + data.member_count + '</strong>\
  613. </a>\
  614. </li>\
  615. <li>\
  616. <a href="' + window.siteInstanceURL + 'group/' + data.nickname + '/admins" class="admin-stats">\
  617. ' + window.sL.adminCount + '\
  618. <strong>' + data.admin_count + '</strong>\
  619. </a>\
  620. </li>\
  621. </ul>\
  622. ' + memberButton + '\
  623. <div class="clearfix"></div>\
  624. </div>\
  625. </div>');
  626. }});
  627. }
  628. /* ·
  629. ·
  630. · Change stream
  631. ·
  632. · @param streamObject: object returned by pathToStreamRouter()
  633. · @param setLocation: whether we should update the browsers location bar when we set the new stream
  634. · @param fallbackId: if we fail to get the stream, it can be due to a bad/changed user/group nickname,
  635. · in that case this parameter can contain a user/group id that we can use to retrieve the correct nickname
  636. · @param actionOnSuccess: callback function on success
  637. ·
  638. · · · · · · · · · */
  639. function setNewCurrentStream(streamObject,setLocation,fallbackId,actionOnSuccess) {
  640. if(!streamObject || !streamObject.stream) {
  641. console.log('invalid streamObject, no stream to set!');
  642. return;
  643. }
  644. // update the cache for the old stream
  645. rememberStreamStateInLocalStorage();
  646. // remove any old error messages
  647. $('.error-message').remove();
  648. // halt interval that checks for new queets
  649. window.clearInterval(checkForNewQueetsInterval);
  650. // scroll to top
  651. $(window).scrollTop(0);
  652. $('body').addClass('androidFix').scrollTop(0).removeClass('androidFix');
  653. // blur any selected links
  654. $('a').blur();
  655. // unset metadata for the old stream saved in attributes
  656. $('#feed-body').removeAttr('data-search-page-number');
  657. $('#feed-body').removeAttr('data-end-reached');
  658. // hide new queets bar and reload stream button
  659. $('#new-queets-bar-container').addClass('hidden');
  660. $('.reload-stream').hide();
  661. // remove old menu cog
  662. $('#stream-menu-cog').remove();
  663. display_spinner('#feed-header-inner');
  664. // are we just reloading?
  665. var weAreReloading = false;
  666. if(typeof window.currentStreamObject != 'undefined' && window.currentStreamObject.stream == streamObject.stream) {
  667. weAreReloading = true;
  668. }
  669. // show hidden items when we reload
  670. if(weAreReloading) {
  671. $('#feed-body').children('.stream-item').removeClass('hidden');
  672. }
  673. // remember the most recent stream object
  674. window.currentStreamObject = streamObject;
  675. // set the new streams header and description
  676. if(streamObject.streamSubHeader) {
  677. $('#stream-header').html(streamObject.streamSubHeader);
  678. }
  679. else {
  680. $('#stream-header').html(streamObject.streamHeader);
  681. }
  682. if(streamObject.streamDescription !== false) {
  683. $('#feed-header-description').html(streamObject.streamDescription);
  684. }
  685. else {
  686. $('#feed-header-description').empty();
  687. }
  688. // add menu cog if this stream has a menu
  689. if(streamObject.menu && window.loggedIn) {
  690. $('#feed-header-inner h2').append('<div id="stream-menu-cog" data-tooltip="' + window.sL.timelineOptions + '"></div>');
  691. }
  692. // subtle animation to show somethings happening
  693. $('#feed-header-inner h2').css('opacity','0.2');
  694. $('#feed-header-inner h2').animate({opacity:'1'},1000);
  695. // if we're just reloading, we dont need to:
  696. // (1) check if we have a cached version of this stream
  697. // (2) remove the stream if we don't
  698. // (3) change design
  699. if(weAreReloading === false) {
  700. // (1) check if we have a cached version of the stream
  701. var haveOldStreamState = localStorageObjectCache_GET('streamState',window.currentStreamObject.path);
  702. // discard and remove any cached data that is not in this (new) format
  703. if(typeof haveOldStreamState.card == 'undefined'
  704. || typeof haveOldStreamState.feed == 'undefined') {
  705. localStorageObjectCache_STORE('streamState',window.currentStreamObject.path, false);
  706. haveOldStreamState = false;
  707. }
  708. // show cached version immediately
  709. if(haveOldStreamState) {
  710. $('.profile-card,.hover-card,.hover-card-caret').remove();
  711. $('#feed').before(haveOldStreamState.card);
  712. var oldStreamState = $('<div/>').html(haveOldStreamState.feed);
  713. // if the cached items has data-quitter-id-in-stream attributes, sort them before adding them
  714. if(oldStreamState.children('.stream-item[data-quitter-id-in-stream]').length>0) {
  715. oldStreamState.sortDivsByAttrDesc('data-quitter-id-in-stream');
  716. }
  717. // hide any removed notices that we know of
  718. if(typeof window.knownDeletedNotices != 'undefined') {
  719. $.each(window.knownDeletedNotices,function(delededURI,v){
  720. oldStreamState.children('.stream-item[data-uri="' + delededURI + '"]').addClass('deleted always-hidden');
  721. });
  722. }
  723. // hide all notices from blocked users (not for user lists)
  724. if(window.currentStreamObject.type != 'users' && typeof window.allBlocking != 'undefined') {
  725. markAllNoticesFromBlockedUsersAsBlockedInJQueryObject(oldStreamState);
  726. }
  727. // mark all notices and profile cards from muted users as muted
  728. markAllNoticesFromMutedUsersAsMutedInJQueryObject(oldStreamState);
  729. markAllProfileCardsFromMutedUsersAsMutedInDOM();
  730. // hide dublicate repeats, only show the first/oldest instance of a notice
  731. oldStreamState = hideAllButOldestInstanceOfStreamItem(oldStreamState);
  732. // show full notice text for all cached notices, if we have it in cache
  733. $.each(oldStreamState.children('.stream-item'),function(){
  734. getFullUnshortenedHtmlForQueet($(this),true);
  735. });
  736. // if this is notidfications we have seen obviously seen them before
  737. oldStreamState.children('.stream-item').removeClass('not-seen');
  738. $('#feed-body').html(oldStreamState.html());
  739. // set location bar from stream
  740. if(setLocation) {
  741. setUrlFromStream(streamObject);
  742. setLocation = false; // don't set location twice if we've already set it here
  743. }
  744. // update all queets times
  745. updateAllQueetsTimes();
  746. // also mark this stream as the current stream immediately, if a saved copy exists
  747. addStreamToHistoryMenuAndMarkAsCurrent(streamObject);
  748. // make sure page-container and feed is visible
  749. $('#page-container').css('opacity','1');
  750. $('#feed').css('opacity','1');
  751. // run callbacks for this stream
  752. if(streamObject.callbacks !== false) {
  753. $.each(streamObject.callbacks, function(){
  754. if(typeof window[this] == 'function') {
  755. window[this]();
  756. }
  757. });
  758. }
  759. // maybe do something
  760. if(typeof actionOnSuccess == 'function') {
  761. actionOnSuccess();
  762. // don't invoke actionOnSuccess later if we already invoked it here
  763. actionOnSuccess = false;
  764. }
  765. }
  766. // (2) if we don't have a cached version we remove and hide the old stream and wait for the new one to load
  767. else {
  768. $('.profile-card,.hover-card,.hover-card-caret').remove();
  769. $('#feed').css('opacity',0);
  770. $('#feed-body').html('');
  771. remove_spinner(); display_spinner(); // display spinner in page header instead feed header
  772. }
  773. // (3) change design immediately to either cached design or logged in user's
  774. if(typeof window.oldStreamsDesigns[window.currentStreamObject.nickname] != 'undefined') {
  775. changeDesign(window.oldStreamsDesigns[window.currentStreamObject.nickname]);
  776. }
  777. else {
  778. changeDesign({backgroundimage:window.loggedIn.background_image, backgroundcolor:window.loggedIn.backgroundcolor, linkcolor:window.loggedIn.linkcolor});
  779. }
  780. }
  781. // get stream
  782. getFromAPI(streamObject.stream, function(queet_data, userArray, error, url){
  783. // while waiting for this data user might have changed stream, so only proceed if current stream still is this one
  784. if(window.currentStreamObject.stream != streamObject.stream) {
  785. console.log('stream has changed, aborting');
  786. return;
  787. }
  788. // if we have a fallbackId and a userArray, and the userArray's id is not equal to
  789. // the fallackId, this is the wrong stream! we need to re-invoke setNewCurrentStream()
  790. // with the correct and up-to-date nickname (maybe best not to send a fallbackId here not
  791. // to risk getting into an infinite loop caused by bad data)
  792. // also, we do the same thing if getting the stream fails, but we have a fallback id
  793. if((userArray && fallbackId && userArray.id != fallbackId)
  794. || (queet_data === false && fallbackId)) {
  795. if(streamObject.name == 'profile') {
  796. getNicknameByUserIdFromAPI(fallbackId,function(nickname) {
  797. if(nickname) {
  798. setNewCurrentStream(pathToStreamRouter(nickname),true,false,actionOnSuccess);
  799. }
  800. else {
  801. // redirect to front page if everything fails
  802. setNewCurrentStream(pathToStreamRouter('/'),true,false,actionOnSuccess);
  803. }
  804. });
  805. }
  806. else if(streamObject.name == 'group notice stream') {
  807. getNicknameByGroupIdFromAPI(fallbackId,function(nickname) {
  808. if(nickname) {
  809. setNewCurrentStream(pathToStreamRouter('group/' + nickname),true,false,actionOnSuccess);
  810. }
  811. else {
  812. // redirect to front page if everything fails
  813. setNewCurrentStream(pathToStreamRouter('/'),true,false,actionOnSuccess);
  814. }
  815. });
  816. }
  817. }
  818. // local for local users requested by id, we want to go to the nickname-url instead
  819. else if(streamObject.name == 'profile by id' && userArray && userArray.is_local === true) {
  820. removeHistoryContainerItemByHref(window.siteInstanceURL + 'user/' + userArray.id);
  821. setNewCurrentStream(pathToStreamRouter('/' + userArray.screen_name),true,false,actionOnSuccess);
  822. }
  823. // getting stream failed, and we don't have a fallback id
  824. else if(queet_data === false) {
  825. // e.g. maybe fade in user-container here, ("success" was a badly chosen name...)
  826. if(typeof actionOnSuccess == 'function') {
  827. actionOnSuccess();
  828. }
  829. if(error.status == 401) {
  830. showErrorMessage(window.sL.ERRORmustBeLoggedIn);
  831. }
  832. else if(error.status == 404) {
  833. if(streamObject.name == 'profile'
  834. || streamObject.name == 'friends timeline'
  835. || streamObject.name == 'mentions'
  836. || streamObject.name == 'favorites'
  837. || streamObject.name == 'subscribers'
  838. || streamObject.name == 'subscriptions'
  839. || streamObject.name == 'user group list') {
  840. showErrorMessage(window.sL.ERRORcouldNotFindUserWithNickname.replace('{nickname}',replaceHtmlSpecialChars(streamObject.nickname)));
  841. }
  842. else if(streamObject.name == 'group notice stream'
  843. || streamObject.name == 'group member list'
  844. || streamObject.name == 'group admin list') {
  845. showErrorMessage(window.sL.ERRORcouldNotFindGroupWithNickname.replace('{nickname}',replaceHtmlSpecialChars(streamObject.nickname)));
  846. }
  847. else if(streamObject.name == 'list notice stream'
  848. || streamObject.name == 'list members'
  849. || streamObject.name == 'list subscribers') {
  850. showErrorMessage(window.sL.ERRORcouldNotFindList);
  851. }
  852. else {
  853. showErrorMessage(window.sL.ERRORcouldNotFindPage + '<br><br>url: ' + url);
  854. }
  855. emptyRememberAndHideFeed();
  856. }
  857. else if(error.status == 410 && streamObject.name == 'notice') {
  858. showErrorMessage(window.sL.ERRORnoticeRemoved);
  859. emptyRememberAndHideFeed();
  860. }
  861. else if(error.status == 0
  862. || (error.status == 200 && error.responseText == 'An error occurred.')
  863. ) {
  864. showErrorMessage(window.sL.ERRORnoContactWithServer + ' (' + replaceHtmlSpecialChars(error.statusText) + ')');
  865. // don't hide feed for these errors
  866. }
  867. else if (typeof error.responseJSON != 'undefined' && typeof error.responseJSON.error != 'undefined' && error.responseJSON.error.length > 0) {
  868. showErrorMessage(error.responseJSON.error);
  869. emptyRememberAndHideFeed();
  870. }
  871. else {
  872. showErrorMessage(window.sL.ERRORsomethingWentWrong + '<br><br>\
  873. url: ' + url + '<br><br>\
  874. jQuery ajax() error:<pre><code>' + replaceHtmlSpecialChars(JSON.stringify(error, null, ' ')) + '</code></pre>\
  875. ');
  876. }
  877. // make sure page-container is visible
  878. $('#page-container').css('opacity','1');
  879. }
  880. // everything seems fine, show the new stream
  881. else if(queet_data) {
  882. // set location bar from stream
  883. if(setLocation) {
  884. setUrlFromStream(streamObject);
  885. }
  886. // add this stream to the history menu
  887. addStreamToHistoryMenuAndMarkAsCurrent(streamObject);
  888. // profile card from user array
  889. if(userArray) {
  890. addProfileCardToDOM(buildProfileCard(userArray));
  891. // set remote users username in the browsing history container
  892. // (because stream-router can't know username from the URL, that only have id:s)
  893. if(userArray.is_local !== true) {
  894. var serverUrl = guessInstanceUrlWithoutProtocolFromProfileUrlAndNickname(userArray.statusnet_profile_url, userArray.screen_name);
  895. var screenNameWithServer = '@' + userArray.screen_name + '@' + serverUrl;
  896. updateHistoryContainerItemByHref(window.siteInstanceURL + 'user/' + userArray.id, screenNameWithServer);
  897. updateHistoryContainerItemByHref(userArray.statusnet_profile_url, screenNameWithServer);
  898. }
  899. // if we for some reason have visited a local user's profile by id, adjust the history container
  900. else {
  901. updateHistoryContainerItemByHref(window.siteInstanceURL + 'user/' + userArray.id, userArray.screen_name);
  902. }
  903. }
  904. // remove any trailing profile cards
  905. else {
  906. $('.profile-card').remove();
  907. }
  908. // show group profile card if this is a group stream
  909. if(streamObject.name == 'group notice stream'
  910. || streamObject.name == 'group member list'
  911. || streamObject.name == 'group admin list') {
  912. groupProfileCard(streamObject.nickname);
  913. }
  914. // say hello to the api if this is notifications stream, to
  915. // get correct unread notifcation count
  916. if(window.currentStreamObject.name == 'notifications') {
  917. helloAPI();
  918. }
  919. // start checking for new queets again
  920. window.clearInterval(checkForNewQueetsInterval);
  921. checkForNewQueetsInterval=window.setInterval(function(){checkForNewQueets()},window.timeBetweenPolling);
  922. remove_spinner();
  923. // some streams, e.g. /statuses/show/1234.json is not enclosed in an array, make sure it is
  924. if(!$.isArray(queet_data)) {
  925. queet_data = [queet_data];
  926. }
  927. // empty feed-body if this is a
  928. // (1) notice page
  929. // (2) if we got an empty result
  930. // (3) it's not a stream of notices or notifications
  931. if(window.currentStreamObject.name == 'notice'
  932. || queet_data.length==0
  933. || (window.currentStreamObject.type != 'notices' && window.currentStreamObject.type != 'notifications')) {
  934. $('#feed-body').html('');
  935. }
  936. // if the last item in the stream doesn't exists in the feed-body, we can't
  937. // just prepend the new items, since it will create a gap in the middle of the
  938. // feed. in that case we just empty the body and start from scratch
  939. else if($('#feed-body').children('.stream-item[data-quitter-id-in-stream="' + queet_data.slice(-1)[0].id + '"]').length == 0) {
  940. $('#feed-body').html('');
  941. }
  942. // if the stream is slow to load, the user might have expanded a notice, or scrolled down
  943. // and started reading. in that case we add the new items _hidden_
  944. if($('#feed-body').children('.stream-item.expanded').length>0 || $(window).scrollTop() > 50) {
  945. addToFeed(queet_data, false,'hidden');
  946. maybeShowTheNewQueetsBar();
  947. }
  948. else {
  949. addToFeed(queet_data, false,'visible');
  950. }
  951. // make sure page-container is visible
  952. $('#page-container').css('opacity','1');
  953. $('#feed').css('opacity','1');
  954. // run callbacks for this stream
  955. if(streamObject.callbacks !== false) {
  956. $.each(streamObject.callbacks, function(){
  957. if(typeof window[this] == 'function') {
  958. window[this]();
  959. }
  960. });
  961. }
  962. $('.reload-stream').show();
  963. $('body').removeClass('loading-older');$('body').removeClass('loading-newer');
  964. // maybe do something
  965. if(typeof actionOnSuccess == 'function') {
  966. actionOnSuccess();
  967. }
  968. }
  969. });
  970. }
  971. /* ·
  972. ·
  973. · Empties the feed body, remembers the new empty state in localstorage and hide the feed and any profile card
  974. · and mark this stream as current
  975. ·
  976. · · · · · · · · · */
  977. function emptyRememberAndHideFeed() {
  978. $('#feed').css('opacity','0');
  979. $('#feed-body').empty();
  980. $('.profile-card').remove();
  981. rememberStreamStateInLocalStorage();
  982. }
  983. /* ·
  984. ·
  985. · Add this stream to history menu if it doesn't exist in stream selection menus (if we're logged in)
  986. · and mark this stream as current
  987. ·
  988. · @param streamObject: stream object returned by pathToStreamRouter()
  989. ·
  990. · · · · · · · · · */
  991. function addStreamToHistoryMenuAndMarkAsCurrent(streamObject) {
  992. if(streamObject.parentPath) {
  993. var urlToMarkAsCurrent = window.siteInstanceURL + streamObject.parentPath;
  994. }
  995. else {
  996. var urlToMarkAsCurrent = window.siteInstanceURL + streamObject.path;
  997. }
  998. if($('.stream-selection[href="' + urlToMarkAsCurrent + '"]').length==0
  999. && typeof window.loggedIn.screen_name != 'undefined') {
  1000. $('#history-container').prepend('<a class="stream-selection" href="' + urlToMarkAsCurrent + '">' + streamObject.streamHeader + '<i class="chev-right" data-tooltip="' + window.sL.tooltipBookmarkStream + '"></i></a>');
  1001. updateHistoryLocalStorage();
  1002. // max 10 in history container
  1003. var historyNum = $('#history-container').children('.stream-selection').length;
  1004. if(historyNum > 10) {
  1005. $('#history-container').children('.stream-selection').slice(-(historyNum-10)).remove();
  1006. }
  1007. }
  1008. $('.stream-selection').removeClass('current');
  1009. $('.stream-selection[href="' + urlToMarkAsCurrent + '"]').addClass('current');
  1010. }
  1011. /* ·
  1012. ·
  1013. · Expand/de-expand queet
  1014. ·
  1015. · @param q: the stream item to expand
  1016. · @param doScrolling: if we should scroll back to position or not
  1017. ·
  1018. · · · · · · · · · */
  1019. function expand_queet(q,doScrolling) {
  1020. // don't do anything if this is a queet being posted
  1021. if(q.hasClass('temp-post')) {
  1022. return;
  1023. }
  1024. // don't expand if this is a popup
  1025. if(q.closest('.modal-container').length>0) {
  1026. return;
  1027. }
  1028. doScrolling = typeof doScrolling !== 'undefined' ? doScrolling : true;
  1029. var qid = q.attr('data-quitter-id');
  1030. // de-expand if expanded
  1031. if(q.hasClass('expanded') && !q.hasClass('collapsing')) {
  1032. var sel = getSelection().toString();
  1033. if(!sel
  1034. && !q.find('.queet-button').children('button').hasClass('enabled')
  1035. && !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
  1036. // remove some things right away
  1037. q.find('.inline-reply-caret').remove();
  1038. // "unplay" gif image on collapse if there's only one attachment (switch to thumb)
  1039. var gifToUnPlay = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button').children('.attachment-thumb[data-mime-type="image/gif"]');
  1040. if(gifToUnPlay.length > 0) {
  1041. gifToUnPlay.attr('src',gifToUnPlay.attr('data-thumb-url'));
  1042. gifToUnPlay.parent('.thumb-container').css('background-image','url(\'' + gifToUnPlay.attr('data-thumb-url') + '\')');
  1043. }
  1044. // show thumbs (if hidden) and remove any iframe video immediately
  1045. q.children('.queet').find('.queet-thumbs').removeClass('hide-thumbs');
  1046. q.children('.queet').find('iframe').remove();
  1047. q.addClass('collapsing');
  1048. if(q.hasClass('conversation')) {
  1049. q.removeClass('expanded');
  1050. q.removeClass('collapsing');
  1051. q.find('.view-more-container-top').remove();
  1052. q.find('.view-more-container-bottom').remove();
  1053. q.find('.stream-item.conversation').remove();
  1054. q.find('.inline-reply-queetbox').remove();
  1055. q.find('.expanded-content').remove();
  1056. }
  1057. else {
  1058. rememberMyScrollPos(q.children('.queet'),qid,0);
  1059. // give stream item a height
  1060. q.css('height',q.height() + 'px');
  1061. q.children('.queet').find('.expanded-content').css('height',q.find('.expanded-content').height() + 'px');
  1062. q.children('.queet').find('.queet-thumbs.thumb-num-1').css('max-height',q.find('.queet-thumbs.thumb-num-1').height() + 'px');
  1063. q.children('.queet').find('.queet-thumbs.thumb-num-1 .thumb-container').css('max-height',q.find('.queet-thumbs.thumb-num-1').height() + 'px');
  1064. q.children('div').not('.queet').children('a').css('opacity','0.5');
  1065. q.children('div').not('.queet').children().children().css('opacity','0.5');
  1066. var collapseTime = 100 + q.find('.stream-item.conversation:not(.hidden-conversation)').length*50;
  1067. // set transition time (needs to be delayed, otherwise webkit animates the height-setting above)
  1068. setTimeout(function() {
  1069. q.children('.queet').css('-moz-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1070. q.children('.queet').css('-o-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1071. q.children('.queet').css('-webkit-transition-duration',Math.round( collapseTime * 1000 * 10) / 10 + 's');
  1072. q.children('.queet').css('transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1073. 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');
  1074. 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');
  1075. 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');
  1076. 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');
  1077. q.css('-moz-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1078. q.css('-o-transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1079. q.css('-webkit-transition-duration',Math.round( collapseTime * 1000 * 10) / 10 + 's');
  1080. q.css('transition-duration',Math.round( collapseTime / 1000 * 10) / 10 + 's');
  1081. // set new heights and margins to animate to
  1082. 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;
  1083. if(animateToHeight < 73) { // no less than this
  1084. animateToHeight = 73;
  1085. }
  1086. q.css('height',animateToHeight + 'px');
  1087. q.children('.queet').css('margin-top', '-' + (q.children('.queet').offset().top - q.offset().top) + 'px');
  1088. q.children('.queet').find('.expanded-content').css('height','0');
  1089. q.children('.queet').find('.queet-thumbs.thumb-num-1, .queet-thumbs.thumb-num-1 .thumb-container').css('max-height','250px');
  1090. if(doScrolling) {
  1091. setTimeout(function() {
  1092. backToMyScrollPos(q,qid,500,function(){
  1093. cleanUpAfterCollapseQueet(q);
  1094. });
  1095. }, collapseTime);
  1096. }
  1097. else {
  1098. setTimeout(function() {
  1099. cleanUpAfterCollapseQueet(q);
  1100. }, collapseTime);
  1101. }
  1102. }, 50);
  1103. }
  1104. }
  1105. }
  1106. else if(!q.hasClass('collapsing')) {
  1107. // not for acitivity or notifications
  1108. if(!q.hasClass('activity') && !q.hasClass('repeat') && !q.hasClass('like') && !q.hasClass('follow')) {
  1109. q.addClass('expanded');
  1110. q.prev().addClass('next-expanded');
  1111. // get full html, if shortened
  1112. getFullUnshortenedHtmlForQueet(q);
  1113. // add expanded container
  1114. var longdate = parseTwitterLongDate(q.find('.created-at').attr('data-created-at'));
  1115. var qurl = q.find('.created-at').find('a').attr('href');
  1116. var metadata = '<span class="longdate" title="' + longdate + '">' + longdate + ' · ' + unescape(q.attr('data-source')) + '</span> · <a href="' + qurl + '" class="permalink-link">' + window.sL.details + '</a>';
  1117. // show expanded content
  1118. q.find('.stream-item-footer').before('<div class="expanded-content"><div class="queet-stats-container"></div><div class="client-and-actions"><span class="metadata">' + metadata + '</span></div></div>');
  1119. // "play" gif image on expand if there's only one attachment (switch to full gif from thumb)
  1120. var gifToPlay = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button').children('.attachment-thumb[data-mime-type="image/gif"]');
  1121. if(gifToPlay.length > 0) {
  1122. gifToPlay.attr('src',gifToPlay.attr('data-full-image-url'));
  1123. gifToPlay.parent('.thumb-container').css('background-image','url(\'' + gifToPlay.attr('data-full-image-url') + '\')');
  1124. }
  1125. // if there's only one thumb and it's a youtube video, show it inline
  1126. if(q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.host-youtube-com').length == 1) {
  1127. var youtubeURL = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.host-youtube-com').children('.attachment-thumb').attr('data-full-image-url');
  1128. if(q.children('.queet').find('.expanded-content').children('.media').children('iframe[src="' + youTubeEmbedLinkFromURL(youtubeURL) + '"]').length < 1) { // not if already showed
  1129. q.children('.queet').find('.queet-thumbs').addClass('hide-thumbs');
  1130. // show video
  1131. q.children('.queet').find('.expanded-content').prepend('<div class="media"><iframe width="510" height="315" src="' + youTubeEmbedLinkFromURL(youtubeURL) + '" frameborder="0" allowfullscreen></iframe></div>');
  1132. }
  1133. }
  1134. // if there's only one thumb and it's a vimeo video, show it inline
  1135. else if(q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.host-vimeo-com').length == 1) {
  1136. var vimeoURL = q.children('.queet').find('.queet-thumbs.thumb-num-1').children('.thumb-container.play-button.host-vimeo-com').children('.attachment-thumb').attr('data-full-image-url');
  1137. var embedURL = vimeoEmbedLinkFromURL(vimeoURL);
  1138. if(q.children('.queet').find('.expanded-content').children('.media').children('iframe[src="' + embedURL + '"]').length < 1) { // not if already showed
  1139. q.children('.queet').find('.queet-thumbs').addClass('hide-thumbs');
  1140. // show video
  1141. q.children('.queet').find('.expanded-content').prepend('<iframe src="' + embedURL + '" width="510" height="315" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>');
  1142. }
  1143. }
  1144. // show certain attachments in expanded content
  1145. if(q.children('.queet').children('script.attachment-json').length > 0
  1146. && q.children('.queet').children('script.attachment-json').text() != 'undefined') {
  1147. try {
  1148. var attachmentsParsed = JSON.parse(q.children('.queet').children('script.attachment-json').text());
  1149. }
  1150. catch(e) {
  1151. var attachmentsParsed = false;
  1152. console.log('could not parse attachment json when expanding the notice: ' + e);
  1153. console.log("attachment-json: " + q.children('.queet').children('script.attachment-json').text());
  1154. }
  1155. if(attachmentsParsed !== false) {
  1156. $.each(attachmentsParsed, function() {
  1157. var attachment_mimetype = this.mimetype;
  1158. var attachment_title = this.url;
  1159. // filename extension
  1160. var attachment_title_extension = attachment_title.substr((~-attachment_title.lastIndexOf(".") >>> 0) + 2);
  1161. // attachments in the content link to /attachment/etc url and not direct to image/video, link is in title
  1162. if(typeof attachment_title != 'undefined') {
  1163. // videos
  1164. if($.inArray(attachment_mimetype, ['video/mp4', 'video/ogg', 'video/quicktime', 'video/webm']) >=0) {
  1165. if(q.children('.queet').find('.expanded-content').children('.media').children('video').children('source[href="' + attachment_title + '"]').length < 1) { // not if already showed
  1166. // local attachment with a thumbnail
  1167. var attachment_poster = '';
  1168. if(typeof this.thumb_url != 'undefined') {
  1169. attachment_poster = ' poster="' + this.thumb_url + '"';
  1170. }
  1171. if(q.children('.queet').find('.expanded-content').children('.media').length > 0) {
  1172. q.children('.queet').find('.media').last().after('<div class="media"><video class="u-video" controls="controls"' + attachment_poster + '><source type="' + attachment_mimetype + '" src="' + attachment_title + '" /></video></div>');
  1173. }
  1174. else {
  1175. q.children('.queet').find('.expanded-content').prepend('<div class="media"><video class="u-video" controls="controls"' + attachment_poster + '><source type="' + attachment_mimetype + '" src="' + attachment_title + '" /></video></div>');
  1176. }
  1177. }
  1178. }
  1179. else {
  1180. // other plugins, e.g. gotabulo, can check for other attachment file formats to expand
  1181. window.currentlyExpanding = {
  1182. "attachment_title":attachment_title,
  1183. "attachment_mimetype":attachment_mimetype,
  1184. "attachment_title_extension":attachment_title_extension,
  1185. "streamItem":q,
  1186. "thisAttachmentLink":$(this)
  1187. };
  1188. $(document).trigger('qvitterExpandOtherAttachments');
  1189. }
  1190. }
  1191. });
  1192. }
  1193. }
  1194. // get and show favs and repeats
  1195. getFavsAndRequeetsForQueet(q, qid);
  1196. // show conversation and reply form (but not if already in conversation)
  1197. if(!q.hasClass('conversation')) {
  1198. // show conversation
  1199. getConversation(q, qid);
  1200. // show inline reply form if logged in
  1201. if(typeof window.loggedIn.screen_name != 'undefined') {
  1202. q.children('.queet').append(replyFormHtml(q,qid));
  1203. maybePrefillQueetBoxWithCachedText(q.children('.queet').find('.queet-box'));
  1204. }
  1205. }
  1206. }
  1207. }
  1208. }
  1209. function cleanUpAfterCollapseQueet(streamItem) {
  1210. var queet = streamItem.children('.queet');
  1211. streamItem.css('height','auto');
  1212. queet.css('margin-top','0');
  1213. streamItem.removeClass('expanded');
  1214. streamItem.prev().removeClass('next-expanded');
  1215. streamItem.removeClass('collapsing');
  1216. streamItem.find('.expanded-content').remove();
  1217. streamItem.find('.view-more-container-top').remove();
  1218. streamItem.find('.view-more-container-bottom').remove();
  1219. streamItem.find('.inline-reply-queetbox').remove();
  1220. streamItem.children('.stream-item.conversation').remove();
  1221. streamItem.find('.show-full-conversation').remove();
  1222. streamItem.removeAttr('style');
  1223. queet.removeAttr('style');
  1224. queet.find('.queet-thumbs.thumb-num-1').removeAttr('style');
  1225. queet.find('.queet-thumbs.thumb-num-1 .thumb-container').css('max-height','');
  1226. }
  1227. /* ·
  1228. ·
  1229. · Get a popup queet box
  1230. ·
  1231. · @return the html for the queet box
  1232. ·
  1233. · · · · · · · · · */
  1234. function queetBoxPopUpHtml() {
  1235. // if we have cached text in localstorage
  1236. var data = localStorageObjectCache_GET('queetBoxInput','pop-up-queet-box');
  1237. if(data) {
  1238. var cachedText = encodeURIComponent(data);
  1239. }
  1240. var startText = encodeURIComponent(window.sL.compose);
  1241. return '<div class="inline-reply-queetbox"><div id="pop-up-queet-box" class="queet-box queet-box-syntax" data-start-text="' + startText + '" data-cached-text="' + cachedText + '">' + decodeURIComponent(startText) + '</div><div class="syntax-middle"></div><div class="syntax-two" contenteditable="true"></div><div class="mentions-suggestions"></div><div class="queet-toolbar toolbar-reply"><div class="queet-box-extras"><button data-tooltip="' + window.sL.tooltipAttachFile + '" class="upload-image"></button><button data-tooltip="' + window.sL.tooltipShortenUrls + '" class="shorten disabled">URL</button></div><div class="queet-button"><span class="queet-counter"></span><button>' + window.sL.queetVerb + '</button></div></div></div>';
  1242. }
  1243. /* ·
  1244. ·
  1245. · Get a reply form
  1246. ·
  1247. · @param q: the stream item to open reply form in
  1248. · @param qid: queet id
  1249. · @return the html for the reply form
  1250. ·
  1251. · · · · · · · · · */
  1252. function replyFormHtml(streamItem,qid) {
  1253. var q = streamItem.children('.queet');
  1254. // if we have cached text in localstorage
  1255. var data = localStorageObjectCache_GET('queetBoxInput','queet-box-' + qid);
  1256. if(data) {
  1257. var cachedText = encodeURIComponent(data);
  1258. }
  1259. // object with ostatus-uri as key to avoid duplicates
  1260. var screenNamesToAdd = {};
  1261. // add the screen name to the one we're replying to first (if it's not me)
  1262. if(!thisIsALinkToMyProfile(streamItem.attr('data-user-profile-url')) && typeof streamItem.attr('data-user-ostatus-uri') != 'undefined') {
  1263. screenNamesToAdd[streamItem.attr('data-user-ostatus-uri')] = streamItem.attr('data-user-screen-name');
  1264. }
  1265. // old style notice (probably cached, this can be removed later)
  1266. else if (typeof streamItem.attr('data-user-ostatus-uri') == 'undefined') {
  1267. screenNamesToAdd[q.find('.account-group').attr('href')] = q.find('.screen-name').text().replace('@','');
  1268. }
  1269. // add the rest of the attentions (not me)
  1270. if(q.children('script.attentions-json').length > 0
  1271. && q.children('script.attentions-json').text() != 'undefined') {
  1272. try {
  1273. var attentionsParsed = JSON.parse(q.children('script.attentions-json').text());
  1274. }
  1275. catch(e) {
  1276. var attentionsParsed = false;
  1277. console.log('could not parse attentions json: ' + e);
  1278. console.log("attentions-json: " + q.children('script.attentions-json').text());
  1279. }
  1280. if(attentionsParsed !== false) {
  1281. $.each(attentionsParsed, function() {
  1282. if(!thisIsALinkToMyProfile(this.profileurl)
  1283. && typeof screenNamesToAdd[this.ostatus_uri] == 'undefined') {
  1284. screenNamesToAdd[this.ostatus_uri] = this.screen_name;
  1285. }
  1286. });
  1287. }
  1288. }
  1289. // build reply/rant strings
  1290. var repliesText = '';
  1291. if(Object.keys(screenNamesToAdd).length < 1
  1292. && q.find('strong.name').attr('data-user-id') == window.loggedIn.id) {
  1293. 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') == '') {
  1294. var startText = window.sL.startRant + ' ';
  1295. }
  1296. else {
  1297. var startText = window.sL.continueRant + ' ';
  1298. }
  1299. }
  1300. else {
  1301. var startText = window.sL.replyTo + ' ';
  1302. var repliesArray = [];
  1303. $.each(screenNamesToAdd, function(url,screenName){
  1304. repliesArray.push(screenName);
  1305. });
  1306. if(repliesArray.length>0) {
  1307. startText = '<a>@' + repliesArray.join('</a>&nbsp;<a>@') + '</a>&nbsp;<br>';
  1308. repliesText = '@' + repliesArray.join(' @') + '&nbsp;';
  1309. }
  1310. }
  1311. startText = encodeURIComponent(startText);
  1312. repliesText = encodeURIComponent(repliesText);
  1313. return '<div class="inline-reply-queetbox"><span class="inline-reply-caret"><span class="caret-inner"></span></span><img class="reply-avatar" src="' + $('#user-avatar').attr('src') + '" /><div class="queet-box queet-box-syntax" id="queet-box-' + qid + '" data-start-text="' + startText + '" data-replies-text="' + repliesText + '" data-cached-text="' + cachedText + '">' + decodeURIComponent(startText) + '</div><div class="syntax-middle"></div><div class="syntax-two" contenteditable="true"></div><div class="mentions-suggestions"></div><div class="queet-toolbar toolbar-reply"><div class="queet-box-extras"><button data-tooltip="' + window.sL.tooltipAttachFile + '" class="upload-image"></button><button data-tooltip="' + window.sL.tooltipShortenUrls + '" class="shorten disabled">URL</button></div><div class="queet-button"><span class="queet-counter"></span><button>' + window.sL.queetVerb + '</button></div></div></div>';
  1314. }
  1315. /* ·
  1316. ·
  1317. · Popup for replies, deletes, etc
  1318. ·
  1319. · @param popupId: popups id
  1320. · @param heading: popops header
  1321. · @param bodyHtml: popups body html
  1322. · @param footerHtml: popups footer html
  1323. ·
  1324. · · · · · · · · · · · · · */
  1325. function popUpAction(popupId, heading, bodyHtml, footerHtml, popUpWidth){
  1326. $('.modal-container').remove(); // remove any open popups
  1327. var allFooterHtml = '';
  1328. if(footerHtml) {
  1329. allFooterHtml = '<div class="modal-footer">' + footerHtml + '</div>';
  1330. }
  1331. $('body').prepend('<div id="' + popupId + '" class="modal-container"><div class="modal-draggable"><div class="modal-content"><button class="modal-close" type="button"><span class="icon"></span></button><div class="modal-header"><h3 class="modal-title">' + heading + '</h3></div><div class="modal-body">' + bodyHtml + '</div>' + allFooterHtml + '</div></div></div>');
  1332. var thisPopUp = $('#' + popupId).children('.modal-draggable');
  1333. if(typeof popUpWidth != 'undefined') {
  1334. thisPopUp.width(popUpWidth);
  1335. }
  1336. centerPopUp(thisPopUp);
  1337. }
  1338. function centerPopUp(thisPopUp) {
  1339. thisPopUp.css('margin-top','');
  1340. thisPopUp.css('margin-left','');
  1341. var this_modal_height = thisPopUp.height();
  1342. var this_modal_width = thisPopUp.width();
  1343. var popupPos = thisPopUp.offset().top - $(window).scrollTop();
  1344. if((popupPos-(this_modal_height/2))<5) {
  1345. var marginTop = 5-popupPos;
  1346. }
  1347. else {
  1348. var marginTop = 0-this_modal_height/2;
  1349. }
  1350. thisPopUp.css('margin-top', marginTop + 'px');
  1351. thisPopUp.css('margin-left', '-' + (this_modal_width/2) + 'px');
  1352. thisPopUp.draggable({ handle: ".modal-header" });
  1353. }
  1354. /* ·
  1355. ·
  1356. · Get and show conversation
  1357. ·
  1358. · This function has grown into a monster, needs fixing
  1359. ·
  1360. · · · · · · · · · · · · · */
  1361. function getConversation(q, qid) {
  1362. // check if we have a conversation for this notice cached in localstorage
  1363. var cacheData = localStorageObjectCache_GET('conversation',q.attr('data-conversation-id'));
  1364. if(cacheData) {
  1365. showConversation(q, qid, cacheData, 8);
  1366. }
  1367. // always get most recent conversation from server
  1368. getFromAPI('statusnet/conversation/' + q.attr('data-conversation-id') + '.json?count=100', function(data){ if(data) {
  1369. // cache in localstorage
  1370. localStorageObjectCache_STORE('conversation',q.attr('data-conversation-id'), data);
  1371. showConversation(q, qid,data, 0);
  1372. }});
  1373. }
  1374. function showConversation(q, qid, data, offsetScroll) {
  1375. if(data && !q.hasClass('collapsing')){
  1376. if(data.length>1) {
  1377. var before_or_after = 'before';
  1378. $.each(data.reverse(), function (key,obj) {
  1379. // switch to append after original queet
  1380. if(obj.id == qid) {
  1381. before_or_after = 'after';
  1382. }
  1383. // don't add clicked queet to DOM, but all else
  1384. // note: first we add the full conversation, but hidden
  1385. if(obj.id != qid) {
  1386. var queetTime = parseTwitterDate(obj.created_at);
  1387. var queetHtml = buildQueetHtml(obj, obj.id, 'hidden-conversation', false, true);
  1388. if(q.hasClass('expanded')) { // add queet to conversation only if still expanded
  1389. // don't add if already exist in conversation
  1390. if(q.children('.stream-item.conversation[data-quitter-id="' + obj.id + '"]').length > 0) {
  1391. // the data in the existing notice is updated automatically in searchForUpdatedNoticeData invoked from getFromAPI
  1392. }
  1393. else if(before_or_after == 'before') {
  1394. q.children('.queet').before(queetHtml);
  1395. }
  1396. else {
  1397. if(q.children('.queet').nextAll('.conversation').length < 1) {
  1398. q.children('.queet').after(queetHtml);
  1399. }
  1400. else {
  1401. q.children('.queet').nextAll('.conversation').last().after(queetHtml);
  1402. }
  1403. }
  1404. }
  1405. }
  1406. convertAttachmentMoreHref();
  1407. });
  1408. }
  1409. else {
  1410. remove_spinner();
  1411. }
  1412. // loop trough this stream items conversation and show the "strict" line of replies
  1413. rememberMyScrollPos(q.children('.queet'),qid,offsetScroll);
  1414. findInReplyToStatusAndShow(q,qid,q.attr('data-in-reply-to-status-id'),true,false);
  1415. backToMyScrollPos(q.children('.queet'),qid,false);
  1416. findAndMarkLastVisibleInConversation(q);
  1417. }
  1418. else {
  1419. remove_spinner();
  1420. }
  1421. }
  1422. /* ·
  1423. ·
  1424. · Add last&first visible class, since that's not possible to select in pure css
  1425. ·
  1426. · · · · · · · · · · · · · */
  1427. function findAndMarkLastVisibleInConversation(streamItem) {
  1428. streamItem.children().removeClass('last-visible');
  1429. streamItem.children().removeClass('first-visible-after-parent');
  1430. streamItem.children().not('.hidden-conversation').not('.always-hidden').last().addClass('last-visible');
  1431. streamItem.children('.queet').nextAll().not('.hidden-conversation').not('.always-hidden').first().addClass('first-visible-after-parent');
  1432. streamItem.children().removeClass('first-visible');
  1433. streamItem.children().not('.hidden-conversation').not('.always-hidden').first().addClass('first-visible');
  1434. }
  1435. /* ·
  1436. ·
  1437. · Recursive walker functions to view only reyplies to replies, not full conversation
  1438. ·
  1439. · · · · · · · · · · · · · */
  1440. function findInReplyToStatusAndShow(q, qid,reply,only_first,onlyINreplyto) {
  1441. var reply_found = q.find('.stream-item[data-quitter-id="' + reply + '"]');
  1442. var reply_found_reply_to = q.find('.stream-item[data-quitter-id="' + reply_found.attr('data-in-reply-to-status-id') + '"]');
  1443. if(reply_found.length>0) {
  1444. reply_found.removeClass('hidden-conversation');
  1445. reply_found.css('opacity','1');
  1446. if(only_first && reply_found_reply_to.length>0) {
  1447. if(q.children('.view-more-container-top').length < 1) {
  1448. if(q.children('.queet').prevAll('.hidden-conversation:not(.always-hidden)').length>0) {
  1449. q.prepend('<div class="view-more-container-top" data-trace-from="' + reply + '"><a>' + window.sL.viewMoreInConvBefore + '</a></div>');
  1450. }
  1451. }
  1452. findReplyToStatusAndShow(q, qid,qid,true);
  1453. }
  1454. else {
  1455. findInReplyToStatusAndShow(q, qid,reply_found.attr('data-in-reply-to-status-id'),false,onlyINreplyto);
  1456. }
  1457. }
  1458. else if(!onlyINreplyto) {
  1459. findReplyToStatusAndShow(q, qid,qid,true);
  1460. }
  1461. else {
  1462. checkForHiddenConversationQueets(q, qid);
  1463. }
  1464. }
  1465. // recursive function to find the replies to a status
  1466. function findReplyToStatusAndShow(q, qid,this_id,only_first) {
  1467. var replies_found = q.find('.stream-item[data-in-reply-to-status-id="' + this_id + '"]');
  1468. $.each(replies_found, function(k,reply_found){
  1469. var reply_founds_reply = q.find('.stream-item[data-in-reply-to-status-id="' + $(reply_found).attr('data-quitter-id') + '"]');
  1470. $(reply_found).removeClass('hidden-conversation');
  1471. $(reply_found).css('opacity','1');
  1472. if(!only_first) {
  1473. findReplyToStatusAndShow(q, qid,$(this).attr('data-quitter-id'),false);
  1474. }
  1475. if(only_first && reply_founds_reply.length>0) {
  1476. if(q.children('.view-more-container-bottom').length < 1) {
  1477. if(q.children('.queet').nextAll('.hidden-conversation:not(.always-hidden)').length>0) {
  1478. q.append('<div class="view-more-container-bottom" data-replies-after="' + qid + '"><a>' + window.sL.viewMoreInConvAfter + '</a></div>');
  1479. }
  1480. }
  1481. }
  1482. });
  1483. checkForHiddenConversationQueets(q, qid);
  1484. }
  1485. // helper function for the above recursive functions
  1486. function checkForHiddenConversationQueets(q, qid) {
  1487. // here we check if there are any remaining hidden queets in conversation, if there are, we put a "show full conversation"-link
  1488. if(q.find('.hidden-conversation:not(.always-hidden)').length>0) {
  1489. if(q.children('.queet').find('.show-full-conversation').length == 0) {
  1490. q.children('.queet').find('.stream-item-footer').append('<span class="show-full-conversation" data-stream-item-id="' + qid + '">' + window.sL.expandFullConversation + '</span>');
  1491. }
  1492. }
  1493. else {
  1494. q.children('.queet').find('.show-full-conversation').remove();
  1495. }
  1496. }
  1497. /* ·
  1498. ·
  1499. · Build stream items and add them to feed
  1500. ·
  1501. · Also a function that has grown out of control... Needs total makeover
  1502. ·
  1503. · · · · · · · · · · · · · */
  1504. function addToFeed(feed, after, extraClasses) {
  1505. // some streams, e.g. /statuses/show/1234.json is not enclosed in an array, make sure it is
  1506. if(!$.isArray(feed)) {
  1507. feed = [feed];
  1508. }
  1509. var addedToTopOfFeedBodyNum = 0;
  1510. $.each(feed.reverse(), function (key,obj) {
  1511. var extraClassesThisRun = extraClasses;
  1512. // if this is the notifications feed
  1513. if(window.currentStreamObject.name == 'notifications') {
  1514. // don't show any notices with object_type "activity"
  1515. if(typeof obj.notice != 'undefined' && obj.notice !== null && obj.notice.is_post_verb === false) {
  1516. return true;
  1517. }
  1518. // only if this notification isn't already in stream
  1519. if($('#feed-body > .stream-item[data-quitter-id-in-stream="' + obj.id + '"]').length == 0) {
  1520. obj.from_profile.description = obj.from_profile.description || '';
  1521. var notificationTime = parseTwitterDate(obj.created_at);
  1522. if(obj.is_seen == '0') {
  1523. extraClassesThisRun += ' not-seen';
  1524. }
  1525. if(isUserMuted(obj.from_profile.id)) {
  1526. extraClassesThisRun += ' user-muted';
  1527. }
  1528. // external
  1529. var ostatusHtml = '';
  1530. if(obj.from_profile.is_local === false) {
  1531. ostatusHtml = '<a target="_blank" data-tooltip="' + window.sL.goToOriginalNotice + '" class="ostatus-link" href="' + obj.from_profile.statusnet_profile_url + '" donthijack></a>';
  1532. }
  1533. if(obj.ntype == 'like') {
  1534. var noticeTime = parseTwitterDate(obj.notice.created_at);
  1535. var notificationHtml = '<div data-user-id="' + obj.from_profile.id + '" data-quitter-id-in-stream="' + obj.id + '" id="stream-item-n-' + obj.id + '" class="stream-item ' + extraClassesThisRun + ' notification like">\
  1536. <div class="queet">\
  1537. <div class="dogear"></div>\
  1538. ' + ostatusHtml + '\
  1539. <div class="queet-content">\
  1540. <div class="stream-item-header">\
  1541. <a class="account-group" href="' + obj.from_profile.statusnet_profile_url + '" data-user-id="' + obj.from_profile.id + '">\
  1542. <img class="avatar standard-size" src="' + obj.from_profile.profile_image_url + '" data-user-id="' + obj.from_profile.id + '" />\
  1543. <strong class="name" data-user-id="' + obj.from_profile.id + '" title="@' + obj.from_profile.screen_name + '">\
  1544. ' + obj.from_profile.name + '\
  1545. </strong>\
  1546. </a> \
  1547. ' + window.sL.xFavedYourQueet + '<small class="created-at" data-created-at="' + obj.created_at + '" data-tooltip="' + parseTwitterLongDate(obj.created_at) + '"> <a>' + notificationTime + '</a></small>\
  1548. </div>\
  1549. <div class="small-grey-notice">\
  1550. <a data-created-at="' + obj.notice.created_at + '" data-tooltip="' + parseTwitterLongDate(obj.notice.created_at) + '" href="' + window.siteInstanceURL + 'notice/' + obj.notice.id + '">\
  1551. ' + noticeTime + '\
  1552. </a>: \
  1553. ' + $.trim(obj.notice.statusnet_html) + '\
  1554. </div>\
  1555. </div>\
  1556. </div>\
  1557. </div>';
  1558. }
  1559. else if(obj.ntype == 'repeat') {
  1560. var noticeTime = parseTwitterDate(obj.notice.created_at);
  1561. var notificationHtml = '<div data-user-id="' + obj.from_profile.id + '" data-quitter-id-in-stream="' + obj.id + '" id="stream-item-n-' + obj.id + '" class="stream-item ' + extraClassesThisRun + ' notification repeat">\
  1562. <div class="queet">\
  1563. <div class="dogear"></div>\
  1564. ' + ostatusHtml + '\
  1565. <div class="queet-content">\
  1566. <div class="stream-item-header">\
  1567. <a class="account-group" href="' + obj.from_profile.statusnet_profile_url + '" data-user-id="' + obj.from_profile.id + '">\
  1568. <img class="avatar standard-size" src="' + obj.from_profile.profile_image_url + '" data-user-id="' + obj.from_profile.id + '" />\
  1569. <strong class="name" data-user-id="' + obj.from_profile.id + '" title="@' + obj.from_profile.screen_name + '">\
  1570. ' + obj.from_profile.name + '\
  1571. </strong>\
  1572. </a> \
  1573. ' + window.sL.xRepeatedYourQueet + '<small class="created-at" data-created-at="' + obj.created_at + '" data-tooltip="' + parseTwitterLongDate(obj.created_at) + '"> <a>' + notificationTime + '</a></small>\
  1574. </div>\
  1575. <div class="small-grey-notice">\
  1576. <a data-created-at="' + obj.notice.created_at + '" data-tooltip="' + parseTwitterLongDate(obj.notice.created_at) + '" href="' + window.siteInstanceURL + 'notice/' + obj.notice.id + '">\
  1577. ' + noticeTime + '\
  1578. </a>: \
  1579. ' + $.trim(obj.notice.statusnet_html) + '\
  1580. </div>\
  1581. </div>\
  1582. </div>\
  1583. </div>';
  1584. }
  1585. else if(obj.ntype == 'mention') {
  1586. var notificationHtml = buildQueetHtml(obj.notice, obj.id, extraClassesThisRun + ' notification mention');
  1587. }
  1588. else if(obj.ntype == 'reply') {
  1589. var notificationHtml = buildQueetHtml(obj.notice, obj.id, extraClassesThisRun + ' notification reply');
  1590. }
  1591. else if(obj.ntype == 'follow') {
  1592. var notificationHtml = '<div data-user-id="' + obj.from_profile.id + '" data-quitter-id-in-stream="' + obj.id + '" id="stream-item-n-' + obj.id + '" class="stream-item ' + extraClassesThisRun + ' notification follow">\
  1593. <div class="queet">\
  1594. <div class="queet-content">\
  1595. ' + ostatusHtml + '\
  1596. <div class="stream-item-header">\
  1597. <a class="account-group" href="' + obj.from_profile.statusnet_profile_url + '" data-user-id="' + obj.from_profile.id + '">\
  1598. <img class="avatar standard-size" src="' + obj.from_profile.profile_image_url + '" data-user-id="' + obj.from_profile.id + '" />\
  1599. <strong class="name" data-user-id="' + obj.from_profile.id + '" title="@' + obj.from_profile.screen_name + '">\
  1600. ' + obj.from_profile.name + '\
  1601. </strong>\
  1602. </a> \
  1603. ' + window.sL.xStartedFollowingYou + '<small class="created-at" data-created-at="' + obj.created_at + '" title="' + obj.created_at + '"> <a>' + notificationTime + '</a></small>\
  1604. </div>\
  1605. </div>\
  1606. </div>\
  1607. </div>';
  1608. }
  1609. if(after) {
  1610. $('#' + after).after(notificationHtml);
  1611. }
  1612. else {
  1613. $('#feed-body').prepend(notificationHtml);
  1614. addedToTopOfFeedBodyNum++;
  1615. }
  1616. }
  1617. }
  1618. // if this is a user feed
  1619. else if(window.currentStreamObject.type == 'users') {
  1620. // only if not user is already in stream
  1621. if($('#stream-item-' + obj.id).length == 0) {
  1622. var userHtml = buildUserStreamItemHtml(obj);
  1623. if(after) {
  1624. $('#' + after).after(userHtml);
  1625. }
  1626. else {
  1627. $('#feed-body').prepend(userHtml);
  1628. addedToTopOfFeedBodyNum++;
  1629. }
  1630. }
  1631. }
  1632. // if this is a list of groups
  1633. else if(window.currentStreamObject.type == 'groups') {
  1634. // only if not group is already in stream
  1635. if($('#stream-item-' + obj.id).length == 0) {
  1636. obj.description = obj.description || '';
  1637. obj.stream_logo = obj.stream_logo || window.defaultAvatarStreamSize;
  1638. // rtl or not
  1639. var rtlOrNot = '';
  1640. if($('body').hasClass('rtl')) {
  1641. rtlOrNot = 'rtl';
  1642. }
  1643. // show group actions if logged in
  1644. var memberClass = '';
  1645. if(obj.member) {
  1646. memberClass = 'member';
  1647. }
  1648. var memberButton = '';
  1649. if(typeof window.loggedIn.screen_name != 'undefined') {
  1650. var memberButton = '<div class="user-actions"><button data-group-id="' + obj.id + '" type="button" class="member-button ' + memberClass + '"><span class="button-text join-text"><i class="join"></i>' + window.sL.joinGroup + '</span><span class="button-text ismember-text">' + window.sL.isMemberOfGroup + '</span><span class="button-text leave-text">' + window.sL.leaveGroup + '</span></button></div>';
  1651. }
  1652. var groupAvatar = obj.stream_logo;
  1653. if(obj.homepage_logo != null) {
  1654. groupAvatar = obj.homepage_logo;
  1655. }
  1656. var groupHtml = '<div id="stream-item-' + obj.id + '" class="stream-item user"><div class="queet ' + rtlOrNot + '">' + memberButton + '<div class="queet-content"><div class="stream-item-header"><a class="account-group" href="' + obj.url + '"><img class="avatar" src="' + groupAvatar + '" /><strong class="name" data-group-id="' + obj.id + '">' + obj.fullname + '</strong> <span class="screen-name">!' + obj.nickname + '</span></a></div><div class="queet-text">' + obj.description + '</div></div></div></div>';
  1657. if(after) {
  1658. $('#' + after).after(groupHtml);
  1659. }
  1660. else {
  1661. $('#feed-body').prepend(groupHtml);
  1662. addedToTopOfFeedBodyNum++;
  1663. }
  1664. }
  1665. }
  1666. // if this is a retweet
  1667. // (note the difference between "the repeat-notice" and "the repeated notice")
  1668. // but the unrepeat delete activity notices have retweeted_status added to them, so check this is not a delete notice
  1669. else if(typeof obj.retweeted_status != 'undefined'
  1670. && (typeof obj.qvitter_delete_notice == 'undefined' || obj.qvitter_delete_notice === false)) {
  1671. // if repeat-notice doesn't already exist in feed
  1672. if($('#stream-item-' + obj.id).length == 0) {
  1673. // if the this or the repeated notice already exist in feed, we add this, but hidden
  1674. if($('.stream-item[data-quitter-id="' + obj.retweeted_status.id + '"]').length > 0) {
  1675. extraClassesThisRun += ' hidden-repeat';
  1676. }
  1677. var queetHtml = buildQueetHtml(obj.retweeted_status, obj.id, extraClassesThisRun, obj);
  1678. if(after) {
  1679. $('#' + after).after(queetHtml);
  1680. }
  1681. else {
  1682. $('#feed-body').prepend(queetHtml);
  1683. addedToTopOfFeedBodyNum++;
  1684. }
  1685. }
  1686. }
  1687. // ordinary tweet
  1688. else {
  1689. // only if not already exist
  1690. if($('#stream-item-' + obj.id).length == 0) {
  1691. // sometimes the notice already exist but in the form of a repeat, because of
  1692. // a repeat reaching our server before the actual notice, or because of date settings
  1693. // on different servers, anyhow, hide this notice if a repeat of it already exist in
  1694. // the stream, using the hidden-repeat class (a little confusing maybe)
  1695. if($('.stream-item[data-quitter-id="' + obj.id + '"]').length > 0) {
  1696. extraClassesThisRun += ' hidden-repeat';
  1697. }
  1698. // remove any matching temp post
  1699. if(typeof obj.user != 'undefined'
  1700. && obj.user.id == window.loggedIn.id
  1701. && $('#feed-body > .temp-post').length > 0
  1702. && after === false) {
  1703. $('#feed-body > .temp-post').each(function (){
  1704. if($(this).children('.queet').find('.queet-text').text() == obj.text) {
  1705. $(this).remove();
  1706. extraClassesThisRun = 'visible';
  1707. }
  1708. });
  1709. }
  1710. var queetHtml = buildQueetHtml(obj, obj.id, extraClassesThisRun);
  1711. if(after) {
  1712. $('#' + after).after(queetHtml);
  1713. }
  1714. else {
  1715. $('#feed-body').prepend(queetHtml);
  1716. addedToTopOfFeedBodyNum++;
  1717. // if this is a single notice, we expand it
  1718. if(window.currentStreamObject.name == 'notice') {
  1719. expand_queet($('#stream-item-' + obj.id));
  1720. }
  1721. }
  1722. }
  1723. }
  1724. });
  1725. convertAttachmentMoreHref();
  1726. // if we've added stuff to the top of feed-body, we update our stream cache
  1727. if(addedToTopOfFeedBodyNum>0) {
  1728. rememberStreamStateInLocalStorage();
  1729. }
  1730. $('.stream-selection').removeAttr('data-current-user-stream-name'); // don't remeber user feeds
  1731. }
  1732. /* ·
  1733. ·
  1734. · Build HTML for a user stream item from an object
  1735. ·
  1736. · @param obj: a user object
  1737. ·
  1738. · · · · · · · · · · · · · */
  1739. function buildUserStreamItemHtml(obj) {
  1740. obj.description = obj.description || '';
  1741. // external
  1742. var ostatusHtml = '';
  1743. if(obj.is_local === false) {
  1744. ostatusHtml = '<a target="_blank" title="' + window.sL.goToTheUsersRemoteProfile + '" class="ostatus-link" href="' + obj.statusnet_profile_url + '" donthijack></a>';
  1745. }
  1746. // rtl or not
  1747. var rtlOrNot = '';
  1748. if($('body').hasClass('rtl')) {
  1749. rtlOrNot = 'rtl';
  1750. }
  1751. // following?
  1752. var followingClass = '';
  1753. if(obj.following) {
  1754. followingClass = ' following';
  1755. }
  1756. // blocking?
  1757. var blockingClass = '';
  1758. if(obj.statusnet_blocking) {
  1759. blockingClass = ' blocking';
  1760. }
  1761. // silenced?
  1762. var silencedClass = '';
  1763. if(obj.is_silenced === true) {
  1764. silencedClass = ' silenced';
  1765. }
  1766. // sandboxed?
  1767. var sandboxedClass = '';
  1768. if(obj.is_sandboxed === true) {
  1769. sandboxedClass = ' sandboxed';
  1770. }
  1771. // logged in?
  1772. var loggedInClass = '';
  1773. if(window.loggedIn !== false) {
  1774. loggedInClass = ' logged-in';
  1775. }
  1776. // muted?
  1777. var mutedClass = '';
  1778. if(isUserMuted(obj.id)) {
  1779. mutedClass = ' user-muted';
  1780. }
  1781. var followButton = '';
  1782. if(typeof window.loggedIn.screen_name != 'undefined' // if logged in
  1783. && window.loggedIn.id != obj.id) { // not if this is me
  1784. if(!(obj.statusnet_profile_url.indexOf('/twitter.com/')>-1 && obj.following === false)) { // only unfollow twitter users
  1785. var followButton = buildFollowBlockbutton(obj);
  1786. }
  1787. }
  1788. return '<div id="stream-item-' + obj.id + '" class="stream-item user' + silencedClass + sandboxedClass + mutedClass + '" data-user-id="' + obj.id + '">\
  1789. <div class="queet ' + rtlOrNot + '">\
  1790. ' + followButton + '\
  1791. <div class="user-menu-cog' + silencedClass + sandboxedClass + loggedInClass + '" data-tooltip="' + window.sL.userOptions + '" data-user-id="' + obj.id + '" data-screen-name="' + obj.screen_name + '"></div>\
  1792. <div class="queet-content">\
  1793. <div class="stream-item-header">\
  1794. <a class="account-group" href="' + obj.statusnet_profile_url + '" data-user-id="' + obj.id + '">\
  1795. <img class="avatar profile-size" src="' + obj.profile_image_url_profile_size + '" data-user-id="' + obj.id + '" />\
  1796. <strong class="name" data-user-id="' + obj.id + '">' + obj.name + '</strong>\
  1797. <span class="silenced-flag" data-tooltip="' + window.sL.silencedStreamDescription + '">' + window.sL.silenced + '</span> \
  1798. <span class="sandboxed-flag" data-tooltip="' + window.sL.sandboxedStreamDescription + '">' + window.sL.sandboxed + '</span> \
  1799. <span class="screen-name" data-user-id="' + obj.id + '">@' + obj.screen_name + '</span>\
  1800. </a>\
  1801. ' + ostatusHtml + '\
  1802. </div>\
  1803. <div class="queet-text">' + obj.description + '</div>\
  1804. </div>\
  1805. </div>\
  1806. </div>';
  1807. }
  1808. /* ·
  1809. ·
  1810. · Build HTML for a queet from an object
  1811. ·
  1812. · @param obj: a queet object
  1813. · @param requeeted_by: if this is a requeet
  1814. ·
  1815. · · · · · · · · · · · · · */
  1816. function buildQueetHtml(obj, idInStream, extraClasses, requeeted_by, isConversation) {
  1817. // if we've blocked this user, but it has slipped through anyway
  1818. var blockingTooltip = '';
  1819. if(typeof window.allBlocking != 'undefined') {
  1820. $.each(window.allBlocking,function(){
  1821. if(this == obj.user.id){
  1822. extraClasses += ' profile-blocked-by-me';
  1823. blockingTooltip = ' data-tooltip="' + window.sL.thisIsANoticeFromABlockedUser + '"';
  1824. return false; // break
  1825. }
  1826. });
  1827. }
  1828. // muted? (if class is not already added)
  1829. if(isUserMuted(obj.user.id) && extraClasses.indexOf(' user-muted') == -1) {
  1830. extraClasses += ' user-muted';
  1831. blockingTooltip = ' data-tooltip="' + window.sL.thisIsANoticeFromAMutedUser + '"';
  1832. }
  1833. // silenced?
  1834. if(obj.user.is_silenced === true) {
  1835. extraClasses += ' silenced';
  1836. }
  1837. // sandboxed?
  1838. if(obj.user.is_sandboxed === true) {
  1839. extraClasses += ' sandboxed';
  1840. }
  1841. // deleted?
  1842. if(typeof window.knownDeletedNotices[obj.uri] != 'undefined') {
  1843. extraClasses += ' deleted always-hidden';
  1844. }
  1845. // unrepeated?
  1846. if(typeof requeeted_by != 'undefined'
  1847. && requeeted_by !== false
  1848. && typeof window.knownDeletedNotices[requeeted_by.uri] != 'undefined') {
  1849. extraClasses += ' unrepeated always-hidden';
  1850. }
  1851. // activity? (hidden with css)
  1852. if(obj.source == 'activity' || obj.is_post_verb === false) {
  1853. extraClasses += ' activity always-hidden';
  1854. // 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
  1855. obj.statusnet_html = replaceHtmlSpecialChars(obj.statusnet_html);
  1856. }
  1857. // if we have the full html for a truncated notice cached in localstorage, we use that
  1858. var cacheData = localStorageObjectCache_GET('fullQueetHtml',obj.id);
  1859. if(cacheData) {
  1860. obj.statusnet_html = cacheData;
  1861. }
  1862. // we don't want to print 'null' in in_reply_to_screen_name-attribute, someone might have that username!
  1863. var in_reply_to_screen_name = '';
  1864. if(obj.in_reply_to_screen_name != null) {
  1865. in_reply_to_screen_name = obj.in_reply_to_screen_name;
  1866. }
  1867. // conversations has some slightly different id's
  1868. var idPrepend = '';
  1869. if(typeof isConversation != 'undefined' && isConversation === true) {
  1870. var idPrepend = 'conversation-';
  1871. extraClasses += ' conversation'
  1872. }
  1873. // is this mine?
  1874. var isThisMine = 'not-mine';
  1875. if(obj.user.id == window.loggedIn.id) {
  1876. var isThisMine = 'is-mine';
  1877. }
  1878. // requeeted by me?
  1879. var requeetedByMe = '';
  1880. if(obj.repeated_id) {
  1881. requeetedByMe = ' data-requeeted-by-me-id="' + obj.repeated_id + '" ';
  1882. }
  1883. // requeet html
  1884. if(obj.repeated) {
  1885. var requeetHtml = '<li class="action-rt-container"><a class="with-icn done"><span class="icon sm-rt" title="' + window.sL.requeetedVerb + '"></span></a></li>';
  1886. extraClasses += ' requeeted';
  1887. }
  1888. else {
  1889. var requeetHtml = '<li class="action-rt-container"><a class="with-icn"><span class="icon sm-rt ' + isThisMine + '" title="' + window.sL.requeetVerb + '"></span></a></li>';
  1890. }
  1891. // favorite html
  1892. if(obj.favorited) {
  1893. var favoriteHtml = '<a class="with-icn done"><span class="icon sm-fav" title="' + window.sL.favoritedVerb + '"></span></a>';
  1894. extraClasses += ' favorited';
  1895. }
  1896. else {
  1897. var favoriteHtml = '<a class="with-icn"><span class="icon sm-fav" title="' + window.sL.favoriteVerb + '"></span></a>';
  1898. }
  1899. // actions only for logged in users
  1900. var queetActions = '';
  1901. if(typeof window.loggedIn.screen_name != 'undefined') {
  1902. queetActions = '<ul class="queet-actions"><li class="action-reply-container"><a class="with-icn"><span class="icon sm-reply" title="' + window.sL.replyVerb + '"></span></a></li>' + requeetHtml + '<li class="action-rq-num" data-rq-num="' + obj.repeat_num + '">' + obj.repeat_num + '</li><li class="action-fav-container">' + favoriteHtml + '</li><li class="action-fav-num" data-fav-num="' + obj.fave_num + '">' + obj.fave_num + '</li><li class="action-ellipsis-container"><a class="with-icn"><span class="icon sm-ellipsis" title="' + window.sL.ellipsisMore + '"></span></a></li></ul>';
  1903. }
  1904. // reply-to html
  1905. var reply_to_html = '';
  1906. if(obj.in_reply_to_screen_name !== null
  1907. && obj.in_reply_to_profileurl !== null
  1908. && obj.in_reply_to_profileurl != obj.user.statusnet_profile_url) {
  1909. var replyToProfileurl = obj.in_reply_to_profileurl;
  1910. var replyToScreenName = obj.in_reply_to_screen_name;
  1911. }
  1912. // if we don't have a reply-to, we might have attentions, in that case use the first one as reply
  1913. else if(typeof obj.attentions != 'undefined' && typeof obj.attentions[0] != 'undefined') {
  1914. var replyToProfileurl = obj.attentions[0].profileurl;
  1915. var replyToScreenName = obj.attentions[0].screen_name;
  1916. }
  1917. if(typeof replyToProfileurl != 'undefined' && typeof replyToScreenName != 'undefined') {
  1918. // if the reply-to nickname doesn't exist in the notice, we add a class to the reply-to nickname in the header, to make the reply more visible
  1919. var mentionedInline = '';
  1920. if(obj.statusnet_html.indexOf('>' + replyToScreenName + '<') === -1) {
  1921. var mentionedInline = 'not-mentioned-inline';
  1922. }
  1923. reply_to_html = '<span class="reply-to"><a class="h-card mention ' + mentionedInline + '" href="' + replyToProfileurl + '">@' + replyToScreenName + '</a></span> ';
  1924. }
  1925. // in-groups html
  1926. var in_groups_html = '';
  1927. if(typeof obj.statusnet_in_groups != 'undefined' && obj.statusnet_in_groups !== false && typeof obj.statusnet_in_groups === 'object') {
  1928. $.each(obj.statusnet_in_groups,function(){
  1929. in_groups_html = in_groups_html + ' <span class="in-groups"><a class="h-card group" href="' + this.url + '">!' + this.nickname + '</a></span>';
  1930. });
  1931. }
  1932. // requeets get's a context element and a identifying class
  1933. // uri used is the repeate-notice's uri for repeats, not the repetED notice's uri (necessary if someone deletes a repeat)
  1934. var URItoUse = obj.uri;
  1935. var requeetHtml = '';
  1936. if(typeof requeeted_by != 'undefined' && requeeted_by !== false) {
  1937. var requeetedByHtml = '<a data-user-id="' + requeeted_by.user.id + '" href="' + requeeted_by.user.statusnet_profile_url + '"> <b>' + requeeted_by.user.name + '</b></a>';
  1938. requeetHtml = '<div class="context" id="requeet-' + requeeted_by.id + '"><span class="with-icn"><i class="badge-requeeted" data-tooltip="' + parseTwitterDate(requeeted_by.created_at) + '"></i><span class="requeet-text"> ' + window.sL.requeetedBy.replace('{requeeted-by}',requeetedByHtml) + '</span></span></div>';
  1939. var URItoUse = requeeted_by.uri;
  1940. extraClasses += ' is-requeet';
  1941. }
  1942. // the URI for delete activity notices are the same as the notice that is to be deleted
  1943. // so we make the URI for the (hidden) actitity notice unique, otherwise we might remove
  1944. // the activity notice from DOM when we remove the deleted notice
  1945. if(typeof obj.qvitter_delete_notice != 'undefined' && obj.qvitter_delete_notice == true) {
  1946. URItoUse += '-activity-notice';
  1947. }
  1948. // attachment html and attachment url's to hide
  1949. var attachmentBuild = buildAttachmentHTML(obj.attachments);
  1950. var statusnetHTML = $('<div/>').html(obj.statusnet_html);
  1951. $.each(statusnetHTML.find('a'),function(k,aElement){
  1952. $.each(attachmentBuild.urlsToHide,function(k,urlToHide){
  1953. var urlToHideWithoutProtocol = removeProtocolFromUrl(urlToHide);
  1954. if(urlToHideWithoutProtocol == removeProtocolFromUrl($(aElement).attr('href'))
  1955. || urlToHideWithoutProtocol == removeProtocolFromUrl($(aElement).text())) {
  1956. $(aElement).addClass('hidden-embedded-link-in-queet-text')
  1957. }
  1958. });
  1959. });
  1960. // if statusnetHTML is contains <p>:s, unwrap those (diaspora..)
  1961. /*statusnetHTML.children('p').each(function(){
  1962. $(this).contents().unwrap();
  1963. });*/
  1964. // bookmarks created by the bookmark plugin get's a tooltip
  1965. statusnetHTML.find('.xfolkentry').each(function(){
  1966. $(this).attr('data-tooltip',window.sL.thisIsABookmark);
  1967. });
  1968. // find a place in the queet-text for the quoted notices
  1969. statusnetHTML = placeQuotedNoticesInQueetText(attachmentBuild.quotedNotices, statusnetHTML);
  1970. statusnetHTML = statusnetHTML.html();
  1971. // remove trailing <br>s from queet text
  1972. while (statusnetHTML.slice(-4) == '<br>') {
  1973. statusnetHTML = statusnetHTML.slice(0,-4);
  1974. }
  1975. // external
  1976. var ostatusHtml = '';
  1977. if(obj.user.is_local === false) {
  1978. ostatusHtml = '<a target="_blank" data-tooltip="' + window.sL.goToOriginalNotice + '" class="ostatus-link" href="' + obj.external_url + '" donthijack></a>';
  1979. var qSource = '<a href="' + obj.external_url + '">' + getHost(obj.external_url) + '</a>';
  1980. }
  1981. else {
  1982. var qSource = obj.source;
  1983. }
  1984. var queetTime = parseTwitterDate(obj.created_at);
  1985. var queetHtml = '<div \
  1986. id="' + idPrepend + 'stream-item-' + idInStream + '" \
  1987. data-uri="' + URItoUse + '" \
  1988. class="stream-item notice ' + extraClasses + '" \
  1989. data-source="' + escape(qSource) + '" \
  1990. data-quitter-id="' + obj.id + '" \
  1991. data-conversation-id="' + obj.statusnet_conversation_id + '" \
  1992. data-quitter-id-in-stream="' + idInStream + '" \
  1993. data-in-reply-to-screen-name="' + in_reply_to_screen_name + '" \
  1994. data-in-reply-to-profile-url="' + obj.in_reply_to_profileurl + '" \
  1995. data-in-reply-to-profile-ostatus-uri="' + obj.in_reply_to_ostatus_uri + '" \
  1996. data-in-reply-to-status-id="' + obj.in_reply_to_status_id + '"\
  1997. data-user-id="' + obj.user.id + '"\
  1998. data-user-screen-name="' + obj.user.screen_name + '"\
  1999. data-user-ostatus-uri="' + obj.user.ostatus_uri + '"\
  2000. data-user-profile-url="' + obj.user.statusnet_profile_url + '"\
  2001. ' + requeetedByMe + '>\
  2002. <div class="queet" id="' + idPrepend + 'q-' + idInStream + '"' + blockingTooltip + '>\
  2003. <script class="attachment-json" type="application/json">' + JSON.stringify(obj.attachments) + '</script>\
  2004. <script class="attentions-json" type="application/json">' + JSON.stringify(obj.attentions) + '</script>\
  2005. ' + requeetHtml + '\
  2006. ' + ostatusHtml + '\
  2007. <div class="queet-content">\
  2008. <div class="stream-item-header">\
  2009. <a class="account-group" href="' + obj.user.statusnet_profile_url + '" data-user-id="' + obj.user.id + '">\
  2010. <img class="avatar profile-size" src="' + obj.user.profile_image_url_profile_size + '" data-user-id="' + obj.user.id + '" />\
  2011. <strong class="name" data-user-id="' + obj.user.id + '">' + obj.user.name + '</strong>\
  2012. <span class="silenced-flag" data-tooltip="' + window.sL.silencedStreamDescription + '">' + window.sL.silenced + '</span> \
  2013. <span class="sandboxed-flag" data-tooltip="' + window.sL.sandboxedStreamDescription + '">' + window.sL.sandboxed + '</span> \
  2014. <span class="screen-name" data-user-id="' + obj.user.id + '">@' + obj.user.screen_name + '</span>' +
  2015. '</a>' +
  2016. '<i class="addressees">' + reply_to_html + in_groups_html + '</i>' +
  2017. '<small class="created-at" data-created-at="' + obj.created_at + '">\
  2018. <a data-tooltip="' + parseTwitterLongDate(obj.created_at) + '" href="' + window.siteInstanceURL + 'notice/' + obj.id + '">' + queetTime + '</a>\
  2019. </small>\
  2020. </div>\
  2021. <div class="queet-text">' + $.trim(statusnetHTML) + '</div>\
  2022. ' + attachmentBuild.html + '\
  2023. <div class="stream-item-footer">\
  2024. ' + queetActions + '\
  2025. </div>\
  2026. </div>\
  2027. </div>\
  2028. </div>';
  2029. // detect rtl
  2030. queetHtml = detectRTL(queetHtml);
  2031. return queetHtml;
  2032. }
  2033. /* ·
  2034. ·
  2035. · Place or update quoted notices in the queet text
  2036. ·
  2037. · @param quotedNotices: object returned by buildAttachmentHTML()
  2038. · @param queetText: jQuery object for queet text
  2039. ·
  2040. · · · · · · · · · */
  2041. function placeQuotedNoticesInQueetText(quotedNotices,queetText) {
  2042. $.each(quotedNotices,function(k,qoutedNotice){
  2043. if(typeof qoutedNotice.url != 'undefined') {
  2044. var quoteLinkFound = queetText.find('a[href*="' + removeProtocolFromUrl(qoutedNotice.url) + '"]:not(.quote-link-container)');
  2045. // if we can't found it in a href, we might find it in data-quote-url attribute!
  2046. if(quoteLinkFound.length==0) {
  2047. quoteLinkFound = queetText.find('a[data-quote-url*="' + removeProtocolFromUrl(qoutedNotice.url) + '"]:not(.quote-link-container)');
  2048. }
  2049. if(quoteLinkFound.length>0) {
  2050. $.each(quoteLinkFound,function(){
  2051. // restore old style links (this can be removed later)
  2052. if($(this).children('.quoted-notice-header, .oembed-item-header').length>0) {
  2053. $(this).html($(this).attr('href'));
  2054. $(this).removeAttr('style');
  2055. $(this).removeClass('quoted-notice');
  2056. $(this).removeClass('oembed-item');
  2057. }
  2058. // place a container if we don't have it
  2059. if(!$(this).next().is('.quote-link-container')) {
  2060. $(this).after('<a class="quote-link-container"></a>');
  2061. }
  2062. // update the link and the (maybe newly added) quote container after the link
  2063. if($(this).next().is('.quote-link-container')) {
  2064. $(this).addClass('hidden-quote-link-in-queet-text');
  2065. $(this).attr('data-quote-url',qoutedNotice.url);
  2066. $(this).next().attr('data-quote-url',qoutedNotice.url);
  2067. $(this).next().addClass(qoutedNotice.class);
  2068. $(this).next().attr('href',qoutedNotice.href);
  2069. $(this).next().html(qoutedNotice.html);
  2070. // remove unnecessary line breaks, i.e. remove br between two quoted notices
  2071. if($(this).prev().is('br')) {
  2072. $(this).prev().remove();
  2073. }
  2074. if(!$(this).next().next().is('br')) {
  2075. $(this).next().after('<br>');
  2076. }
  2077. }
  2078. });
  2079. }
  2080. }
  2081. });
  2082. return queetText;
  2083. }
  2084. /* ·
  2085. ·
  2086. · Build HTML for the attachments to a queet
  2087. ·
  2088. · @param attachments: attachment object returned by the api
  2089. ·
  2090. · · · · · · · · · · · · · */
  2091. function buildAttachmentHTML(attachments){
  2092. var attachmentHTML = '';
  2093. var oembedHTML = '';
  2094. var quotedNotices = [];
  2095. var attachmentNum = 0;
  2096. var oembedNum = 0;
  2097. var urlsToHide = [];
  2098. if(typeof attachments != "undefined") {
  2099. $.each(attachments, function(){
  2100. // quoted notices
  2101. if(typeof this.quoted_notice != 'undefined') {
  2102. var quotedContent = this.quoted_notice.content;
  2103. // quoted notice's attachments' thumb urls
  2104. var quotedAttachmentsHTML = '';
  2105. var quotedAttachmentsHTMLbefore = '';
  2106. var quotedAttachmentsHTMLafter = '';
  2107. if(typeof this.quoted_notice.attachments != 'undefined' && this.quoted_notice.attachments.length > 0) {
  2108. quotedAttachmentsHTML += '<div class="quoted-notice-attachments quoted-notice-attachments-num-' + this.quoted_notice.attachments.length + '">'
  2109. $.each(this.quoted_notice.attachments,function(k,qAttach){
  2110. quotedAttachmentsHTML += '<div class="quoted-notice-img-container" style="background-image:url(\'' + qAttach.thumb_url + '\')"><img class="quoted-notice-img" src="' + qAttach.thumb_url + '" /></div>';
  2111. // remove attachment string from content
  2112. quotedContent = quotedContent.split(window.siteInstanceURL + 'attachment/' + qAttach.attachment_id).join('');
  2113. });
  2114. quotedAttachmentsHTML += '</div>';
  2115. // if there is only one attachment, it goes before, otherwise after
  2116. if(this.quoted_notice.attachments.length == 1) {
  2117. quotedAttachmentsHTMLbefore = quotedAttachmentsHTML;
  2118. }
  2119. else {
  2120. quotedAttachmentsHTMLafter = quotedAttachmentsHTML;
  2121. }
  2122. }
  2123. var quotedNoticeHTML = quotedAttachmentsHTMLbefore + '\
  2124. <div class="quoted-notice-header">\
  2125. <span class="quoted-notice-author-fullname">' + this.quoted_notice.fullname + '</span>\
  2126. <span class="quoted-notice-author-nickname">' + this.quoted_notice.nickname + '</span>\
  2127. </div>\
  2128. <div class="quoted-notice-body">' + $.trim(quotedContent) + '</div>\
  2129. ' + quotedAttachmentsHTMLafter;
  2130. quotedNotices.push({
  2131. url: this.url,
  2132. html: quotedNoticeHTML,
  2133. href: window.siteInstanceURL + 'notice/' + this.quoted_notice.id,
  2134. class:'quoted-notice'
  2135. });
  2136. }
  2137. // if we have Twitter oembed data, we add is as quotes
  2138. else if(typeof this.oembed != 'undefined'
  2139. && this.oembed !== false
  2140. && this.oembed.provider == 'Twitter') {
  2141. var twitterHTML = '<div class="oembed-item-header">\
  2142. <span class="oembed-item-title">' + this.oembed.author_name + '</span>\
  2143. <span class="oembed-username">' + this.oembed.title + '</span>\
  2144. </div>\
  2145. <div class="oembed-item-body">' + this.oembed.oembedHTML + '</div>\
  2146. <div class="oembed-item-footer">\
  2147. <span class="oembed-item-provider">' + this.oembed.provider + '</span>\
  2148. </div>';
  2149. quotedNotices.push({
  2150. url: this.url,
  2151. html: twitterHTML,
  2152. href: this.url,
  2153. class:'oembed-item'
  2154. });
  2155. }
  2156. // if we have other oembed data (but not for photos and youtube, we handle those later)
  2157. else if(typeof this.oembed != 'undefined'
  2158. && this.oembed !== false
  2159. && this.oembed.title !== null
  2160. && this.oembed.provider != 'YouTube'
  2161. && this.oembed.provider != 'Vimeo'
  2162. && this.oembed.type != 'photo') {
  2163. var oembedImage = '';
  2164. // only local images
  2165. if(typeof this.thumb_url != 'undefined'
  2166. && this.thumb_url !== null
  2167. && isLocalURL(this.thumb_url)) {
  2168. oembedImage = '<div class="oembed-img-container" style="background-image:url(\'' + this.thumb_url + '\')"><img class="oembed-img" src="' + this.thumb_url + '" /></div>';
  2169. }
  2170. var oembedBody = '';
  2171. // don't add body if html it's too similar (80%) to the title (wordpress does this..)
  2172. if(this.oembed.oembedHTML !== null
  2173. && this.oembed.oembedHTML.length > 0) {
  2174. if(this.oembed.oembedHTML.length > 200) {
  2175. this.oembed.oembedHTML = this.oembed.oembedHTML.substring(0,200) + '…';
  2176. }
  2177. if(stringSimilarity(this.oembed.oembedHTML,this.oembed.title.substring(0,200)) < 80) {
  2178. oembedBody = this.oembed.oembedHTML;
  2179. }
  2180. }
  2181. if(this.oembed.provider === null) {
  2182. var oembedProvider = this.url;
  2183. var oembedProviderURL = '';
  2184. }
  2185. else {
  2186. var oembedProvider = this.oembed.provider;
  2187. var oembedProviderURL = removeProtocolFromUrl(this.oembed.provider_url);
  2188. // remove trailing /
  2189. if(oembedProviderURL.slice(-1) == '/') {
  2190. oembedProviderURL = oembedProviderURL.slice(0,-1);
  2191. }
  2192. }
  2193. // If the oembed data is generated by Qvitter, we know a better way of showing the title
  2194. var oembedTitle = this.oembed.title;
  2195. var oembedTitleHTML = '<span class="oembed-item-title">' + oembedTitle + '</span>';
  2196. if(oembedTitle.slice(-10) == ' (Qvitter)') {
  2197. var oembedTimePosted = parseTwitterLongDate(oembedTitle.slice(0,-10));
  2198. var oembedGNUsocialUsername = this.oembed.author_name.substring(this.oembed.author_name.lastIndexOf('(')+1,this.oembed.author_name.lastIndexOf(')'));
  2199. var oembedGNUsocialFullname = this.oembed.author_name.slice(0,-(oembedGNUsocialUsername.length+3));
  2200. oembedTitleHTML = '<span class="oembed-item-title">' + oembedGNUsocialFullname + '</span>\
  2201. <span class="oembed-username">@' + oembedGNUsocialUsername + '</span>';
  2202. }
  2203. oembedHTML += '<a href="' + this.url + '" class="oembed-item">\
  2204. ' + oembedImage + '\
  2205. <div class="oembed-item-header">\
  2206. ' + oembedTitleHTML + '\
  2207. </div>\
  2208. <div class="oembed-item-body">' + oembedBody + '</div>\
  2209. <div class="oembed-item-footer">\
  2210. <span class="oembed-item-provider">' + oembedProvider + '</span>\
  2211. <span class="oembed-item-provider-url">' + oembedProviderURL + '</span>\
  2212. </div>\
  2213. </a>';
  2214. oembedNum++;
  2215. }
  2216. // if there's a local thumb_url we assume this is a image or video
  2217. else if(typeof this.thumb_url != 'undefined'
  2218. && this.thumb_url !== null
  2219. && isLocalURL(this.thumb_url)) {
  2220. var bigThumbW = 1000;
  2221. var bigThumbH = 3000;
  2222. if(bigThumbW > window.siteMaxThumbnailSize) {
  2223. bigThumbW = window.siteMaxThumbnailSize;
  2224. }
  2225. if(bigThumbH > window.siteMaxThumbnailSize) {
  2226. bigThumbH = window.siteMaxThumbnailSize;
  2227. }
  2228. // very long landscape images should not have background-size:cover
  2229. var noCoverClass='';
  2230. if(this.width/this.height > 2) {
  2231. noCoverClass=' no-cover';
  2232. }
  2233. // play button for videos and animated gifs
  2234. var playButtonClass = '';
  2235. if(typeof this.animated != 'undefined' && this.animated === true
  2236. || (this.url.indexOf('://www.youtube.com') > -1 || this.url.indexOf('://youtu.be') > -1)
  2237. || this.url.indexOf('://vimeo.com') > -1) {
  2238. playButtonClass = ' play-button';
  2239. }
  2240. // gif-class
  2241. var animatedGifClass = '';
  2242. if(typeof this.animated != 'undefined' && this.animated === true) {
  2243. var animatedGifClass = ' animated-gif';
  2244. }
  2245. // animated gifs always get default small non-animated thumbnail
  2246. if(this.animated === true) {
  2247. var img_url = this.thumb_url;
  2248. }
  2249. // if no dimensions are set, go with default thumb
  2250. else if(this.width === null && this.height === null) {
  2251. var img_url = this.thumb_url;
  2252. }
  2253. // large images get large thumbnail
  2254. else if(this.width > 1000) {
  2255. var img_url = this.large_thumb_url;
  2256. }
  2257. // no thumbnails for small local images
  2258. else if (this.url.indexOf(window.siteInstanceURL) === 0) {
  2259. var img_url = this.url;
  2260. }
  2261. // small thumbnail for small remote images
  2262. else {
  2263. var img_url = this.thumb_url;
  2264. }
  2265. var urlToHide = window.siteInstanceURL + 'attachment/' + this.id;
  2266. attachmentHTML += '<a data-local-attachment-url="' + urlToHide + '" style="background-image:url(\'' + img_url + '\')" class="thumb-container' + noCoverClass + playButtonClass + animatedGifClass + ' ' + CSSclassNameByHostFromURL(this.url) + '" href="' + this.url + '"><img class="attachment-thumb" data-mime-type="' + this.mimetype + '" src="' + img_url + '"/ data-width="' + this.width + '" data-height="' + this.height + '" data-full-image-url="' + this.url + '" data-thumb-url="' + img_url + '"></a>';
  2267. urlsToHide.push(urlToHide); // hide this attachment url from the queet text
  2268. attachmentNum++;
  2269. }
  2270. else if (this.mimetype == 'image/svg+xml') {
  2271. var urlToHide = window.siteInstanceURL + 'attachment/' + this.id;
  2272. attachmentHTML += '<a data-local-attachment-url="' + urlToHide + '" style="background-image:url(\'' + this.url + '\')" class="thumb-container" href="' + this.url + '"><img class="attachment-thumb" data-mime-type="' + this.mimetype + '" src="' + this.url + '"/></a>';
  2273. urlsToHide.push(urlToHide); // hide this attachment url from the queet text
  2274. attachmentNum++;
  2275. }
  2276. });
  2277. }
  2278. return { html: '<div class="oembed-data oembed-num-' + oembedNum + '">' + oembedHTML + '</div><div class="queet-thumbs thumb-num-' + attachmentNum + '">' + attachmentHTML + '</div>',
  2279. urlsToHide: urlsToHide,
  2280. quotedNotices: quotedNotices
  2281. };
  2282. }