Skip to content

Commit

Permalink
feat(ui): social media ui refresh (#459)
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei authored Sep 17, 2024
1 parent dee089e commit fcd96ab
Show file tree
Hide file tree
Showing 9 changed files with 140 additions and 47 deletions.
6 changes: 3 additions & 3 deletions src/renderer/src/components/feed-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar"
import { getColorScheme, stringToHue } from "@renderer/lib/color"
import { getProxyUrl } from "@renderer/lib/img-proxy"
import { getImageProxyUrl } from "@renderer/lib/img-proxy"
import { cn, getUrlIcon } from "@renderer/lib/utils"
import type { CombinedEntryModel, FeedModel } from "@renderer/models"
import type { ReactNode } from "react"
Expand All @@ -23,7 +23,7 @@ const getFeedIconSrc = ({
if (src) {
if (proxy) {
return [
getProxyUrl({
getImageProxyUrl({
url: src,
width: proxy.width,
height: proxy.height,
Expand Down Expand Up @@ -123,7 +123,7 @@ export function FeedIcon({
break
}
case !!image: {
finalSrc = getProxyUrl({
finalSrc = getImageProxyUrl({
url: image,
width: size * 2,
height: size * 2,
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/components/site-icon.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getColorScheme, stringToHue } from "@renderer/lib/color"
import { getProxyUrl } from "@renderer/lib/img-proxy"
import { getImageProxyUrl } from "@renderer/lib/img-proxy"
import { cn, getUrlIcon } from "@renderer/lib/utils"
import { useEffect, useMemo, useState } from "react"

Expand Down Expand Up @@ -50,7 +50,7 @@ export function SiteIcon({
if (src) {
if (stableProxy) {
return [
getProxyUrl({
getImageProxyUrl({
url: src,
width: stableProxy.width,
height: stableProxy.height,
Expand Down
5 changes: 4 additions & 1 deletion src/renderer/src/components/ui/list-item-hover-overlay.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { m } from "@renderer/components/common/Motion"
import { views } from "@renderer/constants"
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
import { FeedViewType } from "@renderer/lib/enum"
import clsx from "clsx"
import { AnimatePresence } from "framer-motion"
import { useEffect, useRef, useState } from "react"
Expand Down Expand Up @@ -39,7 +40,9 @@ export const ListItemHoverOverlay = ({

const mClassName = clsx(
"absolute z-[-1]",
"bg-zinc-200/80 dark:bg-neutral-800",
view !== FeedViewType.SocialMedia
? "bg-zinc-200/80 dark:bg-neutral-800"
: "bg-zinc-100 dark:bg-neutral-800/50",
views[view].wideMode ? "inset-x-0 inset-y-1 rounded-xl" : "-inset-x-2 inset-y-0",
className,
)
Expand Down
5 changes: 3 additions & 2 deletions src/renderer/src/components/ui/media.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { nextFrame } from "@renderer/lib/dom"
import { getProxyUrl } from "@renderer/lib/img-proxy"
import { getImageProxyUrl } from "@renderer/lib/img-proxy"
import { cn } from "@renderer/lib/utils"
import { saveImageDimensionsToDb } from "@renderer/store/image/db"
import { useForceUpdate } from "framer-motion"
Expand Down Expand Up @@ -50,7 +50,7 @@ const MediaImpl: FC<MediaProps> = ({
const [hidden, setHidden] = useState(!src)
const [imgSrc, setImgSrc] = useState(() =>
proxy && src && !failedList.has(src)
? getProxyUrl({
? getImageProxyUrl({
url: src,
width: proxy.width,
height: proxy.height,
Expand Down Expand Up @@ -160,6 +160,7 @@ const MediaImpl: FC<MediaProps> = ({
}
return (
<span
data-state={type !== "video" ? mediaLoadState : undefined}
className={cn("block overflow-hidden rounded", hidden && "hidden", className)}
style={style}
>
Expand Down
13 changes: 10 additions & 3 deletions src/renderer/src/components/ui/media/preview-media.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@ const FallbackableImage: FC<
if (fallbackUrl) {
setCurrentSrc(fallbackUrl)
setCurrentState("fallback")
} else {
setIsAllError(true)
}
break
}
Expand All @@ -274,7 +276,7 @@ const FallbackableImage: FC<
)}
{isAllError && (
<div
className="center flex-col gap-6 text-white/80"
className="center pointer-events-none absolute inset-0 flex-col gap-6 text-white/80"
onClick={stopPropagation}
tabIndex={-1}
>
Expand All @@ -283,7 +285,7 @@ const FallbackableImage: FC<
<span>Failed to load image</span>
<div className="center gap-4">
<MotionButtonBase
className="underline underline-offset-4"
className="pointer-events-auto underline underline-offset-4"
onClick={() => {
setCurrentSrc(replaceImgUrlIfNeed(src))
setIsAllError(false)
Expand All @@ -292,7 +294,12 @@ const FallbackableImage: FC<
Retry
</MotionButtonBase>
or
<a className="underline underline-offset-4" href={src} target="_blank" rel="noreferrer">
<a
className="pointer-events-auto underline underline-offset-4"
href={src}
target="_blank"
rel="noreferrer"
>
Visit Original
</a>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/lib/img-proxy.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { env } from "@env"
import { imageRefererMatches } from "@shared/image"

export const getProxyUrl = ({
export const getImageProxyUrl = ({
url,
width,
height,
Expand All @@ -14,7 +14,7 @@ export const getProxyUrl = ({
export const replaceImgUrlIfNeed = (url: string) => {
for (const rule of imageRefererMatches) {
if (rule.url.test(url)) {
return getProxyUrl({ url, width: 0, height: 0 })
return getImageProxyUrl({ url, width: 0, height: 0 })
}
}
return url
Expand Down
19 changes: 19 additions & 0 deletions src/renderer/src/lib/parsers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { EntryModel } from "@renderer/models"

import { isTwitterUrl, isXUrl } from "./link-parser"

export const parseSocialMedia = (entry: EntryModel) => {
const { authorUrl, url, guid } = entry

const parsedUrl = authorUrl || url || guid
const isX = isXUrl(parsedUrl) || isTwitterUrl(parsedUrl)

if (isX) {
return {
type: "x",
meta: {
handle: new URL(parsedUrl).pathname.split("/").pop(),
},
}
}
}
129 changes: 96 additions & 33 deletions src/renderer/src/modules/entry-column/Items/social-media-item.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,86 +8,149 @@ import { Skeleton } from "@renderer/components/ui/skeleton"
import { useAsRead } from "@renderer/hooks/biz/useAsRead"
import { useEntryActions } from "@renderer/hooks/biz/useEntryActions"
import { useRouteParamsSelector } from "@renderer/hooks/biz/useRouteParams"
import { getImageProxyUrl } from "@renderer/lib/img-proxy"
import { jotaiStore } from "@renderer/lib/jotai"
import { parseSocialMedia } from "@renderer/lib/parsers"
import { cn } from "@renderer/lib/utils"
import { useEntry } from "@renderer/store/entry/hooks"
import { useFeedById } from "@renderer/store/feed"
import { atom } from "jotai"
import { useLayoutEffect, useRef } from "react"

import { ReactVirtuosoItemPlaceholder } from "../../../components/ui/placeholder"
import { StarIcon } from "../star-icon"
import { EntryTranslation } from "../translation"
import type { EntryListItemFC } from "../types"

const socialMediaContentWidthAtom = atom(0)
export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, translation }) => {
const entry = useEntry(entryId) || entryPreview

const previewMedia = usePreviewMedia()
const asRead = useAsRead(entry)
const feed = useFeedById(entry?.feedId)

const ref = useRef<HTMLDivElement>(null)

useLayoutEffect(() => {
if (ref.current) {
jotaiStore.set(socialMediaContentWidthAtom, ref.current.offsetWidth)
}
}, [ref.current])
// NOTE: prevent 0 height element, react virtuoso will not stop render any more
if (!entry || !feed) return <ReactVirtuosoItemPlaceholder />

const content = entry.entries.content || entry.entries.description

const parsed = parseSocialMedia(entry.entries)

return (
<div
className={cn(
"relative flex py-4 pl-3 pr-2",
"relative flex px-8 py-6",
"group",
!asRead &&
"before:absolute before:-left-4 before:top-[28px] before:block before:size-2 before:rounded-full before:bg-accent",
)}
>
<FeedIcon
fallback
className="mask-squircle mask"
feed={feed}
entry={entry.entries}
size={36}
/>
<div className="ml-2 min-w-0 flex-1">
<FeedIcon fallback feed={feed} entry={entry.entries} size={32} className="mt-1" />
<div ref={ref} className="ml-2 min-w-0 flex-1">
<div className={cn("-mt-0.5 flex-1 text-sm", content && "line-clamp-[10]")}>
<div className="w-[calc(100%-10rem)] space-x-1">
<span className="font-semibold">{entry.entries.author}</span>
<div className="w-[calc(100%-10rem)] space-x-1 leading-6">
<span className="text-base font-semibold">
{entry.entries.author}
{parsed?.type === "x" && (
<i className="i-mgc-twitter-cute-fi ml-1 size-3 text-[#4A99E9]" />
)}
</span>

{parsed?.type === "x" && (
<a
href={`https://x.com/${parsed.meta.handle}`}
target="_blank"
className="text-zinc-500"
>
@{parsed.meta.handle}
</a>
)}
<span className="text-zinc-500">·</span>
<span className="text-zinc-500">
<RelativeTime date={entry.entries.publishedAt} />
</span>
</div>
<div
className={cn(
"relative mt-0.5 whitespace-pre-line text-base",
"relative mt-1 whitespace-pre-line text-base",
!!entry.collections && "pr-5",
)}
>
<EntryTranslation
className="cursor-auto select-text prose-blockquote:mt-0 [&_br:last-child]:hidden"
className="cursor-auto select-text text-sm leading-relaxed prose-blockquote:mt-0 [&_br:last-child]:hidden"
source={content}
target={translation?.content}
isHTML
/>
{!!entry.collections && <StarIcon />}
</div>
</div>
<div className="mt-1 flex gap-2 overflow-x-auto pb-2">
{entry.entries.media?.map((media, i, mediaList) => (
<Media
key={media.url}
src={media.url}
type={media.type}
previewImageUrl={media.preview_image_url}
className="size-28 shrink-0"
loading="lazy"
proxy={{
width: 224,
height: 224,
}}
onClick={(e) => {
e.stopPropagation()
previewMedia(mediaList, i)
}}
/>
))}
<div className="mt-4 flex gap-[8px] overflow-x-auto pb-2">
{entry.entries.media?.map((media, i, mediaList) => {
const style: Partial<{
width: string
height: string
}> = {}
const boundsWidth = jotaiStore.get(socialMediaContentWidthAtom)
if (media.height && media.width) {
// has 1 picture, max width is container width, but max height is less than window height: 2/3
if (mediaList.length === 1) {
style.width = `${boundsWidth}px`
style.height = `${(boundsWidth * media.height) / media.width}px`
if (Number.parseInt(style.height) > (window.innerHeight * 2) / 3) {
style.height = `${(window.innerHeight * 2) / 3}px`
style.width = `${(Number.parseInt(style.height) * media.width) / media.height}px`
}
}
// has 2 pictures, max width is container half width, and - gap 8px
else if (mediaList.length === 2) {
style.width = `${(boundsWidth - 8) / 2}px`
style.height = `${(((boundsWidth - 8) / 2) * media.height) / media.width}px`
}
// has over 2 pictures, max width is container 1/3 width
else if (mediaList.length > 2) {
style.width = `${boundsWidth / 3}px`
style.height = `${((boundsWidth / 3) * media.height) / media.width}px`
}
}

const proxySize = {
width: Number.parseInt(style.width || "0") || 0,
height: Number.parseInt(style.height || "0") || 0,
}
return (
<Media
style={style}
key={media.url}
src={media.url}
type={media.type}
previewImageUrl={media.preview_image_url}
className="size-28 shrink-0 data-[state=loading]:!bg-zinc-200 dark:data-[state=loading]:!bg-neutral-700"
loading="lazy"
proxy={proxySize}
onClick={(e) => {
e.stopPropagation()
previewMedia(
mediaList.map((m) => ({
url: m.url,
type: m.type,
fallbackUrl:
m.preview_image_url ?? getImageProxyUrl({ url: m.url, ...proxySize }),
})),
i,
)
}}
/>
)
})}
</div>
</div>

Expand All @@ -103,7 +166,7 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
)
}

SocialMediaItem.wrapperClassName = tw`w-[75ch] m-auto`
SocialMediaItem.wrapperClassName = tw`w-[645px] max-w-full m-auto`

const ActionBar = ({ entryId }: { entryId: string }) => {
const entry = useEntry(entryId)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ const SocialMediaDateItem = ({ date, className }: { date: string; className?: st
// @ts-expect-error
Wrapper={useCallback(
({ children }) => (
<div className="m-auto flex w-[67ch] gap-3 pl-5 text-lg">{children}</div>
<div className="m-auto flex w-[645px] gap-3 pl-5 text-lg">{children}</div>
),
[],
)}
Expand Down

0 comments on commit fcd96ab

Please sign in to comment.