better emoji reactions

This commit is contained in:
2026-03-02 22:21:23 +00:00
parent 2169f28632
commit 2b554dc227
6 changed files with 205 additions and 126 deletions

6
.idea/studiobot.xml generated Normal file
View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="StudioBotProjectSettings">
<option name="shareContext" value="OptedIn" />
</component>
</project>

161
AGENTS.md
View File

@@ -6,7 +6,7 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
**Package**: `com.example.fluffytrix` **Package**: `com.example.fluffytrix`
**Build system**: Gradle with KotlinDSL, version catalog at `gradle/libs.versions.toml` **Build system**: Gradle with KotlinDSL, version catalog at `gradle/libs.versions.toml`
**Container/DI**: Koin **DI**: Koin
**State management**: Jetpack Compose StateFlow, ViewModel **State management**: Jetpack Compose StateFlow, ViewModel
**UI framework**: Jetpack Compose with Material 3 (Dynamic Colors) **UI framework**: Jetpack Compose with Material 3 (Dynamic Colors)
**Protocol**: Trixnity SDK for Matrix **Protocol**: Trixnity SDK for Matrix
@@ -20,28 +20,22 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
```bash ```bash
./gradlew assembleDebug # Build debug APK (minified for performance) ./gradlew assembleDebug # Build debug APK (minified for performance)
./gradlew assembleRelease # Build release APK ./gradlew assembleRelease # Build release APK
./gradlew test # Run unit tests ./gradlew test # Run all unit tests
./gradlew connectedAndroidTest # Run instrumented tests on device/emulator ./gradlew connectedAndroidTest # Run instrumented tests on device
./gradlew testDebugUnitTest --tests "com.example.fluffytrix.ExampleUnitTest" # Run single test ./gradlew testDebugUnitTest --tests "com.example.fluffytrix.*" # Run single test class
./gradlew app:testDebugUnitTest --tests "LoginViewModelTest" # Run specific test
``` ```
**Notes**: - Debug builds use R8 with `isDebuggable = true` for balanced speed/debuggability
- Debug builds use R8 with `isDebuggable = true` but strip material-icons-extended and optimize Compose for performance
- Use `--tests` with Gradle test tasks to run a single test class
- Instrumented tests require a connected device or emulator - Instrumented tests require a connected device or emulator
--- ---
## Testing ## Testing
**Unit tests**: Located in `app/src/test/java/`, run with `./gradlew test` **Unit tests**: `app/src/test/java/`
**Instrumented tests**: Located in `app/src/androidTest/java/`, run with `./gradlew connectedAndroidTest` **Instrumented tests**: `app/src/androidTest/java/`
**Single test**: `./gradlew testDebugUnitTest --tests "com.example.fluffytrix.ui.main.LoginViewModelTest"`
**Single test execution**:
```bash
./gradlew testDebugUnitTest --tests "com.example.fluffytrix.*"
./gradlew app:testDebugUnitTest --tests "ExampleUnitTest"
```
--- ---
@@ -49,17 +43,17 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
### Kotlin Conventions ### Kotlin Conventions
- **Android Official Kotlin style** (`kotlin.code.style=official` in gradle.properties) - **Android Official Kotlin style** (`kotlin.code.style=official`)
- File naming: `PascalCase.kt` (e.g., `MainViewModel.kt`) - File naming: `PascalCase.kt` (e.g., `MainViewModel.kt`)
- Class naming: `PascalCase` (e.g., `MainViewModel`, `AuthRepository`) - Class naming: `PascalCase` (e.g., `AuthRepository`, `MainViewModel`)
- Function/property naming: `camelCase` (e.g., `sendMessage`, `selectedChannel`) - Function/property naming: `camelCase` (e.g., `sendMessage`, `selectedChannel`)
- Constant naming: `PascalCase` for top-level constants - Constants: `PascalCase` for top-level constants
- **Never use underscores in variable names** - **Never use underscores in variable names**
### Imports ### Imports
- Explicit imports only (no wildcard imports) - Explicit imports only (no wildcards)
- Group imports: Android/X → Kotlin → Javax/Java → Third-party → Same package - Group: Android/X → Kotlin → Javax/Java → Third-party → Same package
- Example: - Example:
```kotlin ```kotlin
import android.os.Bundle import android.os.Bundle
@@ -69,59 +63,37 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
import com.example.fluffytrix.ui.theme.FluffytrixTheme import com.example.fluffytrix.ui.theme.FluffytrixTheme
``` ```
### Types ### Types & Error Handling
- Prefer `val` over `var` (immutable data) - Prefer `val` over `var` (immutable data)
- Use `StateFlow<T>` for observable state in ViewModels - Use `StateFlow<T>` for observable ViewModel state, `Flow<T>` for read-only streams
- Use `Flow<T>` for read-only data streams - Use `suspend` for async operations, `Result<T>` for failing operations
- Use `suspend` functions for async operations - `try-catch` with `catch (_: Exception) { }` for graceful degradation in ViewModels
- Use `Result<T>` for operations that can fail - Use `?:` operators when appropriate, **never crash on recoverable errors**
### Error Handling ### Compose & ViewModels
- Prefer `try-catch` with silent failure or `?` operators where appropriate - Use `@Composable` for all UI functions; use `MaterialTheme` for consistent theming
- In ViewModels, use `catch (_: Exception) { }` or `?:` for graceful degradation - Discord-like layout: space sidebar → channel list → message area → member list
- Expose error state via `StateFlow<AuthState>` where users need feedback - Use `Modifier.padding()`, `wrapContentWidth()`, `fillMaxWidth()` appropriately
- **Never crash the app on recoverable errors** - Inject deps via constructor with Koin; use `viewModelScope` for coroutines
- Cache expensive operations (e.g., `messageCache`, `memberCache`)
### Compose UI ### Coroutines & Data Layer
- Use `@Composable` for all UI functions - `Dispatchers.Default` for CPU work, `Dispatchers.IO` for I/O operations
- Follow Discord-like layout: space sidebar → channel list → message area → member list - Cancel jobs on ViewModel cleanup: `job?.cancel()`
- Use `MaterialTheme` for consistent theming - Use `Room` for persistent storage; `DataStore Preferences` for small key-value data
- Prefer `Modifier.padding()` over nested `Box` with margins - Prefer Flow-based APIs; cache in ViewModels
- Use `wrapContentWidth()` and `fillMaxWidth()` appropriately
- For columnar layouts: `Column(horizontalAlignment = Alignment.CenterHorizontally, verticalArrangement = Arrangement.spacedBy(12.dp))`
### ViewModels
- Inject dependencies via constructor (Koin)
- Use `viewModelScope` for coroutines
- Expose state via `StateFlow` (e.g., `val messages: StateFlow<List<MessageItem>>`)
- Use `MutableStateFlow` for internal state, expose as read-only `StateFlow`
### Coroutines
- Use `Dispatchers.Default` for CPU-intensive work (parsing, filtering)
- Use `Dispatchers.IO` for file/network operations
- Always cancel jobs on ViewModel cleanup: `job?.cancel()`
- Use `withContext(Dispatchers.Default)` to switch threads explicitly
### Data Layer
- Use Room for persistent storage (Trixnity uses Room internally)
- Use DataStore Preferences for small key-value data
- Prefer Flow-based APIs for reactive data streams
- Cache expensive operations in ViewModel (e.g., `messageCache`, `memberCache`)
### Naming Conventions ### Naming Conventions
- State Flow properties: `_name` (private) / `name` (public) - State Flow: `_state` (private) / `state` (public)
- Repository class: `AuthService`, `AuthRepository` - Repositories: `AuthRepository`, `MessageRepository`
- ViewModel class: `MainViewModel`, `LoginViewModel` - ViewModels: `MainViewModel`, `LoginViewModel`
- UI composable: `MainScreen`, `ChannelList`, `MessageItem` - UI composables: `MainScreen`, `ChannelList`, `MessageItem`
- Model data class: `MessageItem`, `ChannelItem`, `SpaceItem` - Models: `MessageItem`, `ChannelItem`, `SpaceItem`
- Use `full` property for Matrix IDs (e.g., `userId.full`, `roomId.full`) - Matrix IDs: Use `RoomId`, `UserId` types; access `.full` for strings
--- ---
@@ -130,62 +102,19 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
**Layered Architecture**: **Layered Architecture**:
``` ```
ui/ — ViewModels, Screens, Navigation ui/ — ViewModels, Screens, Navigation
data/ — Repositories, local storage, models data/ — Repositories, storage, models
di/ — Koin modules di/ — Koin modules
ui/theme/ — Material 3 Theme (colors, typography) ui/theme/ — Material 3 Theme
``` ```
**Dependency Injection**: Koin with two modules: **Koin DI**:
- `appModule`: ViewModels (`viewModel { MainViewModel(...) }`) - `appModule`: ViewModel injection
- `dataModule`: singleton services (`single { AuthRepository(...) }`) - `dataModule`: Singleton repositories/UI Flow: NavHost in `MainActivity` with `FluffytrixTheme`, ViewModels expose StateFlow, screens observe with `collectAsState()`
**UI Flow**:
1. `FluffytrixNavigation` handles nav graph and session restoration
2. `MainActivity` hosts the NavHost with `FluffytrixTheme`
3. ViewModels expose state via StateFlow
4. Screens observe state with `collectAsState()`
--- ---
## Key Dependencies (from libs.versions.toml) ## Key Dependencies
- **Compose BOM**: `2025.06.00` - **Compose BOM**: `2025.06.00`, **Kotlin**: `2.2.10`, **AGP**: `9.0.1`
- **Kotlin**: `2.2.10` - **Koin**: `4.1.1`, **Trixnity**: `4.22.7`, **Ktor**: `3.3.0`
- **AGP**: `9.0.1` - **Coroutines**: `1.10.2`, **DataStore**: `1.1.7`, **Coil**: `3.2.0`
- **Koin**: `4.1.1`
- **Trixnity**: `4.22.7`
- **Ktor**: `3.3.0`
- **Coroutines**: `1.10.2`
- **DataStore**: `1.1.7`
- **Coil**: `3.2.0`
- **Media3**: `1.6.0`
---
## Special Notes
1. **Matrix IDs**: Use `RoomId`, `UserId` types from Trixnity; access `.full` for string representation
2. **MXC URLs**: Convert with `MxcUrlHelper.mxcToDownloadUrl()` and `mxcToThumbnailUrl()`
3. **Build Performance**: Debug builds use R8 minification with `isDebuggable = true` to balance speed and debuggability
4. **Channel Reordering**: Channel order is saved per-space in DataStore and restored on navigation
5. **Encrypted Rooms**: Handle decryption state—messages may appear as `Unable to decrypt` until keys arrive
6. **Theme**: Material 3 with Discord-inspired color scheme (primary: `#5865F2`)
---
## Running Lint/Checks
```bash
./gradlew lintDebug
./gradlew spotlessCheck
```
## CI/CD
- All builds use Gradle wrapper (`./gradlew`)
- No manual Gradle installation required (project uses Gradle 9.1.0)

