1
0

Hashtag.vue 13 KB

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