Skip to content

Commit

Permalink
fix: user profile modal
Browse files Browse the repository at this point in the history
Signed-off-by: Innei <i@innei.in>
  • Loading branch information
Innei committed Jul 28, 2024
1 parent 7a8594d commit 374c1d0
Show file tree
Hide file tree
Showing 7 changed files with 337 additions and 107 deletions.
12 changes: 10 additions & 2 deletions src/renderer/src/atoms/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,13 @@ export const [, , useMe, useSetMe, getMe, setMe] = createAtomHooks(
atom<Nullable<User>>(null),
)

export const [, , useLoginModalShow, useSetLoginModalShow, getLoginModalShow, setLoginModalShow] =
createAtomHooks(atom<boolean>(false))
export { useMe as useWhoAmI }

export const [
,
,
useLoginModalShow,
useSetLoginModalShow,
getLoginModalShow,
setLoginModalShow,
] = createAtomHooks(atom<boolean>(false))
9 changes: 4 additions & 5 deletions src/renderer/src/components/user-button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import { defineQuery } from "@renderer/lib/defineQuery"
import { nextFrame } from "@renderer/lib/dom"
import { cn } from "@renderer/lib/utils"
import { LoginModalContent } from "@renderer/modules/auth/LoginModalContent"
import { usePresentUserProfileModal } from "@renderer/modules/profile/hooks"
import { useSettingModal } from "@renderer/modules/settings/modal/hooks"
import { useSession } from "@renderer/queries/auth"
import { WEB_URL } from "@shared/constants"
import type { FC } from "react"
import { memo } from "react"
import { Link } from "react-router-dom"
Expand Down Expand Up @@ -68,9 +68,11 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
const { user } = session || {}
const signOut = useSignOut()
const settingModalPresent = useSettingModal()
const presentUserProfile = usePresentUserProfileModal()
if (status !== "authenticated") {
return <LoginButton {...props} />
}

