Skip to content

Commit

Permalink
Merge branch 'RSSNext:dev' into dev
Browse files Browse the repository at this point in the history
  • Loading branch information
dai authored Nov 27, 2024
2 parents 2e62d84 + c8460da commit b37d786
Show file tree
Hide file tree
Showing 51 changed files with 482 additions and 257 deletions.
69 changes: 69 additions & 0 deletions apps/renderer/src/atoms/feed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { jotaiStore } from "@follow/utils/jotai"
import { isBizId } from "@follow/utils/utils"
import { atom, useAtomValue } from "jotai"
import { selectAtom } from "jotai/utils"
import { useMemo } from "react"

import {
FEED_COLLECTION_LIST,
INBOX_PREFIX_ID,
ROUTE_FEED_IN_LIST,
ROUTE_FEED_PENDING,
} from "~/constants"

const feedUnreadDirtySetAtom = atom(new Set<string>())

// 1. feedId may be feedId, or `inbox-id` or `feedId, feedId,` or `list-id`, or `all`, or `collections`
export const useFeedUnreadIsDirty = (feedId: string) => {
return useAtomValue(
useMemo(
() =>
selectAtom(feedUnreadDirtySetAtom, (set) => {
const isRealFeedId = isBizId(feedId)
if (isRealFeedId) return set.has(feedId)

if (feedId.startsWith(ROUTE_FEED_IN_LIST) || feedId.startsWith(INBOX_PREFIX_ID)) {
// List/Inbox is not supported unread
return false
}

if (feedId === ROUTE_FEED_PENDING) {
return set.size > 0
}

if (feedId === FEED_COLLECTION_LIST) {
// Entry in collections has not unread status
return false
}

const splitted = feedId.split(",")
let isDirty = false
for (const feedId of splitted) {
if (isBizId(feedId)) {
isDirty = isDirty || set.has(feedId)

if (isDirty) break
}
}
return isDirty
}),
[feedId],
),
)
}

export const setFeedUnreadDirty = (feedId: string) => {
jotaiStore.set(feedUnreadDirtySetAtom, (prev) => {
const newSet = new Set(prev)
newSet.add(feedId)
return newSet
})
}

export const clearFeedUnreadDirty = (feedId: string) => {
jotaiStore.set(feedUnreadDirtySetAtom, (prev) => {
const newSet = new Set(prev)
newSet.delete(feedId)
return newSet
})
}
19 changes: 0 additions & 19 deletions apps/renderer/src/initialize/firebase.ts

This file was deleted.

11 changes: 11 additions & 0 deletions apps/renderer/src/lib/parse-markdown.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { unified } from "unified"
import { VFile } from "vfile"

import { MarkdownLink } from "~/components/ui/markdown/renderers/MarkdownLink"
import { VideoPlayer } from "~/components/ui/media/VideoPlayer"

export interface RemarkOptions {
components: Partial<Components>
Expand Down Expand Up @@ -78,6 +79,16 @@ export const parseMarkdown = (content: string, options?: Partial<RemarkOptions>)
passNode: true,
components: {
a: ({ node, ...props }) => createElement(MarkdownLink, { ...props } as any),
img: ({ node, ...props }) => {
const { src } = props
const isVideo = src?.endsWith(".mp4")
if (isVideo) {
return createElement(VideoPlayer, {
src: src as string,
})
}
return createElement("img", { ...props } as any)
},
...components,
},
}),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const MobileFloatBar = ({
animate={animateController}
>
<div className={styles["float-bar"]}>
<PlayerIcon onLogoClick={onLogoClick} />
<PlayerIcon isScrollDown={isScrollDown} onLogoClick={onLogoClick} />
<DividerVertical className="h-3/4 shrink-0" />
<ViewTabs onViewChange={onViewChange} />
<DividerVertical className="h-3/4 shrink-0" />
Expand Down Expand Up @@ -129,7 +129,13 @@ const ViewTabs = ({ onViewChange }: { onViewChange?: (view: number) => void }) =
)
}

