This is an automated email from the ASF dual-hosted git repository. linkinstar pushed a commit to branch dev in repository https://gitbox.apache.org/repos/asf/incubator-answer.git
commit 80bd8cd001c9382d809f5f18c55ccf38b7566631 Author: hgaol <[email protected]> AuthorDate: Tue May 7 10:57:44 2024 +0800 finish backend api and frontend --- internal/controller/meta_controller.go | 24 ++- internal/router/answer_api_router.go | 3 + internal/schema/meta_schema.go | 6 +- internal/service/meta/meta_service.go | 8 +- ui/src/components/Comment/index.tsx | 219 +++++++++++---------- .../Questions/Detail/components/Answer/index.tsx | 3 - .../Questions/Detail/components/Question/index.tsx | 3 - .../Detail/components/Reactions/index.tsx | 155 ++++++++++----- ui/src/pages/Questions/Detail/index.scss | 23 ++- ui/src/services/common.ts | 10 + 10 files changed, 282 insertions(+), 172 deletions(-) diff --git a/internal/controller/meta_controller.go b/internal/controller/meta_controller.go index de7d5ddc..fe598b90 100644 --- a/internal/controller/meta_controller.go +++ b/internal/controller/meta_controller.go @@ -46,11 +46,11 @@ func NewMetaController( // @Accept json // @Produce json // @Security ApiKeyAuth -// @Param data body schema.ReactionReq true "reaction" +// @Param data body schema.UpdateReactionReq true "reaction" // @Success 200 {object} handler.RespBody // @Router /answer/api/v1/meta/reaction [put] func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) { - req := &schema.ReactionReq{} + req := &schema.UpdateReactionReq{} if handler.BindAndCheck(ctx, req) { return } @@ -59,3 +59,23 @@ func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) { resp, err := mc.metaService.AddOrUpdateReaction(ctx, req) handler.HandleResponse(ctx, err, resp) } + +// GetReaction get reaction +// @Summary get reaction +// @Description get reaction for an object +// @Tags Meta +// @Accept json +// @Produce json +// @Security ApiKeyAuth +// @Param data body schema.UpdateReactionReq true "reaction" +// @Success 200 {object} handler.RespBody +// @Router /answer/api/v1/meta/reaction [put] +func (mc *MetaController) GetReaction(ctx *gin.Context) { + req := &schema.GetReactionReq{} + if handler.BindAndCheck(ctx, req) { + return + } + + resp, err := mc.metaService.GetReactionByObjectId(ctx, req.ObjectID) + handler.HandleResponse(ctx, err, resp) +} diff --git a/internal/router/answer_api_router.go b/internal/router/answer_api_router.go index a23ddfbf..90acc755 100644 --- a/internal/router/answer_api_router.go +++ b/internal/router/answer_api_router.go @@ -184,6 +184,9 @@ func (a *AnswerAPIRouter) RegisterUnAuthAnswerAPIRouter(r *gin.RouterGroup) { // rank r.GET("/personal/rank/page", a.rankController.GetRankPersonalWithPage) + + // reaction + r.GET("/meta/reaction", a.metaController.GetReaction) } func (a *AnswerAPIRouter) RegisterAuthUserWithAnyStatusAnswerAPIRouter(r *gin.RouterGroup) { diff --git a/internal/schema/meta_schema.go b/internal/schema/meta_schema.go index 55fc66e7..cf213813 100644 --- a/internal/schema/meta_schema.go +++ b/internal/schema/meta_schema.go @@ -19,13 +19,17 @@ package schema -type ReactionReq struct { +type UpdateReactionReq struct { ObjectID string `validate:"required" form:"object_id" json:"object_id"` // object id Emoji string `validate:"required" form:"emoji" json:"emoji"` // emoji Type string `validate:"required" form:"type" json:"type"` // type UserID string `json:"-"` } +type GetReactionReq struct { + ObjectID string `validate:"required" form:"object_id" json:"object_id"` // object id +} + type ReactSummaryMeta map[string][]string type ReactionResp ReactSummaryMeta diff --git a/internal/service/meta/meta_service.go b/internal/service/meta/meta_service.go index 24e852cb..cd7ca4f7 100644 --- a/internal/service/meta/meta_service.go +++ b/internal/service/meta/meta_service.go @@ -122,7 +122,7 @@ func (ms *MetaService) GetReactionByObjectId(ctx context.Context, objectID strin } // AddOrUpdateReaction add or update reaction -func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.ReactionReq) (resp schema.ReactionResp, err error) { +func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.UpdateReactionReq) (resp schema.ReactionResp, err error) { // get reaction for this object reactionMeta, err := ms.GetMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey) @@ -171,7 +171,7 @@ func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.Reac } // updateReaction update reaction -func (ms *MetaService) updateReaction(req *schema.ReactionReq, reaction schema.ReactSummaryMeta) { +func (ms *MetaService) updateReaction(req *schema.UpdateReactionReq, reaction schema.ReactSummaryMeta) { emojiUserIds, ok := reaction[req.Emoji] if !ok { @@ -215,14 +215,14 @@ func (ms *MetaService) updateReaction(req *schema.ReactionReq, reaction schema.R func (ms *MetaService) convertToReactionResp(ctx context.Context, reaction schema.ReactSummaryMeta) (schema.ReactionResp, error) { resp := schema.ReactionResp{} - // traverse map and convert to user name + // traverse map and convert to username for emoji, userIds := range reaction { userNames := make([]string, 0) userBasicInfos, err := ms.userCommon.BatchUserBasicInfoByID(ctx, userIds) if err != nil { return nil, err } - // get user name + // get username for _, userBasicInfo := range userBasicInfos { userNames = append(userNames, userBasicInfo.Username) } diff --git a/ui/src/components/Comment/index.tsx b/ui/src/components/Comment/index.tsx index 69c705b8..88d29de8 100644 --- a/ui/src/components/Comment/index.tsx +++ b/ui/src/components/Comment/index.tsx @@ -44,6 +44,7 @@ import { postVote, } from '@/services'; import { commentReplyStore } from '@/stores'; +import Reactions from '@/pages/Questions/Detail/components/Reactions'; import { Form, ActionBar, Reply } from './components'; @@ -361,118 +362,130 @@ const Comment = ({ objectId, mode, commentId }) => { }), ); }; + + const handleAddComment = () => { + if (tryNormalLogged(true)) { + setVisibleComment(!visibleComment); + } + }; + return ( - <div - className={classNames( - 'comments-wrap', - comments.length > 0 && 'bg-light px-3 py-2 rounded', - )}> - {comments.map((item) => { - return ( - <div - key={item.comment_id} - id={item.comment_id} - className="py-2 comment-item"> - {item.showEdit ? ( - <Form - className="mt-2" - value={item.original_text} - type="edit" - mode={mode} - onSendReply={(value) => - handleSendReply({ ...item, value, type: 'edit' }) - } - onCancel={() => handleCancel(item.comment_id)} - /> - ) : ( - <div className="d-block"> - {item.reply_user_display_name && ( - <Link to="." className="small me-1 text-nowrap"> - @{item.reply_user_display_name} - </Link> - )} - - <div - className="fmt small text-break text-wrap" - dangerouslySetInnerHTML={{ __html: item.parsed_text }} + <> + <Reactions + objectId={objectId} + showAddCommentBtn={comments.length === 0} + handleClickComment={handleAddComment} + /> + <div + className={classNames( + 'comments-wrap', + comments.length > 0 && 'bg-light px-3 py-2 rounded', + )}> + {comments.map((item) => { + return ( + <div + key={item.comment_id} + id={item.comment_id} + className="py-2 comment-item"> + {item.showEdit ? ( + <Form + className="mt-2" + value={item.original_text} + type="edit" + mode={mode} + onSendReply={(value) => + handleSendReply({ ...item, value, type: 'edit' }) + } + onCancel={() => handleCancel(item.comment_id)} /> - </div> - )} + ) : ( + <div className="d-block"> + {item.reply_user_display_name && ( + <Link to="." className="small me-1 text-nowrap"> + @{item.reply_user_display_name} + </Link> + )} + + <div + className="fmt small text-break text-wrap" + dangerouslySetInnerHTML={{ __html: item.parsed_text }} + /> + </div> + )} + + {currentReplyId === item.comment_id ? ( + <Reply + userName={item.user_display_name} + mode={mode} + onSendReply={(value) => + handleSendReply({ ...item, value, type: 'reply' }) + } + onCancel={() => handleCancel(item.comment_id)} + /> + ) : null} + {item.showEdit || currentReplyId === item.comment_id ? null : ( + <ActionBar + nickName={item.user_display_name} + username={item.username} + createdAt={item.created_at} + voteCount={item.vote_count} + isVote={item.is_vote} + memberActions={item.member_actions} + userStatus={item.user_status} + onReply={() => { + handleReply(item.comment_id); + }} + onAction={(action) => handleAction(action, item)} + onVote={(e) => { + e.preventDefault(); + handleVote(item.comment_id, item.is_vote); + }} + /> + )} + </div> + ); + })} - {currentReplyId === item.comment_id ? ( - <Reply - userName={item.user_display_name} - mode={mode} - onSendReply={(value) => - handleSendReply({ ...item, value, type: 'reply' }) - } - onCancel={() => handleCancel(item.comment_id)} - /> - ) : null} - {item.showEdit || currentReplyId === item.comment_id ? null : ( - <ActionBar - nickName={item.user_display_name} - username={item.username} - createdAt={item.created_at} - voteCount={item.vote_count} - isVote={item.is_vote} - memberActions={item.member_actions} - userStatus={item.user_status} - onReply={() => { - handleReply(item.comment_id); - }} - onAction={(action) => handleAction(action, item)} - onVote={(e) => { - e.preventDefault(); - handleVote(item.comment_id, item.is_vote); - }} - /> - )} - </div> - ); - })} - - <div className={classNames(comments.length > 0 && 'py-2')}> - <Button - variant="link" - className="p-0 btn-no-border" - size="sm" - onClick={() => { - if (tryNormalLogged(true)) { - setVisibleComment(!visibleComment); - } - }}> - {t('btn_add_comment')} - </Button> - {data && - (pageIndex || 1) < Math.ceil((data?.count || 0) / pageSize) && ( + <div className={classNames(comments.length > 0 && 'py-2')}> + {comments.length > 0 && ( <Button variant="link" + className="p-0 btn-no-border" size="sm" - className="p-0 ms-3 btn-no-border" - onClick={() => { - setPageIndex(pageIndex + 1); - }}> - {t('show_more', { - count: - data.count - (pageIndex === 0 ? 3 : pageIndex * pageSize), - })} + onClick={handleAddComment}> + {t('btn_add_comment')} </Button> )} + {data && + (pageIndex || 1) < Math.ceil((data?.count || 0) / pageSize) && ( + <Button + variant="link" + size="sm" + className="p-0 ms-3 btn-no-border" + onClick={() => { + setPageIndex(pageIndex + 1); + }}> + {t('show_more', { + count: + data.count - (pageIndex === 0 ? 3 : pageIndex * pageSize), + })} + </Button> + )} + </div> + + {visibleComment && ( + <Form + mode={mode} + className={classNames( + 'mt-2', + comments.length <= 0 && 'bg-light p-3 rounded', + )} + onSendReply={(value) => handleSendReply({ value, type: 'comment' })} + onCancel={() => setVisibleComment(!visibleComment)} + /> + )} </div> - - {visibleComment && ( - <Form - mode={mode} - className={classNames( - 'mt-2', - comments.length <= 0 && 'bg-light p-3 rounded', - )} - onSendReply={(value) => handleSendReply({ value, type: 'comment' })} - onCancel={() => setVisibleComment(!visibleComment)} - /> - )} - </div> + </> ); }; diff --git a/ui/src/pages/Questions/Detail/components/Answer/index.tsx b/ui/src/pages/Questions/Detail/components/Answer/index.tsx index 6b382dba..474c83d6 100644 --- a/ui/src/pages/Questions/Detail/components/Answer/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Answer/index.tsx @@ -36,7 +36,6 @@ import { scrollToElementTop, bgFadeOut } from '@/utils'; import { AnswerItem } from '@/common/interface'; import { acceptanceAnswer } from '@/services'; import { useRenderHtmlPlugin } from '@/utils/pluginKit'; -import Reactions from '../Reactions'; interface Props { data: AnswerItem; @@ -199,8 +198,6 @@ const Index: FC<Props> = ({ </div> </div> - <Reactions /> - <Comment objectId={data.id} mode="answer" diff --git a/ui/src/pages/Questions/Detail/components/Question/index.tsx b/ui/src/pages/Questions/Detail/components/Question/index.tsx index 1252098a..0a76c2f9 100644 --- a/ui/src/pages/Questions/Detail/components/Question/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Question/index.tsx @@ -37,7 +37,6 @@ import { useRenderHtmlPlugin } from '@/utils/pluginKit'; import { formatCount, guard } from '@/utils'; import { following } from '@/services'; import { pathFactory } from '@/router/pathFactory'; -import Reactions from '../Reactions'; interface Props { data: any; @@ -213,8 +212,6 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => { </div> </div> - <Reactions /> - <Comment objectId={data?.id} mode="question" diff --git a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx index 6236c684..5668682a 100644 --- a/ui/src/pages/Questions/Detail/components/Reactions/index.tsx +++ b/ui/src/pages/Questions/Detail/components/Reactions/index.tsx @@ -1,32 +1,85 @@ -import { FC, memo } from 'react'; +import { FC, memo, useEffect, useState } from 'react'; import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap'; +import { useTranslation } from 'react-i18next'; import { Icon } from '@/components'; +import { queryReactions, updateReaction } from '@/services'; +import { tryNormalLogged } from '@/utils/guard'; -interface Props {} - -interface Data { - title: string; - icon: string; - className: string; +interface Props { + objectId: string; + showAddCommentBtn?: boolean; + handleClickComment: () => void; } -const data: Data[] = [ - { title: 'hello', icon: 'heart-fill', className: 'text-danger' }, - { title: 'hello', icon: 'emoji-laughing-fill', className: 'text-warning' }, - { title: 'hello', icon: 'emoji-frown-fill', className: 'text-warning' }, +const emojiMap = [ + { + name: 'heart', + icon: 'heart-fill', + className: 'text-danger', + }, + { + name: 'smile', + icon: 'emoji-laughing-fill', + className: 'text-warning', + }, + { + name: 'frown', + icon: 'emoji-frown-fill', + className: 'text-warning', + }, ]; -const Index: FC<Props> = () => { +const Index: FC<Props> = ({ + objectId, + showAddCommentBtn, + handleClickComment, +}) => { + const [reactions, setReactions] = useState<Record<string, string[]>>(); + const { t } = useTranslation('translation', { keyPrefix: 'comment' }); + + useEffect(() => { + queryReactions(objectId).then((res) => { + setReactions(res); + }); + }, []); + + const handleSubmit = (params: { object_id: string; emoji: string }) => { + if (!tryNormalLogged(true)) { + return; + } + updateReaction({ ...params, type: 'toggle' }).then((res) => { + setReactions(res); + }); + }; + + const convertToTooltip = (names: string[]) => { + const n: number = Math.min(5, names.length); + let ret = ''; + for (let i = 0; i < n; i += 1) { + if (i === n - 1) { + ret += names[i]; + } else { + ret += `${names[i]}, `; + } + } + if (names.length > 5) { + ret += ` and ${names.length - 5} more...`; + } + return ret; + }; + const renderTooltip = (props) => ( <Tooltip id="reaction-button-tooltip" {...props} bsPrefix="tooltip"> <div className="d-block d-md-flex flex-wrap m-0 p-0"> - {data.map((d) => ( + {emojiMap.map((d) => ( <Button key={d.icon} variant="light" size="sm" - onClick={() => alert('hellob')}> + onClick={() => + handleSubmit({ object_id: objectId, emoji: d.name }) + }> <Icon name={d.icon} className={d.className} /> </Button> ))} @@ -36,49 +89,55 @@ const Index: FC<Props> = () => { return ( <div className="d-block d-md-flex flex-wrap mt-4 mb-3"> - <Button - variant="outline-secondary" - className="rounded-pill answer-reaction-btn" - size="sm"> - <Icon name="chat-text-fill" /> - <span className="ms-1">{6} comments</span> - </Button> + {showAddCommentBtn && ( + <Button + className="rounded-pill btn-no-border answer-reaction-btn bg-light" + size="sm" + onClick={handleClickComment}> + <Icon name="chat-text-fill" /> + <span className="ms-1">{t('btn_add_comment')}</span> + </Button> + )} <OverlayTrigger trigger="click" placement="top" overlay={renderTooltip}> <Button - variant="outline-secondary" size="sm" - className="rounded-pill ms-2 answer-reaction-btn"> + className="rounded-pill ms-2 answer-reaction-btn bg-light btn-no-border"> <Icon name="emoji-smile-fill" /> <span className="ms-1">+</span> </Button> </OverlayTrigger> - {/* <div className="arrow" /> */} - - {data.map((d) => ( - <OverlayTrigger - placement="top" - // trigger="hover" - trigger="click" - overlay={ - <Tooltip> - <div className="text-start"> - <b>heart</b> <br /> fenbox, joyqi, robin, andrus, jackathon and - 7 more... - </div> - </Tooltip> - }> - <Button - title="hahah" - variant="outline-secondary" - className="rounded-pill ms-2 answer-reaction-btn" - size="sm"> - <Icon name={d.icon} className={d.className} /> - <span className="ms-1">{3}</span> - </Button> - </OverlayTrigger> - ))} + {reactions && + emojiMap.map((emoji) => { + if (!reactions[emoji.name] || reactions[emoji.name].length === 0) { + return null; + } + return ( + <OverlayTrigger + key={emoji.name} + placement="top" + overlay={ + <Tooltip> + <div className="text-start"> + <b>{emoji.name}</b> <br />{' '} + {convertToTooltip(reactions[emoji.name])} + </div> + </Tooltip> + }> + <Button + title="hahah" + className="rounded-pill ms-2 answer-reaction-btn bg-light btn-no-border" + size="sm" + onClick={() => + handleSubmit({ object_id: objectId, emoji: emoji.name }) + }> + <Icon name={emoji.icon} className={emoji.className} /> + <span className="ms-1">{reactions[emoji.name].length}</span> + </Button> + </OverlayTrigger> + ); + })} </div> ); }; diff --git a/ui/src/pages/Questions/Detail/index.scss b/ui/src/pages/Questions/Detail/index.scss index cf5e910d..72cb02e8 100644 --- a/ui/src/pages/Questions/Detail/index.scss +++ b/ui/src/pages/Questions/Detail/index.scss @@ -22,19 +22,30 @@ .answer-reaction-btn { padding-top: 0.2rem; padding-bottom: 0.2rem; - border: 1px solid $gray-300; + color: $secondary; + &:hover { + background-color: $gray-400 !important; + color: $secondary; + } + &:active { + background-color: $gray-500 !important; + color: $secondary !important; + } } #reaction-button-tooltip { .tooltip-inner { padding: 0.2rem; background-color: $white; - border: 1px solid $gray-300; + border: 1px solid $gray-500; .btn { background-color: $white; border: none; &:hover { - background-color: $gray-400; + background-color: $gray-100; + } + &:active { + background-color: $gray-300; } } } @@ -42,15 +53,11 @@ .tooltip-arrow { overflow: hidden; bottom: calc(-1 * $tooltip-arrow-height + 1px); - // width: calc($tooltip-arrow-width + 4px); - // height: calc($tooltip-arrow-height + 2px); - // box-sizing: content-box; &::before { - border: 1px solid $gray-300; + border: 1px solid $gray-500; width: 50%; height: 100%; transform: translate(-50%, -50%) rotate(-45deg); - // transform: rotate(-45deg); box-sizing: content-box; left: 50%; bottom: 50%; diff --git a/ui/src/services/common.ts b/ui/src/services/common.ts index 0676cc71..a783fa49 100644 --- a/ui/src/services/common.ts +++ b/ui/src/services/common.ts @@ -88,6 +88,16 @@ export const addComment = (params) => { return request.post('/answer/api/v1/comment', params); }; +export const updateReaction = (params) => { + return request.put('/answer/api/v1/meta/reaction', params); +}; + +export const queryReactions = (object_id: string) => { + return request.get<Record<string, string[]>>( + `/answer/api/v1/meta/reaction?object_id=${object_id}`, + ); +}; + export const queryTags = (tag: string) => { return request.get( `/answer/api/v1/question/tags?tag=${encodeURIComponent(tag)}`,
