diff --git a/icons/mgc/eye_2_cute_re.svg b/icons/mgc/eye_2_cute_re.svg new file mode 100644 index 000000000..a64c84069 --- /dev/null +++ b/icons/mgc/eye_2_cute_re.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/renderer/src/initialize/hydrate.ts b/src/renderer/src/initialize/hydrate.ts index 365091417..adb964a90 100644 --- a/src/renderer/src/initialize/hydrate.ts +++ b/src/renderer/src/initialize/hydrate.ts @@ -26,6 +26,11 @@ export const setHydrated = (v: boolean) => { _isHydrated = v } +/** + * @description Check if database data is hydrated to store, or current database is ready. + * If users disabled data persist, it's always false, that means you can't do operation with database. + * + */ export const isHydrated = () => _isHydrated export const hydrateDatabaseToStore = async () => { diff --git a/src/renderer/src/lib/utils.ts b/src/renderer/src/lib/utils.ts index 878290a14..df05a4178 100644 --- a/src/renderer/src/lib/utils.ts +++ b/src/renderer/src/lib/utils.ts @@ -146,3 +146,13 @@ export const pluralize = ( } return postfix(noun, rule) } + +export const omitObjectUndefinedValue = (obj: Record) => { + const newObj = {} + for (const key in obj) { + if (obj[key] !== undefined) { + newObj[key] = obj[key] + } + } + return newObj +} diff --git a/src/renderer/src/modules/entry-column/hooks.ts b/src/renderer/src/modules/entry-column/hooks.ts index 9b2637eb5..1125f4aac 100644 --- a/src/renderer/src/modules/entry-column/hooks.ts +++ b/src/renderer/src/modules/entry-column/hooks.ts @@ -73,10 +73,26 @@ export const useEntriesByView = () => { view, ...(unreadOnly === true && { read: false }), }) - const entries = useEntryIdsByFeedIdOrView(isAllFeeds ? view : feedId!, { - unread: unreadOnly, - view, - }) + const remoteEntryIds = query.data?.pages + ?.map((page) => page.data?.map((entry) => entry.entries.id)) + .flat() as string[] + + const currentEntries = useEntryIdsByFeedIdOrView( + isAllFeeds ? view : feedId!, + { + unread: unreadOnly, + view, + }, + ) + + // If remote data is not available, we use the local data, get the local data length + // FIXME: remote first, then local store data + // NOTE: We still can't use the store's data handling directly. + // Imagine that the local data may be persistent, and then if there are incremental updates to the data on the server side, + // then we have no way to incrementally update the data. + // We need to add an interface to incrementally update the data based on the version hash. + + const entries = remoteEntryIds || currentEntries useHotkeys( shortcuts.entries.refetch.key, @@ -93,7 +109,7 @@ export const useEntriesByView = () => { useEffect(() => { prevEntries.current = [] }, [routeParams.feedId, routeParams.view]) - const localEntries = useMemo(() => { + const mergedEntries = useMemo(() => { if (!unreadOnly) { prevEntries.current = [] return entries @@ -102,35 +118,22 @@ export const useEntriesByView = () => { prevEntries.current = entries return entries } - if (entries.length > prevEntries.current.length) { - prevEntries.current = entries - return entries - } // merge the new entries with the old entries, and unique them const nextIds = [...new Set([...prevEntries.current, ...entries])] prevEntries.current = nextIds return nextIds }, [entries, prevEntries, unreadOnly]) - const sortLocalEntries = () => + const sortEntries = () => isCollection ? - sortEntriesIdByStarAt(localEntries) : - sortEntriesIdByEntryPublishedAt(localEntries) - const remoteEntryIds = query.data?.pages - ?.map((page) => page.data?.map((entry) => entry.entries.id)) - .flat() as string[] + sortEntriesIdByStarAt(mergedEntries) : + sortEntriesIdByEntryPublishedAt(mergedEntries) return { ...query, - // If remote data is not available, we use the local data, get the local data length - // FIXME: remote first, then local store data - // NOTE: We still can't use the store's data handling directly. - // Imagine that the local data may be persistent, and then if there are incremental updates to the data on the server side, - // then we have no way to incrementally update the data. - // We need to add an interface to incrementally update the data based on the version hash. - entriesIds: remoteEntryIds ?? sortLocalEntries(), - totalCount: query.data?.pages?.[0]?.total ?? localEntries.length, + entriesIds: sortEntries(), + totalCount: query.data?.pages?.[0]?.total ?? mergedEntries.length, } } diff --git a/src/renderer/src/modules/entry-content/index.tsx b/src/renderer/src/modules/entry-content/index.tsx index 642b85b88..a899e0bc3 100644 --- a/src/renderer/src/modules/entry-content/index.tsx +++ b/src/renderer/src/modules/entry-content/index.tsx @@ -1,7 +1,10 @@ import { useUISettingKey } from "@renderer/atoms/settings/ui" +import { useUser } from "@renderer/atoms/user" import { m } from "@renderer/components/common/Motion" import { Logo } from "@renderer/components/icons/logo" import { AutoResizeHeight } from "@renderer/components/ui/auto-resize-height" +import { Avatar, AvatarFallback, AvatarImage } from "@renderer/components/ui/avatar" +import { Tooltip, TooltipContent, TooltipTrigger } from "@renderer/components/ui/tooltip" import { useAuthQuery, useTitle } from "@renderer/hooks/common" import { stopPropagation } from "@renderer/lib/dom" import { parseHtml } from "@renderer/lib/parse-html" @@ -42,6 +45,8 @@ export const EntryContent = ({ entryId }: { entryId: ActiveEntryId }) => { } function EntryContentRender({ entryId }: { entryId: string }) { + const user = useUser() + const { error, data } = useAuthQuery(Queries.entries.byId(entryId), { staleTime: 300_000, meta: { @@ -143,6 +148,67 @@ function EntryContentRender({ entryId }: { entryId: string }) { {entry.entries.publishedAt && new Date(entry.entries.publishedAt).toLocaleString()} +
+
+ + + {( + (data?.entries.entryReadHistories.readCount ?? 0) + + (data?.entries.entryReadHistories.users.every((u) => u.id !== user?.id) ? 1 : 0) // if no me, +1 + ).toLocaleString()} + +
+
+ {[ + { + id: user?.id, + name: user?.name ?? null, + image: user?.image ?? null, + handle: user?.handle ?? null, + }, + ] // myself first + .concat( + data?.entries.entryReadHistories.users.filter( + (u) => u.id !== user?.id, + ) ?? [], + ) // then others + .slice(0, 10) // only show 10 + .concat( + data?.entries.entryReadHistories.readCount && + data.entries.entryReadHistories.readCount > 10 ? + [ + { + id: "more", + name: `+${ + data?.entries.entryReadHistories.readCount - 10 + }`, + image: null, + handle: null, + }, + ] : + [], + ) // show more count + .map((user, i) => ( + + +
+ + + + {user.name?.slice(0, 2)} + + +
+
+ {user.name} +
+ ))} +
+
diff --git a/src/renderer/src/store/entry/store.ts b/src/renderer/src/store/entry/store.ts index a1f345bdf..cb15294d4 100644 --- a/src/renderer/src/store/entry/store.ts +++ b/src/renderer/src/store/entry/store.ts @@ -1,5 +1,8 @@ import { apiClient } from "@renderer/lib/api-fetch" -import { getEntriesParams } from "@renderer/lib/utils" +import { + getEntriesParams, + omitObjectUndefinedValue, +} from "@renderer/lib/utils" import type { CombinedEntryModel, EntryModel, @@ -7,7 +10,7 @@ import type { } from "@renderer/models" import { EntryService } from "@renderer/services" import { produce } from "immer" -import { merge, omit } from "lodash-es" +import { isNil, merge, omit } from "lodash-es" import { isHydrated } from "../../initialize/hydrate" import { feedActions } from "../feed" @@ -98,7 +101,8 @@ class EntryActions { produce(state, (draft) => { const entry = draft.flatMapEntries[entryId] if (!entry) return - Object.assign(entry, changed) + Object.assign(entry, omitObjectUndefinedValue(changed)) + return draft }), ) @@ -204,6 +208,12 @@ class EntryActions { this.patch(entryId, { read, }) + if (!isHydrated()) return + if (!isNil(read)) { + EntryService.bulkStoreReadStatus({ + [entryId]: read, + }) + } } markReadByFeedId(feedId: string) {