const PlayerIcon = ({ onLogoClick }: { onLogoClick?: () => void }) => {
const PlayerIcon = ({
isScrollDown,
onLogoClick,
}: {
isScrollDown: boolean
onLogoClick?: () => void
}) => {
const { isPlaying, entryId } = useAudioPlayerAtomSelector(
useCallback((state) => ({ isPlaying: state.status === "playing", entryId: state.entryId }), []),
)
Expand All @@ -150,7 +156,7 @@ const PlayerIcon = ({ onLogoClick }: { onLogoClick?: () => void }) => {
<FeedIcon feed={feed} noMargin />
</button>

{isShowPlayer && (
{isShowPlayer && !isScrollDown && (
<CornerPlayer
className="absolute inset-x-0 mx-auto w-full max-w-[350px] bottom-safe-or-12"
hideControls
Expand Down
43 changes: 41 additions & 2 deletions apps/renderer/src/modules/editor/css-editor.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { TextArea } from "@follow/components/ui/input/TextArea.js"
import { useInputComposition, useIsDark } from "@follow/hooks"
import { nextFrame } from "@follow/utils/dom"
import { cn } from "@follow/utils/utils"
import { createPlainShiki } from "plain-shiki"
import { useLayoutEffect, useRef } from "react"
import { useLayoutEffect, useMemo, useRef } from "react"
import css from "shiki/langs/css.mjs"
import githubDark from "shiki/themes/github-dark.mjs"
import githubLight from "shiki/themes/github-light.mjs"
Expand All @@ -13,6 +14,7 @@ import { shiki } from "~/components/ui/code-highlighter/shiki/shared"
shiki.loadLanguageSync(css)
shiki.loadThemeSync(githubDark)
shiki.loadThemeSync(githubLight)

export const CSSEditor: Component<{
onChange: (value: string) => void
defaultValue?: string
Expand Down Expand Up @@ -74,6 +76,26 @@ export const CSSEditor: Component<{
onChange(ref.current?.textContent ?? "")
})

const isSupportPlainTextOnly = useIsSupportPlainTextOnly()
if (!isSupportPlainTextOnly) {
return (
<div className="flex size-full flex-col">
<div className="-mt-2 mb-1 text-center text-sm text-theme-placeholder-text">
<i className="i-mingcute-warning-line mr-0.5 translate-y-[2px]" />
Your browser does not support highlight CSS.
</div>
<div className="relative h-0 grow">
<div className="absolute inset-0">
<TextArea
className="font-mono"
defaultValue={defaultValue}
onChange={(e) => onChange(e.target.value)}
/>
</div>
</div>
</div>
)
}
return (
<div
className={cn(
Expand All @@ -88,9 +110,26 @@ export const CSSEditor: Component<{
)}
ref={ref}
onInput={handleInput}
contentEditable="plaintext-only"
contentEditable={isSupportPlainTextOnly ? "plaintext-only" : "true"}
tabIndex={0}
{...props}
/>
)
}
const useIsSupportPlainTextOnly = () => {
const isSupportPlainTextOnly = useMemo(() => {
if (typeof document === "undefined") return false

const div = document.createElement("div")

try {
div.contentEditable = "plaintext-only"
} catch {
return false
}

return div.contentEditable === "plaintext-only"
}, [])

return isSupportPlainTextOnly
}
28 changes: 21 additions & 7 deletions apps/renderer/src/modules/entry-column/Items/picture-masonry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { MediaContainerWidthProvider } from "~/components/ui/media"
import { getEntry } from "~/store/entry"
import { imageActions } from "~/store/image"

import { getMasonryColumnValue, setMasonryColumnValue, useMasonryColumnValue } from "../atoms"
import { batchMarkRead } from "../hooks"
import { PictureWaterFallItem } from "./picture-item"

Expand Down Expand Up @@ -66,9 +67,22 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
})
}, [])

const { containerRef, currentColumn, currentItemWidth } = useMasonryColumn(gutter, () => {
setIsInitLayout(true)
})
const customizeColumn = useMasonryColumnValue()
const { containerRef, currentColumn, currentItemWidth, calcItemWidth } = useMasonryColumn(
gutter,
(column) => {
setIsInitLayout(true)
if (getMasonryColumnValue() === -1) {
setMasonryColumnValue(column)
}
},
)

const finalColumn = customizeColumn !== -1 ? customizeColumn : currentColumn
const finalItemWidth = useMemo(
() => (customizeColumn !== -1 ? calcItemWidth(finalColumn) : currentItemWidth),
[calcItemWidth, currentItemWidth, customizeColumn, finalColumn],
)

const items = useMemo(() => {
const result = data.map((entryId) => {
Expand Down Expand Up @@ -197,17 +211,17 @@ export const PictureMasonry: FC<MasonryProps> = (props) => {
return (
<div ref={containerRef} className="mx-4 pt-2">
{isInitDim && deferIsInitLayout && (
<MasonryItemWidthContext.Provider value={currentItemWidth}>
<MasonryItemWidthContext.Provider value={finalItemWidth}>
<MasonryItemsAspectRatioContext.Provider value={masonryItemsRadio}>
<MasonryItemsAspectRatioSetterContext.Provider value={setMasonryItemsRadio}>
<MasonryIntersectionContext.Provider value={intersectionObserver}>
<MediaContainerWidthProvider width={currentItemWidth}>
<MediaContainerWidthProvider width={finalItemWidth}>
<FirstScreenReadyContext.Provider value={firstScreenReady}>
<Masonry
items={firstScreenReady ? items : items.slice(0, FirstScreenItemCount)}
columnGutter={gutter}
columnWidth={currentItemWidth}
columnCount={currentColumn}
columnWidth={finalItemWidth}
columnCount={finalColumn}
overscanBy={2}
render={MasonryRender}
onRender={handleRender}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,13 @@ export const SocialMediaItem: EntryListItemFC = ({ entryId, entryPreview, transl
<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="-mt-0.5 flex-1 text-sm">
<div className="w-[calc(100%-10rem)] space-x-1 leading-6">
<div className="space-x-1 leading-6">
<span className="inline-flex items-center gap-1 text-base font-semibold">
<FeedTitle feed={feed} title={entry.entries.author || feed.title} />
<FeedTitle
feed={feed}
title={entry.entries.author || feed.title}
titleClassName="max-w-[calc(100vw-8rem)]"
/>
{parsed?.type === "x" && (
<i className="i-mgc-twitter-cute-fi size-3 text-[#4A99E9]" />
)}
Expand Down
5 changes: 5 additions & 0 deletions apps/renderer/src/modules/entry-column/atoms/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { createAtomHooks } from "@follow/utils/jotai"
import { atom } from "jotai"

export const [, , useMasonryColumnValue, , getMasonryColumnValue, setMasonryColumnValue] =
createAtomHooks(atom(-1))
2 changes: 1 addition & 1 deletion apps/renderer/src/modules/entry-column/hooks/useMarkAll.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,5 +40,5 @@ export const useMarkAllByRoute = (filter?: MarkAllFilter) => {
filter,
})
}
}, [routerParams, folderIds, view, filter])
}, [routerParams, inboxId, folderIds, view, filter])
}
24 changes: 2 additions & 22 deletions apps/renderer/src/modules/entry-column/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { ENTRY_COLUMN_LIST_SCROLLER_ID } from "~/constants/dom"
import { useNavigateEntry } from "~/hooks/biz/useNavigateEntry"
import { useRouteParams, useRouteParamsSelector } from "~/hooks/biz/useRouteParams"
import { useFeed } from "~/queries/feed"
import { entryActions, getEntry, useEntry } from "~/store/entry"
import { getEntry } from "~/store/entry"
import { useFeedById, useFeedHeaderTitle } from "~/store/feed"
import { useSubscriptionByFeedId } from "~/store/subscription"

Expand Down Expand Up @@ -72,36 +72,16 @@ function EntryColumnImpl() {
}
}, [entriesIds])

const {
entryId: activeEntryId,
view,
feedId: routeFeedId,
isPendingEntry,
isCollection,
inboxId,
listId,
} = useRouteParams()
const { view, feedId: routeFeedId, isCollection, inboxId, listId } = useRouteParams()

useEffect(() => {
setIsArchived(false)
}, [view, routeFeedId])

const activeEntry = useEntry(activeEntryId)
const feed = useFeedById(routeFeedId)
const title = useFeedHeaderTitle()
useTitle(title)

useEffect(() => {
if (!activeEntryId) return

if (isCollection || isPendingEntry) return

const feedId = activeEntry?.feedId
if (!feedId) return

entryActions.markRead({ feedId, entryId: activeEntryId, read: true })
}, [activeEntry?.feedId, activeEntryId, isCollection, isPendingEntry])

const isInteracted = useRef(false)

const handleMarkReadInRange = useEntryMarkReadHandler(entriesIds)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,14 @@ export const EntryItemWrapper: FC<
(e) => {
e.stopPropagation()

if (!asRead) {
entryActions.markRead({ feedId: entry.feedId, entryId: entry.entries.id, read: true })
}
navigate({
entryId: entry.entries.id,
})
},
[entry.entries.id, navigate],
[asRead, entry.entries.id, entry.feedId, navigate],
)
const handleDoubleClick: React.MouseEventHandler<HTMLDivElement> = useCallback(
() => entry.entries.url && window.open(entry.entries.url, "_blank"),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,17 +47,10 @@ export const EntryListHeader: FC<{
feedId === FEED_COLLECTION_LIST || feedId?.startsWith(ROUTE_FEED_IN_LIST)

const titleInfo = !!headerTitle && (
<div className={"min-w-0 translate-y-1"}>
<div className="h-6 min-w-0 break-all text-lg font-bold leading-tight">
<EllipsisHorizontalTextWithTooltip className="inline-block !w-auto max-w-full">
<span className="relative -top-px">{headerTitle}</span>
</EllipsisHorizontalTextWithTooltip>
</div>
<div className="whitespace-nowrap text-xs font-medium leading-none text-zinc-400">
{unreadOnly && !isInCollectionList ? t("words.unread") : ""}
{t("space", { ns: "common" })}
{t("words.items", { ns: "common", count: 2 })}
</div>
<div className="flex min-w-0 items-center break-all text-lg font-bold leading-tight">
<EllipsisHorizontalTextWithTooltip className="inline-block !w-auto max-w-full">
{headerTitle}
</EllipsisHorizontalTextWithTooltip>
</div>
)
const { mutateAsync: refreshFeed, isPending } = useRefreshFeedMutation(feedId)
Expand Down
Loading

0 comments on commit b37d786

Please sign in to comment.