tweets-timeline.html 7.6 KB

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