This commit is contained in:
hgaol
2024-05-02 18:18:16 +08:00
committed by LinkinStars
parent ad951c8c49
commit 283aa092fe
13 changed files with 385 additions and 33 deletions
+5 -24
View File
@@ -1,28 +1,8 @@
//go:build !wireinject
// +build !wireinject
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package answercmd
@@ -169,7 +149,7 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
collectionCommon := collectioncommon.NewCollectionCommon(collectionRepo)
answerCommon := answercommon.NewAnswerCommon(answerRepo)
metaRepo := meta.NewMetaRepo(dataData)
metaService := meta2.NewMetaService(metaRepo)
metaService := meta2.NewMetaService(metaRepo, userCommon)
questionCommon := questioncommon.NewQuestionCommon(questionRepo, answerRepo, voteRepo, followRepo, tagCommonService, userCommon, collectionCommon, answerCommon, metaService, configService, activityQueueService, revisionRepo, dataData)
userService := content.NewUserService(userRepo, userActiveActivityRepo, activityRepo, emailService, authService, siteInfoCommonService, userRoleRelService, userCommon, userExternalLoginService, userNotificationConfigRepo, userNotificationConfigService, questionCommon)
captchaRepo := captcha.NewCaptchaRepo(dataData)
@@ -250,7 +230,8 @@ func initApplication(debug bool, serverConf *conf.Server, dbConf *data.Database,
permissionController := controller.NewPermissionController(rankService)
userPluginController := controller.NewUserPluginController(pluginCommonService)
reviewController := controller.NewReviewController(reviewService, rankService, captchaService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController)
metaController := controller.NewMetaController(metaService)
answerAPIRouter := router.NewAnswerAPIRouter(langController, userController, commentController, reportController, voteController, tagController, followController, collectionController, questionController, answerController, searchController, revisionController, rankController, userAdminController, reasonController, themeController, siteInfoController, controllerSiteInfoController, notificationController, dashboardController, uploadController, activityController, roleController, pluginController, permissionController, userPluginController, reviewController, metaController)
swaggerRouter := router.NewSwaggerRouter(swaggerConf)
uiRouter := router.NewUIRouter(controllerSiteInfoController, siteInfoCommonService)
authUserMiddleware := middleware.NewAuthUserMiddleware(authService, siteInfoCommonService)
+1
View File
@@ -103,6 +103,7 @@ const (
AddBulkUsersFormatError = "error.user.add_bulk_users_format_error"
AddBulkUsersAmountError = "error.user.add_bulk_users_amount_error"
InvalidURLError = "error.common.invalid_url"
MetaObjectNotFound = "error.meta.object_not_found"
)
// user external login reasons
+1
View File
@@ -49,4 +49,5 @@ var ProviderSetController = wire.NewSet(
NewUserPluginController,
NewReviewController,
NewCaptchaController,
NewMetaController,
)
+61
View File
@@ -0,0 +1,61 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package controller
import (
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/base/middleware"
"github.com/apache/incubator-answer/internal/schema"
"github.com/apache/incubator-answer/internal/service/meta"
"github.com/gin-gonic/gin"
)
type MetaController struct {
metaService *meta.MetaService
}
func NewMetaController(
metaService *meta.MetaService,
) *MetaController {
return &MetaController{
metaService: metaService,
}
}
// AddOrUpdateReaction add or update reaction
// @Summary add or update reaction
// @Description update reaction. if not exist, add one
// @Tags Meta
// @Accept json
// @Produce json
// @Security ApiKeyAuth
// @Param data body schema.ReactionReq true "reaction"
// @Success 200 {object} handler.RespBody
// @Router /answer/api/v1/meta/reaction [put]
func (mc *MetaController) AddOrUpdateReaction(ctx *gin.Context) {
req := &schema.ReactionReq{}
if handler.BindAndCheck(ctx, req) {
return
}
req.UserID = middleware.GetLoginUserIDFromContext(ctx)
resp, err := mc.metaService.AddOrUpdateReaction(ctx, req)
handler.HandleResponse(ctx, err, resp)
}
+1
View File
@@ -26,6 +26,7 @@ const (
QuestionCloseReasonKey = "question.close.reason"
AnswerEditSummaryKey = "answer.edit.summary"
TagEditSummaryKey = "tag.edit.summary"
ObjectReactSummaryKey = "object.react.summary"
)
// Meta meta
+6
View File
@@ -54,6 +54,7 @@ type AnswerAPIRouter struct {
permissionController *controller.PermissionController
userPluginController *controller.UserPluginController
reviewController *controller.ReviewController
metaController *controller.MetaController
}
func NewAnswerAPIRouter(
@@ -84,6 +85,7 @@ func NewAnswerAPIRouter(
permissionController *controller.PermissionController,
userPluginController *controller.UserPluginController,
reviewController *controller.ReviewController,
metaController *controller.MetaController,
) *AnswerAPIRouter {
return &AnswerAPIRouter{
langController: langController,
@@ -113,6 +115,7 @@ func NewAnswerAPIRouter(
permissionController: permissionController,
userPluginController: userPluginController,
reviewController: reviewController,
metaController: metaController,
}
}
@@ -285,6 +288,9 @@ func (a *AnswerAPIRouter) RegisterAnswerAPIRouter(r *gin.RouterGroup) {
r.GET("/user/plugin/configs", a.userPluginController.GetUserPluginList)
r.GET("/user/plugin/config", a.userPluginController.GetUserPluginConfig)
r.PUT("/user/plugin/config", a.userPluginController.UpdatePluginUserConfig)
// meta
r.PUT("/meta/reaction", a.metaController.AddOrUpdateReaction)
}
func (a *AnswerAPIRouter) RegisterAnswerAdminAPIRouter(r *gin.RouterGroup) {
+31
View File
@@ -0,0 +1,31 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you 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.
*/
package schema
type ReactionReq 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 ReactSummaryMeta map[string][]string
type ReactionResp ReactSummaryMeta
+2 -1
View File
@@ -23,9 +23,10 @@ import (
"context"
"encoding/json"
"fmt"
"github.com/apache/incubator-answer/internal/service/activity_common"
"strings"
"github.com/apache/incubator-answer/internal/service/activity_common"
"github.com/apache/incubator-answer/internal/base/constant"
"github.com/apache/incubator-answer/internal/base/handler"
"github.com/apache/incubator-answer/internal/entity"
+144 -5
View File
@@ -21,10 +21,14 @@ package meta
import (
"context"
"encoding/json"
"errors"
"github.com/apache/incubator-answer/internal/base/reason"
"github.com/apache/incubator-answer/internal/entity"
"github.com/segmentfault/pacman/errors"
"github.com/apache/incubator-answer/internal/schema"
usercommon "github.com/apache/incubator-answer/internal/service/user_common"
myErrors "github.com/segmentfault/pacman/errors"
)
// MetaRepo meta repository
@@ -38,12 +42,14 @@ type MetaRepo interface {
// MetaService user service
type MetaService struct {
metaRepo MetaRepo
metaRepo MetaRepo
userCommon *usercommon.UserCommon
}
func NewMetaService(metaRepo MetaRepo) *MetaService {
func NewMetaService(metaRepo MetaRepo, userCommon *usercommon.UserCommon) *MetaService {
return &MetaService{
metaRepo: metaRepo,
metaRepo: metaRepo,
userCommon: userCommon,
}
}
@@ -79,7 +85,7 @@ func (ms *MetaService) GetMetaByObjectIdAndKey(ctx context.Context, objectID, ke
return
}
if !exist {
return nil, errors.BadRequest(reason.UnknownError)
return nil, myErrors.BadRequest(reason.MetaObjectNotFound)
}
return meta, nil
}
@@ -92,3 +98,136 @@ func (ms *MetaService) GetMetaList(ctx context.Context, objID string) (metas []*
}
return metas, err
}
// GetReactionByObjectId get reaction
func (ms *MetaService) GetReactionByObjectId(ctx context.Context, objectID string) (resp schema.ReactionResp, err error) {
reactionMeta, err := ms.GetMetaByObjectIdAndKey(ctx, objectID, entity.ObjectReactSummaryKey)
// if not exist, return nil
if err != nil {
var pacmanErr *myErrors.Error
if errors.As(err, &pacmanErr) && pacmanErr.Reason == reason.MetaObjectNotFound {
return nil, nil
} else {
return nil, err
}
}
var reaction schema.ReactSummaryMeta
err = json.Unmarshal([]byte(reactionMeta.Value), &reaction)
if err != nil {
return nil, err
}
return ms.convertToReactionResp(ctx, reaction)
}
// AddOrUpdateReaction add or update reaction
func (ms *MetaService) AddOrUpdateReaction(ctx context.Context, req *schema.ReactionReq) (resp schema.ReactionResp, err error) {
// get reaction for this object
reactionMeta, err := ms.GetMetaByObjectIdAndKey(ctx, req.ObjectID, entity.ObjectReactSummaryKey)
var reaction schema.ReactSummaryMeta
if err != nil {
var pacmanErr *myErrors.Error
if errors.As(err, &pacmanErr) && pacmanErr.Reason == reason.MetaObjectNotFound {
// create new reaction summary
reaction = schema.ReactSummaryMeta{}
} else {
return nil, err
}
} else {
// json unmarshal reactionMeta.Value to reaction
err = json.Unmarshal([]byte(reactionMeta.Value), &reaction)
if err != nil {
return nil, err
}
}
// update reaction
ms.updateReaction(req, reaction)
// write back to meta repo
reactSumBytes, err := json.Marshal(reaction)
if err != nil {
return nil, err
}
if reactionMeta == nil {
err = ms.AddMeta(ctx, req.ObjectID, entity.ObjectReactSummaryKey, string(reactSumBytes))
} else {
err = ms.UpdateMeta(ctx, reactionMeta.ID, entity.ObjectReactSummaryKey, string(reactSumBytes))
}
if err != nil {
return nil, err
}
resp, err = ms.convertToReactionResp(ctx, reaction)
if err != nil {
return nil, err
}
return resp, nil
}
// updateReaction update reaction
func (ms *MetaService) updateReaction(req *schema.ReactionReq, reaction schema.ReactSummaryMeta) {
emojiUserIds, ok := reaction[req.Emoji]
if !ok {
emojiUserIds = make([]string, 0)
}
found := false
for _, item := range emojiUserIds {
if item == req.UserID {
found = true
break
}
}
removeItem := func(arr []string, target string) []string {
result := make([]string, 0, len(arr))
for _, item := range arr {
if item != target {
result = append(result, item)
}
}
return result
}
if req.Type == "activate" && !found {
emojiUserIds = append(emojiUserIds, req.UserID)
} else if req.Type == "deactivate" && found {
emojiUserIds = removeItem(emojiUserIds, req.UserID)
} else if req.Type == "toggle" {
if found {
emojiUserIds = removeItem(emojiUserIds, req.UserID)
} else {
emojiUserIds = append(emojiUserIds, req.UserID)
}
}
reaction[req.Emoji] = emojiUserIds
}
func (ms *MetaService) convertToReactionResp(ctx context.Context, reaction schema.ReactSummaryMeta) (schema.ReactionResp, error) {
resp := schema.ReactionResp{}
// traverse map and convert to user name
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
for _, userBasicInfo := range userBasicInfos {
userNames = append(userNames, userBasicInfo.Username)
}
resp[emoji] = userNames
}
return resp, nil
}
@@ -36,6 +36,7 @@ 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;
@@ -198,6 +199,8 @@ const Index: FC<Props> = ({
</div>
</div>
<Reactions />
<Comment
objectId={data.id}
mode="answer"
@@ -37,6 +37,7 @@ 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;
@@ -212,6 +213,8 @@ const Index: FC<Props> = ({ data, initPage, hasAnswer, isLogged }) => {
</div>
</div>
<Reactions />
<Comment
objectId={data?.id}
mode="question"
@@ -0,0 +1,86 @@
import { FC, memo } from 'react';
import { Button, OverlayTrigger, Tooltip } from 'react-bootstrap';
import { Icon } from '@/components';
interface Props {}
interface Data {
title: string;
icon: string;
className: string;
}
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 Index: FC<Props> = () => {
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) => (
<Button
key={d.icon}
variant="light"
size="sm"
onClick={() => alert('hellob')}>
<Icon name={d.icon} className={d.className} />
</Button>
))}
</div>
</Tooltip>
);
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>
<OverlayTrigger trigger="click" placement="top" overlay={renderTooltip}>
<Button
variant="outline-secondary"
size="sm"
className="rounded-pill ms-2 answer-reaction-btn">
<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>
))}
</div>
);
};
export default memo(Index);
+41 -3
View File
@@ -16,15 +16,53 @@
* specific language governing permissions and limitations
* under the License.
*/
@import 'bootstrap/scss/functions';
@import 'bootstrap/scss/variables';
.answer-item {
border-top: 1px solid var(--an-answer-item-border-top);
.answer-reaction-btn {
padding-top: 0.2rem;
padding-bottom: 0.2rem;
border: 1px solid $gray-300;
}
#reaction-button-tooltip {
.tooltip-inner {
padding: 0.2rem;
background-color: $white;
border: 1px solid $gray-300;
.btn {
background-color: $white;
border: none;
&:hover {
background-color: $gray-400;
}
}
}
.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;
width: 50%;
height: 100%;
transform: translate(-50%, -50%) rotate(-45deg);
// transform: rotate(-45deg);
box-sizing: content-box;
left: 50%;
bottom: 50%;
background-color: white;
}
}
}
@media screen and (max-width: 768px) {
.questionDetailPage {
h1.h3 {
font-size: calc(1.275rem + .3vw)!important;
font-size: calc(1.275rem + 0.3vw) !important;
}
}
}