CommentDrawer.vue 32 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066
  1. <template>
  2. <div class="post-comment-drawer">
  3. <input type="file" ref="fileInput" class="d-none" accept="image/jpeg,image/png" @change="handleImageUpload">
  4. <div class="post-comment-drawer-feed">
  5. <div v-if="feed.length && feed.length >= 1" class="mb-2 sort-menu">
  6. <b-dropdown size="sm" variant="link" ref="sortMenu" toggle-class="text-decoration-none text-dark font-weight-bold" no-caret>
  7. <template #button-content>
  8. Show {{ sorts[sortIndex] }} comments <i class="far fa-chevron-down ml-1"></i>
  9. </template>
  10. <b-dropdown-item href="#" :class="{ active: sortIndex === 0 }" @click="toggleSort(0)">
  11. <p class="title mb-0">All</p>
  12. <p class="description">All comments in chronological order</p>
  13. </b-dropdown-item>
  14. <b-dropdown-item href="#" :class="{ active: sortIndex === 1 }" @click="toggleSort(1)">
  15. <p class="title mb-0">Newest</p>
  16. <p class="description">Newest comments appear first</p>
  17. </b-dropdown-item>
  18. <b-dropdown-item href="#" :class="{ active: sortIndex === 2 }" @click="toggleSort(2)">
  19. <p class="title mb-0">Popular</p>
  20. <p class="description">The most relevant comments appear first</p>
  21. </b-dropdown-item>
  22. </b-dropdown>
  23. </div>
  24. <div v-if="feedLoading" class="post-comment-drawer-feed-loader">
  25. <b-spinner />
  26. </div>
  27. <div v-else>
  28. <transition-group tag="div" enter-active-class="animate__animated animate__fadeIn" leave-active-class="animate__animated animate__fadeOut" mode="out-in">
  29. <div
  30. v-for="(post, idx) in feed"
  31. :key="'cd:' + post.id + ':' + idx"
  32. class="media media-status align-items-top mb-3"
  33. :style="{ opacity: deletingIndex && deletingIndex === idx ? 0.3 : 1 }">
  34. <a href="#l">
  35. <img class="shadow-sm media-avatar border" :src="getPostAvatar(post)" width="40" height="40" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
  36. </a>
  37. <div class="media-body">
  38. <div class="media-body-wrapper">
  39. <div v-if="!post.media_attachments.length" class="media-body-comment">
  40. <p class="media-body-comment-username">
  41. <a :href="post.account.url" :id="'acpop_'+post.id" tabindex="0" @click.prevent="goToProfile(post.account)">
  42. {{ post.account.acct }}
  43. </a>
  44. <b-popover :target="'acpop_'+post.id" triggers="hover" placement="bottom" custom-class="shadow border-0 rounded-px" :delay="750">
  45. <profile-hover-card
  46. :profile="post.account"
  47. v-on:follow="follow(idx)"
  48. v-on:unfollow="unfollow(idx)" />
  49. </b-popover>
  50. </p>
  51. <span v-if="post.sensitive">
  52. <p class="mb-0">
  53. {{ $t('common.sensitiveContentWarning') }}
  54. </p>
  55. <a href="#" class="small font-weight-bold primary" @click.prevent="post.sensitive = false">Show</a>
  56. </span>
  57. <!-- <span v-else v-html="post.content"></span> -->
  58. <read-more v-else :status="post" />
  59. <button
  60. v-if="post.favourites_count && !hideCounts"
  61. class="btn btn-link media-body-likes-count shadow-sm"
  62. @click.prevent="showLikesModal(idx)">
  63. <i class="far fa-thumbs-up primary"></i>
  64. <span class="count">{{ prettyCount(post.favourites_count) }}</span>
  65. </button>
  66. </div>
  67. <div v-else>
  68. <div :class="[ post.content && post.content.length || post.media_attachments.length ? 'media-body-comment' : '']">
  69. <p class="media-body-comment-username">
  70. <a :href="post.account.url" @click.prevent="goToProfile(post.account)">
  71. {{ post.account.acct }}
  72. </a>
  73. </p>
  74. <div v-if="post.sensitive" class="bh-comment" @click="post.sensitive = false">
  75. <blur-hash-image
  76. :width="blurhashWidth(post)"
  77. :height="blurhashHeight(post)"
  78. :punch="1"
  79. class="img-fluid border shadow"
  80. :hash="post.media_attachments[0].blurhash"
  81. />
  82. <div class="sensitive-warning">
  83. <p class="mb-0"><i class="far fa-eye-slash fa-lg"></i></p>
  84. <p class="mb-0 small">Tap to view</p>
  85. </div>
  86. </div>
  87. <read-more :status="post" class="mb-1" />
  88. <div v-if="!post.sensitive"
  89. class="bh-comment"
  90. :class="[post.media_attachments.length > 1 ? 'bh-comment-borderless' : '']"
  91. :style="{
  92. 'max-width': post.media_attachments.length > 1 ? '100% !important' : '160px',
  93. 'max-height': post.media_attachments.length > 1 ? '100% !important' : '260px',
  94. }">
  95. <div v-if="post.media_attachments[0].type == 'image'">
  96. <div v-if="post.media_attachments.length == 1">
  97. <div @click="lightbox(post)">
  98. <blur-hash-image
  99. :width="blurhashWidth(post)"
  100. :height="blurhashHeight(post)"
  101. :punch="1"
  102. class="img-fluid border shadow"
  103. :hash="post.media_attachments[0].blurhash"
  104. :src="getMediaSource(post)"
  105. />
  106. </div>
  107. </div>
  108. <div v-else
  109. style="
  110. display: grid;
  111. grid-auto-flow:column;
  112. gap:1px;
  113. grid-template-rows: [row1-start] 50% [row1-end row2-start] 50% [row2-end];
  114. grid-template-columns: [column1-start] 50% [column1-end column2-start] 50% [column2-end];
  115. border-radius: 8px;
  116. ">
  117. <div v-for="(albumMedia, idx) in post.media_attachments.slice(0, 4)" @click="lightbox(post, idx)">
  118. <blur-hash-image
  119. :width="30"
  120. :height="30"
  121. :punch="1"
  122. class="img-fluid shadow"
  123. :hash="post.media_attachments[idx].blurhash"
  124. :src="getMediaSource(post, idx)"
  125. />
  126. </div>
  127. </div>
  128. </div>
  129. <div v-else="post.media_attachments[0].type == 'vaideo'">
  130. <div @click="lightbox(post)" class="cursor-pointer">
  131. <div style="position: relative;" class="d-flex align-items-center justify-content-center">
  132. <div style="position: absolute;width: 40px; height: 40px; background-color: rgba(0, 0, 0, 0.5);border-radius: 40px;" class="d-flex justify-content-center align-items-center">
  133. <i class="far fa-play pl-1 text-white fa-lg"></i>
  134. </div>
  135. <video :src="post.media_attachments[0].url" class="img-fluid" style="max-height: 200px"/>
  136. </div>
  137. </div>
  138. </div>
  139. <div v-else>
  140. <p>Cannot render commment</p>
  141. </div>
  142. <button
  143. v-if="post.favourites_count && !hideCounts"
  144. class="btn btn-link media-body-likes-count shadow-sm"
  145. @click.prevent="showLikesModal(idx)">
  146. <i class="far fa-thumbs-up primary"></i>
  147. <span class="count">{{ prettyCount(post.favourites_count) }}</span>
  148. </button>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. <p class="media-body-reactions">
  154. <button
  155. class="btn btn-link font-weight-bold btn-sm p-0"
  156. :class="[ post.favourited ? 'primary' : 'text-muted' ]"
  157. @click="likeComment(idx)">
  158. {{ post.favourited ? 'Liked' : 'Like' }}
  159. </button>
  160. <template v-if="post.visibility != 'public'">
  161. <span class="mx-1">·</span>
  162. <span
  163. v-if="post.visibility === 'unlisted'"
  164. class="text-lighter"
  165. v-b-tooltip:hover.bottom
  166. title="This post is unlisted on timelines">
  167. <i class="far fa-unlock fa-sm"></i>
  168. </span>
  169. <span
  170. v-else-if="post.visibility === 'private'"
  171. class="text-muted"
  172. v-b-tooltip:hover.bottom
  173. title="This post is only visible to followers of this account">
  174. <i class="far fa-lock fa-sm"></i>
  175. </span>
  176. </template>
  177. <span class="mx-1">·</span>
  178. <a class="font-weight-bold text-muted" :href="post.url" @click.prevent="toggleCommentReply(idx)">
  179. Reply
  180. </a>
  181. <span class="mx-1">·</span>
  182. <a class="font-weight-bold text-muted" :href="post.url" @click.prevent="goToPost(post)" v-once>
  183. {{ timeago(post.created_at) }}
  184. </a>
  185. <span v-if="profile && post.account.id === profile.id || status.account.id === profile.id">
  186. <span class="mx-1">·</span>
  187. <a
  188. class="font-weight-bold"
  189. :class="[deletingIndex && deletingIndex === idx ? 'text-danger' : 'text-muted']"
  190. href="#"
  191. @click.prevent="deleteComment(idx)">
  192. {{ deletingIndex && deletingIndex === idx ? 'Deleting...' : 'Delete'}}
  193. </a>
  194. </span>
  195. <span v-else>
  196. <span class="mx-1">·</span>
  197. <a
  198. class="font-weight-bold text-muted"
  199. href="#"
  200. @click.prevent="reportComment(idx)">
  201. Report
  202. </a>
  203. </span>
  204. </p>
  205. <template v-if="post.reply_count">
  206. <div v-if="!post.replies.replies_show && commentReplyIndex !== idx" class="media-body-show-replies">
  207. <a href="#" class="font-weight-bold primary" @click.prevent="showCommentReplies(idx)">
  208. <i class="media-body-show-replies-icon"></i>
  209. <span class="media-body-show-replies-label">Show {{ prettyCount(post.reply_count) }} replies</span>
  210. </a>
  211. </div>
  212. <div v-else class="media-body-show-replies">
  213. <a href="#" class="font-weight-bold text-muted" @click.prevent="hideCommentReplies(idx)">
  214. <i class="media-body-show-replies-icon"></i>
  215. <span class="media-body-show-replies-label">Hide {{ prettyCount(post.reply_count) }} replies</span>
  216. </a>
  217. </div>
  218. </template>
  219. <comment-replies
  220. :key="`cmr-${post.id}-${feed[idx].reply_count}`"
  221. v-if="feed[idx].replies_show"
  222. :status="post"
  223. :feed="feed[idx].replies"
  224. v-on:counter-change="replyCounterChange(idx, $event)"
  225. class="mt-3" />
  226. <div
  227. v-if="post.replies_show == true && commentReplyIndex == idx && feed[idx].reply_count > 3">
  228. <div class="media-body-show-replies mt-n3">
  229. <a href="#" class="font-weight-bold text-dark" @click.prevent="goToPost(post)">
  230. <i class="media-body-show-replies-icon"></i>
  231. <span class="media-body-show-replies-label">View full thread</span>
  232. </a>
  233. </div>
  234. </div>
  235. <comment-reply-form
  236. v-if="commentReplyIndex == idx"
  237. :parent-id="post.id"
  238. v-on:new-comment="pushCommentReply(idx, $event)"
  239. v-on:counter-change="replyCounterChange(idx, $event)" />
  240. <!-- <div v-if="commentReplyIndex != undefined && commentReplyIndex == idx" class="d-flex align-items-top reply-form child-reply-form my-3">
  241. <img class="shadow-sm media-avatar border" :src="profile.avatar" width="40" height="40">
  242. <input
  243. class="form-control bg-light rounded-pill shadow-sm" style="border-color: #e2e8f0 !important;"
  244. placeholder="Write a comment...."
  245. v-model="replyContent"
  246. v-on:keyup.enter="storeComment"
  247. :disabled="isPostingReply" />
  248. <div class="reply-form-input-actions">
  249. <button
  250. class="btn btn-link text-muted px-1 mr-2">
  251. <i class="far fa-image fa-lg"></i>
  252. </button>
  253. <button
  254. class="btn btn-link text-muted px-1 small font-weight-bold py-0 rounded-pill text-decoration-none"
  255. @click="toggleShowReplyOptions">
  256. <i class="far fa-ellipsis-h"></i>
  257. </button>
  258. </div>
  259. </div> -->
  260. </div>
  261. </div>
  262. </transition-group>
  263. </div>
  264. </div>
  265. <div v-if="!feedLoading && canLoadMore" class="post-comment-drawer-loadmore">
  266. <p>
  267. <a class="font-weight-bold text-dark" href="#" @click.prevent="fetchMore()">Load more comments…</a>
  268. </p>
  269. </div>
  270. <div v-if="showEmptyRepliesRefresh" class="post-comment-drawer-loadmore">
  271. <p class="text-center mb-4">
  272. <a class="btn btn-outline-primary font-weight-bold rounded-pill" href="#" @click.prevent="forceRefresh()">
  273. <i class="far fa-sync mr-2"></i> Refresh
  274. </a>
  275. </p>
  276. </div>
  277. <div class="d-flex align-items-top reply-form child-reply-form">
  278. <img class="shadow-sm media-avatar border" :src="profile.avatar" width="40" height="40" draggable="false" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
  279. <div v-show="!settings.expanded" class="w-100">
  280. <vue-tribute :options="tributeSettings">
  281. <textarea
  282. class="form-control bg-light rounded-sm shadow-sm rounded-pill"
  283. placeholder="Write a comment...."
  284. style="resize: none;padding-right:140px;"
  285. rows="1"
  286. v-model="replyContent"
  287. :disabled="isPostingReply"></textarea>
  288. </vue-tribute>
  289. </div>
  290. <div v-show="settings.expanded" class="w-100">
  291. <vue-tribute :options="tributeSettings">
  292. <textarea
  293. class="form-control bg-light rounded-sm shadow-sm"
  294. placeholder="Write a comment...."
  295. style="resize: none;padding-right:140px;"
  296. rows="5"
  297. v-model="replyContent"
  298. :disabled="isPostingReply"></textarea>
  299. </vue-tribute>
  300. </div>
  301. <div class="reply-form-input-actions" :class="{ open: settings.expanded }">
  302. <button
  303. @click="replyUpload()"
  304. class="btn btn-link text-muted px-1 mr-2">
  305. <i class="far fa-image fa-lg"></i>
  306. </button>
  307. <button
  308. @click="toggleReplyExpand()"
  309. class="btn btn-link text-muted px-1 mr-2">
  310. <i class="far fa-text-size fa-lg"></i>
  311. </button>
  312. <button
  313. class="btn btn-link text-muted px-1 small font-weight-bold py-0 rounded-pill text-decoration-none"
  314. @click="toggleShowReplyOptions">
  315. <i class="far fa-ellipsis-h"></i>
  316. </button>
  317. </div>
  318. </div>
  319. <div v-if="showReplyOptions" class="child-reply-form-options mt-2" style="margin-left: 60px;">
  320. <b-form-checkbox v-model="settings.sensitive" switch>
  321. {{ $t('common.sensitive') }}
  322. </b-form-checkbox>
  323. </div>
  324. <div v-if="replyContent && replyContent.length" class="text-right mt-2">
  325. <button class="btn btn-primary btn-sm font-weight-bold primary rounded-pill px-4" @click="storeComment">{{ $t('common.comment') }}</button>
  326. </div>
  327. <b-modal ref="lightboxModal"
  328. id="lightbox"
  329. :hide-header="true"
  330. :hide-footer="true"
  331. centered
  332. size="lg"
  333. body-class="p-0"
  334. content-class="bg-transparent border-0 position-relative"
  335. >
  336. <div v-if="lightboxStatus && lightboxStatus.type == 'image'" @click="hideLightbox">
  337. <img :src="lightboxStatus.url" style="width: 100%;max-height: 90vh;object-fit: contain;">
  338. </div>
  339. <div v-else-if="lightboxStatus && lightboxStatus.type == 'video'" style="position: relative" class="d-flex align-items-center justify-content-center">
  340. <button
  341. class="btn btn-dark d-flex align-items-center justify-content-center"
  342. style="position: fixed; top: 10px; right: 10px;width: 56px; height: 56px; border-radius: 56px;"
  343. @click="hideLightbox">
  344. <i class="far fa-times-circle fa-2x text-warning" style="padding-top:2px"></i>
  345. </button>
  346. <video :src="lightboxStatus.url" controls style="max-height: 90vh;object-fit: contain;" autoplay @ended="hideLightbox"/>
  347. </div>
  348. </b-modal>
  349. </div>
  350. </template>
  351. <script type="text/javascript">
  352. import VueTribute from 'vue-tribute'
  353. import ReadMore from './ReadMore.vue';
  354. import ProfileHoverCard from './../profile/ProfileHoverCard.vue';
  355. import CommentReplies from './CommentReplies.vue';
  356. import CommentReplyForm from './CommentReplyForm.vue';
  357. export default {
  358. props: {
  359. status: {
  360. type: Object
  361. }
  362. },
  363. components: {
  364. VueTribute,
  365. ReadMore,
  366. ProfileHoverCard,
  367. CommentReplyForm,
  368. CommentReplies
  369. },
  370. data() {
  371. return {
  372. profile: window._sharedData.user,
  373. ids: [],
  374. feed: [],
  375. sortIndex: 0,
  376. sorts: [
  377. 'all',
  378. 'newest',
  379. 'popular'
  380. ],
  381. replyContent: undefined,
  382. nextUrl: undefined,
  383. canLoadMore: false,
  384. isPostingReply: false,
  385. showReplyOptions: false,
  386. feedLoading: false,
  387. isUploading: false,
  388. uploadProgress: 0,
  389. lightboxStatus: null,
  390. settings: {
  391. expanded: false,
  392. sensitive: false
  393. },
  394. tributeSettings: {
  395. noMatchTemplate: null,
  396. collection: [
  397. {
  398. trigger: '@',
  399. menuShowMinLength: 2,
  400. values: (function (text, cb) {
  401. let url = '/api/compose/v0/search/mention';
  402. axios.get(url, { params: { q: text }})
  403. .then(res => {
  404. cb(res.data);
  405. })
  406. .catch(err => {
  407. cb();
  408. console.log(err);
  409. })
  410. })
  411. },
  412. {
  413. trigger: '#',
  414. menuShowMinLength: 2,
  415. values: (function (text, cb) {
  416. let url = '/api/compose/v0/search/hashtag';
  417. axios.get(url, { params: { q: text }})
  418. .then(res => {
  419. cb(res.data);
  420. })
  421. .catch(err => {
  422. cb();
  423. console.log(err);
  424. })
  425. })
  426. }
  427. ]
  428. },
  429. showEmptyRepliesRefresh: false,
  430. commentReplyIndex: undefined,
  431. deletingIndex: undefined
  432. }
  433. },
  434. mounted() {
  435. // if(this.status.replies && this.status.replies.length) {
  436. // this.feed.push(this.status.replies);
  437. // }
  438. this.fetchContext();
  439. },
  440. computed: {
  441. hideCounts: {
  442. get() {
  443. return this.$store.state.hideCounts == true;
  444. }
  445. },
  446. },
  447. methods: {
  448. fetchContext() {
  449. axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
  450. params: {
  451. limit: 3
  452. }
  453. })
  454. .then(res => {
  455. if(res.data.next) {
  456. this.nextUrl = res.data.next;
  457. this.canLoadMore = true;
  458. }
  459. res.data.data.forEach(post => {
  460. this.ids.push(post.id);
  461. this.feed.push(post);
  462. });
  463. if(!res.data || !res.data.data || !res.data.data.length && this.status.reply_count) {
  464. this.showEmptyRepliesRefresh = true;
  465. }
  466. })
  467. },
  468. fetchMore(limit = 3) {
  469. if(event) {
  470. event.target?.blur();
  471. }
  472. if(!this.nextUrl) {
  473. return;
  474. }
  475. axios.get(this.nextUrl, {
  476. params: {
  477. limit: limit,
  478. sort: this.sorts[this.sortIndex]
  479. }
  480. }).then(res => {
  481. this.feedLoading = false;
  482. if(!res.data.next) {
  483. this.canLoadMore = false;
  484. }
  485. this.nextUrl = res.data.next;
  486. res.data.data.forEach(post => {
  487. if(this.ids && this.ids.indexOf(post.id) == -1) {
  488. this.ids.push(post.id);
  489. this.feed.push(post);
  490. }
  491. });
  492. })
  493. },
  494. fetchSortedFeed() {
  495. axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
  496. params: {
  497. limit: 3,
  498. sort: this.sorts[this.sortIndex]
  499. }
  500. })
  501. .then(res => {
  502. this.feed = res.data.data;
  503. this.nextUrl = res.data.next;
  504. this.feedLoading = false;
  505. });
  506. },
  507. forceRefresh() {
  508. axios.get('/api/v2/statuses/' + this.status.id + '/replies', {
  509. params: {
  510. limit: 3,
  511. refresh_cache: true
  512. }
  513. })
  514. .then(res => {
  515. if(res.data.next) {
  516. this.nextUrl = res.data.next;
  517. this.canLoadMore = true;
  518. }
  519. res.data.data.forEach(post => {
  520. this.ids.push(post.id);
  521. this.feed.push(post);
  522. });
  523. this.showEmptyRepliesRefresh = false;
  524. })
  525. },
  526. timeago(ts) {
  527. return App.util.format.timeAgo(ts);
  528. },
  529. prettyCount(val) {
  530. return App.util.format.count(val);
  531. },
  532. goToPost(post) {
  533. this.$router.push({
  534. name: 'post',
  535. path: `/i/web/post/${post.id}`,
  536. params: {
  537. id: post.id,
  538. cachedStatus: post,
  539. cachedProfile: this.profile
  540. }
  541. })
  542. },
  543. goToProfile(account) {
  544. this.$router.push({
  545. name: 'profile',
  546. path: `/i/web/profile/${account.id}`,
  547. params: {
  548. id: account.id,
  549. cachedProfile: account,
  550. cachedUser: this.profile
  551. }
  552. })
  553. },
  554. storeComment() {
  555. this.isPostingReply = true;
  556. axios.post('/api/v1/statuses', {
  557. status: this.replyContent,
  558. in_reply_to_id: this.status.id,
  559. sensitive: this.settings.sensitive
  560. })
  561. .then(res => {
  562. let cmt = res.data;
  563. cmt.replies = [];
  564. this.replyContent = undefined;
  565. this.isPostingReply = false;
  566. this.ids.push(res.data.id);
  567. this.feed.push(cmt);
  568. this.$emit('counter-change', 'comment-increment');
  569. })
  570. },
  571. toggleSort(index) {
  572. this.$refs.sortMenu.hide();
  573. this.feedLoading = true;
  574. this.sortIndex = index;
  575. this.fetchSortedFeed();
  576. },
  577. deleteComment(index) {
  578. event.currentTarget.blur();
  579. if(!window.confirm(this.$t('menu.deletePostConfirm'))) {
  580. return;
  581. }
  582. this.deletingIndex = index;
  583. axios.post('/i/delete', {
  584. type: 'status',
  585. item: this.feed[index].id
  586. })
  587. .then(res => {
  588. if(this.ids && this.ids.length) {
  589. this.ids.splice(index, 1);
  590. }
  591. if(this.feed && this.feed.length) {
  592. this.feed.splice(index, 1);
  593. }
  594. this.$emit('counter-change', 'comment-decrement');
  595. })
  596. .then(() => {
  597. this.deletingIndex = undefined;
  598. this.fetchMore(1);
  599. })
  600. },
  601. showLikesModal(index) {
  602. this.$emit('show-likes', this.feed[index]);
  603. },
  604. reportComment(index) {
  605. // location.href = '/i/report?type=post&id=' + this.feed[index].id;
  606. this.$emit('handle-report', this.feed[index]);
  607. },
  608. likeComment(index) {
  609. event.currentTarget.blur();
  610. let post = this.feed[index];
  611. let count = post.favourites_count;
  612. let state = post.favourited;
  613. this.feed[index].favourited = !this.feed[index].favourited;
  614. this.feed[index].favourites_count = state ? count - 1 : count + 1;
  615. axios.post('/api/v1/statuses/' + post.id + '/' + (state ? 'unfavourite' : 'favourite'))
  616. .then(res => {
  617. })
  618. },
  619. toggleShowReplyOptions() {
  620. event.currentTarget.blur();
  621. this.showReplyOptions = !this.showReplyOptions;
  622. },
  623. replyUpload() {
  624. event.currentTarget.blur();
  625. this.$refs.fileInput.click();
  626. },
  627. handleImageUpload() {
  628. if(!this.$refs.fileInput.files.length) {
  629. return;
  630. }
  631. this.isUploading = true;
  632. let self = this;
  633. let data = new FormData();
  634. data.append('file', this.$refs.fileInput.files[0]);
  635. axios.post('/api/v1/media', data)
  636. .then(res => {
  637. axios.post('/api/v1/statuses', {
  638. status: this.replyContent,
  639. media_ids: [ res.data.id ],
  640. in_reply_to_id: this.status.id,
  641. sensitive: this.settings.sensitive
  642. }).then(res => {
  643. this.feed.push(res.data);
  644. this.replyContent = undefined;
  645. this.isPostingReply = false;
  646. this.ids.push(res.data.id);
  647. this.$emit('counter-change', 'comment-increment');
  648. })
  649. });
  650. },
  651. lightbox(status, idx = 0) {
  652. this.lightboxStatus = status.media_attachments[idx];
  653. this.$refs.lightboxModal.show();
  654. },
  655. hideLightbox() {
  656. this.lightboxStatus = null;
  657. this.$refs.lightboxModal.hide();
  658. },
  659. blurhashWidth(status) {
  660. if(!status.media_attachments[0].meta) {
  661. return 25;
  662. }
  663. let aspect = status.media_attachments[0].meta.original.aspect;
  664. if(aspect == 1) {
  665. return 25;
  666. } else if(aspect > 1) {
  667. return 30;
  668. } else {
  669. return 20;
  670. }
  671. },
  672. blurhashHeight(status) {
  673. if(!status.media_attachments[0].meta) {
  674. return 25;
  675. }
  676. let aspect = status.media_attachments[0].meta.original.aspect;
  677. if(aspect == 1) {
  678. return 25;
  679. } else if(aspect > 1) {
  680. return 20;
  681. } else {
  682. return 30;
  683. }
  684. },
  685. getMediaSource(status, idx = 0) {
  686. let media = status.media_attachments[idx];
  687. if(media.preview_url.endsWith('storage/no-preview.png')) {
  688. return media.url;
  689. }
  690. return media.preview_url;
  691. },
  692. toggleReplyExpand() {
  693. event.currentTarget.blur();
  694. this.settings.expanded = !this.settings.expanded;
  695. },
  696. toggleCommentReply(index) {
  697. this.commentReplyIndex = index;
  698. this.showCommentReplies(index);
  699. },
  700. showCommentReplies(index) {
  701. if(this.feed[index].hasOwnProperty('replies_show') && this.feed[index].replies_show) {
  702. this.feed[index].replies_show = false;
  703. this.commentReplyIndex = undefined;
  704. return;
  705. }
  706. this.feed[index].replies_show = true;
  707. this.commentReplyIndex = index;
  708. this.fetchCommentReplies(index);
  709. },
  710. hideCommentReplies(index) {
  711. this.commentReplyIndex = undefined;
  712. this.feed[index].replies_show = false;
  713. },
  714. fetchCommentReplies(index) {
  715. axios.get('/api/v2/statuses/' + this.feed[index].id + '/replies', {
  716. params: {
  717. limit: 3
  718. }
  719. })
  720. .then(res => {
  721. this.feed[index].replies = res.data.data;
  722. })
  723. },
  724. getPostAvatar(post) {
  725. if(this.profile.id == post.account.id) {
  726. return window._sharedData.user.avatar;
  727. }
  728. return post.account.avatar;
  729. },
  730. follow(index) {
  731. axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/follow')
  732. .then(res => {
  733. this.$store.commit('updateRelationship', [res.data]);
  734. this.feed[index].account.followers_count = this.feed[index].account.followers_count + 1;
  735. window._sharedData.user.following_count = window._sharedData.user.following_count + 1;
  736. })
  737. },
  738. unfollow(index) {
  739. axios.post('/api/v1/accounts/' + this.feed[index].account.id + '/unfollow')
  740. .then(res => {
  741. this.$store.commit('updateRelationship', [res.data]);
  742. this.feed[index].account.followers_count = this.feed[index].account.followers_count - 1;
  743. window._sharedData.user.following_count = window._sharedData.user.following_count - 1;
  744. })
  745. },
  746. handleCounterChange(payload) {
  747. this.$emit('counter-change', payload);
  748. },
  749. pushCommentReply(index, post) {
  750. if(!this.feed[index].hasOwnProperty('replies')) {
  751. this.feed[index].replies = [post];
  752. } else {
  753. this.feed[index].replies.push(post);
  754. }
  755. this.feed[index].reply_count = this.feed[index].reply_count + 1;
  756. this.feed[index].replies_show = true;
  757. },
  758. replyCounterChange(index, type) {
  759. switch(type) {
  760. case 'comment-increment':
  761. this.feed[index].reply_count = this.feed[index].reply_count + 1;
  762. break;
  763. case 'comment-decrement':
  764. this.feed[index].reply_count = this.feed[index].reply_count - 1;
  765. break;
  766. }
  767. }
  768. }
  769. }
  770. </script>
  771. <style lang="scss">
  772. .post-comment-drawer {
  773. &-feed {
  774. margin-bottom: 1rem;
  775. .sort-menu {
  776. .dropdown {
  777. border-radius: 18px;
  778. }
  779. .dropdown-menu {
  780. padding: 0;
  781. }
  782. .dropdown-item:active {
  783. background-color: inherit;
  784. }
  785. .title {
  786. color: var(--dropdown-item-color);
  787. }
  788. .description {
  789. margin-bottom: 0;
  790. color: var(--dropdown-item-color);
  791. font-size: 12px;
  792. }
  793. .active {
  794. .title {
  795. font-weight: 600;
  796. color: var(--dropdown-item-active-color);
  797. }
  798. .description {
  799. color: var(--dropdown-item-active-color);
  800. }
  801. }
  802. }
  803. &-loader {
  804. display: flex;
  805. justify-content: center;
  806. align-items: center;
  807. height: 200px;
  808. }
  809. }
  810. .media-body {
  811. &-comment {
  812. position: relative;
  813. min-width: 240px;
  814. }
  815. &-wrapper {
  816. .media-body-comment {
  817. padding: 0.7rem;
  818. }
  819. .media-body-likes-count {
  820. z-index: 3;
  821. position: absolute;
  822. right: -5px;
  823. bottom: -10px;
  824. background-color: var(--body-bg);
  825. padding: 1px 8px;
  826. font-weight: 600;
  827. font-size: 12px;
  828. border-radius: 15px;
  829. text-decoration: none;
  830. user-select: none !important;
  831. i {
  832. margin-right: 3px;
  833. }
  834. .count {
  835. color: #334155;
  836. }
  837. }
  838. }
  839. &-show-replies {
  840. margin-top: -5px;
  841. margin-bottom: 5px;
  842. font-size: 13px;
  843. a {
  844. display: flex;
  845. align-items: center;
  846. text-decoration: none;
  847. }
  848. &-icon {
  849. display: inline-block;
  850. font-style: normal;
  851. font-variant: normal;
  852. text-rendering: auto;
  853. line-height: 1;
  854. padding-left: 0.5rem;
  855. margin-right: 0.25rem;
  856. transform: rotate(90deg);
  857. font-family: 'Font Awesome 5 Free';
  858. font-weight: 400;
  859. text-decoration: none;
  860. &:before {
  861. content: "\F148";
  862. }
  863. }
  864. &-label {
  865. padding-top: 9px;
  866. }
  867. }
  868. }
  869. &-loadmore {
  870. font-size: 0.7875rem;
  871. }
  872. .reply-form {
  873. &-input {
  874. flex: 1;
  875. position: relative;
  876. &-actions {
  877. position: absolute;
  878. right: 10px;
  879. top: 50%;
  880. transform: translateY(-50%);
  881. &.open {
  882. top: 85%;
  883. transform: translateY(-85%);
  884. }
  885. }
  886. }
  887. }
  888. .child-reply-form {
  889. position: relative;
  890. }
  891. .bh-comment {
  892. position: relative;
  893. width: 100%;
  894. height: auto;
  895. max-width: 160px !important;
  896. max-height: 260px !important;
  897. .img-fluid,
  898. canvas {
  899. border-radius: 8px;
  900. }
  901. span {
  902. width: 100%;
  903. height: auto;
  904. max-width: 160px !important;
  905. max-height: 260px !important;
  906. }
  907. img {
  908. width: 100%;
  909. height: auto;
  910. max-width: 160px !important;
  911. max-height: 260px !important;
  912. object-fit: cover;
  913. border-radius: 8px;
  914. }
  915. &.bh-comment-borderless {
  916. .img-fluid,
  917. img,
  918. canvas {
  919. border-radius: 0;
  920. }
  921. border-radius: 8px;
  922. overflow: hidden;
  923. margin-bottom: 5px;
  924. }
  925. .sensitive-warning {
  926. position: absolute;
  927. left: 50%;
  928. top: 50%;
  929. transform: translate(-50%, -50%);
  930. text-align: center;
  931. color: #fff;
  932. user-select: none;
  933. cursor: pointer;
  934. background: rgba(0,0,0,0.4);
  935. padding: 5px;
  936. border-radius: 8px;
  937. }
  938. }
  939. .v-tribute {
  940. width: 100%;
  941. }
  942. }
  943. </style>