Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

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

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