diff --git a/README.md b/README.md index 4b5db34..403f80a 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,10 @@ The app includes five navigable examples: default rows and composer reply to any message, edit and delete the user's own, and go to a quoted message wherever it is in the history. +The 0.10.0 showcase seeds a reaction chip, lets Maya toggle an emoji, and opens +the reactor list. The Quoted screen renders the same controls in its custom +message row with `ConvoKitReactionBar`; Live uses the SDK-backed defaults. + The showcase screens pass real `InboxSummary` values to the controlled `ConvoKitConversationListView` through its `summaries` and `currentUserId` parameters, so the default row derives the preview line, the `activityAt` time @@ -84,12 +88,12 @@ dependencyResolutionManagement { } dependencies { - implementation("app.convokit:convokit-android-ui:0.8.0") + implementation("app.convokit:convokit-android-ui:0.10.0") } ``` The UI artifact exposes the compatible core SDK transitively. An application -may also declare `app.convokit:convokit-android:0.8.0` explicitly. +may also declare `app.convokit:convokit-android:0.10.0` explicitly. ## Live room example diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 66830af..e3c6bc3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -35,7 +35,7 @@ kotlin { } dependencies { - implementation("app.convokit:convokit-android-ui:0.9.0") + implementation("app.convokit:convokit-android-ui:0.10.0") implementation(platform("androidx.compose:compose-bom:2025.05.01")) implementation("androidx.activity:activity-compose:1.10.1") implementation("androidx.compose.material3:material3") diff --git a/app/src/main/kotlin/app/convokit/ui/example/ShowcaseScreen.kt b/app/src/main/kotlin/app/convokit/ui/example/ShowcaseScreen.kt index c1525da..de73656 100644 --- a/app/src/main/kotlin/app/convokit/ui/example/ShowcaseScreen.kt +++ b/app/src/main/kotlin/app/convokit/ui/example/ShowcaseScreen.kt @@ -39,12 +39,17 @@ import androidx.compose.ui.unit.dp import app.convokit.sdk.Conversation import app.convokit.sdk.InboxSummary import app.convokit.sdk.Message +import app.convokit.sdk.MessageReactionSummary +import app.convokit.sdk.ReactionSummary +import app.convokit.sdk.ReactionUser +import app.convokit.sdk.ReactionUsersPage import app.convokit.ui.ConvoKitReplyPreview import app.convokit.ui.ConvoKitWindowMode import app.convokit.ui.components.ConvoKitConversationListView import app.convokit.ui.components.ConvoKitConversationView import app.convokit.ui.components.ConvoKitImageLoader import app.convokit.ui.components.ConvoKitMessageItemScope +import app.convokit.ui.components.ConvoKitReactionBar import app.convokit.ui.components.ConversationHeaderContent import app.convokit.ui.components.DefaultComposer import app.convokit.ui.components.DefaultMediaBlock @@ -83,6 +88,13 @@ internal fun ShowcaseScreen(variant: ShowcaseVariant, systemPadding: PaddingValu // is set the package shows the banner, prefills the composer silently and routes the one // submit handed to the default and custom composers to `onSaveEdit` instead of `onSendMessage`. var room by remember(variant) { mutableStateOf(ShowcaseRoom()) } + var reactionSummaries by remember(variant) { mutableStateOf>(mapOf( + showcaseMessages.last().id to MessageReactionSummary( + showcaseMessages.last().id, + listOf(ReactionSummary("❤️", 2, false)), + false, + ), + )) } // The highlight belongs to whoever owns the state, so a controlled host clears it itself; the // package's own controller clears its own after about two seconds. LaunchedEffect(room.highlightedMessageId) { @@ -202,6 +214,27 @@ internal fun ShowcaseScreen(variant: ShowcaseVariant, systemPadding: PaddingValu onReplyToMessage = { room = room.startReplying(it) }, onCancelReply = { room = room.cancelReplying() }, replyPreviewByMessageId = room.replyPreviews, + reactionSummaries = reactionSummaries, + onToggleReaction = { message, emoji -> + val previous = reactionSummaries[message.id]?.reactions.orEmpty() + val found = previous.find { it.emoji == emoji } + val count = (found?.count ?: 0) + if (found?.reactedByMe == true) -1 else 1 + val updated = previous.filterNot { it.emoji == emoji } + + if (count > 0) listOf(ReactionSummary(emoji, count, found?.reactedByMe != true)) else emptyList() + reactionSummaries = reactionSummaries + (message.id to MessageReactionSummary(message.id, updated, false)) + true + }, + onListReactionUsers = { message, emoji, cursor -> + ReactionUsersPage( + if (cursor != null) emptyList() else buildList { + if (reactionSummaries[message.id]?.reactions?.any { it.emoji == emoji && it.reactedByMe } == true) { + add(ReactionUser(currentUserId, "Maya", null, showcaseInstant(40))) + } + add(ReactionUser("alex", "Alex", null, showcaseInstant(40))) + }, + null, + ) + }, onJumpToMessage = { room = room.jumpTo(it) }, highlightedMessageId = room.highlightedMessageId, hasNewerMessages = room.hasNewerMessages, @@ -407,6 +440,9 @@ private fun QuotedMessage(scope: ConvoKitMessageItemScope) { } scope.message.text?.let { Text(it, style = MaterialTheme.typography.bodySmall) } scope.message.media.forEach { media -> DefaultMediaBlock(media, scope.message) } + scope.listReactionUsers?.let { listUsers -> + ConvoKitReactionBar(scope.reactionSummary, scope.toggleReaction, listUsers) + } Row(verticalAlignment = Alignment.CenterVertically) { if (mine) { Text(