diff --git a/package.json b/package.json index 0b272b02d..7fad76816 100644 --- a/package.json +++ b/package.json @@ -65,6 +65,7 @@ "lodash-es": "4.17.21", "lucide-react": "0.381.0", "ofetch": "1.3.4", + "react-helmet-async": "2.0.5", "react-hook-form": "7.51.5", "react-intersection-observer": "9.10.2", "react-router-dom": "6.23.1", @@ -88,6 +89,7 @@ "@egoist/tailwindcss-icons": "1.8.0", "@electron-toolkit/tsconfig": "^1.0.1", "@iconify-json/mingcute": "1.1.17", + "@tailwindcss/container-queries": "0.1.1", "@tailwindcss/typography": "0.5.13", "@types/lodash-es": "4.17.12", "@types/node": "^20.12.13", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 24317b402..e30668bb7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -134,6 +134,9 @@ importers: ofetch: specifier: 1.3.4 version: 1.3.4 + react-helmet-async: + specifier: 2.0.5 + version: 2.0.5(react@18.3.1) react-hook-form: specifier: 7.51.5 version: 7.51.5(react@18.3.1) @@ -198,6 +201,9 @@ importers: '@iconify-json/mingcute': specifier: 1.1.17 version: 1.1.17 + '@tailwindcss/container-queries': + specifier: 0.1.1 + version: 0.1.1(tailwindcss@3.4.3) '@tailwindcss/typography': specifier: 0.5.13 version: 0.5.13(tailwindcss@3.4.3) @@ -1811,6 +1817,11 @@ packages: resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} engines: {node: '>=10'} + '@tailwindcss/container-queries@0.1.1': + resolution: {integrity: sha512-p18dswChx6WnTSaJCSGx6lTmrGzNNvm2FtXmiO6AuA1V4U5REyoqwmT6kgAsIMdjo07QdAfYXHJ4hnMtfHzWgA==} + peerDependencies: + tailwindcss: '>=3.2.0' + '@tailwindcss/typography@0.5.13': resolution: {integrity: sha512-ADGcJ8dX21dVVHIwTRgzrcunY6YY9uSlAHHGVKvkA+vLc5qLwEszvKts40lx7z0qc4clpjclwLeK5rVCV2P/uw==} peerDependencies: @@ -4374,6 +4385,14 @@ packages: peerDependencies: react: ^18.3.1 + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-helmet-async@2.0.5: + resolution: {integrity: sha512-rYUYHeus+i27MvFE+Jaa4WsyBKGkL6qVgbJvSBoX8mbsWoABJXdEO0bZyi0F6i+4f0NuIb8AvqPMj3iXFHkMwg==} + peerDependencies: + react: ^16.6.0 || ^17.0.0 || ^18.0.0 + react-hook-form@7.51.5: resolution: {integrity: sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==} engines: {node: '>=12.22.0'} @@ -4636,6 +4655,9 @@ packages: set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -6901,6 +6923,10 @@ snapshots: dependencies: defer-to-connect: 2.0.1 + '@tailwindcss/container-queries@0.1.1(tailwindcss@3.4.3)': + dependencies: + tailwindcss: 3.4.3 + '@tailwindcss/typography@0.5.13(tailwindcss@3.4.3)': dependencies: lodash.castarray: 4.4.0 @@ -10085,6 +10111,15 @@ snapshots: react: 18.3.1 scheduler: 0.23.2 + react-fast-compare@3.2.2: {} + + react-helmet-async@2.0.5(react@18.3.1): + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + react-hook-form@7.51.5(react@18.3.1): dependencies: react: 18.3.1 @@ -10380,6 +10415,8 @@ snapshots: set-blocking@2.0.0: {} + shallowequal@1.1.0: {} + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 diff --git a/src/main/window.ts b/src/main/window.ts index 4d873ce6b..50b9c9ad2 100644 --- a/src/main/window.ts +++ b/src/main/window.ts @@ -33,7 +33,7 @@ export function createWindow(options?: { transparent: true, backgroundColor: "#00000000", // transparent hexadecimal or anything with transparency, vibrancy: "sidebar", - visualEffectState: "followWindow", + visualEffectState: "active", }) window.on("ready-to-show", () => { diff --git a/src/renderer/index.html b/src/renderer/index.html index d02f2005b..5090dec0b 100644 --- a/src/renderer/index.html +++ b/src/renderer/index.html @@ -18,6 +18,7 @@ (document.documentElement.dataset.theme = "dark"); })(); +
diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4982f385e..4f18c5fa3 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -19,10 +19,12 @@ function App() { return ( <> -
+ {window.electron && ( +
+ )} diff --git a/src/renderer/src/components/entry-column/index.tsx b/src/renderer/src/components/entry-column/index.tsx index 8eae8516d..70b2f84d4 100644 --- a/src/renderer/src/components/entry-column/index.tsx +++ b/src/renderer/src/components/entry-column/index.tsx @@ -241,7 +241,7 @@ const ListHeader: FC = () => {
-
+
{activeList?.name}
{entries.data?.pages?.[0].total || 0} diff --git a/src/renderer/src/components/entry-column/item-wrapper.tsx b/src/renderer/src/components/entry-column/item-wrapper.tsx index 627c663ad..3f78b54d5 100644 --- a/src/renderer/src/components/entry-column/item-wrapper.tsx +++ b/src/renderer/src/components/entry-column/item-wrapper.tsx @@ -34,7 +34,9 @@ export function EntryItemWrapper({ }, }), onMutate: () => { - entryActions.markRead(entry.feeds.id, entry.entries.id, true) + entryActions.optimisticUpdate(entry.entries.id, { + read: true, + }) }, // TODO fallback }) diff --git a/src/renderer/src/components/entry-content/index.tsx b/src/renderer/src/components/entry-content/index.tsx index 19638f20b..266dda0eb 100644 --- a/src/renderer/src/components/entry-content/index.tsx +++ b/src/renderer/src/components/entry-content/index.tsx @@ -30,36 +30,38 @@ export function EntryContent({ entryId }: { entryId: ActiveEntry }) { ( <> - -
- -
- {entry.data?.entries.title} +
+ +
- + + +
) : ( @@ -68,7 +70,11 @@ export function EntryContent({ entryId }: { entryId: ActiveEntry }) { initial={{ opacity: 0.01, y: 300 }} animate={{ opacity: 1, y: 0 }} > - logo + logo {activeList?.name} ) diff --git a/src/renderer/src/components/feed-column/index.tsx b/src/renderer/src/components/feed-column/index.tsx index 9232cbca8..92c8f9ca7 100644 --- a/src/renderer/src/components/feed-column/index.tsx +++ b/src/renderer/src/components/feed-column/index.tsx @@ -5,7 +5,7 @@ import { TooltipTrigger, } from "@renderer/components/ui/tooltip" import { UserButton } from "@renderer/components/user-button" -import { levels, views } from "@renderer/lib/constants" +import { APP_NAME, levels, views } from "@renderer/lib/constants" import { clamp, cn } from "@renderer/lib/utils" import { feedActions } from "@renderer/store" import { useWheel } from "@use-gesture/react" @@ -55,9 +55,14 @@ export function FeedColumn() { }, ) + const normalStyle = !window.electron || (window.electron.process.platform !== "darwin") + return (
setActiveList({ level: levels.view, @@ -67,7 +72,30 @@ export function FeedColumn() { preventNavigate: true, })} > -
+
+ {normalStyle && ( +
{ + e.stopPropagation() + setActiveList({ + level: levels.view, + id: active, + name: views[active].name, + view: active, + }) + }} + > + logo + {APP_NAME} +
+ )}
@@ -118,7 +146,10 @@ export function FeedColumn() { key={item.name} className="shrink-0 snap-center overflow-y-auto" > - + ))} diff --git a/src/renderer/src/components/feed-column/item.tsx b/src/renderer/src/components/feed-column/item.tsx index 8ff4fd6f6..211076d1b 100644 --- a/src/renderer/src/components/feed-column/item.tsx +++ b/src/renderer/src/components/feed-column/item.tsx @@ -109,7 +109,7 @@ export function FeedItem({ }} onDoubleClick={() => { window.open( - `${import.meta.env.VITE_WEB_URL}/feed/${feed.feedId}`, + `${import.meta.env.VITE_WEB_URL}/feed/${feed.feedId}?view=${view}`, "_blank", ) }} diff --git a/src/renderer/src/components/feed-column/list.tsx b/src/renderer/src/components/feed-column/list.tsx index 4d5781c11..906419b76 100644 --- a/src/renderer/src/components/feed-column/list.tsx +++ b/src/renderer/src/components/feed-column/list.tsx @@ -158,15 +158,17 @@ export function FeedList({ /> )) ) : ( -
- - - Add some feeds - -
+ !hideTitle && ( +
+ + + Add some feeds + +
+ ) )}
) diff --git a/src/renderer/src/components/header.tsx b/src/renderer/src/components/header.tsx index 70f5f212f..b1b0c86c9 100644 --- a/src/renderer/src/components/header.tsx +++ b/src/renderer/src/components/header.tsx @@ -5,6 +5,7 @@ import { NavigationMenuList, navigationMenuTriggerStyle, } from "@renderer/components/ui/navigation-menu" +import { APP_NAME } from "@renderer/lib/constants" import { Link } from "react-router-dom" import { UserButton } from "./user-button" @@ -21,15 +22,16 @@ const items = [{ }] export function Header() { + // TODO optimize ui in mobile view return ( -
+
logo - Follow + {APP_NAME}
- + {items.map((item) => ( @@ -43,7 +45,7 @@ export function Header() {
- +
) diff --git a/src/renderer/src/components/icons/follow.tsx b/src/renderer/src/components/icons/follow.tsx new file mode 100644 index 000000000..6e0aba0dc --- /dev/null +++ b/src/renderer/src/components/icons/follow.tsx @@ -0,0 +1,17 @@ +export const FollowIcon: Component = (props) => ( + + + + + + + + + + +) diff --git a/src/renderer/src/components/ui/alert-dialog.tsx b/src/renderer/src/components/ui/alert-dialog.tsx index 071ddbd9c..b46b8badf 100644 --- a/src/renderer/src/components/ui/alert-dialog.tsx +++ b/src/renderer/src/components/ui/alert-dialog.tsx @@ -1,8 +1,9 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" -import { buttonVariants } from "@renderer/components/ui/button" import { cn } from "@renderer/lib/utils" import * as React from "react" +import { buttonVariants } from "./button/variants" + const AlertDialog = AlertDialogPrimitive.Root const AlertDialogTrigger = AlertDialogPrimitive.Trigger diff --git a/src/renderer/src/components/ui/button.tsx b/src/renderer/src/components/ui/button/index.tsx similarity index 62% rename from src/renderer/src/components/ui/button.tsx rename to src/renderer/src/components/ui/button/index.tsx index f90861b95..6ffbe7546 100644 --- a/src/renderer/src/components/ui/button.tsx +++ b/src/renderer/src/components/ui/button/index.tsx @@ -1,40 +1,12 @@ import { Slot } from "@radix-ui/react-slot" import { cn } from "@renderer/lib/utils" import type { VariantProps } from "class-variance-authority" -import { cva } from "class-variance-authority" +import type { HTMLMotionProps } from "framer-motion" +import { m } from "framer-motion" import * as React from "react" -import { Tooltip, TooltipContent, TooltipTrigger } from "./tooltip" - -export const buttonVariants = cva( - "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", - { - variants: { - size: { - default: "h-10 px-4 py-2", - sm: "h-8 rounded-md px-4", - lg: "h-11 rounded-md px-8", - xl: "h-14 rounded-lg px-10", - icon: "size-10", - }, - variant: { - default: "bg-primary text-primary-foreground hover:bg-primary/90", - destructive: - "bg-destructive text-destructive-foreground hover:bg-destructive/90", - outline: - "border border-input bg-background hover:bg-accent hover:text-accent-foreground", - secondary: - "bg-secondary text-secondary-foreground hover:bg-secondary/80", - ghost: "hover:bg-zinc-500/10 px-1.5", - link: "text-primary underline-offset-4 hover:underline", - }, - }, - defaultVariants: { - variant: "default", - size: "default", - }, - }, -) +import { Tooltip, TooltipContent, TooltipTrigger } from "../tooltip" +import { buttonVariants, styledButtonVariant } from "./variants" export interface ButtonProps extends React.ButtonHTMLAttributes, @@ -110,3 +82,40 @@ export const HeaderActionButton = React.forwardRef< ), ) + +export const MotionButtonBase = React.forwardRef< + HTMLButtonElement, + HTMLMotionProps<"button"> +>(({ children, ...rest }, ref) => ( + + {children} + +)) + +MotionButtonBase.displayName = "MotionButtonBase" + +export const StyledButton = React.forwardRef< + HTMLButtonElement, + HTMLMotionProps<"button"> +>(({ className, ...props }, ref) => ( + + {props.children} + +)) diff --git a/src/renderer/src/components/ui/button/variants.tsx b/src/renderer/src/components/ui/button/variants.tsx new file mode 100644 index 000000000..1406de9a7 --- /dev/null +++ b/src/renderer/src/components/ui/button/variants.tsx @@ -0,0 +1,49 @@ +import { cn } from "@renderer/lib/utils" +import { cva } from "class-variance-authority" + +export const buttonVariants = cva( + "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50", + { + variants: { + size: { + default: "h-10 px-4 py-2", + sm: "h-8 rounded-md px-4", + lg: "h-11 rounded-md px-8", + xl: "h-14 rounded-lg px-10", + icon: "size-10", + }, + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/90", + destructive: + "bg-destructive text-destructive-foreground hover:bg-destructive/90", + outline: + "border border-input bg-background hover:bg-accent hover:text-accent-foreground", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80", + ghost: "hover:bg-zinc-500/10 px-1.5", + link: "text-primary underline-offset-4 hover:underline", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + }, +) +export const styledButtonVariant = cva( + "inline-flex select-none cursor-default items-center gap-2 justify-center rounded-lg py-2 px-3 text-sm outline-offset-2 transition active:transition-none", + { + variants: { + variant: { + primary: cn( + "bg-theme-accent text-zinc-100", + "hover:contrast-[1.10] active:contrast-125", + "font-semibold", + "disabled:cursor-not-allowed disabled:bg-theme-accent/40 disabled:opacity-80 disabled:dark:text-zinc-50", + "dark:text-neutral-800", + ), + + }, + }, + }, +) diff --git a/src/renderer/src/components/ui/list-item-hover-overlay.tsx b/src/renderer/src/components/ui/list-item-hover-overlay.tsx new file mode 100644 index 000000000..bf2794efb --- /dev/null +++ b/src/renderer/src/components/ui/list-item-hover-overlay.tsx @@ -0,0 +1,70 @@ +/* eslint-disable unicorn/prefer-add-event-listener */ +"use client" + +import clsx from "clsx" +import { AnimatePresence, m } from "framer-motion" +import { useEffect, useRef, useState } from "react" + +export const ListItemHoverOverlay = () => { + const [mouseEnter, setMouseEnter] = useState(false) + const ref = useRef(null) + useEffect(() => { + const $ref = ref.current + if (!$ref) return + const $parent = $ref.parentElement + if (!$parent) return + $parent.onfocus = () => { + setMouseEnter(true) + } + + $parent.onblur = () => { + setMouseEnter(false) + } + + return () => { + $parent.onfocus = null + $parent.onblur = null + } + }, []) + return ( + <> +
{ + setMouseEnter(true) + }} + onMouseLeave={() => { + setMouseEnter(false) + }} + /> + + + {mouseEnter && ( + + )} + + + ) +} diff --git a/src/renderer/src/components/ui/tooltip.tsx b/src/renderer/src/components/ui/tooltip.tsx index 86e37511f..9a18fb53b 100644 --- a/src/renderer/src/components/ui/tooltip.tsx +++ b/src/renderer/src/components/ui/tooltip.tsx @@ -16,9 +16,9 @@ const TooltipContent = React.forwardRef< ref={ref} sideOffset={sideOffset} className={cn( - "z-50 overflow-hidden border border-border bg-theme-tooltip-background px-2 py-1 text-theme-tooltip-foreground backdrop-blur", + "z-50 overflow-hidden border border-border bg-theme-tooltip-background px-2 py-1 text-theme-tooltip-foreground backdrop-blur-xl", "animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2", - "rounded-lg text-xs shadow shadow-theme-tooltip-background", + "rounded-lg text-xs", className, )} {...props} diff --git a/src/renderer/src/hooks/useAuth.ts b/src/renderer/src/hooks/useAuth.ts new file mode 100644 index 000000000..7d1f31c15 --- /dev/null +++ b/src/renderer/src/hooks/useAuth.ts @@ -0,0 +1,6 @@ +import { signOut } from "@hono/auth-js/react" +import { useCallback } from "react" + +export const useSignOut = () => useCallback(() => { + signOut() +}, []) diff --git a/src/renderer/src/lib/constants.tsx b/src/renderer/src/lib/constants.tsx index ae3f484d0..6a2f76412 100644 --- a/src/renderer/src/lib/constants.tsx +++ b/src/renderer/src/lib/constants.tsx @@ -42,3 +42,5 @@ export const views = [ className: "text-yellow-600", }, ] + +export const APP_NAME = "Follow" diff --git a/src/renderer/src/pages/(external)/feed/index.tsx b/src/renderer/src/pages/(external)/(with-layout)/feed/index.tsx similarity index 53% rename from src/renderer/src/pages/(external)/feed/index.tsx rename to src/renderer/src/pages/(external)/(with-layout)/feed/index.tsx index c0fef4254..add1ac5d7 100644 --- a/src/renderer/src/pages/(external)/feed/index.tsx +++ b/src/renderer/src/pages/(external)/(with-layout)/feed/index.tsx @@ -5,13 +5,17 @@ import { SocialMediaItem } from "@renderer/components/entry-column/social-media- import type { UniversalItemProps } from "@renderer/components/entry-column/types" import { VideoItem } from "@renderer/components/entry-column/video-item" import { FeedIcon } from "@renderer/components/feed-icon" -import { Button } from "@renderer/components/ui/button" -import { views } from "@renderer/lib/constants" +import { FollowIcon } from "@renderer/components/icons/follow" +import { StyledButton } from "@renderer/components/ui/button" +import { ListItemHoverOverlay } from "@renderer/components/ui/list-item-hover-overlay" +import { APP_NAME, views } from "@renderer/lib/constants" +import { FeedViewType } from "@renderer/lib/enum" import { cn } from "@renderer/lib/utils" import { useEntriesPreview } from "@renderer/queries/entries" import { useFeed } from "@renderer/queries/feed" import { DEEPLINK_SCHEME } from "@shared/constants" import type { FC } from "react" +import { Helmet } from "react-helmet-async" import { useParams } from "react-router-dom" export function Component() { @@ -29,23 +33,23 @@ export function Component() { let Item: FC switch (view) { - case 0: { + case FeedViewType.Articles: { Item = ArticleItem break } - case 1: { + case FeedViewType.SocialMedia: { Item = SocialMediaItem break } - case 2: { + case FeedViewType.Pictures: { Item = PictureItem break } - case 3: { + case FeedViewType.Videos: { Item = VideoItem break } - case 5: { + case FeedViewType.Notifications: { Item = NotificationItem break } @@ -57,13 +61,27 @@ export function Component() { return ( <> {feed.data?.feed && ( -
-
- -

