Skip to content

Commit 328be26

Browse files
committed
feat: extension for integration with Jetpack Compose
Added `com.ably.chat:chat-extensions-compose` package with extension functions for better integration with Jetpack Compose.
1 parent 802c95f commit 328be26

File tree

26 files changed

+915
-137
lines changed

26 files changed

+915
-137
lines changed

chat-android/src/main/java/com/ably/chat/Connection.kt

+2-2
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,12 @@ public data class ConnectionStatusChange(
7272
* An error that provides a reason why the connection has
7373
* entered the new status, if applicable.
7474
*/
75-
val error: ErrorInfo?,
75+
val error: ErrorInfo? = null,
7676

7777
/**
7878
* The time in milliseconds that the client will wait before attempting to reconnect.
7979
*/
80-
val retryIn: Long?,
80+
val retryIn: Long? = null,
8181
)
8282

8383
/**

chat-android/src/main/java/com/ably/chat/Typing.kt

+1
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ internal class DefaultTyping(
189189
logger.trace("DefaultTyping.stop()")
190190
typingScope.launch {
191191
typingJob?.cancel()
192+
typingJob = null
192193
room.ensureAttached(logger) // CHA-T5e, CHA-T5c, CHA-T5d
193194
channelWrapper.presence.leaveClientCoroutine(room.clientId)
194195
}.join()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
@file:Suppress("Filename")
2+
3+
package com.ably.chat.annotations
4+
5+
/**
6+
* API marked with this annotation is internal, and it is not intended to be used outside Ably.
7+
* It could be modified or removed without any notice. Using it outside Ably could cause undefined behaviour and/or
8+
* any unexpected effects.
9+
*/
10+
@MustBeDocumented
11+
@RequiresOptIn(
12+
level = RequiresOptIn.Level.WARNING,
13+
message = "This API is experimental in Ably Chat SDK. Roughly speaking, there is a chance " +
14+
"that those declarations will be deprecated in the near future or the semantics of " +
15+
"their behavior may change in some way that may break some code",
16+
)
17+
@Target(
18+
AnnotationTarget.CLASS,
19+
AnnotationTarget.TYPEALIAS,
20+
AnnotationTarget.FUNCTION,
21+
AnnotationTarget.PROPERTY,
22+
AnnotationTarget.FIELD,
23+
AnnotationTarget.CONSTRUCTOR,
24+
AnnotationTarget.PROPERTY_SETTER,
25+
)
26+
public annotation class ExperimentalChatApi
+64
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
plugins {
2+
alias(libs.plugins.android.library)
3+
alias(libs.plugins.android.kotlin)
4+
alias(libs.plugins.maven.publish)
5+
alias(libs.plugins.compose.compiler)
6+
alias(libs.plugins.dokka)
7+
}
8+
9+
val version = property("VERSION_NAME")
10+
11+
android {
12+
namespace = "com.ably.chat.extensions.compose"
13+
compileSdk = 34
14+
defaultConfig {
15+
minSdk = 24
16+
consumerProguardFiles("consumer-rules.pro")
17+
}
18+
19+
buildTypes {
20+
release {
21+
isMinifyEnabled = false
22+
23+
proguardFiles(
24+
getDefaultProguardFile("proguard-android-optimize.txt"),
25+
"proguard-rules.pro",
26+
)
27+
}
28+
}
29+
30+
buildFeatures {
31+
compose = true
32+
}
33+
34+
compileOptions {
35+
sourceCompatibility = JavaVersion.VERSION_1_8
36+
targetCompatibility = JavaVersion.VERSION_1_8
37+
}
38+
39+
kotlinOptions {
40+
jvmTarget = "1.8"
41+
}
42+
43+
@Suppress("UnstableApiUsage")
44+
testOptions {
45+
unitTests {
46+
isReturnDefaultValues = true
47+
}
48+
}
49+
}
50+
51+
kotlin {
52+
explicitApi()
53+
}
54+
55+
dependencies {
56+
implementation(project(":chat-android"))
57+
implementation(libs.androidx.activity.compose)
58+
implementation(libs.androidx.compose.foundation)
59+
testImplementation(libs.junit)
60+
testImplementation(libs.mockk)
61+
testImplementation(libs.coroutine.test)
62+
testImplementation(libs.turbine)
63+
testImplementation(libs.molecule)
64+
}

