Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
at main 214 lines 7.4 kB view raw
1import { 2 AppBskyFeedDefs, 3 AppBskyFeedPost, 4 AppBskyRichtextFacet, 5 RichText, 6} from '@atproto/api' 7import {h} from 'preact' 8 9import logo from '../../assets/logo_full_name.svg' 10import {Like as LikeIcon} from '../icons/Like' 11import {Reply as ReplyIcon} from '../icons/Reply' 12import {Repost as RepostIcon} from '../icons/Repost' 13import {CONTENT_LABELS} from '../labels' 14import * as bsky from '../types/bsky' 15import {niceDate} from '../util/nice-date' 16import {prettyNumber} from '../util/pretty-number' 17import {getRkey} from '../util/rkey' 18import {getVerificationState} from '../util/verification-state' 19import {Container} from './container' 20import {Embed} from './embed' 21import {Link} from './link' 22import {VerificationCheck} from './verification-check' 23 24interface Props { 25 thread: AppBskyFeedDefs.ThreadViewPost 26} 27 28export function Post({thread}: Props) { 29 const post = thread.post 30 31 const isAuthorLabeled = post.author.labels?.some(label => 32 CONTENT_LABELS.includes(label.val), 33 ) 34 35 let record: AppBskyFeedPost.Record | null = null 36 if ( 37 bsky.dangerousIsType<AppBskyFeedPost.Record>( 38 post.record, 39 AppBskyFeedPost.isRecord, 40 ) 41 ) { 42 record = post.record 43 } 44 45 const verification = getVerificationState({profile: post.author}) 46 47 const href = `/profile/${post.author.did}/post/${getRkey(post)}` 48 49 return ( 50 <Container href={href}> 51 <div 52 className="flex-1 flex-col flex gap-2 bg-neutral-50 dark:bg-black dark:hover:bg-slate-900 hover:bg-blue-50 rounded-[14px] p-4" 53 lang={record?.langs?.[0]}> 54 <div className="flex gap-2.5 items-center cursor-pointer w-full max-w-full "> 55 <Link 56 href={`/profile/${post.author.did}`} 57 className="rounded-full shrink-0"> 58 <div className="w-10 h-10 overflow-hidden rounded-full bg-neutral-300 dark:bg-slate-700 shrink-0"> 59 <img 60 src={post.author.avatar} 61 style={isAuthorLabeled ? {filter: 'blur(2.5px)'} : undefined} 62 /> 63 </div> 64 </Link> 65 <div className="flex flex-1 flex-col min-w-0"> 66 <div className="flex flex-1 items-center"> 67 <Link 68 href={`/profile/${post.author.did}`} 69 className="block font-bold text-[17px] leading-5 line-clamp-1 hover:underline underline-offset-2 text-ellipsis decoration-2"> 70 {post.author.displayName?.trim() || post.author.handle} 71 </Link> 72 {verification.isVerified && ( 73 <VerificationCheck 74 className="pl-[3px] mt-px shrink-0" 75 verifier={verification.role === 'verifier'} 76 size={15} 77 /> 78 )} 79 </div> 80 <Link 81 href={`/profile/${post.author.did}`} 82 className="block text-[15px] text-textLight dark:text-textDimmed hover:underline line-clamp-1"> 83 @{post.author.handle} 84 </Link> 85 </div> 86 </div> 87 <PostContent record={record} /> 88 <Embed content={post.embed} labels={post.labels} /> 89 90 <div className="flex items-center justify-between w-full pt-2.5 text-sm"> 91 <div className="flex items-center gap-3 text-sm cursor-pointer"> 92 {!!post.likeCount && ( 93 <div className="flex items-center gap-1 cursor-pointer group"> 94 <LikeIcon 95 width={20} 96 height={20} 97 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" 98 /> 99 <p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400"> 100 {prettyNumber(post.likeCount)} 101 </p> 102 </div> 103 )} 104 {!!post.replyCount && ( 105 <div className="flex items-center gap-1 cursor-pointer group"> 106 <ReplyIcon 107 width={20} 108 height={20} 109 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" 110 /> 111 <p className="font-medium text-slate-600 text-neutral-600 dark:text-neutral-300 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors dark:text-slate-400"> 112 {prettyNumber(post.replyCount)} 113 </p> 114 </div> 115 )} 116 117 {!!post.repostCount && ( 118 <div className="flex items-center gap-1 cursor-pointer group"> 119 <RepostIcon 120 width={20} 121 height={20} 122 className="text-slate-600 dark:text-slate-400 group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" 123 /> 124 <p className="font-medium text-slate-600 dark:text-slate-400 mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"> 125 {prettyNumber(post.repostCount)} 126 </p> 127 </div> 128 )} 129 </div> 130 <Link href={href}> 131 <time 132 datetime={new Date(post.indexedAt).toISOString()} 133 className="text-slate-500 dark:text-textDimmed text-sm hover:underline dark:text-slate-500"> 134 {niceDate(post.indexedAt)} 135 </time> 136 </Link> 137 </div> 138 </div> 139 <div className="flex items-center justify-end pt-2"> 140 <Link 141 href={href} 142 className="transition-transform hover:scale-110 shrink-0"> 143 <img src={logo} className="h-8" /> 144 </Link> 145 </div> 146 </Container> 147 ) 148} 149 150function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { 151 if (!record) return null 152 153 const rt = new RichText({ 154 text: record.text, 155 facets: record.facets, 156 }) 157 158 const richText = [] 159 160 let counter = 0 161 for (const segment of rt.segments()) { 162 if ( 163 segment.link && 164 AppBskyRichtextFacet.validateLink(segment.link).success 165 ) { 166 richText.push( 167 <Link 168 key={counter} 169 href={segment.link.uri} 170 className="text-blue-500 hover:underline" 171 disableTracking={ 172 !segment.link.uri.startsWith('https://bsky.app') && 173 !segment.link.uri.startsWith('https://go.bsky.app') 174 }> 175 {segment.text} 176 </Link>, 177 ) 178 } else if ( 179 segment.mention && 180 AppBskyRichtextFacet.validateMention(segment.mention).success 181 ) { 182 richText.push( 183 <Link 184 key={counter} 185 href={`/profile/${segment.mention.did}`} 186 className="text-blue-500 hover:underline"> 187 {segment.text} 188 </Link>, 189 ) 190 } else if ( 191 segment.tag && 192 AppBskyRichtextFacet.validateTag(segment.tag).success 193 ) { 194 richText.push( 195 <Link 196 key={counter} 197 href={`/hashtag/${segment.tag.tag}`} 198 className="text-blue-500 hover:underline"> 199 {segment.text} 200 </Link>, 201 ) 202 } else { 203 richText.push(segment.text) 204 } 205 206 counter++ 207 } 208 209 return ( 210 <p className="min-[300px]:text-lg leading-6 min-[300px]:leading-6 break-word break-words whitespace-pre-wrap"> 211 {richText} 212 </p> 213 ) 214}