Hashtag.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359
  1. <template>
  2. <div class="hashtag-component">
  3. <div class="container-fluid mt-3">
  4. <div class="row">
  5. <div class="col-md-3 d-md-block">
  6. <sidebar :user="profile" />
  7. </div>
  8. <div class="col-md-9">
  9. <div class="card border-0 shadow-sm mb-3" style="border-radius: 18px;">
  10. <div class="card-body">
  11. <div class="media align-items-center py-3">
  12. <div class="media-body">
  13. <p class="h3 text-break mb-0">
  14. <span class="text-lighter">#</span>{{ hashtag.name }}
  15. </p>
  16. <p v-if="hashtag.count && hashtag.count > 100" class="mb-0 text-muted font-weight-bold">
  17. {{ formatCount(hashtag.count) }} Posts
  18. </p>
  19. </div>
  20. <template v-if="hashtag && hashtag.hasOwnProperty('following') && feed && feed.length">
  21. <button
  22. v-if="hashtag.following"
  23. :disabled="followingLoading"
  24. class="btn btn-light hashtag-follow border rounded-pill font-weight-bold py-1 px-4"
  25. @click="unfollowHashtag()"
  26. >
  27. <b-spinner v-if="followingLoading" small />
  28. <span v-else>
  29. {{ $t('profile.unfollow') }}
  30. </span>
  31. </button>
  32. <button
  33. v-else
  34. :disabled="followingLoading"
  35. class="btn btn-primary hashtag-follow font-weight-bold rounded-pill py-1 px-4"
  36. @click="followHashtag()">
  37. <b-spinner v-if="followingLoading" small />
  38. <span v-else>
  39. {{ $t('profile.follow') }}
  40. </span>
  41. </button>
  42. </template>
  43. </div>
  44. </div>
  45. </div>
  46. <template v-if="isLoaded && feedLoaded">
  47. <div class="row mx-0 hashtag-feed">
  48. <div class="col-6 col-md-4 col-lg-3 p-1" v-for="(status, index) in feed" :key="'tlob:'+index">
  49. <a
  50. class="card info-overlay card-md-border-0"
  51. :href="statusUrl(status)"
  52. @click.prevent="goToPost(status)">
  53. <div class="square">
  54. <div v-if="status.sensitive" class="square-content">
  55. <div class="info-overlay-text-label">
  56. <h5 class="text-white m-auto font-weight-bold">
  57. <span v-if="status.hasOwnProperty('filtered')">
  58. <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
  59. <span>Filtered</span>
  60. </span>
  61. <span v-else>
  62. <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
  63. </span>
  64. </h5>
  65. </div>
  66. <blur-hash-canvas
  67. width="32"
  68. height="32"
  69. :hash="status.media_attachments[0].blurhash"
  70. />
  71. </div>
  72. <div v-else class="square-content">
  73. <blur-hash-image
  74. width="32"
  75. height="32"
  76. :hash="status.media_attachments[0].blurhash"
  77. :src="getMediaSource(status)"
  78. />
  79. </div>
  80. <span v-if="status.pf_type == 'photo:album'" class="float-right mr-3 post-icon"><i class="fas fa-images fa-2x"></i></span>
  81. <span v-if="status.pf_type == 'video'" class="float-right mr-3 post-icon"><i class="fas fa-video fa-2x"></i></span>
  82. <span v-if="status.pf_type == 'video:album'" class="float-right mr-3 post-icon"><i class="fas fa-film fa-2x"></i></span>
  83. <div class="info-overlay-text">
  84. <h5 class="text-white m-auto font-weight-bold d-flex flex-column" style="gap:.5rem;">
  85. <span>
  86. <span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
  87. <span class="d-flex-inline">{{formatCount(status.favourites_count)}}</span>
  88. </span>
  89. <span>
  90. <span class="far fa-retweet fa-lg p-2 d-flex-inline"></span>
  91. <span class="d-flex-inline">{{formatCount(status.reblogs_count)}}</span>
  92. </span>
  93. <span>
  94. <span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
  95. <span class="d-flex-inline">{{formatCount(status.reply_count)}}</span>
  96. </span>
  97. </h5>
  98. </div>
  99. </div>
  100. </a>
  101. </div>
  102. <div v-if="canLoadMore" class="col-12">
  103. <intersect @enter="enterIntersect">
  104. <div class="d-flex justify-content-center py-5">
  105. <b-spinner />
  106. </div>
  107. </intersect>
  108. </div>
  109. </div>
  110. <div v-if="feedLoaded && !feed.length" class="row mx-0 hashtag-feed justify-content-center">
  111. <div class="col-12 col-md-8 text-center">
  112. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;max-width:400px">
  113. <p class="lead text-muted font-weight-bold">{{ $t('hashtags.emptyFeed') }}</p>
  114. </div>
  115. </div>
  116. </template>
  117. <template v-else>
  118. <div class="row justify-content-center align-items-center pt-5 mt-5">
  119. <b-spinner />
  120. </div>
  121. </template>
  122. </div>
  123. </div>
  124. <drawer />
  125. </div>
  126. </div>
  127. </template>
  128. <script type="text/javascript">
  129. import Drawer from './partials/drawer.vue';
  130. import Intersect from 'vue-intersect'
  131. import Sidebar from './partials/sidebar.vue';
  132. import Rightbar from './partials/rightbar.vue';
  133. export default {
  134. props: {
  135. id: {
  136. type: String
  137. }
  138. },
  139. components: {
  140. "drawer": Drawer,
  141. "intersect": Intersect,
  142. "sidebar": Sidebar,
  143. "rightbar": Rightbar,
  144. },
  145. data() {
  146. return {
  147. isLoaded: false,
  148. profile: undefined,
  149. canLoadMore: false,
  150. isIntersecting: false,
  151. feedLoaded: false,
  152. feed: [],
  153. page: 1,
  154. hashtag: {
  155. name: this.id,
  156. count: 0
  157. },
  158. followingLoading: false,
  159. maxId: undefined,
  160. };
  161. },
  162. mounted() {
  163. this.init();
  164. },
  165. watch: {
  166. '$route': 'init'
  167. },
  168. methods: {
  169. init() {
  170. this.profile = window._sharedData.user;
  171. axios.get('/api/v1/tags/' + this.id, {
  172. params: {
  173. '_pe': 1
  174. }
  175. })
  176. .then(res => {
  177. this.hashtag = res.data;
  178. })
  179. .catch(err => {
  180. swal('Error', 'Something went wrong, please try again later!', 'error');
  181. this.isLoaded = true;
  182. this.feedLoaded = true;
  183. })
  184. .finally(() => {
  185. this.fetchFeed();
  186. })
  187. },
  188. fetchFeed() {
  189. axios.get('/api/v1/timelines/tag/' + this.id, {
  190. params: {
  191. limit: 80,
  192. }
  193. })
  194. .then(res => {
  195. if(res.data && res.data.length) {
  196. this.feed = res.data.map(s => {
  197. if(s.filtered) {
  198. s.sensitive = true;
  199. }
  200. return s;
  201. });
  202. this.maxId = res.data[res.data.length - 1].id;
  203. this.canLoadMore = true;
  204. } else {
  205. this.feedLoaded = true;
  206. this.isLoaded = true;
  207. }
  208. })
  209. .finally(() => {
  210. this.feedLoaded = true;
  211. this.isLoaded = true;
  212. })
  213. },
  214. statusUrl(status) {
  215. return '/i/web/post/' + status.id;
  216. },
  217. formatCount(val) {
  218. return App.util.format.count(val);
  219. },
  220. enterIntersect() {
  221. if(this.isIntersecting) {
  222. return;
  223. }
  224. this.isIntersecting = true;
  225. axios.get('/api/v1/timelines/tag/' + this.id, {
  226. params: {
  227. max_id: this.maxId,
  228. limit: 40,
  229. }
  230. })
  231. .then(res => {
  232. if(res.data && res.data.length) {
  233. const data = res.data.map(s => {
  234. if(s.filtered) {
  235. s.sensitive = true;
  236. }
  237. return s;
  238. });
  239. this.feed.push(...data);
  240. this.maxId = res.data[res.data.length - 1].id;
  241. this.canLoadMore = true;
  242. } else {
  243. this.canLoadMore = false;
  244. }
  245. })
  246. .finally(() => {
  247. this.isIntersecting = false;
  248. })
  249. },
  250. goToPost(status) {
  251. this.$router.push({
  252. name: 'post',
  253. path: `/i/web/post/${status.id}`,
  254. params: {
  255. id: status.id,
  256. cachedStatus: status,
  257. cachedProfile: this.profile
  258. }
  259. })
  260. },
  261. followHashtag() {
  262. this.followingLoading = true;
  263. axios.post('/api/v1/tags/' + this.id + '/follow')
  264. .then(res => {
  265. setTimeout(() => {
  266. this.hashtag.following = true;
  267. this.followingLoading = false;
  268. }, 500);
  269. });
  270. },
  271. unfollowHashtag() {
  272. this.followingLoading = true;
  273. axios.post('/api/v1/tags/' + this.id + '/unfollow')
  274. .then(res => {
  275. setTimeout(() => {
  276. this.hashtag.following = false;
  277. this.followingLoading = false;
  278. }, 500);
  279. });
  280. },
  281. getMediaSource(status) {
  282. let media = status.media_attachments[0];
  283. if(media.preview_url && media.preview_url.endsWith('storage/no-preview.png')) {
  284. return media.url;
  285. }
  286. if(media.preview_url && media.preview_url.length) {
  287. return media.url;
  288. }
  289. return media.url;
  290. }
  291. }
  292. }
  293. </script>
  294. <style lang="scss">
  295. .hashtag-component {
  296. .hashtag-feed {
  297. .card,
  298. .info-overlay-text,
  299. .info-overlay-text-label,
  300. img,
  301. canvas {
  302. border-radius: 18px !important;
  303. }
  304. }
  305. .hashtag-follow {
  306. width: 200px;
  307. }
  308. .ph-wrapper {
  309. padding: 0.25rem;
  310. .ph-item {
  311. margin: 0;
  312. padding: 0;
  313. border: none;
  314. background-color: transparent;
  315. .ph-picture {
  316. height: auto;
  317. padding-bottom: 100%;
  318. border-radius: 18px;
  319. }
  320. & > * {
  321. margin-bottom: 0;
  322. }
  323. }
  324. }
  325. }
  326. </style>