RemoteProfile.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  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;">
  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>
  48. <span class="font-weight-bold text-dark">{{profile.following_count}}</span>
  49. <span>Following</span>
  50. </span>
  51. <span>
  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. </div>
  61. <div class="col-12 col-md-8 pt-5">
  62. <div class="row">
  63. <div class="col-12" v-for="(status, index) in feed" :key="'remprop' + index">
  64. <status-card
  65. :class="{'border-top': index === 0}"
  66. :status="status" />
  67. </div>
  68. <div v-if="feed.length == 0" class="col-12 mb-2">
  69. <div class="d-flex justify-content-center align-items-center bg-white border rounded" style="height:60vh;">
  70. <div class="text-center">
  71. <p class="lead">We haven't seen any posts from this account.</p>
  72. </div>
  73. </div>
  74. </div>
  75. <div v-else class="col-12 mt-4">
  76. <p v-if="showLoadMore" class="text-center mb-0 px-0">
  77. <button @click="loadMorePosts()" class="btn btn-outline-primary btn-block font-weight-bold">
  78. <span v-if="!loadingMore">Load More</span>
  79. <span v-else>
  80. <div class="spinner-border spinner-border-sm" role="status">
  81. <span class="sr-only">Loading...</span>
  82. </div>
  83. </span>
  84. </button>
  85. </p>
  86. </div>
  87. </div>
  88. </div>
  89. </div>
  90. <b-modal ref="visitorContextMenu"
  91. id="visitor-context-menu"
  92. hide-footer
  93. hide-header
  94. centered
  95. size="sm"
  96. body-class="list-group-flush p-0">
  97. <div class="list-group" v-if="relationship">
  98. <div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
  99. Copy Link
  100. </div>
  101. <div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
  102. Mute
  103. </div>
  104. <div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
  105. Unmute
  106. </div>
  107. <div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
  108. Report User
  109. </div>
  110. <div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
  111. Block
  112. </div>
  113. <div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
  114. Unblock
  115. </div>
  116. <div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
  117. Close
  118. </div>
  119. </div>
  120. </b-modal>
  121. <b-modal ref="ctxModal"
  122. id="ctx-modal"
  123. hide-header
  124. hide-footer
  125. centered
  126. rounded
  127. size="sm"
  128. body-class="list-group-flush p-0 rounded">
  129. <div class="list-group text-center">
  130. <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>
  131. <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>
  132. <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>
  133. <div class="list-group-item rounded cursor-pointer" @click="ctxMenuGoToPost()">Go to post</div>
  134. <div class="list-group-item rounded cursor-pointer" @click="ctxMenuCopyLink()">Copy Link</div>
  135. <div v-if="profile && profile.is_admin == true" class="list-group-item rounded cursor-pointer" @click="ctxModMenuShow()">Moderation Tools</div>
  136. <div v-if="ctxMenuStatus && (profile.is_admin || profile.id == profile.id)" class="list-group-item rounded cursor-pointer" @click="deletePost(ctxMenuStatus)">Delete</div>
  137. <div class="list-group-item rounded cursor-pointer text-lighter" @click="closeCtxMenu()">Cancel</div>
  138. </div>
  139. </b-modal>
  140. </div>
  141. </div>
  142. </template>
  143. <script type="text/javascript">
  144. import StatusCard from './partials/StatusCard.vue';
  145. export default {
  146. props: [
  147. 'profile-id',
  148. ],
  149. components: {
  150. StatusCard
  151. },
  152. data() {
  153. return {
  154. id: [],
  155. ids: [],
  156. user: false,
  157. profile: {},
  158. feed: [],
  159. min_id: null,
  160. max_id: null,
  161. loading: true,
  162. owner: false,
  163. layoutType: true,
  164. relationship: null,
  165. warning: false,
  166. ctxMenuStatus: false,
  167. ctxMenuRelationship: false,
  168. fetchingRemotePosts: false,
  169. showMutualFollowers: false,
  170. loadingMore: false,
  171. showLoadMore: true
  172. }
  173. },
  174. beforeMount() {
  175. this.fetchRelationships();
  176. this.fetchProfile();
  177. },
  178. updated() {
  179. document.querySelectorAll('.hashtag').forEach(function(i, e) {
  180. i.href = App.util.format.rewriteLinks(i);
  181. });
  182. },
  183. methods: {
  184. fetchProfile() {
  185. axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
  186. this.user = res.data
  187. window._sharedData.curUser = res.data;
  188. window.App.util.navatar();
  189. });
  190. axios.get('/api/pixelfed/v1/accounts/' + this.profileId)
  191. .then(res => {
  192. this.profile = res.data;
  193. this.fetchPosts();
  194. });
  195. },
  196. fetchPosts() {
  197. let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
  198. axios.get(apiUrl, {
  199. params: {
  200. only_media: true,
  201. min_id: 1,
  202. }
  203. })
  204. .then(res => {
  205. let data = res.data
  206. .filter(status => status.media_attachments.length > 0);
  207. let ids = data.map(status => status.id);
  208. this.ids = ids;
  209. this.min_id = Math.max(...ids);
  210. this.max_id = Math.min(...ids);
  211. this.feed = data;
  212. this.loading = false;
  213. //this.loadSponsor();
  214. }).catch(err => {
  215. swal('Oops, something went wrong',
  216. 'Please release the page.',
  217. 'error');
  218. });
  219. },
  220. loadMorePosts() {
  221. this.loadingMore = true;
  222. let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
  223. axios.get(apiUrl, {
  224. params: {
  225. only_media: true,
  226. max_id: this.max_id,
  227. }
  228. })
  229. .then(res => {
  230. let data = res.data
  231. .filter(status => this.ids.indexOf(status.id) === -1)
  232. .filter(status => status.media_attachments.length > 0)
  233. .map(status => {
  234. return {
  235. id: status.id,
  236. caption: {
  237. text: status.content_text,
  238. html: status.content
  239. },
  240. count: {
  241. likes: status.favourites_count,
  242. shares: status.reblogs_count,
  243. comments: status.reply_count
  244. },
  245. thumb: status.media_attachments[0].url,
  246. media: status.media_attachments,
  247. timestamp: status.created_at,
  248. type: status.pf_type,
  249. url: status.url,
  250. sensitive: status.sensitive,
  251. cw: status.sensitive,
  252. spoiler_text: status.spoiler_text
  253. }
  254. });
  255. let ids = data.map(status => status.id);
  256. this.ids.push(...ids);
  257. this.max_id = Math.min(...ids);
  258. this.feed.push(...data);
  259. this.loadingMore = false;
  260. }).catch(err => {
  261. this.loadingMore = false;
  262. this.showLoadMore = false;
  263. });
  264. },
  265. fetchRelationships() {
  266. if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
  267. return;
  268. }
  269. axios.get('/api/pixelfed/v1/accounts/relationships', {
  270. params: {
  271. 'id[]': this.profileId
  272. }
  273. }).then(res => {
  274. if(res.data.length) {
  275. this.relationship = res.data[0];
  276. if(res.data[0].blocking == true) {
  277. this.loading = false;
  278. this.warning = true;
  279. }
  280. }
  281. });
  282. },
  283. postPreviewUrl(post) {
  284. return 'background: url("'+post.thumb+'");background-size:cover';
  285. },
  286. timestampFormat(timestamp) {
  287. let ts = new Date(timestamp);
  288. return ts.toDateString() + ' ' + ts.toLocaleTimeString();
  289. },
  290. remoteProfileUrl(profile) {
  291. return '/i/web/profile/_/' + profile.id;
  292. },
  293. remotePostUrl(status) {
  294. return '/i/web/post/_/' + this.profile.id + '/' + status.id;
  295. },
  296. followProfile() {
  297. axios.post('/i/follow', {
  298. item: this.profileId
  299. }).then(res => {
  300. swal('Followed', 'You are now following ' + this.profile.username +'!', 'success');
  301. this.relationship.following = true;
  302. }).catch(err => {
  303. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  304. });
  305. },
  306. unfollowProfile() {
  307. axios.post('/i/follow', {
  308. item: this.profileId
  309. }).then(res => {
  310. swal('Unfollowed', 'You are no longer following ' + this.profile.username +'.', 'warning');
  311. this.relationship.following = false;
  312. }).catch(err => {
  313. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  314. });
  315. },
  316. showCtxMenu() {
  317. this.$refs.visitorContextMenu.show();
  318. },
  319. copyProfileLink() {
  320. navigator.clipboard.writeText(window.location.href);
  321. this.$refs.visitorContextMenu.hide();
  322. },
  323. muteProfile() {
  324. let id = this.profileId;
  325. axios.post('/i/mute', {
  326. type: 'user',
  327. item: id
  328. }).then(res => {
  329. this.fetchRelationships();
  330. this.$refs.visitorContextMenu.hide();
  331. swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
  332. }).catch(err => {
  333. swal('Error', 'Something went wrong. Please try again later.', 'error');
  334. });
  335. this.$refs.visitorContextMenu.hide();
  336. },
  337. unmuteProfile() {
  338. let id = this.profileId;
  339. axios.post('/i/unmute', {
  340. type: 'user',
  341. item: id
  342. }).then(res => {
  343. this.fetchRelationships();
  344. this.$refs.visitorContextMenu.hide();
  345. swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
  346. }).catch(err => {
  347. swal('Error', 'Something went wrong. Please try again later.', 'error');
  348. });
  349. this.$refs.visitorContextMenu.hide();
  350. },
  351. blockProfile() {
  352. let id = this.profileId;
  353. axios.post('/i/block', {
  354. type: 'user',
  355. item: id
  356. }).then(res => {
  357. this.warning = true;
  358. this.fetchRelationships();
  359. this.$refs.visitorContextMenu.hide();
  360. swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
  361. }).catch(err => {
  362. swal('Error', 'Something went wrong. Please try again later.', 'error');
  363. });
  364. this.$refs.visitorContextMenu.hide();
  365. },
  366. unblockProfile() {
  367. let id = this.profileId;
  368. axios.post('/i/unblock', {
  369. type: 'user',
  370. item: id
  371. }).then(res => {
  372. this.warning = false;
  373. this.fetchRelationships();
  374. this.$refs.visitorContextMenu.hide();
  375. swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
  376. }).catch(err => {
  377. swal('Error', 'Something went wrong. Please try again later.', 'error');
  378. });
  379. this.$refs.visitorContextMenu.hide();
  380. },
  381. reportProfile() {
  382. window.location.href = '/l/i/report?type=profile&id=' + this.profileId;
  383. this.$refs.visitorContextMenu.hide();
  384. },
  385. ctxMenu(status) {
  386. this.ctxMenuStatus = status;
  387. let self = this;
  388. axios.get('/api/pixelfed/v1/accounts/relationships', {
  389. params: {
  390. 'id[]': self.profileId
  391. }
  392. }).then(res => {
  393. self.ctxMenuRelationship = res.data[0];
  394. self.$refs.ctxModal.show();
  395. });
  396. },
  397. closeCtxMenu() {
  398. this.ctxMenuStatus = false;
  399. this.ctxMenuRelationship = false;
  400. this.$refs.ctxModal.hide();
  401. },
  402. ctxMenuCopyLink() {
  403. let status = this.ctxMenuStatus;
  404. navigator.clipboard.writeText(status.url);
  405. this.closeCtxMenu();
  406. return;
  407. },
  408. ctxMenuGoToPost() {
  409. let status = this.ctxMenuStatus;
  410. window.location.href = this.statusUrl(status);
  411. this.closeCtxMenu();
  412. return;
  413. },
  414. statusUrl(status) {
  415. return '/i/web/post/_/' + this.profile.id + '/' + status.id;
  416. },
  417. deletePost(status) {
  418. if(this.user.is_admin == false) {
  419. return;
  420. }
  421. if(window.confirm('Are you sure you want to delete this post?') == false) {
  422. return;
  423. }
  424. axios.post('/i/delete', {
  425. type: 'status',
  426. item: status.id
  427. }).then(res => {
  428. this.feed = this.feed.filter(s => {
  429. return s.id != status.id;
  430. });
  431. this.$refs.ctxModal.hide();
  432. }).catch(err => {
  433. swal('Error', 'Something went wrong. Please try again later.', 'error');
  434. });
  435. },
  436. manuallyFetchRemotePosts($event) {
  437. this.fetchingRemotePosts = true;
  438. event.target.blur();
  439. swal(
  440. 'Fetching Remote Posts',
  441. 'Check back in a few minutes!',
  442. 'info'
  443. );
  444. },
  445. timeAgo(ts, suffix = false) {
  446. if(ts == null) {
  447. return 'never';
  448. }
  449. suffix = suffix ? ' ' + suffix : '';
  450. return App.util.format.timeAgo(ts) + suffix;
  451. },
  452. urlRedirectHandler(url) {
  453. let p = new URL(url);
  454. let path = '';
  455. if(p.hostname == window.location.hostname) {
  456. path = url;
  457. } else {
  458. path = '/i/redirect?url=';
  459. path += encodeURI(url);
  460. }
  461. window.location.href = path;
  462. }
  463. }
  464. }
  465. </script>
  466. <style type="text/css" scoped>
  467. @media (min-width: 1200px) {
  468. .container {
  469. max-width: 1050px;
  470. }
  471. }
  472. </style>