View File

@@ -101,6 +101,9 @@ dependencies {
implementation(libs.markdown.renderer.code) implementation(libs.markdown.renderer.code)
implementation(libs.markdown.renderer.coil3) implementation(libs.markdown.renderer.coil3)
// Jetpack Emoji Picker
implementation(libs.emoji.picker)
testImplementation(libs.junit) testImplementation(libs.junit)
androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso.core) androidTestImplementation(libs.androidx.espresso.core)

View File

@@ -88,6 +88,7 @@ data class MessageItem(
val timestamp: Long, val timestamp: Long,
val replyTo: ReplyInfo? = null, val replyTo: ReplyInfo? = null,
val threadRootEventId: String? = null, val threadRootEventId: String? = null,
val reactions: Map<String, List<String>> = emptyMap(), // emoji -> list of full Matrix user IDs
) )
data class ThreadItem( data class ThreadItem(
@@ -752,6 +753,12 @@ class MainViewModel(
senderNameCache[localpart] = senderName senderNameCache[localpart] = senderName
if (senderAvatar != null) senderAvatarCache[localpart] = senderAvatar if (senderAvatar != null) senderAvatarCache[localpart] = senderAvatar
val reactions = if (content is TimelineItemContent.MsgLike) {
content.content.reactions
.filter { it.senders.isNotEmpty() }
.associate { reaction -> reaction.key to reaction.senders.map { it.senderId } }
} else emptyMap()
val msg = MessageItem( val msg = MessageItem(
eventId = eventId, eventId = eventId,
senderId = localpart, senderId = localpart,
@@ -761,6 +768,7 @@ class MainViewModel(
timestamp = eventItem.timestamp.toLong(), timestamp = eventItem.timestamp.toLong(),
replyTo = replyInfo, replyTo = replyInfo,
threadRootEventId = threadRootId, threadRootEventId = threadRootId,
reactions = reactions,
) )
ids.add(eventId) ids.add(eventId)

View File

@@ -3,6 +3,7 @@ package com.example.fluffytrix.ui.screens.main.components
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.FlowRow
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@@ -22,13 +23,15 @@ import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.People import androidx.compose.material.icons.filled.People
import androidx.compose.material.icons.filled.Tag import androidx.compose.material.icons.filled.Tag
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField import androidx.compose.material3.TextField
import androidx.compose.material3.TextFieldDefaults import androidx.compose.material3.TextFieldDefaults
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@@ -50,6 +53,7 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.compose.material3.Text
import android.net.Uri import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
@@ -67,6 +71,8 @@ import com.mikepenz.markdown.m3.markdownColor
import com.mikepenz.markdown.m3.markdownTypography import com.mikepenz.markdown.m3.markdownTypography
import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl import com.mikepenz.markdown.coil3.Coil3ImageTransformerImpl
import androidx.compose.material.icons.automirrored.filled.ArrowBack import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.emoji2.emojipicker.EmojiPickerView
import androidx.emoji2.emojipicker.EmojiViewItem
import androidx.compose.material.icons.filled.AttachFile import androidx.compose.material.icons.filled.AttachFile
import androidx.compose.material.icons.filled.Check import androidx.compose.material.icons.filled.Check
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
@@ -78,6 +84,7 @@ import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.rememberUpdatedState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
@@ -103,6 +110,8 @@ import java.util.Locale
private val LocalImageViewer = compositionLocalOf<(String) -> Unit> { {} } private val LocalImageViewer = compositionLocalOf<(String) -> Unit> { {} }
private val LocalVideoPlayer = compositionLocalOf<(String) -> Unit> { {} } private val LocalVideoPlayer = compositionLocalOf<(String) -> Unit> { {} }
private val LocalScrollToEvent = compositionLocalOf<(String) -> Unit> { {} } private val LocalScrollToEvent = compositionLocalOf<(String) -> Unit> { {} }
private val LocalReactionHandler = compositionLocalOf<(eventId: String, emoji: String) -> Unit> { { _, _ -> } }
private val LocalCurrentUserId = compositionLocalOf<String?> { null }
private val senderColors = arrayOf( private val senderColors = arrayOf(
Color(0xFF5865F2), Color(0xFF5865F2),
@@ -204,9 +213,18 @@ fun MessageTimeline(
) )
} }
val reactionHandler: (String, String) -> Unit = remember(selectedThread, onSendReaction, onSendThreadReaction) {
{ eventId, emoji ->
if (selectedThread != null) onSendThreadReaction(eventId, emoji)
else onSendReaction(eventId, emoji)
}
}
CompositionLocalProvider( CompositionLocalProvider(
LocalImageViewer provides { url -> fullscreenImageUrl = url }, LocalImageViewer provides { url -> fullscreenImageUrl = url },
LocalVideoPlayer provides { url -> fullscreenVideoUrl = url }, LocalVideoPlayer provides { url -> fullscreenVideoUrl = url },
LocalReactionHandler provides reactionHandler,
LocalCurrentUserId provides currentUserId,
) { ) {
Column( Column(
modifier = modifier modifier = modifier
@@ -447,6 +465,42 @@ private fun ThreadTopBar(title: String, onClose: () -> Unit) {
} }
} }
@Composable
private fun ReactionRow(eventId: String, reactions: Map<String, List<String>>) {
if (reactions.isEmpty()) return
val onReact = LocalReactionHandler.current
val currentUserId = LocalCurrentUserId.current
FlowRow(
horizontalArrangement = Arrangement.spacedBy(4.dp),
verticalArrangement = Arrangement.spacedBy(4.dp),
modifier = Modifier.padding(top = 4.dp),
) {
reactions.forEach { (emoji, senders) ->
val isMine = currentUserId != null && senders.any { it == currentUserId }
Surface(
shape = RoundedCornerShape(12.dp),
color = if (isMine) MaterialTheme.colorScheme.primaryContainer
else MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier.clickable { onReact(eventId, emoji) },
) {
Row(
modifier = Modifier.padding(horizontal = 8.dp, vertical = 4.dp),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(4.dp),
) {
Text(emoji, fontSize = 14.sp)
Text(
"${senders.size}",
style = MaterialTheme.typography.labelSmall,
color = if (isMine) MaterialTheme.colorScheme.onPrimaryContainer
else MaterialTheme.colorScheme.onSurfaceVariant,
)
}
}
}
}
}
@Composable @Composable
private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {}, threadReplyCount: Int = 0, onLongPress: (MessageItem) -> Unit = {}) { private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {}, threadReplyCount: Int = 0, onLongPress: (MessageItem) -> Unit = {}) {
val senderColor = remember(message.senderName) { colorForSender(message.senderName) } val senderColor = remember(message.senderName) { colorForSender(message.senderName) }
@@ -488,6 +542,7 @@ private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {
} }
Spacer(Modifier.height(2.dp)) Spacer(Modifier.height(2.dp))
MessageContentView(message.content) MessageContentView(message.content)
ReactionRow(message.eventId, message.reactions)
if (threadReplyCount > 0) { if (threadReplyCount > 0) {
Text( Text(
text = "$threadReplyCount ${if (threadReplyCount == 1) "reply" else "replies"}", text = "$threadReplyCount ${if (threadReplyCount == 1) "reply" else "replies"}",
@@ -511,7 +566,10 @@ private fun CompactMessage(message: MessageItem, onLongPress: (MessageItem) -> U
} }
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 1.dp)) { Row(modifier = Modifier.fillMaxWidth().padding(vertical = 1.dp)) {
Spacer(Modifier.width(52.dp)) Spacer(Modifier.width(52.dp))
Column {
MessageContentView(message.content) MessageContentView(message.content)
ReactionRow(message.eventId, message.reactions)
}
} }
} }
} }
@@ -1028,29 +1086,100 @@ private fun MessageContextMenu(
onEdit: () -> Unit, onEdit: () -> Unit,
onStartThread: () -> Unit, onStartThread: () -> Unit,
) { ) {
var showEmojiPicker by remember { mutableStateOf(false) }
val currentOnReact by rememberUpdatedState(onReact)
if (showEmojiPicker) {
Dialog(
onDismissRequest = { showEmojiPicker = false },
properties = DialogProperties(usePlatformDefaultWidth = false),
) {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.surface,
) {
Column(
modifier = Modifier
.fillMaxSize()
.padding(4.dp),
) {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp),
) {
IconButton(onClick = { showEmojiPicker = false }) {
Icon(
Icons.Default.Close,
"Close picker",
tint = MaterialTheme.colorScheme.onSurface,
)
}
Text(
"Choose an emoji",
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
modifier = Modifier.weight(1f),
)
}
Spacer(modifier = Modifier.height(8.dp))
AndroidView(
factory = { ctx -> EmojiPickerView(ctx) },
update = { view ->
view.setOnEmojiPickedListener { emojiViewItem: EmojiViewItem ->
currentOnReact(emojiViewItem.emoji)
showEmojiPicker = false
}
},
modifier = Modifier
.fillMaxWidth()
.weight(1f),
)
}
}
}
}
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
title = null, title = null,
text = { text = {
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) { Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
// Quick emoji reactions Row(
LazyRow( modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(4.dp), horizontalArrangement = Arrangement.spacedBy(4.dp),
contentPadding = PaddingValues(vertical = 8.dp),
) { ) {
items(QUICK_REACTIONS) { emoji -> QUICK_REACTIONS.forEach { emoji ->
Box( Box(
modifier = Modifier modifier = Modifier
.size(40.dp) .size(36.dp)
.clip(CircleShape) .clip(CircleShape)
.background(MaterialTheme.colorScheme.surfaceVariant) .background(MaterialTheme.colorScheme.surfaceVariant)
.clickable { onReact(emoji) }, .clickable { onReact(emoji) },
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
) { ) {
Text(emoji, fontSize = 20.sp) Text(emoji, fontSize = 18.sp)
} }
} }
} }
Button(
onClick = { showEmojiPicker = true },
modifier = Modifier
.fillMaxWidth()
.height(48.dp),
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.primaryContainer,
contentColor = MaterialTheme.colorScheme.onPrimaryContainer,
),
) {
Icon(
Icons.Default.Add,
"Add reaction",
modifier = Modifier.size(20.dp),
)
Spacer(modifier = Modifier.width(8.dp))
Text("More emojis")
}
HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp)) HorizontalDivider(modifier = Modifier.padding(vertical = 4.dp))
TextButton( TextButton(
onClick = onReply, onClick = onReply,

View File

@@ -19,6 +19,7 @@ matrixRustSdk = "26.02.19"
coil = "3.2.0" coil = "3.2.0"
media3 = "1.6.0" media3 = "1.6.0"
markdownRenderer = "0.37.0" markdownRenderer = "0.37.0"
emojiPicker = "1.6.0"
kotlinxSerialization = "1.8.1" kotlinxSerialization = "1.8.1"
[libraries] [libraries]
@@ -78,6 +79,9 @@ markdown-renderer-m3 = { group = "com.mikepenz", name = "multiplatform-markdown-
markdown-renderer-code = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-code-android", version.ref = "markdownRenderer" } markdown-renderer-code = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-code-android", version.ref = "markdownRenderer" }
markdown-renderer-coil3 = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-coil3-android", version.ref = "markdownRenderer" } markdown-renderer-coil3 = { group = "com.mikepenz", name = "multiplatform-markdown-renderer-coil3-android", version.ref = "markdownRenderer" }
# Jetpack Emoji Picker
emoji-picker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emojiPicker" }
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }