tweets-timeline.html 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <script src="{{ url_for('static', filename='tweets-ui.js') }}"></script>
  2. <script>
  3. {% if notes_app_url %}
  4. var notesAppUrl = {{ notes_app_url | tojson }}
  5. {% endif %}
  6. if (!window['dataset']) {
  7. {% if visjs_enabled %}
  8. window.dataset = new vis.DataSet();
  9. {% else %}
  10. window.dataset = {
  11. items: [],
  12. update: function (items) {
  13. dataset.items = dataset.items.concat(items);
  14. },
  15. get: function () {
  16. return items;
  17. }
  18. }
  19. {% endif %}
  20. }
  21. </script>
  22. {% if twitter_live_enabled and visjs_enabled %}
  23. <div class="w-100" style="position: sticky; top: 20px; background-color: silver; padding: 20px 0; margin: 10px 0;">
  24. <div id="visualization"></div>
  25. </div>
  26. {% endif %}
  27. <ul id="tweets" class="tweets w-75 center z-0">
  28. {% for tweet in tweets %}
  29. <li class="tweet w-100 dt {% if tweet.is_marked %}marked{% endif %}">
  30. <script>
  31. function feed_item_to_activity (fi) {
  32. var group = 'tweet';
  33. var y = 1;
  34. if (fi.retweeted_tweet_id) {
  35. group = 'retweet';
  36. y = 2;
  37. } else if (fi.replied_tweet_id) {
  38. group = 'reply';
  39. y = 3;
  40. }
  41. return {
  42. //'id': fi.id,
  43. 'x': new Date(fi.created_at),
  44. 'y': y,
  45. 'group': group,
  46. 'feed_item': fi
  47. }
  48. }
  49. function feed_item_to_likes (fi) {
  50. if ( !fi['public_metrics'] || !fi.public_metrics['like_count'] ) {
  51. return;
  52. }
  53. var group = 'likes';
  54. var y = fi.public_metrics.like_count;
  55. return {
  56. //'id': fi.id,
  57. 'x': new Date(fi.created_at),
  58. 'y': y,
  59. 'group': group,
  60. 'feed_item': fi
  61. }
  62. }
  63. function feed_item_to_replies (fi) {
  64. if ( !fi['public_metrics'] || !fi.public_metrics['reply_count'] ) {
  65. return;
  66. }
  67. var group = 'replies';
  68. var y = fi.public_metrics.reply_count;
  69. return {
  70. //'id': fi.id,
  71. 'x': new Date(fi.created_at),
  72. 'y': y,
  73. 'group': group,
  74. 'feed_item': fi
  75. }
  76. }
  77. var feedItem = {{ tweet | tojson }};
  78. var plotItems = [];
  79. var likesPoint = feed_item_to_likes(feedItem);
  80. if (likesPoint) { plotItems.push(likesPoint); }
  81. var repliesPoint = feed_item_to_replies(feedItem);
  82. if (repliesPoint) { plotItems.push(repliesPoint); }
  83. if (!plotItems.length) {
  84. plotItems.push(feed_item_to_activity(feedItem))
  85. }
  86. dataset.update(plotItems);
  87. </script>
  88. {% if tweet.retweeted_by %}
  89. <div class="dt-row moon-gray">
  90. <p class="dtc w-10 tr pa1">RT</p>
  91. <p class="dtc w-90"><a class="moon-gray" href="{{ tweet.retweeted_by_url }}">{{ tweet.retweeted_by }} Retweeted</a></p>
  92. </div>
  93. {% endif %}
  94. <div class="dt-row">
  95. {% include "partial/timeline-tweet.html" %}
  96. </div>
  97. <div class="dt-row">
  98. <div class="dtc"></div>
  99. <div class="dtc ">
  100. <p class="tweet-actions-box">
  101. {% if tweet.actions.view_replies %}
  102. <a href="{{ url_for(tweet.actions.view_replies.route, **tweet.actions.view_replies.route_params) }}">replies</a>
  103. |
  104. {% endif %}
  105. {% if show_thread_controls and tweet.conversation_id %}
  106. {% with tweet=tweets[0] %}
  107. {% if tweet.actions.view_thread %}
  108. <a href="{{ url_for(tweet.actions.view_thread.route, **tweet.actions.view_thread.route_params) }}">author thread</a>
  109. |
  110. {% endif %}
  111. {% if tweet.actions.view_conversation %}
  112. <a href="{{ url_for(tweet.actions.view_conversation.route, **tweet.actions.view_conversation.route_params) }}">full convo</a>
  113. |
  114. {% endif %}
  115. {% endwith %}
  116. {% endif %}
  117. {% if tweet.actions.view_activity %}
  118. <a href="{{ url_for(tweet.actions.view_activity.route, **tweet.actions.view_activity.route_params) }}">activity</a>
  119. |
  120. {% endif %}
  121. {% if tweet.actions.retweet %}
  122. <a hx-post="{{ url_for(tweet.actions.retweet.route, **tweet.actions.retweet.route_params) }}">retweet</a>
  123. |
  124. {% endif %}
  125. {% if tweet.actions.bookmark %}
  126. <a hx-post="{{ url_for(tweet.actions.bookmark.route, **tweet.actions.bookmark.route_params) }}">bookmark</a>
  127. {% if tweet.actions.delete_bookmark %}
  128. [
  129. <a hx-delete="{{ url_for(tweet.actions.delete_bookmark.route, **tweet.actions.delete_bookmark.route_params) }}">-</a>
  130. ]
  131. {% endif %}
  132. |
  133. {% endif %}
  134. <a class="tweet-action copy-formatted" href="javascript:copyTweetToClipboard('{{ tweet.id }}')">copy formatted</a>
  135. {% if notes_app_url %}
  136. |
  137. <a class="tweet-action swipe-to-note" href="javascript:swipeTweetToNotesApp('{{ tweet.id }}')">swipe to note</a>
  138. {% endif %}
  139. </p>
  140. </div>
  141. </div>
  142. </li>
  143. {% endfor %}
  144. {% if query.next_data_url %}
  145. <li style="height: 50px; vertical-align: middle"
  146. hx-get="{{ query.next_data_url }}"
  147. hx-trigger="revealed"
  148. hx-swap="outerHTML"
  149. hx-select="ul#tweets > li"
  150. >
  151. <center style="height: 100%">
  152. <span class="js-only">
  153. Loading more tweets...
  154. </span>
  155. </center>
  156. </li>
  157. {% elif query.next_page_url %}
  158. <li style="height: 50px; vertical-align: middle"
  159. >
  160. <center style="height: 100%">
  161. <a href="{{ query.next_page_url }}">
  162. Go to Next Page
  163. </a>
  164. </center>
  165. </li>
  166. {% endif %}
  167. <li style="display: none">
  168. <script>
  169. // https://stackoverflow.com/questions/22663353/algorithm-to-remove-extreme-outliers-in-array
  170. // we should remove outliers on the X axis. That will mainly be RTs with old dates.
  171. // we might also be able to get the date of RT as opposed to OG tweet date.
  172. // https://towardsdatascience.com/ways-to-detect-and-remove-the-outliers-404d16608dba
  173. var profileDataEl = document.querySelector('#profile-data');
  174. if (window['dataset'] && profileDataEl) {
  175. profileDataEl.innerHTML = dataset.get().filter(i => 'public_metrics' in i).map(i => i.public_metrics.like_count).join(', ');
  176. }
  177. {% if visjs_enabled %}
  178. if (window.profileActivity) {
  179. window.profileActivity.fit()
  180. }
  181. {% endif %}
  182. </script>
  183. </li>
  184. </ul>
  185. {% if twitter_live_enabled and visjs_enabled %}
  186. <script>
  187. function onClick (e) {
  188. // we need to scan the dataset between min/max x/y
  189. //
  190. // TODO we want to scale these based on the zoom level / pixel values
  191. //
  192. // FIXME sometimes we get several points:
  193. // We could also go for the closest point within the bound.
  194. // Perhaps cycle through upon multiple clicks.
  195. // For now we can just zoom in closer.
  196. //
  197. // range: graph2d.components[3].options.dataAxis.left.range.max
  198. // fixing this is lower priority since it is currently static.
  199. graph2d = window.profileActivity;
  200. var timeWindow = graph2d.getWindow();
  201. var windowInSeconds = (timeWindow.end - timeWindow.start) / 1000;
  202. var pixelWidth = graph2d.dom.centerContainer.offsetWidth;
  203. var secondsPerPixel = windowInSeconds / pixelWidth;
  204. console.log('secondsPerPixel = ' + secondsPerPixel);
  205. //var MAX_TIME_DIFF = 1000 * 60 * 60;
  206. var MAX_TIME_DIFF = 10 * secondsPerPixel * 1000;
  207. var MAX_VALUE_DIFF = 10;
  208. console.log(`click. value=${e.value[0]}, time=${e.time}`);
  209. console.log(e);
  210. var nearbyItems = dataset.get({filter: function (item) {
  211. var timeDiff = new Date(item.x).getTime() - e.time.getTime();
  212. var valueDiff = item.y - e.value[0];
  213. return Math.abs(timeDiff) < MAX_TIME_DIFF
  214. && Math.abs(valueDiff) < MAX_VALUE_DIFF;
  215. }});
  216. //console.log([e.time, e.value[0]]);
  217. console.log('nearby points:');
  218. console.log(nearbyItems);
  219. }
  220. var container = document.getElementById('visualization');
  221. var options = {
  222. sort: false,
  223. sampling:false,
  224. style:'points',
  225. dataAxis: {
  226. width: '88px',
  227. //visible: false,
  228. left: {
  229. range: {
  230. min: 0, max: 10
  231. }
  232. }
  233. },
  234. drawPoints: {
  235. enabled: true,
  236. size: 6,
  237. style: 'circle' // square, circle
  238. },
  239. defaultGroup: 'Scatterplot',
  240. graphHeight: '50px',
  241. width: '100%'
  242. };
  243. var groups = [{id: 'feed_item'}];
  244. window.profileActivity = new vis.Graph2d(container, window.dataset, groups, options);
  245. window.profileActivity.on('click', onClick);
  246. </script>
  247. {% endif %}