diff --git a/src/controls/listItemComments/common/ECommentAction.ts b/src/controls/listItemComments/common/ECommentAction.ts index 1fd9bde23..50ea34b2e 100644 --- a/src/controls/listItemComments/common/ECommentAction.ts +++ b/src/controls/listItemComments/common/ECommentAction.ts @@ -1,4 +1,6 @@ export enum ECommentAction { - "ADD" = "ADD", - "DELETE" = "DELETE" + 'ADD' = 'ADD', + 'DELETE' = 'DELETE', + 'LIKE' = 'LIKE', + 'UNLIKE' = 'UNLIKE', } diff --git a/src/controls/listItemComments/components/Comments/CommentsList.tsx b/src/controls/listItemComments/components/Comments/CommentsList.tsx index 89bab65cd..1952b7c0c 100644 --- a/src/controls/listItemComments/components/Comments/CommentsList.tsx +++ b/src/controls/listItemComments/components/Comments/CommentsList.tsx @@ -20,8 +20,22 @@ import { RenderComments } from "./RenderComments"; export const CommentsList: React.FunctionComponent = () => { const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); const { configurationListClasses } = useListItemCommentsStyles(); - const { getListItemComments, getNextPageOfComments, addComment, deleteComment } = useSpAPI(); - const { comments, isScrolling, pageInfo, commentAction, commentToAdd, selectedComment } = listItemCommentsState; + const { + getListItemComments, + getNextPageOfComments, + addComment, + deleteComment, + likeComment, + unlikeComment, + } = useSpAPI(); + const { + comments, + isScrolling, + pageInfo, + commentAction, + commentToAdd, + selectedComment, + } = listItemCommentsState; const { hasMore, nextLink } = pageInfo; const scrollPanelRef = useRef(); const { errorInfo } = listItemCommentsState; @@ -32,16 +46,23 @@ export const CommentsList: React.FunctionComponent = () => { type: EListItemCommentsStateTypes.SET_IS_LOADING, payload: true, }); - const _commentsResults: IlistItemCommentsResults = await getListItemComments(); + const _commentsResults: IlistItemCommentsResults = + await getListItemComments(); setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_LIST_ITEM_COMMENTS, payload: _commentsResults.comments, }); setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_DATA_PAGE_INFO, - payload: { hasMore: _commentsResults.hasMore, nextLink: _commentsResults.nextLink } as IPageInfo, + payload: { + hasMore: _commentsResults.hasMore, + nextLink: _commentsResults.nextLink, + } as IPageInfo, + }); + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, + payload: undefined, }); - setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, payload: undefined }); setlistItemCommentsState({ type: EListItemCommentsStateTypes.SET_IS_LOADING, payload: false, @@ -99,25 +120,110 @@ export const CommentsList: React.FunctionComponent = () => { [setlistItemCommentsState, _loadComments] ); + const _onCommentLike = useCallback( + async (commentId: number) => { + try { + const _errorInfo: IErrorInfo = { showError: false, error: undefined }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + await likeComment(commentId); + await _loadComments(); + } catch (error) { + const _errorInfo: IErrorInfo = { showError: true, error: error }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + } + }, + [setlistItemCommentsState, _loadComments] + ); + const _onCommentUnlike = useCallback( + async (commentId: number) => { + try { + const _errorInfo: IErrorInfo = { showError: false, error: undefined }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + await unlikeComment(commentId); + await _loadComments(); + } catch (error) { + const _errorInfo: IErrorInfo = { showError: true, error: error }; + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_ERROR_INFO, + payload: _errorInfo, + }); + } + }, + [setlistItemCommentsState, _loadComments] + ); + useEffect(() => { switch (commentAction) { case ECommentAction.ADD: (async () => { // Add new comment await _onAddComment(commentToAdd); - })().then(() => { /* no-op; */}).catch(() => { /* no-op; */ }); + })() + .then(() => { + /* no-op; */ + }) + .catch(() => { + /* no-op; */ + }); + break; + case ECommentAction.LIKE: + (async () => { + // Add new comment + const commentId = Number(selectedComment.id); + await _onCommentLike(commentId); + })() + .then(() => { + /* no-op; */ + }) + .catch(() => { + /* no-op; */ + }); + break; + case ECommentAction.UNLIKE: + (async () => { + // Add new comment + const commentId = Number(selectedComment.id); + await _onCommentUnlike(commentId); + })() + .then(() => { + /* no-op; */ + }) + .catch(() => { + /* no-op; */ + }); break; case ECommentAction.DELETE: (async () => { // delete comment const commentId = Number(selectedComment.id); await _onADeleteComment(commentId); - })().then(() => { /* no-op; */}).catch(() => { /* no-op; */ }); + })() + .then(() => { + /* no-op; */ + }) + .catch(() => { + /* no-op; */ + }); break; default: break; } - }, [commentAction, selectedComment, commentToAdd, _onAddComment, _onADeleteComment]); + }, [ + commentAction, + selectedComment, + commentToAdd, + _onAddComment, + _onADeleteComment, + ]); useEffect(() => { (async () => { diff --git a/src/controls/listItemComments/components/Comments/RenderComments.tsx b/src/controls/listItemComments/components/Comments/RenderComments.tsx index 1c3bc3293..a9a1cfc22 100644 --- a/src/controls/listItemComments/components/Comments/RenderComments.tsx +++ b/src/controls/listItemComments/components/Comments/RenderComments.tsx @@ -1,36 +1,92 @@ -import { IconButton } from "@fluentui/react/lib/Button"; -import { DocumentCard, DocumentCardDetails } from "@fluentui/react/lib/DocumentCard"; -import { Stack } from "@fluentui/react/lib/Stack"; -import * as React from "react"; -import { useCallback } from "react"; -import { useContext } from "react"; -import { ConfirmDelete } from "../ConfirmDelete/ConfirmDelete"; -import { EListItemCommentsStateTypes, ListItemCommentsStateContext } from "../ListItemCommentsStateProvider"; -import { CommentItem } from "./CommentItem"; -import { IComment } from "./IComment"; -import { RenderSpinner } from "./RenderSpinner"; -import { useListItemCommentsStyles } from "./useListItemCommentsStyles"; -import { useBoolean } from "@fluentui/react-hooks"; -import { List } from "@fluentui/react/lib/List"; -import { AppContext, ECommentAction } from "../.."; +import { IconButton } from '@fluentui/react/lib/Button'; +import { + DocumentCard, + DocumentCardDetails, +} from '@fluentui/react/lib/DocumentCard'; +import { Stack } from '@fluentui/react/lib/Stack'; +import * as React from 'react'; +import { useCallback } from 'react'; +import { useContext } from 'react'; +import { ConfirmDelete } from '../ConfirmDelete/ConfirmDelete'; +import { + EListItemCommentsStateTypes, + ListItemCommentsStateContext, +} from '../ListItemCommentsStateProvider'; +import { CommentItem } from './CommentItem'; +import { IComment } from './IComment'; +import { RenderSpinner } from './RenderSpinner'; +import { useListItemCommentsStyles } from './useListItemCommentsStyles'; +import { useBoolean } from '@fluentui/react-hooks'; +import { List, Text } from '@fluentui/react'; +import { AppContext, ECommentAction } from '../..'; -export interface IRenderCommentsProps { } +export interface IRenderCommentsProps {} -export const RenderComments: React.FunctionComponent = () => { +export const RenderComments: React.FunctionComponent< + IRenderCommentsProps +> = () => { const { highlightedCommentId } = useContext(AppContext); - const { listItemCommentsState, setlistItemCommentsState } = useContext(ListItemCommentsStateContext); - const { documentCardStyles,documentCardHighlightedStyles, itemContainerStyles, deleteButtonContainerStyles } = useListItemCommentsStyles(); + const { listItemCommentsState, setlistItemCommentsState } = useContext( + ListItemCommentsStateContext + ); + const { + documentCardStyles, + documentCardHighlightedStyles, + itemContainerStyles, + ButtonsContainerStyles, + } = useListItemCommentsStyles(); const { comments, isLoading } = listItemCommentsState; const [hideDialog, { toggle: setHideDialog }] = useBoolean(true); + const _likeComment = useCallback(() => { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, + payload: ECommentAction.LIKE, + }); + }, []); + + const _unLikeComment = useCallback(() => { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_COMMENT_ACTION, + payload: ECommentAction.UNLIKE, + }); + }, []); + const onRenderCell = useCallback( (comment: IComment, index: number): JSX.Element => { return ( - - + + +
+ {comment.likeCount} + { + setlistItemCommentsState({ + type: EListItemCommentsStateTypes.SET_SELECTED_COMMENT, + payload: comment, + }); + !comment.isLikedByUser ? _likeComment() : _unLikeComment(); + }} + /> +
{ setlistItemCommentsState({ @@ -57,10 +113,13 @@ export const RenderComments: React.FunctionComponent = () }, [comments] ); - return ( <> - {isLoading ? : } + {isLoading ? ( + + ) : ( + + )} { diff --git a/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts b/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts index beefed387..372fb041a 100644 --- a/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts +++ b/src/controls/listItemComments/components/Comments/useListItemCommentsStyles.ts @@ -11,7 +11,7 @@ import { TILE_HEIGHT } from "../../common/constants"; interface returnObjectStyles { itemContainerStyles: IStackStyles; - deleteButtonContainerStyles: Partial; + ButtonsContainerStyles: Partial; userListContainerStyles: Partial; renderUserContainerStyles: Partial; documentCardStyles: Partial; @@ -29,12 +29,17 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { : 7 * TILE_HEIGHT; const itemContainerStyles: IStackStyles = { - root: { paddingTop: 0, paddingLeft: 20, paddingRight: 20, paddingBottom: 20 } as IStyle, + root: { + paddingTop: 0, + paddingLeft: 20, + paddingRight: 20, + paddingBottom: 20, + } as IStyle, }; - const deleteButtonContainerStyles: Partial = { + const ButtonsContainerStyles: Partial = { root: { - position: "absolute", + position: 'absolute', top: 0, right: 0, }, @@ -45,15 +50,20 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { }; const renderUserContainerStyles: Partial = { - root: { paddingTop: 5, paddingBottom: 5, paddingLeft: 10, paddingRight: 10 }, + root: { + paddingTop: 5, + paddingBottom: 5, + paddingLeft: 10, + paddingRight: 10, + }, }; const documentCardStyles: Partial = { root: { marginBottom: 7, width: 322, backgroundColor: theme.neutralLighterAlt, - userSelect: "text", - ":hover": { + userSelect: 'text', + ':hover': { borderColor: theme.themePrimary, borderWidth: 1, } as IStyle, @@ -65,9 +75,9 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { marginBottom: 7, width: 322, backgroundColor: theme.themeLighter, - userSelect: "text", - border: "solid 3px "+theme.themePrimary, - ":hover": { + userSelect: 'text', + border: 'solid 3px ' + theme.themePrimary, + ':hover': { borderColor: theme.themePrimary, borderWidth: 1, } as IStyle, @@ -78,7 +88,7 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { root: { marginBottom: 5, backgroundColor: theme.neutralLighterAlt, - ":hover": { + ':hover': { borderColor: theme.themePrimary, borderWidth: 1, } as IStyle, @@ -89,9 +99,9 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { root: { marginTop: 2, backgroundColor: theme?.white, - boxShadow: "0 5px 15px rgba(50, 50, 90, .1)", + boxShadow: '0 5px 15px rgba(50, 50, 90, .1)', - ":hover": { + ':hover': { borderColor: theme.themePrimary, backgroundColor: theme.neutralLighterAlt, borderWidth: 1, @@ -113,18 +123,18 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { color: theme.themePrimary, }), divContainer: { - display: "block", + display: 'block', } as IStyle, titlesContainer: { height: tilesHeight, marginBottom: 10, - display: "flex", + display: 'flex', marginTop: 15, - overflow: "auto", - "&::-webkit-scrollbar-thumb": { + overflow: 'auto', + '&::-webkit-scrollbar-thumb': { backgroundColor: theme.neutralLighter, }, - "&::-webkit-scrollbar": { + '&::-webkit-scrollbar': { width: 5, }, } as IStyle, @@ -132,7 +142,7 @@ export const useListItemCommentsStyles = (): returnObjectStyles => { return { itemContainerStyles, - deleteButtonContainerStyles, + ButtonsContainerStyles, userListContainerStyles, renderUserContainerStyles, documentCardStyles, diff --git a/src/controls/listItemComments/hooks/useSpAPI.ts b/src/controls/listItemComments/hooks/useSpAPI.ts index f848068a1..9e0cdf6f9 100644 --- a/src/controls/listItemComments/hooks/useSpAPI.ts +++ b/src/controls/listItemComments/hooks/useSpAPI.ts @@ -7,14 +7,20 @@ import { IComment } from "../components/Comments/IComment"; import { PageContext } from "@microsoft/sp-page-context"; interface returnObject { getListItemComments: () => Promise; - getNextPageOfComments: (nextLink: string) => Promise; + getNextPageOfComments: ( + nextLink: string + ) => Promise; addComment: (comment: IAddCommentPayload) => Promise; deleteComment: (commentId: number) => Promise; + likeComment: (commentId: number) => Promise; + unlikeComment: (commentId: number) => Promise; } export const useSpAPI = (): returnObject => { - const { serviceScope, webUrl, listId, itemId, numberCommentsPerPage } = useContext(AppContext); - let _webUrl: string = ""; + const { serviceScope, webUrl, listId, itemId, numberCommentsPerPage } = + useContext(AppContext); + let _webUrl: string = ''; + console.log('web', webUrl); serviceScope.whenFinished(async () => { _webUrl = serviceScope.consume(PageContext.serviceKey).web.absoluteUrl; }); @@ -28,7 +34,7 @@ export const useSpAPI = (): returnObject => { webUrl ?? _webUrl }/_api/web/lists(@a1)/GetItemById(@a2)/Comments(@a3)?@a1='${listId}'&@a2='${itemId}'&@a3='${commentId}'`; const spOpts: ISPHttpClientOptions = { - method: "DELETE", + method: 'DELETE', }; await spHttpClient.fetch( `${_endPointUrl}`, @@ -48,7 +54,9 @@ export const useSpAPI = (): returnObject => { webUrl ?? _webUrl }/_api/web/lists(@a1)/GetItemById(@a2)/Comments()?@a1='${listId}'&@a2='${itemId}'`; const spOpts: ISPHttpClientOptions = { - body: `{ "text": "${comment.text}", "mentions": ${JSON.stringify(comment.mentions)}}`, + body: `{ "text": "${comment.text}", "mentions": ${JSON.stringify( + comment.mentions + )}}`, }; const _listResults: SPHttpClientResponse = await spHttpClient.post( `${_endPointUrl}`, @@ -61,26 +69,71 @@ export const useSpAPI = (): returnObject => { [serviceScope] ); - const getListItemComments = useCallback(async (): Promise => { - const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); - if (!spHttpClient) return; - const _endPointUrl = `${ - webUrl ?? _webUrl - }/_api/web/lists(@a1)/GetItemById(@a2)/GetComments()?@a1='${listId}'&@a2='${itemId}'&$top=${ - numberCommentsPerPage ?? 10 - }`; - const _listResults: SPHttpClientResponse = await spHttpClient.get( - `${_endPointUrl}`, - SPHttpClient.configurations.v1 - ); - const _commentsResults = (await _listResults.json()) as any; // eslint-disable-line @typescript-eslint/no-explicit-any - const _returnComments: IlistItemCommentsResults = { - comments: _commentsResults.value, - hasMore: _commentsResults["@odata.nextLink"] ? true : false, - nextLink: _commentsResults["@odata.nextLink"] ?? undefined, - }; - return _returnComments; - }, [serviceScope]); + const likeComment = useCallback( + async (commentId: number): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/Comments(@a3)/like?@a1='${listId}'&@a2='${itemId}'&@a3='${commentId}'`; + + const spOpts: ISPHttpClientOptions = { + headers: { + Accept: 'application/json;odata=nometadata', + }, + }; + await spHttpClient.post( + `${_endPointUrl}`, + SPHttpClient.configurations.v1, + spOpts + ); + }, + [serviceScope, webUrl, _webUrl, listId, itemId] + ); + + const unlikeComment = useCallback( + async (commentId: number): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/Comments(@a3)/unlike?@a1='${listId}'&@a2='${itemId}'&@a3='${commentId}'`; + + const spOpts: ISPHttpClientOptions = { + headers: { + Accept: 'application/json;odata=nometadata', + }, + }; + await spHttpClient.post( + `${_endPointUrl}`, + SPHttpClient.configurations.v1, + spOpts + ); + }, + [serviceScope, webUrl, _webUrl, listId, itemId] + ); + + const getListItemComments = + useCallback(async (): Promise => { + const spHttpClient = serviceScope.consume(SPHttpClient.serviceKey); + if (!spHttpClient) return; + const _endPointUrl = `${ + webUrl ?? _webUrl + }/_api/web/lists(@a1)/GetItemById(@a2)/GetComments()?@a1='${listId}'&@a2='${itemId}'&$top=${ + numberCommentsPerPage ?? 10 + }`; + const _listResults: SPHttpClientResponse = await spHttpClient.get( + `${_endPointUrl}`, + SPHttpClient.configurations.v1 + ); + const _commentsResults = (await _listResults.json()) as any; // eslint-disable-line @typescript-eslint/no-explicit-any + const _returnComments: IlistItemCommentsResults = { + comments: _commentsResults.value, + hasMore: _commentsResults['@odata.nextLink'] ? true : false, + nextLink: _commentsResults['@odata.nextLink'] ?? undefined, + }; + return _returnComments; + }, [serviceScope]); const getNextPageOfComments = useCallback( async (nextLink: string): Promise => { @@ -94,13 +147,20 @@ export const useSpAPI = (): returnObject => { const _commentsResults = (await _listResults.json()) as any; // eslint-disable-line @typescript-eslint/no-explicit-any const _returnComments: IlistItemCommentsResults = { comments: _commentsResults.value, - hasMore: _commentsResults["@odata.nextLink"] ? true : false, - nextLink: _commentsResults["@odata.nextLink"] ?? undefined, + hasMore: _commentsResults['@odata.nextLink'] ? true : false, + nextLink: _commentsResults['@odata.nextLink'] ?? undefined, }; return _returnComments; }, [serviceScope] ); - return { getListItemComments, getNextPageOfComments, addComment, deleteComment }; + return { + getListItemComments, + getNextPageOfComments, + addComment, + deleteComment, + likeComment, + unlikeComment, + }; };