1
0

ProfileFollowing.vue 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259
  1. <template>
  2. <div class="profile-following-component">
  3. <div class="row justify-content-center">
  4. <div class="col-12 col-md-7">
  5. <div v-if="isLoaded" class="d-flex justify-content-between align-items-center mb-4">
  6. <div>
  7. <button
  8. class="btn btn-outline-dark rounded-pill font-weight-bold"
  9. @click="goBack()">
  10. Back
  11. </button>
  12. </div>
  13. <div class="d-flex align-items-center justify-content-center flex-column w-100 overflow-hidden">
  14. <p class="small text-muted mb-0 text-uppercase font-weight-light cursor-pointer text-truncate text-center" style="width: 70%;" @click="goBack()">&commat;{{ profile.acct }}</p>
  15. <p class="lead font-weight-bold mt-n1 mb-0">{{ $t('profile.following') }}</p>
  16. </div>
  17. <div>
  18. <a class="btn btn-dark rounded-pill font-weight-bold spacer-btn" href="#">Back</a>
  19. </div>
  20. </div>
  21. <div v-if="isLoaded" class="list-group scroll-card">
  22. <div v-for="(account, idx) in feed" class="list-group-item">
  23. <a
  24. :id="'apop_'+account.id"
  25. :href="account.url"
  26. @click.prevent="goToProfile(account)"
  27. class="text-decoration-none">
  28. <div class="media">
  29. <img
  30. :src="account.avatar"
  31. width="40"
  32. height="40"
  33. style="border-radius: 8px;"
  34. class="mr-3 shadow-sm"
  35. draggable="false"
  36. loading="lazy"
  37. onerror="this.src='/storage/avatars/default.jpg?v=0';this.onerror=null;">
  38. <div class="media-body">
  39. <p class="mb-0 text-truncate">
  40. <span class="text-dark font-weight-bold text-decoration-none" v-html="getUsername(account)"></span>
  41. </p>
  42. <p class="mb-0 mt-n1 text-muted small text-break">&commat;{{ account.acct }}</p>
  43. </div>
  44. </div>
  45. </a>
  46. <b-popover :target="'apop_'+account.id" triggers="hover" placement="left" delay="1000" custom-class="shadow border-0 rounded-px">
  47. <profile-hover-card :profile="account" />
  48. </b-popover>
  49. </div>
  50. <div v-if="canLoadMore">
  51. <intersect @enter="enterIntersect">
  52. <placeholder />
  53. </intersect>
  54. </div>
  55. <div v-if="!canLoadMore && !feed.length">
  56. <div class="list-group-item text-center">
  57. <div v-if="isWarmingCache" class="px-4">
  58. <p class="mb-0 lead font-weight-bold">Loading Following...</p>
  59. <div class="py-3">
  60. <b-spinner variant="primary" style="width: 1.5rem; height: 1.5rem;" />
  61. </div>
  62. <p class="small text-muted mb-0">Please wait while we collect following accounts, this shouldn't take long!</p>
  63. </div>
  64. <p v-else class="mb-0 font-weight-bold">No following anyone yet!</p>
  65. </div>
  66. </div>
  67. </div>
  68. <div v-else class="list-group">
  69. <placeholder />
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </template>
  75. <script type="text/javascript">
  76. import Intersect from 'vue-intersect'
  77. import Placeholder from './../post/LikeListPlaceholder.vue';
  78. import ProfileHoverCard from './ProfileHoverCard.vue';
  79. import { mapGetters } from 'vuex';
  80. import { parseLinkHeader } from '@web3-storage/parse-link-header';
  81. export default {
  82. props: {
  83. profile: {
  84. type: Object
  85. }
  86. },
  87. components: {
  88. ProfileHoverCard,
  89. Intersect,
  90. Placeholder
  91. },
  92. computed: {
  93. ...mapGetters([
  94. 'getCustomEmoji'
  95. ])
  96. },
  97. data() {
  98. return {
  99. isLoaded: false,
  100. feed: [],
  101. cursor: null,
  102. canLoadMore: true,
  103. isFetchingMore: false,
  104. cacheWarmTimeout: undefined,
  105. cacheWarmInterations: 0,
  106. }
  107. },
  108. mounted() {
  109. this.fetchFollowers();
  110. },
  111. beforeDestroy() {
  112. clearTimeout(this.cacheWarmTimeout);
  113. },
  114. methods: {
  115. fetchFollowers() {
  116. axios.get('/api/v1/accounts/'+this.profile.id+'/following', {
  117. params: {
  118. cursor: this.cursor,
  119. '_pe': 1
  120. }
  121. }).then(res => {
  122. if(!res.data.length) {
  123. this.canLoadMore = false;
  124. this.isLoaded = true;
  125. if(this.cursor == null && this.profile.following_count) {
  126. this.isWarmingCache = true;
  127. this.setCacheWarmTimeout();
  128. }
  129. return;
  130. }
  131. if(res.headers && res.headers.link) {
  132. const links = parseLinkHeader(res.headers.link);
  133. if(links.prev) {
  134. this.cursor = links.prev.cursor;
  135. this.canLoadMore = true;
  136. } else {
  137. this.canLoadMore = false;
  138. }
  139. } else {
  140. this.canLoadMore = false;
  141. }
  142. this.feed.push(...res.data);
  143. this.isLoaded = true;
  144. this.isFetchingMore = false;
  145. if(this.isWarmingCache || this.cacheWarmTimeout) {
  146. this.isWarmingCache = false;
  147. clearTimeout(this.cacheWarmTimeout);
  148. this.cacheWarmTimeout = undefined;
  149. }
  150. })
  151. .catch(err => {
  152. this.canLoadMore = false;
  153. this.isLoaded = true;
  154. this.isFetchingMore = false;
  155. })
  156. },
  157. enterIntersect() {
  158. if(this.isFetchingMore) {
  159. return;
  160. }
  161. this.isFetchingMore = true;
  162. this.fetchFollowers();
  163. },
  164. getUsername(profile) {
  165. let self = this;
  166. let dn = profile.display_name;
  167. if(!dn || !dn.trim().length) {
  168. return profile.username;
  169. }
  170. if(dn.includes(':')) {
  171. let re = /(<a?)?:\w+:(\d{18}>)?/g;
  172. let un = dn.replaceAll(re, function(em) {
  173. let shortcode = em.slice(1, em.length - 1);
  174. let emoji = self.getCustomEmoji.filter(e => {
  175. return e.shortcode == shortcode;
  176. });
  177. return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
  178. });
  179. return un;
  180. } else {
  181. return dn;
  182. }
  183. },
  184. goToProfile(account) {
  185. this.$router.push({
  186. path: `/i/web/profile/${account.id}`,
  187. params: {
  188. id: account.id,
  189. cachedProfile: account,
  190. cachedUser: this.profile
  191. }
  192. })
  193. },
  194. goBack() {
  195. this.$emit('back');
  196. },
  197. setCacheWarmTimeout() {
  198. if(this.cacheWarmInterations >= 5) {
  199. this.isWarmingCache = false;
  200. swal('Oops', 'Its taking longer than expected to collect following accounts. Please try again later', 'error');
  201. return;
  202. }
  203. this.cacheWarmTimeout = setTimeout(() => {
  204. this.cacheWarmInterations++;
  205. this.fetchFollowers();
  206. }, 45000);
  207. }
  208. }
  209. }
  210. </script>
  211. <style lang="scss">
  212. .profile-following-component {
  213. .list-group-item {
  214. border: none;
  215. &:not(:last-child) {
  216. border-bottom: 1px solid rgba(0, 0, 0, 0.125);
  217. }
  218. }
  219. .scroll-card {
  220. max-height: calc(100vh - 250px);
  221. overflow-y: auto;
  222. -ms-overflow-style: none;
  223. scrollbar-width: none;
  224. scroll-behavior: smooth;
  225. &::-webkit-scrollbar {
  226. display: none;
  227. }
  228. }
  229. .spacer-btn {
  230. opacity: 0;
  231. pointer-events: none;
  232. }
  233. }
  234. </style>