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
3 changes: 1 addition & 2 deletions src/main/kotlin/chatroom/users/Users.kt
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,10 @@ val usersModule =
module {
scope<ChatroomWindow> {
scoped { UsersPanel(presenter = get()) }
scoped { UsersListUseCase() }
scoped {
UsersListPresenter(
users = get(),
truncate = get(),
repository = get(),
scope = get<ChatroomWindow>().windowScope,
dispatchers = get(),
)
Expand Down
30 changes: 20 additions & 10 deletions src/main/kotlin/chatroom/users/UsersListPresenter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,47 @@ package chatroom.users
import arch.Presenter
import arch.RokyDispatchers
import chatroom.users.UsersViewState.*
import chatserver.ChatRepositories
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class UsersListPresenter(
private val users: UsersListUseCase,
private val truncate: UsersListTruncation,
private val repository: ChatRepositories,
private val scope: CoroutineScope,
dispatchers: RokyDispatchers,
) : Presenter<UsersListView>(dispatchers) {
private var job: Job? = null

override fun onAttach(view: UsersListView) {
view.show(Empty)
scope.launch(dispatchers.io) {
users()
.truncateUsernames()
.map(::Users).collect { state ->
withContext(dispatchers.main) {
show(state)

job =
scope.launch(dispatchers.io) {
repository.subscribePresence().subscribe()
repository.readPresence().observe()
.map { it.item.toList() }
.truncateUsernames()
.map(::Users)
.collect { state ->
withContext(dispatchers.main) {
show(state)
}
}
}
}
}
}

private fun show(viewState: UsersViewState) {
withView { it.show(viewState) }
}

override fun onDetach(view: UsersListView) {
// Deliberately empty
repository.subscribePresence().unsubscribe()
job?.cancel()
}

private fun Flow<List<String>>.truncateUsernames() =
Expand Down
24 changes: 0 additions & 24 deletions src/main/kotlin/chatroom/users/UsersListUseCase.kt

This file was deleted.

6 changes: 6 additions & 0 deletions src/main/kotlin/chatserver/ChatRepositories.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
package chatserver

import chatserver.messages.LocalChatMessages
import chatserver.presence.LocalPresenceRepository
import chatserver.profiles.LocalProfilesRepository

class ChatRepositories(
private val profiles: LocalProfilesRepository,
private val messages: LocalChatMessages,
private val presence: LocalPresenceRepository,
) {
fun writeProfiles(): WriteChatRepository<String> = profiles

Expand All @@ -18,4 +20,8 @@ class ChatRepositories(
fun subscribeMessages(): SubscribeChatRepository = messages

fun writeMessages(): WriteChatRepository<String> = messages

fun subscribePresence(): SubscribeChatRepository = presence

fun readPresence(): ReadChatRepository<PresenceResult> = presence
}
3 changes: 2 additions & 1 deletion src/main/kotlin/chatserver/ChatServer.kt
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
package chatserver

import chatserver.messages.chatServerMessagesModule
import chatserver.presence.chatServerPresenceModule
import chatserver.profiles.chatServerProfilesModule
import org.koin.core.module.dsl.factoryOf
import org.koin.dsl.module

val chatServerModule =
module {
includes(chatServerProfilesModule, chatServerMessagesModule)
includes(chatServerProfilesModule, chatServerMessagesModule, chatServerPresenceModule)
factoryOf(::ChatRepositories)
}
8 changes: 4 additions & 4 deletions src/main/kotlin/chatserver/PresenceResult.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@ import chatserver.ReadChatRepository.ReadResult

data class PresenceResult(
override val isOk: Boolean,
override val item: Map<String, Boolean>,
override val item: Set<String>,
override val error: Exception?,
) :
ReadResult<Map<String, Boolean>> {
ReadResult<Set<String>> {
companion object {
fun ok(item: Map<String, Boolean>): PresenceResult = PresenceResult(true, item, null)
fun ok(item: Set<String>): PresenceResult = PresenceResult(true, item, null)

fun fail(e: Exception? = null): PresenceResult = PresenceResult(false, emptyMap(), e)
fun fail(e: Exception? = null): PresenceResult = PresenceResult(false, emptySet(), e)
}
}
25 changes: 25 additions & 0 deletions src/main/kotlin/chatserver/presence/ChatServerPresence.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package chatserver.presence

import arch.RokyDispatchers
import chatserver.messages.LocalChatMessages.Companion.sampleUsers
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.SupervisorJob
import org.koin.dsl.module

val chatServerPresenceModule =
module {
single {
LocalPresenceRepository(
scope =
CoroutineScope(
SupervisorJob() + get<RokyDispatchers>().default,
),
dispatchers = get<RokyDispatchers>(),
users = {
with(sampleUsers) {
shuffled().subList(0, (4..<size).random()).toSet()
}
},
)
}
}
42 changes: 42 additions & 0 deletions src/main/kotlin/chatserver/presence/LocalPresenceRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package chatserver.presence

import arch.RokyDispatchers
import chatserver.PresenceResult
import chatserver.ReadChatRepository
import chatserver.SubscribeChatRepository
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch
import kotlin.time.Duration.Companion.seconds

class LocalPresenceRepository(
private val users: () -> Set<String>,
private val scope: CoroutineScope,
private val dispatchers: RokyDispatchers,
) : SubscribeChatRepository, ReadChatRepository<PresenceResult> {
private val state: MutableStateFlow<PresenceResult> = MutableStateFlow(PresenceResult.ok(emptySet()))
private var job: Job? = null

override fun subscribe() {
unsubscribe()
job =
scope.launch(dispatchers.default) {
while (true) {
state.value = PresenceResult.ok(users())
delay(10.seconds)
}
}
}

override fun unsubscribe() {
job?.cancel()
}

override fun latest(): PresenceResult = state.value

override fun observe(): Flow<PresenceResult> = state.asStateFlow()
}
28 changes: 18 additions & 10 deletions src/test/kotlin/chatroom/users/UsersListPresenterTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package chatroom.users
import arch.RokyDispatchers
import chatroom.users.UsersViewState.Empty
import chatroom.users.UsersViewState.Users
import chatserver.*
import coAnswersDelayed
import io.mockk.*
import kotlinx.coroutines.CoroutineScope
Expand All @@ -18,22 +19,29 @@ import org.junit.jupiter.api.Test
@OptIn(ExperimentalCoroutinesApi::class)
class UsersListPresenterTest {
private lateinit var scope: CoroutineScope
private lateinit var listUsers: UsersListUseCase
private lateinit var subscribePresence: SubscribeChatRepository
private lateinit var readPresence: ReadChatRepository<PresenceResult>
private lateinit var view: UsersListView
private lateinit var presenter: UsersListPresenter

@BeforeEach
fun setUp() {
view = mockk(relaxed = true)
listUsers = mockk(relaxed = true)
coEvery { listUsers() } coAnswersDelayed { flowOf() }
val repositories: ChatRepositories = mockk(relaxed = true)
subscribePresence = mockk()
readPresence = mockk()
every { repositories.subscribePresence() } returns subscribePresence
every { subscribePresence.subscribe() } just runs
every { subscribePresence.unsubscribe() } just runs
every { repositories.readPresence() } returns readPresence
coEvery { readPresence.observe() } coAnswersDelayed { flowOf() }
val dispatchers: RokyDispatchers =
mockk<RokyDispatchers>().apply {
every { main } returns dispatcher
every { io } returns dispatcher
}
scope = CoroutineScope(dispatcher)
presenter = UsersListPresenter(listUsers, UsersListTruncation(10), scope, dispatchers)
presenter = UsersListPresenter(UsersListTruncation(10), repositories, scope, dispatchers)
}

@Test
Expand All @@ -46,7 +54,7 @@ class UsersListPresenterTest {
@Test
fun `when users list is empty, then show empty list of users`() =
runTest(dispatcher) {
coEvery { listUsers() } coAnswersDelayed { flowOf(emptyList()) }
coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(emptySet())) }
presenter.attach(view)
advanceUntilIdle()
verifyOrder {
Expand All @@ -63,16 +71,16 @@ class UsersListPresenterTest {
@Test
fun `when users list is not empty, then show list of users`() =
runTest(dispatcher) {
val userList = listOf("Robert", "Tom", "Allie", "Kai")
coEvery { listUsers() } coAnswersDelayed { flowOf(userList) }
val userSet = setOf("Robert", "Tom", "Allie", "Kai")
coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(userSet)) }
presenter.attach(view)
advanceUntilIdle()
verifyOrder {
view.show(Empty)
view.show(
withArg {
assertTrue { it is Users }
assertTrue { (it as Users).users == userList }
assertTrue { (it as Users).users.toSet() == userSet }
},
)
}
Expand All @@ -81,8 +89,8 @@ class UsersListPresenterTest {
@Test
fun `when users list contains very long usernames, then truncate long usernames only`() =
runTest(dispatcher) {
val userList = listOf("Allie", "Kai", "A very very long username")
coEvery { listUsers() } coAnswersDelayed { flowOf(userList) }
val userSet = setOf("Allie", "Kai", "A very very long username")
coEvery { readPresence.observe() } coAnswersDelayed { flowOf(PresenceResult.ok(userSet)) }
presenter.attach(view)
advanceUntilIdle()
verifyOrder {
Expand Down
54 changes: 0 additions & 54 deletions src/test/kotlin/chatroom/users/UsersListUseCaseTest.kt

This file was deleted.

Loading