Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

feat: Add Comments tab to User Profile #653

Merged
merged 4 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions packages/backend/schema.gql
Original file line number Diff line number Diff line change
Expand Up @@ -578,6 +578,22 @@ type User {
team: String
title: String
updatedAt: DateTime!
userComments(
"""Paginate after opaque cursor"""
after: String

"""Paginate before opaque cursor"""
before: String

"""Paginate first"""
first: Float

"""Paginate last"""
last: Float

"""Sort columns"""
sort: [Sort!]
): CommentConnection
userName: String!
}

Expand Down
4 changes: 4 additions & 0 deletions packages/backend/src/models/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { Permission } from '../models/permission';
import { encrypt, decrypt } from '../shared/crypto';
import { toGlobalId } from '../shared/resolver-utils';

import { CommentConnection } from './comment';
import { Connection, Edge } from './connection';
import { PageInfo } from './connection';
import { InsightConnection } from './insight';
Expand Down Expand Up @@ -154,6 +155,9 @@ export class User extends BaseModel {
@Field(() => InsightConnection, { nullable: true })
likedInsights?: InsightConnection;

@Field(() => CommentConnection, { nullable: true })
userComments?: CommentConnection;

@Field()
commentCount!: number;

Expand Down
6 changes: 6 additions & 0 deletions packages/backend/src/resolvers/user.resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { Service } from 'typedi';

import { userCache } from '../middleware/oauth-authenticator';
import { ActivityType } from '../models/activity';
import { CommentConnection } from '../models/comment';
import { ConnectionArgs } from '../models/connection';
import { Context } from '../models/context';
import { InsightConnection } from '../models/insight';
Expand Down Expand Up @@ -109,6 +110,11 @@ export class UserResolver {
return this.userService.getLikedInsights(user, connectionArgs);
}

@FieldResolver()
async userComments(@Root() user: User, @Args() connectionArgs: ConnectionArgs): Promise<CommentConnection> {
return this.userService.getUserComments(user, connectionArgs);
}

@FieldResolver()
async commentCount(@Root() user: User): Promise<number> {
return this.userService.getCommentCount(user.userId);
Expand Down
23 changes: 21 additions & 2 deletions packages/backend/src/services/user.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,13 +27,13 @@ import { getInsights, getInsightsByContributor } from '../lib/elasticsearch';
import { ActivityType } from '../models/activity';
import { UniqueValue } from '../models/autocomplete';
import { GitHubTokenMetadata } from '../models/backends/github';
import { Comment } from '../models/comment';
import { Comment, CommentConnection } from '../models/comment';
import { ConnectionArgs } from '../models/connection';
import { Insight, InsightConnection } from '../models/insight';
import { UpdateUserInput, User, UserGitHubProfile } from '../models/user';
import { UserHealthCheck } from '../models/user-health-check';
import { UserInsight } from '../models/user-insight';
import { fromGlobalId } from '../shared/resolver-utils';
import { fromGlobalId, toCursor } from '../shared/resolver-utils';

import { ActivityService } from './activity.service';

Expand Down Expand Up @@ -284,4 +284,23 @@ export class UserService {

return result.map((row: any) => ({ value: row.team as string, occurrences: Number.parseInt(row.count) }));
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
async getUserComments(user: User, connectionArgs: ConnectionArgs): Promise<CommentConnection> {
logger.silly('[USER.SERVICE] userComments');

const userComments = await Comment.query()
.where('authorId', user.userId)
.innerJoin('insight', 'comment.insightId', 'insight.insightId')
.whereNull('insight.deletedAt')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably need to add .whereNull('comment.deletedAt') here.

.whereNull('comment.deletedAt')
.orderBy('updatedAt', 'desc');

return {
pageInfo: {
total: userComments.length
},
edges: userComments.map((comment, i) => ({ cursor: toCursor('Comment', i), node: comment as Comment }))
} as CommentConnection;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/**
* Copyright 2022 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { HStack, Stack, Text, Tooltip, VStack } from '@chakra-ui/react';

import { ItemTypeIcon } from '../../../../../../components/item-type-icon/item-type-icon';
import { LikeButton } from '../../../../../../components/like-button/like-button';
import { LikedByTooltip } from '../../../../../../components/liked-by-tooltip/liked-by-tooltip';
import { Link } from '../../../../../../components/link/link';
import { MarkdownContainer } from '../../../../../../components/markdown-container/markdown-container';
import type { Comment, User } from '../../../../../../models/generated/graphql';
import { formatDateIntl, formatRelativeIntl } from '../../../../../../shared/date-utils';

interface Props {
comment: Comment;
userName: string;
displayName: string;
onFetchLikedBy: (commentId: string) => Promise<User[]>;
onLike: (commentId: string, liked: boolean) => Promise<boolean>;
}

export const ProfileCommentCard = ({ comment, userName, displayName, onFetchLikedBy, onLike }: Props) => {
const likeLabel = comment.viewerHasLiked ? 'Unlike this comment' : 'Like this comment';

const toggleLike = async (liked: boolean) => {
return onLike(comment.id, liked);
};

return (
<VStack spacing="1rem" align="stretch">
<Stack direction={{ base: 'column', md: 'row' }}>
<Stack direction="row">
{' '}
<ItemTypeIcon itemType={comment?.insight.itemType ?? 'insight'} />
<Link to={`/${comment?.insight.itemType}/${comment?.insight.fullName}`} display="inline-block">
<Text fontWeight="bold" fontSize="md" display="inline-block">
{comment?.insight.name}
</Text>
</Link>
</Stack>

<Tooltip
label={formatDateIntl(comment.createdAt)}
aria-label={`Occurred at ${formatDateIntl(comment.createdAt)}`}
>
<Text fontSize="sm" color="polar.600" flexShrink={0}>
{formatRelativeIntl(comment.createdAt)}
</Text>
</Tooltip>

<Text color="polar.600" fontSize="sm">
{comment.isEdited && (
<Tooltip label="This comment was edited" aria-label="This comment was edited">
{' (edited)'}
</Tooltip>
)}
</Text>

<HStack spacing="0.25rem" flexGrow={1} justify="flex-end" align="center">
<LikedByTooltip
label={likeLabel}
likeCount={comment.likeCount}
onFetchLikedBy={() => onFetchLikedBy(comment.id)}
placement="bottom"
>
<LikeButton
label={likeLabel}
liked={comment.viewerHasLiked}
likeCount={comment.likeCount}
onLike={toggleLike}
disabled={comment.isOwnComment}
/>
</LikedByTooltip>
</HStack>
</Stack>

<MarkdownContainer contents={comment.commentText} />
</VStack>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
/**
* Copyright 2022 Expedia, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

import { Flex, StackDivider, Text, useToast, VStack } from '@chakra-ui/react';
import { gql, useMutation } from 'urql';

import type { User, Comment } from '../../../../../../models/generated/graphql';
import { useLikedBy } from '../../../../../../shared/useLikedBy';
import { ProfileCommentCard } from '../profile-comment-card/profile-comment-card';

const COMMENT_FRAGMENT = gql`
fragment CommentFields on Comment {
id
commentText
createdAt
isEdited
isDeleted
isOwnComment
viewerHasLiked
likeCount
author {
id
userName
displayName
email
}
childComments {
edges {
node {
id
commentText
createdAt
isEdited
isDeleted
isOwnComment
viewerHasLiked
likeCount
author {
id
userName
displayName
email
}
}
}
}
}
`;

const LIKE_COMMENT_MUTATION = gql`
${COMMENT_FRAGMENT}
mutation LikeComment($commentId: ID!, $liked: Boolean!) {
likeComment(commentId: $commentId, liked: $liked) {
...CommentFields
}
}
`;

interface Props {
user: User;
}

export const UserComments = ({ user }: Props) => {
const toast = useToast();

const comments = user.userComments?.edges.map((e) => e.node) || [];
const totalComments = user.userComments?.pageInfo?.total || 0;

const [, likeComment] = useMutation(LIKE_COMMENT_MUTATION);

const onLike = async (commentId: string, liked: boolean): Promise<boolean> => {
console.log('Liking comment:', commentId);
const { error } = await likeComment({
commentId,
liked
});

if (error) {
toast({
position: 'bottom-right',
title: 'Unable to like comment.',
status: 'error',
duration: 9000,
isClosable: true
});
return false;
}

return true;
};

const { onFetchLikedBy } = useLikedBy('comment');

return (
<VStack spacing="1rem" p="1rem" pt={0} align="stretch" divider={<StackDivider borderColor="snowstorm.100" />}>
<Flex mb="0.5rem">
<Text textTransform="uppercase" fontSize="sm" fontWeight="bold" color="polar.600">
{totalComments} result{totalComments > 1 && 's'}
</Text>
</Flex>
{comments.map((comment: Comment) => {
return (
<ProfileCommentCard
comment={comment}
displayName={user.displayName}
key={comment.id}
userName={user.userName}
onFetchLikedBy={onFetchLikedBy}
onLike={onLike}
/>
);
})}
</VStack>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { iconFactory } from '../../../../shared/icon-factory';

import { UserAbout } from './components/user-about/user-about';
import { UserActivity } from './components/user-activity/user-activity';
import { UserComments } from './components/user-comments/user-comments';
import { UserDrafts } from './components/user-drafts/user-drafts';
import { UserInsights } from './components/user-insights/user-insights';
import { UserSidebar } from './components/user-sidebar/user-sidebar';
Expand All @@ -37,6 +38,7 @@ const tabs = [
{ label: 'Activity', path: 'activity' },
{ label: 'Authored Insights', path: 'insights' },
{ label: 'Liked Insights', path: 'likes' },
{ label: 'Comments', path: 'comments' },
{ label: 'Drafts', path: 'drafts', selfOnly: true }
];

Expand Down Expand Up @@ -102,6 +104,9 @@ export const UserProfile = ({ user }: Props) => {
<TabPanel>
<UserInsights user={user} insightConnection={user.likedInsights} />
</TabPanel>
<TabPanel>
<UserComments user={user} />
</TabPanel>
{user.isSelf && (
<TabPanel px={0}>
<UserDrafts user={user} />
Expand Down
22 changes: 22 additions & 0 deletions packages/frontend/src/shared/useUser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,28 @@ const PROFILE_FRAGMENT = gql`
}
}
}
userComments(first: 50) {
pageInfo {
total
}
edges {
node {
commentText
createdAt
id
insight {
id
fullName
name
itemType
}
isEdited
isOwnComment
likeCount
updatedAt
}
}
}
}
`;

Expand Down