diff --git a/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt b/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt index b0cd0cf..14bece1 100644 --- a/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt +++ b/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt @@ -1,17 +1,35 @@ package chatroom.viewmessages import chatserver.ChatRepositories -import kotlinx.coroutines.flow.* +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import utils.SmartWrap +import java.lang.System.lineSeparator -class DisplayableMessages(private val repositories: ChatRepositories) { - operator fun invoke(): Flow = +class DisplayableMessages( + private val repositories: ChatRepositories, + private val wrap: SmartWrap, +) { + operator fun invoke(): Flow> = repositories.readMessages().observe() .filter { it.isOk } - .map { it.item.lastOrNull() } - .filterNotNull() + .mapNotNull { it.item.lastOrNull() } .map { message -> val usernames = repositories.readProfiles().latest().takeIf { it.isOk }?.item ?: emptyMap() val user = usernames[message.userId]?.username ?: "anon" "$user: ${message.message}" + .removeLineBreaks() + .wrap() + .toList() } + + private fun String.wrap(): String = wrap(this) + + companion object { + private fun String.removeLineBreaks(): String = replace("[\r\n]+".toRegex(), " ").trim() + + private fun String.toList(): List = split(lineSeparator()) + } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt index 7dc2d2a..0857f14 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt @@ -1,8 +1,10 @@ package chatroom.viewmessages import chatroom.ChatroomWindow +import chatroom.SEND_MESSAGES_WIDTH import chatserver.ChatRepositories import org.koin.dsl.module +import utils.SmartWrap val viewMessagesModule = module { @@ -16,6 +18,7 @@ val viewMessagesModule = channel = get().subscribeMessages(), ) } - scoped { DisplayableMessages(get()) } + scoped { DisplayableMessages(get(), get()) } + scoped { SmartWrap(SEND_MESSAGES_WIDTH) } } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPanel.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPanel.kt index 9419511..c9fe87f 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPanel.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPanel.kt @@ -3,12 +3,8 @@ package chatroom.viewmessages import chatroom.BorderedPanel import chatroom.viewmessages.ViewMessagesViewState.* import com.googlecode.lanterna.TerminalSize -import com.googlecode.lanterna.gui2.Border -import com.googlecode.lanterna.gui2.BorderLayout +import com.googlecode.lanterna.gui2.* import com.googlecode.lanterna.gui2.BorderLayout.Location.CENTER -import com.googlecode.lanterna.gui2.Borders -import com.googlecode.lanterna.gui2.Label -import com.googlecode.lanterna.gui2.Panel import view.NonFocusableTextBox class ViewMessagesPanel( @@ -62,7 +58,7 @@ class ViewMessagesPanel( empty.isVisible = false messages.isVisible = true textGUI.guiThread.invokeLater { - messages.addLineAndMaybeScrollDown(state.message) + state.lines.forEach(messages::addLineAndMaybeScrollDown) } } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesViewState.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesViewState.kt index 486fc78..3ed8e2b 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesViewState.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessagesViewState.kt @@ -9,5 +9,5 @@ sealed interface ViewMessagesViewState { const val STATUS = "No Messages Yet" } - data class Messages(val message: String) : ViewMessagesViewState + data class Messages(val lines: List) : ViewMessagesViewState } diff --git a/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt b/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt index b972cac..511f23c 100644 --- a/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt +++ b/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt @@ -12,6 +12,7 @@ import kotlinx.coroutines.test.advanceTimeBy import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import utils.SmartWrap import kotlin.time.Duration.Companion.seconds @OptIn(ExperimentalCoroutinesApi::class) @@ -35,53 +36,100 @@ class DisplayableMessagesTest { every { it.readProfiles() } returns users every { it.readMessages() } returns messages } - displayableMessages = DisplayableMessages(repository) + displayableMessages = DisplayableMessages(repository, SmartWrap(50)) } @Test - fun `when known user sends message, then show username and message`() = + fun `when known user sends message, and message is less than one line long, then show username and message`() = runTest { every { users.latest() } returns ProfileResult.ok(mapOf("id" to Profile("id", "Tony KnowsItAll"))) every { messages.observe() } returns flowOf(MessageResult.ok(listOf(Message("id", "I will fail everyone")))) - val emissions = mutableListOf() + val emissions = mutableListOf>() backgroundScope.launch { displayableMessages().collect(emissions::add) } advanceTimeBy(10.seconds) - emissions.shouldContainExactly("Tony KnowsItAll: I will fail everyone") + emissions[0].shouldContainExactly("Tony KnowsItAll: I will fail everyone") } @Test - fun `when unknown user post message, then show anon message`() = + fun `when known user sends message, and message was sent with many short sentences, then show message split by receiver preferred line length`() = + runTest { + every { users.latest() } returns ProfileResult.ok(mapOf("id" to Profile("id", "Tony KnowsItAll"))) + every { messages.observe() } returns + flowOf( + MessageResult.ok( + listOf( + Message( + "id", + "I will\nfail\neveryone\n\n", + ), + ), + ), + ) + val emissions = mutableListOf>() + backgroundScope.launch { displayableMessages().collect(emissions::add) } + advanceTimeBy(10.seconds) + emissions[0].shouldContainExactly("Tony KnowsItAll: I will fail everyone") + } + + @Test + fun `when known user sends message, and message overflows max line size, then show message split by receiver preference`() = + runTest { + every { users.latest() } returns ProfileResult.ok(mapOf("id" to Profile("id", "Tony KnowsItAll"))) + every { messages.observe() } returns + flowOf( + MessageResult.ok( + listOf( + Message( + "id", + "I will fail everyone because I hate all my students", + ), + ), + ), + ) + val emissions = mutableListOf>() + backgroundScope.launch { displayableMessages().collect(emissions::add) } + advanceTimeBy(10.seconds) + emissions[0].shouldContainExactly( + "Tony KnowsItAll: I will fail everyone because I ", + "hate all my students", + ) + } + + @Test + fun `when unknown user post message, and message is less than one line long, then show anon message`() = runTest { every { users.latest() } returns ProfileResult.ok(emptyMap()) every { messages.observe() } returns flowOf(MessageResult.ok(listOf(Message("id", "I will fail everyone")))) - val emissions = mutableListOf() + val emissions = mutableListOf>() backgroundScope.launch { displayableMessages().collect(emissions::add) } advanceTimeBy(10.seconds) - emissions.shouldContainExactly("anon: I will fail everyone") + emissions[0].shouldContainExactly("anon: I will fail everyone") } @Test - fun `when know user sends message, and changes its name, then show new name`() = + fun `when know user sends message, and all messages are less than one line long, and user changes their name, then show new name`() = runTest { every { users.latest() } returns ProfileResult.ok(mapOf("id" to Profile("id", "Tony KnowsItAll"))) every { messages.observe() } returns flowOf( MessageResult.ok(listOf(Message("id", "I will fail everyone"))), ) - val emissions = mutableListOf() + val emissions = mutableListOf>() backgroundScope.launch { displayableMessages().collect(emissions::add) } advanceTimeBy(10.seconds) every { users.latest() } returns ProfileResult.ok(mapOf("id" to Profile("id", "Neamah"))) every { messages.observe() } returns flowOf( - MessageResult.ok(listOf(Message("id", "I hate my student, but not as much as i hate myself"))), + MessageResult.ok(listOf(Message("id", "I hate my students..."))), ) backgroundScope.launch { displayableMessages().collect(emissions::add) } advanceTimeBy(10.seconds) - emissions.shouldContainExactly( + emissions[0].shouldContainExactly( "Tony KnowsItAll: I will fail everyone", - "Neamah: I hate my student, but not as much as i hate myself", + ) + emissions[1].shouldContainExactly( + "Neamah: I hate my students...", ) } } diff --git a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt index 3c5a830..425ffbc 100644 --- a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt +++ b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt @@ -52,7 +52,7 @@ class ViewMessagesPresenterTest { @Test fun `given message exist, when attached, then show message`() = runTest(dispatcher) { - every { read() } returns flowOf("Kai: Biggleswad ez badd") + every { read() } returns flowOf(listOf("Kai: Biggleswad ez badd")) presenter.attach(view) advanceUntilIdle() verifyOrder { @@ -81,8 +81,8 @@ class ViewMessagesPresenterTest { runTest(dispatcher) { every { read() } returns flowOf( - "Kai: Pokemon good", - "Allie: Barry is the best", + listOf("Kai: Pokemon good"), + listOf("Allie: Barry is the best"), ) presenter.attach(view) @@ -99,7 +99,7 @@ class ViewMessagesPresenterTest { assertTrue { it is Messages } assertEquals( message, - (it as Messages).message, + (it as Messages).lines[0], ) } }