{feed.data.feed.title}

-
-
- {feed.data.feed.description} +
+ + + {feed.data.feed.title} + {" "} + | + {" "} + {APP_NAME} + + + +
+
+

{feed.data.feed.title}

+
+
+ {feed.data.feed.description} +
{feed.data.subscriptionCount} @@ -72,20 +90,34 @@ export function Component() { {" "} {feed.data.readCount} {" "} - reads on Follow + reads on + {" "} + {APP_NAME}
- + + + follow on + {" "} + {APP_NAME} +
{entries.data?.map((entry) => ( - + + +
+ + + ) +} diff --git a/src/renderer/src/pages/(external)/layout.tsx b/src/renderer/src/pages/(external)/layout.tsx index 2196414a3..62ac36aa3 100644 --- a/src/renderer/src/pages/(external)/layout.tsx +++ b/src/renderer/src/pages/(external)/layout.tsx @@ -1,11 +1,7 @@ -import { Header } from "@renderer/components/header" import { Outlet } from "react-router-dom" export function Component() { return ( - <> -
- - + ) } diff --git a/src/renderer/src/pages/(external)/login.tsx b/src/renderer/src/pages/(external)/login.tsx index 289fe5131..e5a0c9e39 100644 --- a/src/renderer/src/pages/(external)/login.tsx +++ b/src/renderer/src/pages/(external)/login.tsx @@ -1,5 +1,6 @@ import { signIn, useSession } from "@hono/auth-js/react" import { Button } from "@renderer/components/ui/button" +import { APP_NAME } from "@renderer/lib/constants" import { useEffect, useState } from "react" import { useNavigate } from "react-router-dom" @@ -29,7 +30,12 @@ export function Component() { return (
logo -

Log in to Follow

+

+ Log in to + {" "} + + {APP_NAME} +

{redirecting ? (
Redirecting
diff --git a/src/renderer/src/pages/(external)/redirect.tsx b/src/renderer/src/pages/(external)/redirect.tsx index b3b5468fc..31e48bc5b 100644 --- a/src/renderer/src/pages/(external)/redirect.tsx +++ b/src/renderer/src/pages/(external)/redirect.tsx @@ -1,5 +1,6 @@ import { Button } from "@renderer/components/ui/button" import { UserButton } from "@renderer/components/user-button" +import { APP_NAME } from "@renderer/lib/constants" import { apiFetch } from "@renderer/queries/api-fetch" import { DEEPLINK_SCHEME } from "@shared/constants" import { useEffect } from "react" @@ -32,18 +33,32 @@ export function Component() { logo

- Successfully connected to Follow Account + Successfully connected to + {" "} + {APP_NAME} + {" "} + Account

- You have successfully connected to Follow Account. Now is the time to - open Follow and safely close this page. + You have successfully connected to + {" "} + {APP_NAME} + {" "} + Account. Now is the time to + open + {" "} + {APP_NAME} + {" "} + and safely close this page.

) diff --git a/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx b/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx index abcb220c8..7419b95df 100644 --- a/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx +++ b/src/renderer/src/pages/(main)/(context)/(nonhome)/profile/index.tsx @@ -5,12 +5,14 @@ import { AvatarFallback, AvatarImage, } from "@renderer/components/ui/avatar" +import { Button } from "@renderer/components/ui/button" +import { useSignOut } from "@renderer/hooks/useAuth" import { views } from "@renderer/lib/constants" import { cn } from "@renderer/lib/utils" export function Component() { const { data: session } = useSession() - + const signOut = useSignOut() return (
@@ -20,6 +22,7 @@ export function Component() {
{session?.user?.name}
+
Subscriptions
@@ -33,15 +36,25 @@ export function Component() { {view.name}
))}
+ +
+ +
) diff --git a/src/renderer/src/pages/(main)/(context)/layout.tsx b/src/renderer/src/pages/(main)/(context)/layout.tsx index 725c6be46..ca48ef1a0 100644 --- a/src/renderer/src/pages/(main)/(context)/layout.tsx +++ b/src/renderer/src/pages/(main)/(context)/layout.tsx @@ -1,11 +1,10 @@ import { FeedColumn } from "@renderer/components/feed-column" -import { cn } from "@renderer/lib/utils" import { Outlet } from "react-router-dom" export function Component() { return (
-
+
diff --git a/src/renderer/src/pages/settings/layout.tsx b/src/renderer/src/pages/settings/layout.tsx index 7ef474276..f47a4aaa3 100644 --- a/src/renderer/src/pages/settings/layout.tsx +++ b/src/renderer/src/pages/settings/layout.tsx @@ -29,7 +29,7 @@ export function Component() { className="h-10 border-b flex items-center pl-20 text-sm font-medium" aria-hidden > - Follow Settings + {APP_NAME} Settings
*/}
diff --git a/src/renderer/src/providers/root-providers.tsx b/src/renderer/src/providers/root-providers.tsx index 7af8b8ab9..e754dbc12 100644 --- a/src/renderer/src/providers/root-providers.tsx +++ b/src/renderer/src/providers/root-providers.tsx @@ -7,10 +7,11 @@ import { QueryClientProvider } from "@tanstack/react-query" import { LazyMotion, MotionConfig } from "framer-motion" import { Provider } from "jotai" import type { FC, PropsWithChildren } from "react" +import { HelmetProvider } from "react-helmet-async" const loadFeatures = () => import("../framer-lazy-feature").then((res) => res.default) -export const RootProviders: FC = (props) => ( +export const RootProviders: FC = ({ children }) => ( = (props) => ( - {props.children} + + {children} + diff --git a/src/renderer/src/router.tsx b/src/renderer/src/router.tsx index 930225b31..2adc35653 100644 --- a/src/renderer/src/router.tsx +++ b/src/renderer/src/router.tsx @@ -25,27 +25,38 @@ export const router = createBrowserRouter([ children: [ { path: "", - lazy: () => import("./pages/(main)/(context)/(nonhome)/layout"), + lazy: () => + import("./pages/(main)/(context)/(nonhome)/layout"), children: [ { path: "/discover", - lazy: () => import("./pages/(main)/(context)/(nonhome)/discover/layout"), + lazy: () => + import( + "./pages/(main)/(context)/(nonhome)/discover/layout" + ), children: [ { path: "", lazy: () => - import("./pages/(main)/(context)/(nonhome)/discover/index"), + import( + "./pages/(main)/(context)/(nonhome)/discover/index" + ), }, ], }, { path: "/profile", - lazy: () => import("./pages/(main)/(context)/(nonhome)/profile/layout"), + lazy: () => + import( + "./pages/(main)/(context)/(nonhome)/profile/layout" + ), children: [ { path: "", lazy: () => - import("./pages/(main)/(context)/(nonhome)/profile/index"), + import( + "./pages/(main)/(context)/(nonhome)/profile/index" + ), }, ], }, @@ -74,12 +85,20 @@ export const router = createBrowserRouter([ lazy: () => import("./pages/(external)/debug"), }, { - path: "/feed/:id", - lazy: () => import("./pages/(external)/feed/layout"), + path: "", + lazy: () => import("./pages/(external)/(with-layout)/layout"), children: [ { - path: "", - lazy: () => import("./pages/(external)/feed/index"), + path: "/feed/:id", + lazy: () => + import("./pages/(external)/(with-layout)/feed/layout"), + children: [ + { + path: "", + lazy: () => + import("./pages/(external)/(with-layout)/feed/index"), + }, + ], }, ], }, diff --git a/src/renderer/src/store/entry.ts b/src/renderer/src/store/entry.ts index a9475f094..283f1b695 100644 --- a/src/renderer/src/store/entry.ts +++ b/src/renderer/src/store/entry.ts @@ -4,11 +4,9 @@ import { apiClient } from "@renderer/queries/api-fetch" import type { InferResponseType } from "hono/client" import { produce } from "immer" import { omit } from "lodash-es" -import { create } from "zustand" -import { persist } from "zustand/middleware" import { unreadActions } from "./unread" -import { zustandStorage } from "./utils/helper" +import { createZustandStore } from "./utils/helper" type FeedId = string type EntryId = string @@ -41,120 +39,107 @@ interface EntryActions { markReadByFeedId: (feedId: string) => void } -export const useEntryStore = create( - persist( - (set, get) => ({ - entries: {}, - flatMapEntries: {}, +export const useEntryStore = createZustandStore( + "entry", +)((set, get) => ({ + entries: {}, + flatMapEntries: {}, - fetchEntries: async ({ - level, - id, - view, + fetchEntries: async ({ + level, + id, + view, + read, + + pageParam, + }) => { + const res = await apiClient.entries.$post({ + json: { + publishedAfter: pageParam as string, read, + ...getEntriesParams({ + level, + id, + view, + }), + }, + }) - pageParam, - }: { - level?: string - id?: number | string - view?: number - read?: boolean + const data = await res.json() - pageParam?: string - }) => { - const res = await apiClient.entries.$post({ - json: { - publishedAfter: pageParam as string, - read, - ...getEntriesParams({ - level, - id, - view, - }), - }, + if (data.data) { + data.data.forEach((entry: EntryModel) => { + get().upsert(entry.feeds.id, entry) + }) + } + return data + }, + + getFlattenMapEntries() { + return get().flatMapEntries + }, + optimisticUpdate(entryId: string, changed: Partial) { + set((state) => + produce(state, (draft) => { + const entry = draft.flatMapEntries[entryId] + if (!entry) return + Object.assign(entry, changed) + return draft + }), + ) + }, + optimisticUpdateManyByFeedId(feedId, changed) { + set((state) => + produce(state, (draft) => { + const ids = draft.entries[feedId] + if (!ids) return + + ids.forEach((entryId) => { + Object.assign(draft.flatMapEntries[entryId], changed) }) - const data = await res.json() - - if (data.data) { - data.data.forEach((entry: EntryModel) => { - get().upsert(entry.feeds.id, entry) - }) + return draft + }), + ) + }, + optimisticUpdateAll(changed) { + set((state) => + produce(state, (draft) => { + for (const entry of Object.values(draft.flatMapEntries)) { + Object.assign(entry, changed) } - return data - }, + return draft + }), + ) + }, + upsert(feedId: string, entry: EntryModel) { + set((state) => + produce(state, (draft) => { + if (!draft.entries[feedId]) { + draft.entries[feedId] = [] + } + draft.entries[feedId].push(entry.entries.id) + draft.flatMapEntries[entry.entries.id] = entry + return draft + }), + ) + }, - getFlattenMapEntries() { - return get().flatMapEntries - }, - optimisticUpdate(entryId: string, changed: Partial) { - set((state) => - produce(state, (draft) => { - const entry = draft.flatMapEntries[entryId] - if (!entry) return - Object.assign(entry, changed) - return draft - }), - ) - }, - optimisticUpdateManyByFeedId(feedId, changed) { - set((state) => - produce(state, (draft) => { - const ids = draft.entries[feedId] - if (!ids) return - - ids.forEach((entryId) => { - Object.assign(draft.flatMapEntries[entryId], changed) - }) - - return draft - }), - ) - }, - optimisticUpdateAll(changed) { - set((state) => - produce(state, (draft) => { - for (const entry of Object.values(draft.flatMapEntries)) { - Object.assign(entry, changed) - } - return draft - }), - ) - }, - upsert(feedId: string, entry: EntryModel) { - set((state) => - produce(state, (draft) => { - if (!draft.entries[feedId]) { - draft.entries[feedId] = [] - } - draft.entries[feedId].push(entry.entries.id) - draft.flatMapEntries[entry.entries.id] = entry - return draft - }), - ) - }, - - markRead: (feedId: string, entryId: string, read: boolean) => { - unreadActions.incrementByFeedId(feedId, read ? -1 : 1) - entryActions.optimisticUpdate(entryId, { - read, - }) - }, - markReadByFeedId: (feedId: string) => { - const state = get() - const entries = state.entries[feedId] || [] - entries.forEach((entryId) => { - entryActions.markRead(feedId, entryId, true) - }) - unreadActions.updateByFeedId(feedId, 0) - }, - }), - { - name: "entry", - storage: zustandStorage, - }, - ), -) + markRead: (feedId: string, entryId: string, read: boolean) => { + unreadActions.incrementByFeedId(feedId, read ? -1 : 1) + entryActions.optimisticUpdate(entryId, { + read, + }) + }, + markReadByFeedId: (feedId: string) => { + const state = get() + const entries = state.entries[feedId] || [] + entries.forEach((entryId) => { + entryActions.markRead(feedId, entryId, true) + }) + unreadActions.updateByFeedId(feedId, 0) + }, +})) export const entryActions = { ...(omit(useEntryStore.getState(), [ @@ -170,9 +155,3 @@ export const useEntriesByFeedId = (feedId: string) => }) export const useEntry = (entryId: string) => useEntryStore((state) => state.flatMapEntries[entryId]) - -Object.assign(window, { - __entry() { - return useEntryStore.getState() - }, -}) diff --git a/src/renderer/src/store/subscription.ts b/src/renderer/src/store/subscription.ts index ef84b2f77..de0b7c3f4 100644 --- a/src/renderer/src/store/subscription.ts +++ b/src/renderer/src/store/subscription.ts @@ -3,12 +3,10 @@ import type { SubscriptionModel } from "@renderer/models" import { apiClient } from "@renderer/queries/api-fetch" import { produce } from "immer" import { omit } from "lodash-es" -import { create } from "zustand" -import { persist } from "zustand/middleware" import { entryActions } from "./entry" import { unreadActions } from "./unread" -import { zustandStorage } from "./utils/helper" +import { createZustandStore } from "./utils/helper" type FeedId = string interface SubscriptionState { @@ -19,58 +17,46 @@ interface SubscriptionActions { fetchByView: (view?: FeedViewType) => Promise markReadByView: (view?: FeedViewType) => void } -export const useSubscriptionStore = create( - persist( - (set, get) => ({ - data: {}, +export const useSubscriptionStore = createZustandStore< + SubscriptionState & SubscriptionActions +>("subscription")((set, get) => ({ + data: {}, - async fetchByView(view) { - const res = await ( - await apiClient.subscriptions.$get({ query: { view: String(view) } }) - ).json() + async fetchByView(view) { + const res = await ( + await apiClient.subscriptions.$get({ query: { view: String(view) } }) + ).json() - res.data.forEach((subscription: SubscriptionModel) => { - set((state) => - produce(state, (state) => { - state.data[subscription.feeds.id] = subscription - return state - }), - ) - }) + res.data.forEach((subscription: SubscriptionModel) => { + set((state) => + produce(state, (state) => { + state.data[subscription.feeds.id] = subscription + return state + }), + ) + }) - return res.data - }, - upsert: (feedId, subscription) => { - set((state) => - produce(state, (state) => { - state.data[feedId] = subscription - return state - }), - ) - }, - markReadByView(view) { - const state = get() - for (const feedId in state.data) { - if (state.data[feedId].view === view) { - unreadActions.updateByFeedId(feedId, 0) - entryActions.optimisticUpdateManyByFeedId(feedId, { read: true }) - } - } - }, - }), - { - name: "subscription", - storage: zustandStorage, - }, - ), -) + return res.data + }, + upsert: (feedId, subscription) => { + set((state) => + produce(state, (state) => { + state.data[feedId] = subscription + return state + }), + ) + }, + markReadByView(view) { + const state = get() + for (const feedId in state.data) { + if (state.data[feedId].view === view) { + unreadActions.updateByFeedId(feedId, 0) + entryActions.optimisticUpdateManyByFeedId(feedId, { read: true }) + } + } + }, +})) export const subscriptionActions = { ...omit(useSubscriptionStore.getState(), ["data"]), } - -Object.assign(window, { - __subscription() { - return useSubscriptionStore.getState() - }, -}) diff --git a/src/renderer/src/store/unread.ts b/src/renderer/src/store/unread.ts index cffa511ea..3c013ce52 100644 --- a/src/renderer/src/store/unread.ts +++ b/src/renderer/src/store/unread.ts @@ -2,10 +2,8 @@ import type { FeedViewType } from "@renderer/lib/enum" import { apiClient } from "@renderer/queries/api-fetch" import { produce } from "immer" import { omit } from "lodash-es" -import { create } from "zustand" -import { persist } from "zustand/middleware" -import { zustandStorage } from "./utils/helper" +import { createZustandStore } from "./utils/helper" interface UnreadState { data: Record @@ -15,59 +13,47 @@ interface UnreadActions { fetchUnreadByView: (view?: FeedViewType) => Promise> incrementByFeedId: (feedId: string, inc: number) => void } -export const useUnreadStore = create( - persist( - (set) => ({ - data: {}, +export const useUnreadStore = createZustandStore( + "unread", +)((set) => ({ + data: {}, - async fetchUnreadByView(view) { - const unread = await ( - await apiClient.reads.$get({ query: { view: String(view) } }) - ).json() + async fetchUnreadByView(view) { + const unread = await ( + await apiClient.reads.$get({ query: { view: String(view) } }) + ).json() - const { data } = unread - for (const feedId in data) { - set((state) => - produce(state, (state) => { - state.data[feedId] = data[feedId] - return state - }), - ) - } - return data - }, - incrementByFeedId: (feedId, inc: number) => { - set((state) => - produce(state, (state) => { - const cur = state.data[feedId] - if (cur === undefined) return state - state.data[feedId] = (cur || 0) + inc - return state - }), - ) - }, - updateByFeedId: (feedId, unread) => { - set((state) => - produce(state, (state) => { - state.data[feedId] = unread - return state - }), - ) - }, - }), - { - name: "unread", - storage: zustandStorage, - }, - ), -) + const { data } = unread + for (const feedId in data) { + set((state) => + produce(state, (state) => { + state.data[feedId] = data[feedId] + return state + }), + ) + } + return data + }, + incrementByFeedId: (feedId, inc: number) => { + set((state) => + produce(state, (state) => { + const cur = state.data[feedId] + if (cur === undefined) return state + state.data[feedId] = (cur || 0) + inc + return state + }), + ) + }, + updateByFeedId: (feedId, unread) => { + set((state) => + produce(state, (state) => { + state.data[feedId] = unread + return state + }), + ) + }, +})) export const unreadActions = { ...omit(useUnreadStore.getState(), ["data"]), } - -Object.assign(window, { - __unread() { - return useUnreadStore.getState() - }, -}) diff --git a/src/renderer/src/store/utils/helper.ts b/src/renderer/src/store/utils/helper.ts index 669beeeda..e7cd3d2e7 100644 --- a/src/renderer/src/store/utils/helper.ts +++ b/src/renderer/src/store/utils/helper.ts @@ -1,6 +1,9 @@ import { del, get, set } from "idb-keyval" +import type { StateCreator } from "zustand" import type { StateStorage } from "zustand/middleware" -import { createJSONStorage } from "zustand/middleware" +import { createJSONStorage, persist } from "zustand/middleware" +import { shallow } from "zustand/shallow" +import { createWithEqualityFn } from "zustand/traditional" export const dbStorage: StateStorage = { getItem: async (name: string): Promise => @@ -14,3 +17,29 @@ export const dbStorage: StateStorage = { } export const zustandStorage = createJSONStorage(() => dbStorage) as any + +export const createZustandStore = + < + S, + T extends StateCreator< + S, + [["zustand/persist", unknown]], + [] + > = StateCreator, + >( + name: string, + ) => + (store: T) => { + const newStore = createWithEqualityFn( + persist(store, { name, storage: zustandStorage }), + shallow, + ) + + // const newStore = create(persist(store, { name, storage: zustandStorage })) + Object.assign(window, { + [`__${name}`]() { + return newStore.getState() + }, + }) + return newStore + } diff --git a/src/renderer/src/styles/colors.css b/src/renderer/src/styles/colors.css index 5ed6f42a4..039ad429b 100644 --- a/src/renderer/src/styles/colors.css +++ b/src/renderer/src/styles/colors.css @@ -1,6 +1,6 @@ :root { --fo-vibrancy-foreground: theme(colors.zinc.500); - --fo-vibrancy-background: theme(colors.native.active/10); + --fo-vibrancy-background: theme(colors.native.active); --fo-item-active: theme(colors.native.active/0.4); --fo-item-hover: theme(colors.native.active/0.2); @@ -12,7 +12,7 @@ [data-theme="dark"] { --fo-vibrancy-foreground: theme(colors.zinc.400); /* --fo-vibrancy-background: theme(colors.zinc.600/10); */ - --fo-vibrancy-background: theme(colors.native.active/10); + --fo-vibrancy-background: theme(colors.native.active); --fo-item-active: theme(colors.neutral.700/0.4); --fo-item-hover: theme(colors.native.active/0.2); diff --git a/src/renderer/src/styles/tailwind-extend.css b/src/renderer/src/styles/tailwind-extend.css index 4a4a3c191..f719b3c86 100644 --- a/src/renderer/src/styles/tailwind-extend.css +++ b/src/renderer/src/styles/tailwind-extend.css @@ -10,4 +10,12 @@ .no-drag-region { -webkit-app-region: no-drag; } + .mask-squircle { + mask-image: url(data:image/svg+xml;base64,PHN2ZyB3aWR0aD0nMjAwJyBoZWlnaHQ9JzIwMCcgeG1sbnM9J2h0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnJz48cGF0aCBkPSdNMTAwIDBDMjAgMCAwIDIwIDAgMTAwczIwIDEwMCAxMDAgMTAwIDEwMC0yMCAxMDAtMTAwUzE4MCAwIDEwMCAwWicvPjwvc3ZnPg==); + } + .mask { + mask-size: contain; + mask-repeat: no-repeat; + mask-position: center; + } } diff --git a/tailwind.config.cjs b/tailwind.config.cjs index 5b38d486d..7fccc894e 100644 --- a/tailwind.config.cjs +++ b/tailwind.config.cjs @@ -109,6 +109,7 @@ module.exports = { collections: getIconCollections(["mingcute"]), }), require("tailwindcss-animate"), + require("@tailwindcss/container-queries"), require("@tailwindcss/typography"), require("./src/renderer/src/styles/tailwind-extend.css"), ],