PostHeader.vue 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <template>
  2. <div>
  3. <div v-if="isReblog" class="card-header bg-light border-0" style="border-top-left-radius: 15px;border-top-right-radius: 15px;">
  4. <div class="media align-items-center" style="height:10px;">
  5. <a :href="reblogAccount.url" class="mx-2" @click.prevent="goToProfileById(reblogAccount.id)">
  6. <img :src="reblogAccount.avatar" style="border-radius:10px;" width="24" height="24" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
  7. </a>
  8. <div style="font-size:12px;font-weight:bold">
  9. <i class="far fa-retweet text-warning mr-1"></i> Reblogged by <a :href="reblogAccount.url" class="text-dark" @click.prevent="goToProfileById(reblogAccount.id)">&commat;{{ reblogAccount.acct }}</a>
  10. </div>
  11. </div>
  12. </div>
  13. <div class="card-header border-0" style="border-top-left-radius: 15px;border-top-right-radius: 15px;">
  14. <div class="media align-items-center">
  15. <a :href="status.account.url" @click.prevent="goToProfile()" style="margin-right: 10px;">
  16. <img :src="getStatusAvatar()" style="border-radius:15px;" width="44" height="44" onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
  17. </a>
  18. <div class="media-body">
  19. <p class="font-weight-bold username">
  20. <a :href="status.account.url" class="text-dark" :id="'apop_'+status.id" @click.prevent="goToProfile">
  21. {{ status.account.acct }}
  22. </a>
  23. <b-popover :target="'apop_'+status.id" triggers="hover" placement="bottom" custom-class="shadow border-0 rounded-px">
  24. <profile-hover-card
  25. :profile="status.account"
  26. v-on:follow="follow"
  27. v-on:unfollow="unfollow" />
  28. </b-popover>
  29. </p>
  30. <p class="text-lighter mb-0" style="font-size: 13px;">
  31. <span v-if="status.account.is_admin" class="d-none d-md-inline-block">
  32. <span class="badge badge-light text-danger user-select-none" title="Admin account">ADMIN</span>
  33. <span class="mx-1 text-lighter">·</span>
  34. </span>
  35. <a class="timestamp text-lighter" :href="status.url" @click.prevent="goToPost()" :title="status.created_at">
  36. {{ timeago(status.created_at) }}
  37. </a>
  38. <span v-if="config.ab.pue && status.hasOwnProperty('edited_at') && status.edited_at">
  39. <span class="mx-1 text-lighter">·</span>
  40. <a class="text-lighter" href="#" @click.prevent="openEditModal">Edited</a>
  41. </span>
  42. <span class="mx-1 text-lighter">·</span>
  43. <span class="visibility text-lighter" :title="scopeTitle(status.visibility)"><i :class="scopeIcon(status.visibility)"></i></span>
  44. <span v-if="status.place && status.place.hasOwnProperty('name')" class="d-none d-md-inline-block">
  45. <span class="mx-1 text-lighter">·</span>
  46. <span class="location text-lighter"><i class="far fa-map-marker-alt"></i> {{ status.place.name }}, {{ status.place.country }}</span>
  47. </span>
  48. </p>
  49. </div>
  50. <button v-if="!useDropdownMenu" class="btn btn-link text-lighter" @click="openMenu">
  51. <i class="far fa-ellipsis-v fa-lg"></i>
  52. </button>
  53. <b-dropdown
  54. v-else
  55. no-caret
  56. right
  57. variant="link"
  58. toggle-class="text-lighter"
  59. html="<i class='far fa-ellipsis-v fa-lg px-3'></i>"
  60. >
  61. <b-dropdown-item>
  62. <p class="mb-0 font-weight-bold">{{ $t('menu.viewPost') }}</p>
  63. </b-dropdown-item>
  64. <b-dropdown-item>
  65. <p class="mb-0 font-weight-bold">{{ $t('common.copyLink') }}</p>
  66. </b-dropdown-item>
  67. <b-dropdown-item v-if="status.local">
  68. <p class="mb-0 font-weight-bold">{{ $t('menu.embed') }}</p>
  69. </b-dropdown-item>
  70. <b-dropdown-divider v-if="!owner"></b-dropdown-divider>
  71. <b-dropdown-item v-if="!owner">
  72. <p class="mb-0 font-weight-bold">{{ $t('menu.report') }}</p>
  73. <p class="small text-muted mb-0">Report content that violate our rules</p>
  74. </b-dropdown-item>
  75. <b-dropdown-item v-if="!owner && status.hasOwnProperty('relationship')">
  76. <p class="mb-0 font-weight-bold">{{ status.relationship.muting ? 'Unmute' : 'Mute' }}</p>
  77. <p class="small text-muted mb-0">Hide posts from this account in your feeds</p>
  78. </b-dropdown-item>
  79. <b-dropdown-item v-if="!owner && status.hasOwnProperty('relationship')">
  80. <p class="mb-0 font-weight-bold text-danger">{{ status.relationship.blocking ? 'Unblock' : 'Block' }}</p>
  81. <p class="small text-muted mb-0">Restrict all content from this account</p>
  82. </b-dropdown-item>
  83. <b-dropdown-divider v-if="owner || admin"></b-dropdown-divider>
  84. <b-dropdown-item v-if="owner || admin">
  85. <p class="mb-0 font-weight-bold text-danger">
  86. {{ $t('common.delete') }}
  87. </p>
  88. </b-dropdown-item>
  89. </b-dropdown>
  90. </div>
  91. <edit-history-modal ref="editModal" :status="status" />
  92. </div>
  93. </div>
  94. </template>
  95. <script type="text/javascript">
  96. import ProfileHoverCard from './../profile/ProfileHoverCard.vue';
  97. import EditHistoryModal from './EditHistoryModal.vue';
  98. export default {
  99. props: {
  100. status: {
  101. type: Object
  102. },
  103. profile: {
  104. type: Object
  105. },
  106. useDropdownMenu: {
  107. type: Boolean,
  108. default: false
  109. },
  110. isReblog: {
  111. type: Boolean,
  112. default: false
  113. },
  114. reblogAccount: {
  115. type: Object
  116. }
  117. },
  118. components: {
  119. "profile-hover-card": ProfileHoverCard,
  120. "edit-history-modal": EditHistoryModal
  121. },
  122. data() {
  123. return {
  124. config: window.App.config,
  125. menuLoading: true,
  126. owner: false,
  127. admin: false,
  128. license: false
  129. }
  130. },
  131. methods: {
  132. timeago(ts) {
  133. let short = App.util.format.timeAgo(ts);
  134. if(
  135. short.endsWith('s') ||
  136. short.endsWith('m') ||
  137. short.endsWith('h')
  138. ) {
  139. return short;
  140. }
  141. const intl = new Intl.DateTimeFormat(undefined, {
  142. year: 'numeric',
  143. month: 'short',
  144. day: 'numeric',
  145. hour: 'numeric',
  146. minute: 'numeric'
  147. });
  148. return intl.format(new Date(ts));
  149. },
  150. openMenu() {
  151. this.$emit('menu');
  152. },
  153. scopeIcon(scope) {
  154. switch(scope) {
  155. case 'public':
  156. return 'far fa-globe';
  157. break;
  158. case 'unlisted':
  159. return 'far fa-lock-open';
  160. break;
  161. case 'private':
  162. return 'far fa-lock';
  163. break;
  164. default:
  165. return 'far fa-globe';
  166. break;
  167. }
  168. },
  169. scopeTitle(scope) {
  170. switch(scope) {
  171. case 'public':
  172. return 'Visible to everyone';
  173. break;
  174. case 'unlisted':
  175. return 'Hidden from public feeds';
  176. break;
  177. case 'private':
  178. return 'Only visible to followers';
  179. break;
  180. default:
  181. return '';
  182. break;
  183. }
  184. },
  185. goToPost() {
  186. if(location.pathname.split('/').pop() == this.status.id) {
  187. location.href = this.status.local ? this.status.url + '?fs=1' : this.status.url;
  188. return;
  189. }
  190. this.$router.push({
  191. name: 'post',
  192. path: `/i/web/post/${this.status.id}`,
  193. params: {
  194. id: this.status.id,
  195. cachedStatus: this.status,
  196. cachedProfile: this.profile
  197. }
  198. })
  199. },
  200. goToProfileById(id) {
  201. this.$nextTick(() => {
  202. this.$router.push({
  203. name: 'profile',
  204. path: `/i/web/profile/${id}`,
  205. params: {
  206. id: id,
  207. cachedUser: this.profile
  208. }
  209. });
  210. });
  211. },
  212. goToProfile() {
  213. this.$nextTick(() => {
  214. this.$router.push({
  215. name: 'profile',
  216. path: `/i/web/profile/${this.status.account.id}`,
  217. params: {
  218. id: this.status.account.id,
  219. cachedProfile: this.status.account,
  220. cachedUser: this.profile
  221. }
  222. });
  223. });
  224. },
  225. toggleContentWarning() {
  226. this.key++;
  227. this.sensitive = true;
  228. this.status.sensitive = !this.status.sensitive;
  229. },
  230. like() {
  231. event.currentTarget.blur();
  232. if(this.status.favourited) {
  233. this.$emit('unlike');
  234. } else {
  235. this.$emit('like');
  236. }
  237. },
  238. toggleMenu(bvEvent) {
  239. setTimeout(() => {
  240. this.menuLoading = false;
  241. }, 500);
  242. },
  243. closeMenu(bvEvent) {
  244. setTimeout(() => {
  245. bvEvent.target.parentNode.firstElementChild.blur();
  246. }, 100);
  247. },
  248. showLikes() {
  249. event.currentTarget.blur();
  250. this.$emit('likes-modal');
  251. },
  252. showShares() {
  253. event.currentTarget.blur();
  254. this.$emit('shares-modal');
  255. },
  256. showComments() {
  257. event.currentTarget.blur();
  258. this.showCommentDrawer = !this.showCommentDrawer;
  259. },
  260. copyLink() {
  261. event.currentTarget.blur();
  262. App.util.clipboard(this.status.url);
  263. },
  264. shareToOther() {
  265. if (navigator.canShare) {
  266. navigator.share({
  267. url: this.status.url
  268. })
  269. .then(() => console.log('Share was successful.'))
  270. .catch((error) => console.log('Sharing failed', error));
  271. } else {
  272. swal('Not supported', 'Your current device does not support native sharing.', 'error');
  273. }
  274. },
  275. counterChange(type) {
  276. this.$emit('counter-change', type);
  277. },
  278. showCommentLikes(post) {
  279. this.$emit('comment-likes-modal', post);
  280. },
  281. shareStatus() {
  282. this.$emit('share');
  283. },
  284. unshareStatus() {
  285. this.$emit('unshare');
  286. },
  287. handleReport(post) {
  288. this.$emit('handle-report', post);
  289. },
  290. follow() {
  291. this.$emit('follow');
  292. },
  293. unfollow() {
  294. this.$emit('unfollow');
  295. },
  296. handleReblog() {
  297. this.isReblogging = true;
  298. if(this.status.reblogged) {
  299. this.$emit('unshare');
  300. } else {
  301. this.$emit('share');
  302. }
  303. setTimeout(() => {
  304. this.isReblogging = false;
  305. }, 5000);
  306. },
  307. handleBookmark() {
  308. event.currentTarget.blur();
  309. this.isBookmarking = true;
  310. this.$emit('bookmark');
  311. setTimeout(() => {
  312. this.isBookmarking = false;
  313. }, 5000);
  314. },
  315. getStatusAvatar() {
  316. if(window._sharedData.user.id == this.status.account.id) {
  317. return window._sharedData.user.avatar;
  318. }
  319. return this.status.account.avatar;
  320. },
  321. openModTools() {
  322. this.$emit('mod-tools');
  323. },
  324. openEditModal() {
  325. this.$refs.editModal.open();
  326. }
  327. }
  328. }
  329. </script>