wip bsky client for the web & android
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(posts): use AppLink for comments button; support mmb to open in new tab

willow 9e7e4477 2faa31e5

+64 -12
+64 -12
src/components/Feed/FeedItem.vue
··· 9 9 IconFavoriteRounded, 10 10 } from '@iconify-prerendered/vue-material-symbols' 11 11 12 + import { useNavigationStore } from '@/stores/navigation' 12 13 import { usePostStore } from '@/stores/posts' 14 + 15 + import AppLink from '@/components/Navigation/AppLink.vue' 13 16 import ImageEmbed from './Embeds/ImageEmbed.vue' 14 17 import EmbedRecord from './Embeds/EmbedRecord.vue' 15 18 import ExternalEmbed from './Embeds/ExternalEmbed.vue' ··· 21 24 item?: AppBskyFeedDefs.FeedViewPost 22 25 post?: PostInput 23 26 embedded?: boolean 27 + rootPost?: boolean 24 28 }>() 25 29 26 30 const postStore = usePostStore() 31 + const navigationStore = useNavigationStore() 27 32 28 33 const displayPost = computed(() => { 29 34 if (props.item) return props.item.post ··· 47 52 48 53 const embed = computed(() => displayPost.value?.embed) 49 54 55 + const rkey = computed(() => { 56 + if (!displayPost.value) return null 57 + const uriParts = displayPost.value.uri.split('/') 58 + return uriParts.pop() || null 59 + }) 60 + 50 61 const formatTime = (dateString?: string) => { 51 62 if (!dateString) return '' 52 63 const date = new Date(dateString) ··· 76 87 77 88 const handleClick = (e: MouseEvent) => { 78 89 if (window.getSelection()?.toString().length) return 90 + if (props.rootPost) return 79 91 80 92 if (props.embedded) { 81 93 e.stopPropagation() 94 + if (displayPost.value) navigateToPost(displayPost.value) 95 + return 96 + } 97 + 98 + if (displayPost.value) { 99 + navigateToPost(displayPost.value) 100 + } 101 + } 102 + 103 + const navigateToPost = (post: AppBskyFeedDefs.PostView, event?: MouseEvent | KeyboardEvent) => { 104 + const uriParts = post.uri.split('/') 105 + const rkey = uriParts.pop() 106 + const identifier = post.author.handle || post.author.did 107 + 108 + if (!rkey || !identifier) return 109 + 110 + const isModifierKey = event && (event.ctrlKey || event.metaKey || event.shiftKey) 111 + 112 + if (isModifierKey) { 113 + const url = `${window.location.origin}/profile/${identifier}/post/${rkey}` 114 + window.open(url, '_blank') 115 + } else { 116 + navigationStore.push('post-thread', { 117 + props: { identifier, rkey: rkey }, 118 + }) 82 119 } 83 120 } 84 121 85 122 const handleMiddleClick = () => { 86 - console.log(displayPost.value) 87 - navigator.clipboard.writeText(JSON.stringify(displayPost.value, null, 2)) 123 + if (!displayPost.value) return 124 + const uriParts = displayPost.value.uri.split('/') 125 + const rkey = uriParts.pop() 126 + const identifier = displayPost.value.author.handle || displayPost.value.author.did 127 + 128 + if (rkey && identifier) { 129 + const url = `${window.location.origin}/profile/${identifier}/post/${rkey}` 130 + window.open(url, '_blank') 131 + } 88 132 } 89 133 </script> 90 134 ··· 92 136 <article 93 137 v-if="displayPost" 94 138 :key="displayPost.uri" 139 + :id="displayPost.uri" 95 140 class="feed-item" 96 - :class="{ 'is-embedded': embedded }" 141 + :class="{ 'is-embedded': embedded, 'is-root-post': rootPost }" 97 142 @click="handleClick" 98 143 @click.middle="handleMiddleClick" 99 144 > ··· 150 195 </template> 151 196 </div> 152 197 153 - <div class="post-footer" v-if="!embedded" @click.stop> 198 + <div class="post-footer" v-if="!embedded"> 154 199 <div class="metrics"> 155 - <button class="action-button reply" aria-label="Reply"> 200 + <AppLink 201 + name="post-thread" 202 + :params="{ identifier: displayPost.author.handle, rkey: rkey! }" 203 + class="action-button reply" 204 + aria-label="Reply" 205 + @click.stop 206 + > 156 207 <div class="icon-wrapper"><IconChatBubbleOutlineRounded /></div> 157 208 <span class="count" v-if="displayPost.replyCount && displayPost.replyCount > 0"> 158 209 {{ formatCount(displayPost.replyCount) }} 159 210 </span> 160 - </button> 211 + </AppLink> 161 212 162 213 <button 163 214 class="action-button repost" 164 215 :class="{ 'is-active': !!displayPost.viewer?.repost }" 165 - @click="handleRepost" 216 + @click.stop="handleRepost" 166 217 aria-label="Repost" 167 218 > 168 219 <div class="icon-wrapper"><IconRepeatRounded /></div> ··· 174 225 <button 175 226 class="action-button like" 176 227 :class="{ 'is-active': !!displayPost.viewer?.like }" 177 - @click="handleLike" 228 + @click.stop="handleLike" 178 229 aria-label="Like" 179 230 > 180 231 <div class="icon-wrapper"> ··· 200 251 flex-direction: column; 201 252 gap: 0.25rem; 202 253 203 - &:hover { 204 - background-color: hsla(var(--surface0) / 0.3); 205 - cursor: pointer; 254 + &:not(.is-root-post) { 255 + &:hover { 256 + background-color: hsla(var(--surface0) / 0.3); 257 + cursor: pointer; 258 + } 206 259 } 207 260 208 261 &.is-embedded { ··· 347 400 align-items: center; 348 401 margin-top: 0.5rem; 349 402 margin-left: -0.5rem; 350 - margin-right: -0.5rem; 351 403 352 404 .metrics { 353 405 display: flex;