Skip to content

Commit

Permalink
Merge branch 'main' into feature/room-ATTACH
Browse files Browse the repository at this point in the history
# Conflicts:
#	chat-android/src/main/java/com/ably/chat/RoomReactions.kt
  • Loading branch information
sacOO7 committed Nov 8, 2024
2 parents ce9c0ba + 4861812 commit b8c3cc7
Show file tree
Hide file tree
Showing 7 changed files with 227 additions and 18 deletions.
3 changes: 2 additions & 1 deletion chat-android/src/main/java/com/ably/chat/Room.kt
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,8 @@ internal class DefaultRoom(

override val reactions = DefaultRoomReactions(
roomId = roomId,
realtimeClient = realtimeClient,
clientId = realtimeClient.auth.clientId,
realtimeChannels = realtimeClient.channels,
)

override val occupancy = DefaultOccupancy(
Expand Down
48 changes: 44 additions & 4 deletions chat-android/src/main/java/com/ably/chat/RoomReactions.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

package com.ably.chat

import com.google.gson.JsonObject
import io.ably.lib.realtime.AblyRealtime
import io.ably.lib.types.AblyException
import io.ably.lib.types.ErrorInfo
import io.ably.lib.types.MessageExtras
import io.ably.lib.realtime.Channel as AblyRealtimeChannel

/**
Expand Down Expand Up @@ -100,25 +105,60 @@ data class SendReactionParams(

internal class DefaultRoomReactions(
roomId: String,
private val realtimeClient: RealtimeClient,
private val clientId: String,
realtimeChannels: AblyRealtime.Channels,
) : RoomReactions, ContributesToRoomLifecycle, ResolvedContributor {

private val roomReactionsChannelName = "$roomId::\$chat::\$reactions"

override val channel: AblyRealtimeChannel = realtimeClient.channels.get(roomReactionsChannelName, ChatChannelOptions())
override val channel: AblyRealtimeChannel = realtimeChannels.get(roomReactionsChannelName, ChatChannelOptions())

override val contributor: ContributesToRoomLifecycle = this

override val attachmentErrorCode: ErrorCodes = ErrorCodes.ReactionsAttachmentFailed

override val detachmentErrorCode: ErrorCodes = ErrorCodes.ReactionsDetachmentFailed

// (CHA-ER3) Ephemeral room reactions are sent to Ably via the Realtime connection via a send method.
// (CHA-ER3a) Reactions are sent on the channel using a message in a particular format - see spec for format.
override suspend fun send(params: SendReactionParams) {
TODO("Not yet implemented")
val pubSubMessage = PubSubMessage().apply {
name = RoomReactionEventType.Reaction.eventName
data = JsonObject().apply {
addProperty("type", params.type)
params.metadata?.let { add("metadata", it.toJson()) }
}
params.headers?.let {
extras = MessageExtras(
JsonObject().apply {
add("headers", it.toJson())
},
)
}
}
channel.publishCoroutine(pubSubMessage)
}

override fun subscribe(listener: RoomReactions.Listener): Subscription {
TODO("Not yet implemented")
val messageListener = PubSubMessageListener {
val pubSubMessage = it ?: throw AblyException.fromErrorInfo(
ErrorInfo("Got empty pubsub channel message", HttpStatusCodes.BadRequest, ErrorCodes.BadRequest.errorCode),
)
val data = pubSubMessage.data as? JsonObject ?: throw AblyException.fromErrorInfo(
ErrorInfo("Unrecognized Pub/Sub channel's message for `roomReaction` event", HttpStatusCodes.InternalServerError),
)
val reaction = Reaction(
type = data.requireString("type"),
createdAt = pubSubMessage.timestamp,
clientId = pubSubMessage.clientId,
metadata = data.get("metadata")?.toMap() ?: mapOf(),
headers = pubSubMessage.extras?.asJsonObject()?.get("headers")?.toMap() ?: mapOf(),
isSelf = pubSubMessage.clientId == clientId,
)
listener.onReaction(reaction)
}
channel.subscribe(RoomReactionEventType.Reaction.eventName, messageListener)
return Subscription { channel.unsubscribe(RoomReactionEventType.Reaction.eventName, messageListener) }
}

override fun onDiscontinuity(listener: EmitsDiscontinuities.Listener): Subscription {
Expand Down
15 changes: 15 additions & 0 deletions chat-android/src/main/java/com/ably/chat/Utils.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,21 @@ suspend fun Channel.detachCoroutine() = suspendCoroutine { continuation ->
})
}

suspend fun Channel.publishCoroutine(message: PubSubMessage) = suspendCoroutine { continuation ->
publish(
message,
object : CompletionListener {
override fun onSuccess() {
continuation.resume(Unit)
}

override fun onError(reason: ErrorInfo?) {
continuation.resumeWithException(AblyException.fromErrorInfo(reason))
}
},
)
}

@Suppress("FunctionName")
fun ChatChannelOptions(init: (ChannelOptions.() -> Unit)? = null): ChannelOptions {
val options = ChannelOptions()
Expand Down
109 changes: 109 additions & 0 deletions chat-android/src/test/java/com/ably/chat/RoomReactionsTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package com.ably.chat

import com.google.gson.JsonObject
import io.ably.lib.realtime.AblyRealtime.Channels
import io.ably.lib.realtime.Channel
import io.ably.lib.realtime.buildRealtimeChannel
import io.ably.lib.types.MessageExtras
import io.mockk.every
import io.mockk.mockk
import io.mockk.slot
import io.mockk.spyk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test

class RoomReactionsTest {
private val realtimeChannels = mockk<Channels>(relaxed = true)
private val realtimeChannel = spyk<Channel>(buildRealtimeChannel("room1::\$chat::\$reactions"))
private lateinit var roomReactions: DefaultRoomReactions

@Before
fun setUp() {
every { realtimeChannels.get(any(), any()) } answers {
val channelName = firstArg<String>()
if (channelName == "room1::\$chat::\$reactions") {
realtimeChannel
} else {
buildRealtimeChannel(channelName)
}
}

roomReactions = DefaultRoomReactions(
roomId = "room1",
clientId = "client1",
realtimeChannels = realtimeChannels,
)
}

/**
* @spec CHA-ER1
*/
@Test
fun `channel name is set according to the spec`() = runTest {
val roomReactions = DefaultRoomReactions(
roomId = "foo",
clientId = "client1",
realtimeChannels = realtimeChannels,
)

assertEquals(
"foo::\$chat::\$reactions",
roomReactions.channel.name,
)
}

/**
* @spec CHA-ER3a
*/
@Test
fun `should be able to subscribe to incoming reactions`() = runTest {
val pubSubMessageListenerSlot = slot<PubSubMessageListener>()

every { realtimeChannel.subscribe("roomReaction", capture(pubSubMessageListenerSlot)) } returns Unit

val deferredValue = DeferredValue<Reaction>()

roomReactions.subscribe {
deferredValue.completeWith(it)
}

verify { realtimeChannel.subscribe("roomReaction", any()) }

pubSubMessageListenerSlot.captured.onMessage(
PubSubMessage().apply {
data = JsonObject().apply {
addProperty("type", "like")
}
clientId = "clientId"
timestamp = 1000L
extras = MessageExtras(
JsonObject().apply {
add(
"headers",
JsonObject().apply {
addProperty("foo", "bar")
},
)
},
)
},
)

val reaction = deferredValue.await()

assertEquals(
Reaction(
type = "like",
createdAt = 1000L,
clientId = "clientId",
metadata = mapOf(),
headers = mapOf("foo" to "bar"),
isSelf = false,
),
reaction,
)
}
}
1 change: 1 addition & 0 deletions example/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ dependencies {
implementation(libs.androidx.ui.graphics)
implementation(libs.androidx.ui.tooling.preview)
implementation(libs.androidx.material3)
implementation(libs.konfetti.compose)
testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core)
Expand Down
67 changes: 54 additions & 13 deletions example/src/main/java/com/ably/chat/example/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import com.ably.chat.ChatClient
import com.ably.chat.Message
import com.ably.chat.RealtimeClient
import com.ably.chat.SendMessageParams
import com.ably.chat.SendReactionParams
import com.ably.chat.example.ui.theme.AblyChatExampleTheme
import io.ably.lib.types.ClientOptions
import java.util.UUID
Expand Down Expand Up @@ -72,17 +73,30 @@ class MainActivity : ComponentActivity() {
}
}

