a tool for shared writing and social publishing

added contributor invite notification, some tweaks to avatar component prop names

+193 -96
+98
app/(home-pages)/notifications/ContributorNotification.tsx
··· 1 + import { Notification } from "./Notification"; 2 + import { HydratedSubscribeNotification } from "src/notifications"; 3 + import { blobRefToSrc } from "src/utils/blobRefToSrc"; 4 + import { AppBskyActorProfile, PubLeafletPublication } from "lexicons/api"; 5 + import { PubIcon } from "components/ActionBar/Publications"; 6 + import { InvitedContent } from "app/lish/[did]/[publication]/invite-contributor/content"; 7 + import { ButtonPrimary } from "components/Buttons"; 8 + import { usePubTheme } from "components/ThemeManager/PublicationThemeProvider"; 9 + import { AtUri } from "@atproto/api"; 10 + import { BaseThemeProvider } from "components/ThemeManager/ThemeProvider"; 11 + 12 + export const ContributorNotification = ( 13 + props: HydratedSubscribeNotification, 14 + ) => { 15 + const profileRecord = props.subscriptionData?.identities?.bsky_profiles 16 + ?.record as AppBskyActorProfile.Record; 17 + const displayName = 18 + profileRecord?.displayName || 19 + props.subscriptionData?.identities?.bsky_profiles?.handle || 20 + "Someone"; 21 + const pubRecord = props.subscriptionData?.publications 22 + ?.record as PubLeafletPublication.Record; 23 + 24 + return ( 25 + // TODO: GET CORRECT TIMESTAMP 26 + <Notification 27 + timestamp={""} 28 + href={`https://${pubRecord?.base_path}/invite-contributor`} 29 + icon={ 30 + <PubIcon 31 + record={pubRecord} 32 + uri={props.subscriptionData?.publications?.uri} 33 + small 34 + /> 35 + } 36 + actionText={ 37 + <> 38 + {displayName} invited you to contribute to {pubRecord?.name}! 39 + </> 40 + } 41 + content={ 42 + <PubListing 43 + record={pubRecord} 44 + uri={props.subscriptionData?.publications?.uri!} 45 + /> 46 + } 47 + /> 48 + ); 49 + }; 50 + 51 + const PubListing = (props: { 52 + record: PubLeafletPublication.Record; 53 + uri: string; 54 + }) => { 55 + let record = props.record; 56 + let theme = usePubTheme(record); 57 + let backgroundImage = record?.theme?.backgroundImage?.image?.ref 58 + ? blobRefToSrc( 59 + record?.theme?.backgroundImage?.image?.ref, 60 + new AtUri(props.uri).host, 61 + ) 62 + : null; 63 + 64 + let backgroundImageRepeat = record?.theme?.backgroundImage?.repeat; 65 + let backgroundImageSize = record?.theme?.backgroundImage?.width || 500; 66 + if (!record) return null; 67 + return ( 68 + <BaseThemeProvider {...theme} local> 69 + <div 70 + className={`no-underline! flex flex-row gap-2 71 + bg-bg-leaflet 72 + border border-border-light rounded-lg 73 + p-2 selected-outline 74 + hover:outline-accent-contrast hover:border-accent-contrast 75 + relative overflow-hidden`} 76 + style={{ 77 + backgroundImage: `url(${backgroundImage})`, 78 + backgroundRepeat: backgroundImageRepeat ? "repeat" : "no-repeat", 79 + backgroundSize: `${backgroundImageRepeat ? `${backgroundImageSize}px` : "cover"}`, 80 + }} 81 + > 82 + <div 83 + className={` 84 + p-3 w-full flex flex-col justify-center text-center border border-border-light rounded-lg hover:no-underline! no-underline! ${record.theme?.showPageBackground ? "bg-[rgba(var(--bg-page),var(--bg-page-alpha))]" : ""}`} 85 + > 86 + <PubIcon record={record} uri={props.uri} className="mx-auto my-1" /> 87 + <h4 className="leading-tight">{record.name}</h4> 88 + <div className="text-tertiary text-sm italic"> 89 + {record.description} 90 + </div> 91 + <ButtonPrimary compact className="mx-auto mt-2 mb-1 text-sm"> 92 + Accept Invite 93 + </ButtonPrimary> 94 + </div> 95 + </div> 96 + </BaseThemeProvider> 97 + ); 98 + };
+1 -1
app/(home-pages)/notifications/FollowNotification.tsx
··· 24 24 <Notification 25 25 timestamp={props.created_at} 26 26 href={`https://${pubRecord?.base_path}`} 27 - icon={<Avatar src={avatarSrc} displayName={displayName} tiny />} 27 + icon={<Avatar src={avatarSrc} displayName={displayName} small />} 28 28 actionText={ 29 29 <> 30 30 {displayName} subscribed to {pubRecord?.name}!
+2 -5
app/(home-pages)/notifications/MentionNotification.tsx
··· 24 24 content={ 25 25 <ContentLayout postTitle={docRecord.title} pubRecord={pubRecord}> 26 26 <div className="flex gap-2 text-sm w-full"> 27 - <Avatar 28 - src={author.avatar} 29 - displayName={displayName} 30 - /> 27 + <Avatar src={author.avatar} displayName={displayName} /> 31 28 <pre 32 29 style={{ wordBreak: "break-word" }} 33 - className="whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6" 30 + className="pt-[2px] whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6" 34 31 > 35 32 {postText} 36 33 </pre>
+1 -1
app/(home-pages)/notifications/Notification.tsx
··· 102 102 <Avatar src={props.avatar} displayName={props.displayName} /> 103 103 <pre 104 104 style={{ wordBreak: "break-word" }} 105 - className={`whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6 ${props.className}`} 105 + className={`pt-[2px] whitespace-pre-wrap text-secondary line-clamp-3 sm:line-clamp-6 ${props.className}`} 106 106 > 107 107 <BaseTextBlock 108 108 preview
+1 -1
app/(home-pages)/notifications/NotificationList.tsx
··· 33 33 </div> 34 34 ); 35 35 return ( 36 - <div className="max-w-prose mx-auto w-full"> 36 + <div className="max-w-prose mx-auto w-full pt-1 sm:pt-2"> 37 37 <div className={`flex flex-col gap-2`}> 38 38 {notifications.map((n) => { 39 39 if (n.type === "comment") {
+80
app/lish/[did]/[publication]/invite-contributor/content.tsx
··· 1 + import { PubLeafletPublication } from "lexicons/api"; 2 + import { ButtonPrimary } from "components/Buttons"; 3 + import { PubIcon } from "components/ActionBar/Publications"; 4 + import { SpeedyLink } from "components/SpeedyLink"; 5 + import { BlueskyLogin } from "app/login/LoginForm"; 6 + 7 + export const InvitedContent = (props: { 8 + pubRecord: PubLeafletPublication.Record; 9 + uri: string | undefined; 10 + href: string; 11 + }) => { 12 + return ( 13 + <> 14 + <h2>Become a Contributor!</h2> 15 + <PubLink {...props} /> 16 + <div> 17 + You've been invited to write for <br /> 18 + {props.pubRecord.name}!{" "} 19 + </div> 20 + <ButtonPrimary className="mx-auto mt-4 mb-2">Accept Invite</ButtonPrimary> 21 + </> 22 + ); 23 + }; 24 + 25 + export const NotInvitedContent = (props: { 26 + pubRecord: PubLeafletPublication.Record; 27 + uri: string | undefined; 28 + href: string; 29 + }) => { 30 + return ( 31 + <> 32 + <h3 className="pb-2">You haven't been invited to contribute yet...</h3> 33 + <PubLink {...props} /> 34 + 35 + <div> 36 + If you are expecting an invite, please check that the owner of this 37 + publication added you to the invited contributors list. 38 + </div> 39 + </> 40 + ); 41 + }; 42 + 43 + export const LoggedOutContent = (props: { 44 + pubRecord: PubLeafletPublication.Record; 45 + uri: string | undefined; 46 + href: string; 47 + }) => { 48 + return ( 49 + <> 50 + <h2>Log in to Contribute</h2> 51 + <PubLink {...props} /> 52 + <div className="pb-2"> 53 + Log in with an AT Proto handle to contribute this publication 54 + </div> 55 + <BlueskyLogin redirectRoute={`${props.href}/invite-contributor`} /> 56 + </> 57 + ); 58 + }; 59 + 60 + const PubLink = (props: { 61 + pubRecord: PubLeafletPublication.Record; 62 + uri: string | undefined; 63 + href: string; 64 + }) => { 65 + return ( 66 + <SpeedyLink 67 + href={props.href} 68 + className="p-4 flex flex-col justify-center text-center border border-border rounded-lg mt-2 mb-4 hover:no-underline! no-underline!" 69 + > 70 + <PubIcon 71 + large 72 + record={props.pubRecord} 73 + uri={props.uri} 74 + className="mx-auto mb-3 mt-1" 75 + /> 76 + <h3 className="leading-tight">{props.pubRecord.name}</h3> 77 + <div className="text-tertiary italic">{props.pubRecord.description}</div> 78 + </SpeedyLink> 79 + ); 80 + };
+4 -82
app/lish/[did]/[publication]/invite-contributor/page.tsx
··· 5 5 } from "components/ThemeManager/PublicationThemeProvider"; 6 6 import { PubLeafletPublication } from "lexicons/api"; 7 7 import { supabaseServerClient } from "supabase/serverClient"; 8 - import { ButtonPrimary } from "components/Buttons"; 9 8 import { PubNotFound } from "../PubNotFound"; 10 - import { PubIcon } from "components/ActionBar/Publications"; 11 - import { SpeedyLink } from "components/SpeedyLink"; 12 - import { BlueskyLogin } from "app/login/LoginForm"; 13 9 import { getIdentityData } from "actions/getIdentityData"; 14 10 import { getPublicationURL } from "app/lish/createPub/getPublicationURL"; 11 + import { InvitedContent, NotInvitedContent, LoggedOutContent } from "./content"; 15 12 16 13 export default async function Publication(props: { 17 14 params: Promise<{ publication: string; did: string }>; ··· 71 68 <LoggedOutContent 72 69 pubRecord={pubRecord} 73 70 uri={publication.uri} 74 - pubUrl={pubUrl} 71 + href={pubUrl} 75 72 /> 76 73 ) : invited ? ( 77 74 <InvitedContent 78 75 pubRecord={pubRecord} 79 76 uri={publication.uri} 80 - pubUrl={pubUrl} 77 + href={pubUrl} 81 78 /> 82 79 ) : ( 83 80 <NotInvitedContent 84 81 pubRecord={pubRecord} 85 82 uri={publication.uri} 86 - pubUrl={pubUrl} 83 + href={pubUrl} 87 84 /> 88 85 )} 89 86 </div> ··· 92 89 </PublicationThemeProvider> 93 90 ); 94 91 } 95 - 96 - const InvitedContent = (props: { 97 - pubRecord: PubLeafletPublication.Record; 98 - uri: string; 99 - pubUrl: string; 100 - }) => { 101 - return ( 102 - <> 103 - <h2>Become a Contributor!</h2> 104 - <PubLink {...props} /> 105 - <div> 106 - You've been invited to write for <br /> 107 - {props.pubRecord.name}!{" "} 108 - </div> 109 - <ButtonPrimary className="mx-auto mt-4 mb-2">Accept Invite</ButtonPrimary> 110 - </> 111 - ); 112 - }; 113 - 114 - const NotInvitedContent = (props: { 115 - pubRecord: PubLeafletPublication.Record; 116 - uri: string; 117 - pubUrl: string; 118 - }) => { 119 - return ( 120 - <> 121 - <h3 className="pb-2">You haven't been invited to contribute yet...</h3> 122 - <PubLink {...props} /> 123 - 124 - <div> 125 - If you are expecting an invite, please check that the owner of this 126 - publication added you to the invited contributors list. 127 - </div> 128 - </> 129 - ); 130 - }; 131 - 132 - const LoggedOutContent = (props: { 133 - pubRecord: PubLeafletPublication.Record; 134 - uri: string; 135 - pubUrl: string; 136 - }) => { 137 - return ( 138 - <> 139 - <h2>Log in to Contribute</h2> 140 - <PubLink {...props} /> 141 - <div className="pb-2"> 142 - Log in with an AT Proto handle to contribute this publication 143 - </div> 144 - <BlueskyLogin redirectRoute={`${props.pubUrl}/invite-contributor`} /> 145 - </> 146 - ); 147 - }; 148 - 149 - const PubLink = (props: { 150 - pubRecord: PubLeafletPublication.Record; 151 - uri: string; 152 - pubUrl: string; 153 - }) => { 154 - return ( 155 - <SpeedyLink 156 - href={props.pubUrl} 157 - className="p-4 flex flex-col justify-center text-center border border-border rounded-lg mt-2 mb-4 hover:no-underline! no-underline!" 158 - > 159 - <PubIcon 160 - large 161 - record={props.pubRecord} 162 - uri={props.uri} 163 - className="mx-auto mb-3 mt-1" 164 - /> 165 - <h3 className="leading-tight">{props.pubRecord.name}</h3> 166 - <div className="text-tertiary italic">{props.pubRecord.description}</div> 167 - </SpeedyLink> 168 - ); 169 - };
+2 -2
components/ActionBar/Publications.tsx
··· 157 157 158 158 export const PubIcon = (props: { 159 159 record: PubLeafletPublication.Record; 160 - uri: string; 160 + uri: string | undefined; 161 161 small?: boolean; 162 162 large?: boolean; 163 163 className?: string; ··· 166 166 167 167 let iconSizeClassName = `${props.small ? "w-4 h-4" : props.large ? "w-12 h-12" : "w-6 h-6"} rounded-full`; 168 168 169 - return props.record.icon ? ( 169 + return props.record.icon && props.uri ? ( 170 170 <div 171 171 className={`${iconSizeClassName} ${props.className} relative overflow-hidden`} 172 172 >
+4 -4
components/Avatar.tsx
··· 3 3 export const Avatar = (props: { 4 4 src: string | undefined; 5 5 displayName: string | undefined; 6 - tiny?: boolean; 6 + small?: boolean; 7 7 }) => { 8 8 if (props.src) 9 9 return ( 10 10 <img 11 - className={`${props.tiny ? "w-4 h-4" : "w-5 h-5"} rounded-full shrink-0 border border-border-light`} 11 + className={`${props.small ? "w-4 h-4" : "w-6 h-6"} rounded-full shrink-0 border border-border-light`} 12 12 src={props.src} 13 13 alt={ 14 14 props.displayName ··· 20 20 else 21 21 return ( 22 22 <div 23 - className={`bg-[var(--accent-light)] flex rounded-full shrink-0 border border-border-light place-items-center justify-center text-accent-1 ${props.tiny ? "w-4 h-4" : "w-5 h-5"}`} 23 + className={`bg-[var(--accent-light)] flex rounded-full shrink-0 border border-border-light place-items-center justify-center text-accent-1 ${props.small ? "w-4 h-4" : "w-5 h-5"}`} 24 24 > 25 - <AccountTiny className={props.tiny ? "scale-80" : "scale-90"} /> 25 + <AccountTiny className={props.small ? "scale-80" : "scale-90"} /> 26 26 </div> 27 27 ); 28 28 };