mirror of
https://github.com/ouchadam/small-talk.git
synced 2024-12-22 07:55:36 +01:00
porting messenger domain to reducer
This commit is contained in:
parent
327ba92767
commit
a5b7ede2d8
@ -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>
|
||||
|
@ -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?
|
||||
|
@ -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(),
|
||||
|
@ -1,4 +1,4 @@
|
||||
package app.dapk.st.directory.state
|
||||
package app.dapk.st.core
|
||||
|
||||
import kotlinx.coroutines.Job
|
||||
|
@ -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")
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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) }
|
||||
}
|
||||
}
|
||||
|
@ -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.*
|
||||
|
@ -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
|
||||
|
@ -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) }
|
||||
|
@ -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"))
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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())
|
||||
|
@ -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 -> {
|
||||
|
@ -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
|
||||
}
|
@ -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 {
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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?
|
||||
)
|
@ -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),
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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 -> {
|
||||
|
@ -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)
|
Loading…
Reference in New Issue
Block a user