Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Release note structure source: `docs/project/RELEASE_NOTES_TEMPLATE.md`

### Added
- Android: react to messages — long-pressing a message and choosing "React" now opens an emoji picker (16 common reactions) that adds your reaction, instead of doing nothing
- Android: direct messages are now accessible — a new envelope button on the home screen opens a conversation list (avatar, name, last-message preview, unread badge); tapping a conversation opens it like any channel
- Android: message attachments now display — images render as inline thumbnails, other files as a chip with name and size (previously attachments were invisible)
- Android: unread channels now show a count badge and bolder name in the channel list, clearing when you open the channel and re-raising on new messages
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package io.wolftown.kaiku.ui.channel

import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp

/**
* A modal emoji picker for reacting to a message.
*
* Selecting an emoji invokes [onEmojiSelected] (the screen forwards it to the
* view model, which calls the reaction API) and the caller is expected to
* dismiss. Tapping outside, pressing back, or "Cancel" invokes [onDismiss].
*/
@Composable
fun EmojiPicker(
onEmojiSelected: (String) -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
confirmButton = {},
dismissButton = {
TextButton(onClick = onDismiss) { Text("Cancel") }
},
title = { Text("Add reaction") },
text = {
LazyVerticalGrid(
columns = GridCells.Fixed(4),
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 240.dp),
) {
items(COMMON_REACTIONS) { emoji ->
Text(
text = emoji,
fontSize = 28.sp,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.clickable { onEmojiSelected(emoji) }
.padding(12.dp),
)
}
}
},
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package io.wolftown.kaiku.ui.channel

/**
* Curated set of quick-reaction emoji shown in the [EmojiPicker]. Kept small
* and ordered by expected frequency so the most common reactions are reachable
* without scrolling. Pure data (no Compose dependency) so it can be unit tested.
*/
val COMMON_REACTIONS: List<String> = listOf(
"👍", // thumbs up
"👎", // thumbs down
"❤️", // red heart
"😂", // joy
"😮", // open mouth
"😢", // crying
"😡", // angry
"🎉", // party popper
"🔥", // fire
"👏", // clapping
"🙏", // folded hands
"💯", // hundred points
"🚀", // rocket
"👀", // eyes
"✅", // check mark
"❓", // question mark
)
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@ fun TextChannelScreen(

val listState = rememberLazyListState()

// When set, the emoji picker is shown for this message id.
var reactionTargetId by remember { mutableStateOf<String?>(null) }

// Auto-scroll to bottom only when a genuinely new message arrives
// and the user is already near the bottom of the list.
var lastMessageId by remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -127,7 +130,7 @@ fun TextChannelScreen(
viewModel.onDeleteMessage(msgId)
},
onReact = { msgId ->
// In a full implementation, this would show an emoji picker.
reactionTargetId = msgId
}
)
}
Expand All @@ -136,4 +139,14 @@ fun TextChannelScreen(
}
}
}

reactionTargetId?.let { messageId ->
EmojiPicker(
onEmojiSelected = { emoji ->
viewModel.onAddReaction(messageId, emoji)
reactionTargetId = null
},
onDismiss = { reactionTargetId = null }
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package io.wolftown.kaiku.domain

import io.wolftown.kaiku.ui.channel.COMMON_REACTIONS
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test

class ReactionsTest {

@Test
fun reactions_areNonEmpty() {
assertTrue("expected at least one quick reaction", COMMON_REACTIONS.isNotEmpty())
}

@Test
fun reactions_haveNoDuplicates() {
assertEquals(COMMON_REACTIONS.size, COMMON_REACTIONS.toSet().size)
}

@Test
fun reactions_areAllNonBlank() {
assertTrue(COMMON_REACTIONS.all { it.isNotBlank() })
}

@Test
fun reactions_fitTheFourColumnGridEvenly() {
// The picker renders a fixed 4-column grid; a multiple of 4 avoids a
// ragged last row.
assertEquals(0, COMMON_REACTIONS.size % 4)
}
}
Loading