@SuppressWarnings("LongMethod")
@Composable
fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) {
var messageText by remember { mutableStateOf(TextFieldValue("")) }
var sending by remember { mutableStateOf(false) }
var messages by remember { mutableStateOf(listOf<Message>()) }
val listState = rememberLazyListState()
val coroutineScope = rememberCoroutineScope()
var receivedReactions by remember { mutableStateOf<List<String>>(listOf()) }

val roomId = "my-room"
val room = chatClient.rooms.get(roomId)

DisposableEffect(Unit) {
coroutineScope.launch {
room.attach()
}
onDispose {
coroutineScope.launch {
room.detach()
}
}
}

DisposableEffect(Unit) {
val subscription = room.messages.subscribe {
messages += it.message
Expand All @@ -101,6 +115,16 @@ fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) {
}
}

DisposableEffect(Unit) {
val subscription = room.reactions.subscribe {
receivedReactions += it.type
}

onDispose {
subscription.unsubscribe()
}
}

Column(
modifier = modifier.fillMaxSize(),
verticalArrangement = Arrangement.SpaceBetween,
Expand All @@ -119,17 +143,26 @@ fun Chat(chatClient: ChatClient, modifier: Modifier = Modifier) {
sending = sending,
messageInput = messageText,
onMessageChange = { messageText = it },
) {
sending = true
coroutineScope.launch {
room.messages.send(
SendMessageParams(
text = messageText.text,
),
)
messageText = TextFieldValue("")
sending = false
}
onSendClick = {
sending = true
coroutineScope.launch {
room.messages.send(
SendMessageParams(
text = messageText.text,
),
)
messageText = TextFieldValue("")
sending = false
}
},
onReactionClick = {
coroutineScope.launch {
room.reactions.send(SendReactionParams(type = "\uD83D\uDC4D"))
}
},
)
if (receivedReactions.isNotEmpty()) {
Text("Received reactions: ${receivedReactions.joinToString()}", modifier = Modifier.padding(16.dp))
}
}
}
Expand Down Expand Up @@ -164,6 +197,7 @@ fun ChatInputField(
messageInput: TextFieldValue,
onMessageChange: (TextFieldValue) -> Unit,
onSendClick: () -> Unit,
onReactionClick: () -> Unit,
) {
Row(
modifier = Modifier
Expand All @@ -181,8 +215,14 @@ fun ChatInputField(
.background(Color.White),
placeholder = { Text("Type a message...") },
)
Button(enabled = !sending, onClick = onSendClick) {
Text("Send")
if (messageInput.text.isNotEmpty()) {
Button(enabled = !sending, onClick = onSendClick) {
Text("Send")
}
} else {
Button(onClick = onReactionClick) {
Text("\uD83D\uDC4D")
}
}
}
}
Expand Down Expand Up @@ -214,6 +254,7 @@ fun ChatInputPreview() {
messageInput = TextFieldValue(""),
onMessageChange = {},
onSendClick = {},
onReactionClick = {},
)
}
}
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ ably = "1.2.44"
junit = "4.13.2"
agp = "8.5.2"
detekt = "1.23.6"
konfetti-compose = "2.0.4"
kotlin = "2.0.10"
androidx-test = "1.6.1"
androidx-junit = "1.2.1"
Expand Down Expand Up @@ -43,6 +44,7 @@ androidx-material3 = { group = "androidx.compose.material3", name = "material3"

gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }

konfetti-compose = { module = "nl.dionsegijn:konfetti-compose", version.ref = "konfetti-compose" }
mockk = { group = "io.mockk", name = "mockk", version.ref = "mockk" }

coroutine-core = { group = "org.jetbrains.kotlinx", name = "kotlinx-coroutines-core", version.ref = "coroutine" }
Expand Down

0 comments on commit b8c3cc7

Please sign in to comment.