first commit
This commit is contained in:
160
src/component/Common/User/UserAvatar.tsx
Executable file
160
src/component/Common/User/UserAvatar.tsx
Executable file
@@ -0,0 +1,160 @@
|
||||
import { Avatar, Skeleton } from "@mui/material";
|
||||
import { grey } from "@mui/material/colors";
|
||||
import { bindHover, bindPopover } from "material-ui-popup-state";
|
||||
import { usePopupState } from "material-ui-popup-state/hooks";
|
||||
import { memo, useCallback, useEffect, useMemo, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useInView } from "react-intersection-observer";
|
||||
import { ApiPrefix } from "../../../api/request.ts";
|
||||
import { User } from "../../../api/user.ts";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { loadUserInfo } from "../../../redux/thunks/session.ts";
|
||||
import InPrivate from "../../Icons/InPrivate.tsx";
|
||||
import UserPopover from "./UserPopover.tsx";
|
||||
|
||||
export interface UserAvatarProps {
|
||||
user?: User;
|
||||
uid?: string;
|
||||
overwriteTextSize?: boolean;
|
||||
onUserLoaded?: (user: User) => void;
|
||||
enablePopover?: boolean;
|
||||
square?: boolean;
|
||||
cacheKey?: string;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
function stringToColor(string: string) {
|
||||
let hash = 0;
|
||||
let i;
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
for (i = 0; i < string.length; i += 1) {
|
||||
hash = string.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
|
||||
let color = "#";
|
||||
|
||||
for (i = 0; i < 3; i += 1) {
|
||||
const value = (hash >> (i * 8)) & 0xff;
|
||||
color += `00${value.toString(16)}`.slice(-2);
|
||||
}
|
||||
/* eslint-enable no-bitwise */
|
||||
|
||||
return color;
|
||||
}
|
||||
|
||||
export const AnonymousUser: User = {
|
||||
id: "",
|
||||
nickname: "",
|
||||
created_at: "",
|
||||
};
|
||||
|
||||
const UserAvatar = memo(
|
||||
({
|
||||
user,
|
||||
key,
|
||||
overwriteTextSize,
|
||||
onUserLoaded,
|
||||
uid,
|
||||
sx,
|
||||
square,
|
||||
cacheKey,
|
||||
enablePopover,
|
||||
...rest
|
||||
}: UserAvatarProps) => {
|
||||
const [loadedUser, setLoadedUser] = useState<User | undefined>(undefined);
|
||||
const dispatch = useAppDispatch();
|
||||
const popupState = usePopupState({
|
||||
variant: "popover",
|
||||
popupId: "user",
|
||||
});
|
||||
const { t } = useTranslation();
|
||||
const { ref, inView } = useInView({
|
||||
triggerOnce: true,
|
||||
rootMargin: "200px 0px",
|
||||
skip: !!user,
|
||||
});
|
||||
useEffect(() => {
|
||||
if (inView && !loadedUser) {
|
||||
if (uid) {
|
||||
loadUser(uid);
|
||||
}
|
||||
}
|
||||
}, [inView]);
|
||||
useEffect(() => {
|
||||
if (user) {
|
||||
setLoadedUser(user);
|
||||
}
|
||||
}, [user]);
|
||||
const loadUser = useCallback(
|
||||
async (uid: string) => {
|
||||
try {
|
||||
const u = await dispatch(loadUserInfo(uid));
|
||||
setLoadedUser(u);
|
||||
if (onUserLoaded) {
|
||||
onUserLoaded(u);
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("Failed to load user info", e);
|
||||
}
|
||||
},
|
||||
[dispatch, setLoadedUser, onUserLoaded],
|
||||
);
|
||||
|
||||
const avatarUrl = useMemo(() => {
|
||||
if (loadedUser) {
|
||||
return ApiPrefix + `/user/avatar/${loadedUser.id}${cacheKey ? `?nocache=1&key=${cacheKey ?? 0}` : ""}`;
|
||||
}
|
||||
return undefined;
|
||||
}, [loadedUser, cacheKey]);
|
||||
|
||||
return (
|
||||
<>
|
||||
{loadedUser && (
|
||||
<>
|
||||
<Avatar
|
||||
alt={loadedUser.nickname ?? t("modals.anonymous")}
|
||||
src={avatarUrl}
|
||||
slotProps={{
|
||||
img: {
|
||||
loading: "lazy",
|
||||
alt: "",
|
||||
},
|
||||
}}
|
||||
{...rest}
|
||||
{...bindHover(popupState)}
|
||||
sx={[
|
||||
{
|
||||
bgcolor: loadedUser.nickname ? stringToColor(loadedUser.nickname) : grey[500],
|
||||
...sx,
|
||||
},
|
||||
overwriteTextSize && {
|
||||
fontSize: `${sx.width * 0.6}px!important`,
|
||||
},
|
||||
square && {
|
||||
borderRadius: (theme) => `${theme.shape.borderRadius}px`,
|
||||
},
|
||||
]}
|
||||
>
|
||||
{loadedUser.id == "" ? (
|
||||
<InPrivate
|
||||
sx={{
|
||||
...(overwriteTextSize
|
||||
? {
|
||||
fontSize: `${sx.width * 0.6}px`,
|
||||
}
|
||||
: {}),
|
||||
}}
|
||||
/>
|
||||
) : undefined}
|
||||
</Avatar>
|
||||
{enablePopover && user && <UserPopover user={user} {...bindPopover(popupState)} />}
|
||||
</>
|
||||
)}
|
||||
{!loadedUser && <Skeleton ref={ref} variant={"circular"} sx={{ ...sx }} {...rest} />}
|
||||
</>
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
export default UserAvatar;
|
||||
52
src/component/Common/User/UserBadge.tsx
Executable file
52
src/component/Common/User/UserBadge.tsx
Executable file
@@ -0,0 +1,52 @@
|
||||
import UserAvatar, { UserAvatarProps } from "./UserAvatar.tsx";
|
||||
import { Skeleton, TypographyProps } from "@mui/material";
|
||||
import { useState } from "react";
|
||||
import { BadgeText, DefaultButton } from "../StyledComponents.tsx";
|
||||
import { usePopupState } from "material-ui-popup-state/hooks";
|
||||
import UserPopover from "./UserPopover.tsx";
|
||||
import { bindHover, bindPopover } from "material-ui-popup-state";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export interface UserBadgeProps extends UserAvatarProps {
|
||||
textProps?: TypographyProps;
|
||||
}
|
||||
|
||||
const UserBadge = ({ textProps, user, uid, ...rest }: UserBadgeProps) => {
|
||||
const { t } = useTranslation();
|
||||
const [userLoaded, setUserLoaded] = useState(user);
|
||||
const navigate = useNavigate();
|
||||
const popupState = usePopupState({
|
||||
variant: "popover",
|
||||
popupId: "user",
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<DefaultButton
|
||||
{...bindHover(popupState)}
|
||||
onClick={() => navigate(`/profile/${user?.id ?? uid}`)}
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
maxWidth: "150px",
|
||||
}}
|
||||
>
|
||||
<UserAvatar overwriteTextSize user={user} onUserLoaded={(u) => setUserLoaded(u)} uid={uid} {...rest} />
|
||||
<BadgeText {...textProps}>
|
||||
{userLoaded ? (
|
||||
userLoaded.id ? (
|
||||
userLoaded.nickname
|
||||
) : (
|
||||
t("application:modals.anonymous")
|
||||
)
|
||||
) : (
|
||||
<Skeleton width={60} />
|
||||
)}
|
||||
</BadgeText>
|
||||
</DefaultButton>
|
||||
{userLoaded && <UserPopover user={userLoaded} {...bindPopover(popupState)} />}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserBadge;
|
||||
122
src/component/Common/User/UserPopover.tsx
Executable file
122
src/component/Common/User/UserPopover.tsx
Executable file
@@ -0,0 +1,122 @@
|
||||
import { Box, Button, PopoverProps, styled, Tooltip, Typography } from "@mui/material";
|
||||
import HoverPopover from "material-ui-popup-state/HoverPopover";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { Trans, useTranslation } from "react-i18next";
|
||||
import { User } from "../../../api/user.ts";
|
||||
import UserAvatar from "./UserAvatar.tsx";
|
||||
import { useAppDispatch } from "../../../redux/hooks.ts";
|
||||
import { loadUserInfo } from "../../../redux/thunks/session.ts";
|
||||
import MailOutlined from "../../Icons/MailOutlined.tsx";
|
||||
import HomeOutlined from "../../Icons/HomeOutlined.tsx";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
import TimeBadge from "../TimeBadge.tsx";
|
||||
|
||||
interface UserPopoverProps extends PopoverProps {
|
||||
user: User;
|
||||
}
|
||||
|
||||
const ActionButton = styled(Button)({
|
||||
minWidth: "initial",
|
||||
});
|
||||
|
||||
export const UserProfile = ({ user, open, displayOnly }: { user: User; open: boolean; displayOnly?: boolean }) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const navigate = useNavigate();
|
||||
const [loadedUser, setLoadedUser] = useState(user);
|
||||
useEffect(() => {
|
||||
if (open && user.id && !user.group) {
|
||||
dispatch(loadUserInfo(user.id)).then((u) => {
|
||||
if (u) {
|
||||
setLoadedUser(u);
|
||||
}
|
||||
});
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
<UserAvatar overwriteTextSize user={user} sx={{ width: 80, height: 80 }} />
|
||||
<Box sx={{ ml: 2 }}>
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
<Typography variant={"h6"} fontWeight={600}>
|
||||
{user.id ? user.nickname : t("application:modals.anonymous")}
|
||||
</Typography>
|
||||
{displayOnly && (
|
||||
<Typography variant={"body2"} sx={{ ml: 1 }} color={"text.secondary"}>
|
||||
{loadedUser?.group ? loadedUser.group.name : ""}
|
||||
</Typography>
|
||||
)}
|
||||
</Box>
|
||||
{!displayOnly && (
|
||||
<Typography variant={"body2"} color={"text.secondary"}>
|
||||
{loadedUser?.group ? loadedUser.group.name : ""}
|
||||
</Typography>
|
||||
)}
|
||||
{displayOnly && loadedUser?.created_at && (
|
||||
<Typography variant={"body2"} color={"text.secondary"}>
|
||||
<Trans
|
||||
i18nKey={"setting.accountCreatedAt"}
|
||||
ns={"application"}
|
||||
components={[<TimeBadge variant={"inherit"} datetime={loadedUser.created_at} />]}
|
||||
/>
|
||||
</Typography>
|
||||
)}
|
||||
<Box
|
||||
sx={{
|
||||
mt: 0.5,
|
||||
position: "relative",
|
||||
left: -10,
|
||||
display: "flex",
|
||||
}}
|
||||
>
|
||||
{!displayOnly && user.id && (
|
||||
<Tooltip title={t("application:setting.profilePage")}>
|
||||
<ActionButton onClick={() => navigate(`/profile/${user.id}`)}>
|
||||
<HomeOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
{user.email && (
|
||||
<Tooltip title={user.email}>
|
||||
<ActionButton onClick={() => window.open(`mailto:${user.email}`)}>
|
||||
<MailOutlined />
|
||||
</ActionButton>
|
||||
</Tooltip>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
const UserPopover = ({ user, open, ...rest }: UserPopoverProps) => {
|
||||
const stopPropagation = useCallback((e: any) => e.stopPropagation(), []);
|
||||
return (
|
||||
<HoverPopover
|
||||
onMouseDown={stopPropagation}
|
||||
onMouseUp={stopPropagation}
|
||||
onClick={stopPropagation}
|
||||
open={open}
|
||||
{...rest}
|
||||
anchorOrigin={{
|
||||
vertical: "bottom",
|
||||
horizontal: "left",
|
||||
}}
|
||||
transformOrigin={{
|
||||
vertical: "top",
|
||||
horizontal: "left",
|
||||
}}
|
||||
>
|
||||
<Box sx={{ minWidth: "300px", p: 2 }}>
|
||||
<UserProfile user={user} open={open} />
|
||||
</Box>
|
||||
</HoverPopover>
|
||||
);
|
||||
};
|
||||
|
||||
export default UserPopover;
|
||||
Reference in New Issue
Block a user