[MM-44084] Feature: Top threads insights (#20195)
* Add route endpoints, model, store functions, and tests for top threads
* Run make store-layers
* Make the following changes
- Fix top user threads query
- Fix passing parameters in api4/insights.go to handler in app
- Add top user threads test
* Add post-message, user_id, participants information to insights results
* model.TopThread.UserID -> model.TopThread.UserId, for compatibility with MySQL
* Rename name -> channel_name
* Add user information to response
* Link post in response, filter out deleted root posts from top threads
* Handle thread delete cases, add app tests for threads insights
* lint: fix typo
* lint: rename asserts
* lint: require.nil -> require.NoError
* Add integration tests for thread insights
* Add embeds and images to top posts
* Add license checks for top threads endpoints
* Query users in batch to populate post-creator
* Make the following changes
- Add license to test server in api4/
- Add tests for threads insights
- top team threads shouldn't include threads from other teams, DMs
- Test duration constraint
- Pagination testing for top threads in model/insights_test.go
* Add i18n-extract
* i18n fixes
* Add username, nickname to user_information
* Hide message, user_id, post_id, reply_count in depth=1 of top threads response
* Fix tests using response.reply_count to use response.post.reply_count
Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
de50943d61
Коммит
2cd83d2f8d
@@ -935,3 +935,209 @@ func (s *SqlThreadStore) GetThreadUnreadReplyCount(threadMembership *model.Threa
|
||||
|
||||
return unreadReplies, nil
|
||||
}
|
||||
|
||||
// Top threads in all public channels and private channels userID is a member of. Returns a list of threads ranked by interactions.
|
||||
func (s *SqlThreadStore) GetTopThreadsForTeamSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
var args []interface{}
|
||||
query := `select
|
||||
threads_list.PostId,
|
||||
threads_list.ReplyCount,
|
||||
threads_list.ChannelId,
|
||||
threads_list.DisplayName,
|
||||
threads_list.Name,
|
||||
threads_list.Participants,
|
||||
p.UserId
|
||||
from((
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND c.TeamId = ?
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)
|
||||
UNION
|
||||
ALL (
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
|
||||
LEFT JOIN Channels c ON t.ChannelId = c.Id
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND cm.UserId = ?
|
||||
AND c.Type = 'P'
|
||||
AND c.TeamId = ?
|
||||
AND t.LastReplyAt > ?
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)) as threads_list
|
||||
LEFT JOIN Posts as p on p.Id = threads_list.PostId
|
||||
ORDER BY ReplyCount DESC
|
||||
limit ? offset ?`
|
||||
|
||||
args = append(args, since, teamID, userID, teamID, since, limit+1, offset)
|
||||
|
||||
topThreads := make([]*model.TopThread, 0)
|
||||
err := s.GetReplicaX().Select(&topThreads, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
|
||||
}
|
||||
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.GetTopThreadListWithPagination(topThreads, limit), nil
|
||||
}
|
||||
|
||||
func (s *SqlThreadStore) GetTopThreadsForUserSince(teamID string, userID string, since int64, offset int, limit int) (*model.TopThreadList, error) {
|
||||
var args []interface{}
|
||||
|
||||
// gets all threads within the team which user follows.
|
||||
query := `select
|
||||
threads_list.PostId,
|
||||
threads_list.ReplyCount,
|
||||
threads_list.ChannelId,
|
||||
threads_list.DisplayName,
|
||||
threads_list.Name,
|
||||
threads_list.Participants,
|
||||
p.UserId
|
||||
from((
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN PublicChannels c ON t.ChannelId = c.Id
|
||||
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
|
||||
WHERE
|
||||
t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND c.TeamId = ?
|
||||
AND tm.UserId = ?
|
||||
AND tm.Following = TRUE
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)
|
||||
UNION
|
||||
ALL (
|
||||
SELECT
|
||||
t.PostId,
|
||||
t.ReplyCount,
|
||||
t.ChannelId,
|
||||
t.Participants,
|
||||
c.DisplayName,
|
||||
c.Name
|
||||
FROM
|
||||
Threads t
|
||||
LEFT JOIN ChannelMembers cm ON t.ChannelId = cm.ChannelId
|
||||
LEFT JOIN Channels c ON t.ChannelId = c.Id
|
||||
LEFT JOIN ThreadMemberships as tm on t.PostId = tm.PostId
|
||||
WHERE
|
||||
cm.UserId = ?
|
||||
AND c.Type = 'P'
|
||||
AND c.TeamId = ?
|
||||
AND t.threaddeleteat IS NULL
|
||||
AND t.LastReplyAt > ?
|
||||
AND tm.UserId = ?
|
||||
AND tm.Following = TRUE
|
||||
GROUP BY
|
||||
t.PostId,
|
||||
c.DisplayName,
|
||||
c.Name,
|
||||
t.Participants
|
||||
)) as threads_list
|
||||
LEFT JOIN Posts as p on p.Id = threads_list.PostId
|
||||
ORDER BY ReplyCount DESC
|
||||
limit ? offset ?`
|
||||
|
||||
args = append(args, since, teamID, userID, userID, teamID, since, userID, limit+1, offset)
|
||||
|
||||
topThreads := make([]*model.TopThread, 0)
|
||||
err := s.GetReplicaX().Select(&topThreads, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get top threads=%s", teamID)
|
||||
}
|
||||
topThreads, err = postProcessTopThreads(topThreads, s, teamID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return model.GetTopThreadListWithPagination(topThreads, limit), nil
|
||||
}
|
||||
|
||||
func userContains(userIDs []string, searchedUserID string) bool {
|
||||
for _, userID := range userIDs {
|
||||
if userID == searchedUserID {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func postProcessTopThreads(topThreads []*model.TopThread, s *SqlThreadStore, teamID string) ([]*model.TopThread, error) {
|
||||
// create list of userIDs
|
||||
var userIDs []string
|
||||
for _, topThread := range topThreads {
|
||||
userID := topThread.UserId
|
||||
if !userContains(userIDs, userID) {
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
}
|
||||
|
||||
usersMap := map[string]*model.User{}
|
||||
|
||||
users, err := s.User().GetProfileByIds(context.Background(), userIDs, &store.UserGetByIdsOpts{}, true)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get users for top threads in team=%s", teamID)
|
||||
}
|
||||
for _, user := range users {
|
||||
usersMap[user.Id] = user
|
||||
}
|
||||
|
||||
// resolve user, root post for each top thread
|
||||
for _, topThread := range topThreads {
|
||||
postCreator := usersMap[topThread.UserId]
|
||||
topThread.UserInformation = &model.InsightUserInformation{
|
||||
Id: postCreator.Id,
|
||||
LastPictureUpdate: postCreator.LastPictureUpdate,
|
||||
FirstName: postCreator.FirstName,
|
||||
LastName: postCreator.LastName,
|
||||
Username: postCreator.Username,
|
||||
NickName: postCreator.Nickname,
|
||||
}
|
||||
post, err := s.Post().GetSingle(topThread.PostId, false)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(err, "failed to get extended post for post id=%s", topThread.PostId)
|
||||
}
|
||||
topThread.Post = post
|
||||
}
|
||||
return topThreads, nil
|
||||
}
|
||||
|
||||
Ссылка в новой задаче
Block a user