return (
<DropdownMenu>
<DropdownMenuTrigger className="!outline-none focus-visible:bg-theme-item-hover">
Expand All @@ -96,10 +98,7 @@ export const ProfileButton: FC<LoginProps> = memo((props) => {
<DropdownMenuSeparator />
<DropdownMenuItem
onClick={() => {
window.open(
`${WEB_URL}/profile/${user?.handle || user?.id}`,
"_blank",
)
presentUserProfile(user?.id)
}}
>
<i className="i-mgc-user-3-cute-re mr-1.5" />
Expand Down
4 changes: 2 additions & 2 deletions src/renderer/src/modules/discover/feed-form.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,11 @@ export const FeedForm: Component<{
},
})

const { setClickOutSideToDismiss } = useCurrentModal()
const { setClickOutSideToDismiss } = useCurrentModal() || {}

useEffect(() => {
if (form.formState.isDirty) {
setClickOutSideToDismiss(false)
setClickOutSideToDismiss?.(false)
}
}, [form.formState.isDirty])

Expand Down
20 changes: 15 additions & 5 deletions src/renderer/src/modules/entry-content/read-history.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { useEntryReadHistory } from "@renderer/store/entry"
import { useUserById } from "@renderer/store/user"
import { Fragment } from "react"

import { usePresentUserProfileModal } from "../profile/hooks"

export const EntryReadHistory: Component<{ entryId: string }> = ({
entryId,
}) => {
Expand Down Expand Up @@ -66,6 +68,7 @@ const EntryUser: Component<{
i: number
}> = ({ userId, i }) => {
const user = useUserById(userId)
const presentUserProfile = usePresentUserProfileModal()
if (!user) return null
return (
<Tooltip>
Expand All @@ -76,14 +79,21 @@ const EntryUser: Component<{
zIndex: i,
}}
>
<Avatar className="aspect-square size-8 border border-border ring-1 ring-background">
<AvatarImage src={user?.image || undefined} />
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
<button
className="no-drag-region"
type="button"
onClick={() => {
presentUserProfile(userId)
}}
>
<Avatar className="aspect-square size-8 border border-border ring-1 ring-background">
<AvatarImage src={user?.image || undefined} />
<AvatarFallback>{user.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
</button>
</TooltipTrigger>
<TooltipContent side="top">
Recent reader:
{" "}
{user.name}
</TooltipContent>
</Tooltip>
Expand Down
63 changes: 63 additions & 0 deletions src/renderer/src/modules/profile/hooks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { useModalStack } from "@renderer/components/ui/modal"
import { NoopChildren } from "@renderer/components/ui/modal/stacked/utils"
import { useAuthQuery } from "@renderer/hooks/common"
import { apiClient } from "@renderer/lib/api-fetch"
import { defineQuery } from "@renderer/lib/defineQuery"
import { capitalizeFirstLetter } from "@renderer/lib/utils"
import { createElement, useCallback } from "react"
import { parse } from "tldts"

import { UserProfileModalContent } from "./user-profile-modal"

export const useUserSubscriptionsQuery = (userId: string | undefined) => {
const subscriptions = useAuthQuery(
defineQuery(["subscriptions", userId], async () => {
const res = await apiClient.subscriptions.$get({
query: { userId },
})
const groupFolder = {} as Record<string, typeof res.data>

for (const subscription of res.data || []) {
if (!subscription.category && subscription.feeds) {
const { siteUrl } = subscription.feeds
if (!siteUrl) continue
const parsed = parse(siteUrl)
parsed.domain &&
(subscription.category = capitalizeFirstLetter(parsed.domain))
}
if (subscription.category) {
if (!groupFolder[subscription.category]) {
groupFolder[subscription.category] = []
}
groupFolder[subscription.category].push(subscription)
}
}

return groupFolder
}),
{
enabled: !!userId,
},
)
return subscriptions
}

export const usePresentUserProfileModal = () => {
const { present } = useModalStack()

return useCallback(
(userId: string | undefined) => {
if (!userId) return
present({
title: "User Profile",
content: () =>
createElement(UserProfileModalContent, {
userId,
}),
CustomModalComponent: NoopChildren,
clickOutsideToDismiss: true,
})
},
[present],
)
}
178 changes: 178 additions & 0 deletions src/renderer/src/modules/profile/user-profile-modal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import { getSidebarActiveView } from "@renderer/atoms/sidebar"
import { m } from "@renderer/components/common/Motion"
import { FeedIcon } from "@renderer/components/feed-icon"
import { FollowIcon } from "@renderer/components/icons/follow"
import {
Avatar,
AvatarFallback,
AvatarImage,
} from "@renderer/components/ui/avatar"
import { StyledButton } from "@renderer/components/ui/button"
import { LoadingCircle } from "@renderer/components/ui/loading"
import { useCurrentModal, useModalStack } from "@renderer/components/ui/modal"
import { useAuthQuery } from "@renderer/hooks/common"
import { apiClient } from "@renderer/lib/api-fetch"
import { defineQuery } from "@renderer/lib/defineQuery"
import { nextFrame } from "@renderer/lib/dom"
import type { FeedViewType } from "@renderer/lib/enum"
import { cn } from "@renderer/lib/utils"
import { useUserSubscriptionsQuery } from "@renderer/modules/profile/hooks"
import { useAnimationControls } from "framer-motion"
import type { FC } from "react"
import { Fragment, useEffect, useState } from "react"

import { FeedForm } from "../discover/feed-form"

export const UserProfileModalContent: FC<{
userId: string
}> = ({ userId }) => {
const user = useAuthQuery(
defineQuery(["profiles", userId], async () => {
const res = await apiClient.profiles.$get({
query: { id: userId! },
})
return res.data
}),
)

const subscriptions = useUserSubscriptionsQuery(user.data?.id)
const modal = useCurrentModal()
const controller = useAnimationControls()
useEffect(() => {
nextFrame(() => controller.start("enter"))
}, [controller])

const { present } = useModalStack()
const winHeight = useState(() => window.innerHeight)[0]

return (
<div className="container center h-full" onClick={modal.dismiss}>
<m.div
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
initial="initial"
animate={controller}
variants={{
enter: {
y: 0,
opacity: 1,
},
initial: {
y: "100%",
opacity: 0.9,
},
exit: {
y: winHeight,
},
}}
transition={{
type: "spring",

mass: 0.4,
tension: 100,
friction: 1,
}}
exit="exit"
className="shadow-perfect perfect-sm relative flex max-h-[80vh] flex-col items-center overflow-hidden rounded-xl border bg-theme-background p-8"
>
<button
className="absolute right-2 top-2 z-10 p-2"
onClick={modal.dismiss}
type="button"
aria-label="close profile"
>
<i className="i-mgc-close-cute-re text-[20px] opacity-80 hover:to-theme-button-hover" />
</button>
{user.data && (
<Fragment>
<div className="center m-12 flex shrink-0 flex-col">
<Avatar className="aspect-square size-16">
<AvatarImage src={user.data.image || undefined} />
<AvatarFallback>{user.data.name?.slice(0, 2)}</AvatarFallback>
</Avatar>
<div className="flex flex-col items-center">
<div className="mb-2 mt-4 flex items-center text-2xl font-bold">
<h1>{user.data.name}</h1>
</div>
<div className="mb-8 text-sm text-zinc-500">
{user.data.handle}
</div>
</div>
</div>
<div className="mb-4 h-full w-[70ch] max-w-full space-y-10 overflow-auto px-5">
{Object.keys(subscriptions.data || {}).map((category) => (
<div key={category}>
<div className="mb-4 flex items-center text-2xl font-bold">
<h3>{category}</h3>
</div>
<div>
{subscriptions.data?.[category].map((subscription) => (
<div
key={subscription.feedId}
className="group relative border-b py-5"
>
<a
className="flex flex-1 cursor-default"
href={subscription.feeds.siteUrl!}
target="_blank"
>
<FeedIcon
feed={subscription.feeds}
size={22}
className="mr-3"
/>
<div
className={cn(
"w-0 flex-1 grow",
"group-hover:grow-[0.85]",
)}
>
<div className="truncate font-medium leading-none">
{subscription.feeds?.title}
</div>
<div className="mt-1 line-clamp-1 text-xs text-zinc-500">
{subscription.feeds?.description}
</div>
</div>
<div className="absolute right-0 opacity-0 transition-opacity group-hover:opacity-100">
<StyledButton
onClick={(e) => {
e.stopPropagation()
e.preventDefault()
const defaultView =
getSidebarActiveView() as FeedViewType

present({
title: "Add follow",
content: ({ dismiss }) => (
<FeedForm
asWidget
url={subscription.feeds.url}
defaultView={defaultView}
onSuccess={dismiss}
/>
),
})
}}
>
<FollowIcon className="mr-1 size-3" />
{APP_NAME}
</StyledButton>
</div>
</a>
</div>
))}
</div>
</div>
))}
</div>
</Fragment>
)}

{!user.data && (
<LoadingCircle size="large" className="center h-48 w-[46.125rem] max-w-full" />
)}
</m.div>
</div>
)
}
Loading

0 comments on commit 374c1d0

Please sign in to comment.