1
0

ProfileFeed.vue 31 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165
  1. <template>
  2. <div class="profile-feed-component">
  3. <div class="profile-feed-component-nav d-flex justify-content-center justify-content-md-between align-items-center mb-4">
  4. <div class="d-none d-md-block border-bottom flex-grow-1 profile-nav-btns">
  5. <div class="btn-group">
  6. <button
  7. class="btn btn-link"
  8. :class="[ tabIndex === 1 ? 'active' : '' ]"
  9. @click="toggleTab(1)"
  10. >
  11. Posts
  12. </button>
  13. <!-- <button
  14. class="btn btn-link"
  15. :class="[ tabIndex === 3 ? 'text-dark font-weight-bold' : 'text-lighter' ]"
  16. @click="toggleTab(3)">
  17. Albums
  18. </button> -->
  19. <button
  20. v-if="isOwner"
  21. class="btn btn-link"
  22. :class="[ tabIndex === 'archives' ? 'active' : '' ]"
  23. @click="toggleTab('archives')">
  24. Archives
  25. </button>
  26. <button
  27. v-if="isOwner"
  28. class="btn btn-link"
  29. :class="[ tabIndex === 'bookmarks' ? 'active' : '' ]"
  30. @click="toggleTab('bookmarks')">
  31. Bookmarks
  32. </button>
  33. <button
  34. v-if="canViewCollections"
  35. class="btn btn-link"
  36. :class="[ tabIndex === 2 ? 'active' : '' ]"
  37. @click="toggleTab(2)">
  38. Collections
  39. </button>
  40. <button
  41. v-if="isOwner"
  42. class="btn btn-link"
  43. :class="[ tabIndex === 3 ? 'active' : '' ]"
  44. @click="toggleTab(3)">
  45. Likes
  46. </button>
  47. </div>
  48. </div>
  49. <div v-if="tabIndex === 1" class="btn-group layout-sort-toggle">
  50. <button
  51. class="btn btn-sm"
  52. :class="[ layoutIndex === 0 ? 'btn-dark' : 'btn-light' ]"
  53. @click="toggleLayout(0, true)">
  54. <i class="far fa-th fa-lg"></i>
  55. </button>
  56. <button
  57. class="btn btn-sm"
  58. :class="[ layoutIndex === 1 ? 'btn-dark' : 'btn-light' ]"
  59. @click="toggleLayout(1, true)">
  60. <i class="fas fa-th-large fa-lg"></i>
  61. </button>
  62. <button
  63. class="btn btn-sm"
  64. :class="[ layoutIndex === 2 ? 'btn-dark' : 'btn-light' ]"
  65. @click="toggleLayout(2, true)">
  66. <i class="far fa-bars fa-lg"></i>
  67. </button>
  68. </div>
  69. </div>
  70. <div v-if="tabIndex == 0" class="d-flex justify-content-center mt-5">
  71. <b-spinner />
  72. </div>
  73. <div v-else-if="tabIndex == 1" class="px-0 mx-0">
  74. <div v-if="layoutIndex === 0" class="row">
  75. <div class="col-4 p-1" v-for="(s, index) in feed" :key="'tlob:'+index+s.id">
  76. <a v-if="s.hasOwnProperty('pf_type') && s.pf_type == 'video'" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
  77. <div class="square">
  78. <div v-if="s.sensitive" class="square-content">
  79. <div class="info-overlay-text-label">
  80. <h5 class="text-white m-auto font-weight-bold">
  81. <span>
  82. <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
  83. </span>
  84. </h5>
  85. </div>
  86. <blur-hash-canvas
  87. width="32"
  88. height="32"
  89. :hash="s.media_attachments[0].blurhash">
  90. </blur-hash-canvas>
  91. </div>
  92. <div v-else class="square-content">
  93. <blur-hash-image
  94. width="32"
  95. height="32"
  96. :hash="s.media_attachments[0].blurhash"
  97. :src="s.media_attachments[0].preview_url">
  98. </blur-hash-image>
  99. </div>
  100. <div class="info-overlay-text">
  101. <div class="text-white m-auto">
  102. <p class="info-overlay-text-field font-weight-bold">
  103. <span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
  104. <span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
  105. </p>
  106. <p class="info-overlay-text-field font-weight-bold">
  107. <span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
  108. <span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
  109. </p>
  110. <p class="mb-0 info-overlay-text-field font-weight-bold">
  111. <span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
  112. <span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
  113. </p>
  114. </div>
  115. </div>
  116. </div>
  117. <span class="badge badge-light video-overlay-badge">
  118. <i class="far fa-video fa-2x"></i>
  119. </span>
  120. <span class="badge badge-light timestamp-overlay-badge">
  121. {{ timeago(s.created_at) }}
  122. </span>
  123. </a>
  124. <a v-else class="card info-overlay card-md-border-0" :href="statusUrl(s)">
  125. <div class="square">
  126. <div v-if="s.sensitive" class="square-content">
  127. <div class="info-overlay-text-label">
  128. <h5 class="text-white m-auto font-weight-bold">
  129. <span>
  130. <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
  131. </span>
  132. </h5>
  133. </div>
  134. <blur-hash-canvas
  135. width="32"
  136. height="32"
  137. :hash="s.media_attachments[0].blurhash">
  138. </blur-hash-canvas>
  139. </div>
  140. <div v-else class="square-content">
  141. <blur-hash-image
  142. width="32"
  143. height="32"
  144. :hash="s.media_attachments[0].blurhash"
  145. :src="s.media_attachments[0].url">
  146. </blur-hash-image>
  147. </div>
  148. <div class="info-overlay-text">
  149. <div class="text-white m-auto">
  150. <p class="info-overlay-text-field font-weight-bold">
  151. <span class="far fa-heart fa-lg p-2 d-flex-inline"></span>
  152. <span class="d-flex-inline">{{formatCount(s.favourites_count)}}</span>
  153. </p>
  154. <p class="info-overlay-text-field font-weight-bold">
  155. <span class="far fa-comment fa-lg p-2 d-flex-inline"></span>
  156. <span class="d-flex-inline">{{formatCount(s.reply_count)}}</span>
  157. </p>
  158. <p class="mb-0 info-overlay-text-field font-weight-bold">
  159. <span class="far fa-sync fa-lg p-2 d-flex-inline"></span>
  160. <span class="d-flex-inline">{{formatCount(s.reblogs_count)}}</span>
  161. </p>
  162. </div>
  163. </div>
  164. </div>
  165. <span class="badge badge-light timestamp-overlay-badge">
  166. {{ timeago(s.created_at) }}
  167. </span>
  168. </a>
  169. </div>
  170. <intersect v-if="canLoadMore" @enter="enterIntersect">
  171. <div class="col-4 ph-wrapper">
  172. <div class="ph-item">
  173. <div class="ph-picture big"></div>
  174. </div>
  175. </div>
  176. </intersect>
  177. </div>
  178. <div v-else-if="layoutIndex === 1" class="row">
  179. <masonry
  180. :cols="{default: 3, 800: 2}"
  181. :gutter="{default: '5px'}">
  182. <div class="p-1" v-for="(s, index) in feed" :key="'tlog:'+index+s.id">
  183. <a v-if="s.hasOwnProperty('pf_type') && s.pf_type == 'video'" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
  184. <div class="square">
  185. <div class="square-content">
  186. <blur-hash-image
  187. width="32"
  188. height="32"
  189. class="rounded"
  190. :hash="s.media_attachments[0].blurhash"
  191. :src="s.media_attachments[0].preview_url">
  192. </blur-hash-image>
  193. </div>
  194. </div>
  195. <span class="badge badge-light video-overlay-badge">
  196. <i class="far fa-video fa-2x"></i>
  197. </span>
  198. <span class="badge badge-light timestamp-overlay-badge">
  199. {{ timeago(s.created_at) }}
  200. </span>
  201. </a>
  202. <a v-else-if="s.sensitive" class="card info-overlay card-md-border-0" :href="statusUrl(s)">
  203. <div class="square">
  204. <div class="square-content">
  205. <div class="info-overlay-text-label rounded">
  206. <h5 class="text-white m-auto font-weight-bold">
  207. <span>
  208. <span class="far fa-eye-slash fa-lg p-2 d-flex-inline"></span>
  209. </span>
  210. </h5>
  211. </div>
  212. <blur-hash-canvas
  213. width="32"
  214. height="32"
  215. class="rounded"
  216. :hash="s.media_attachments[0].blurhash">
  217. </blur-hash-canvas>
  218. </div>
  219. </div>
  220. </a>
  221. <a v-else class="card info-overlay card-md-border-0" :href="statusUrl(s)">
  222. <img :src="previewUrl(s)" class="img-fluid w-100 rounded-lg" onerror="this.onerror=null;this.src='/storage/no-preview.png?v=0'">
  223. <span class="badge badge-light timestamp-overlay-badge">
  224. {{ timeago(s.created_at) }}
  225. </span>
  226. </a>
  227. </div>
  228. <intersect v-if="canLoadMore" @enter="enterIntersect">
  229. <div class="p-1 ph-wrapper">
  230. <div class="ph-item">
  231. <div class="ph-picture big"></div>
  232. </div>
  233. </div>
  234. </intersect>
  235. </masonry>
  236. </div>
  237. <div v-else-if="layoutIndex === 2" class="row justify-content-center">
  238. <div class="col-12 col-md-10">
  239. <status-card
  240. v-for="(s, index) in feed"
  241. :key="'prs'+s.id+':'+index"
  242. :profile="user"
  243. :status="s"
  244. v-on:like="likeStatus(index)"
  245. v-on:unlike="unlikeStatus(index)"
  246. v-on:share="shareStatus(index)"
  247. v-on:unshare="unshareStatus(index)"
  248. v-on:menu="openContextMenu(index)"
  249. v-on:counter-change="counterChange(index, $event)"
  250. v-on:likes-modal="openLikesModal(index)"
  251. v-on:shares-modal="openSharesModal(index)"
  252. v-on:comment-likes-modal="openCommentLikesModal"
  253. v-on:handle-report="handleReport" />
  254. </div>
  255. <intersect v-if="canLoadMore" @enter="enterIntersect">
  256. <div class="col-12 col-md-10">
  257. <status-placeholder style="margin-bottom: 10rem;" />
  258. </div>
  259. </intersect>
  260. </div>
  261. <div v-if="feedLoaded && !feed.length">
  262. <div class="row justify-content-center">
  263. <div class="col-12 col-md-8 text-center">
  264. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
  265. <p class="lead text-muted font-weight-bold">{{ $t('profile.emptyPosts') }}</p>
  266. </div>
  267. </div>
  268. </div>
  269. </div>
  270. <div v-else-if="tabIndex === 'private'" class="row justify-content-center">
  271. <div class="col-12 col-md-8 text-center">
  272. <img src="/img/illustrations/dk-secure-feed.svg" class="img-fluid" style="opacity: 0.6;">
  273. <p class="h3 text-dark font-weight-bold mt-3 py-3">This profile is private</p>
  274. <div class="lead text-muted px-3">
  275. Only approved followers can see <span class="font-weight-bold text-dark text-break">&commat;{{ profile.acct }}</span>'s <br />
  276. posts. To request access, click <span class="font-weight-bold">Follow</span>.
  277. </div>
  278. </div>
  279. </div>
  280. <div v-else-if="tabIndex == 2" class="row justify-content-center">
  281. <div class="col-12 col-md-8">
  282. <div class="list-group">
  283. <a
  284. v-for="(collection, index) in collections"
  285. class="list-group-item text-decoration-none text-dark"
  286. :href="collection.url">
  287. <div class="media">
  288. <img :src="collection.thumb" width="65" height="65" style="object-fit: cover;" class="rounded-lg border mr-3" onerror="this.onerror=null;this.src='/storage/no-preview.png';">
  289. <div class="media-body text-left">
  290. <p class="lead mb-0">{{ collection.title ? collection.title : 'Untitled' }}</p>
  291. <p class="small text-muted mb-1">{{ collection.description || 'No description available' }}</p>
  292. <p class="small text-lighter mb-0 font-weight-bold">
  293. <span>{{ collection.post_count }} posts</span>
  294. <span>&middot;</span>
  295. <span v-if="collection.visibility === 'public'" class="text-dark">Public</span>
  296. <span v-else-if="collection.visibility === 'private'" class="text-dark"><i class="far fa-lock fa-sm"></i> Followers Only</span>
  297. <span v-else-if="collection.visibility === 'draft'" class="primary"><i class="far fa-lock fa-sm"></i> Draft</span>
  298. <span>&middot;</span>
  299. <span v-if="collection.published_at">Created {{ timeago(collection.published_at) }} ago</span>
  300. <span v-else class="text-warning">UNPUBLISHED</span>
  301. </p>
  302. </div>
  303. </div>
  304. </a>
  305. </div>
  306. </div>
  307. <div v-if="collectionsLoaded && !collections.length" class="col-12 col-md-8 text-center">
  308. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
  309. <p class="lead text-muted font-weight-bold">{{ $t('profile.emptyCollections') }}</p>
  310. </div>
  311. <div v-if="canLoadMoreCollections" class="col-12 col-md-8">
  312. <intersect @enter="enterCollectionsIntersect">
  313. <div class="d-flex justify-content-center mt-5">
  314. <b-spinner small />
  315. </div>
  316. </intersect>
  317. </div>
  318. </div>
  319. <div v-else-if="tabIndex == 3" class="px-0 mx-0">
  320. <div class="row justify-content-center">
  321. <div class="col-12 col-md-10">
  322. <status-card
  323. v-for="(s, index) in favourites"
  324. :key="'prs'+s.id+':'+index"
  325. :profile="user"
  326. :status="s"
  327. v-on:like="likeStatus(index)"
  328. v-on:unlike="unlikeStatus(index)"
  329. v-on:share="shareStatus(index)"
  330. v-on:unshare="unshareStatus(index)"
  331. v-on:counter-change="counterChange(index, $event)"
  332. v-on:likes-modal="openLikesModal(index)"
  333. v-on:comment-likes-modal="openCommentLikesModal"
  334. v-on:handle-report="handleReport" />
  335. </div>
  336. <div v-if="canLoadMoreFavourites" class="col-12 col-md-10">
  337. <intersect @enter="enterFavouritesIntersect">
  338. <status-placeholder style="margin-bottom: 10rem;" />
  339. </intersect>
  340. </div>
  341. </div>
  342. <div v-if="!favourites || !favourites.length" class="row justify-content-center">
  343. <div class="col-12 col-md-8 text-center">
  344. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
  345. <p class="lead text-muted font-weight-bold">We can't seem to find any posts you have liked</p>
  346. </div>
  347. </div>
  348. </div>
  349. <div v-else-if="tabIndex == 'bookmarks'" class="px-0 mx-0">
  350. <div class="row justify-content-center">
  351. <div class="col-12 col-md-10">
  352. <status-card
  353. v-for="(s, index) in bookmarks"
  354. :key="'prs'+s.id+':'+index"
  355. :profile="user"
  356. :new-reactions="true"
  357. :status="s"
  358. v-on:menu="openContextMenu(index)"
  359. v-on:counter-change="counterChange(index, $event)"
  360. v-on:likes-modal="openLikesModal(index)"
  361. v-on:bookmark="handleBookmark(index)"
  362. v-on:comment-likes-modal="openCommentLikesModal"
  363. v-on:handle-report="handleReport" />
  364. </div>
  365. <div class="col-12 col-md-10">
  366. <intersect v-if="canLoadMoreBookmarks" @enter="enterBookmarksIntersect">
  367. <status-placeholder style="margin-bottom: 10rem;" />
  368. </intersect>
  369. </div>
  370. </div>
  371. <div v-if="!bookmarks || !bookmarks.length" class="row justify-content-center">
  372. <div class="col-12 col-md-8 text-center">
  373. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
  374. <p class="lead text-muted font-weight-bold">We can't seem to find any posts you have bookmarked</p>
  375. </div>
  376. </div>
  377. </div>
  378. <div v-else-if="tabIndex == 'archives'" class="px-0 mx-0">
  379. <div class="row justify-content-center">
  380. <div class="col-12 col-md-10">
  381. <status-card
  382. v-for="(s, index) in archives"
  383. :key="'prarc'+s.id+':'+index"
  384. :profile="user"
  385. :new-reactions="true"
  386. :reaction-bar="false"
  387. :status="s"
  388. v-on:menu="openContextMenu(index, 'archive')"
  389. />
  390. </div>
  391. <div v-if="canLoadMoreArchives" class="col-12 col-md-10">
  392. <intersect @enter="enterArchivesIntersect">
  393. <status-placeholder style="margin-bottom: 10rem;" />
  394. </intersect>
  395. </div>
  396. </div>
  397. <div v-if="!archives || !archives.length" class="row justify-content-center">
  398. <div class="col-12 col-md-8 text-center">
  399. <img src="/img/illustrations/dk-nature-man-monochrome.svg" class="img-fluid" style="opacity: 0.6;">
  400. <p class="lead text-muted font-weight-bold">We can't seem to find any posts you have bookmarked</p>
  401. </div>
  402. </div>
  403. </div>
  404. <context-menu
  405. v-if="showMenu"
  406. ref="contextMenu"
  407. :status="contextMenuPost"
  408. :profile="user"
  409. v-on:moderate="commitModeration"
  410. v-on:delete="deletePost"
  411. v-on:archived="handleArchived"
  412. v-on:unarchived="handleUnarchived"
  413. v-on:report-modal="handleReport"
  414. />
  415. <likes-modal
  416. v-if="showLikesModal"
  417. ref="likesModal"
  418. :status="likesModalPost"
  419. :profile="user"
  420. />
  421. <shares-modal
  422. v-if="showSharesModal"
  423. ref="sharesModal"
  424. :status="sharesModalPost"
  425. :profile="profile"
  426. />
  427. <report-modal
  428. ref="reportModal"
  429. :key="reportedStatusId"
  430. :status="reportedStatus"
  431. />
  432. </div>
  433. </template>
  434. <script type="text/javascript">
  435. import Intersect from 'vue-intersect'
  436. import StatusCard from './../TimelineStatus.vue';
  437. import StatusPlaceholder from './../StatusPlaceholder.vue';
  438. import BlurHashCanvas from './../BlurhashCanvas.vue';
  439. import ContextMenu from './../../partials/post/ContextMenu.vue';
  440. import LikesModal from './../../partials/post/LikeModal.vue';
  441. import SharesModal from './../../partials/post/ShareModal.vue';
  442. import ReportModal from './../../partials/modal/ReportPost.vue';
  443. import { parseLinkHeader } from '@web3-storage/parse-link-header';
  444. export default {
  445. props: {
  446. profile: {
  447. type: Object
  448. },
  449. relationship: {
  450. type: Object
  451. }
  452. },
  453. components: {
  454. "intersect": Intersect,
  455. "status-card": StatusCard,
  456. "bh-canvas": BlurHashCanvas,
  457. "status-placeholder": StatusPlaceholder,
  458. "context-menu": ContextMenu,
  459. "likes-modal": LikesModal,
  460. "shares-modal": SharesModal,
  461. "report-modal": ReportModal
  462. },
  463. data() {
  464. return {
  465. isLoaded: false,
  466. user: {},
  467. isOwner: false,
  468. layoutIndex: 0,
  469. tabIndex: 0,
  470. ids: [],
  471. feed: [],
  472. feedLoaded: false,
  473. collections: [],
  474. collectionsLoaded: false,
  475. canLoadMore: false,
  476. max_id: 1,
  477. isIntersecting: false,
  478. postIndex: 0,
  479. showMenu: false,
  480. showLikesModal: false,
  481. likesModalPost: {},
  482. showReportModal: false,
  483. reportedStatus: {},
  484. reportedStatusId: 0,
  485. favourites: [],
  486. favouritesLoaded: false,
  487. favouritesPage: 1,
  488. canLoadMoreFavourites: false,
  489. bookmarks: [],
  490. bookmarksLoaded: false,
  491. bookmarksPage: 1,
  492. bookmarksCursor: undefined,
  493. canLoadMoreBookmarks: false,
  494. canLoadMoreCollections: false,
  495. collectionsPage: 1,
  496. isCollectionsIntersecting: false,
  497. canViewCollections: false,
  498. showSharesModal: false,
  499. sharesModalPost: {},
  500. archives: [],
  501. archivesLoaded: false,
  502. archivesPage: 1,
  503. canLoadMoreArchives: false,
  504. contextMenuPost: {}
  505. }
  506. },
  507. mounted() {
  508. this.init();
  509. },
  510. methods: {
  511. init() {
  512. this.user = window._sharedData.user;
  513. if(this.$store.state.profileLayout != 'grid') {
  514. let index = this.$store.state.profileLayout === 'masonry' ? 1 : 2;
  515. this.toggleLayout(index);
  516. }
  517. if(this.user) {
  518. this.isOwner = this.user.id == this.profile.id;
  519. if(this.isOwner) {
  520. this.canViewCollections = true;
  521. }
  522. }
  523. if(this.profile.locked) {
  524. this.privateProfileCheck();
  525. } else {
  526. if(this.profile.local) {
  527. this.canViewCollections = true;
  528. }
  529. this.fetchFeed();
  530. }
  531. },
  532. privateProfileCheck() {
  533. if(this.relationship.following || this.isOwner) {
  534. this.canViewCollections = true;
  535. this.fetchFeed();
  536. } else {
  537. this.tabIndex = 'private';
  538. this.isLoaded = true;
  539. }
  540. },
  541. fetchFeed() {
  542. axios.get('/api/pixelfed/v1/accounts/' + this.profile.id + '/statuses', {
  543. params: {
  544. limit: 9,
  545. only_media: true,
  546. min_id: 1
  547. }
  548. })
  549. .then(res => {
  550. this.tabIndex = 1;
  551. let data = res.data.filter(status => status.media_attachments.length > 0);
  552. let ids = data.map(status => status.id);
  553. this.ids = ids;
  554. this.max_id = Math.min(...ids);
  555. data.forEach(s => {
  556. this.feed.push(s);
  557. });
  558. setTimeout(() => {
  559. this.canLoadMore = res.data.length > 1;
  560. this.feedLoaded = true;
  561. }, 500);
  562. });
  563. },
  564. enterIntersect() {
  565. if(this.isIntersecting) {
  566. return;
  567. }
  568. this.isIntersecting = true;
  569. axios.get('/api/pixelfed/v1/accounts/' + this.profile.id + '/statuses', {
  570. params: {
  571. limit: 9,
  572. only_media: true,
  573. max_id: this.max_id,
  574. }
  575. })
  576. .then(res => {
  577. if(!res.data || !res.data.length) {
  578. this.canLoadMore = false;
  579. }
  580. let data = res.data
  581. .filter(status => status.media_attachments.length > 0)
  582. .filter(status => this.ids.indexOf(status.id) == -1)
  583. if(!data || !data.length) {
  584. this.canLoadMore = false;
  585. this.isIntersecting = false;
  586. return;
  587. }
  588. let filtered = data.forEach(status => {
  589. if(status.id < this.max_id) {
  590. this.max_id = status.id;
  591. } else {
  592. this.max_id--;
  593. }
  594. this.ids.push(status.id);
  595. this.feed.push(status);
  596. });
  597. this.isIntersecting = false;
  598. this.canLoadMore = res.data.length >= 1;
  599. }).catch(err => {
  600. this.canLoadMore = false;
  601. });
  602. },
  603. toggleLayout(idx, blur = false) {
  604. if(blur) {
  605. event.currentTarget.blur();
  606. }
  607. this.layoutIndex = idx;
  608. this.isIntersecting = false;
  609. },
  610. toggleTab(idx) {
  611. event.currentTarget.blur();
  612. switch(idx) {
  613. case 1:
  614. this.isIntersecting = false;
  615. this.tabIndex = 1;
  616. break;
  617. case 2:
  618. this.fetchCollections();
  619. break;
  620. case 3:
  621. this.fetchFavourites();
  622. break;
  623. case 'bookmarks':
  624. this.fetchBookmarks();
  625. break;
  626. case 'archives':
  627. this.fetchArchives();
  628. break;
  629. }
  630. },
  631. fetchCollections() {
  632. if(this.collectionsLoaded) {
  633. this.tabIndex = 2;
  634. }
  635. axios.get('/api/local/profile/collections/' + this.profile.id)
  636. .then(res => {
  637. this.collections = res.data;
  638. this.collectionsLoaded = true;
  639. this.tabIndex = 2;
  640. this.collectionsPage++;
  641. this.canLoadMoreCollections = res.data.length === 9;
  642. })
  643. },
  644. enterCollectionsIntersect() {
  645. if(this.isCollectionsIntersecting) {
  646. return;
  647. }
  648. this.isCollectionsIntersecting = true;
  649. axios.get('/api/local/profile/collections/' + this.profile.id, {
  650. params: {
  651. limit: 9,
  652. page: this.collectionsPage
  653. }
  654. })
  655. .then(res => {
  656. if(!res.data || !res.data.length) {
  657. this.canLoadMoreCollections = false;
  658. }
  659. this.collectionsLoaded = true;
  660. this.collections.push(...res.data);
  661. this.collectionsPage++;
  662. this.canLoadMoreCollections = res.data.length > 0;
  663. this.isCollectionsIntersecting = false;
  664. }).catch(err => {
  665. this.canLoadMoreCollections = false;
  666. this.isCollectionsIntersecting = false;
  667. });
  668. },
  669. fetchFavourites() {
  670. this.tabIndex = 0;
  671. axios.get('/api/pixelfed/v1/favourites')
  672. .then(res => {
  673. this.tabIndex = 3;
  674. this.favourites = res.data;
  675. this.favouritesPage++;
  676. this.favouritesLoaded = true;
  677. if(res.data.length != 0) {
  678. this.canLoadMoreFavourites = true;
  679. }
  680. })
  681. },
  682. enterFavouritesIntersect() {
  683. if(this.isIntersecting) {
  684. return;
  685. }
  686. this.isIntersecting = true;
  687. axios.get('/api/pixelfed/v1/favourites', {
  688. params: {
  689. page: this.favouritesPage,
  690. }
  691. })
  692. .then(res => {
  693. this.favourites.push(...res.data);
  694. this.favouritesPage++;
  695. this.canLoadMoreFavourites = res.data.length != 0;
  696. this.isIntersecting = false;
  697. })
  698. .catch(err => {
  699. this.canLoadMoreFavourites = false;
  700. })
  701. },
  702. fetchBookmarks() {
  703. this.tabIndex = 0;
  704. axios.get('/api/v1/bookmarks', {
  705. params: {
  706. '_pe': 1
  707. }
  708. })
  709. .then(res => {
  710. this.tabIndex = 'bookmarks';
  711. this.bookmarks = res.data;
  712. if(res.headers && res.headers.link) {
  713. const links = parseLinkHeader(res.headers.link);
  714. if(links.next) {
  715. this.bookmarksPage = links.next.cursor;
  716. this.canLoadMoreBookmarks = true;
  717. } else {
  718. this.canLoadMoreBookmarks = false;
  719. }
  720. }
  721. this.bookmarksLoaded = true;
  722. })
  723. },
  724. enterBookmarksIntersect() {
  725. if(this.isIntersecting) {
  726. return;
  727. }
  728. this.isIntersecting = true;
  729. axios.get('/api/v1/bookmarks', {
  730. params: {
  731. '_pe': 1,
  732. cursor: this.bookmarksPage,
  733. }
  734. })
  735. .then(res => {
  736. this.bookmarks.push(...res.data);
  737. if(res.headers && res.headers.link) {
  738. const links = parseLinkHeader(res.headers.link);
  739. if(links.next) {
  740. this.bookmarksPage = links.next.cursor;
  741. this.canLoadMoreBookmarks = true;
  742. } else {
  743. this.canLoadMoreBookmarks = false;
  744. }
  745. }
  746. this.isIntersecting = false;
  747. })
  748. .catch(err => {
  749. this.canLoadMoreBookmarks = false;
  750. })
  751. },
  752. fetchArchives() {
  753. this.tabIndex = 0;
  754. axios.get('/api/pixelfed/v2/statuses/archives')
  755. .then(res => {
  756. this.tabIndex = 'archives';
  757. this.archives = res.data;
  758. this.archivesPage++;
  759. this.archivesLoaded = true;
  760. if(res.data.length != 0) {
  761. this.canLoadMoreArchives = true;
  762. }
  763. })
  764. },
  765. formatCount(val) {
  766. return App.util.format.count(val);
  767. },
  768. statusUrl(s) {
  769. return '/i/web/post/' + s.id;
  770. },
  771. previewUrl(status) {
  772. return status.sensitive ? '/storage/no-preview.png?v=' + new Date().getTime() : status.media_attachments[0].url;
  773. },
  774. timeago(ts) {
  775. return App.util.format.timeAgo(ts);
  776. },
  777. likeStatus(index) {
  778. let status = this.feed[index];
  779. let state = status.favourited;
  780. let count = status.favourites_count;
  781. this.feed[index].favourites_count = count + 1;
  782. this.feed[index].favourited = !status.favourited;
  783. axios.post('/api/v1/statuses/' + status.id + '/favourite')
  784. .catch(err => {
  785. this.feed[index].favourites_count = count;
  786. this.feed[index].favourited = false;
  787. })
  788. },
  789. unlikeStatus(index) {
  790. let status = this.feed[index];
  791. let state = status.favourited;
  792. let count = status.favourites_count;
  793. this.feed[index].favourites_count = count - 1;
  794. this.feed[index].favourited = !status.favourited;
  795. axios.post('/api/v1/statuses/' + status.id + '/unfavourite')
  796. .catch(err => {
  797. this.feed[index].favourites_count = count;
  798. this.feed[index].favourited = false;
  799. })
  800. },
  801. openContextMenu(idx, type = 'feed') {
  802. switch(type) {
  803. case 'feed':
  804. this.postIndex = idx;
  805. this.contextMenuPost = this.feed[idx];
  806. break;
  807. case 'archive':
  808. this.postIndex = idx;
  809. this.contextMenuPost = this.archives[idx];
  810. break;
  811. }
  812. this.showMenu = true;
  813. this.$nextTick(() => {
  814. this.$refs.contextMenu.open();
  815. });
  816. },
  817. openLikesModal(idx) {
  818. this.postIndex = idx;
  819. this.likesModalPost = this.feed[this.postIndex];
  820. this.showLikesModal = true;
  821. this.$nextTick(() => {
  822. this.$refs.likesModal.open();
  823. });
  824. },
  825. openSharesModal(idx) {
  826. this.postIndex = idx;
  827. this.sharesModalPost = this.feed[this.postIndex];
  828. this.showSharesModal = true;
  829. this.$nextTick(() => {
  830. this.$refs.sharesModal.open();
  831. });
  832. },
  833. commitModeration(type) {
  834. let idx = this.postIndex;
  835. switch(type) {
  836. case 'addcw':
  837. this.feed[idx].sensitive = true;
  838. break;
  839. case 'remcw':
  840. this.feed[idx].sensitive = false;
  841. break;
  842. case 'unlist':
  843. this.feed.splice(idx, 1);
  844. break;
  845. case 'spammer':
  846. let id = this.feed[idx].account.id;
  847. this.feed = this.feed.filter(post => {
  848. return post.account.id != id;
  849. });
  850. break;
  851. }
  852. },
  853. counterChange(index, type) {
  854. switch(type) {
  855. case 'comment-increment':
  856. this.feed[index].reply_count = this.feed[index].reply_count + 1;
  857. break;
  858. case 'comment-decrement':
  859. this.feed[index].reply_count = this.feed[index].reply_count - 1;
  860. break;
  861. }
  862. },
  863. openCommentLikesModal(post) {
  864. this.likesModalPost = post;
  865. this.showLikesModal = true;
  866. this.$nextTick(() => {
  867. this.$refs.likesModal.open();
  868. });
  869. },
  870. shareStatus(index) {
  871. let status = this.feed[index];
  872. let state = status.reblogged;
  873. let count = status.reblogs_count;
  874. this.feed[index].reblogs_count = count + 1;
  875. this.feed[index].reblogged = !status.reblogged;
  876. axios.post('/api/v1/statuses/' + status.id + '/reblog')
  877. .catch(err => {
  878. this.feed[index].reblogs_count = count;
  879. this.feed[index].reblogged = false;
  880. })
  881. },
  882. unshareStatus(index) {
  883. let status = this.feed[index];
  884. let state = status.reblogged;
  885. let count = status.reblogs_count;
  886. this.feed[index].reblogs_count = count - 1;
  887. this.feed[index].reblogged = !status.reblogged;
  888. axios.post('/api/v1/statuses/' + status.id + '/unreblog')
  889. .catch(err => {
  890. this.feed[index].reblogs_count = count;
  891. this.feed[index].reblogged = false;
  892. })
  893. },
  894. handleReport(post) {
  895. this.reportedStatusId = post.id;
  896. this.$nextTick(() => {
  897. this.reportedStatus = post;
  898. this.$refs.reportModal.open();
  899. });
  900. },
  901. deletePost() {
  902. this.feed.splice(this.postIndex, 1);
  903. },
  904. handleArchived(id) {
  905. this.feed.splice(this.postIndex, 1);
  906. },
  907. handleUnarchived(id) {
  908. this.feed = [];
  909. this.fetchFeed();
  910. },
  911. enterArchivesIntersect() {
  912. if(this.isIntersecting) {
  913. return;
  914. }
  915. this.isIntersecting = true;
  916. axios.get('/api/pixelfed/v2/statuses/archives', {
  917. params: {
  918. page: this.archivesPage
  919. }
  920. })
  921. .then(res => {
  922. this.archives.push(...res.data);
  923. this.archivesPage++;
  924. this.canLoadMoreArchives = res.data.length != 0;
  925. this.isIntersecting = false;
  926. })
  927. .catch(err => {
  928. this.canLoadMoreArchives = false;
  929. })
  930. },
  931. handleBookmark(index) {
  932. if(!window.confirm('Are you sure you want to unbookmark this post?')) {
  933. return;
  934. }
  935. let p = this.bookmarks[index];
  936. axios.post('/i/bookmark', {
  937. item: p.id
  938. })
  939. .then(res => {
  940. this.bookmarks = this.bookmarks.map(post => {
  941. if(post.id == p.id) {
  942. post.bookmarked = false;
  943. delete post.bookmarked_at;
  944. }
  945. return post;
  946. });
  947. this.bookmarks.splice(index, 1);
  948. })
  949. .catch(err => {
  950. this.$bvToast.toast('Cannot bookmark post at this time.', {
  951. title: 'Bookmark Error',
  952. variant: 'danger',
  953. autoHideDelay: 5000
  954. });
  955. });
  956. },
  957. }
  958. }
  959. </script>
  960. <style lang="scss">
  961. .profile-feed-component {
  962. margin-top: 0;
  963. .ph-wrapper {
  964. padding: 0.25rem;
  965. .ph-item {
  966. margin: 0;
  967. padding: 0;
  968. border: none;
  969. background-color: transparent;
  970. .ph-picture {
  971. height: auto;
  972. padding-bottom: 100%;
  973. border-radius: 5px;
  974. }
  975. & > * {
  976. margin-bottom: 0;
  977. }
  978. }
  979. }
  980. .info-overlay-text-field {
  981. font-size: 13.5px;
  982. margin-bottom: 2px;
  983. @media (min-width: 768px) {
  984. font-size: 20px;
  985. margin-bottom: 15px;
  986. }
  987. }
  988. .video-overlay-badge {
  989. position: absolute;
  990. top: 10px;
  991. right: 10px;
  992. opacity: 0.6;
  993. color: var(--dark);
  994. padding-bottom: 1px;
  995. }
  996. .timestamp-overlay-badge {
  997. position: absolute;
  998. bottom: 10px;
  999. right: 10px;
  1000. opacity: 0.6;
  1001. }
  1002. .profile-nav-btns {
  1003. margin-right: 1rem;
  1004. .btn-group {
  1005. min-height: 45px;
  1006. }
  1007. .btn-link {
  1008. color: var(--text-lighter);
  1009. font-size: 14px;
  1010. border-radius: 0;
  1011. margin-right: 1rem;
  1012. font-weight: bold;
  1013. &:hover {
  1014. color: var(--text-muted);
  1015. text-decoration: none;
  1016. }
  1017. &.active {
  1018. color: var(--dark);
  1019. border-bottom: 1px solid var(--dark);
  1020. transition: border-bottom 250ms ease-in-out;
  1021. }
  1022. }
  1023. }
  1024. .layout-sort-toggle {
  1025. .btn {
  1026. border: none;
  1027. &.btn-light {
  1028. opacity: 0.4;
  1029. }
  1030. }
  1031. }
  1032. }
  1033. </style>