Skip to content

Commit 0135863

Browse files
authored
[CHA-794] Add Query Threads (#317)
* Add query threads
1 parent f4421d4 commit 0135863

File tree

3 files changed

+283
-0
lines changed

3 files changed

+283
-0
lines changed

common.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package stream_chat
2+
3+
type PagerRequest struct {
4+
Limit *int `json:"limit" validate:"omitempty,gte=0,lte=100"`
5+
Next *string `json:"next"`
6+
Prev *string `json:"prev"`
7+
}
8+
type SortParamRequestList []*SortParamRequest
9+
10+
type SortParamRequest struct {
11+
// Name of field to sort by
12+
Field string `json:"field"`
13+
14+
// Direction is the sorting direction, 1 for Ascending, -1 for Descending, default is 1
15+
Direction int `json:"direction"`
16+
}
17+
18+
type PagerResponse struct {
19+
Next *string `json:"next,omitempty"`
20+
Prev *string `json:"prev,omitempty"`
21+
}

thread.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package stream_chat
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"time"
7+
)
8+
9+
type QueryThreadsRequest struct {
10+
User *User `json:"user,omitempty"`
11+
UserID string `json:"user_id,omitempty"`
12+
13+
Filter map[string]any `json:"filter,omitempty"`
14+
Sort *SortParamRequestList `json:"sort,omitempty"`
15+
Watch *bool `json:"watch,omitempty"`
16+
PagerRequest
17+
}
18+
19+
type QueryThreadsResponse struct {
20+
Threads []ThreadResponse `json:"threads"`
21+
Response
22+
PagerResponse
23+
}
24+
25+
type ThreadResponse struct {
26+
ChannelCID string `json:"channel_cid"`
27+
Channel *Channel `json:"channel,omitempty"`
28+
ParentMessageID string `json:"parent_message_id"`
29+
ParentMessage *MessageResponse `json:"parent_message,omitempty"`
30+
CreatedByUserID string `json:"created_by_user_id"`
31+
CreatedBy *UsersResponse `json:"created_by,omitempty"`
32+
ReplyCount int `json:"reply_count,omitempty"`
33+
ParticipantCount int `json:"participant_count,omitempty"`
34+
ActiveParticipantCount int `json:"active_participant_count,omitempty"`
35+
Participants ThreadParticipants `json:"thread_participants,omitempty"`
36+
LastMessageAt *time.Time `json:"last_message_at,omitempty"`
37+
CreatedAt *time.Time `json:"created_at"`
38+
UpdatedAt *time.Time `json:"updated_at"`
39+
DeletedAt *time.Time `json:"deleted_at,omitempty"`
40+
Title string `json:"title"`
41+
Custom map[string]any `json:"custom"`
42+
43+
LatestReplies []MessageResponse `json:"latest_replies,omitempty"`
44+
Read ChannelRead `json:"read,omitempty"`
45+
Draft Draft
46+
}
47+
48+
type Thread struct {
49+
AppPK int `json:"app_pk"`
50+
51+
ChannelCID string `json:"channel_cid"`
52+
Channel *Channel `json:"channel,omitempty"`
53+
54+
ParentMessageID string `json:"parent_message_id"`
55+
ParentMessage *Message `json:"parent_message,omitempty"`
56+
57+
CreatedByUserID string `json:"created_by_user_id"`
58+
CreatedBy *User `json:"created_by,omitempty"`
59+
60+
ReplyCount int `json:"reply_count,omitempty"`
61+
ParticipantCount int `json:"participant_count,omitempty"`
62+
ActiveParticipantCount int `json:"active_participant_count,omitempty"`
63+
Participants ThreadParticipants `json:"thread_participants,omitempty"`
64+
65+
LastMessageAt time.Time `json:"last_message_at,omitempty"`
66+
CreatedAt time.Time `json:"created_at"`
67+
UpdatedAt time.Time `json:"updated_at"`
68+
DeletedAt *time.Time `json:"deleted_at,omitempty"`
69+
70+
Title string `json:"title"`
71+
Custom map[string]any `json:"custom"`
72+
}
73+
74+
type ThreadParticipant struct {
75+
AppPK int `json:"app_pk"`
76+
77+
ChannelCID string `json:"channel_cid"`
78+
79+
LastThreadMessageAt *time.Time `json:"last_thread_message_at"`
80+
ThreadID string `json:"thread_id,omitempty"`
81+
82+
UserID string `json:"user_id,omitempty"`
83+
User *User `json:"user,omitempty"`
84+
85+
CreatedAt time.Time `json:"created_at"`
86+
LeftThreadAt *time.Time `json:"left_thread_at,omitempty"`
87+
LastReadAt time.Time `json:"last_read_at"`
88+
89+
Custom map[string]interface{} `json:"custom"`
90+
}
91+
92+
type ThreadParticipants []*ThreadParticipant
93+
94+
func (c *Client) QueryThreads(ctx context.Context, query *QueryThreadsRequest) (*QueryThreadsResponse, error) {
95+
var resp QueryThreadsResponse
96+
err := c.makeRequest(ctx, http.MethodPost, "threads", nil, query, &resp)
97+
if err != nil {
98+
return nil, err
99+
}
100+
101+
return &resp, nil
102+
}

