a tool for shared writing and social publishing

Merge main into refactor/standard.site

Resolved conflicts:
- actions/publishToPublication.ts: Keep normalization approach and dual-schema
support from branch, add backdate feature (publishedAt param) from main
- components/Pages/PublicationMetadata.tsx: Use normalizedPublication from
branch with !== false pattern from main for showMentions/showComments defaults

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

+329 -104
+6 -3
actions/publishToPublication.ts
··· 77 77 tags, 78 78 cover_image, 79 79 entitiesToDelete, 80 + publishedAt, 80 81 }: { 81 82 root_entity: string; 82 83 publication_uri?: string; ··· 86 87 tags?: string[]; 87 88 cover_image?: string | null; 88 89 entitiesToDelete?: string[]; 90 + publishedAt?: string; 89 91 }): Promise<PublishResult> { 90 92 let identity = await getIdentityData(); 91 93 if (!identity || !identity.atp_did) { ··· 233 235 title: title || "Untitled", 234 236 site: siteUri, 235 237 path: rkey, 236 - publishedAt: existingRecord.publishedAt || new Date().toISOString(), 238 + publishedAt: 239 + publishedAt || existingRecord.publishedAt || new Date().toISOString(), 237 240 ...(description && { description }), 238 241 ...(tags !== undefined && { tags }), 239 242 ...(coverImageBlob && { coverImage: coverImageBlob }), ··· 248 251 // pub.leaflet.document format (legacy) 249 252 record = { 250 253 $type: "pub.leaflet.document", 251 - publishedAt: new Date().toISOString(), 252 - ...existingRecord, 253 254 author: credentialSession.did!, 254 255 ...(publication_uri && { publication: publication_uri }), 255 256 ...(theme && { theme }), ··· 258 259 ...(tags !== undefined && { tags }), 259 260 ...(coverImageBlob && { coverImage: coverImageBlob }), 260 261 pages: pagesArray, 262 + publishedAt: 263 + publishedAt || existingRecord.publishedAt || new Date().toISOString(), 261 264 } satisfies PubLeafletDocument.Record; 262 265 } 263 266
+9 -2
app/[leaflet_id]/actions/PublishButton.tsx
··· 24 24 import { DotLoader } from "components/utils/DotLoader"; 25 25 import { normalizePublicationRecord } from "src/utils/normalizeRecords"; 26 26 import { useParams, useRouter, useSearchParams } from "next/navigation"; 27 - import { useState, useMemo } from "react"; 27 + import { useState, useMemo, useEffect } from "react"; 28 28 import { useIsMobile } from "src/hooks/isMobile"; 29 29 import { useReplicache, useEntity } from "src/replicache"; 30 30 import { useSubscribe } from "src/replicache/useSubscribe"; ··· 40 40 import { moveLeafletToPublication } from "actions/publications/moveLeafletToPublication"; 41 41 import { AddTiny } from "components/Icons/AddTiny"; 42 42 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 43 + import { useLocalPublishedAt } from "components/Pages/Backdater"; 43 44 44 45 export const PublishButton = (props: { entityID: string }) => { 45 46 let { data: pub } = useLeafletPublicationData(); ··· 95 96 tx.get<string | null>("publication_cover_image"), 96 97 ); 97 98 99 + // Get local published at from Replicache (session-only state, not persisted to DB) 100 + let publishedAt = useLocalPublishedAt((s) => 101 + pub?.doc ? s[pub?.doc] : undefined, 102 + ); 103 + 98 104 return ( 99 105 <ActionButton 100 106 primary ··· 111 117 description: currentDescription, 112 118 tags: currentTags, 113 119 cover_image: coverImage, 120 + publishedAt: publishedAt?.toISOString(), 114 121 }); 115 122 setIsLoading(false); 116 123 mutate(); ··· 134 141 135 142 toaster({ 136 143 content: ( 137 - <div> 144 + <div className="font-bold"> 138 145 {pub.doc ? "Updated! " : "Published! "} 139 146 <SpeedyLink className="underline" href={docUrl}> 140 147 See Published Post
+114 -11
app/[leaflet_id]/publish/PublishPost.tsx
··· 23 23 import { LooseLeafSmall } from "components/Icons/LooseleafSmall"; 24 24 import { PubIcon } from "components/ActionBar/Publications"; 25 25 import { OAuthErrorMessage, isOAuthSessionError } from "components/OAuthError"; 26 + import { DatePicker, TimePicker } from "components/DatePicker"; 27 + import { Popover } from "components/Popover"; 28 + import { useLocalizedDate } from "src/hooks/useLocalizedDate"; 29 + import { Separator } from "react-aria-components"; 30 + import { setHours, setMinutes } from "date-fns"; 26 31 27 32 type Props = { 28 33 title: string; ··· 78 83 ); 79 84 let [localTags, setLocalTags] = useState<string[]>([]); 80 85 86 + let [localPublishedAt, setLocalPublishedAt] = useState<Date | undefined>( 87 + undefined, 88 + ); 81 89 // Get cover image from Replicache 82 90 let replicacheCoverImage = useSubscribe(rep, (tx) => 83 91 tx.get<string | null>("publication_cover_image"), 84 92 ); 85 93 86 94 // Use Replicache tags only when we have a draft 87 - const hasDraft = props.hasDraft; 88 - const currentTags = hasDraft 95 + const currentTags = props.hasDraft 89 96 ? Array.isArray(replicacheTags) 90 97 ? replicacheTags 91 98 : [] ··· 93 100 94 101 // Update tags via Replicache mutation or local state depending on context 95 102 const handleTagsChange = async (newTags: string[]) => { 96 - if (hasDraft) { 103 + if (props.hasDraft) { 97 104 await rep?.mutate.updatePublicationDraft({ 98 105 tags: newTags, 99 106 }); ··· 116 123 tags: currentTags, 117 124 cover_image: replicacheCoverImage, 118 125 entitiesToDelete: props.entitiesToDelete, 126 + publishedAt: localPublishedAt?.toISOString() || new Date().toISOString(), 119 127 }); 120 128 121 129 if (!result.success) { ··· 168 176 record={props.record} 169 177 /> 170 178 <hr className="border-border" /> 171 - <ShareOptions 172 - setShareOption={setShareOption} 173 - shareOption={shareOption} 174 - charCount={charCount} 175 - setCharCount={setCharCount} 176 - editorStateRef={editorStateRef} 177 - {...props} 179 + 180 + <BackdateOptions 181 + publishedAt={localPublishedAt} 182 + setPublishedAt={setLocalPublishedAt} 178 183 /> 179 184 <hr className="border-border " /> 185 + 180 186 <div className="flex flex-col gap-2"> 181 187 <h4>Tags</h4> 182 188 <TagSelector ··· 184 190 setSelectedTags={handleTagsChange} 185 191 /> 186 192 </div> 193 + <hr className="border-border" /> 194 + <ShareOptions 195 + setShareOption={setShareOption} 196 + shareOption={shareOption} 197 + charCount={charCount} 198 + setCharCount={setCharCount} 199 + editorStateRef={editorStateRef} 200 + {...props} 201 + /> 187 202 <hr className="border-border mb-2" /> 188 203 189 204 <div className="flex flex-col gap-2"> ··· 219 234 ); 220 235 }; 221 236 237 + const BackdateOptions = (props: { 238 + publishedAt: Date | undefined; 239 + setPublishedAt: (date: Date | undefined) => void; 240 + }) => { 241 + const formattedDate = useLocalizedDate( 242 + props.publishedAt?.toISOString() || "", 243 + { 244 + month: "short", 245 + day: "numeric", 246 + year: "numeric", 247 + hour: "numeric", 248 + minute: "numeric", 249 + hour12: true, 250 + }, 251 + ); 252 + 253 + const [timeValue, setTimeValue] = useState<string>(() => { 254 + const date = props.publishedAt || new Date(); 255 + return `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}`; 256 + }); 257 + 258 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 259 + 260 + const handleTimeChange = (time: string) => { 261 + setTimeValue(time); 262 + if (!props.publishedAt) return; 263 + 264 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 265 + const newDate = setHours(setMinutes(props.publishedAt, minutes), hours); 266 + const currentDate = new Date(); 267 + 268 + if (newDate > currentDate) { 269 + props.setPublishedAt(currentDate); 270 + setTimeValue(currentTime); 271 + } else props.setPublishedAt(newDate); 272 + }; 273 + 274 + const handleDateChange = (date: Date | undefined) => { 275 + if (!date) { 276 + props.setPublishedAt(undefined); 277 + return; 278 + } 279 + const [hours, minutes] = timeValue 280 + .split(":") 281 + .map((str) => parseInt(str, 10)); 282 + const newDate = new Date( 283 + date.getFullYear(), 284 + date.getMonth(), 285 + date.getDate(), 286 + hours, 287 + minutes, 288 + ); 289 + const currentDate = new Date(); 290 + if (newDate > currentDate) { 291 + props.setPublishedAt(currentDate); 292 + setTimeValue(currentTime); 293 + } else props.setPublishedAt(newDate); 294 + }; 295 + 296 + return ( 297 + <div className="flex justify-between gap-2"> 298 + <h4>Publish Date</h4> 299 + <Popover 300 + className="w-64 px-2!" 301 + trigger={ 302 + props.publishedAt ? ( 303 + <div className="text-secondary">{formattedDate}</div> 304 + ) : ( 305 + <div className="text-tertiary italic">now</div> 306 + ) 307 + } 308 + > 309 + <div className="flex flex-col gap-3"> 310 + <DatePicker 311 + selected={props.publishedAt} 312 + onSelect={handleDateChange} 313 + disabled={(date) => date > new Date()} 314 + /> 315 + <Separator className="border-border" /> 316 + <div className="flex gap-4 pb-1 items-center"> 317 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 318 + </div> 319 + </div> 320 + </Popover> 321 + </div> 322 + ); 323 + }; 324 + 222 325 const ShareOptions = (props: { 223 326 shareOption: "quiet" | "bluesky"; 224 327 setShareOption: (option: typeof props.shareOption) => void; ··· 232 335 }) => { 233 336 return ( 234 337 <div className="flex flex-col gap-2"> 235 - <h4>Notifications</h4> 338 + <h4>Share and Notify</h4> 236 339 <Radio 237 340 checked={props.shareOption === "quiet"} 238 341 onChange={(e) => {
+10 -2
app/lish/[did]/[publication]/[rkey]/Interactions/Comments/index.tsx
··· 119 119 }) => { 120 120 const did = props.comment.bsky_profiles?.did; 121 121 122 + let timeAgoDate = timeAgo(props.record.createdAt); 123 + const formattedDate = useLocalizedDate(props.record.createdAt, { 124 + year: "numeric", 125 + month: "long", 126 + day: "2-digit", 127 + }); 128 + 122 129 return ( 123 130 <div id={props.comment.uri} className="comment"> 124 131 <div className="flex gap-2"> 125 - {did && ( 132 + {did ? ( 126 133 <ProfilePopover 127 134 didOrHandle={did} 128 135 trigger={ ··· 131 138 </div> 132 139 } 133 140 /> 134 - )} 141 + ) : null} 142 + <div className="text-sm text-tertiary">{timeAgoDate}</div> 135 143 </div> 136 144 {props.record.attachment && 137 145 PubLeafletComment.isLinearDocumentQuote(props.record.attachment) && (
+2 -32
components/Blocks/DateTimeBlock.tsx
··· 1 1 import { useEntity, useReplicache } from "src/replicache"; 2 2 import { BlockProps, BlockLayout } from "./Block"; 3 - import { ChevronProps, DayPicker } from "react-day-picker"; 4 3 import { Popover } from "components/Popover"; 5 4 import { useEffect, useMemo, useState } from "react"; 6 5 import { useEntitySetContext } from "components/EntitySetProvider"; ··· 10 9 import { Checkbox } from "components/Checkbox"; 11 10 import { useHasPageLoaded } from "components/InitialPageLoadProvider"; 12 11 import { useSpring, animated } from "@react-spring/web"; 13 - import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 14 12 import { BlockCalendarSmall } from "components/Icons/BlockCalendarSmall"; 13 + import { DatePicker } from "components/DatePicker"; 15 14 16 15 export function DateTimeBlock(props: BlockProps) { 17 16 const [isClient, setIsClient] = useState(false); ··· 167 166 } 168 167 > 169 168 <div className="flex flex-col gap-3 "> 170 - <DayPicker 171 - components={{ 172 - Chevron: (props: ChevronProps) => <CustomChevron {...props} />, 173 - }} 174 - classNames={{ 175 - months: "relative", 176 - month_caption: 177 - "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", 178 - button_next: 179 - "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", 180 - button_previous: 181 - "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center ", 182 - chevron: "text-inherit", 183 - month_grid: "w-full table-fixed", 184 - weekdays: "text-secondary text-sm", 185 - selected: "bg-accent-1! text-accent-2 rounded-md font-bold", 186 - 187 - day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", 188 - outside: "text-border", 189 - today: "font-bold", 190 - }} 191 - mode="single" 169 + <DatePicker 192 170 selected={dateFact ? selectedDate : undefined} 193 171 onSelect={handleDaySelect} 194 172 /> ··· 230 208 let spring = useSpring({ opacity: props.active ? 1 : 0 }); 231 209 return <animated.div style={spring}>{props.children}</animated.div>; 232 210 }; 233 - 234 - const CustomChevron = (props: ChevronProps) => { 235 - return ( 236 - <div {...props} className="w-full pointer-events-none"> 237 - <ArrowRightTiny /> 238 - </div> 239 - ); 240 - };
+71
components/DatePicker.tsx
··· 1 + import { ChevronProps, DayPicker as ReactDayPicker } from "react-day-picker"; 2 + import { ArrowRightTiny } from "components/Icons/ArrowRightTiny"; 3 + 4 + const CustomChevron = (props: ChevronProps) => { 5 + return ( 6 + <div {...props} className="w-full pointer-events-none"> 7 + <ArrowRightTiny /> 8 + </div> 9 + ); 10 + }; 11 + 12 + interface DayPickerProps { 13 + selected: Date | undefined; 14 + onSelect: (date: Date | undefined) => void; 15 + disabled?: (date: Date) => boolean; 16 + } 17 + 18 + export const DatePicker = ({ 19 + selected, 20 + onSelect, 21 + disabled, 22 + }: DayPickerProps) => { 23 + return ( 24 + <ReactDayPicker 25 + components={{ 26 + Chevron: (props: ChevronProps) => <CustomChevron {...props} />, 27 + }} 28 + classNames={{ 29 + months: "relative", 30 + month_caption: 31 + "font-bold text-center w-full bg-border-light mb-2 py-1 rounded-md", 32 + button_next: 33 + "absolute right-0 top-1 p-1 text-secondary hover:text-accent-contrast flex align-center", 34 + button_previous: 35 + "absolute left-0 top-1 p-1 text-secondary hover:text-accent-contrast rotate-180 flex align-center", 36 + chevron: "text-inherit", 37 + month_grid: "w-full table-fixed", 38 + weekdays: "text-secondary text-sm", 39 + selected: "bg-accent-1! text-accent-2 rounded-md font-bold", 40 + day: "h-[34px] text-center rounded-md sm:hover:bg-border-light", 41 + outside: "text-tertiary", 42 + today: "font-bold", 43 + disabled: "text-border cursor-not-allowed hover:bg-transparent!", 44 + }} 45 + mode="single" 46 + selected={selected} 47 + defaultMonth={selected} 48 + onSelect={onSelect} 49 + disabled={disabled} 50 + /> 51 + ); 52 + }; 53 + 54 + export const TimePicker = (props: { 55 + value: string; 56 + onChange: (time: string) => void; 57 + className?: string; 58 + }) => { 59 + let handleTimeChange: React.ChangeEventHandler<HTMLInputElement> = (e) => { 60 + props.onChange(e.target.value); 61 + }; 62 + 63 + return ( 64 + <input 65 + type="time" 66 + value={props.value} 67 + onChange={handleTimeChange} 68 + className={`dateBlockTimeInput input-with-border bg-bg-page text-primary w-full ${props.className}`} 69 + /> 70 + ); 71 + };
+2 -2
components/InteractionsPreview.tsx
··· 38 38 </> 39 39 )} 40 40 41 - {props.showMentions || props.quotesCount === 0 ? null : ( 41 + {!props.showMentions || props.quotesCount === 0 ? null : ( 42 42 <SpeedyLink 43 43 aria-label="Post quotes" 44 44 href={`${props.postUrl}?interactionDrawer=quotes`} ··· 47 47 <QuoteTiny /> {props.quotesCount} 48 48 </SpeedyLink> 49 49 )} 50 - {props.showComments === false || props.commentsCount === 0 ? null : ( 50 + {!props.showComments || props.commentsCount === 0 ? null : ( 51 51 <SpeedyLink 52 52 aria-label="Post comments" 53 53 href={`${props.postUrl}?interactionDrawer=comments`}
+84
components/Pages/Backdater.tsx
··· 1 + "use client"; 2 + import { DatePicker, TimePicker } from "components/DatePicker"; 3 + import { useMemo, useState } from "react"; 4 + import { timeAgo } from "src/utils/timeAgo"; 5 + import { Popover } from "components/Popover"; 6 + import { Separator } from "react-aria-components"; 7 + import { useReplicache } from "src/replicache"; 8 + import { create } from "zustand"; 9 + 10 + export const useLocalPublishedAt = create<{ [uri: string]: Date }>(() => ({})); 11 + export const Backdater = (props: { publishedAt: string; docURI: string }) => { 12 + let { rep } = useReplicache(); 13 + let localPublishedAtDate = useLocalPublishedAt((s) => 14 + s[props.docURI] ? s[props.docURI] : null, 15 + ); 16 + let localPublishedAt = useMemo( 17 + () => localPublishedAtDate || new Date(props.publishedAt), 18 + [localPublishedAtDate, props.publishedAt], 19 + ); 20 + 21 + let [timeValue, setTimeValue] = useState( 22 + `${localPublishedAt.getHours().toString().padStart(2, "0")}:${localPublishedAt.getMinutes().toString().padStart(2, "0")}`, 23 + ); 24 + 25 + let currentTime = `${new Date().getHours().toString().padStart(2, "0")}:${new Date().getMinutes().toString().padStart(2, "0")}`; 26 + 27 + const handleTimeChange = async (time: string) => { 28 + setTimeValue(time); 29 + const [hours, minutes] = time.split(":").map((str) => parseInt(str, 10)); 30 + const newDate = new Date(localPublishedAt); 31 + newDate.setHours(hours); 32 + newDate.setMinutes(minutes); 33 + 34 + let currentDate = new Date(); 35 + if (newDate > currentDate) { 36 + useLocalPublishedAt.setState({ [props.docURI]: currentDate }); 37 + setTimeValue(currentTime); 38 + } else { 39 + useLocalPublishedAt.setState({ [props.docURI]: newDate }); 40 + } 41 + }; 42 + 43 + const handleDateChange = async (date: Date | undefined) => { 44 + if (!date) return; 45 + const [hours, minutes] = timeValue 46 + .split(":") 47 + .map((str) => parseInt(str, 10)); 48 + const newDate = new Date(date); 49 + newDate.setHours(hours); 50 + newDate.setMinutes(minutes); 51 + 52 + let currentDate = new Date(); 53 + if (newDate > currentDate) { 54 + useLocalPublishedAt.setState({ [props.docURI]: currentDate }); 55 + 56 + setTimeValue(currentTime); 57 + } else { 58 + useLocalPublishedAt.setState({ [props.docURI]: newDate }); 59 + } 60 + }; 61 + 62 + return ( 63 + <Popover 64 + className="w-64 z-10 px-2!" 65 + trigger={ 66 + <div className="underline"> 67 + {timeAgo(localPublishedAt.toISOString())} 68 + </div> 69 + } 70 + > 71 + <div className="flex flex-col gap-3"> 72 + <DatePicker 73 + selected={localPublishedAt} 74 + onSelect={handleDateChange} 75 + disabled={(date) => date > new Date()} 76 + /> 77 + <Separator className="border-border" /> 78 + <div className="flex gap-4 pb-1 items-center"> 79 + <TimePicker value={timeValue} onChange={handleTimeChange} /> 80 + </div> 81 + </div> 82 + </Popover> 83 + ); 84 + };
+10 -5
components/Pages/PublicationMetadata.tsx
··· 19 19 import { TagSelector } from "components/Tags"; 20 20 import { useIdentityData } from "components/IdentityProvider"; 21 21 import { PostHeaderLayout } from "app/lish/[did]/[publication]/[rkey]/PostHeader/PostHeader"; 22 + import { Backdater } from "./Backdater"; 23 + 22 24 export const PublicationMetadata = () => { 23 25 let { rep } = useReplicache(); 24 26 let { data: pub, normalizedDocument, normalizedPublication } = useLeafletPublicationData(); ··· 91 93 {pub.doc ? ( 92 94 <div className="flex gap-2 items-center"> 93 95 <p className="text-sm text-tertiary"> 94 - Published {publishedAt && timeAgo(publishedAt)} 96 + Published{" "} 97 + {publishedAt && ( 98 + <Backdater publishedAt={publishedAt} docURI={pub.doc} /> 99 + )} 95 100 </p> 96 101 97 102 <Link ··· 113 118 {tags && ( 114 119 <> 115 120 <AddTags /> 116 - {normalizedPublication?.preferences?.showMentions || 117 - normalizedPublication?.preferences?.showComments ? ( 121 + {normalizedPublication?.preferences?.showMentions !== false || 122 + normalizedPublication?.preferences?.showComments !== false ? ( 118 123 <Separator classname="h-4!" /> 119 124 ) : null} 120 125 </> 121 126 )} 122 - {normalizedPublication?.preferences?.showMentions && ( 127 + {normalizedPublication?.preferences?.showMentions !== false && ( 123 128 <div className="flex gap-1 items-center"> 124 129 <QuoteTiny />— 125 130 </div> 126 131 )} 127 - {normalizedPublication?.preferences?.showComments && ( 132 + {normalizedPublication?.preferences?.showComments !== false && ( 128 133 <div className="flex gap-1 items-center"> 129 134 <CommentTiny />— 130 135 </div>
+1 -35
components/ThemeManager/PublicationThemeProvider.tsx
··· 168 168 ...localOverrides, 169 169 showPageBackground, 170 170 }; 171 - let newAccentContrast; 172 - let sortedAccents = [newTheme.accent1, newTheme.accent2].sort((a, b) => { 173 - return ( 174 - getColorDifference( 175 - colorToString(b, "rgb"), 176 - colorToString( 177 - showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 178 - "rgb", 179 - ), 180 - ) - 181 - getColorDifference( 182 - colorToString(a, "rgb"), 183 - colorToString( 184 - showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 185 - "rgb", 186 - ), 187 - ) 188 - ); 189 - }); 190 - if ( 191 - getColorDifference( 192 - colorToString(sortedAccents[0], "rgb"), 193 - colorToString(newTheme.primary, "rgb"), 194 - ) < 0.15 && 195 - getColorDifference( 196 - colorToString(sortedAccents[1], "rgb"), 197 - colorToString( 198 - showPageBackground ? newTheme.bgPage : newTheme.bgLeaflet, 199 - "rgb", 200 - ), 201 - ) > 0.08 202 - ) { 203 - newAccentContrast = sortedAccents[1]; 204 - } else newAccentContrast = sortedAccents[0]; 171 + 205 172 return { 206 173 ...newTheme, 207 - accentContrast: newAccentContrast, 208 174 }; 209 175 }, [pubTheme, localOverrides, showPageBackground]); 210 176 return {
+10 -10
components/ThemeManager/ThemeProvider.tsx
··· 133 133 // pageBg should inherit from leafletBg 134 134 const bgPage = 135 135 !showPageBackground && !hasBackgroundImage ? bgLeaflet : bgPageProp; 136 - // set accent contrast to the accent color that has the highest contrast with the page background 136 + 137 137 let accentContrast; 138 - 139 - //sorting the accents by contrast on background 140 138 let sortedAccents = [accent1, accent2].sort((a, b) => { 139 + // sort accents by contrast against the background 141 140 return ( 142 141 getColorDifference( 143 142 colorToString(b, "rgb"), ··· 149 148 ) 150 149 ); 151 150 }); 152 - 153 - // if the contrast-y accent is too similar to the primary text color, 154 - // and the not contrast-y option is different from the backgrond, 155 - // then use the not contrasty option 156 - 157 151 if ( 152 + // if the contrast-y accent is too similar to text color 158 153 getColorDifference( 159 154 colorToString(sortedAccents[0], "rgb"), 160 155 colorToString(primary, "rgb"), 161 156 ) < 0.15 && 157 + // and if the other accent is different enough from the background 162 158 getColorDifference( 163 159 colorToString(sortedAccents[1], "rgb"), 164 160 colorToString(showPageBackground ? bgPage : bgLeaflet, "rgb"), 165 - ) > 0.08 161 + ) > 0.31 166 162 ) { 163 + //then choose the less contrast-y accent 167 164 accentContrast = sortedAccents[1]; 168 - } else accentContrast = sortedAccents[0]; 165 + } else { 166 + // otherwise, choose the more contrast-y option 167 + accentContrast = sortedAccents[0]; 168 + } 169 169 170 170 useEffect(() => { 171 171 if (local) return;
+4 -2
src/replicache/mutations.ts
··· 637 637 description?: string; 638 638 tags?: string[]; 639 639 cover_image?: string | null; 640 + localPublishedAt?: string | null; 640 641 }> = async (args, ctx) => { 641 642 await ctx.runOnServer(async (serverCtx) => { 642 643 console.log("updating"); ··· 670 671 } 671 672 }); 672 673 await ctx.runOnClient(async ({ tx }) => { 673 - if (args.title !== undefined) 674 - await tx.set("publication_title", args.title); 674 + if (args.title !== undefined) await tx.set("publication_title", args.title); 675 675 if (args.description !== undefined) 676 676 await tx.set("publication_description", args.description); 677 677 if (args.tags !== undefined) await tx.set("publication_tags", args.tags); 678 678 if (args.cover_image !== undefined) 679 679 await tx.set("publication_cover_image", args.cover_image); 680 + if (args.localPublishedAt !== undefined) 681 + await tx.set("publication_local_published_at", args.localPublishedAt); 680 682 }); 681 683 }; 682 684
+6
src/utils/timeAgo.ts
··· 6 6 const diffMinutes = Math.floor(diffSeconds / 60); 7 7 const diffHours = Math.floor(diffMinutes / 60); 8 8 const diffDays = Math.floor(diffHours / 24); 9 + const diffWeeks = Math.floor(diffDays / 7); 10 + const diffMonths = Math.floor(diffDays / 30); 9 11 const diffYears = Math.floor(diffDays / 365); 10 12 11 13 if (diffYears > 0) { 12 14 return `${diffYears} year${diffYears === 1 ? "" : "s"} ago`; 15 + } else if (diffMonths > 0) { 16 + return `${diffMonths} month${diffMonths === 1 ? "" : "s"} ago`; 17 + } else if (diffWeeks > 0) { 18 + return `${diffWeeks} week${diffWeeks === 1 ? "" : "s"} ago`; 13 19 } else if (diffDays > 0) { 14 20 return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; 15 21 } else if (diffHours > 0) {