diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt index e14bb92..6aadfc9 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt @@ -14,6 +14,7 @@ val viewMessagesModule = dispatchers = get(), read = get().readMessages(), channel = get().subscribeMessages(), + smartWrap = get(), ) } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt index 5c5d9e0..881116f 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt @@ -12,11 +12,13 @@ import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import utils.SmartWrap class ViewMessagesPresenter( private val windowScope: CoroutineScope, private val read: ReadChatRepository, private val channel: SubscribeChatRepository, + private val smartWrap: SmartWrap, dispatchers: RokyDispatchers, ) : Presenter(dispatchers) { override fun onAttach(view: ViewMessagesView) { @@ -25,6 +27,7 @@ class ViewMessagesPresenter( read.observe() .filter { it.isOk } .map { it.item } + .map { smartWrap(it) } .map(::Messages) .collect { message -> withContext(dispatchers.main) { diff --git a/src/main/kotlin/main.kt b/src/main/kotlin/main.kt index 30c0cb5..8de967d 100644 --- a/src/main/kotlin/main.kt +++ b/src/main/kotlin/main.kt @@ -32,6 +32,7 @@ import org.koin.dsl.binds import org.koin.dsl.module import org.koin.java.KoinJavaComponent.get import profile.profileModules +import utils.smartWrapModule import view.rokyTheme fun main() { @@ -53,6 +54,7 @@ val mainModules = profileModules, chatroomModules, chatServerModule, + smartWrapModule, ) single { DefaultTerminalFactory().createScreen() } bind Screen::class single { MultiWindowTextGUI(get()).also { it.theme = rokyTheme } } diff --git a/src/main/kotlin/utils/LoggingSmartWrap.kt b/src/main/kotlin/utils/LoggingSmartWrap.kt new file mode 100644 index 0000000..eadfd9b --- /dev/null +++ b/src/main/kotlin/utils/LoggingSmartWrap.kt @@ -0,0 +1,8 @@ +package utils + +class LoggingSmartWrap : SmartWrap { + override fun invoke(input: String): String { + println(input) + return input + } +} diff --git a/src/main/kotlin/utils/SimpleSmartWrap.kt b/src/main/kotlin/utils/SimpleSmartWrap.kt new file mode 100644 index 0000000..d1aabeb --- /dev/null +++ b/src/main/kotlin/utils/SimpleSmartWrap.kt @@ -0,0 +1,61 @@ +package utils + +import org.jetbrains.annotations.VisibleForTesting +import java.lang.System.lineSeparator + +class SimpleSmartWrap( + private val maxCharsPerLine: Int, + private val lineSeparator: String = lineSeparator(), +) : SmartWrap { + override operator fun invoke(input: String): String = + with(input) { + if (length <= maxCharsPerLine) { + return this + } + val words = split("\\s".toRegex()) + var paragraph = "" + words.forEach { word -> + if (paragraph.wouldOverFlow(maxCharsPerLine, word)) { + if (paragraph.isNotEmpty()) paragraph += lineSeparator + paragraph += + if (word.canFitOnALine(maxCharsPerLine)) { + word + } else { + word.chunked(maxCharsPerLine - 1).joinToString(lineSeparator) { "$it-" }.trimEnd('-') + } + } else { + paragraph += word + } + paragraph += if (paragraph.isLineFull(maxCharsPerLine)) lineSeparator else " " + } + return paragraph.trimEnd() + } + + companion object { + @VisibleForTesting + fun String.isSingleLineParagraph() = !contains(lineSeparator()) + + @VisibleForTesting + fun String.canFitOnALine(maxCharsPerLine: Int) = length <= maxCharsPerLine + + @VisibleForTesting + fun String.wouldOverFlow( + maxCharsPerLine: Int, + word: String, + ) = charactersUsedInLastLine() + word.length > maxCharsPerLine + + @VisibleForTesting + fun String.charactersUsedInLastLine(): Int = + if (isSingleLineParagraph()) { + length + } else { + lastIndex - (lastIndexOf(lineSeparator()) + (lineSeparator().length - 1)) + } + + @VisibleForTesting + fun String.charactersRemainingInLastLine(maxCharsPerLine: Int) = maxCharsPerLine - charactersUsedInLastLine() + + @VisibleForTesting + fun String.isLineFull(maxCharsPerLine: Int) = charactersRemainingInLastLine(maxCharsPerLine) <= 0 + } +} diff --git a/src/main/kotlin/utils/SmartWrap.kt b/src/main/kotlin/utils/SmartWrap.kt index 76a5e45..37b045a 100644 --- a/src/main/kotlin/utils/SmartWrap.kt +++ b/src/main/kotlin/utils/SmartWrap.kt @@ -1,62 +1,5 @@ package utils -import org.jetbrains.annotations.VisibleForTesting -import utils.SmartWrap.Companion.canFitOnALine -import java.lang.System.lineSeparator - -class SmartWrap( - private val maxCharsPerLine: Int, - private val lineSeparator: String = lineSeparator(), -) { - operator fun invoke(input: String): String = - with(input) { - if (length <= maxCharsPerLine) { - return this - } - val words = split("\\s".toRegex()) - var paragraph = "" - words.forEach { word -> - if (paragraph.wouldOverFlow(maxCharsPerLine, word)) { - if (paragraph.isNotEmpty()) paragraph += lineSeparator - paragraph += - if (word.canFitOnALine(maxCharsPerLine)) { - word - } else { - word.chunked(maxCharsPerLine - 1).joinToString(lineSeparator) { "$it-" }.trimEnd('-') - } - } else { - paragraph += word - } - paragraph += if (paragraph.isLineFull(maxCharsPerLine)) lineSeparator else " " - } - return paragraph.trimEnd() - } - - companion object { - @VisibleForTesting - fun String.isSingleLineParagraph() = !contains(lineSeparator()) - - @VisibleForTesting - fun String.canFitOnALine(maxCharsPerLine: Int) = length <= maxCharsPerLine - - @VisibleForTesting - fun String.wouldOverFlow( - maxCharsPerLine: Int, - word: String, - ) = charactersUsedInLastLine() + word.length > maxCharsPerLine - - @VisibleForTesting - fun String.charactersUsedInLastLine(): Int = - if (isSingleLineParagraph()) { - length - } else { - lastIndex - (lastIndexOf(lineSeparator()) + (lineSeparator().length - 1)) - } - - @VisibleForTesting - fun String.charactersRemainingInLastLine(maxCharsPerLine: Int) = maxCharsPerLine - charactersUsedInLastLine() - - @VisibleForTesting - fun String.isLineFull(maxCharsPerLine: Int) = charactersRemainingInLastLine(maxCharsPerLine) <= 0 - } +fun interface SmartWrap { + operator fun invoke(input: String): String } diff --git a/src/main/kotlin/utils/SmartWrapDependencyInjection.kt b/src/main/kotlin/utils/SmartWrapDependencyInjection.kt new file mode 100644 index 0000000..bacc191 --- /dev/null +++ b/src/main/kotlin/utils/SmartWrapDependencyInjection.kt @@ -0,0 +1,9 @@ +package utils + +import org.koin.dsl.factory +import org.koin.dsl.module + +val smartWrapModule = + module { + factory { LoggingSmartWrap() } + } diff --git a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt index ad6d2fb..d1e6d92 100644 --- a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt +++ b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt @@ -15,6 +15,7 @@ import kotlinx.coroutines.test.advanceUntilIdle import kotlinx.coroutines.test.runTest import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test +import utils.SmartWrap import kotlin.test.assertEquals import kotlin.test.assertFalse import kotlin.test.assertTrue @@ -24,6 +25,7 @@ class ViewMessagesPresenterTest { private lateinit var channel: SubscribeChatRepository private lateinit var read: ReadChatRepository private lateinit var scope: CoroutineScope + private lateinit var smartWrap: SmartWrap private lateinit var view: ViewMessagesView private lateinit var presenter: ViewMessagesPresenter @@ -32,6 +34,9 @@ class ViewMessagesPresenterTest { channel = mockk(relaxed = true) read = mockk() every { read.observe() } returns flowOf() + smartWrap = mockk(relaxed = true) + every { smartWrap.invoke(any()) } answers { it.invocation.args[0] as String } + view = mockk(relaxed = true) view = mockk(relaxed = true) val dispatchers: RokyDispatchers = @@ -40,7 +45,7 @@ class ViewMessagesPresenterTest { every { io } returns dispatcher } scope = CoroutineScope(dispatcher) - presenter = ViewMessagesPresenter(scope, read, channel, dispatchers) + presenter = ViewMessagesPresenter(scope, read, channel, smartWrap, dispatchers) } @Test diff --git a/src/test/kotlin/utils/SmartWrapKtTest.kt b/src/test/kotlin/utils/SimpleSmartWrapTest.kt similarity index 97% rename from src/test/kotlin/utils/SmartWrapKtTest.kt rename to src/test/kotlin/utils/SimpleSmartWrapTest.kt index 3c8b716..b7848b9 100644 --- a/src/test/kotlin/utils/SmartWrapKtTest.kt +++ b/src/test/kotlin/utils/SimpleSmartWrapTest.kt @@ -1,10 +1,11 @@ package utils + import io.kotest.matchers.equals.shouldBeEqual import io.kotest.matchers.string.shouldBeEmpty import org.junit.jupiter.api.Test import java.lang.System.lineSeparator -class SmartWrapKtTest { +class SimpleSmartWrapTest { @Test fun `given empty String, when cutoff size is 0, then return empty string`() { "".smartWrap(0).shouldBeEmpty() @@ -65,6 +66,6 @@ class SmartWrapKtTest { } companion object { - private fun String.smartWrap(maxCharsPerLine: Int): String = SmartWrap(maxCharsPerLine).invoke(this) + private fun String.smartWrap(maxCharsPerLine: Int): String = SimpleSmartWrap(maxCharsPerLine).invoke(this) } } diff --git a/src/test/kotlin/utils/StringUtilsFunctionsTest.kt b/src/test/kotlin/utils/SimpleSmartWrapUtilsFunctionsTest.kt similarity index 94% rename from src/test/kotlin/utils/StringUtilsFunctionsTest.kt rename to src/test/kotlin/utils/SimpleSmartWrapUtilsFunctionsTest.kt index cd77e7d..a1ea020 100644 --- a/src/test/kotlin/utils/StringUtilsFunctionsTest.kt +++ b/src/test/kotlin/utils/SimpleSmartWrapUtilsFunctionsTest.kt @@ -1,16 +1,17 @@ package utils + import io.kotest.matchers.booleans.shouldBeFalse import io.kotest.matchers.booleans.shouldBeTrue import io.kotest.matchers.equals.shouldBeEqual import org.junit.jupiter.api.Test -import utils.SmartWrap.Companion.canFitOnALine -import utils.SmartWrap.Companion.charactersUsedInLastLine -import utils.SmartWrap.Companion.isLineFull -import utils.SmartWrap.Companion.isSingleLineParagraph -import utils.SmartWrap.Companion.wouldOverFlow +import utils.SimpleSmartWrap.Companion.canFitOnALine +import utils.SimpleSmartWrap.Companion.charactersUsedInLastLine +import utils.SimpleSmartWrap.Companion.isLineFull +import utils.SimpleSmartWrap.Companion.isSingleLineParagraph +import utils.SimpleSmartWrap.Companion.wouldOverFlow import java.lang.System.lineSeparator -class StringUtilsFunctionsTest { +class SimpleSmartWrapUtilsFunctionsTest { @Test fun `given empty paragraph, when isSingleLineParagraph, then return true`() { "".isSingleLineParagraph().shouldBeTrue()