ProfileHoverCard.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347
  1. <template>
  2. <div class="profile-hover-card">
  3. <div class="profile-hover-card-inner">
  4. <div class="d-flex justify-content-between align-items-start" style="max-width: 240px;">
  5. <a
  6. :href="profile.url"
  7. @click.prevent="goToProfile()">
  8. <img
  9. :src="profile.avatar"
  10. width="50"
  11. height="50"
  12. class="avatar"
  13. onerror="this.onerror=null;this.src='/storage/avatars/default.png?v=0';">
  14. </a>
  15. <div v-if="user.id == profile.id">
  16. <a class="btn btn-outline-primary px-3 py-1 font-weight-bold rounded-pill" href="/settings/home">Edit Profile</a>
  17. </div>
  18. <div v-if="user.id != profile.id && relationship">
  19. <button
  20. v-if="relationship.following"
  21. class="btn btn-outline-primary px-3 py-1 font-weight-bold rounded-pill"
  22. :disabled="isLoading"
  23. @click="performUnfollow()">
  24. <span v-if="isLoading"><b-spinner small /></span>
  25. <span v-else>Following</span>
  26. </button>
  27. <div v-else>
  28. <button
  29. v-if="!relationship.requested"
  30. class="btn btn-primary primary px-3 py-1 font-weight-bold rounded-pill"
  31. :disabled="isLoading"
  32. @click="performFollow()">
  33. <span v-if="isLoading"><b-spinner small /></span>
  34. <span v-else>Follow</span>
  35. </button>
  36. <button v-else class="btn btn-primary primary px-3 py-1 font-weight-bold rounded-pill" disabled>Follow Requested</button>
  37. </div>
  38. </div>
  39. </div>
  40. <p class="display-name">
  41. <a
  42. :href="profile.url"
  43. @click.prevent="goToProfile()"
  44. v-html="getDisplayName()">
  45. </a>
  46. </p>
  47. <div class="username">
  48. <a
  49. :href="profile.url"
  50. class="username-link"
  51. @click.prevent="goToProfile()">
  52. &commat;{{ getUsername() }}
  53. </a>
  54. <p v-if="user.id != profile.id && relationship && relationship.followed_by" class="username-follows-you">
  55. <span>Follows You</span>
  56. </p>
  57. </div>
  58. <p
  59. v-if="profile.hasOwnProperty('pronouns') && profile.pronouns && profile.pronouns.length"
  60. class="pronouns">
  61. {{ profile.pronouns.join(', ') }}
  62. </p>
  63. <p class="bio" v-html="bio"></p>
  64. <p class="stats">
  65. <span class="stats-following">
  66. <span class="following-count">{{ formatCount(profile.following_count) }}</span> Following
  67. </span>
  68. <span class="stats-followers">
  69. <span class="followers-count">{{ formatCount(profile.followers_count) }}</span> Followers
  70. </span>
  71. </p>
  72. </div>
  73. </div>
  74. </template>
  75. <script type="text/javascript">
  76. import ReadMore from './../post/ReadMore.vue';
  77. import { mapGetters } from 'vuex';
  78. export default {
  79. props: {
  80. profile: {
  81. type: Object
  82. },
  83. // relationship: {
  84. // type: Object
  85. // }
  86. },
  87. components: {
  88. ReadMore
  89. },
  90. data() {
  91. return {
  92. user: window._sharedData.user,
  93. bio: undefined,
  94. isLoading: false,
  95. relationship: undefined
  96. };
  97. },
  98. mounted() {
  99. this.rewriteLinks();
  100. this.relationship = this.$store.getters.getRelationship(this.profile.id);
  101. if(!this.relationship && this.profile.id != this.user.id) {
  102. axios.get('/api/pixelfed/v1/accounts/relationships', {
  103. params: {
  104. 'id[]': this.profile.id
  105. }
  106. })
  107. .then(res => {
  108. this.relationship = res.data[0];
  109. this.$store.commit('updateRelationship', res.data);
  110. })
  111. }
  112. },
  113. computed: {
  114. ...mapGetters([
  115. 'getCustomEmoji'
  116. ])
  117. },
  118. methods: {
  119. getDisplayName() {
  120. let self = this;
  121. let profile = this.profile;
  122. let dn = profile.display_name;
  123. if(!dn) {
  124. return profile.username;
  125. }
  126. if(dn.includes(':')) {
  127. let re = /(<a?)?:\w+:(\d{18}>)?/g;
  128. let un = dn.replaceAll(re, function(em) {
  129. let shortcode = em.slice(1, em.length - 1);
  130. let emoji = self.getCustomEmoji.filter(e => {
  131. return e.shortcode == shortcode;
  132. });
  133. return emoji.length ? `<img draggable="false" class="emojione custom-emoji" alt="${emoji[0].shortcode}" title="${emoji[0].shortcode}" src="${emoji[0].url}" data-original="${emoji[0].url}" data-static="${emoji[0].static_url}" width="16" height="16" onerror="this.onerror=null;this.src='/storage/emoji/missing.png';" />`: em;
  134. });
  135. return un;
  136. } else {
  137. return dn;
  138. }
  139. },
  140. getUsername() {
  141. let profile = this.profile;
  142. // if(profile.hasOwnProperty('local') && profile.local) {
  143. // return profile.acct + '@' + window.location.hostname;
  144. // }
  145. return profile.acct;
  146. },
  147. formatCount(val) {
  148. return App.util.format.count(val);
  149. },
  150. goToProfile() {
  151. this.$router.push({
  152. name: 'profile',
  153. path: `/i/web/profile/${this.profile.id}`,
  154. params: {
  155. id: this.profile.id,
  156. cachedProfile: this.profile,
  157. cachedUser: this.user
  158. }
  159. })
  160. },
  161. rewriteLinks() {
  162. let content = this.profile.note;
  163. let el = document.createElement('div');
  164. el.innerHTML = content;
  165. el.querySelectorAll('a[class*="hashtag"]')
  166. .forEach(elr => {
  167. let tag = elr.innerText;
  168. if(tag.substr(0, 1) == '#') {
  169. tag = tag.substr(1);
  170. }
  171. elr.removeAttribute('target');
  172. elr.setAttribute('href', '/i/web/hashtag/' + tag);
  173. })
  174. el.querySelectorAll('a:not(.hashtag)[class*="mention"], a:not(.hashtag)[class*="list-slug"]')
  175. .forEach(elr => {
  176. let name = elr.innerText;
  177. if(name.substr(0, 1) == '@') {
  178. name = name.substr(1);
  179. }
  180. if(this.profile.local == false && !name.includes('@')) {
  181. let domain = document.createElement('a');
  182. domain.href = this.profile.url;
  183. name = name + '@' + domain.hostname;
  184. }
  185. elr.removeAttribute('target');
  186. elr.setAttribute('href', '/i/web/username/' + name);
  187. })
  188. this.bio = el.outerHTML;
  189. },
  190. performFollow() {
  191. this.isLoading = true;
  192. this.$emit('follow');
  193. setTimeout(() => {
  194. this.relationship.following = true;
  195. this.isLoading = false;
  196. }, 1000);
  197. },
  198. performUnfollow() {
  199. this.isLoading = true;
  200. this.$emit('unfollow');
  201. setTimeout(() => {
  202. this.relationship.following = false;
  203. this.isLoading = false;
  204. }, 1000);
  205. }
  206. }
  207. }
  208. </script>
  209. <style lang="scss">
  210. .profile-hover-card {
  211. display: block;
  212. width: 300px;
  213. overflow: hidden;
  214. padding: 0.5rem;
  215. border: none;
  216. font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  217. .avatar {
  218. border-radius: 15px;
  219. box-shadow: 0 0.5rem 1rem rgb(0 0 0 / 15%) !important;
  220. margin-bottom: 0.5rem;
  221. }
  222. .display-name {
  223. max-width: 240px;
  224. word-break: break-word;
  225. font-weight: 800;
  226. margin-top: 5px;
  227. margin-bottom: 2px;
  228. line-height: 0.8;
  229. font-size: 16px;
  230. font-weight: 800 !important;
  231. user-select: all;
  232. font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  233. a {
  234. color: var(--body-color);
  235. text-decoration: none;
  236. }
  237. }
  238. .username {
  239. max-width: 240px;
  240. word-break: break-word;
  241. font-size: 12px;
  242. margin-top: 0;
  243. margin-bottom: 0.6rem;
  244. user-select: all;
  245. font-weight: 700;
  246. overflow: hidden;
  247. &-link {
  248. color: var(--text-lighter);
  249. text-decoration: none;
  250. margin-right: 4px;
  251. }
  252. &-follows-you {
  253. margin: 4px 0;
  254. span {
  255. color: var(--dropdown-item-color);
  256. background-color: var(--comment-bg);
  257. font-size: 12px;
  258. font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif;
  259. font-weight: 500;
  260. padding: 2px 4px;
  261. line-height: 16px;
  262. border-radius: 6px;
  263. }
  264. }
  265. }
  266. .pronouns {
  267. font-size: 11px;
  268. color: #9CA3AF;
  269. margin-top: -0.8rem;
  270. margin-bottom: 0.6rem;
  271. font-weight: 600;
  272. }
  273. .bio {
  274. max-width: 240px;
  275. max-height: 60px;
  276. word-break: break-word;
  277. margin-bottom: 0;
  278. overflow: hidden;
  279. text-overflow: ellipsis;
  280. line-height: 1.2;
  281. font-size: 12px;
  282. color: var(--body-color);
  283. .invisible {
  284. display: none;
  285. }
  286. }
  287. .stats {
  288. margin-top: 0.5rem;
  289. margin-bottom: 0;
  290. font-size: 14px;
  291. user-select: none;
  292. color: var(--body-color);
  293. .stats-following {
  294. margin-right: 0.8rem;
  295. }
  296. .following-count,
  297. .followers-count {
  298. font-weight: 800;
  299. }
  300. }
  301. .btn {
  302. &.rounded-pill {
  303. min-width: 80px;
  304. }
  305. }
  306. }
  307. </style>