github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/store/sqlstore/thread_store.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package sqlstore
     5  
     6  import (
     7  	"context"
     8  	"database/sql"
     9  	"time"
    10  
    11  	sq "github.com/Masterminds/squirrel"
    12  	"github.com/pkg/errors"
    13  
    14  	"github.com/mattermost/mattermost-server/v5/model"
    15  	"github.com/mattermost/mattermost-server/v5/store"
    16  	"github.com/mattermost/mattermost-server/v5/utils"
    17  )
    18  
    19  type SqlThreadStore struct {
    20  	*SqlStore
    21  }
    22  
    23  func (s *SqlThreadStore) ClearCaches() {
    24  }
    25  
    26  func newSqlThreadStore(sqlStore *SqlStore) store.ThreadStore {
    27  	s := &SqlThreadStore{
    28  		SqlStore: sqlStore,
    29  	}
    30  
    31  	for _, db := range sqlStore.GetAllConns() {
    32  		tableThreads := db.AddTableWithName(model.Thread{}, "Threads").SetKeys(false, "PostId")
    33  		tableThreads.ColMap("PostId").SetMaxSize(26)
    34  		tableThreads.ColMap("ChannelId").SetMaxSize(26)
    35  		tableThreads.ColMap("Participants").SetMaxSize(0)
    36  		tableThreadMemberships := db.AddTableWithName(model.ThreadMembership{}, "ThreadMemberships").SetKeys(false, "PostId", "UserId")
    37  		tableThreadMemberships.ColMap("PostId").SetMaxSize(26)
    38  		tableThreadMemberships.ColMap("UserId").SetMaxSize(26)
    39  	}
    40  
    41  	return s
    42  }
    43  
    44  func threadSliceColumns() []string {
    45  	return []string{"PostId", "ChannelId", "LastReplyAt", "ReplyCount", "Participants"}
    46  }
    47  
    48  func threadToSlice(thread *model.Thread) []interface{} {
    49  	return []interface{}{
    50  		thread.PostId,
    51  		thread.ChannelId,
    52  		thread.LastReplyAt,
    53  		thread.ReplyCount,
    54  		thread.Participants,
    55  	}
    56  }
    57  
    58  func (s *SqlThreadStore) createIndexesIfNotExists() {
    59  	s.CreateIndexIfNotExists("idx_thread_memberships_last_update_at", "ThreadMemberships", "LastUpdated")
    60  	s.CreateIndexIfNotExists("idx_thread_memberships_last_view_at", "ThreadMemberships", "LastViewed")
    61  	s.CreateIndexIfNotExists("idx_thread_memberships_user_id", "ThreadMemberships", "UserId")
    62  	s.CreateIndexIfNotExists("idx_threads_channel_id", "Threads", "ChannelId")
    63  }
    64  
    65  func (s *SqlThreadStore) SaveMultiple(threads []*model.Thread) ([]*model.Thread, int, error) {
    66  	builder := s.getQueryBuilder().Insert("Threads").Columns(threadSliceColumns()...)
    67  	for _, thread := range threads {
    68  		builder = builder.Values(threadToSlice(thread)...)
    69  	}
    70  	query, args, err := builder.ToSql()
    71  	if err != nil {
    72  		return nil, -1, errors.Wrap(err, "thread_tosql")
    73  	}
    74  
    75  	if _, err := s.GetMaster().Exec(query, args...); err != nil {
    76  		return nil, -1, errors.Wrap(err, "failed to save Post")
    77  	}
    78  
    79  	return threads, -1, nil
    80  }
    81  
    82  func (s *SqlThreadStore) Save(thread *model.Thread) (*model.Thread, error) {
    83  	threads, _, err := s.SaveMultiple([]*model.Thread{thread})
    84  	if err != nil {
    85  		return nil, err
    86  	}
    87  	return threads[0], nil
    88  }
    89  
    90  func (s *SqlThreadStore) Update(thread *model.Thread) (*model.Thread, error) {
    91  	if _, err := s.GetMaster().Update(thread); err != nil {
    92  		return nil, errors.Wrapf(err, "failed to update thread with id=%s", thread.PostId)
    93  	}
    94  
    95  	return thread, nil
    96  }
    97  
    98  func (s *SqlThreadStore) Get(id string) (*model.Thread, error) {
    99  	var thread model.Thread
   100  	query, args, _ := s.getQueryBuilder().Select("*").From("Threads").Where(sq.Eq{"PostId": id}).ToSql()
   101  	err := s.GetReplica().SelectOne(&thread, query, args...)
   102  	if err != nil {
   103  		if err == sql.ErrNoRows {
   104  			return nil, store.NewErrNotFound("Thread", id)
   105  		}
   106  
   107  		return nil, errors.Wrapf(err, "failed to get thread with id=%s", id)
   108  	}
   109  	return &thread, nil
   110  }
   111  
   112  func (s *SqlThreadStore) GetThreadMentionsForUserPerChannel(userId, teamId string) (map[string]int64, error) {
   113  	type Count struct {
   114  		UnreadMentions int64
   115  		ChannelId      string
   116  	}
   117  	var counts []Count
   118  
   119  	sql, args, _ := s.getQueryBuilder().
   120  		Select("SUM(UnreadMentions) as UnreadMentions", "ChannelId").
   121  		From("ThreadMemberships").
   122  		LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
   123  		LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
   124  		Where(sq.And{
   125  			sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}},
   126  			sq.Eq{"ThreadMemberships.UserId": userId},
   127  			sq.Eq{"ThreadMemberships.Following": true},
   128  		}).
   129  		GroupBy("Threads.ChannelId").ToSql()
   130  
   131  	if _, err := s.GetMaster().Select(&counts, sql, args...); err != nil {
   132  		return nil, err
   133  	}
   134  	result := map[string]int64{}
   135  	for _, count := range counts {
   136  		result[count.ChannelId] = count.UnreadMentions
   137  	}
   138  	return result, nil
   139  }
   140  
   141  func (s *SqlThreadStore) GetThreadsForUser(userId, teamId string, opts model.GetUserThreadsOpts) (*model.Threads, error) {
   142  	type JoinedThread struct {
   143  		PostId         string
   144  		ReplyCount     int64
   145  		LastReplyAt    int64
   146  		LastViewedAt   int64
   147  		UnreadReplies  int64
   148  		UnreadMentions int64
   149  		Participants   model.StringArray
   150  		model.Post
   151  	}
   152  
   153  	unreadRepliesQuery := "SELECT COUNT(Posts.Id) From Posts Where Posts.RootId=ThreadMemberships.PostId AND Posts.UpdateAt >= ThreadMemberships.LastViewed"
   154  	fetchConditions := sq.And{
   155  		sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}},
   156  		sq.Eq{"ThreadMemberships.UserId": userId},
   157  		sq.Eq{"ThreadMemberships.Following": true},
   158  	}
   159  	if !opts.Deleted {
   160  		fetchConditions = sq.And{fetchConditions, sq.Eq{"Posts.DeleteAt": 0}}
   161  	}
   162  
   163  	pageSize := uint64(30)
   164  	if opts.PageSize != 0 {
   165  		pageSize = opts.PageSize
   166  	}
   167  
   168  	totalUnreadThreadsChan := make(chan store.StoreResult, 1)
   169  	totalCountChan := make(chan store.StoreResult, 1)
   170  	totalUnreadMentionsChan := make(chan store.StoreResult, 1)
   171  	threadsChan := make(chan store.StoreResult, 1)
   172  	go func() {
   173  		repliesQuery, repliesQueryArgs, _ := s.getQueryBuilder().
   174  			Select("COUNT(Posts.Id)").
   175  			From("Posts").
   176  			LeftJoin("ThreadMemberships ON Posts.Id = ThreadMemberships.PostId").
   177  			LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
   178  			Where(fetchConditions).
   179  			Where("Posts.UpdateAt >= ThreadMemberships.LastViewed").ToSql()
   180  
   181  		totalUnreadThreads, err := s.GetMaster().SelectInt(repliesQuery, repliesQueryArgs...)
   182  		totalUnreadThreadsChan <- store.StoreResult{Data: totalUnreadThreads, NErr: errors.Wrapf(err, "failed to get count unread on threads for user id=%s", userId)}
   183  		close(totalUnreadThreadsChan)
   184  	}()
   185  	go func() {
   186  		newFetchConditions := fetchConditions
   187  
   188  		if opts.Unread {
   189  			newFetchConditions = sq.And{newFetchConditions, sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt")}
   190  		}
   191  
   192  		threadsQuery, threadsQueryArgs, _ := s.getQueryBuilder().
   193  			Select("COUNT(ThreadMemberships.PostId)").
   194  			LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
   195  			LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
   196  			LeftJoin("Posts ON Posts.Id = ThreadMemberships.PostId").
   197  			From("ThreadMemberships").
   198  			Where(newFetchConditions).ToSql()
   199  
   200  		totalCount, err := s.GetMaster().SelectInt(threadsQuery, threadsQueryArgs...)
   201  		totalCountChan <- store.StoreResult{Data: totalCount, NErr: err}
   202  		close(totalCountChan)
   203  	}()
   204  	go func() {
   205  		mentionsQuery, mentionsQueryArgs, _ := s.getQueryBuilder().
   206  			Select("COALESCE(SUM(ThreadMemberships.UnreadMentions),0)").
   207  			From("ThreadMemberships").
   208  			LeftJoin("Threads ON Threads.PostId = ThreadMemberships.PostId").
   209  			LeftJoin("Posts ON Posts.Id = ThreadMemberships.PostId").
   210  			LeftJoin("Channels ON Threads.ChannelId = Channels.Id").
   211  			Where(fetchConditions).ToSql()
   212  		totalUnreadMentions, err := s.GetMaster().SelectInt(mentionsQuery, mentionsQueryArgs...)
   213  		totalUnreadMentionsChan <- store.StoreResult{Data: totalUnreadMentions, NErr: err}
   214  		close(totalUnreadMentionsChan)
   215  	}()
   216  	go func() {
   217  		newFetchConditions := fetchConditions
   218  		if opts.Since > 0 {
   219  			newFetchConditions = sq.And{newFetchConditions, sq.GtOrEq{"ThreadMemberships.LastUpdated": opts.Since}}
   220  		}
   221  		order := "DESC"
   222  		if opts.Before != "" {
   223  			newFetchConditions = sq.And{
   224  				newFetchConditions,
   225  				sq.Expr(`LastReplyAt < (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.Before),
   226  			}
   227  		}
   228  		if opts.After != "" {
   229  			order = "ASC"
   230  			newFetchConditions = sq.And{
   231  				newFetchConditions,
   232  				sq.Expr(`LastReplyAt > (SELECT LastReplyAt FROM Threads WHERE PostId = ?)`, opts.After),
   233  			}
   234  		}
   235  		if opts.Unread {
   236  			newFetchConditions = sq.And{newFetchConditions, sq.Expr("ThreadMemberships.LastViewed < Threads.LastReplyAt")}
   237  		}
   238  		var threads []*JoinedThread
   239  		query, args, _ := s.getQueryBuilder().
   240  			Select("Threads.*, Posts.*, ThreadMemberships.LastViewed as LastViewedAt, ThreadMemberships.UnreadMentions as UnreadMentions").
   241  			From("Threads").
   242  			Column(sq.Alias(sq.Expr(unreadRepliesQuery), "UnreadReplies")).
   243  			LeftJoin("Posts ON Posts.Id = Threads.PostId").
   244  			LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
   245  			LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId").
   246  			Where(newFetchConditions).
   247  			OrderBy("Threads.LastReplyAt " + order).
   248  			Limit(pageSize).ToSql()
   249  
   250  		_, err := s.GetReplica().Select(&threads, query, args...)
   251  		threadsChan <- store.StoreResult{Data: threads, NErr: err}
   252  		close(threadsChan)
   253  	}()
   254  
   255  	threadsResult := <-threadsChan
   256  	if threadsResult.NErr != nil {
   257  		return nil, threadsResult.NErr
   258  	}
   259  	threads := threadsResult.Data.([]*JoinedThread)
   260  
   261  	totalUnreadMentionsResult := <-totalUnreadMentionsChan
   262  	if totalUnreadMentionsResult.NErr != nil {
   263  		return nil, totalUnreadMentionsResult.NErr
   264  	}
   265  	totalUnreadMentions := totalUnreadMentionsResult.Data.(int64)
   266  
   267  	totalCountResult := <-totalCountChan
   268  	if totalCountResult.NErr != nil {
   269  		return nil, totalCountResult.NErr
   270  	}
   271  	totalCount := totalCountResult.Data.(int64)
   272  
   273  	totalUnreadThreadsResult := <-totalUnreadThreadsChan
   274  	if totalUnreadThreadsResult.NErr != nil {
   275  		return nil, totalUnreadThreadsResult.NErr
   276  	}
   277  	totalUnreadThreads := totalUnreadThreadsResult.Data.(int64)
   278  
   279  	var userIds []string
   280  	userIdMap := map[string]bool{}
   281  	for _, thread := range threads {
   282  		for _, participantId := range thread.Participants {
   283  			if _, ok := userIdMap[participantId]; !ok {
   284  				userIdMap[participantId] = true
   285  				userIds = append(userIds, participantId)
   286  			}
   287  		}
   288  	}
   289  	var users []*model.User
   290  	if opts.Extended {
   291  		var err error
   292  		users, err = s.User().GetProfileByIds(context.Background(), userIds, &store.UserGetByIdsOpts{}, true)
   293  		if err != nil {
   294  			return nil, errors.Wrapf(err, "failed to get threads for user id=%s", userId)
   295  		}
   296  	} else {
   297  		for _, userId := range userIds {
   298  			users = append(users, &model.User{Id: userId})
   299  		}
   300  	}
   301  
   302  	result := &model.Threads{
   303  		Total:               totalCount,
   304  		Threads:             []*model.ThreadResponse{},
   305  		TotalUnreadMentions: totalUnreadMentions,
   306  		TotalUnreadThreads:  totalUnreadThreads,
   307  	}
   308  
   309  	for _, thread := range threads {
   310  		var participants []*model.User
   311  		for _, participantId := range thread.Participants {
   312  			var participant *model.User
   313  			for _, u := range users {
   314  				if u.Id == participantId {
   315  					participant = u
   316  					break
   317  				}
   318  			}
   319  			if participant == nil {
   320  				return nil, errors.New("cannot find thread participant with id=" + participantId)
   321  			}
   322  			participants = append(participants, participant)
   323  		}
   324  		result.Threads = append(result.Threads, &model.ThreadResponse{
   325  			PostId:         thread.PostId,
   326  			ReplyCount:     thread.ReplyCount,
   327  			LastReplyAt:    thread.LastReplyAt,
   328  			LastViewedAt:   thread.LastViewedAt,
   329  			UnreadReplies:  thread.UnreadReplies,
   330  			UnreadMentions: thread.UnreadMentions,
   331  			Participants:   participants,
   332  			Post:           &thread.Post,
   333  		})
   334  	}
   335  
   336  	return result, nil
   337  }
   338  func (s *SqlThreadStore) GetThreadForUser(userId, teamId, threadId string, extended bool) (*model.ThreadResponse, error) {
   339  	type JoinedThread struct {
   340  		PostId         string
   341  		Following      bool
   342  		ReplyCount     int64
   343  		LastReplyAt    int64
   344  		LastViewedAt   int64
   345  		UnreadReplies  int64
   346  		UnreadMentions int64
   347  		Participants   model.StringArray
   348  		model.Post
   349  	}
   350  
   351  	unreadRepliesQuery := "SELECT COUNT(Posts.Id) From Posts Where Posts.RootId=ThreadMemberships.PostId AND Posts.UpdateAt >= ThreadMemberships.LastViewed AND Posts.DeleteAt=0"
   352  	fetchConditions := sq.And{
   353  		sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}},
   354  		sq.Eq{"ThreadMemberships.UserId": userId},
   355  		sq.Eq{"Threads.PostId": threadId},
   356  	}
   357  
   358  	var thread JoinedThread
   359  	query, args, _ := s.getQueryBuilder().
   360  		Select("Threads.*, Posts.*, ThreadMemberships.LastViewed as LastViewedAt, ThreadMemberships.UnreadMentions as UnreadMentions, ThreadMemberships.Following").
   361  		From("Threads").
   362  		Column(sq.Alias(sq.Expr(unreadRepliesQuery), "UnreadReplies")).
   363  		LeftJoin("Posts ON Posts.Id = Threads.PostId").
   364  		LeftJoin("Channels ON Posts.ChannelId = Channels.Id").
   365  		LeftJoin("ThreadMemberships ON ThreadMemberships.PostId = Threads.PostId").
   366  		Where(fetchConditions).ToSql()
   367  	err := s.GetReplica().SelectOne(&thread, query, args...)
   368  
   369  	if err != nil {
   370  		return nil, err
   371  	}
   372  
   373  	if !thread.Following {
   374  		return nil, nil // in case the thread is not followed anymore - return nil error to be interpreted as 404
   375  	}
   376  
   377  	var users []*model.User
   378  	if extended {
   379  		var err error
   380  		users, err = s.User().GetProfileByIds(context.Background(), thread.Participants, &store.UserGetByIdsOpts{}, true)
   381  		if err != nil {
   382  			return nil, errors.Wrapf(err, "failed to get threads for user id=%s", userId)
   383  		}
   384  	} else {
   385  		for _, userId := range thread.Participants {
   386  			users = append(users, &model.User{Id: userId})
   387  		}
   388  	}
   389  
   390  	result := &model.ThreadResponse{
   391  		PostId:         thread.PostId,
   392  		ReplyCount:     thread.ReplyCount,
   393  		LastReplyAt:    thread.LastReplyAt,
   394  		LastViewedAt:   thread.LastViewedAt,
   395  		UnreadReplies:  thread.UnreadReplies,
   396  		UnreadMentions: thread.UnreadMentions,
   397  		Participants:   users,
   398  		Post:           &thread.Post,
   399  	}
   400  
   401  	return result, nil
   402  }
   403  
   404  func (s *SqlThreadStore) MarkAllAsRead(userId, teamId string) error {
   405  	memberships, err := s.GetMembershipsForUser(userId, teamId)
   406  	if err != nil {
   407  		return err
   408  	}
   409  	var membershipIds []string
   410  	for _, m := range memberships {
   411  		membershipIds = append(membershipIds, m.PostId)
   412  	}
   413  	timestamp := model.GetMillis()
   414  	query, args, _ := s.getQueryBuilder().
   415  		Update("ThreadMemberships").
   416  		Where(sq.Eq{"PostId": membershipIds}).
   417  		Where(sq.Eq{"UserId": userId}).
   418  		Set("LastViewed", timestamp).
   419  		Set("UnreadMentions", 0).
   420  		ToSql()
   421  	if _, err := s.GetMaster().Exec(query, args...); err != nil {
   422  		return errors.Wrapf(err, "failed to update thread read state for user id=%s", userId)
   423  	}
   424  	return nil
   425  }
   426  
   427  func (s *SqlThreadStore) MarkAsRead(userId, threadId string, timestamp int64) error {
   428  	query, args, _ := s.getQueryBuilder().
   429  		Update("ThreadMemberships").
   430  		Where(sq.Eq{"UserId": userId}).
   431  		Where(sq.Eq{"PostId": threadId}).
   432  		Set("LastViewed", timestamp).
   433  		ToSql()
   434  	if _, err := s.GetMaster().Exec(query, args...); err != nil {
   435  		return errors.Wrapf(err, "failed to update thread read state for user id=%s thread_id=%v", userId, threadId)
   436  	}
   437  	return nil
   438  }
   439  
   440  func (s *SqlThreadStore) Delete(threadId string) error {
   441  	query, args, _ := s.getQueryBuilder().Delete("Threads").Where(sq.Eq{"PostId": threadId}).ToSql()
   442  	if _, err := s.GetMaster().Exec(query, args...); err != nil {
   443  		return errors.Wrap(err, "failed to update threads")
   444  	}
   445  
   446  	return nil
   447  }
   448  
   449  func (s *SqlThreadStore) SaveMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
   450  	if err := s.GetMaster().Insert(membership); err != nil {
   451  		return nil, errors.Wrapf(err, "failed to save thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
   452  	}
   453  
   454  	return membership, nil
   455  }
   456  
   457  func (s *SqlThreadStore) UpdateMembership(membership *model.ThreadMembership) (*model.ThreadMembership, error) {
   458  	if _, err := s.GetMaster().Update(membership); err != nil {
   459  		return nil, errors.Wrapf(err, "failed to update thread membership with postid=%s userid=%s", membership.PostId, membership.UserId)
   460  	}
   461  
   462  	return membership, nil
   463  }
   464  
   465  func (s *SqlThreadStore) GetMembershipsForUser(userId, teamId string) ([]*model.ThreadMembership, error) {
   466  	var memberships []*model.ThreadMembership
   467  
   468  	query, args, _ := s.getQueryBuilder().
   469  		Select("ThreadMemberships.*").
   470  		Join("Threads ON Threads.PostId = ThreadMemberships.PostId").
   471  		Join("Channels ON Threads.ChannelId = Channels.Id").
   472  		From("ThreadMemberships").
   473  		Where(sq.Or{sq.Eq{"Channels.TeamId": teamId}, sq.Eq{"Channels.TeamId": ""}}).
   474  		Where(sq.Eq{"ThreadMemberships.UserId": userId}).
   475  		ToSql()
   476  
   477  	_, err := s.GetReplica().Select(&memberships, query, args...)
   478  	if err != nil {
   479  		return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s", userId)
   480  	}
   481  	return memberships, nil
   482  }
   483  
   484  func (s *SqlThreadStore) GetMembershipForUser(userId, postId string) (*model.ThreadMembership, error) {
   485  	var membership model.ThreadMembership
   486  	err := s.GetReplica().SelectOne(&membership, "SELECT * from ThreadMemberships WHERE UserId = :UserId AND PostId = :PostId", map[string]interface{}{"UserId": userId, "PostId": postId})
   487  	if err != nil {
   488  		if err == sql.ErrNoRows {
   489  			return nil, store.NewErrNotFound("Thread", postId)
   490  		}
   491  		return nil, errors.Wrapf(err, "failed to get thread membership with userid=%s postid=%s", userId, postId)
   492  	}
   493  	return &membership, nil
   494  }
   495  
   496  func (s *SqlThreadStore) DeleteMembershipForUser(userId string, postId string) error {
   497  	if _, err := s.GetMaster().Exec("DELETE FROM ThreadMemberships Where PostId = :PostId AND UserId = :UserId", map[string]interface{}{"PostId": postId, "UserId": userId}); err != nil {
   498  		return errors.Wrap(err, "failed to update thread membership")
   499  	}
   500  
   501  	return nil
   502  }
   503  
   504  func (s *SqlThreadStore) CreateMembershipIfNeeded(userId, postId string, following, incrementMentions, updateFollowing bool) error {
   505  	membership, err := s.GetMembershipForUser(userId, postId)
   506  	now := utils.MillisFromTime(time.Now())
   507  	if err == nil {
   508  		if (updateFollowing && !membership.Following || membership.Following != following) || incrementMentions {
   509  			if updateFollowing {
   510  				membership.Following = following
   511  			}
   512  			membership.LastUpdated = now
   513  			if incrementMentions {
   514  				membership.UnreadMentions += 1
   515  			}
   516  			_, err = s.UpdateMembership(membership)
   517  		}
   518  		return err
   519  	}
   520  
   521  	var nfErr *store.ErrNotFound
   522  
   523  	if !errors.As(err, &nfErr) {
   524  		return errors.Wrap(err, "failed to get thread membership")
   525  	}
   526  	mentions := 0
   527  	if incrementMentions {
   528  		mentions = 1
   529  	}
   530  	_, err = s.SaveMembership(&model.ThreadMembership{
   531  		PostId:         postId,
   532  		UserId:         userId,
   533  		Following:      following,
   534  		LastViewed:     0,
   535  		LastUpdated:    now,
   536  		UnreadMentions: int64(mentions),
   537  	})
   538  	if err != nil {
   539  		return err
   540  	}
   541  
   542  	thread, err := s.Get(postId)
   543  	if err != nil {
   544  		return err
   545  	}
   546  	if !thread.Participants.Contains(userId) {
   547  		thread.Participants = append(thread.Participants, userId)
   548  		_, err = s.Update(thread)
   549  	}
   550  	return err
   551  }
   552  
   553  func (s *SqlThreadStore) CollectThreadsWithNewerReplies(userId string, channelIds []string, timestamp int64) ([]string, error) {
   554  	var changedThreads []string
   555  	query, args, _ := s.getQueryBuilder().
   556  		Select("Threads.PostId").
   557  		From("Threads").
   558  		LeftJoin("ChannelMembers ON ChannelMembers.ChannelId=Threads.ChannelId").
   559  		Where(sq.And{
   560  			sq.Eq{"Threads.ChannelId": channelIds},
   561  			sq.Eq{"ChannelMembers.UserId": userId},
   562  			sq.Or{
   563  				sq.Expr("Threads.LastReplyAt >= ChannelMembers.LastViewedAt"),
   564  				sq.GtOrEq{"Threads.LastReplyAt": timestamp},
   565  			},
   566  		}).
   567  		ToSql()
   568  	if _, err := s.GetReplica().Select(&changedThreads, query, args...); err != nil {
   569  		return nil, errors.Wrap(err, "failed to fetch threads")
   570  	}
   571  	return changedThreads, nil
   572  }
   573  
   574  func (s *SqlThreadStore) UpdateUnreadsByChannel(userId string, changedThreads []string, timestamp int64, updateViewedTimestamp bool) error {
   575  	if len(changedThreads) == 0 {
   576  		return nil
   577  	}
   578  
   579  	qb := s.getQueryBuilder().
   580  		Update("ThreadMemberships").
   581  		Where(sq.Eq{"UserId": userId, "PostId": changedThreads}).
   582  		Set("LastUpdated", timestamp)
   583  
   584  	if updateViewedTimestamp {
   585  		qb = qb.Set("LastViewed", timestamp)
   586  	}
   587  	updateQuery, updateArgs, _ := qb.ToSql()
   588  
   589  	if _, err := s.GetMaster().Exec(updateQuery, updateArgs...); err != nil {
   590  		return errors.Wrap(err, "failed to update thread membership")
   591  	}
   592  
   593  	return nil
   594  }
   595  
   596  func (s *SqlThreadStore) GetPosts(threadId string, since int64) ([]*model.Post, error) {
   597  	query, args, _ := s.getQueryBuilder().
   598  		Select("*").
   599  		From("Posts").
   600  		Where(sq.Eq{"RootId": threadId}).
   601  		Where(sq.Eq{"DeleteAt": 0}).
   602  		Where(sq.GtOrEq{"UpdateAt": since}).ToSql()
   603  	var result []*model.Post
   604  	if _, err := s.GetReplica().Select(&result, query, args...); err != nil {
   605  		return nil, errors.Wrap(err, "failed to fetch thread posts")
   606  	}
   607  	return result, nil
   608  }