Skip to content

Commit

Permalink
feat: add user image to user page
Browse files Browse the repository at this point in the history
closes #216
  • Loading branch information
drodil committed Nov 26, 2024
1 parent 1d03797 commit c38ebe7
Show file tree
Hide file tree
Showing 6 changed files with 103 additions and 49 deletions.
10 changes: 7 additions & 3 deletions plugins/qeta-react/src/components/Buttons/UserFollowButton.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React from 'react';
import React, { CSSProperties } from 'react';
import Visibility from '@material-ui/icons/Visibility';
import VisibilityOff from '@material-ui/icons/VisibilityOff';
import { useTranslation, useUserFollow } from '../../hooks';
import { IconButton, Tooltip } from '@material-ui/core';

export const UserFollowButton = (props: { userRef: string }) => {
const { userRef } = props;
export const UserFollowButton = (props: {
userRef: string;
style?: CSSProperties;
}) => {
const { userRef, style } = props;
const { t } = useTranslation();
const users = useUserFollow();
if (users.loading) {
Expand All @@ -25,6 +28,7 @@ export const UserFollowButton = (props: { userRef: string }) => {
users.followUser(userRef);
}
}}
style={style}
>
{users.isFollowingUser(userRef) ? <VisibilityOff /> : <Visibility />}
</IconButton>
Expand Down
46 changes: 35 additions & 11 deletions plugins/qeta-react/src/components/TagsAndEntities/UserChip.tsx
Original file line number Diff line number Diff line change
@@ -1,35 +1,59 @@
import { useRouteRef } from '@backstage/core-plugin-api';
import { userRouteRef } from '../../routes';
import { useTranslation, useUserFollow } from '../../hooks';
import {
useIdentityApi,
useTranslation,
useUserFollow,
useUserInfo,
} from '../../hooks';
import { useEntityPresentation } from '@backstage/plugin-catalog-react';
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Button, Chip, Grid, Tooltip, Typography } from '@material-ui/core';
import {
Avatar,
Box,
Button,
Chip,
Grid,
Tooltip,
Typography,
} from '@material-ui/core';