chat-extensions-compose/consumer-rules.pro

Whitespace-only changes.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
POM_ARTIFACT_ID=chat-extensions-compose
2+
POM_NAME=Ably Chat SDK library extensions for Jetpack Compose
3+
POM_DESCRIPTION=Ably Chat SDK library extensions for Jetpack Compose.
4+
POM_PACKAGING=aar
+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Add project specific ProGuard rules here.
2+
# You can control the set of applied configuration files using the
3+
# proguardFiles setting in build.gradle.
4+
#
5+
# For more details, see
6+
# http://developer.android.com/guide/developing/tools/proguard.html
7+
8+
# If your project uses WebView with JS, uncomment the following
9+
# and specify the fully qualified class name to the JavaScript interface
10+
# class:
11+
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12+
# public *;
13+
#}
14+
15+
# Uncomment this to preserve the line number information for
16+
# debugging stack traces.
17+
#-keepattributes SourceFile,LineNumberTable
18+
19+
# If you keep the line number information, uncomment this to
20+
# hide the original source file name.
21+
#-renamesourcefileattribute SourceFile
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
</manifest>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.ably.chat.extensions.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import com.ably.chat.Connection
10+
import com.ably.chat.ConnectionStatus
11+
import com.ably.chat.annotations.ExperimentalChatApi
12+
import com.ably.chat.statusAsFlow
13+
14+
/**
15+
* @return active connection status
16+
*/
17+
@ExperimentalChatApi
18+
@Composable
19+
public fun Connection.asComposable(): ConnectionStatus {
20+
var connectionStatus by remember(this) { mutableStateOf(status) }
21+
22+
LaunchedEffect(this) {
23+
statusAsFlow().collect { connectionStatus = it.current }
24+
}
25+
26+
return connectionStatus
27+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package com.ably.chat.extensions.compose
2+
3+
import androidx.compose.foundation.lazy.LazyListState
4+
import androidx.compose.foundation.lazy.rememberLazyListState
5+
import androidx.compose.runtime.Composable
6+
import androidx.compose.runtime.DisposableEffect
7+
import androidx.compose.runtime.getValue
8+
import androidx.compose.runtime.mutableStateOf
9+
import androidx.compose.runtime.remember
10+
import androidx.compose.runtime.rememberCoroutineScope
11+
import androidx.compose.runtime.setValue
12+
import com.ably.chat.Message
13+
import com.ably.chat.MessageEventType
14+
import com.ably.chat.Messages
15+
import com.ably.chat.PaginatedResult
16+
import com.ably.chat.annotations.ExperimentalChatApi
17+
import kotlinx.coroutines.launch
18+
19+
/**
20+
* @return paginated messages
21+
*/
22+
@ExperimentalChatApi
23+
@Composable
24+
public fun Messages.asComposable(): PaginatedMessages {
25+
val listState = rememberLazyListState()
26+
var loaded by remember(this) { mutableStateOf(listOf<Message>()) }
27+
var loading by remember(this) { mutableStateOf(true) }
28+
var lastReceivedPaginatedResult: PaginatedResult<Message>? by remember(this) { mutableStateOf(null) }
29+
30+
val coroutineScope = rememberCoroutineScope()
31+
32+
DisposableEffect(this) {
33+
val subscription = subscribe { event ->
34+
when (event.type) {
35+
MessageEventType.Created -> {
36+
loaded += event.message
37+
coroutineScope.launch {
38+
listState.animateScrollToItem(loaded.size - 1)
39+
}
40+
}
41+
42+
MessageEventType.Updated -> loaded = loaded.map {
43+
if (it.serial != event.message.serial) it else event.message
44+
}
45+
46+
MessageEventType.Deleted -> loaded = loaded.filter {
47+
it.serial != event.message.serial
48+
}
49+
}
50+
}
51+
52+
coroutineScope.launch {
53+
loading = true
54+
val receivedPaginatedResult = subscription.getPreviousMessages()
55+
lastReceivedPaginatedResult = receivedPaginatedResult
56+
loaded = receivedPaginatedResult.items.reversed() + loaded
57+
loading = false
58+
if (loaded.isNotEmpty()) listState.animateScrollToItem(loaded.size - 1)
59+
}
60+
61+
onDispose {
62+
subscription.unsubscribe()
63+
}
64+
}
65+
66+
return DefaultPaginatedMessages(
67+
loaded = loaded,
68+
listState = listState,
69+
loading = loading,
70+
hasMore = lastReceivedPaginatedResult?.hasNext() ?: true,
71+
)
72+
}
73+
74+
public interface PaginatedMessages {
75+
public val loaded: List<Message>
76+
public val listState: LazyListState
77+
public val loading: Boolean
78+
public val hasMore: Boolean
79+
}
80+
81+
private data class DefaultPaginatedMessages(
82+
override val loaded: List<Message>,
83+
override val listState: LazyListState,
84+
override val loading: Boolean,
85+
override val hasMore: Boolean,
86+
) : PaginatedMessages
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.ably.chat.extensions.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import com.ably.chat.Occupancy
10+
import com.ably.chat.annotations.ExperimentalChatApi
11+
import com.ably.chat.asFlow
12+
import kotlinx.coroutines.cancelAndJoin
13+
import kotlinx.coroutines.launch
14+
15+
public data class CurrentOccupancy(
16+
val connections: Int = 0,
17+
val presenceMembers: Int = 0,
18+
)
19+
20+
/**
21+
* @return current occupancy
22+
*/
23+
@ExperimentalChatApi
24+
@Composable
25+
public fun Occupancy.asComposable(): CurrentOccupancy {
26+
var currentOccupancy by remember(this) { mutableStateOf(CurrentOccupancy()) }
27+
28+
LaunchedEffect(this) {
29+
val initialOccupancyGet = launch {
30+
val occupancyEvent = get()
31+
currentOccupancy = CurrentOccupancy(
32+
connections = occupancyEvent.connections,
33+
presenceMembers = occupancyEvent.presenceMembers,
34+
)
35+
}
36+
asFlow().collect {
37+
if (initialOccupancyGet.isActive) initialOccupancyGet.cancelAndJoin()
38+
currentOccupancy = CurrentOccupancy(
39+
connections = it.connections,
40+
presenceMembers = it.presenceMembers,
41+
)
42+
}
43+
}
44+
45+
return currentOccupancy
46+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
package com.ably.chat.extensions.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import com.ably.chat.Presence
10+
import com.ably.chat.PresenceMember
11+
import com.ably.chat.annotations.ExperimentalChatApi
12+
import com.ably.chat.asFlow
13+
import kotlinx.coroutines.cancelAndJoin
14+
import kotlinx.coroutines.launch
15+
16+
/**
17+
* @return currently present members
18+
*/
19+
@ExperimentalChatApi
20+
@Composable
21+
public fun Presence.asComposable(): List<PresenceMember> {
22+
var presenceMembers by remember(this) { mutableStateOf(emptyList<PresenceMember>()) }
23+
24+
LaunchedEffect(this) {
25+
val initialPresenceGet = launch {
26+
presenceMembers = get()
27+
}
28+
asFlow().collect {
29+
if (initialPresenceGet.isActive) initialPresenceGet.cancelAndJoin()
30+
presenceMembers = get()
31+
}
32+
}
33+
34+
return presenceMembers
35+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
package com.ably.chat.extensions.compose
2+
3+
import androidx.compose.runtime.Composable
4+
import androidx.compose.runtime.LaunchedEffect
5+
import androidx.compose.runtime.getValue
6+
import androidx.compose.runtime.mutableStateOf
7+
import androidx.compose.runtime.remember
8+
import androidx.compose.runtime.setValue
9+
import com.ably.chat.Room
10+
import com.ably.chat.RoomStatus
11+
import com.ably.chat.annotations.ExperimentalChatApi
12+
import com.ably.chat.statusAsFlow
13+
14+
/**
15+
* @return room status
16+
*/
17+
@ExperimentalChatApi
18+
@Composable
19+
public fun Room.asComposable(): RoomStatus {
20+
var roomStatus by remember(this) { mutableStateOf(status) }
21+
22+
LaunchedEffect(this) {
23+
statusAsFlow().collect { roomStatus = it.current }
24+
}
25+
26+
return roomStatus
27+
}

0 commit comments

Comments
 (0)