Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import io.getstream.chat.android.client.api2.endpoint.PollsApi
import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi
import io.getstream.chat.android.client.api2.endpoint.RemindersApi
import io.getstream.chat.android.client.api2.endpoint.RoleApi
import io.getstream.chat.android.client.api2.endpoint.SyncApi
import io.getstream.chat.android.client.api2.endpoint.ThreadsApi
import io.getstream.chat.android.client.api2.endpoint.UserApi
import io.getstream.chat.android.client.api2.endpoint.UserGroupApi
Expand Down Expand Up @@ -215,6 +216,7 @@ constructor(
private val deviceApi: DeviceApi,
private val moderationApi: ModerationApi,
private val generalApi: GeneralApi,
private val syncApi: SyncApi,
private val configApi: ConfigApi,
private val fileDownloadApi: FileDownloadApi,
private val ogApi: OpenGraphApi,
Expand Down Expand Up @@ -1679,7 +1681,7 @@ constructor(

override fun getSyncHistory(channelIds: List<String>, lastSyncAt: String): Call<List<ChatEvent>> =
with(eventMapping) {
return generalApi.getSyncHistory(
return syncApi.getSyncHistory(
body = SyncHistoryRequest(channelIds, lastSyncAt),
connectionId = connectionId,
).map { response ->
Expand Down Expand Up @@ -2009,7 +2011,7 @@ constructor(
}

override fun warmUp() {
generalApi.warmUp().enqueue()
syncApi.warmUp().enqueue()
}

private fun <T : Any> postponeCall(call: () -> Call<T>): Call<T> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,44 +17,28 @@
package io.getstream.chat.android.client.api2.endpoint

import io.getstream.chat.android.client.api.AuthenticatedApi
import io.getstream.chat.android.client.api.QueryParams
import io.getstream.chat.android.client.api2.UrlQueryPayload
import io.getstream.chat.android.client.api2.model.requests.SyncHistoryRequest
import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse
import io.getstream.chat.android.client.call.RetrofitCall
import io.getstream.chat.android.network.models.MembersResponse
import io.getstream.chat.android.network.models.QueryMembersPayload
import io.getstream.chat.android.network.models.SearchPayload
import io.getstream.chat.android.network.models.SearchResponse
import io.getstream.chat.android.network.models.WrappedUnreadCountsResponse
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.OPTIONS
import retrofit2.http.POST
import retrofit2.http.Query

@AuthenticatedApi
internal interface GeneralApi {
@OPTIONS("/connect")
fun warmUp(): RetrofitCall<ResponseBody>

@POST("/sync")
fun getSyncHistory(
@Body body: SyncHistoryRequest,
@Query(QueryParams.CONNECTION_ID) connectionId: String,
): RetrofitCall<SyncHistoryResponse>

@GET("/search")
@GET("/api/v2/chat/search")
fun searchMessages(
@UrlQueryPayload @Query("payload") payload: SearchPayload,
): RetrofitCall<SearchResponse>

@GET("/members")
@GET("/api/v2/chat/members")
fun queryMembers(
@UrlQueryPayload @Query("payload") payload: QueryMembersPayload,
): RetrofitCall<MembersResponse>

@GET("/unread")
@GET("/api/v2/chat/unread")
fun getUnreadCounts(): RetrofitCall<WrappedUnreadCountsResponse>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* 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 io.getstream.chat.android.client.api2.endpoint

import io.getstream.chat.android.client.api.AuthenticatedApi
import io.getstream.chat.android.client.api.QueryParams
import io.getstream.chat.android.client.api2.model.requests.SyncHistoryRequest
import io.getstream.chat.android.client.api2.model.response.SyncHistoryResponse
import io.getstream.chat.android.client.call.RetrofitCall
import okhttp3.ResponseBody
import retrofit2.http.Body
import retrofit2.http.OPTIONS
import retrofit2.http.POST
import retrofit2.http.Query

/**
* Endpoints tied to the websocket connection, which stay on v1. `/sync` returns events in the socket's encoding, so it
* moves together with the socket; the `/connect` warm-up has no v2 route (the v2 socket is `/api/v2/connect`).
*/
@AuthenticatedApi
internal interface SyncApi {

@OPTIONS("/connect")
fun warmUp(): RetrofitCall<ResponseBody>

@POST("/sync")
fun getSyncHistory(
@Body body: SyncHistoryRequest,
@Query(QueryParams.CONNECTION_ID) connectionId: String,
): RetrofitCall<SyncHistoryResponse>
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ import io.getstream.chat.android.client.api2.endpoint.PollsApi
import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi
import io.getstream.chat.android.client.api2.endpoint.RemindersApi
import io.getstream.chat.android.client.api2.endpoint.RoleApi
import io.getstream.chat.android.client.api2.endpoint.SyncApi
import io.getstream.chat.android.client.api2.endpoint.ThreadsApi
import io.getstream.chat.android.client.api2.endpoint.UserApi
import io.getstream.chat.android.client.api2.endpoint.UserGroupApi
Expand Down Expand Up @@ -351,7 +352,8 @@ constructor(
buildRetrofitApi<ChannelApi>(),
buildV2RetrofitApi<DeviceApi>(),
buildRetrofitApi<ModerationApi>(),
buildRetrofitApi<GeneralApi>(),
buildV2RetrofitApi<GeneralApi>(),
buildRetrofitApi<SyncApi>(),
buildV2RetrofitApi<ConfigApi>(),
buildFileDownloadApi(),
buildV2RetrofitApi<OpenGraphApi>(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import io.getstream.chat.android.client.api2.endpoint.PollsApi
import io.getstream.chat.android.client.api2.endpoint.PushPreferencesApi
import io.getstream.chat.android.client.api2.endpoint.RemindersApi
import io.getstream.chat.android.client.api2.endpoint.RoleApi
import io.getstream.chat.android.client.api2.endpoint.SyncApi
import io.getstream.chat.android.client.api2.endpoint.ThreadsApi
import io.getstream.chat.android.client.api2.endpoint.UserApi
import io.getstream.chat.android.client.api2.endpoint.UserGroupApi
Expand Down Expand Up @@ -2650,10 +2651,10 @@ internal class MoshiChatApiTest {
@MethodSource("io.getstream.chat.android.client.api2.MoshiChatApiTestArguments#getSyncHistoryInput")
fun testGetSyncHistory(call: RetrofitCall<SyncHistoryResponse>, expected: KClass<*>) = runTest {
// given
val api = mock<GeneralApi>()
val api = mock<SyncApi>()
whenever(api.getSyncHistory(any(), any())).doReturn(call)
val sut = Fixture()
.withGeneralApi(api)
.withSyncApi(api)
.get()
// when
val userId = randomString()
Expand Down Expand Up @@ -3502,10 +3503,10 @@ internal class MoshiChatApiTest {
@Test
fun testWarmUp() = runTest {
// given
val api = mock<GeneralApi>()
val api = mock<SyncApi>()
whenever(api.warmUp()).doReturn(RetroSuccess(randomString().toResponseBody()).toRetrofitCall())
val sut = Fixture()
.withGeneralApi(api)
.withSyncApi(api)
.get()
// when
sut.warmUp()
Expand Down Expand Up @@ -3762,6 +3763,7 @@ internal class MoshiChatApiTest {
private var deviceApi: DeviceApi = mock()
private var moderationApi: ModerationApi = mock()
private var generalApi: GeneralApi = mock()
private var syncApi: SyncApi = mock()
private var configApi: ConfigApi = mock()
private var fileDownloadApi: FileDownloadApi = mock()
private var ogApi: OpenGraphApi = mock()
Expand Down Expand Up @@ -3807,6 +3809,10 @@ internal class MoshiChatApiTest {
this.generalApi = generalApi
}

fun withSyncApi(syncApi: SyncApi) = apply {
this.syncApi = syncApi
}

fun withConfigApi(configApi: ConfigApi) = apply {
this.configApi = configApi
}
Expand Down Expand Up @@ -3871,6 +3877,7 @@ internal class MoshiChatApiTest {
deviceApi = deviceApi,
moderationApi = moderationApi,
generalApi = generalApi,
syncApi = syncApi,
configApi = configApi,
fileDownloadApi = fileDownloadApi,
ogApi = ogApi,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
/*
* Copyright (c) 2014-2026 Stream.io Inc. All rights reserved.
*
* Licensed under the Stream License;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://github.com/GetStream/stream-chat-android/blob/main/LICENSE
*
* 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 io.getstream.chat.android.network.infrastructure

import io.getstream.chat.android.client.api2.mapping.DomainMapping
import io.getstream.chat.android.client.parser2.ParserFactory
import io.getstream.chat.android.models.NoOpChannelTransformer
import io.getstream.chat.android.models.NoOpMessageTransformer
import io.getstream.chat.android.models.NoOpUserTransformer
import io.getstream.chat.android.network.models.ChannelMemberRequest
import io.getstream.chat.android.network.models.MembersResponse
import io.getstream.chat.android.network.models.QueryMembersPayload
import io.getstream.chat.android.network.models.SearchPayload
import io.getstream.chat.android.network.models.SearchResponse
import io.getstream.chat.android.network.models.SortParamRequest
import io.getstream.chat.android.network.models.WrappedUnreadCountsResponse
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
import java.util.Date

/**
* Search, members and unread counts parse their recorded v2 responses through the generated [Serializer] alone. Their
* options travel as the JSON `payload` query parameter on both versions, with the same content except the custom data
* of the members in a members query, which v2 keeps nested.
*/
internal class V2LaneBatchSixTest {

private val v1Parser = ParserFactory.createMoshiChatParser()
private val mapping = DomainMapping(
currentUserIdProvider = { "filip" },
channelTransformer = NoOpChannelTransformer,
messageTransformer = NoOpMessageTransformer,
userTransformer = NoOpUserTransformer,
)

private inline fun <reified T> parse(json: String): T = Serializer.moshi.adapter(T::class.java).fromJson(json)!!

private inline fun <reified T> v2Body(request: T): String = Serializer.moshi.adapter(T::class.java).toJson(request)

@Test
fun `A search result parses its nested custom data and integer dates from v2`() {
val message = with(mapping) { parse<SearchResponse>(SEARCH).results.single().message!!.toDomain() }

assertEquals(mapOf("s_probe" to "nested"), message.extraData)
assertEquals(Date(1791798765916), message.createdAt)
}

@Test
fun `A member parses its user's nested custom data from v2`() {
val member = with(mapping) { parse<MembersResponse>(MEMBERS).members.single().toDomain() }

assertEquals(mapOf("birthland" to "Tatooine"), member.user.extraData)
assertEquals(Date(1634854188381), member.createdAt)
}

@Test
fun `Unread counts parse their integer dates from v2`() {
val counts = with(mapping) { parse<WrappedUnreadCountsResponse>(UNREAD).toDomain() }

assertEquals(2, counts.threadsCount)
assertEquals(Date(1789047633686), counts.threads.single().lastRead)
}

@Test
fun `Search and members payloads are the same on v2 as on v1`() {
val sort = listOf(SortParamRequest(field = "created_at", direction = -1))
val since = mapOf("created_at" to mapOf("\$gt" to Date(1791798765916)))
assertSameBody(
SearchPayload(
filterConditions = mapOf("cid" to "messaging:sample-app-channel-8"),
messageFilterConditions = since,
query = "zebra",
limit = 2,
sort = sort,
),
)
assertSameBody(
QueryMembersPayload(type = "messaging", id = "a", filterConditions = since, limit = 2, sort = sort),
)
}

@Test
fun `A members query nests the members' custom data on v2`() {
val payload = QueryMembersPayload(
type = "messaging",
members = listOf(ChannelMemberRequest(userId = "amit", custom = mapOf("k" to "v"))),
)

assertEquals(
"""{"type":"messaging","filter_conditions":{},"members":[{"user_id":"amit","custom":{"k":"v"}}],"sort":[]}""",
v2Body(payload),
)
}

private inline fun <reified T : Any> assertSameBody(request: T) {
assertEquals(v1Parser.toJson(request), v2Body(request))
}

private companion object {
private const val USER = """{"id":"filip","name":"Chewbacca","custom":{},"language":"","role":"user",
"teams":[],"created_at":1634853490403855000,"updated_at":1791791284528654000,"banned":false,
"online":true,"blocked_user_ids":[]}"""

private const val SEARCH = """{"results":[{"message":{"id":"fc511a6e-2e8e-4d2c-b0a8-46b52266c39b",
"text":"v2capsearch zebra","html":"<p>v2capsearch zebra</p>\n","type":"regular","user":$USER,
"attachments":[],"latest_reactions":[],"own_reactions":[],"reaction_counts":{},"reaction_scores":{},
"reply_count":0,"deleted_reply_count":0,"cid":"messaging:sample-app-channel-8",
"created_at":1791798765916694000,"updated_at":1791798765916694000,"custom":{"s_probe":"nested"},
"shadowed":false,"mentioned_users":[],"mentioned_channel":false,"mentioned_here":false,"silent":false,
"pinned":false,"restricted_visibility":[]}}],"duration":"45.99ms"}"""

private const val MEMBERS = """{"members":[{"user_id":"amit","user":{"id":"amit","name":"Leia Organa",
"custom":{"birthland":"Tatooine"},"language":"hi","role":"user","teams":[],
"created_at":1634853490411137000,"updated_at":1788527535982343000,"banned":false,"online":false,
"blocked_user_ids":[]},"custom":{},"created_at":1634854188381661000,"updated_at":1786086894668787000,
"banned":false,"shadow_banned":false,"role":"member","channel_role":"channel_member",
"notifications_muted":false}],"duration":"9.60ms"}"""

private const val UNREAD = """{"total_unread_count":0,"total_unread_threads_count":2,"channels":[],
"channel_type":[],"threads":[{"unread_count":1,"last_read":1789047633686986000,"last_read_message_id":"",
"parent_message_id":"2b1bbee3-eeb9-434c-ab78-9765abecc357"}],"duration":"7.50ms"}"""
}
}
Loading