export const UserTooltip = (props: { entityRef: string }) => {
const { entityRef } = props;
const { t } = useTranslation();
const {
primaryTitle: userName,
Icon,
secondaryTitle,
} = useEntityPresentation(
entityRef.startsWith('user:') ? entityRef : `user:${entityRef}`,
const { name, initials, user, secondaryTitle } = useUserInfo(
entityRef,
entityRef === 'anonymous',
);

const { value: currentUser } = useIdentityApi(
api => api.getBackstageIdentity(),
[],
);
const users = useUserFollow();

return (
<Grid container style={{ padding: '0.5em' }} spacing={1}>
<Grid item xs={12}>
<Typography variant="h6">
{Icon ? <Icon fontSize="small" /> : null}
{entityRef === 'anonymous' ? t('userLink.anonymous') : userName}
<Box style={{ display: 'inline-block', marginRight: '0.3em' }}>
<Avatar
src={user?.spec?.profile?.picture}
alt={name}
variant="rounded"
style={{ width: '1em', height: '1em' }}
>
{initials}
</Avatar>
</Box>
{name}
</Typography>
</Grid>
<Grid item xs={12}>
<Typography variant="subtitle2">{secondaryTitle}</Typography>
</Grid>
{!users.loading && (
{!users.loading && currentUser?.userEntityRef !== entityRef && (
<Grid item xs={12}>
<Button
size="small"
Expand Down
1 change: 1 addition & 0 deletions plugins/qeta-react/src/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export * from './useQetaApi';
export * from './useTagsFollow';
export * from './useTranslation';
export * from './useEntityFollow';
export * from './useEntityAuthor';
export * from './useUserFollow';
export * from './useIdentityApi';
export * from './useCollectionsFollow';
Expand Down
66 changes: 40 additions & 26 deletions plugins/qeta-react/src/hooks/useEntityAuthor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { UserEntity } from '@backstage/catalog-model';
import { parseEntityRef, UserEntity } from '@backstage/catalog-model';
import { CatalogApi } from '@backstage/catalog-client';
import DataLoader from 'dataloader';
import { identityApiRef, useApi } from '@backstage/core-plugin-api';
Expand All @@ -13,6 +13,7 @@ import {
PostResponse,
UserResponse,
} from '@drodil/backstage-plugin-qeta-common';
import { useTranslation } from './useTranslation';

const userCache: Map<string, UserEntity> = new Map();
const dataLoaderFactory = (catalogApi: CatalogApi) =>
Expand Down Expand Up @@ -44,41 +45,34 @@ const dataLoaderFactory = (catalogApi: CatalogApi) =>
},
);

export const useEntityAuthor = (
entity: PostResponse | AnswerResponse | CollectionResponse | UserResponse,
) => {
export const useUserInfo = (entityRef: string, anonymous?: boolean) => {
const catalogApi = useApi(catalogApiRef);
const identityApi = useApi(identityApiRef);
const { t } = useTranslation();
const [name, setName] = React.useState<string | undefined>(undefined);
const [user, setUser] = React.useState<UserEntity | null>(null);
const [initials, setInitials] = React.useState<string | null>(null);
const [currentUser, setCurrentUser] = React.useState<string | null>(null);
const anonymous = 'anonymous' in entity ? entity.anonymous ?? false : false;
let author =
// eslint-disable-next-line no-nested-ternary
'author' in entity
? entity.author
: 'userRef' in entity
? entity.userRef
: entity.owner;
if (!author.startsWith('user:')) {
author = `user:${author}`;
}
const ref = entityRef.startsWith('user:') ? entityRef : `user:${entityRef}`;

const { primaryTitle: userName } = useEntityPresentation(author);
const {
primaryTitle: userName,
secondaryTitle,
Icon,
} = useEntityPresentation(ref);

useEffect(() => {
if (anonymous) {
return;
}

if (userCache.get(author)) {
setUser(userCache.get(author) as UserEntity);
if (userCache.get(ref)) {
setUser(userCache.get(ref) as UserEntity);
return;
}

dataLoaderFactory(catalogApi)
.load(author)
.load(ref)
.then(data => {
if (data) {
setUser(data as UserEntity);
Expand All @@ -89,7 +83,7 @@ export const useEntityAuthor = (
.catch(() => {
setUser(null);
});
}, [catalogApi, author, anonymous]);
}, [catalogApi, ref, anonymous]);

useEffect(() => {
identityApi.getBackstageIdentity().then(res => {
Expand All @@ -99,14 +93,20 @@ export const useEntityAuthor = (

useEffect(() => {
let displayName = userName;
if (author === currentUser) {
displayName = 'You';
if (anonymous) {
displayName += ' (anonymous)';
if (anonymous) {
displayName = t('userLink.anonymous');
} else if (currentUser) {
const currentUserRef = parseEntityRef(currentUser);
const userRef = parseEntityRef(ref);
if (
currentUserRef.name === userRef.name &&
currentUserRef.namespace === userRef.namespace
) {
displayName = t('userLink.you');
}
}
setName(displayName);
}, [author, anonymous, currentUser, userName]);
}, [ref, anonymous, currentUser, userName, t]);

useEffect(() => {
const init = (name ?? '')
Expand All @@ -118,5 +118,19 @@ export const useEntityAuthor = (
setInitials(init);
}, [name]);

return { name, initials, user };
return { name, initials, user, secondaryTitle, Icon };
};

export const useEntityAuthor = (
entity: PostResponse | AnswerResponse | CollectionResponse | UserResponse,
) => {
const anonymous = 'anonymous' in entity ? entity.anonymous ?? false : false;
const author =
// eslint-disable-next-line no-nested-ternary
'author' in entity
? entity.author
: 'userRef' in entity
? entity.userRef
: entity.owner;
return useUserInfo(author, anonymous);
};
1 change: 1 addition & 0 deletions plugins/qeta-react/src/translation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,7 @@ export const qetaTranslationRef = createTranslationRef({
},
userLink: {
anonymous: 'Anonymous',
you: 'You',
},
articlePage: {
notFound: 'Could not find the article',
Expand Down
28 changes: 19 additions & 9 deletions plugins/qeta/src/components/UserPage/UserPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,21 +11,21 @@ import {
useIdentityApi,
UserFollowButton,
useTranslation,
useUserInfo,
WriteArticleButton,
} from '@drodil/backstage-plugin-qeta-react';
import { useEntityPresentation } from '@backstage/plugin-catalog-react';
import { UserStatsContent } from './UserStatsContent';
import { TabContext, TabList, TabPanel } from '@material-ui/lab';
import { Box, Tab, Typography } from '@material-ui/core';
import { Avatar, Box, Tab, Typography } from '@material-ui/core';

export const UserPage = () => {
const identity = useParams()['*'] ?? 'unknown';
const presentation = useEntityPresentation(identity);
const { name, initials, user, secondaryTitle } = useUserInfo(identity);
const [tab, setTab] = useState('statistics');
const { t } = useTranslation();
const [_searchParams, setSearchParams] = useSearchParams();
const {
value: user,
value: currentUser,
loading: loadingUser,
error: userError,
} = useIdentityApi(api => api.getBackstageIdentity(), []);
Expand All @@ -36,16 +36,26 @@ export const UserPage = () => {
};
const title = (
<Typography variant="h5" component="h2">
{presentation.primaryTitle}
{!loadingUser && !userError && user?.userEntityRef !== identity && (
<UserFollowButton userRef={identity} />
)}
<Box style={{ display: 'inline-block', marginRight: '0.5em' }}>
<Avatar src={user?.spec?.profile?.picture} alt={name} variant="rounded">
{initials}
</Avatar>
</Box>
{name}
{!loadingUser &&
!userError &&
currentUser?.userEntityRef !== identity && (
<UserFollowButton
userRef={identity}
style={{ marginLeft: '0.5em' }}
/>
)}
</Typography>
);

return (
<>
<ContentHeader titleComponent={title}>
<ContentHeader titleComponent={title} description={secondaryTitle}>
<ButtonContainer>
<AskQuestionButton />
<WriteArticleButton />
Expand Down

0 comments on commit c38ebe7

Please sign in to comment.