a tool for shared writing and social publishing

add some scroll preservation to pub layouts

+136 -113
+24
app/lish/[did]/[publication]/PublicationHomeLayout.tsx
··· 1 + "use client"; 2 + 3 + import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 4 + 5 + export function PublicationHomeLayout(props: { 6 + uri: string; 7 + showPageBackground: boolean; 8 + children: React.ReactNode; 9 + }) { 10 + let { ref } = usePreserveScroll<HTMLDivElement>(props.uri); 11 + return ( 12 + <div 13 + ref={props.showPageBackground ? null : ref} 14 + className={`pubWrapper flex flex-col sm:py-6 h-full ${props.showPageBackground ? "max-w-prose mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 15 + > 16 + <div 17 + ref={!props.showPageBackground ? null : ref} 18 + className={`pub sm:max-w-prose max-w-(--page-width-units) w-[1000px] mx-auto px-3 sm:px-4 py-5 ${props.showPageBackground ? "overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg" : "h-fit"}`} 19 + > 20 + {props.children} 21 + </div> 22 + </div> 23 + ); 24 + }
+109 -113
app/lish/[did]/[publication]/page.tsx
··· 15 15 import { QuoteTiny } from "components/Icons/QuoteTiny"; 16 16 import { CommentTiny } from "components/Icons/CommentTiny"; 17 17 import { LocalizedDate } from "./LocalizedDate"; 18 + import { PublicationHomeLayout } from "./PublicationHomeLayout"; 18 19 19 20 export default async function Publication(props: { 20 21 params: Promise<{ publication: string; did: string }>; ··· 66 67 record={record} 67 68 pub_creator={publication.identity_did} 68 69 > 69 - <div 70 - className={`pubWrapper flex flex-col sm:py-6 h-full ${showPageBackground ? "max-w-prose mx-auto sm:px-0 px-[6px] py-2" : "w-full overflow-y-scroll"}`} 70 + <PublicationHomeLayout 71 + uri={publication.uri} 72 + showPageBackground={showPageBackground} 71 73 > 72 - <div 73 - className={`pub sm:max-w-prose max-w-(--page-width-units) w-[1000px] mx-auto px-3 sm:px-4 py-5 ${showPageBackground ? "overflow-auto h-full bg-[rgba(var(--bg-page),var(--bg-page-alpha))] border border-border rounded-lg" : "h-fit"}`} 74 - > 75 - <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 76 - {record?.icon && ( 77 - <div 78 - className="shrink-0 w-10 h-10 rounded-full mx-auto" 79 - style={{ 80 - backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 81 - backgroundRepeat: "no-repeat", 82 - backgroundPosition: "center", 83 - backgroundSize: "cover", 84 - }} 85 - /> 86 - )} 87 - <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 88 - {publication.name} 89 - </h2> 90 - <p className="sm:text-lg text-secondary"> 91 - {record?.description}{" "} 74 + <div className="pubHeader flex flex-col pb-8 w-full text-center justify-center "> 75 + {record?.icon && ( 76 + <div 77 + className="shrink-0 w-10 h-10 rounded-full mx-auto" 78 + style={{ 79 + backgroundImage: `url(/api/atproto_images?did=${did}&cid=${(record.icon.ref as unknown as { $link: string })["$link"]})`, 80 + backgroundRepeat: "no-repeat", 81 + backgroundPosition: "center", 82 + backgroundSize: "cover", 83 + }} 84 + /> 85 + )} 86 + <h2 className="text-accent-contrast sm:text-xl text-[22px] pt-1 "> 87 + {publication.name} 88 + </h2> 89 + <p className="sm:text-lg text-secondary"> 90 + {record?.description}{" "} 91 + </p> 92 + {profile && ( 93 + <p className="italic text-tertiary sm:text-base text-sm"> 94 + <strong className="">by {profile.displayName}</strong>{" "} 95 + <a 96 + className="text-tertiary" 97 + href={`https://bsky.app/profile/${profile.handle}`} 98 + > 99 + @{profile.handle} 100 + </a> 92 101 </p> 93 - {profile && ( 94 - <p className="italic text-tertiary sm:text-base text-sm"> 95 - <strong className="">by {profile.displayName}</strong>{" "} 96 - <a 97 - className="text-tertiary" 98 - href={`https://bsky.app/profile/${profile.handle}`} 99 - > 100 - @{profile.handle} 101 - </a> 102 - </p> 103 - )} 104 - <div className="sm:pt-4 pt-4"> 105 - <SubscribeWithBluesky 106 - base_url={getPublicationURL(publication)} 107 - pubName={publication.name} 108 - pub_uri={publication.uri} 109 - subscribers={publication.publication_subscriptions} 110 - /> 111 - </div> 102 + )} 103 + <div className="sm:pt-4 pt-4"> 104 + <SubscribeWithBluesky 105 + base_url={getPublicationURL(publication)} 106 + pubName={publication.name} 107 + pub_uri={publication.uri} 108 + subscribers={publication.publication_subscriptions} 109 + /> 112 110 </div> 113 - <div className="publicationPostList w-full flex flex-col gap-4"> 114 - {publication.documents_in_publications 115 - .filter((d) => !!d?.documents) 116 - .sort((a, b) => { 117 - let aRecord = a.documents 118 - ?.data! as PubLeafletDocument.Record; 119 - let bRecord = b.documents 120 - ?.data! as PubLeafletDocument.Record; 121 - const aDate = aRecord.publishedAt 122 - ? new Date(aRecord.publishedAt) 123 - : new Date(0); 124 - const bDate = bRecord.publishedAt 125 - ? new Date(bRecord.publishedAt) 126 - : new Date(0); 127 - return bDate.getTime() - aDate.getTime(); // Sort by most recent first 128 - }) 129 - .map((doc) => { 130 - if (!doc.documents) return null; 131 - let uri = new AtUri(doc.documents.uri); 132 - let doc_record = doc.documents 133 - .data as PubLeafletDocument.Record; 134 - let quotes = 135 - doc.documents.document_mentions_in_bsky[0].count || 0; 136 - let comments = 137 - record?.preferences?.showComments === false 138 - ? 0 139 - : doc.documents.comments_on_documents[0].count || 0; 111 + </div> 112 + <div className="publicationPostList w-full flex flex-col gap-4"> 113 + {publication.documents_in_publications 114 + .filter((d) => !!d?.documents) 115 + .sort((a, b) => { 116 + let aRecord = a.documents?.data! as PubLeafletDocument.Record; 117 + let bRecord = b.documents?.data! as PubLeafletDocument.Record; 118 + const aDate = aRecord.publishedAt 119 + ? new Date(aRecord.publishedAt) 120 + : new Date(0); 121 + const bDate = bRecord.publishedAt 122 + ? new Date(bRecord.publishedAt) 123 + : new Date(0); 124 + return bDate.getTime() - aDate.getTime(); // Sort by most recent first 125 + }) 126 + .map((doc) => { 127 + if (!doc.documents) return null; 128 + let uri = new AtUri(doc.documents.uri); 129 + let doc_record = doc.documents 130 + .data as PubLeafletDocument.Record; 131 + let quotes = 132 + doc.documents.document_mentions_in_bsky[0].count || 0; 133 + let comments = 134 + record?.preferences?.showComments === false 135 + ? 0 136 + : doc.documents.comments_on_documents[0].count || 0; 140 137 141 - return ( 142 - <React.Fragment key={doc.documents?.uri}> 143 - <div className="flex w-full grow flex-col "> 144 - <SpeedyLink 145 - href={`${getPublicationURL(publication)}/${uri.rkey}`} 146 - className="publishedPost hover:no-underline! flex flex-col" 147 - > 148 - <h3 className="text-primary">{doc_record.title}</h3> 149 - <p className="italic text-secondary"> 150 - {doc_record.description} 151 - </p> 152 - </SpeedyLink> 138 + return ( 139 + <React.Fragment key={doc.documents?.uri}> 140 + <div className="flex w-full grow flex-col "> 141 + <SpeedyLink 142 + href={`${getPublicationURL(publication)}/${uri.rkey}`} 143 + className="publishedPost hover:no-underline! flex flex-col" 144 + > 145 + <h3 className="text-primary">{doc_record.title}</h3> 146 + <p className="italic text-secondary"> 147 + {doc_record.description} 148 + </p> 149 + </SpeedyLink> 153 150 154 - <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2"> 155 - <p className="text-sm text-tertiary "> 156 - {doc_record.publishedAt && ( 157 - <LocalizedDate 158 - dateString={doc_record.publishedAt} 159 - options={{ 160 - year: "numeric", 161 - month: "long", 162 - day: "2-digit", 163 - }} 164 - /> 165 - )}{" "} 166 - </p> 167 - {comments > 0 || quotes > 0 ? "| " : ""} 168 - {quotes > 0 && ( 151 + <div className="text-sm text-tertiary flex gap-1 flex-wrap pt-2"> 152 + <p className="text-sm text-tertiary "> 153 + {doc_record.publishedAt && ( 154 + <LocalizedDate 155 + dateString={doc_record.publishedAt} 156 + options={{ 157 + year: "numeric", 158 + month: "long", 159 + day: "2-digit", 160 + }} 161 + /> 162 + )}{" "} 163 + </p> 164 + {comments > 0 || quotes > 0 ? "| " : ""} 165 + {quotes > 0 && ( 166 + <SpeedyLink 167 + href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=quotes`} 168 + className="flex flex-row gap-0 text-sm text-tertiary items-center flex-wrap" 169 + > 170 + <QuoteTiny /> {quotes} 171 + </SpeedyLink> 172 + )} 173 + {comments > 0 && 174 + record?.preferences?.showComments !== false && ( 169 175 <SpeedyLink 170 - href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=quotes`} 176 + href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=comments`} 171 177 className="flex flex-row gap-0 text-sm text-tertiary items-center flex-wrap" 172 178 > 173 - <QuoteTiny /> {quotes} 179 + <CommentTiny /> {comments} 174 180 </SpeedyLink> 175 181 )} 176 - {comments > 0 && 177 - record?.preferences?.showComments !== false && ( 178 - <SpeedyLink 179 - href={`${getPublicationURL(publication)}/${uri.rkey}?interactionDrawer=comments`} 180 - className="flex flex-row gap-0 text-sm text-tertiary items-center flex-wrap" 181 - > 182 - <CommentTiny /> {comments} 183 - </SpeedyLink> 184 - )} 185 - </div> 186 182 </div> 187 - <hr className="last:hidden border-border-light" /> 188 - </React.Fragment> 189 - ); 190 - })} 191 - </div> 183 + </div> 184 + <hr className="last:hidden border-border-light" /> 185 + </React.Fragment> 186 + ); 187 + })} 192 188 </div> 193 - </div> 189 + </PublicationHomeLayout> 194 190 </PublicationBackgroundProvider> 195 191 </PublicationThemeProvider> 196 192 );
+3
components/Pages/Page.tsx
··· 16 16 import { PageOptions } from "./PageOptions"; 17 17 import { CardThemeProvider } from "components/ThemeManager/ThemeProvider"; 18 18 import { useDrawerOpen } from "app/lish/[did]/[publication]/[rkey]/Interactions/InteractionDrawer"; 19 + import { usePreserveScroll } from "src/hooks/usePreserveScroll"; 19 20 20 21 export function Page(props: { 21 22 entityID: string; ··· 83 84 pageType: "canvas" | "doc"; 84 85 drawerOpen: boolean | undefined; 85 86 }) => { 87 + let { ref } = usePreserveScroll(props.id); 86 88 return ( 87 89 // this div wraps the contents AND the page options. 88 90 // it needs to be its own div because this container does NOT scroll, and therefore doesn't clip the absolutely positioned pageOptions ··· 95 97 it needs to be a separate div so that the user can scroll from anywhere on the page if there isn't a card border 96 98 */} 97 99 <div 100 + ref={ref} 98 101 onClick={props.onClickAction} 99 102 id={props.id} 100 103 className={`