First Impressions

Help you keep track of new people.

  1. // ==UserScript==
  2. // @name First Impressions
  3. // @namespace https://ejew.in/
  4. // @version 2024-10-21-2
  5. // @description Help you keep track of new people.
  6. // @author EntranceJew
  7. // @match https://bsky.app/*
  8. // @icon https://www.google.com/s2/favicons?sz=64&domain=bsky.app
  9. // @grant GM_getValue
  10. // @grant GM_setValue
  11. // @grant GM_addStyle
  12. // @run-at document-idle
  13. // @license MIT
  14. // ==/UserScript==
  15.  
  16. // thanks to:
  17. // Starburst by Julynn B. from <a href="https://thenounproject.com/browse/icons/term/starburst/" target="_blank" title="Starburst Icons">Noun Project</a> (CC BY 3.0)
  18.  
  19. (function() {
  20. 'use strict';
  21. let size = '0.25em';
  22. GM_addStyle(`
  23. .new-skeeter {
  24. }
  25.  
  26. .new-skeeter::after {
  27. content: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' fill='gold' viewBox='0 0 100 100'%3E%3Cpath d='M57.8 99c1.6 1.7 4 1.1 4.8-1l4.2-11.6c-.2.6-.9.9-1.4.7l11.3 4.6c2.1.9 4.1-.7 4-2.9l-.8-12.3c0 .7-.4 1.2-1.1 1.2l12.2-.5c2.2-.1 3.6-2.3 2.4-4.3L87.8 62c.3.6.1 1.2-.4 1.6l11-5.4c2-1 2.2-3.6.6-4.9l-9.7-7.7c.4.4.6 1.1.2 1.7l7.9-9.4c1.4-1.8.7-4.1-1.6-4.7l-11.9-3c.6.1 1 .8.8 1.4l3.3-12c.6-2.1-1.1-4.1-3.3-3.7l-12.1 1.9c.7-.1 1.2.3 1.3 1L72.1 6.6c-.3-2.2-2.7-3.2-4.4-2l-10.2 6.9c.6-.3 1.2-.2 1.6.3L52.6 1.4c-1.2-1.9-3.8-1.9-4.9 0l-6.6 10.5c.3-.6 1-.7 1.6-.3l-10.3-7c-1.9-1.2-4.2-.2-4.4 2l-1.8 12.2c.1-.7.7-1.1 1.3-1l-12.1-2.1c-2.2-.3-3.9 1.6-3.3 3.7l3.3 11.9c-.2-.6.2-1.2.8-1.4L4.3 33c-2.2.6-3 3-1.6 4.7l7.9 9.4c-.4-.4-.3-1.2.2-1.7l-9.7 7.7C-.6 54.6-.3 57 1.7 58l11 5.4c-.6-.3-.8-1-.4-1.6l-5.7 11c-1.2 2.1.1 4.3 2.3 4.4l12.2.4c-.7 0-1.1-.6-1.1-1.2l-.8 12.3c-.1 2.2 1.9 3.8 4 2.9L34.5 87c-.6.2-1.2-.1-1.4-.7L37.4 98c.8 2.1 3.2 2.7 4.8 1l8.6-8.8c-.4.4-1.2.4-1.7 0l8.7 8.8zM47.7 87.1l-8.6 8.8c.7-.8 2.1-.4 2.4.6l-4.2-11.6c-.7-1.8-2.6-2.7-4.3-1.9l-11.3 4.6c.9-.3 2.1.4 2.1 1.6l.8-12.3c.1-1.9-1.3-3.4-3.1-3.6l-12.3-.5c1 0 1.8 1.3 1.2 2.2l5.7-10.9c.9-1.7.2-3.7-1.4-4.6l-11-5.4c.9.4 1.1 1.9.2 2.6l9.7-7.7c1.4-1.1 1.7-3.3.4-4.7l-7.8-9.4c.7.8.2 2.2-.8 2.4l11.9-3c1.8-.4 2.9-2.3 2.3-4.1l-3.3-11.9c.2 1-.7 2.1-1.7 1.9l12.1 2.1c1.8.3 3.6-.9 3.8-2.8l1.8-12.2c-.1 1-1.4 1.6-2.3 1l10.2 6.9c1.6 1 3.7.6 4.7-1l6.4-10.5c-.6.9-2 .9-2.6 0l6.6 10.5c1 1.6 3.1 2 4.7 1l10.2-6.9c-.9.6-2.2 0-2.3-1l1.8 12.2c.2 1.9 2 3.1 3.8 2.8l12.1-2.1c-1 .2-2-.9-1.7-1.9l-3.3 11.9c-.6 1.8.6 3.7 2.3 4.1l11.9 3c-1-.2-1.4-1.7-.8-2.4l-7.9 9.4c-1.2 1.4-1 3.6.4 4.7l9.7 7.7c-.8-.7-.7-2.1.2-2.6l-11 5.4c-1.7.8-2.3 2.9-1.4 4.6L89.7 75c-.4-.9.2-2.2 1.2-2.2l-12.2.4c-1.9.1-3.3 1.7-3.1 3.6l.8 12.3c-.1-1 1.1-1.9 2.1-1.6L67 83c-1.8-.7-3.7.2-4.3 1.9l-4.2 11.6c.3-1 1.8-1.2 2.4-.6l-8.4-8.8c-1.4-1.3-3.5-1.3-4.8 0z'/%3E%3C/svg%3E");
  28. position: absolute;
  29. border-width: ${size};
  30. left: -${size};
  31. top: -${size};
  32. height: calc(100% + ${size} + ${size});
  33. width: calc(100% + ${size} + ${size});
  34. z-index: -1;
  35. animation: rotate-center 30s linear infinite both;
  36. }
  37.  
  38. @keyframes rotate-center {
  39. 0% {
  40. transform: rotate(0) scale(1);
  41. }
  42. 25% {
  43. transform: rotate(90deg) scale(1.1);
  44. }
  45. 50% {
  46. transform: rotate(180deg) scale(1);
  47. }
  48. 75% {
  49. transform: rotate(270deg) scale(1.1);
  50. }
  51. 100% {
  52. transform: rotate(360deg) scale(1);
  53. }
  54. }
  55. `);
  56.  
  57. function isElementInViewport(el) {
  58. let rect = el.getBoundingClientRect();
  59.  
  60. return (
  61. rect.top >= 0 &&
  62. rect.left >= 0 &&
  63. rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
  64. rect.right <= (window.innerWidth || document.documentElement.clientWidth)
  65. );
  66. }
  67.  
  68. function isElementVisible(el) {
  69. return el.checkVisibility({
  70. contentVisibilityAuto: true,
  71. opacityProperty: true,
  72. visibilityProperty: true,
  73. }) && isElementInViewport(el);
  74. }
  75.  
  76. //:not(:has(div[aria-label^="Post by"]))
  77.  
  78. function getSkeetLikeThings() {
  79. let skeet_likes = document.querySelectorAll('[data-testid*="-by-"]');
  80. return skeet_likes;
  81. }
  82.  
  83. function getSkeetLikeThingAvatar(skeetLikeThing){
  84. return skeetLikeThing.querySelector('a[aria-label$="\'s avatar"]');
  85. }
  86.  
  87. function getSkeetLikeThingHandle(skeetLikeThing){
  88. let elm = skeetLikeThing.parentElement.querySelector('[data-testid*="-by-"]');
  89.  
  90. if(elm && elm.dataset && elm.dataset.testid){
  91. return elm.dataset.testid.replace(/^.*-by-/,"");
  92. }
  93. }
  94.  
  95. function skeetAvatarSweeper(){
  96. let visible_skeets = [...getSkeetLikeThings()].filter(n => isElementVisible(getSkeetLikeThingAvatar(n)));
  97. let seen_skeeters = GM_getValue("seen_skeeters", []);
  98. visible_skeets.forEach((skeetLikeThing) => {
  99. let handle = getSkeetLikeThingHandle(skeetLikeThing);
  100. let avatar = getSkeetLikeThingAvatar(skeetLikeThing);
  101. if( handle && !seen_skeeters.includes(handle) ){
  102. if( !avatar.classList.contains('new-skeeter') ){
  103. avatar.classList.add('new-skeeter');
  104. seen_skeeters.push(handle);
  105. }
  106. }
  107. });
  108. GM_setValue("seen_skeeters", seen_skeeters);
  109. }
  110.  
  111. setInterval(skeetAvatarSweeper, 1000);
  112. })();