Hashtag.vue 8.1 KB

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