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

Configure Feed

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

at a876aae44ea07494ebea9727350aa060b81f317b 220 lines 8.2 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-4 bg-white dark:bg-black hover:bg-brandHover dark:hover:bg-brandHoverDark rounded-[30px] p-5" 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-semibold text-[15px] min-[400px]: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 <div className="flex items-center gap-1 text-[13px] min-[400px]:text-[15px] min-w-0"> 91 <Link 92 href={`/profile/${post.author.did}`} 93 className="text-textNeutral hover:underline line-clamp-1"> 94 @{post.author.handle} 95 </Link> 96 <span className="text-textNeutral shrink-0"></span> 97 <Link 98 href={`/profile/${post.author.did}`} 99 className="text-brand hover:underline shrink-0"> 100 Follow 101 </Link> 102 </div> 103 </div> 104 </div> 105 106 <PostContent record={record} /> 107 <Embed content={post.embed} labels={post.labels} /> 108 109 <div className="flex items-end justify-between w-full"> 110 <div className="flex flex-col min-[400px]:gap-0.5"> 111 <div className="flex items-center gap-3 text-sm cursor-pointer ml-[-2px]"> 112 {!!post.likeCount && ( 113 <div className="flex items-center gap-0.5 min-[400px]:gap-1 cursor-pointer group"> 114 <LikeIcon className="w-5 h-5 min-[400px]:w-[22px] min-[400px]:h-[22px] text-textLight dark:text-textDimmed group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" /> 115 <p className="text-[11px] min-[400px]:text-[15px] font-semibold text-textLight dark:text-textDimmed mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"> 116 {prettyNumber(post.likeCount)} 117 </p> 118 </div> 119 )} 120 {!!post.replyCount && ( 121 <div className="flex items-center gap-0.5 min-[400px]:gap-1 cursor-pointer group"> 122 <ReplyIcon className="w-5 h-5 min-[400px]:w-[22px] min-[400px]:h-[22px] text-textLight dark:text-textDimmed group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" /> 123 <p className="text-[11px] min-[400px]:text-[15px] font-semibold text-textLight dark:text-textDimmed mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"> 124 {prettyNumber(post.replyCount)} 125 </p> 126 </div> 127 )} 128 {!!post.repostCount && ( 129 <div className="flex items-center gap-0.5 min-[400px]:gap-1 cursor-pointer group"> 130 <RepostIcon className="w-5 h-5 min-[400px]:w-[22px] min-[400px]:h-[22px] text-textLight dark:text-textDimmed group-hover:text-neutral-800 dark:group-hover:text-white transition-colors" /> 131 <p className="text-[11px] min-[400px]:text-[15px] font-semibold text-textLight dark:text-textDimmed mb-px group-hover:text-neutral-800 dark:group-hover:text-white transition-colors"> 132 {prettyNumber(post.repostCount)} 133 </p> 134 </div> 135 )} 136 </div> 137 <Link href={href}> 138 <time 139 datetime={new Date(post.indexedAt).toISOString()} 140 className="text-[11px] min-[400px]:text-[15px] text-textNeutral hover:underline"> 141 {niceDate(post.indexedAt)} 142 </time> 143 </Link> 144 </div> 145 <Link 146 href={href} 147 className="transition-transform hover:scale-110 shrink-0"> 148 <img src={logo} className="h-5 min-[400px]:h-7" /> 149 </Link> 150 </div> 151 </div> 152 </Container> 153 ) 154} 155 156function PostContent({record}: {record: AppBskyFeedPost.Record | null}) { 157 if (!record) return null 158 159 const rt = new RichText({ 160 text: record.text, 161 facets: record.facets, 162 }) 163 164 const richText = [] 165 166 let counter = 0 167 for (const segment of rt.segments()) { 168 if ( 169 segment.link && 170 AppBskyRichtextFacet.validateLink(segment.link).success 171 ) { 172 richText.push( 173 <Link 174 key={counter} 175 href={segment.link.uri} 176 className="text-brand hover:underline" 177 disableTracking={ 178 !segment.link.uri.startsWith('https://bsky.app') && 179 !segment.link.uri.startsWith('https://go.bsky.app') 180 }> 181 {segment.text} 182 </Link>, 183 ) 184 } else if ( 185 segment.mention && 186 AppBskyRichtextFacet.validateMention(segment.mention).success 187 ) { 188 richText.push( 189 <Link 190 key={counter} 191 href={`/profile/${segment.mention.did}`} 192 className="text-brand hover:underline"> 193 {segment.text} 194 </Link>, 195 ) 196 } else if ( 197 segment.tag && 198 AppBskyRichtextFacet.validateTag(segment.tag).success 199 ) { 200 richText.push( 201 <Link 202 key={counter} 203 href={`/hashtag/${segment.tag.tag}`} 204 className="text-brand hover:underline"> 205 {segment.text} 206 </Link>, 207 ) 208 } else { 209 richText.push(segment.text) 210 } 211 212 counter++ 213 } 214 215 return ( 216 <p className="text-md min-[400px]:text-lg leading-snug min-[400px]:leading-snug break-word break-words whitespace-pre-wrap"> 217 {richText} 218 </p> 219 ) 220}