RemoteProfile.vue 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746
  1. <template>
  2. <div>
  3. <div v-if="relationship && relationship.blocking && warning" class="bg-white pt-3 border-bottom">
  4. <div class="container">
  5. <p class="text-center font-weight-bold">You are blocking this account</p>
  6. <p class="text-center font-weight-bold">Click <a href="#" class="cursor-pointer" @click.prevent="warning = false;">here</a> to view profile</p>
  7. </div>
  8. </div>
  9. <div v-if="loading" style="height: 80vh;" class="d-flex justify-content-center align-items-center">
  10. <img src="/img/pixelfed-icon-grey.svg" class="">
  11. </div>
  12. <div v-if="!loading && !warning" class="container">
  13. <div class="row">
  14. <div class="col-12 col-md-4 pt-5">
  15. <div class="card shadow-none border">
  16. <div class="card-header p-0 m-0">
  17. <img v-if="profile.header_bg" :src="profile.header_bg" style="width: 100%; height: 140px; object-fit: cover;">
  18. <div v-else class="bg-primary" style="width: 100%;height: 140px;"></div>
  19. </div>
  20. <div class="card-body pb-0">
  21. <div class="mt-n5 mb-3">
  22. <img class="rounded-circle p-1 border mt-n4 bg-white shadow" :src="profile.avatar" width="90px" height="90px;" onerror="this.onerror=null;this.src='/storage/avatars/default.jpg?v=0';">
  23. <span class="float-right mt-n1">
  24. <span>
  25. <button v-if="relationship && relationship.following == false" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="followProfile();">Follow</button>
  26. <button v-if="relationship && relationship.following == true" class="btn btn-outline-light py-0 px-3 mt-n1" style="font-size:13px; font-weight: 500;" @click="unfollowProfile();">Unfollow</button>
  27. </span>
  28. <span class="mx-2">
  29. <a :href="'/account/direct/t/' + profile.id" class="btn btn-outline-light btn-sm mt-n1" style="padding-top:2px;padding-bottom:1px;">
  30. <i class="far fa-comment-dots cursor-pointer" style="font-size:13px;"></i>
  31. </a>
  32. </span>
  33. <span>
  34. <button class="btn btn-outline-light btn-sm mt-n1" @click="showCtxMenu()" style="padding-top:2px;padding-bottom:1px;">
  35. <i class="fas fa-cog cursor-pointer" style="font-size:13px;"></i>
  36. </button>
  37. </span>
  38. </span>
  39. </div>
  40. <p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
  41. <p class="pl-2 font-weight-bold mb-2"><a class="text-muted" :href="profile.url" @click.prevent="urlRedirectHandler(profile.url)">{{profile.acct}}</a></p>
  42. <p class="pl-2 text-muted small d-flex justify-content-between">
  43. <span>
  44. <span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
  45. <span>Posts</span>
  46. </span>
  47. <span class="cursor-pointer" @click="followingModal()">
  48. <span class="font-weight-bold text-dark">{{profile.following_count}}</span>
  49. <span>Following</span>
  50. </span>
  51. <span class="cursor-pointer" @click="followersModal()">
  52. <span class="font-weight-bold text-dark">{{profile.followers_count}}</span>
  53. <span>Followers</span>
  54. </span>
  55. </p>
  56. <p class="pl-2 text-muted small pt-2" v-html="profile.note"></p>
  57. </div>
  58. </div>
  59. <p class="small text-lighter p-2">Last updated: <time :datetime="profile.last_fetched_at">{{timeAgo(profile.last_fetched_at, 'ago')}}</time></p>
  60. <p class="card border-left-primary card-body small py-2 text-muted font-weight-bold shadow-none border-top border-bottom border-right">You are viewing a profile from a remote server, it may not contain up-to-date information.</p>
  61. </div>
  62. <div class="col-12 col-md-8 pt-5">
  63. <div class="row">
  64. <div class="col-12" v-for="(status, index) in feed" :key="'remprop' + index">
  65. <status-card
  66. :class="{'border-top': index === 0}"
  67. :status="status" />
  68. </div>
  69. <div v-if="feed.length == 0" class="col-12 mb-2">
  70. <div class="d-flex justify-content-center align-items-center bg-white border rounded" style="height:60vh;">
  71. <div class="text-center">
  72. <p class="lead">We haven't seen any posts from this account.</p>
  73. </div>
  74. </div>
  75. </div>
  76. <div v-else class="col-12 mt-4">
  77. <p v-if="showLoadMore" class="text-center mb-0 px-0">
  78. <button @click="loadMorePosts()" class="btn btn-outline-primary btn-block font-weight-bold">
  79. <span v-if="!loadingMore">Load More</span>
  80. <span v-else>
  81. <div class="spinner-border spinner-border-sm" role="status">
  82. <span class="sr-only">Loading...</span>
  83. </div>
  84. </span>
  85. </button>
  86. </p>
  87. </div>
  88. </div>
  89. </div>
  90. </div>
  91. <b-modal
  92. v-if="profile && following"
  93. ref="followingModal"
  94. id="following-modal"
  95. hide-footer
  96. centered
  97. scrollable
  98. title="Following"
  99. body-class="list-group-flush py-3 px-0"
  100. dialog-class="follow-modal">
  101. <div v-if="!followingLoading" class="list-group" style="max-height: 60vh;">
  102. <div v-if="!following.length" class="list-group-item border-0">
  103. <p class="text-center mb-0 font-weight-bold text-muted py-5">
  104. <span class="text-dark">{{profileUsername}}</span> is not following yet</p>
  105. </div>
  106. <div v-else>
  107. <div v-if="owner == true" class="list-group-item border-0 pt-0 px-0 mt-n2 mb-3">
  108. <span class="d-flex px-4 pb-0 align-items-center">
  109. <i class="fas fa-search text-lighter"></i>
  110. <input type="text" class="form-control border-0 shadow-0 no-focus" placeholder="Search Following..." v-model="followingModalSearch" v-on:keyup="followingModalSearchHandler">
  111. </span>
  112. </div>
  113. <div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in following" :key="'following_'+index">
  114. <div class="media">
  115. <a :href="profileUrlRedirect(user)">
  116. <img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
  117. </a>
  118. <div class="media-body text-truncate">
  119. <p class="mb-0" style="font-size: 14px">
  120. <a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
  121. {{user.username}}
  122. </a>
  123. </p>
  124. <p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
  125. <span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
  126. </p>
  127. <p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
  128. {{user.display_name ? user.display_name : user.username}}
  129. </p>
  130. </div>
  131. <div v-if="owner">
  132. <a class="btn btn-outline-dark btn-sm font-weight-bold" href="#" @click.prevent="followModalAction(user.id, index, 'following')">Following</a>
  133. </div>
  134. </div>
  135. </div>
  136. <div v-if="followingModalSearch && following.length == 0" class="list-group-item border-0">
  137. <div class="list-group-item border-0 pt-5">
  138. <p class="p-3 text-center mb-0 lead">No Results Found</p>
  139. </div>
  140. </div>
  141. <div v-if="following.length > 0 && followingMore" class="list-group-item text-center" v-on:click="followingLoadMore()">
  142. <p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
  143. </div>
  144. </div>
  145. </div>
  146. <div v-else class="text-center py-5">
  147. <div class="spinner-border" role="status">
  148. <span class="sr-only">Loading...</span>
  149. </div>
  150. </div>
  151. </b-modal>
  152. <b-modal ref="followerModal"
  153. id="follower-modal"
  154. hide-footer
  155. centered
  156. scrollable
  157. title="Followers"
  158. body-class="list-group-flush py-3 px-0"
  159. dialog-class="follow-modal"
  160. >
  161. <div v-if="!followerLoading" class="list-group" style="max-height: 60vh;">
  162. <div v-if="!followers.length" class="list-group-item border-0">
  163. <p class="text-center mb-0 font-weight-bold text-muted py-5">
  164. <span class="text-dark">{{profileUsername}}</span> has no followers yet</p>
  165. </div>
  166. <div v-else>
  167. <div class="list-group-item border-0 py-1 mb-1" v-for="(user, index) in followers" :key="'follower_'+index">
  168. <div class="media mb-0">
  169. <a :href="profileUrlRedirect(user)">
  170. <img class="mr-3 rounded-circle box-shadow" :src="user.avatar" :alt="user.username + '’s avatar'" width="30px" height="30px" loading="lazy" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0'">
  171. </a>
  172. <div class="media-body mb-0">
  173. <p class="mb-0" style="font-size: 14px">
  174. <a :href="profileUrlRedirect(user)" class="font-weight-bold text-dark">
  175. {{user.username}}
  176. </a>
  177. </p>
  178. <p v-if="!user.local" class="text-muted mb-0 text-break mr-3" style="font-size: 14px" :title="user.acct" data-toggle="dropdown" data-placement="bottom">
  179. <span class="font-weight-bold">{{user.acct.split('@')[0]}}</span><span class="text-lighter">&commat;{{user.acct.split('@')[1]}}</span>
  180. </p>
  181. <p v-else class="text-muted mb-0 text-truncate" style="font-size: 14px">
  182. {{user.display_name ? user.display_name : user.username}}
  183. </p>
  184. </div>
  185. <!-- <button class="btn btn-primary font-weight-bold btn-sm py-1">FOLLOW</button> -->
  186. </div>
  187. </div>
  188. <div v-if="followers.length && followerMore" class="list-group-item text-center" v-on:click="followersLoadMore()">
  189. <p class="mb-0 small text-muted font-weight-light cursor-pointer">Load more</p>
  190. </div>
  191. </div>
  192. </div>
  193. <div v-else class="text-center py-5">
  194. <div class="spinner-border" role="status">
  195. <span class="sr-only">Loading...</span>
  196. </div>
  197. </div>
  198. </b-modal>
  199. <b-modal ref="visitorContextMenu"
  200. id="visitor-context-menu"
  201. hide-footer
  202. hide-header
  203. centered
  204. size="sm"
  205. body-class="list-group-flush p-0">
  206. <div class="list-group" v-if="relationship">
  207. <div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
  208. Copy Link
  209. </div>
  210. <div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
  211. Mute
  212. </div>
  213. <div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
  214. Unmute
  215. </div>
  216. <div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
  217. Report User
  218. </div>
  219. <div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
  220. Block
  221. </div>
  222. <div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
  223. Unblock
  224. </div>
  225. <div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
  226. Close
  227. </div>
  228. </div>
  229. </b-modal>
  230. <b-modal ref="ctxModal"
  231. id="ctx-modal"
  232. hide-header
  233. hide-footer
  234. centered
  235. rounded
  236. size="sm"
  237. body-class="list-group-flush p-0 rounded">
  238. <div class="list-group text-center">
  239. <div v-if="ctxMenuStatus && profile.id != profile.id" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuReportPost()">Report inappropriate</div>
  240. <div v-if="ctxMenuStatus && profile.id != profile.id && ctxMenuRelationship && ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-danger" @click="ctxMenuUnfollow()">Unfollow</div>
  241. <div v-if="ctxMenuStatus && profile.id != profile.id && ctxMenuRelationship && !ctxMenuRelationship.following" class="list-group-item rounded cursor-pointer font-weight-bold text-primary" @click="ctxMenuFollow()">Follow</div>
  242. <div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div>
  243. <div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
  244. <div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
  245. <div v-if="ctxMenuStatus && (profile.is_admin || profile.id == profile.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div>
  246. <div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
  247. </div>
  248. </b-modal>
  249. </div>
  250. </div>
  251. </template>
  252. <script type="text/javascript">
  253. import StatusCard from './partials/StatusCard.vue';
  254. export default {
  255. props: [
  256. 'profile-id',
  257. ],
  258. components: {
  259. StatusCard
  260. },
  261. data() {
  262. return {
  263. id: [],
  264. ids: [],
  265. user: false,
  266. profile: {},
  267. feed: [],
  268. min_id: null,
  269. max_id: null,
  270. loading: true,
  271. owner: false,
  272. layoutType: true,
  273. relationship: null,
  274. warning: false,
  275. ctxMenuStatus: false,
  276. ctxMenuRelationship: false,
  277. fetchingRemotePosts: false,
  278. showMutualFollowers: false,
  279. loadingMore: false,
  280. showLoadMore: true,
  281. followers: [],
  282. followerCursor: 1,
  283. followerMore: true,
  284. followerLoading: true,
  285. following: [],
  286. followingCursor: 1,
  287. followingMore: true,
  288. followingLoading: true,
  289. followingModalSearch: null,
  290. followingModalSearchCache: null,
  291. followingModalTab: 'following',
  292. }
  293. },
  294. beforeMount() {
  295. this.fetchRelationships();
  296. this.fetchProfile();
  297. },
  298. updated() {
  299. document.querySelectorAll('.hashtag').forEach(function(i, e) {
  300. i.href = App.util.format.rewriteLinks(i);
  301. });
  302. },
  303. methods: {
  304. fetchProfile() {
  305. axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
  306. this.user = res.data
  307. window._sharedData.curUser = res.data;
  308. window.App.util.navatar();
  309. });
  310. axios.get('/api/pixelfed/v1/accounts/' + this.profileId)
  311. .then(res => {
  312. this.profile = res.data;
  313. this.fetchPosts();
  314. });
  315. },
  316. fetchPosts() {
  317. let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
  318. axios.get(apiUrl, {
  319. params: {
  320. only_media: true,
  321. min_id: 1,
  322. }
  323. })
  324. .then(res => {
  325. let data = res.data
  326. .filter(status => status.media_attachments.length > 0);
  327. let ids = data.map(status => status.id);
  328. this.ids = ids;
  329. this.min_id = Math.max(...ids);
  330. this.max_id = Math.min(...ids);
  331. this.feed = data;
  332. this.loading = false;
  333. //this.loadSponsor();
  334. }).catch(err => {
  335. swal('Oops, something went wrong',
  336. 'Please release the page.',
  337. 'error');
  338. });
  339. },
  340. loadMorePosts() {
  341. this.loadingMore = true;
  342. let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
  343. axios.get(apiUrl, {
  344. params: {
  345. only_media: true,
  346. max_id: this.max_id,
  347. }
  348. })
  349. .then(res => {
  350. let data = res.data
  351. .filter(status => this.ids.indexOf(status.id) === -1)
  352. .filter(status => status.media_attachments.length > 0)
  353. .map(status => {
  354. return {
  355. id: status.id,
  356. caption: {
  357. text: status.content_text,
  358. html: status.content
  359. },
  360. count: {
  361. likes: status.favourites_count,
  362. shares: status.reblogs_count,
  363. comments: status.reply_count
  364. },
  365. thumb: status.media_attachments[0].url,
  366. media: status.media_attachments,
  367. timestamp: status.created_at,
  368. type: status.pf_type,
  369. url: status.url,
  370. sensitive: status.sensitive,
  371. cw: status.sensitive,
  372. spoiler_text: status.spoiler_text
  373. }
  374. });
  375. let ids = data.map(status => status.id);
  376. this.ids.push(...ids);
  377. this.max_id = Math.min(...ids);
  378. this.feed.push(...data);
  379. this.loadingMore = false;
  380. }).catch(err => {
  381. this.loadingMore = false;
  382. this.showLoadMore = false;
  383. });
  384. },
  385. fetchRelationships() {
  386. if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
  387. return;
  388. }
  389. axios.get('/api/pixelfed/v1/accounts/relationships', {
  390. params: {
  391. 'id[]': this.profileId
  392. }
  393. }).then(res => {
  394. if(res.data.length) {
  395. this.relationship = res.data[0];
  396. if(res.data[0].blocking == true) {
  397. this.loading = false;
  398. this.warning = true;
  399. }
  400. }
  401. });
  402. },
  403. postPreviewUrl(post) {
  404. return 'background: url("'+post.thumb+'");background-size:cover';
  405. },
  406. timestampFormat(timestamp) {
  407. let ts = new Date(timestamp);
  408. return ts.toDateString() + ' ' + ts.toLocaleTimeString();
  409. },
  410. remoteProfileUrl(profile) {
  411. return '/i/web/profile/_/' + profile.id;
  412. },
  413. remotePostUrl(status) {
  414. return '/i/web/post/_/' + this.profile.id + '/' + status.id;
  415. },
  416. followProfile() {
  417. axios.post('/i/follow', {
  418. item: this.profileId
  419. }).then(res => {
  420. swal('Followed', 'You are now following ' + this.profile.username +'!', 'success');
  421. this.relationship.following = true;
  422. }).catch(err => {
  423. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  424. });
  425. },
  426. unfollowProfile() {
  427. axios.post('/i/follow', {
  428. item: this.profileId
  429. }).then(res => {
  430. swal('Unfollowed', 'You are no longer following ' + this.profile.username +'.', 'warning');
  431. this.relationship.following = false;
  432. }).catch(err => {
  433. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  434. });
  435. },
  436. showCtxMenu() {
  437. this.$refs.visitorContextMenu.show();
  438. },
  439. copyProfileLink() {
  440. navigator.clipboard.writeText(window.location.href);
  441. this.$refs.visitorContextMenu.hide();
  442. },
  443. muteProfile() {
  444. let id = this.profileId;
  445. axios.post('/i/mute', {
  446. type: 'user',
  447. item: id
  448. }).then(res => {
  449. this.fetchRelationships();
  450. this.$refs.visitorContextMenu.hide();
  451. swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
  452. }).catch(err => {
  453. swal('Error', 'Something went wrong. Please try again later.', 'error');
  454. });
  455. this.$refs.visitorContextMenu.hide();
  456. },
  457. unmuteProfile() {
  458. let id = this.profileId;
  459. axios.post('/i/unmute', {
  460. type: 'user',
  461. item: id
  462. }).then(res => {
  463. this.fetchRelationships();
  464. this.$refs.visitorContextMenu.hide();
  465. swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
  466. }).catch(err => {
  467. swal('Error', 'Something went wrong. Please try again later.', 'error');
  468. });
  469. this.$refs.visitorContextMenu.hide();
  470. },
  471. blockProfile() {
  472. let id = this.profileId;
  473. axios.post('/i/block', {
  474. type: 'user',
  475. item: id
  476. }).then(res => {
  477. this.warning = true;
  478. this.fetchRelationships();
  479. this.$refs.visitorContextMenu.hide();
  480. swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
  481. }).catch(err => {
  482. swal('Error', 'Something went wrong. Please try again later.', 'error');
  483. });
  484. this.$refs.visitorContextMenu.hide();
  485. },
  486. unblockProfile() {
  487. let id = this.profileId;
  488. axios.post('/i/unblock', {
  489. type: 'user',
  490. item: id
  491. }).then(res => {
  492. this.warning = false;
  493. this.fetchRelationships();
  494. this.$refs.visitorContextMenu.hide();
  495. swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
  496. }).catch(err => {
  497. swal('Error', 'Something went wrong. Please try again later.', 'error');
  498. });
  499. this.$refs.visitorContextMenu.hide();
  500. },
  501. reportProfile() {
  502. window.location.href = '/l/i/report?type=profile&id=' + this.profileId;
  503. this.$refs.visitorContextMenu.hide();
  504. },
  505. ctxMenu(status) {
  506. this.ctxMenuStatus = status;
  507. let self = this;
  508. axios.get('/api/pixelfed/v1/accounts/relationships', {
  509. params: {
  510. 'id[]': self.profileId
  511. }
  512. }).then(res => {
  513. self.ctxMenuRelationship = res.data[0];
  514. self.$refs.ctxModal.show();
  515. });
  516. },
  517. closeCtxMenu() {
  518. this.ctxMenuStatus = false;
  519. this.ctxMenuRelationship = false;
  520. this.$refs.ctxModal.hide();
  521. },
  522. ctxMenuCopyLink() {
  523. let status = this.ctxMenuStatus;
  524. navigator.clipboard.writeText(status.url);
  525. this.closeCtxMenu();
  526. return;
  527. },
  528. ctxMenuGoToPost() {
  529. let status = this.ctxMenuStatus;
  530. window.location.href = this.statusUrl(status);
  531. this.closeCtxMenu();
  532. return;
  533. },
  534. statusUrl(status) {
  535. return '/i/web/post/_/' + this.profile.id + '/' + status.id;
  536. },
  537. deletePost(status) {
  538. if(this.user.is_admin == false) {
  539. return;
  540. }
  541. if(window.confirm('Are you sure you want to delete this post?') == false) {
  542. return;
  543. }
  544. axios.post('/i/delete', {
  545. type: 'status',
  546. item: status.id
  547. }).then(res => {
  548. this.feed = this.feed.filter(s => {
  549. return s.id != status.id;
  550. });
  551. this.$refs.ctxModal.hide();
  552. }).catch(err => {
  553. swal('Error', 'Something went wrong. Please try again later.', 'error');
  554. });
  555. },
  556. manuallyFetchRemotePosts($event) {
  557. this.fetchingRemotePosts = true;
  558. event.target.blur();
  559. swal(
  560. 'Fetching Remote Posts',
  561. 'Check back in a few minutes!',
  562. 'info'
  563. );
  564. },
  565. timeAgo(ts, suffix = false) {
  566. if(ts == null) {
  567. return 'never';
  568. }
  569. suffix = suffix ? ' ' + suffix : '';
  570. return App.util.format.timeAgo(ts) + suffix;
  571. },
  572. urlRedirectHandler(url) {
  573. let p = new URL(url);
  574. let path = '';
  575. if(p.hostname == window.location.hostname) {
  576. path = url;
  577. } else {
  578. path = '/i/redirect?url=';
  579. path += encodeURI(url);
  580. }
  581. window.location.href = path;
  582. },
  583. followingModal() {
  584. if(this.followingCursor > 1) {
  585. this.$refs.followingModal.show();
  586. return;
  587. } else {
  588. axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/following', {
  589. params: {
  590. page: this.followingCursor
  591. }
  592. })
  593. .then(res => {
  594. this.following = res.data;
  595. this.followingModalSearchCache = res.data;
  596. this.followingCursor++;
  597. if(res.data.length < 10) {
  598. this.followingMore = false;
  599. }
  600. this.followingLoading = false;
  601. });
  602. this.$refs.followingModal.show();
  603. return;
  604. }
  605. },
  606. followersModal() {
  607. if(this.followerCursor > 1) {
  608. this.$refs.followerModal.show();
  609. return;
  610. } else {
  611. axios.get('/api/pixelfed/v1/accounts/'+this.profileId+'/followers', {
  612. params: {
  613. page: this.followerCursor
  614. }
  615. })
  616. .then(res => {
  617. this.followers.push(...res.data);
  618. this.followerCursor++;
  619. if(res.data.length < 10) {
  620. this.followerMore = false;
  621. }
  622. this.followerLoading = false;
  623. })
  624. this.$refs.followerModal.show();
  625. return;
  626. }
  627. },
  628. followingLoadMore() {
  629. axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/following', {
  630. params: {
  631. page: this.followingCursor,
  632. fbu: this.followingModalSearch
  633. }
  634. })
  635. .then(res => {
  636. if(res.data.length > 0) {
  637. this.following.push(...res.data);
  638. this.followingCursor++;
  639. this.followingModalSearchCache = this.following;
  640. }
  641. if(res.data.length < 10) {
  642. this.followingModalSearchCache = this.following;
  643. this.followingMore = false;
  644. }
  645. });
  646. },
  647. followersLoadMore() {
  648. axios.get('/api/pixelfed/v1/accounts/'+this.profile.id+'/followers', {
  649. params: {
  650. page: this.followerCursor
  651. }
  652. })
  653. .then(res => {
  654. if(res.data.length > 0) {
  655. this.followers.push(...res.data);
  656. this.followerCursor++;
  657. }
  658. if(res.data.length < 10) {
  659. this.followerMore = false;
  660. }
  661. });
  662. },
  663. profileUrlRedirect(profile) {
  664. if(profile.local == true) {
  665. return profile.url;
  666. }
  667. return '/i/web/profile/_/' + profile.id;
  668. },
  669. followingModalSearchHandler() {
  670. let self = this;
  671. let q = this.followingModalSearch;
  672. if(q.length == 0) {
  673. this.following = this.followingModalSearchCache;
  674. this.followingModalSearch = null;
  675. }
  676. if(q.length > 0) {
  677. let url = '/api/pixelfed/v1/accounts/' +
  678. self.profileId + '/following?page=1&fbu=' +
  679. q;
  680. axios.get(url).then(res => {
  681. this.following = res.data;
  682. }).catch(err => {
  683. self.following = self.followingModalSearchCache;
  684. self.followingModalSearch = null;
  685. });
  686. }
  687. },
  688. }
  689. }
  690. </script>
  691. <style type="text/css" scoped>
  692. @media (min-width: 1200px) {
  693. .container {
  694. max-width: 1050px;
  695. }
  696. }
  697. </style>