tweets-timeline-bs.html 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360
  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 z-0">
  76. {% for tweet in tweets %}
  77. <li class="tweet d-flex flex-column mb-2 {% 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="d-flex flex-row">
  92. <p class="text-end pe-3" style="width: 60px; max-width: 60px; min-width: 60px">
  93. <i class="bi-repeat"></i>
  94. </p>
  95. <p class="flex-grow-1"><a class="moon-gray" href="{{ tweet.retweeted_by_url }}">{{ tweet.retweeted_by }} Retweeted</a></p>
  96. </div>
  97. {% endif %}
  98. <div class="d-flex flex-row">
  99. {% include "partial/timeline-tweet-bs.html" %}
  100. </div>
  101. <div class="d-flex flex-row">
  102. <div class="tweet-actions-box d-flex flex-row justify-content-lg-between border-bottom g-2 p-2 flex-grow-1 flex-wrap" style="margin-left: 60px"">
  103. {% if tweet.actions.view_replies %}
  104. <a class="btn btn-sm btn-secondary m-1" href="{{ url_for(tweet.actions.view_replies.route, **tweet.actions.view_replies.route_params) }}">
  105. <i class="bi-chat"></i>
  106. replies
  107. </a>
  108. {% endif %}
  109. {% if show_thread_controls and tweet.conversation_id %}
  110. {% with tweet=tweets[0] %}
  111. {% if tweet.actions.view_thread %}
  112. <a class="btn btn-sm btn-secondary m-1" href="{{ url_for(tweet.actions.view_thread.route, **tweet.actions.view_thread.route_params) }}">author thread</a>
  113. {% endif %}
  114. {% if tweet.actions.view_conversation %}
  115. <a class="btn btn-sm btn-secondary m-1" href="{{ url_for(tweet.actions.view_conversation.route, **tweet.actions.view_conversation.route_params) }}">full convo</a>
  116. {% endif %}
  117. {% endwith %}
  118. {% endif %}
  119. {% if tweet.actions.view_activity %}
  120. <a class="btn btn-sm btn-secondary m-1" href="{{ url_for(tweet.actions.view_activity.route, **tweet.actions.view_activity.route_params) }}">
  121. <i class="bi-graph-up"></i>
  122. activity
  123. </a>
  124. {% endif %}
  125. {% if tweet.actions.retweet %}
  126. <a class="btn btn-sm btn-secondary m-1" hx-post="{{ url_for(tweet.actions.retweet.route, **tweet.actions.retweet.route_params) }}">
  127. <i class="bi-repeat"></i>
  128. retweet
  129. </a>
  130. {% endif %}
  131. {% if tweet.actions.bookmark %}
  132. <a class="btn btn-sm btn-secondary m-1" hx-post="{{ url_for(tweet.actions.bookmark.route, **tweet.actions.bookmark.route_params) }}">
  133. <i class="bi-bookmark"></i>
  134. bookmark
  135. </a>
  136. {% if tweet.actions.delete_bookmark %}
  137. <a class="btn btn-sm btn-secondary m-1" hx-delete="{{ url_for(tweet.actions.delete_bookmark.route, **tweet.actions.delete_bookmark.route_params) }}">-</a>
  138. {% endif %}
  139. {% endif %}
  140. <a class="btn btn-sm btn-secondary m-1" class="tweet-action copy-formatted" href="javascript:copyTweetToClipboard('{{ tweet.id }}')">copy formatted</a>
  141. {% if notes_app_url %}
  142. <a class="btn btn-sm btn-secondary m-1" class="tweet-action swipe-to-note" href="javascript:swipeTweetToNotesApp('{{ tweet.id }}')">swipe to note</a>
  143. {% endif %}
  144. </div>
  145. </div>
  146. </li>
  147. {% endfor %}
  148. {% if query.next_data_url %}
  149. <li style="height: 50px; vertical-align: middle"
  150. hx-get="{{ query.next_data_url }}"
  151. hx-trigger="revealed"
  152. hx-swap="outerHTML"
  153. hx-select="ul#tweets > li"
  154. >
  155. <center style="height: 100%">
  156. <span class="js-only">
  157. Loading more tweets...
  158. </span>
  159. </center>
  160. </li>
  161. {% elif query.next_page_url %}
  162. <li style="height: 50px; vertical-align: middle"
  163. >
  164. <center style="height: 100%">
  165. <a href="{{ query.next_page_url }}">
  166. Go to Next Page
  167. </a>
  168. </center>
  169. </li>
  170. {% endif %}
  171. <li style="display: none">
  172. <script>
  173. // https://stackoverflow.com/questions/22663353/algorithm-to-remove-extreme-outliers-in-array
  174. // we should remove outliers on the X axis. That will mainly be RTs with old dates.
  175. // we might also be able to get the date of RT as opposed to OG tweet date.
  176. // https://towardsdatascience.com/ways-to-detect-and-remove-the-outliers-404d16608dba
  177. var profileDataEl = document.querySelector('#profile-data');
  178. if (window['dataset'] && profileDataEl) {
  179. profileDataEl.innerHTML = dataset.get().filter(i => 'public_metrics' in i).map(i => i.public_metrics.like_count).join(', ');
  180. }
  181. {% if visjs_enabled %}
  182. if (window.profileActivity) {
  183. window.profileActivity.fit()
  184. }
  185. {% endif %}
  186. </script>
  187. </li>
  188. </ul>
  189. {% if twitter_live_enabled and visjs_enabled and not skip_plot %}
  190. <script>
  191. function onClick (e) {
  192. // we need to scan the dataset between min/max x/y
  193. //
  194. // TODO we want to scale these based on the zoom level / pixel values
  195. //
  196. // FIXME sometimes we get several points:
  197. // We could also go for the closest point within the bound.
  198. // Perhaps cycle through upon multiple clicks.
  199. // For now we can just zoom in closer.
  200. //
  201. // range: graph2d.components[3].options.dataAxis.left.range.max
  202. // fixing this is lower priority since it is currently static.
  203. graph2d = window.profileActivity;
  204. var timeWindow = graph2d.getWindow();
  205. var windowInSeconds = (timeWindow.end - timeWindow.start) / 1000;
  206. var pixelWidth = graph2d.dom.centerContainer.offsetWidth;
  207. var secondsPerPixel = windowInSeconds / pixelWidth;
  208. console.log('secondsPerPixel = ' + secondsPerPixel);
  209. //var MAX_TIME_DIFF = 1000 * 60 * 60;
  210. var MAX_TIME_DIFF = 10 * secondsPerPixel * 1000;
  211. var MAX_VALUE_DIFF = 10;
  212. console.log(`click. value=${e.value[0]}, time=${e.time}`);
  213. console.log(e);
  214. var nearbyItems = dataset.get({filter: function (item) {
  215. var timeDiff = new Date(item.x).getTime() - e.time.getTime();
  216. var valueDiff = item.y - e.value[0];
  217. return Math.abs(timeDiff) < MAX_TIME_DIFF
  218. && Math.abs(valueDiff) < MAX_VALUE_DIFF;
  219. }});
  220. //console.log([e.time, e.value[0]]);
  221. console.log('nearby points:');
  222. console.log(nearbyItems);
  223. }
  224. var container = document.getElementById('visualization');
  225. var options = {
  226. sort: false,
  227. sampling:false,
  228. style:'points',
  229. dataAxis: {
  230. width: '88px',
  231. //visible: false,
  232. left: {
  233. range: {
  234. min: 0, max: 10
  235. }
  236. }
  237. },
  238. drawPoints: {
  239. enabled: true,
  240. size: 6,
  241. style: 'circle' // square, circle
  242. },
  243. defaultGroup: 'Scatterplot',
  244. graphHeight: '50px',
  245. width: '100%'
  246. };
  247. var groups = [{id: 'feed_item'}];
  248. window.profileActivity = new vis.Graph2d(container, window.dataset, groups, options);
  249. window.profileActivity.on('click', onClick);
  250. </script>
  251. {% endif %}