diff --git a/.github/workflows/release-artefact.yml b/.github/workflows/release-artefact.yml index ccd5d9d..faf089b 100644 --- a/.github/workflows/release-artefact.yml +++ b/.github/workflows/release-artefact.yml @@ -29,8 +29,8 @@ jobs: - name: Build Distribution Zip env: USE_LOCAL_MOCKS: "false" - SERVER_URL: ${{ secrets.SUPABASE_SERVER_URL }} - CLIENT_KEY: ${{ secrets.SUPABASE_CLIENT_KEY }} + SERVER_URL: ${{ secrets.SERVER_URL }} + CLIENT_KEY: ${{ secrets.CLIENT_KEY }} run: ./gradlew distZip --no-daemon - name: Extract Version diff --git a/gradle.properties b/gradle.properties index eaa7d85..b528f1b 100644 --- a/gradle.properties +++ b/gradle.properties @@ -2,4 +2,4 @@ kotlin.code.style=official # Determines whether this project should use hardcoded mocks or Supabase-CLI. # See https://github.com/PPartisan/Roky/wiki/Project-Setup USE_LOCAL_MOCKS=true -version=0.1.0 +version=0.1.2 diff --git a/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt b/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt new file mode 100644 index 0000000..b0cd0cf --- /dev/null +++ b/src/main/kotlin/chatroom/viewmessages/DisplayableMessages.kt @@ -0,0 +1,17 @@ +package chatroom.viewmessages + +import chatserver.ChatRepositories +import kotlinx.coroutines.flow.* + +class DisplayableMessages(private val repositories: ChatRepositories) { + operator fun invoke(): Flow = + repositories.readMessages().observe() + .filter { it.isOk } + .map { it.item.lastOrNull() } + .filterNotNull() + .map { message -> + val usernames = repositories.readProfiles().latest().takeIf { it.isOk }?.item ?: emptyMap() + val user = usernames[message.userId]?.username ?: "anon" + "$user: ${message.message}" + } +} diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt index e14bb92..7dc2d2a 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessages.kt @@ -12,9 +12,10 @@ val viewMessagesModule = ViewMessagesPresenter( windowScope = get().windowScope, dispatchers = get(), - read = get().readMessages(), + read = get(), channel = get().subscribeMessages(), ) } + scoped { DisplayableMessages(get()) } } } diff --git a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt index ac3e8ca..2ccf2be 100644 --- a/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt +++ b/src/main/kotlin/chatroom/viewmessages/ViewMessagesPresenter.kt @@ -4,33 +4,26 @@ import arch.Presenter import arch.RokyDispatchers import chatroom.viewmessages.ViewMessagesViewState.Messages import chatroom.viewmessages.ViewMessagesViewState.NoMessages -import chatserver.MessageResult -import chatserver.ReadChatRepository import chatserver.SubscribeChatRepository import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.map import kotlinx.coroutines.launch import kotlinx.coroutines.withContext class ViewMessagesPresenter( private val windowScope: CoroutineScope, - private val read: ReadChatRepository, + private val read: DisplayableMessages, private val channel: SubscribeChatRepository, dispatchers: RokyDispatchers, ) : Presenter(dispatchers) { override fun onAttach(view: ViewMessagesView) { view.show(NoMessages) windowScope.launch(dispatchers.io) { - read.observe() - .filter { it.isOk } - .map { it.item.lastOrNull()?.let { "${it.userId}: ${it.message}" }.orEmpty() } - .map(::Messages) - .collect { message -> - withContext(dispatchers.main) { - withView { it.show(message) } - } + read().map(::Messages).collect { message -> + withContext(dispatchers.main) { + withView { it.show(message) } } + } } channel.subscribe() } diff --git a/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt b/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt new file mode 100644 index 0000000..b972cac --- /dev/null +++ b/src/test/kotlin/chatroom/viewmessages/DisplayableMessagesTest.kt @@ -0,0 +1,87 @@ +package chatroom.viewmessages + +import chatserver.* +import chatserver.profiles.SupabaseProfilesRepository.Profile +import io.kotest.matchers.collections.shouldContainExactly +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.launch +import kotlinx.coroutines.test.advanceTimeBy +import kotlinx.coroutines.test.runTest +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.Test +import kotlin.time.Duration.Companion.seconds + +@OptIn(ExperimentalCoroutinesApi::class) +class DisplayableMessagesTest { + private lateinit var users: ReadChatRepository + private lateinit var messages: ReadChatRepository + private lateinit var displayableMessages: DisplayableMessages + + @BeforeEach + fun setUp() { + users = + mockk>().also { + every { it.latest() } returns + ProfileResult.ok( + emptyMap(), + ) + } + messages = mockk>().also { every { it.observe() } returns flowOf() } + val repository = + mockk().also { + every { it.readProfiles() } returns users + every { it.readMessages() } returns messages + } + displayableMessages = DisplayableMessages(repository) + } + + @Test + fun `when known user sends message, 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() + backgroundScope.launch { displayableMessages().collect(emissions::add) } + advanceTimeBy(10.seconds) + emissions.shouldContainExactly("Tony KnowsItAll: I will fail everyone") + } + + @Test + fun `when unknown user post message, 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() + backgroundScope.launch { displayableMessages().collect(emissions::add) } + advanceTimeBy(10.seconds) + emissions.shouldContainExactly("anon: I will fail everyone") + } + + @Test + fun `when know user sends message, and changes its 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() + 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"))), + ) + backgroundScope.launch { displayableMessages().collect(emissions::add) } + advanceTimeBy(10.seconds) + emissions.shouldContainExactly( + "Tony KnowsItAll: I will fail everyone", + "Neamah: I hate my student, but not as much as i hate myself", + ) + } +} diff --git a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt index 3d94640..3c5a830 100644 --- a/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt +++ b/src/test/kotlin/chatroom/viewmessages/ViewMessagesPresenterTest.kt @@ -3,9 +3,6 @@ package chatroom.viewmessages import arch.RokyDispatchers import chatroom.viewmessages.ViewMessagesViewState.Messages import chatroom.viewmessages.ViewMessagesViewState.NoMessages -import chatserver.Message -import chatserver.MessageResult -import chatserver.ReadChatRepository import chatserver.SubscribeChatRepository import io.mockk.* import kotlinx.coroutines.CoroutineScope @@ -22,7 +19,7 @@ import kotlin.test.assertTrue @OptIn(ExperimentalCoroutinesApi::class) class ViewMessagesPresenterTest { private lateinit var channel: SubscribeChatRepository - private lateinit var read: ReadChatRepository + private lateinit var read: DisplayableMessages private lateinit var scope: CoroutineScope private lateinit var view: ViewMessagesView private lateinit var presenter: ViewMessagesPresenter @@ -31,7 +28,7 @@ class ViewMessagesPresenterTest { fun setUp() { channel = mockk(relaxed = true) read = mockk() - every { read.observe() } returns flowOf() + every { read() } returns flowOf() view = mockk(relaxed = true) view = mockk(relaxed = true) val dispatchers: RokyDispatchers = @@ -55,8 +52,7 @@ class ViewMessagesPresenterTest { @Test fun `given message exist, when attached, then show message`() = runTest(dispatcher) { - val messages = listOf(Message("Kai", "Biggleswad ez badd")) - every { read.observe() } returns flowOf(MessageResult.ok(messages)) + every { read() } returns flowOf("Kai: Biggleswad ez badd") presenter.attach(view) advanceUntilIdle() verifyOrder { @@ -65,24 +61,6 @@ class ViewMessagesPresenterTest { } } - @Test - fun `given message exist, and message is not ok, when attached, then show nothing`() = - runTest(dispatcher) { - every { read.observe() } returns flowOf(MessageResult.fail(RuntimeException("Connection with server failed"))) - presenter.attach(view) - advanceUntilIdle() - verifyOrder { - view.show(NoMessages) - } - verify(exactly = 0) { - view.show( - withArg { - assertTrue { it is Messages } - }, - ) - } - } - @Test fun `when attached, then subscribe to chat messages`() = runTest(dispatcher) { @@ -101,12 +79,10 @@ class ViewMessagesPresenterTest { @Test fun `given two messages exist, when attached, then show two messages`() = runTest(dispatcher) { - val messageOne = listOf(Message("Kai", "Pokemon good")) - val messageTwo = listOf(Message("Allie", "Barry is the best")) - every { read.observe() } returns + every { read() } returns flowOf( - MessageResult.ok(messageOne), - MessageResult.ok(messageOne + messageTwo), + "Kai: Pokemon good", + "Allie: Barry is the best", ) presenter.attach(view)