RemoteProfile.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375
  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" style="margin-top:40px;">
  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="ml-3">
  29. <button class="btn btn-outline-light btn-sm mt-n1" @click="showCtxMenu()" style="padding-top:2px;padding-bottom:1px;">
  30. <i class="fas fa-cog cursor-pointer" style="font-size:13px;"></i>
  31. </button>
  32. </span>
  33. </span>
  34. </div>
  35. <p class="pl-2 h4 font-weight-bold mb-1">{{profile.display_name}}</p>
  36. <p class="pl-2 font-weight-bold mb-1 text-muted">{{profile.acct}}</p>
  37. <p class="pl-2 text-muted small" v-html="profile.note"></p>
  38. <p class="pl-2 text-muted small d-flex justify-content-between">
  39. <span>
  40. <span class="font-weight-bold text-dark">{{profile.statuses_count}}</span>
  41. <span>Posts</span>
  42. </span>
  43. <span>
  44. <span class="font-weight-bold text-dark">{{profile.following_count}}</span>
  45. <span>Following</span>
  46. </span>
  47. <span>
  48. <span class="font-weight-bold text-dark">{{profile.followers_count}}</span>
  49. <span>Followers</span>
  50. </span>
  51. </p>
  52. </div>
  53. </div>
  54. </div>
  55. <div class="col-12 col-md-8 pt-5">
  56. <div class="row">
  57. <div class="col-12 text-center mb-2">
  58. <div class="custom-control custom-switch">
  59. <label :class="layoutType ? ' pr-5 font-weight-bold text-lighter' : 'pr-5 font-weight-bold text-dark'" @click="layoutType = !layoutType">Grid</label>
  60. <input type="checkbox" class="custom-control-input" id="customSwitch1" v-model="layoutType">
  61. <label :class="!layoutType ? 'pl-2 custom-control-label font-weight-bold text-lighter' : 'pl-2 custom-control-label font-weight-bold text-dark'" for="customSwitch1">Feed</label>
  62. </div>
  63. </div>
  64. <div v-if="layoutType == false" class="col-12 col-md-4 mb-3 d-flex justify-content-center align-items-center" v-for="(post, index) in feed" :key="'remprop' + index">
  65. <a :href="remotePostUrl(post)">
  66. <img :src="post.thumb" class="img-fluid rounded border">
  67. </a>
  68. </div>
  69. <div v-if="layoutType == true" class="col-12 mb-2" v-for="(status, index) in feed" :key="'remprop' + index">
  70. <div class="card mb-sm-4 status-card card-md-rounded-0 shadow-none border cursor-pointer">
  71. <div class="card-header d-inline-flex align-items-center bg-white">
  72. <img v-bind:src="profile.avatar" width="38px" height="38px" style="border-radius: 38px;" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=2'">
  73. <div class="pl-2">
  74. <span class="username font-weight-bold text-dark">{{profile.username}}
  75. </span>
  76. </div>
  77. </div>
  78. <div class="card-body p-0">
  79. <a :href="status.url">
  80. <img v-once :src="status.thumb" class="w-100 h-100">
  81. </a>
  82. </div>
  83. <div class="card-body">
  84. <div class="caption">
  85. <p class="mb-2 read-more" style="overflow: hidden;">
  86. <span class="username font-weight-bold">
  87. <bdi><span class="text-dark">{{profile.username}}</span></bdi>
  88. </span>
  89. <span class="status-content" v-html="status.caption.html"></span>
  90. </p>
  91. </div>
  92. <div class="timestamp mt-2">
  93. <p class="small text-uppercase mb-0">
  94. <a :href="remotePostUrl(status)" class="text-muted">
  95. <timeago :datetime="status.timestamp" :auto-update="90" :converter-options="{includeSeconds:true}" :title="timestampFormat(status.timestamp)" v-b-tooltip.hover.bottom></timeago>
  96. </a>
  97. </p>
  98. </div>
  99. </div>
  100. </div>
  101. </div>
  102. <!-- <div class="col-12 mt-4">
  103. <p class="text-center mb-0 px-0"><button class="btn btn-outline-primary btn-block font-weight-bold">Load More</button></p>
  104. </div> -->
  105. </div>
  106. </div>
  107. </div>
  108. <b-modal ref="visitorContextMenu"
  109. id="visitor-context-menu"
  110. hide-footer
  111. hide-header
  112. centered
  113. size="sm"
  114. body-class="list-group-flush p-0">
  115. <div class="list-group" v-if="relationship">
  116. <div class="list-group-item cursor-pointer text-center rounded text-dark" @click="copyProfileLink">
  117. Copy Link
  118. </div>
  119. <div v-if="user && !owner && !relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="muteProfile">
  120. Mute
  121. </div>
  122. <div v-if="user && !owner && relationship.muting" class="list-group-item cursor-pointer text-center rounded" @click="unmuteProfile">
  123. Unmute
  124. </div>
  125. <div v-if="user && !owner" class="list-group-item cursor-pointer text-center rounded text-dark" @click="reportProfile">
  126. Report User
  127. </div>
  128. <div v-if="user && !owner && !relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="blockProfile">
  129. Block
  130. </div>
  131. <div v-if="user && !owner && relationship.blocking" class="list-group-item cursor-pointer text-center rounded text-dark" @click="unblockProfile">
  132. Unblock
  133. </div>
  134. <div class="list-group-item cursor-pointer text-center rounded text-muted" @click="$refs.visitorContextMenu.hide()">
  135. Close
  136. </div>
  137. </div>
  138. </b-modal>
  139. </div>
  140. </div>
  141. </template>
  142. <script type="text/javascript">
  143. export default {
  144. props: [
  145. 'profile-id',
  146. ],
  147. data() {
  148. return {
  149. id: [],
  150. user: false,
  151. profile: {},
  152. feed: [],
  153. min_id: null,
  154. max_id: null,
  155. loading: true,
  156. owner: false,
  157. layoutType: false,
  158. relationship: null,
  159. warning: false,
  160. }
  161. },
  162. beforeMount() {
  163. this.fetchRelationships();
  164. this.fetchProfile();
  165. },
  166. methods: {
  167. fetchProfile() {
  168. axios.get('/api/pixelfed/v1/accounts/verify_credentials').then(res => {
  169. this.user = res.data
  170. });
  171. axios.get('/api/pixelfed/v1/accounts/' + this.profileId)
  172. .then(res => {
  173. this.profile = res.data;
  174. this.fetchPosts();
  175. });
  176. },
  177. fetchPosts() {
  178. let apiUrl = '/api/pixelfed/v1/accounts/' + this.profileId + '/statuses';
  179. axios.get(apiUrl, {
  180. params: {
  181. only_media: true,
  182. min_id: 1,
  183. }
  184. })
  185. .then(res => {
  186. let data = res.data
  187. .filter(status => status.media_attachments.length > 0)
  188. .map(status => {
  189. return {
  190. id: status.id,
  191. caption: {
  192. text: status.content_text,
  193. html: status.content
  194. },
  195. count: {
  196. likes: status.favourites_count,
  197. shares: status.reblogs_count,
  198. comments: status.reply_count
  199. },
  200. thumb: status.media_attachments[0].preview_url,
  201. media: status.media_attachments,
  202. timestamp: status.created_at,
  203. type: status.pf_type,
  204. url: status.url
  205. }
  206. });
  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. fetchRelationships() {
  221. if(document.querySelectorAll('body')[0].classList.contains('loggedIn') == false) {
  222. return;
  223. }
  224. axios.get('/api/pixelfed/v1/accounts/relationships', {
  225. params: {
  226. 'id[]': this.profileId
  227. }
  228. }).then(res => {
  229. if(res.data.length) {
  230. this.relationship = res.data[0];
  231. if(res.data[0].blocking == true) {
  232. this.loading = false;
  233. this.warning = true;
  234. }
  235. }
  236. });
  237. },
  238. postPreviewUrl(post) {
  239. return 'background: url("'+post.thumb+'");background-size:cover';
  240. },
  241. timestampFormat(timestamp) {
  242. let ts = new Date(timestamp);
  243. return ts.toDateString() + ' ' + ts.toLocaleTimeString();
  244. },
  245. remoteProfileUrl(profile) {
  246. return '/i/web/profile/_/' + profile.id;
  247. },
  248. remotePostUrl(status) {
  249. return '/i/web/post/_/' + this.profile.id + '/' + status.id;
  250. },
  251. followProfile() {
  252. axios.post('/i/follow', {
  253. item: this.profileId
  254. }).then(res => {
  255. swal('Followed', 'You are now following ' + this.profile.username +'!', 'success');
  256. this.relationship.following = true;
  257. }).catch(err => {
  258. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  259. });
  260. },
  261. unfollowProfile() {
  262. axios.post('/i/follow', {
  263. item: this.profileId
  264. }).then(res => {
  265. swal('Unfollowed', 'You are no longer following ' + this.profile.username +'.', 'warning');
  266. this.relationship.following = false;
  267. }).catch(err => {
  268. swal('Oops!', 'Something went wrong, please try again later.', 'error');
  269. });
  270. },
  271. showCtxMenu() {
  272. this.$refs.visitorContextMenu.show();
  273. },
  274. copyProfileLink() {
  275. navigator.clipboard.writeText(window.location.href);
  276. this.$refs.visitorContextMenu.hide();
  277. },
  278. muteProfile() {
  279. let id = this.profileId;
  280. axios.post('/i/mute', {
  281. type: 'user',
  282. item: id
  283. }).then(res => {
  284. this.fetchRelationships();
  285. this.$refs.visitorContextMenu.hide();
  286. swal('Success', 'You have successfully muted ' + this.profile.acct, 'success');
  287. }).catch(err => {
  288. swal('Error', 'Something went wrong. Please try again later.', 'error');
  289. });
  290. this.$refs.visitorContextMenu.hide();
  291. },
  292. unmuteProfile() {
  293. let id = this.profileId;
  294. axios.post('/i/unmute', {
  295. type: 'user',
  296. item: id
  297. }).then(res => {
  298. this.fetchRelationships();
  299. this.$refs.visitorContextMenu.hide();
  300. swal('Success', 'You have successfully unmuted ' + this.profile.acct, 'success');
  301. }).catch(err => {
  302. swal('Error', 'Something went wrong. Please try again later.', 'error');
  303. });
  304. this.$refs.visitorContextMenu.hide();
  305. },
  306. blockProfile() {
  307. let id = this.profileId;
  308. axios.post('/i/block', {
  309. type: 'user',
  310. item: id
  311. }).then(res => {
  312. this.warning = true;
  313. this.fetchRelationships();
  314. this.$refs.visitorContextMenu.hide();
  315. swal('Success', 'You have successfully blocked ' + this.profile.acct, 'success');
  316. }).catch(err => {
  317. swal('Error', 'Something went wrong. Please try again later.', 'error');
  318. });
  319. this.$refs.visitorContextMenu.hide();
  320. },
  321. unblockProfile() {
  322. let id = this.profileId;
  323. axios.post('/i/unblock', {
  324. type: 'user',
  325. item: id
  326. }).then(res => {
  327. this.warning = false;
  328. this.fetchRelationships();
  329. this.$refs.visitorContextMenu.hide();
  330. swal('Success', 'You have successfully unblocked ' + this.profile.acct, 'success');
  331. }).catch(err => {
  332. swal('Error', 'Something went wrong. Please try again later.', 'error');
  333. });
  334. this.$refs.visitorContextMenu.hide();
  335. },
  336. reportProfile() {
  337. window.location.href = '/l/i/report?type=profile&id=' + this.profileId;
  338. this.$refs.visitorContextMenu.hide();
  339. }
  340. }
  341. }
  342. </script>
  343. <style type="text/css" scoped>
  344. @media (min-width: 1200px) {
  345. .container {
  346. max-width: 1050px;
  347. }
  348. }
  349. </style>