better emoji reactions
This commit is contained in:
6
.idea/studiobot.xml
generated
Normal file
6
.idea/studiobot.xml
generated
Normal 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>
|
||||
165
AGENTS.md
165
AGENTS.md
@@ -5,13 +5,13 @@
|
||||
Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with Kotlin, targeting Android 14+ (minSdk 34, targetSdk 36, compileSdk 36).
|
||||
|
||||
**Package**: `com.example.fluffytrix`
|
||||
**Build system**: Gradle with Kotlin DSL, version catalog at `gradle/libs.versions.toml`
|
||||
**Container/DI**: Koin
|
||||
**Build system**: Gradle with KotlinDSL, version catalog at `gradle/libs.versions.toml`
|
||||
**DI**: Koin
|
||||
**State management**: Jetpack Compose StateFlow, ViewModel
|
||||
**UI framework**: Jetpack Compose with Material 3 (Dynamic Colors)
|
||||
**Protocol**: Trixnity SDK for Matrix
|
||||
**Storage**: Room Database, DataStore Preferences
|
||||
**Async**: Kotlin Coroutines
|
||||
**Async**: Kotlin Coroutines
|
||||
|
||||
---
|
||||
|
||||
@@ -20,28 +20,22 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
|
||||
```bash
|
||||
./gradlew assembleDebug # Build debug APK (minified for performance)
|
||||
./gradlew assembleRelease # Build release APK
|
||||
./gradlew test # Run unit tests
|
||||
./gradlew connectedAndroidTest # Run instrumented tests on device/emulator
|
||||
./gradlew testDebugUnitTest --tests "com.example.fluffytrix.ExampleUnitTest" # Run single test
|
||||
./gradlew test # Run all unit tests
|
||||
./gradlew connectedAndroidTest # Run instrumented tests on device
|
||||
./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` but strip material-icons-extended and optimize Compose for performance
|
||||
- Use `--tests` with Gradle test tasks to run a single test class
|
||||
- Debug builds use R8 with `isDebuggable = true` for balanced speed/debuggability
|
||||
- Instrumented tests require a connected device or emulator
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
**Unit tests**: Located in `app/src/test/java/`, run with `./gradlew test`
|
||||
**Instrumented tests**: Located in `app/src/androidTest/java/`, run with `./gradlew connectedAndroidTest`
|
||||
|
||||
**Single test execution**:
|
||||
```bash
|
||||
./gradlew testDebugUnitTest --tests "com.example.fluffytrix.*"
|
||||
./gradlew app:testDebugUnitTest --tests "ExampleUnitTest"
|
||||
```
|
||||
**Unit tests**: `app/src/test/java/`
|
||||
**Instrumented tests**: `app/src/androidTest/java/`
|
||||
**Single test**: `./gradlew testDebugUnitTest --tests "com.example.fluffytrix.ui.main.LoginViewModelTest"`
|
||||
|
||||
---
|
||||
|
||||
@@ -49,17 +43,17 @@ Fluffytrix is an Android Matrix chat client with a Discord-like UI. Built with K
|
||||
|
||||
### 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`)
|
||||
- Class naming: `PascalCase` (e.g., `MainViewModel`, `AuthRepository`)
|
||||
- Class naming: `PascalCase` (e.g., `AuthRepository`, `MainViewModel`)
|
||||
- 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**
|
||||
|
||||
### Imports
|
||||
|
||||
- Explicit imports only (no wildcard imports)
|
||||
- Group imports: Android/X → Kotlin → Javax/Java → Third-party → Same package
|
||||
- Explicit imports only (no wildcards)
|
||||
- Group: Android/X → Kotlin → Javax/Java → Third-party → Same package
|
||||
- Example:
|
||||
```kotlin
|
||||
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
|
||||
```
|
||||
|
||||
### Types
|
||||
### Types & Error Handling
|
||||
|
||||
- Prefer `val` over `var` (immutable data)
|
||||
- Use `StateFlow<T>` for observable state in ViewModels
|
||||
- Use `Flow<T>` for read-only data streams
|
||||
- Use `suspend` functions for async operations
|
||||
- Use `Result<T>` for operations that can fail
|
||||
- Use `StateFlow<T>` for observable ViewModel state, `Flow<T>` for read-only streams
|
||||
- Use `suspend` for async operations, `Result<T>` for failing operations
|
||||
- `try-catch` with `catch (_: Exception) { }` for graceful degradation in ViewModels
|
||||
- Use `?:` operators when appropriate, **never crash on recoverable errors**
|
||||
|
||||
### Error Handling
|
||||
### Compose & ViewModels
|
||||
|
||||
- Prefer `try-catch` with silent failure or `?` operators where appropriate
|
||||
- In ViewModels, use `catch (_: Exception) { }` or `?:` for graceful degradation
|
||||
- Expose error state via `StateFlow<AuthState>` where users need feedback
|
||||
- **Never crash the app on recoverable errors**
|
||||
- Use `@Composable` for all UI functions; use `MaterialTheme` for consistent theming
|
||||
- Discord-like layout: space sidebar → channel list → message area → member list
|
||||
- Use `Modifier.padding()`, `wrapContentWidth()`, `fillMaxWidth()` appropriately
|
||||
- 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
|
||||
- Follow Discord-like layout: space sidebar → channel list → message area → member list
|
||||
- Use `MaterialTheme` for consistent theming
|
||||
- Prefer `Modifier.padding()` over nested `Box` with margins
|
||||
- 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`)
|
||||
- `Dispatchers.Default` for CPU work, `Dispatchers.IO` for I/O operations
|
||||
- Cancel jobs on ViewModel cleanup: `job?.cancel()`
|
||||
- Use `Room` for persistent storage; `DataStore Preferences` for small key-value data
|
||||
- Prefer Flow-based APIs; cache in ViewModels
|
||||
|
||||
### Naming Conventions
|
||||
|
||||
- State Flow properties: `_name` (private) / `name` (public)
|
||||
- Repository class: `AuthService`, `AuthRepository`
|
||||
- ViewModel class: `MainViewModel`, `LoginViewModel`
|
||||
- UI composable: `MainScreen`, `ChannelList`, `MessageItem`
|
||||
- Model data class: `MessageItem`, `ChannelItem`, `SpaceItem`
|
||||
- Use `full` property for Matrix IDs (e.g., `userId.full`, `roomId.full`)
|
||||
- State Flow: `_state` (private) / `state` (public)
|
||||
- Repositories: `AuthRepository`, `MessageRepository`
|
||||
- ViewModels: `MainViewModel`, `LoginViewModel`
|
||||
- UI composables: `MainScreen`, `ChannelList`, `MessageItem`
|
||||
- Models: `MessageItem`, `ChannelItem`, `SpaceItem`
|
||||
- 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**:
|
||||
```
|
||||
ui/ — ViewModels, Screens, Navigation
|
||||
data/ — Repositories, local storage, models
|
||||
data/ — Repositories, storage, models
|
||||
di/ — Koin modules
|
||||
ui/theme/ — Material 3 Theme (colors, typography)
|
||||
ui/theme/ — Material 3 Theme
|
||||
```
|
||||
|
||||
**Dependency Injection**: Koin with two modules:
|
||||
- `appModule`: ViewModels (`viewModel { MainViewModel(...) }`)
|
||||
- `dataModule`: singleton services (`single { AuthRepository(...) }`)
|
||||
|
||||
**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()`
|
||||
**Koin DI**:
|
||||
- `appModule`: ViewModel injection
|
||||
- `dataModule`: Singleton repositories/UI Flow: NavHost in `MainActivity` with `FluffytrixTheme`, ViewModels expose StateFlow, screens observe with `collectAsState()`
|
||||
|
||||
---
|
||||
|
||||
## Key Dependencies (from libs.versions.toml)
|
||||
## Key Dependencies
|
||||
|
||||
- **Compose BOM**: `2025.06.00`
|
||||
- **Kotlin**: `2.2.10`
|
||||
- **AGP**: `9.0.1`
|
||||
- **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)
|
||||
- **Compose BOM**: `2025.06.00`, **Kotlin**: `2.2.10`, **AGP**: `9.0.1`
|
||||
- **Koin**: `4.1.1`, **Trixnity**: `4.22.7`, **Ktor**: `3.3.0`
|
||||
- **Coroutines**: `1.10.2`, **DataStore**: `1.1.7`, **Coil**: `3.2.0`
|
||||
|
||||
@@ -101,6 +101,9 @@ dependencies {
|
||||
implementation(libs.markdown.renderer.code)
|
||||
implementation(libs.markdown.renderer.coil3)
|
||||
|
||||
// Jetpack Emoji Picker
|
||||
implementation(libs.emoji.picker)
|
||||
|
||||
testImplementation(libs.junit)
|
||||
androidTestImplementation(libs.androidx.junit)
|
||||
androidTestImplementation(libs.androidx.espresso.core)
|
||||
|
||||
@@ -88,6 +88,7 @@ data class MessageItem(
|
||||
val timestamp: Long,
|
||||
val replyTo: ReplyInfo? = null,
|
||||
val threadRootEventId: String? = null,
|
||||
val reactions: Map<String, List<String>> = emptyMap(), // emoji -> list of full Matrix user IDs
|
||||
)
|
||||
|
||||
data class ThreadItem(
|
||||
@@ -752,6 +753,12 @@ class MainViewModel(
|
||||
senderNameCache[localpart] = senderName
|
||||
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(
|
||||
eventId = eventId,
|
||||
senderId = localpart,
|
||||
@@ -761,6 +768,7 @@ class MainViewModel(
|
||||
timestamp = eventItem.timestamp.toLong(),
|
||||
replyTo = replyInfo,
|
||||
threadRootEventId = threadRootId,
|
||||
reactions = reactions,
|
||||
)
|
||||
|
||||
ids.add(eventId)
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.example.fluffytrix.ui.screens.main.components
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
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.material.icons.Icons
|
||||
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.Tag
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.HorizontalDivider
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.TextField
|
||||
import androidx.compose.material3.TextFieldDefaults
|
||||
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.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.compose.material3.Text
|
||||
import android.net.Uri
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
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.coil3.Coil3ImageTransformerImpl
|
||||
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.Check
|
||||
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.derivedStateOf
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.runtime.rememberUpdatedState
|
||||
import kotlinx.coroutines.launch
|
||||
import androidx.compose.ui.graphics.graphicsLayer
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
@@ -103,6 +110,8 @@ import java.util.Locale
|
||||
private val LocalImageViewer = compositionLocalOf<(String) -> Unit> { {} }
|
||||
private val LocalVideoPlayer = 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(
|
||||
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(
|
||||
LocalImageViewer provides { url -> fullscreenImageUrl = url },
|
||||
LocalVideoPlayer provides { url -> fullscreenVideoUrl = url },
|
||||
LocalReactionHandler provides reactionHandler,
|
||||
LocalCurrentUserId provides currentUserId,
|
||||
) {
|
||||
Column(
|
||||
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
|
||||
private fun FullMessage(message: MessageItem, onOpenThread: (String) -> Unit = {}, threadReplyCount: Int = 0, onLongPress: (MessageItem) -> Unit = {}) {
|
||||
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))
|
||||
MessageContentView(message.content)
|
||||
ReactionRow(message.eventId, message.reactions)
|
||||
if (threadReplyCount > 0) {
|
||||
Text(
|
||||
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)) {
|
||||
Spacer(Modifier.width(52.dp))
|
||||
MessageContentView(message.content)
|
||||
Column {
|
||||
MessageContentView(message.content)
|
||||
ReactionRow(message.eventId, message.reactions)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1028,29 +1086,100 @@ private fun MessageContextMenu(
|
||||
onEdit: () -> 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(
|
||||
onDismissRequest = onDismiss,
|
||||
title = null,
|
||||
text = {
|
||||
Column(verticalArrangement = Arrangement.spacedBy(0.dp)) {
|
||||
// Quick emoji reactions
|
||||
LazyRow(
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||
contentPadding = PaddingValues(vertical = 8.dp),
|
||||
) {
|
||||
items(QUICK_REACTIONS) { emoji ->
|
||||
QUICK_REACTIONS.forEach { emoji ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(40.dp)
|
||||
.size(36.dp)
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colorScheme.surfaceVariant)
|
||||
.clickable { onReact(emoji) },
|
||||
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))
|
||||
TextButton(
|
||||
onClick = onReply,
|
||||
|
||||
@@ -19,6 +19,7 @@ matrixRustSdk = "26.02.19"
|
||||
coil = "3.2.0"
|
||||
media3 = "1.6.0"
|
||||
markdownRenderer = "0.37.0"
|
||||
emojiPicker = "1.6.0"
|
||||
kotlinxSerialization = "1.8.1"
|
||||
|
||||
[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-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]
|
||||
android-application = { id = "com.android.application", version.ref = "agp" }
|
||||
kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
|
||||
|
||||
Reference in New Issue
Block a user