porting messenger domain to reducer

This commit is contained in:
Adam Brown 2022-11-01 10:35:48 +00:00
parent 327ba92767
commit a5b7ede2d8
24 changed files with 289 additions and 267 deletions

View File

@ -11,7 +11,7 @@ interface ChatEngine : TaskRunner {
fun directory(): Flow<DirectoryState>
fun invites(): Flow<InviteState>
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerPageState>
fun notificationsMessages(): Flow<UnreadNotifications>
fun notificationsInvites(): Flow<InviteNotification>

View File

@ -84,7 +84,7 @@ sealed interface ImportResult {
data class Update(val importedKeysCount: Long) : ImportResult
}
data class MessengerState(
data class MessengerPageState(
val self: UserId,
val roomState: RoomState,
val typing: Typing?

View File

@ -7,7 +7,7 @@ fun aMessengerState(
self: UserId = aUserId(),
roomState: RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)
) = MessengerPageState(self, roomState, typing)
fun aRoomOverview(
roomId: RoomId = aRoomId(),

View File

@ -1,4 +1,4 @@
package app.dapk.st.directory.state
package app.dapk.st.core
import kotlinx.coroutines.Job

View File

@ -24,7 +24,7 @@ inline fun <reified VM : ViewModel> ComponentActivity.viewModel(
inline fun <reified S, E> ComponentActivity.state(
noinline factory: () -> StateViewModel<S, E>
noinline factory: () -> State<S, E>
): Lazy<State<S, E>> {
val factoryPromise = object : Factory {
@Suppress("UNCHECKED_CAST")

View File

@ -14,8 +14,9 @@ fun <S> createStore(reducerFactory: ReducerFactory<S>, coroutineScope: Coroutine
override suspend fun dispatch(action: Action) {
scope.coroutineScope.launch {
state = reducer.reduce(action).also { nextState ->
println("!!! next state: $nextState")
subscribers.forEach { it.invoke(nextState) }
if (nextState != state) {
subscribers.forEach { it.invoke(nextState) }
}
}
}
}

View File

@ -2,11 +2,9 @@ package app.dapk.st.directory
import android.content.Context
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.StateViewModel
import app.dapk.st.core.createStateViewModel
import app.dapk.st.directory.state.DirectoryEvent
import app.dapk.st.directory.state.DirectoryScreenState
import app.dapk.st.directory.state.JobBag
import app.dapk.st.core.JobBag
import app.dapk.st.directory.state.DirectoryState
import app.dapk.st.directory.state.directoryReducer
import app.dapk.st.engine.ChatEngine
@ -15,7 +13,7 @@ class DirectoryModule(
private val chatEngine: ChatEngine,
) : ProvidableModule {
fun directoryViewModel(): StateViewModel<DirectoryScreenState, DirectoryEvent> {
fun directoryState(): DirectoryState {
return createStateViewModel { directoryReducer(chatEngine, ShortcutHandler(context), JobBag(), it) }
}
}

View File

@ -1,5 +1,6 @@
package app.dapk.st.directory.state
import app.dapk.st.core.JobBag
import app.dapk.st.directory.ShortcutHandler
import app.dapk.st.engine.ChatEngine
import app.dapk.state.*

View File

@ -1,9 +1,9 @@
package app.dapk.st.directory
import app.dapk.st.core.JobBag
import app.dapk.st.directory.state.*
import app.dapk.st.engine.DirectoryItem
import app.dapk.st.engine.UnreadCount
import app.dapk.state.ReducerFactory
import fake.FakeChatEngine
import fixture.aRoomOverview
import io.mockk.mockk

View File

@ -13,10 +13,9 @@ import androidx.compose.ui.Modifier
import androidx.lifecycle.lifecycleScope
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.viewModel
import app.dapk.st.core.state
import app.dapk.st.core.viewModel
import app.dapk.st.directory.DirectoryModule
import app.dapk.st.directory.state.DirectoryState
import app.dapk.st.login.LoginModule
import app.dapk.st.profile.ProfileModule
import kotlinx.coroutines.flow.launchIn
@ -24,7 +23,7 @@ import kotlinx.coroutines.flow.onEach
class MainActivity : DapkActivity() {
private val directoryViewModel: DirectoryState by state { module<DirectoryModule>().directoryViewModel() }
private val directoryViewModel by state { module<DirectoryModule>().directoryState() }
private val loginViewModel by viewModel { module<LoginModule>().loginViewModel() }
private val profileViewModel by viewModel { module<ProfileModule>().profileViewModel() }
private val homeViewModel by viewModel { module<HomeModule>().homeViewModel(directoryViewModel, loginViewModel, profileViewModel) }

View File

@ -6,6 +6,7 @@ dependencies {
implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel")
implementation project(":domains:store")
implementation project(":domains:state")
implementation project(":core")
implementation project(":features:navigator")
implementation project(":design-library")
@ -16,6 +17,7 @@ dependencies {
androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:state"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))

View File

@ -14,6 +14,10 @@ import app.dapk.st.core.*
import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.gallery.GetImageFromGallery
import app.dapk.st.messenger.state.ComposerStateChange
import app.dapk.st.messenger.state.MessengerEvent
import app.dapk.st.messenger.state.MessengerScreenState
import app.dapk.st.messenger.state.MessengerState
import app.dapk.st.navigator.MessageAttachment
import coil.request.ImageRequest
import kotlinx.parcelize.Parcelize
@ -23,7 +27,7 @@ val LocalImageRequestFactory = staticCompositionLocalOf<ImageRequest.Builder> {
class MessengerActivity : DapkActivity() {
private val module by unsafeLazy { module<MessengerModule>() }
private val viewModel by viewModel { module.messengerViewModel() }
private val state by state { module.messengerState(readPayload()) }
companion object {
@ -54,8 +58,8 @@ class MessengerActivity : DapkActivity() {
val galleryLauncher = registerForActivityResult(GetImageFromGallery()) {
it?.let { uri ->
viewModel.post(
MessengerAction.ComposerImageUpdate(
state.dispatch(
ComposerStateChange.SelectAttachmentToSend(
MessageAttachment(
AndroidUri(it.toString()),
MimeType.Image,
@ -68,7 +72,7 @@ class MessengerActivity : DapkActivity() {
setContent {
Surface(Modifier.fillMaxSize()) {
CompositionLocalProvider(LocalImageRequestFactory provides factory) {
MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator, galleryLauncher)
MessengerScreen(state, navigator, galleryLauncher)
}
}
}

View File

@ -3,10 +3,14 @@ package app.dapk.st.messenger
import android.content.ClipboardManager
import android.content.Context
import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.JobBag
import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.createStateViewModel
import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.state.MessengerState
import app.dapk.st.messenger.state.messengerReducer
class MessengerModule(
private val chatEngine: ChatEngine,
@ -15,13 +19,19 @@ class MessengerModule(
private val deviceMeta: DeviceMeta,
) : ProvidableModule {
internal fun messengerViewModel(): MessengerViewModel {
return MessengerViewModel(
chatEngine,
messageOptionsStore,
CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager),
deviceMeta,
)
internal fun messengerState(launchPayload: MessagerActivityPayload): MessengerState {
return createStateViewModel {
messengerReducer(
JobBag(),
chatEngine,
CopyToClipboard(context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager),
deviceMeta,
messageOptionsStore,
RoomId(launchPayload.roomId),
launchPayload.attachments,
it
)
}
}
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, roomId, chatEngine.mediaDecrypter())

View File

@ -45,14 +45,13 @@ import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.*
import app.dapk.st.engine.MessageMeta
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomState
import app.dapk.st.matrix.common.RichText
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.messenger.state.*
import app.dapk.st.navigator.Navigator
import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest
@ -62,18 +61,16 @@ import kotlin.math.roundToInt
@Composable
internal fun MessengerScreen(
roomId: RoomId,
attachments: List<MessageAttachment>?,
viewModel: MessengerViewModel,
viewModel: MessengerState,
navigator: Navigator,
galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>
) {
val state = viewModel.state
val state = viewModel.current
viewModel.ObserveEvents(galleryLauncher)
LifecycleEffect(
onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) },
onStop = { viewModel.post(MessengerAction.OnMessengerGone) }
onStart = { viewModel.dispatch(ComponentLifecycle.Visible) },
onStop = { viewModel.dispatch(ComponentLifecycle.Gone) }
)
val roomTitle = when (val roomState = state.roomState) {
@ -82,10 +79,10 @@ internal fun MessengerScreen(
}
val messageActions = MessageActions(
onReply = { viewModel.post(MessengerAction.ComposerEnterReplyMode(it)) },
onDismiss = { viewModel.post(MessengerAction.ComposerExitReplyMode) },
onLongClick = { viewModel.post(MessengerAction.CopyToClipboard(it)) },
onImageClick = { viewModel.selectImage(it) }
onReply = { viewModel.dispatch(ComposerStateChange.ReplyMode.Enter(it)) },
onDismiss = { viewModel.dispatch(ComposerStateChange.ReplyMode.Exit) },
onLongClick = { viewModel.dispatch(ScreenAction.CopyToClipboard(it)) },
onImageClick = { viewModel.dispatch(ComposerStateChange.ImagePreview.Show(it)) }
)
Column {
@ -97,12 +94,12 @@ internal fun MessengerScreen(
when (state.composerState) {
is ComposerState.Text -> {
Room(state.roomState, messageActions, onRetry = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) })
Room(state.roomState, messageActions, onRetry = { viewModel.dispatch(ComponentLifecycle.Visible) })
TextComposer(
state.composerState,
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
onSend = { viewModel.post(MessengerAction.ComposerSendText) },
onAttach = { viewModel.startAttachment() },
onTextChange = { viewModel.dispatch(ComposerStateChange.TextUpdate(it)) },
onSend = { viewModel.dispatch(ScreenAction.SendMessage) },
onAttach = { viewModel.dispatch(ScreenAction.OpenGalleryPicker) },
messageActions = messageActions,
)
}
@ -110,8 +107,8 @@ internal fun MessengerScreen(
is ComposerState.Attachments -> {
AttachmentComposer(
state.composerState,
onSend = { viewModel.post(MessengerAction.ComposerSendText) },
onCancel = { viewModel.post(MessengerAction.ComposerClear) }
onSend = { viewModel.dispatch(ScreenAction.SendMessage) },
onCancel = { viewModel.dispatch(ComposerStateChange.Clear) }
)
}
}
@ -124,10 +121,10 @@ internal fun MessengerScreen(
else -> {
Box(Modifier.fillMaxSize().background(Color.Black)) {
BackHandler(onBack = { viewModel.unselectImage() })
BackHandler(onBack = { viewModel.dispatch(ComposerStateChange.ImagePreview.Hide) })
ZoomableImage(state.viewerState)
Toolbar(
onNavigate = { viewModel.unselectImage() },
onNavigate = { viewModel.dispatch(ComposerStateChange.ImagePreview.Hide) },
title = state.viewerState.event.event.authorName,
color = Color.Black.copy(alpha = 0.4f),
)
@ -199,13 +196,13 @@ private fun ZoomableImage(viewerState: ViewerState) {
}
@Composable
private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>) {
private fun MessengerState.ObserveEvents(galleryLauncher: ActivityResultLauncher<ImageGalleryActivityPayload>) {
val context = LocalContext.current
StartObserving {
this@ObserveEvents.events.launch {
when (it) {
MessengerEvent.SelectImageAttachment -> {
state.roomState.takeIfContent()?.let {
current.roomState.takeIfContent()?.let {
galleryLauncher.launch(ImageGalleryActivityPayload(it.roomState.roomOverview.roomName ?: ""))
}
}
@ -219,7 +216,7 @@ private fun MessengerViewModel.ObserveEvents(galleryLauncher: ActivityResultLaun
}
@Composable
private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>, messageActions: MessageActions, onRetry: () -> Unit) {
private fun ColumnScope.Room(roomStateLce: Lce<MessengerPageState>, messageActions: MessageActions, onRetry: () -> Unit) {
when (val state = roomStateLce) {
is Lce.Loading -> CenteredLoading()
is Lce.Content -> {

View File

@ -1,197 +0,0 @@
package app.dapk.st.messenger
import android.os.Build
import androidx.lifecycle.viewModelScope
import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.Lce
import app.dapk.st.core.asString
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.asString
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch
internal class MessengerViewModel(
private val chatEngine: ChatEngine,
private val messageOptionsStore: MessageOptionsStore,
private val copyToClipboard: CopyToClipboard,
private val deviceMeta: DeviceMeta,
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
initialState = MessengerScreenState(
roomId = null,
roomState = Lce.Loading(),
composerState = ComposerState.Text(value = "", reply = null),
viewerState = null,
),
factory = factory,
) {
private var syncJob: Job? = null
fun post(action: MessengerAction) {
when (action) {
is MessengerAction.OnMessengerVisible -> start(action)
MessengerAction.OnMessengerGone -> syncJob?.cancel()
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue, composerState.reply)) }
MessengerAction.ComposerSendText -> sendMessage()
MessengerAction.ComposerClear -> resetComposer()
is MessengerAction.ComposerImageUpdate -> updateState {
copy(
composerState = ComposerState.Attachments(
listOf(action.newValue),
composerState.reply
)
)
}
is MessengerAction.ComposerEnterReplyMode -> updateState {
copy(
composerState = when (composerState) {
is ComposerState.Attachments -> composerState.copy(reply = action.replyingTo)
is ComposerState.Text -> composerState.copy(reply = action.replyingTo)
}
)
}
MessengerAction.ComposerExitReplyMode -> updateState {
copy(
composerState = when (composerState) {
is ComposerState.Attachments -> composerState.copy(reply = null)
is ComposerState.Text -> composerState.copy(reply = null)
}
)
}
is MessengerAction.CopyToClipboard -> {
viewModelScope.launch {
when (val result = action.model.findCopyableContent()) {
is CopyableResult.Content -> {
copyToClipboard.copy(result.value)
if (deviceMeta.apiVersion <= Build.VERSION_CODES.S_V2) {
_events.emit(MessengerEvent.Toast("Copied to clipboard"))
}
}
CopyableResult.NothingToCopy -> _events.emit(MessengerEvent.Toast("Nothing to copy"))
}
}
}
}
}
private fun start(action: MessengerAction.OnMessengerVisible) {
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
viewModelScope.launch {
syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
.onEach { updateState { copy(roomState = Lce.Content(it)) } }
.launchIn(this)
}
}
private fun sendMessage() {
when (val composerState = state.composerState) {
is ComposerState.Text -> {
val copy = composerState.copy()
updateState { copy(composerState = composerState.copy(value = "", reply = null)) }
state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState
viewModelScope.launch {
chatEngine.send(
message = SendMessage.TextMessage(
content = copy.value,
reply = copy.reply?.let {
SendMessage.TextMessage.Reply(
author = it.author,
originalMessage = when (it) {
is RoomEvent.Image -> TODO()
is RoomEvent.Reply -> TODO()
is RoomEvent.Message -> it.content.asString()
is RoomEvent.Encrypted -> error("Should never happen")
},
eventId = it.eventId,
timestampUtc = it.utcTimestamp,
)
}
),
room = roomState.roomOverview,
)
}
}
}
is ComposerState.Attachments -> {
val copy = composerState.copy()
resetComposer()
state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState
viewModelScope.launch {
chatEngine.send(SendMessage.ImageMessage(uri = copy.values.first().uri.value), roomState.roomOverview)
}
}
}
}
}
private fun resetComposer() {
updateState { copy(composerState = ComposerState.Text("", reply = null)) }
}
fun startAttachment() {
viewModelScope.launch {
_events.emit(MessengerEvent.SelectImageAttachment)
}
}
fun selectImage(image: BubbleModel.Image) {
updateState {
copy(viewerState = ViewerState(image))
}
}
fun unselectImage() {
updateState {
copy(viewerState = null)
}
}
}
private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
is BubbleModel.Image -> CopyableResult.NothingToCopy
is BubbleModel.Reply -> this.reply.findCopyableContent()
is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content.asString()))
}
private sealed interface CopyableResult {
object NothingToCopy : CopyableResult
data class Content(val value: CopyToClipboard.Copyable) : CopyableResult
}
sealed interface MessengerAction {
data class ComposerTextUpdate(val newValue: String) : MessengerAction
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
object ComposerExitReplyMode : MessengerAction
data class CopyToClipboard(val model: BubbleModel) : MessengerAction
data class ComposerImageUpdate(val newValue: MessageAttachment) : MessengerAction
object ComposerSendText : MessengerAction
object ComposerClear : MessengerAction
data class OnMessengerVisible(val roomId: RoomId, val attachments: List<MessageAttachment>?) : MessengerAction
object OnMessengerGone : MessengerAction
}

View File

@ -9,16 +9,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Surface
import androidx.compose.ui.Modifier
import app.dapk.st.core.DapkActivity
import app.dapk.st.core.module
import app.dapk.st.core.viewModel
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.messenger.MessengerModule
import kotlinx.parcelize.Parcelize
class RoomSettingsActivity : DapkActivity() {
private val viewModel by viewModel { module<MessengerModule>().messengerViewModel() }
companion object {
fun newInstance(context: Context, roomId: RoomId): Intent {
return Intent(context, RoomSettingsActivity::class.java).apply {

View File

@ -0,0 +1,38 @@
package app.dapk.st.messenger.state
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.navigator.MessageAttachment
import app.dapk.state.Action
sealed interface ScreenAction : Action {
data class CopyToClipboard(val model: BubbleModel) : ScreenAction
object SendMessage : ScreenAction
object OpenGalleryPicker : ScreenAction
}
sealed interface ComponentLifecycle : Action {
object Visible : ComponentLifecycle
object Gone : ComponentLifecycle
}
sealed interface MessagesStateChange : Action {
data class Content(val content: MessengerPageState) : ComposerStateChange
}
sealed interface ComposerStateChange : Action {
data class SelectAttachmentToSend(val newValue: MessageAttachment) : ComposerStateChange
data class TextUpdate(val newValue: String) : ComposerStateChange
object Clear : ComposerStateChange
sealed interface ReplyMode : ComposerStateChange {
data class Enter(val replyingTo: RoomEvent) : ReplyMode
object Exit : ReplyMode
}
sealed interface ImagePreview : ComposerStateChange {
data class Show(val image: BubbleModel.Image) : ImagePreview
object Hide : ImagePreview
}
}

View File

@ -0,0 +1,169 @@
package app.dapk.st.messenger.state
import android.os.Build
import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.JobBag
import app.dapk.st.core.Lce
import app.dapk.st.core.asString
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.engine.ChatEngine
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.asString
import app.dapk.st.messenger.CopyToClipboard
import app.dapk.st.navigator.MessageAttachment
import app.dapk.state.*
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
internal fun messengerReducer(
jobBag: JobBag,
chatEngine: ChatEngine,
copyToClipboard: CopyToClipboard,
deviceMeta: DeviceMeta,
messageOptionsStore: MessageOptionsStore,
roomId: RoomId,
initialAttachments: List<MessageAttachment>?,
eventEmitter: suspend (MessengerEvent) -> Unit,
): ReducerFactory<MessengerScreenState> {
return createReducer(
initialState = MessengerScreenState(
roomId = roomId,
roomState = Lce.Loading(),
composerState = initialAttachments?.let { ComposerState.Attachments(it, null) } ?: ComposerState.Text(value = "", reply = null),
viewerState = null,
),
async(ComponentLifecycle::class) { action ->
val state = getState()
when (action) {
is ComponentLifecycle.Visible -> {
jobBag.add("messages", chatEngine.messages(state.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
.onEach { dispatch(MessagesStateChange.Content(it)) }
.launchIn(coroutineScope)
)
}
ComponentLifecycle.Gone -> jobBag.cancel("messages")
}
},
change(MessagesStateChange.Content::class) { action, state ->
state.copy(roomState = Lce.Content(action.content))
},
change(ComposerStateChange.SelectAttachmentToSend::class) { action, state ->
state.copy(
composerState = ComposerState.Attachments(
listOf(action.newValue),
state.composerState.reply,
)
)
},
change(ComposerStateChange.ImagePreview::class) { action, state ->
when (action) {
is ComposerStateChange.ImagePreview.Show -> state.copy(viewerState = ViewerState(action.image))
ComposerStateChange.ImagePreview.Hide -> state.copy(viewerState = null)
}
},
change(ComposerStateChange.TextUpdate::class) { action, state ->
state.copy(composerState = ComposerState.Text(action.newValue, state.composerState.reply))
},
change(ComposerStateChange.Clear::class) { _, state ->
state.copy(composerState = ComposerState.Text("", reply = null))
},
change(ComposerStateChange.ReplyMode::class) { action, state ->
when (action) {
is ComposerStateChange.ReplyMode.Enter -> state.copy(
composerState = when (state.composerState) {
is ComposerState.Attachments -> state.composerState.copy(reply = action.replyingTo)
is ComposerState.Text -> state.composerState.copy(reply = action.replyingTo)
}
)
ComposerStateChange.ReplyMode.Exit -> state.copy(
composerState = when (state.composerState) {
is ComposerState.Attachments -> state.composerState.copy(reply = null)
is ComposerState.Text -> state.composerState.copy(reply = null)
}
)
}
},
sideEffect(ScreenAction.CopyToClipboard::class) { action, state ->
when (val result = action.model.findCopyableContent()) {
is CopyableResult.Content -> {
copyToClipboard.copy(result.value)
if (deviceMeta.apiVersion <= Build.VERSION_CODES.S_V2) {
eventEmitter.invoke(MessengerEvent.Toast("Copied to clipboard"))
}
}
CopyableResult.NothingToCopy -> eventEmitter.invoke(MessengerEvent.Toast("Nothing to copy"))
}
},
sideEffect(ScreenAction.OpenGalleryPicker::class) { _, _ ->
eventEmitter.invoke(MessengerEvent.SelectImageAttachment)
},
async(ScreenAction.SendMessage::class) {
val state = getState()
when (val composerState = state.composerState) {
is ComposerState.Text -> {
dispatch(ComposerStateChange.Clear)
state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState
chatEngine.send(
message = SendMessage.TextMessage(
content = composerState.value,
reply = composerState.reply?.let {
SendMessage.TextMessage.Reply(
author = it.author,
originalMessage = when (it) {
is RoomEvent.Image -> TODO()
is RoomEvent.Reply -> TODO()
is RoomEvent.Message -> it.content.asString()
is RoomEvent.Encrypted -> error("Should never happen")
},
eventId = it.eventId,
timestampUtc = it.utcTimestamp,
)
}
),
room = roomState.roomOverview,
)
}
}
is ComposerState.Attachments -> {
dispatch(ComposerStateChange.Clear)
state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState
chatEngine.send(SendMessage.ImageMessage(uri = composerState.values.first().uri.value), roomState.roomOverview)
}
}
}
},
)
}
private fun BubbleModel.findCopyableContent(): CopyableResult = when (this) {
is BubbleModel.Encrypted -> CopyableResult.NothingToCopy
is BubbleModel.Image -> CopyableResult.NothingToCopy
is BubbleModel.Reply -> this.reply.findCopyableContent()
is BubbleModel.Text -> CopyableResult.Content(CopyToClipboard.Copyable.Text(this.content.asString()))
}
private sealed interface CopyableResult {
object NothingToCopy : CopyableResult
data class Content(val value: CopyToClipboard.Copyable) : CopyableResult
}

View File

@ -1,15 +1,18 @@
package app.dapk.st.messenger
package app.dapk.st.messenger.state
import app.dapk.st.core.Lce
import app.dapk.st.core.State
import app.dapk.st.design.components.BubbleModel
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.navigator.MessageAttachment
typealias MessengerState = State<MessengerScreenState, MessengerEvent>
data class MessengerScreenState(
val roomId: RoomId?,
val roomState: Lce<MessengerState>,
val roomId: RoomId,
val roomState: Lce<MessengerPageState>,
val composerState: ComposerState,
val viewerState: ViewerState?
)

View File

@ -4,12 +4,14 @@ import ViewModelTest
import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.MessengerPageState
import app.dapk.st.engine.RoomState
import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.messenger.state.ComposerState
import app.dapk.st.messenger.state.MessengerScreenState
import fake.FakeChatEngine
import fake.FakeMessageOptionsStore
import fixture.*
@ -112,7 +114,7 @@ class MessengerViewModelTest {
}
fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, messageContent: String?) = MessengerScreenState(
fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerPageState, messageContent: String?) = MessengerScreenState(
roomId = roomId,
roomState = Lce.Content(roomState),
composerState = ComposerState.Text(value = messageContent ?: "", reply = null),

View File

@ -48,7 +48,7 @@ class MatrixEngine internal constructor(
override fun directory() = directoryUseCase.value.state()
override fun invites() = inviteUseCase.value.invites()
override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> {
override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerPageState> {
return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts)
}

View File

@ -19,7 +19,7 @@ class ReadMarkingTimeline(
private val roomService: RoomService,
) {
fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerState> {
fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerPageState> {
return flow {
val credentials = credentialsStore.credentials()!!
roomStore.markRead(roomId)
@ -37,7 +37,7 @@ class ReadMarkingTimeline(
}
}
private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState, isReadReceiptsDisabled: Boolean): Deferred<*> {
private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerPageState, isReadReceiptsDisabled: Boolean): Deferred<*> {
return coroutineScope {
async {
runCatching {
@ -50,7 +50,7 @@ class ReadMarkingTimeline(
}
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
private fun MessengerPageState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
.filterIsInstance<RoomEvent.Message>()
.filterNot { it.author.id == self }
.firstOrNull()

View File

@ -10,7 +10,7 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerPageState>
internal class TimelineUseCaseImpl(
private val syncService: SyncService,
@ -19,13 +19,13 @@ internal class TimelineUseCaseImpl(
private val mergeWithLocalEchosUseCase: MergeWithLocalEchosUseCase
) : ObserveTimelineUseCase {
override fun invoke(roomId: RoomId, userId: UserId): Flow<MessengerState> {
override fun invoke(roomId: RoomId, userId: UserId): Flow<MessengerPageState> {
return combine(
roomDatasource(roomId),
messageService.localEchos(roomId),
syncService.events(roomId)
) { roomState, localEchos, events ->
MessengerState(
MessengerPageState(
roomState = when {
localEchos.isEmpty() -> roomState
else -> {

View File

@ -135,4 +135,4 @@ fun aMessengerState(
self: UserId = aUserId(),
roomState: app.dapk.st.engine.RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)
) = MessengerPageState(self, roomState, typing)