From 6fed8a35ce8462b0af7d4c8a6de7d3fb690801d2 Mon Sep 17 00:00:00 2001 From: Adam Brown Date: Wed, 8 Jun 2022 20:31:07 +0100 Subject: [PATCH] wip, passing the image urls down to the matrix client layer --- .idea/inspectionProfiles/Project_Default.xml | 29 ++++++ .../kotlin/app/dapk/st/graph/AppModule.kt | 8 ++ .../main/kotlin/app/dapk/st/core/MimeType.kt | 5 ++ .../domain/localecho/LocalEchoPersistence.kt | 2 + .../app/dapk/st/directory/DirectoryUseCase.kt | 1 + .../app/dapk/st/messenger/LocalEchoMapper.kt | 1 + .../dapk/st/messenger/MessengerActivity.kt | 17 ++-- .../app/dapk/st/messenger/MessengerScreen.kt | 89 +++++++++++++++++-- .../app/dapk/st/messenger/MessengerState.kt | 5 ++ .../dapk/st/messenger/MessengerViewModel.kt | 31 ++++++- .../roomsettings/RoomSettingsActivity.kt | 2 +- features/navigator/build.gradle | 1 + .../kotlin/app/dapk/st/navigator/Navigator.kt | 33 ++++++- features/share-entry/build.gradle | 1 + .../app/dapk/st/share/FetchRoomsUseCase.kt | 22 +++++ .../app/dapk/st/share/ShareEntryActivity.kt | 4 +- .../app/dapk/st/share/ShareEntryScreen.kt | 44 +++++---- .../app/dapk/st/share/ShareEntryState.kt | 3 +- .../app/dapk/st/share/ShareEntryViewModel.kt | 33 ++----- .../app/dapk/st/matrix/common/EventType.kt | 3 +- .../dapk/st/matrix/message/MessageService.kt | 19 ++++ .../message/internal/DefaultMessageService.kt | 6 ++ .../message/internal/SendMessageUseCase.kt | 30 +++++++ .../st/matrix/message/internal/SendRequest.kt | 1 + .../src/test/kotlin/test/TestMatrix.kt | 2 + 25 files changed, 316 insertions(+), 76 deletions(-) create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 core/src/main/kotlin/app/dapk/st/core/MimeType.kt create mode 100644 features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..bd2a505 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,29 @@ + + + + \ No newline at end of file diff --git a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt index f5ea076..2fc928e 100644 --- a/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt +++ b/app/src/main/kotlin/app/dapk/st/graph/AppModule.kt @@ -44,6 +44,7 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerModule import app.dapk.st.navigator.IntentFactory +import app.dapk.st.navigator.MessageAttachment import app.dapk.st.notifications.NotificationsModule import app.dapk.st.olm.DeviceKeyFactory import app.dapk.st.olm.OlmPersistenceWrapper @@ -93,6 +94,11 @@ internal class AppModule(context: Application, logger: MatrixLogger) { override fun home(context: Context) = Intent(context, MainActivity::class.java) override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId) override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId) + override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List) = MessengerActivity.newMessageAttachment( + context, + roomId, + attachments + ) }) val featureModules = FeatureModules( @@ -236,6 +242,7 @@ internal class MatrixModules( val result = serviceProvider.cryptoService().encrypt( roomId = when (message) { is MessageService.Message.TextMessage -> message.roomId + is MessageService.Message.ImageMessage -> message.roomId }, credentials = credentialsStore.credentials()!!, when (message) { @@ -250,6 +257,7 @@ internal class MatrixModules( ) ) ) + is MessageService.Message.ImageMessage -> TODO() } ) diff --git a/core/src/main/kotlin/app/dapk/st/core/MimeType.kt b/core/src/main/kotlin/app/dapk/st/core/MimeType.kt new file mode 100644 index 0000000..4d24bf8 --- /dev/null +++ b/core/src/main/kotlin/app/dapk/st/core/MimeType.kt @@ -0,0 +1,5 @@ +package app.dapk.st.core + +sealed interface MimeType { + object Image: MimeType +} \ No newline at end of file diff --git a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt b/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt index bb28e31..f560c1c 100644 --- a/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt +++ b/domains/store/src/main/kotlin/app/dapk/st/domain/localecho/LocalEchoPersistence.kt @@ -33,11 +33,13 @@ class LocalEchoPersistence( inMemoryEchos.value = echos.groupBy { when (val message = it.message) { is MessageService.Message.TextMessage -> message.roomId + is MessageService.Message.ImageMessage -> message.roomId } }.mapValues { it.value.associateBy { when (val message = it.message) { is MessageService.Message.TextMessage -> message.localId + is MessageService.Message.ImageMessage -> message.localId } } } diff --git a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt index 7e327fc..0be9307 100644 --- a/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt +++ b/features/directory/src/main/kotlin/app/dapk/st/directory/DirectoryUseCase.kt @@ -75,6 +75,7 @@ class DirectoryUseCase( lastMessage = LastMessage( content = when (val message = latestEcho.message) { is MessageService.Message.TextMessage -> message.content.body + is MessageService.Message.ImageMessage -> "\uD83D\uDCF7" }, utcTimestamp = latestEcho.timestampUtc, author = member, diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt index 6f445ef..478a654 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/LocalEchoMapper.kt @@ -19,6 +19,7 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) { meta = metaMapper.toMeta(this) ) } + is MessageService.Message.ImageMessage -> TODO() } } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt index 712842f..6f707f9 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerActivity.kt @@ -9,11 +9,10 @@ import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material.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.core.* import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.matrix.common.RoomId +import app.dapk.st.navigator.MessageAttachment import kotlinx.parcelize.Parcelize class MessengerActivity : DapkActivity() { @@ -34,15 +33,22 @@ class MessengerActivity : DapkActivity() { putExtra("shortcut_key", roomId.value) } } + + fun newMessageAttachment(context: Context, roomId: RoomId, attachments: List): Intent { + return Intent(context, MessengerActivity::class.java).apply { + putExtra("key", MessagerActivityPayload(roomId.value, attachments)) + } + } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val payload = readPayload() + log(AppLogTag.ERROR_NON_FATAL, payload) setContent { SmallTalkTheme { Surface(Modifier.fillMaxSize()) { - MessengerScreen(RoomId(payload.roomId), viewModel, navigator) + MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator) } } } @@ -51,7 +57,8 @@ class MessengerActivity : DapkActivity() { @Parcelize data class MessagerActivityPayload( - val roomId: String + val roomId: String, + val attachments: List? = null ) : Parcelable fun Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let { diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt index f0f2a9a..d465ea8 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerScreen.kt @@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.* +import androidx.core.net.toUri import app.dapk.st.core.Lce import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.StartObserving @@ -39,18 +40,19 @@ import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent.Message import app.dapk.st.matrix.sync.RoomState +import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.Navigator import coil.compose.rememberAsyncImagePainter import coil.request.ImageRequest import kotlinx.coroutines.launch @Composable -internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navigator: Navigator) { +internal fun MessengerScreen(roomId: RoomId, attachments: List?, viewModel: MessengerViewModel, navigator: Navigator) { val state = viewModel.state viewModel.ObserveEvents() LifecycleEffect( - onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId)) }, + onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) }, onStop = { viewModel.post(MessengerAction.OnMessengerGone) } ) @@ -70,12 +72,19 @@ internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navi Room(state.roomState) when (state.composerState) { is ComposerState.Text -> { - Composer( - state.composerState.value, + TextComposer( + state.composerState, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, onSend = { viewModel.post(MessengerAction.ComposerSendText) }, ) } + is ComposerState.Attachments -> { + AttachmentComposer( + state.composerState, + onSend = { viewModel.post(MessengerAction.ComposerSendText) }, + onCancel = { viewModel.post(MessengerAction.ComposerClear) } + ) + } } } } @@ -524,7 +533,7 @@ private fun RowScope.SendStatus(message: RoomEvent) { } @Composable -private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: () -> Unit) { +private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit) { Row( Modifier .fillMaxWidth() @@ -541,12 +550,12 @@ private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: () contentAlignment = Alignment.TopStart, ) { Box(Modifier.padding(14.dp)) { - if (message.isEmpty()) { + if (state.value.isEmpty()) { Text("Message") } BasicTextField( modifier = Modifier.fillMaxWidth(), - value = message, + value = state.value, onValueChange = { onTextChange(it) }, cursorBrush = SolidColor(MaterialTheme.colors.primary), textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current.copy(LocalContentAlpha.current)), @@ -557,10 +566,10 @@ private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: () Spacer(modifier = Modifier.width(6.dp)) var size by remember { mutableStateOf(IntSize(0, 0)) } IconButton( - enabled = message.isNotEmpty(), + enabled = state.value.isNotEmpty(), modifier = Modifier .clip(CircleShape) - .background(if (message.isEmpty()) Color.DarkGray else MaterialTheme.colors.primary) + .background(if (state.value.isEmpty()) Color.DarkGray else MaterialTheme.colors.primary) .run { if (size.height == 0 || size.width == 0) { this @@ -584,3 +593,65 @@ private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: () } } } + +@Composable +private fun AttachmentComposer(state: ComposerState.Attachments, onSend: () -> Unit, onCancel: () -> Unit) { + Row( + Modifier + .fillMaxWidth() + .padding(horizontal = 12.dp, vertical = 8.dp) + .fillMaxWidth() + .height(IntrinsicSize.Min), verticalAlignment = Alignment.Bottom + ) { + Box( + modifier = Modifier + .align(Alignment.Bottom) + .weight(1f) + .fillMaxHeight() + .background(MaterialTheme.colors.onSurface.copy(alpha = TextFieldDefaults.BackgroundOpacity), RoundedCornerShape(24.dp)), + contentAlignment = Alignment.TopStart, + ) { + Box(Modifier.padding(14.dp)) { + val context = LocalContext.current + Image( + modifier = Modifier.size(50.dp, 50.dp), + painter = rememberAsyncImagePainter( + model = ImageRequest.Builder(context) + .data(state.values.first().uri.value.toUri()) + .build() + ), + contentDescription = null, + ) + } + } + Spacer(modifier = Modifier.width(6.dp)) + var size by remember { mutableStateOf(IntSize(0, 0)) } + IconButton( + enabled = true, + modifier = Modifier + .clip(CircleShape) + .background(MaterialTheme.colors.primary) + .run { + if (size.height == 0 || size.width == 0) { + this + .onSizeChanged { + size = it + } + .fillMaxHeight() + } else { + with(LocalDensity.current) { + size(size.width.toDp(), size.height.toDp()) + } + } + }, + onClick = onSend, + ) { + Icon( + imageVector = Icons.Filled.Send, + contentDescription = "", + tint = MaterialTheme.colors.onPrimary, + ) + } + } +} + diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt index 80e1aa1..17bcd11 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerState.kt @@ -2,6 +2,7 @@ package app.dapk.st.messenger import app.dapk.st.core.Lce import app.dapk.st.matrix.common.RoomId +import app.dapk.st.navigator.MessageAttachment data class MessengerScreenState( val roomId: RoomId?, @@ -17,4 +18,8 @@ sealed interface ComposerState { val value: String, ) : ComposerState + data class Attachments( + val values: List, + ) : ComposerState + } diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt index 2a4621b..47e7676 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/MessengerViewModel.kt @@ -11,6 +11,7 @@ import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomStore +import app.dapk.st.navigator.MessageAttachment import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.defaultStateFactory @@ -46,18 +47,19 @@ internal class MessengerViewModel( MessengerAction.OnMessengerGone -> syncJob?.cancel() is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) } MessengerAction.ComposerSendText -> sendMessage() + MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) } } } private fun start(action: MessengerAction.OnMessengerVisible) { - updateState { copy(roomId = action.roomId) } + updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it) } ?: composerState) } syncJob = viewModelScope.launch { roomStore.markRead(action.roomId) val credentials = credentialsStore.credentials()!! var lastKnownReadEvent: EventId? = null observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state -> - state.lastestMessageEventFromOthers(self = credentials.userId)?.let { + state.latestMessageEventFromOthers(self = credentials.userId)?.let { if (lastKnownReadEvent != it) { updateRoomReadStateAsync(latestReadEvent = it, state) lastKnownReadEvent = it @@ -98,12 +100,32 @@ internal class MessengerViewModel( } } } + is ComposerState.Attachments -> { + val copy = composerState.copy() + updateState { copy(composerState = ComposerState.Text("")) } + + state.roomState.takeIfContent()?.let { content -> + val roomState = content.roomState + viewModelScope.launch { + messageService.scheduleMessage( + MessageService.Message.ImageMessage( + MessageService.Message.Content.ImageContent(uri = copy.values.first().uri.value), + roomId = roomState.roomOverview.roomId, + sendEncrypted = roomState.roomOverview.isEncrypted, + localId = localIdFactory.create(), + timestampUtc = clock.millis(), + ) + ) + } + } + + } } } } -private fun MessengerState.lastestMessageEventFromOthers(self: UserId) = this.roomState.events +private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events .filterIsInstance() .filterNot { it.author.id == self } .firstOrNull() @@ -112,6 +134,7 @@ private fun MessengerState.lastestMessageEventFromOthers(self: UserId) = this.ro sealed interface MessengerAction { data class ComposerTextUpdate(val newValue: String) : MessengerAction object ComposerSendText : MessengerAction - data class OnMessengerVisible(val roomId: RoomId) : MessengerAction + object ComposerClear : MessengerAction + data class OnMessengerVisible(val roomId: RoomId, val attachments: List?) : MessengerAction object OnMessengerGone : MessengerAction } \ No newline at end of file diff --git a/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt b/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt index 79f8fe9..fe564b8 100644 --- a/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt +++ b/features/messenger/src/main/kotlin/app/dapk/st/messenger/roomsettings/RoomSettingsActivity.kt @@ -36,7 +36,7 @@ class RoomSettingsActivity : DapkActivity() { setContent { SmallTalkTheme { Surface(Modifier.fillMaxSize()) { - MessengerScreen(RoomId(payload.roomId), viewModel, navigator) +// MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator) } } } diff --git a/features/navigator/build.gradle b/features/navigator/build.gradle index c6b5dba..06e2a88 100644 --- a/features/navigator/build.gradle +++ b/features/navigator/build.gradle @@ -1,4 +1,5 @@ applyAndroidLibraryModule(project) +apply plugin: 'kotlin-parcelize' dependencies { implementation project(":core") diff --git a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt index a73e2fa..a996466 100644 --- a/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt +++ b/features/navigator/src/main/kotlin/app/dapk/st/navigator/Navigator.kt @@ -3,7 +3,13 @@ package app.dapk.st.navigator import android.app.Activity import android.content.Context import android.content.Intent +import android.os.Parcel +import android.os.Parcelable +import app.dapk.st.core.AndroidUri +import app.dapk.st.core.MimeType import app.dapk.st.matrix.common.RoomId +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize import kotlin.properties.ReadOnlyProperty import kotlin.reflect.KProperty @@ -29,8 +35,9 @@ interface Navigator { activity.navigateUpTo(intentFactory.home(activity)) } - fun toMessenger(roomId: RoomId) { - intentFactory.messenger(activity, roomId) + fun toMessenger(roomId: RoomId, attachments: List) { + val intent = intentFactory.messengerAttachments(activity, roomId, attachments) + activity.startActivity(intent) } fun toFilePicker(requestCode: Int) { @@ -47,6 +54,7 @@ interface IntentFactory { fun home(context: Context): Intent fun messenger(context: Context, roomId: RoomId): Intent fun messengerShortcut(context: Context, roomId: RoomId): Intent + fun messengerAttachments(context: Context, roomId: RoomId, attachments: List): Intent } @@ -65,3 +73,24 @@ private class DefaultNavigator(activity: Activity, intentFactory: IntentFactory) override val navigate: Navigator.Dsl = Navigator.Dsl(activity, intentFactory) } +@Parcelize +data class MessageAttachment(val uri: AndroidUri, val type: MimeType) : Parcelable { + private companion object : Parceler { + override fun create(parcel: Parcel): MessageAttachment { + val uri = AndroidUri(parcel.readString()!!) + val type = when(parcel.readString()!!) { + "mimetype-image" -> MimeType.Image + else -> throw IllegalStateException() + } + return MessageAttachment(uri, type) + } + + override fun MessageAttachment.write(parcel: Parcel, flags: Int) { + parcel.writeString(uri.value) + when (type) { + MimeType.Image -> parcel.writeString("mimetype-image") + } + } + } + +} \ No newline at end of file diff --git a/features/share-entry/build.gradle b/features/share-entry/build.gradle index 3e36d64..542fb16 100644 --- a/features/share-entry/build.gradle +++ b/features/share-entry/build.gradle @@ -9,4 +9,5 @@ dependencies { implementation project(':matrix:services:message') implementation project(":core") implementation project(":design-library") + implementation project(":features:navigator") } \ No newline at end of file diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt new file mode 100644 index 0000000..18aca0c --- /dev/null +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/FetchRoomsUseCase.kt @@ -0,0 +1,22 @@ +package app.dapk.st.share + +import app.dapk.st.matrix.room.RoomService +import app.dapk.st.matrix.sync.SyncService +import kotlinx.coroutines.flow.first + +class FetchRoomsUseCase( + private val syncSyncService: SyncService, + private val roomService: RoomService, +) { + + suspend fun bar(): List { + return syncSyncService.overview().first().map { + Item( + it.roomId, + it.roomAvatarUrl, + it.roomName ?: "", + roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value } + ) + } + } +} \ No newline at end of file diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt index b4651d5..da44704 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryActivity.kt @@ -19,11 +19,11 @@ class ShareEntryActivity : DapkActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("") + val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("Expected deeplink uris but they were missing") setContent { SmallTalkTheme { Surface(Modifier.fillMaxSize()) { - ShareEntryScreen(viewModel) + ShareEntryScreen(navigator, viewModel) } } } diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt index 634c6dd..85ddb3b 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryScreen.kt @@ -11,37 +11,36 @@ import androidx.compose.material.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.sp import app.dapk.st.core.LifecycleEffect +import app.dapk.st.core.MimeType import app.dapk.st.core.StartObserving import app.dapk.st.core.components.CenteredLoading import app.dapk.st.design.components.CircleishAvatar import app.dapk.st.design.components.GenericEmpty import app.dapk.st.design.components.GenericError import app.dapk.st.design.components.Toolbar -import app.dapk.st.matrix.common.RoomId +import app.dapk.st.navigator.MessageAttachment +import app.dapk.st.navigator.Navigator import app.dapk.st.share.DirectoryScreenState.* @Composable -fun ShareEntryScreen(viewModel: ShareEntryViewModel) { +fun ShareEntryScreen(navigator: Navigator, viewModel: ShareEntryViewModel) { val state = viewModel.state - - val listState: LazyListState = rememberLazyListState( - initialFirstVisibleItemIndex = 0, - ) - - viewModel.ObserveEvents(listState) + viewModel.ObserveEvents(navigator) LifecycleEffect( onStart = { viewModel.start() }, onStop = { viewModel.stop() } ) + val listState: LazyListState = rememberLazyListState( + initialFirstVisibleItemIndex = 0, + ) Box(modifier = Modifier.fillMaxSize()) { Toolbar(title = "Send to...") when (state) { @@ -50,18 +49,21 @@ fun ShareEntryScreen(viewModel: ShareEntryViewModel) { is Error -> GenericError { // TODO } - is Content -> Content(listState, state) + is Content -> Content(listState, state) { + viewModel.onRoomSelected(it) + } } } } @Composable -private fun ShareEntryViewModel.ObserveEvents(listState: LazyListState) { - val context = LocalContext.current +private fun ShareEntryViewModel.ObserveEvents(navigator: Navigator) { StartObserving { this@ObserveEvents.events.launch { when (it) { - is DirectoryEvent.SelectRoom -> TODO() + is DirectoryEvent.SelectRoom -> { + navigator.navigate.toMessenger(it.item.id, it.uris.map { MessageAttachment(it, MimeType.Image) }) + } } } } @@ -69,33 +71,27 @@ private fun ShareEntryViewModel.ObserveEvents(listState: LazyListState) { @Composable -private fun Content(listState: LazyListState, state: Content) { - val context = LocalContext.current - val navigateToRoom = { roomId: RoomId -> - // todo -// context.startActivity(MessengerActivity.newInstance(context, roomId)) - } +private fun Content(listState: LazyListState, state: Content, onClick: (Item) -> Unit) { LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(top = 72.dp)) { items( items = state.items, key = { it.id.value }, ) { - DirectoryItem(it, onClick = navigateToRoom) + DirectoryItem(it, onClick = onClick) } } } @Composable -private fun DirectoryItem(item: Item, onClick: (RoomId) -> Unit) { +private fun DirectoryItem(item: Item, onClick: (Item) -> Unit) { val roomName = item.roomName Box( Modifier .height(IntrinsicSize.Min) .fillMaxWidth() - .clickable { - onClick(item.id) - }) { + .clickable { onClick(item) } + ) { Row(Modifier.padding(20.dp)) { val secondaryText = MaterialTheme.colors.onBackground.copy(alpha = 0.5f) diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt index 17a4737..57a21f3 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryState.kt @@ -1,5 +1,6 @@ package app.dapk.st.share +import app.dapk.st.core.AndroidUri import app.dapk.st.matrix.common.AvatarUrl import app.dapk.st.matrix.common.RoomId @@ -13,7 +14,7 @@ sealed interface DirectoryScreenState { } sealed interface DirectoryEvent { - data class SelectRoom(val item: Item) : DirectoryEvent + data class SelectRoom(val item: Item, val uris: List) : DirectoryEvent } data class Item(val id: RoomId, val roomAvatarUrl: AvatarUrl?, val roomName: String, val members: List) diff --git a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt index db3e9cc..ddd6fbe 100644 --- a/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt +++ b/features/share-entry/src/main/kotlin/app/dapk/st/share/ShareEntryViewModel.kt @@ -3,15 +3,10 @@ package app.dapk.st.share import android.net.Uri import androidx.lifecycle.viewModelScope import app.dapk.st.core.AndroidUri -import app.dapk.st.matrix.common.RoomId -import app.dapk.st.matrix.message.MessageService -import app.dapk.st.matrix.room.RoomService -import app.dapk.st.matrix.sync.SyncService 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.first import kotlinx.coroutines.launch class ShareEntryViewModel( @@ -22,6 +17,7 @@ class ShareEntryViewModel( factory, ) { + private var urisToShare: List? = null private var syncJob: Job? = null fun start() { @@ -34,31 +30,14 @@ class ShareEntryViewModel( syncJob?.cancel() } - - fun sendAttachment() { - - } - fun withUris(urisToShare: List) { -// TODO("Not yet implemented") + this.urisToShare = urisToShare.map { AndroidUri(it.toString()) } } -} - -class FetchRoomsUseCase( - private val syncSyncService: SyncService, - private val roomService: RoomService, -) { - - suspend fun bar(): List { - return syncSyncService.overview().first().map { - Item( - it.roomId, - it.roomAvatarUrl, - it.roomName ?: "", - roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value } - ) + fun onRoomSelected(item: Item) { + viewModelScope.launch { + _events.emit(DirectoryEvent.SelectRoom(item, uris = urisToShare ?: throw IllegalArgumentException("Not uris set"))) } } -} +} diff --git a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt index 4f1c309..85a6e4e 100644 --- a/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt +++ b/matrix/common/src/main/kotlin/app/dapk/st/matrix/common/EventType.kt @@ -14,5 +14,6 @@ enum class EventType(val value: String) { } enum class MessageType(val value: String) { - TEXT("m.text") + TEXT("m.text"), + IMAGE("m.image"), } \ No newline at end of file diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt index 6103470..7ef72a5 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/MessageService.kt @@ -46,6 +46,16 @@ interface MessageService : MatrixService { @SerialName("timestamp") val timestampUtc: Long, ) : Message() + @Serializable + @SerialName("image_message") + data class ImageMessage( + @SerialName("content") val content: Content.ImageContent, + @SerialName("send_encrypted") val sendEncrypted: Boolean, + @SerialName("room_id") val roomId: RoomId, + @SerialName("local_id") val localId: String, + @SerialName("timestamp") val timestampUtc: Long, + ) : Message() + @Serializable sealed class Content { @Serializable @@ -53,6 +63,12 @@ interface MessageService : MatrixService { @SerialName("body") val body: String, @SerialName("msgtype") val type: String = MessageType.TEXT.value, ) : Content() + + @Serializable + data class ImageContent( + @SerialName("uri") val uri: String, + @SerialName("msgtype") val type: String = MessageType.IMAGE.value, + ) : Content() } } @@ -66,16 +82,19 @@ interface MessageService : MatrixService { @Transient val timestampUtc = when (message) { is Message.TextMessage -> message.timestampUtc + is Message.ImageMessage -> message.timestampUtc } @Transient val roomId = when (message) { is Message.TextMessage -> message.roomId + is Message.ImageMessage -> message.roomId } @Transient val localId = when (message) { is Message.TextMessage -> message.localId + is Message.ImageMessage -> message.localId } @Serializable diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt index 4a30b43..c02ab8d 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/DefaultMessageService.kt @@ -13,6 +13,7 @@ import java.net.SocketException import java.net.UnknownHostException private const val MATRIX_MESSAGE_TASK_TYPE = "matrix-text-message" +private const val MATRIX_IMAGE_MESSAGE_TASK_TYPE = "matrix-image-message" internal class DefaultMessageService( httpClient: MatrixHttpClient, @@ -50,6 +51,7 @@ internal class DefaultMessageService( localEchoStore.markSending(message) val localId = when (message) { is MessageService.Message.TextMessage -> message.localId + is MessageService.Message.ImageMessage -> message.localId } backgroundScheduler.schedule(key = localId, message.toTask()) } @@ -68,6 +70,10 @@ internal class DefaultMessageService( Json.encodeToString(MessageService.Message.TextMessage.serializer(), this) ) } + is MessageService.Message.ImageMessage -> BackgroundScheduler.Task( + type = MATRIX_IMAGE_MESSAGE_TASK_TYPE, + Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this) + ) } } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt index 6a6e2be..e35fb0f 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendMessageUseCase.kt @@ -34,6 +34,36 @@ internal class SendMessageUseCase( } httpClient.execute(request).eventId } + is MessageService.Message.ImageMessage -> { + // upload image, then send message + // POST /_matrix/media/v3/upload +// message.content.uri + + /** + * { + "content": { + "body": "filename.jpg", + "info": { + "h": 398, + "mimetype": "image/jpeg", + "size": 31037, + "w": 394 + }, + "msgtype": "m.image", + "url": "mxc://example.org/JWEIFJgwEIhweiWJE" + }, + "event_id": "$143273582443PhrSn:example.org", + "origin_server_ts": 1432735824653, + "room_id": "!jEsUZKDJdhlrceRyVU:example.org", + "sender": "@example:example.org", + "type": "m.room.message", + "unsigned": { + "age": 1234 + } + } + */ + TODO() + } } } diff --git a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt index 53df1c3..5183f72 100644 --- a/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt +++ b/matrix/services/message/src/main/kotlin/app/dapk/st/matrix/message/internal/SendRequest.kt @@ -16,6 +16,7 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, con method = MatrixHttpClient.Method.PUT, body = when (content) { is Message.Content.TextContent -> jsonBody(Message.Content.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults) + is Message.Content.ImageContent -> jsonBody(Message.Content.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults) } ) diff --git a/test-harness/src/test/kotlin/test/TestMatrix.kt b/test-harness/src/test/kotlin/test/TestMatrix.kt index c324929..6b1c505 100644 --- a/test-harness/src/test/kotlin/test/TestMatrix.kt +++ b/test-harness/src/test/kotlin/test/TestMatrix.kt @@ -115,6 +115,7 @@ class TestMatrix( val result = serviceProvider.cryptoService().encrypt( roomId = when (message) { is MessageService.Message.TextMessage -> message.roomId + is MessageService.Message.ImageMessage -> message.roomId }, credentials = storeModule.credentialsStore().credentials()!!, when (message) { @@ -128,6 +129,7 @@ class TestMatrix( ) ) ) + is MessageService.Message.ImageMessage -> TODO() } )