thread_test.go

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
package stream_chat
2+
3+
import (
4+
"context"
5+
"testing"
6+
7+
"github.com/stretchr/testify/assert"
8+
"github.com/stretchr/testify/require"
9+
)
10+
11+
func TestClient_QueryThreads(t *testing.T) {
12+
c := initClient(t)
13+
ctx := context.Background()
14+
15+
t.Run("basic query", func(t *testing.T) {
16+
membersID, ch, parentMsg, replyMsg := testThreadSetup(t, c, 3)
17+
18+
limit := 10
19+
query := &QueryThreadsRequest{
20+
Filter: map[string]any{
21+
"channel_cid": map[string]any{
22+
"$eq": ch.CID,
23+
},
24+
},
25+
Sort: &SortParamRequestList{
26+
{
27+
Field: "created_at",
28+
Direction: -1,
29+
},
30+
},
31+
PagerRequest: PagerRequest{
32+
Limit: &limit,
33+
},
34+
UserID: membersID[0],
35+
}
36+
37+
resp, err := c.QueryThreads(ctx, query)
38+
require.NoError(t, err)
39+
require.NotNil(t, resp, "response should not be nil")
40+
require.NotEmpty(t, resp.Threads, "threads should not be empty")
41+
42+
thread := resp.Threads[0]
43+
assertThreadData(t, thread, ch, parentMsg, replyMsg)
44+
assertThreadParticipants(t, thread, ch.CreatedBy.ID)
45+
46+
assert.Empty(t, resp.PagerResponse)
47+
})
48+
49+
t.Run("with pagination", func(t *testing.T) {
50+
membersID, ch, parentMsg1, replyMsg1 := testThreadSetup(t, c, 3)
51+
limit := 1
52+
53+
// Create a second thread
54+
parentMsg2, err := ch.SendMessage(ctx, &Message{Text: "Parent message for thread 2"}, ch.CreatedBy.ID)
55+
require.NoError(t, err, "send second parent message")
56+
57+
replyMsg2, err := ch.SendMessage(ctx, &Message{
58+
Text: "Reply message 2",
59+
ParentID: parentMsg2.Message.ID,
60+
}, ch.CreatedBy.ID)
61+
require.NoError(t, err, "send second reply message")
62+
63+
// First page query
64+
query := &QueryThreadsRequest{
65+
Filter: map[string]any{
66+
"channel_cid": map[string]any{
67+
"$eq": ch.CID,
68+
},
69+
},
70+
Sort: &SortParamRequestList{
71+
{
72+
Field: "created_at",
73+
Direction: 1,
74+
},
75+
},
76+
PagerRequest: PagerRequest{
77+
Limit: &limit,
78+
},
79+
UserID: membersID[0],
80+
}
81+
82+
resp, err := c.QueryThreads(ctx, query)
83+
require.NoError(t, err)
84+
require.NotNil(t, resp, "response should not be nil")
85+
require.NotEmpty(t, resp.Threads, "threads should not be empty")
86+
87+
thread := resp.Threads[0]
88+
assertThreadData(t, thread, ch, parentMsg1, replyMsg1)
89+
assertThreadParticipants(t, thread, ch.CreatedBy.ID)
90+
91+
// Second page query
92+
query2 := &QueryThreadsRequest{
93+
Filter: map[string]any{
94+
"channel_cid": map[string]any{
95+
"$eq": ch.CID,
96+
},
97+
},
98+
Sort: &SortParamRequestList{
99+
{
100+
Field: "created_at",
101+
Direction: -1,
102+
},
103+
},
104+
PagerRequest: PagerRequest{
105+
Limit: &limit,
106+
Next: resp.Next,
107+
},
108+
UserID: membersID[0],
109+
}
110+
111+
resp, err = c.QueryThreads(ctx, query2)
112+
require.NoError(t, err)
113+
require.NotNil(t, resp, "response should not be nil")
114+
require.NotEmpty(t, resp.Threads, "threads should not be empty")
115+
116+
thread = resp.Threads[0]
117+
assertThreadData(t, thread, ch, parentMsg2, replyMsg2)
118+
assertThreadParticipants(t, thread, ch.CreatedBy.ID)
119+
})
120+
}
121+
122+
// testThreadSetup creates a channel with members and returns necessary test data
123+
func testThreadSetup(t *testing.T, c *Client, numMembers int) ([]string, *Channel, *MessageResponse, *MessageResponse) {
124+
membersID := randomUsersID(t, c, numMembers)
125+
ch := initChannel(t, c, membersID...)
126+
127+
// Create a parent message
128+
parentMsg, err := ch.SendMessage(context.Background(), &Message{Text: "Parent message for thread"}, ch.CreatedBy.ID)
129+
require.NoError(t, err, "send parent message")
130+
131+
// Create a thread by sending a reply
132+
replyMsg, err := ch.SendMessage(context.Background(), &Message{
133+
Text: "Reply message",
134+
ParentID: parentMsg.Message.ID,
135+
}, ch.CreatedBy.ID)
136+
require.NoError(t, err, "send reply message")
137+
138+
return membersID, ch, parentMsg, replyMsg
139+
}
140+
141+
// assertThreadData validates common thread data fields
142+
func assertThreadData(t *testing.T, thread ThreadResponse, ch *Channel, parentMsg, replyMsg *MessageResponse) {
143+
assert.Equal(t, ch.CID, thread.ChannelCID, "channel CID should match")
144+
assert.Equal(t, parentMsg.Message.ID, thread.ParentMessageID, "parent message ID should match")
145+
assert.Equal(t, ch.CreatedBy.ID, thread.CreatedByUserID, "created by user ID should match")
146+
assert.Equal(t, 1, thread.ReplyCount, "reply count should be 1")
147+
assert.Equal(t, 1, thread.ParticipantCount, "participant count should be 1")
148+
assert.Equal(t, parentMsg.Message.Text, thread.Title, "title should not be empty")
149+
assert.Equal(t, replyMsg.Message.CreatedAt, thread.CreatedAt, "created at should not be zero")
150+
assert.Equal(t, replyMsg.Message.UpdatedAt, thread.UpdatedAt, "updated at should not be zero")
151+
assert.Nil(t, thread.DeletedAt, "deleted at should be nil")
152+
}
153+
154+
// assertThreadParticipants validates thread participant data
155+
func assertThreadParticipants(t *testing.T, thread ThreadResponse, createdByID string) {
156+
require.Len(t, thread.Participants, 1, "should have one participant")
157+
assert.Equal(t, createdByID, thread.Participants[0].UserID, "participant user ID should match")
158+
assert.NotZero(t, thread.Participants[0].CreatedAt, "participant created at should not be zero")
159+
assert.NotZero(t, thread.Participants[0].LastReadAt, "participant last read at should not be zero")
160+
}

0 commit comments

Comments
 (0)