wip, passing the image urls down to the matrix client layer

This commit is contained in:
Adam Brown 2022-06-08 20:31:07 +01:00
parent 92ad630e45
commit 6fed8a35ce
25 changed files with 316 additions and 76 deletions

View File

@ -0,0 +1,29 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PreviewAnnotationInFunctionWithParameters" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewDimensionRespectsLimit" enabled="true" level="WARNING" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewFontScaleMustBeGreaterThanZero" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMultipleParameterProviders" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewMustBeTopLevelFunction" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNeedsComposableAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewNotSupportedInUnitTestFiles" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
<inspection_tool class="PreviewPickerAnnotation" enabled="true" level="ERROR" enabled_by_default="true">
<option name="composableFile" value="true" />
</inspection_tool>
</profile>
</component>

View File

@ -44,6 +44,7 @@ import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerActivity
import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.MessengerModule
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.notifications.NotificationsModule import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.olm.DeviceKeyFactory import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmPersistenceWrapper 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 home(context: Context) = Intent(context, MainActivity::class.java)
override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId) override fun messenger(context: Context, roomId: RoomId) = MessengerActivity.newInstance(context, roomId)
override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId) override fun messengerShortcut(context: Context, roomId: RoomId) = MessengerActivity.newShortcutInstance(context, roomId)
override fun messengerAttachments(context: Context, roomId: RoomId, attachments: List<MessageAttachment>) = MessengerActivity.newMessageAttachment(
context,
roomId,
attachments
)
}) })
val featureModules = FeatureModules( val featureModules = FeatureModules(
@ -236,6 +242,7 @@ internal class MatrixModules(
val result = serviceProvider.cryptoService().encrypt( val result = serviceProvider.cryptoService().encrypt(
roomId = when (message) { roomId = when (message) {
is MessageService.Message.TextMessage -> message.roomId is MessageService.Message.TextMessage -> message.roomId
is MessageService.Message.ImageMessage -> message.roomId
}, },
credentials = credentialsStore.credentials()!!, credentials = credentialsStore.credentials()!!,
when (message) { when (message) {
@ -250,6 +257,7 @@ internal class MatrixModules(
) )
) )
) )
is MessageService.Message.ImageMessage -> TODO()
} }
) )

View File

@ -0,0 +1,5 @@
package app.dapk.st.core
sealed interface MimeType {
object Image: MimeType
}

View File

@ -33,11 +33,13 @@ class LocalEchoPersistence(
inMemoryEchos.value = echos.groupBy { inMemoryEchos.value = echos.groupBy {
when (val message = it.message) { when (val message = it.message) {
is MessageService.Message.TextMessage -> message.roomId is MessageService.Message.TextMessage -> message.roomId
is MessageService.Message.ImageMessage -> message.roomId
} }
}.mapValues { }.mapValues {
it.value.associateBy { it.value.associateBy {
when (val message = it.message) { when (val message = it.message) {
is MessageService.Message.TextMessage -> message.localId is MessageService.Message.TextMessage -> message.localId
is MessageService.Message.ImageMessage -> message.localId
} }
} }
} }

View File

@ -75,6 +75,7 @@ class DirectoryUseCase(
lastMessage = LastMessage( lastMessage = LastMessage(
content = when (val message = latestEcho.message) { content = when (val message = latestEcho.message) {
is MessageService.Message.TextMessage -> message.content.body is MessageService.Message.TextMessage -> message.content.body
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
}, },
utcTimestamp = latestEcho.timestampUtc, utcTimestamp = latestEcho.timestampUtc,
author = member, author = member,

View File

@ -19,6 +19,7 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
meta = metaMapper.toMeta(this) meta = metaMapper.toMeta(this)
) )
} }
is MessageService.Message.ImageMessage -> TODO()
} }
} }

View File

@ -9,11 +9,10 @@ import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import app.dapk.st.core.DapkActivity import app.dapk.st.core.*
import app.dapk.st.core.module
import app.dapk.st.core.viewModel
import app.dapk.st.design.components.SmallTalkTheme import app.dapk.st.design.components.SmallTalkTheme
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.navigator.MessageAttachment
import kotlinx.parcelize.Parcelize import kotlinx.parcelize.Parcelize
class MessengerActivity : DapkActivity() { class MessengerActivity : DapkActivity() {
@ -34,15 +33,22 @@ class MessengerActivity : DapkActivity() {
putExtra("shortcut_key", roomId.value) putExtra("shortcut_key", roomId.value)
} }
} }
fun newMessageAttachment(context: Context, roomId: RoomId, attachments: List<MessageAttachment>): Intent {
return Intent(context, MessengerActivity::class.java).apply {
putExtra("key", MessagerActivityPayload(roomId.value, attachments))
}
}
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val payload = readPayload<MessagerActivityPayload>() val payload = readPayload<MessagerActivityPayload>()
log(AppLogTag.ERROR_NON_FATAL, payload)
setContent { setContent {
SmallTalkTheme { SmallTalkTheme {
Surface(Modifier.fillMaxSize()) { 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 @Parcelize
data class MessagerActivityPayload( data class MessagerActivityPayload(
val roomId: String val roomId: String,
val attachments: List<MessageAttachment>? = null
) : Parcelable ) : Parcelable
fun <T : Parcelable> Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let { fun <T : Parcelable> Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let {

View File

@ -28,6 +28,7 @@ import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.input.KeyboardCapitalization import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.* import androidx.compose.ui.unit.*
import androidx.core.net.toUri
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.StartObserving 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
import app.dapk.st.matrix.sync.RoomEvent.Message import app.dapk.st.matrix.sync.RoomEvent.Message
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.navigator.Navigator import app.dapk.st.navigator.Navigator
import coil.compose.rememberAsyncImagePainter import coil.compose.rememberAsyncImagePainter
import coil.request.ImageRequest import coil.request.ImageRequest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@Composable @Composable
internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navigator: Navigator) { internal fun MessengerScreen(roomId: RoomId, attachments: List<MessageAttachment>?, viewModel: MessengerViewModel, navigator: Navigator) {
val state = viewModel.state val state = viewModel.state
viewModel.ObserveEvents() viewModel.ObserveEvents()
LifecycleEffect( LifecycleEffect(
onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId)) }, onStart = { viewModel.post(MessengerAction.OnMessengerVisible(roomId, attachments)) },
onStop = { viewModel.post(MessengerAction.OnMessengerGone) } onStop = { viewModel.post(MessengerAction.OnMessengerGone) }
) )
@ -70,12 +72,19 @@ internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navi
Room(state.roomState) Room(state.roomState)
when (state.composerState) { when (state.composerState) {
is ComposerState.Text -> { is ComposerState.Text -> {
Composer( TextComposer(
state.composerState.value, state.composerState,
onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) }, onTextChange = { viewModel.post(MessengerAction.ComposerTextUpdate(it)) },
onSend = { viewModel.post(MessengerAction.ComposerSendText) }, 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 @Composable
private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: () -> Unit) { private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Unit, onSend: () -> Unit) {
Row( Row(
Modifier Modifier
.fillMaxWidth() .fillMaxWidth()
@ -541,12 +550,12 @@ private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: ()
contentAlignment = Alignment.TopStart, contentAlignment = Alignment.TopStart,
) { ) {
Box(Modifier.padding(14.dp)) { Box(Modifier.padding(14.dp)) {
if (message.isEmpty()) { if (state.value.isEmpty()) {
Text("Message") Text("Message")
} }
BasicTextField( BasicTextField(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
value = message, value = state.value,
onValueChange = { onTextChange(it) }, onValueChange = { onTextChange(it) },
cursorBrush = SolidColor(MaterialTheme.colors.primary), cursorBrush = SolidColor(MaterialTheme.colors.primary),
textStyle = LocalTextStyle.current.copy(color = LocalContentColor.current.copy(LocalContentAlpha.current)), 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)) Spacer(modifier = Modifier.width(6.dp))
var size by remember { mutableStateOf(IntSize(0, 0)) } var size by remember { mutableStateOf(IntSize(0, 0)) }
IconButton( IconButton(
enabled = message.isNotEmpty(), enabled = state.value.isNotEmpty(),
modifier = Modifier modifier = Modifier
.clip(CircleShape) .clip(CircleShape)
.background(if (message.isEmpty()) Color.DarkGray else MaterialTheme.colors.primary) .background(if (state.value.isEmpty()) Color.DarkGray else MaterialTheme.colors.primary)
.run { .run {
if (size.height == 0 || size.width == 0) { if (size.height == 0 || size.width == 0) {
this 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,
)
}
}
}

View File

@ -2,6 +2,7 @@ package app.dapk.st.messenger
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.navigator.MessageAttachment
data class MessengerScreenState( data class MessengerScreenState(
val roomId: RoomId?, val roomId: RoomId?,
@ -17,4 +18,8 @@ sealed interface ComposerState {
val value: String, val value: String,
) : ComposerState ) : ComposerState
data class Attachments(
val values: List<MessageAttachment>,
) : ComposerState
} }

View File

@ -11,6 +11,7 @@ import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory import app.dapk.st.viewmodel.defaultStateFactory
@ -46,18 +47,19 @@ internal class MessengerViewModel(
MessengerAction.OnMessengerGone -> syncJob?.cancel() MessengerAction.OnMessengerGone -> syncJob?.cancel()
is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) } is MessengerAction.ComposerTextUpdate -> updateState { copy(composerState = ComposerState.Text(action.newValue)) }
MessengerAction.ComposerSendText -> sendMessage() MessengerAction.ComposerSendText -> sendMessage()
MessengerAction.ComposerClear -> updateState { copy(composerState = ComposerState.Text("")) }
} }
} }
private fun start(action: MessengerAction.OnMessengerVisible) { 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 { syncJob = viewModelScope.launch {
roomStore.markRead(action.roomId) roomStore.markRead(action.roomId)
val credentials = credentialsStore.credentials()!! val credentials = credentialsStore.credentials()!!
var lastKnownReadEvent: EventId? = null var lastKnownReadEvent: EventId? = null
observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state -> observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
state.lastestMessageEventFromOthers(self = credentials.userId)?.let { state.latestMessageEventFromOthers(self = credentials.userId)?.let {
if (lastKnownReadEvent != it) { if (lastKnownReadEvent != it) {
updateRoomReadStateAsync(latestReadEvent = it, state) updateRoomReadStateAsync(latestReadEvent = it, state)
lastKnownReadEvent = it 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<RoomEvent.Message>() .filterIsInstance<RoomEvent.Message>()
.filterNot { it.author.id == self } .filterNot { it.author.id == self }
.firstOrNull() .firstOrNull()
@ -112,6 +134,7 @@ private fun MessengerState.lastestMessageEventFromOthers(self: UserId) = this.ro
sealed interface MessengerAction { sealed interface MessengerAction {
data class ComposerTextUpdate(val newValue: String) : MessengerAction data class ComposerTextUpdate(val newValue: String) : MessengerAction
object ComposerSendText : MessengerAction object ComposerSendText : MessengerAction
data class OnMessengerVisible(val roomId: RoomId) : MessengerAction object ComposerClear : MessengerAction
data class OnMessengerVisible(val roomId: RoomId, val attachments: List<MessageAttachment>?) : MessengerAction
object OnMessengerGone : MessengerAction object OnMessengerGone : MessengerAction
} }

View File

@ -36,7 +36,7 @@ class RoomSettingsActivity : DapkActivity() {
setContent { setContent {
SmallTalkTheme { SmallTalkTheme {
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize()) {
MessengerScreen(RoomId(payload.roomId), viewModel, navigator) // MessengerScreen(RoomId(payload.roomId), payload.attachments, viewModel, navigator)
} }
} }
} }

View File

@ -1,4 +1,5 @@
applyAndroidLibraryModule(project) applyAndroidLibraryModule(project)
apply plugin: 'kotlin-parcelize'
dependencies { dependencies {
implementation project(":core") implementation project(":core")

View File

@ -3,7 +3,13 @@ package app.dapk.st.navigator
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.Intent 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 app.dapk.st.matrix.common.RoomId
import kotlinx.parcelize.Parceler
import kotlinx.parcelize.Parcelize
import kotlin.properties.ReadOnlyProperty import kotlin.properties.ReadOnlyProperty
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
@ -29,8 +35,9 @@ interface Navigator {
activity.navigateUpTo(intentFactory.home(activity)) activity.navigateUpTo(intentFactory.home(activity))
} }
fun toMessenger(roomId: RoomId) { fun toMessenger(roomId: RoomId, attachments: List<MessageAttachment>) {
intentFactory.messenger(activity, roomId) val intent = intentFactory.messengerAttachments(activity, roomId, attachments)
activity.startActivity(intent)
} }
fun toFilePicker(requestCode: Int) { fun toFilePicker(requestCode: Int) {
@ -47,6 +54,7 @@ interface IntentFactory {
fun home(context: Context): Intent fun home(context: Context): Intent
fun messenger(context: Context, roomId: RoomId): Intent fun messenger(context: Context, roomId: RoomId): Intent
fun messengerShortcut(context: Context, roomId: RoomId): Intent fun messengerShortcut(context: Context, roomId: RoomId): Intent
fun messengerAttachments(context: Context, roomId: RoomId, attachments: List<MessageAttachment>): Intent
} }
@ -65,3 +73,24 @@ private class DefaultNavigator(activity: Activity, intentFactory: IntentFactory)
override val navigate: Navigator.Dsl = Navigator.Dsl(activity, 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<MessageAttachment> {
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")
}
}
}
}

View File

@ -9,4 +9,5 @@ dependencies {
implementation project(':matrix:services:message') implementation project(':matrix:services:message')
implementation project(":core") implementation project(":core")
implementation project(":design-library") implementation project(":design-library")
implementation project(":features:navigator")
} }

View File

@ -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<Item> {
return syncSyncService.overview().first().map {
Item(
it.roomId,
it.roomAvatarUrl,
it.roomName ?: "",
roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value }
)
}
}
}

View File

@ -19,11 +19,11 @@ class ShareEntryActivity : DapkActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("") val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("Expected deeplink uris but they were missing")
setContent { setContent {
SmallTalkTheme { SmallTalkTheme {
Surface(Modifier.fillMaxSize()) { Surface(Modifier.fillMaxSize()) {
ShareEntryScreen(viewModel) ShareEntryScreen(navigator, viewModel)
} }
} }
} }

View File

@ -11,37 +11,36 @@ import androidx.compose.material.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.MimeType
import app.dapk.st.core.StartObserving import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.design.components.CircleishAvatar import app.dapk.st.design.components.CircleishAvatar
import app.dapk.st.design.components.GenericEmpty import app.dapk.st.design.components.GenericEmpty
import app.dapk.st.design.components.GenericError import app.dapk.st.design.components.GenericError
import app.dapk.st.design.components.Toolbar 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.* import app.dapk.st.share.DirectoryScreenState.*
@Composable @Composable
fun ShareEntryScreen(viewModel: ShareEntryViewModel) { fun ShareEntryScreen(navigator: Navigator, viewModel: ShareEntryViewModel) {
val state = viewModel.state val state = viewModel.state
viewModel.ObserveEvents(navigator)
val listState: LazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = 0,
)
viewModel.ObserveEvents(listState)
LifecycleEffect( LifecycleEffect(
onStart = { viewModel.start() }, onStart = { viewModel.start() },
onStop = { viewModel.stop() } onStop = { viewModel.stop() }
) )
val listState: LazyListState = rememberLazyListState(
initialFirstVisibleItemIndex = 0,
)
Box(modifier = Modifier.fillMaxSize()) { Box(modifier = Modifier.fillMaxSize()) {
Toolbar(title = "Send to...") Toolbar(title = "Send to...")
when (state) { when (state) {
@ -50,18 +49,21 @@ fun ShareEntryScreen(viewModel: ShareEntryViewModel) {
is Error -> GenericError { is Error -> GenericError {
// TODO // TODO
} }
is Content -> Content(listState, state) is Content -> Content(listState, state) {
viewModel.onRoomSelected(it)
}
} }
} }
} }
@Composable @Composable
private fun ShareEntryViewModel.ObserveEvents(listState: LazyListState) { private fun ShareEntryViewModel.ObserveEvents(navigator: Navigator) {
val context = LocalContext.current
StartObserving { StartObserving {
this@ObserveEvents.events.launch { this@ObserveEvents.events.launch {
when (it) { 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 @Composable
private fun Content(listState: LazyListState, state: Content) { private fun Content(listState: LazyListState, state: Content, onClick: (Item) -> Unit) {
val context = LocalContext.current
val navigateToRoom = { roomId: RoomId ->
// todo
// context.startActivity(MessengerActivity.newInstance(context, roomId))
}
LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(top = 72.dp)) { LazyColumn(Modifier.fillMaxSize(), state = listState, contentPadding = PaddingValues(top = 72.dp)) {
items( items(
items = state.items, items = state.items,
key = { it.id.value }, key = { it.id.value },
) { ) {
DirectoryItem(it, onClick = navigateToRoom) DirectoryItem(it, onClick = onClick)
} }
} }
} }
@Composable @Composable
private fun DirectoryItem(item: Item, onClick: (RoomId) -> Unit) { private fun DirectoryItem(item: Item, onClick: (Item) -> Unit) {
val roomName = item.roomName val roomName = item.roomName
Box( Box(
Modifier Modifier
.height(IntrinsicSize.Min) .height(IntrinsicSize.Min)
.fillMaxWidth() .fillMaxWidth()
.clickable { .clickable { onClick(item) }
onClick(item.id) ) {
}) {
Row(Modifier.padding(20.dp)) { Row(Modifier.padding(20.dp)) {
val secondaryText = MaterialTheme.colors.onBackground.copy(alpha = 0.5f) val secondaryText = MaterialTheme.colors.onBackground.copy(alpha = 0.5f)

View File

@ -1,5 +1,6 @@
package app.dapk.st.share package app.dapk.st.share
import app.dapk.st.core.AndroidUri
import app.dapk.st.matrix.common.AvatarUrl import app.dapk.st.matrix.common.AvatarUrl
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
@ -13,7 +14,7 @@ sealed interface DirectoryScreenState {
} }
sealed interface DirectoryEvent { sealed interface DirectoryEvent {
data class SelectRoom(val item: Item) : DirectoryEvent data class SelectRoom(val item: Item, val uris: List<AndroidUri>) : DirectoryEvent
} }
data class Item(val id: RoomId, val roomAvatarUrl: AvatarUrl?, val roomName: String, val members: List<String>) data class Item(val id: RoomId, val roomAvatarUrl: AvatarUrl?, val roomName: String, val members: List<String>)

View File

@ -3,15 +3,10 @@ package app.dapk.st.share
import android.net.Uri import android.net.Uri
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.dapk.st.core.AndroidUri 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.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory import app.dapk.st.viewmodel.defaultStateFactory
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ShareEntryViewModel( class ShareEntryViewModel(
@ -22,6 +17,7 @@ class ShareEntryViewModel(
factory, factory,
) { ) {
private var urisToShare: List<AndroidUri>? = null
private var syncJob: Job? = null private var syncJob: Job? = null
fun start() { fun start() {
@ -34,31 +30,14 @@ class ShareEntryViewModel(
syncJob?.cancel() syncJob?.cancel()
} }
fun sendAttachment() {
}
fun withUris(urisToShare: List<Uri>) { fun withUris(urisToShare: List<Uri>) {
// TODO("Not yet implemented") this.urisToShare = urisToShare.map { AndroidUri(it.toString()) }
} }
} fun onRoomSelected(item: Item) {
viewModelScope.launch {
class FetchRoomsUseCase( _events.emit(DirectoryEvent.SelectRoom(item, uris = urisToShare ?: throw IllegalArgumentException("Not uris set")))
private val syncSyncService: SyncService,
private val roomService: RoomService,
) {
suspend fun bar(): List<Item> {
return syncSyncService.overview().first().map {
Item(
it.roomId,
it.roomAvatarUrl,
it.roomName ?: "",
roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value }
)
} }
} }
}
}

View File

@ -14,5 +14,6 @@ enum class EventType(val value: String) {
} }
enum class MessageType(val value: String) { enum class MessageType(val value: String) {
TEXT("m.text") TEXT("m.text"),
IMAGE("m.image"),
} }

View File

@ -46,6 +46,16 @@ interface MessageService : MatrixService {
@SerialName("timestamp") val timestampUtc: Long, @SerialName("timestamp") val timestampUtc: Long,
) : Message() ) : 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 @Serializable
sealed class Content { sealed class Content {
@Serializable @Serializable
@ -53,6 +63,12 @@ interface MessageService : MatrixService {
@SerialName("body") val body: String, @SerialName("body") val body: String,
@SerialName("msgtype") val type: String = MessageType.TEXT.value, @SerialName("msgtype") val type: String = MessageType.TEXT.value,
) : Content() ) : 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 @Transient
val timestampUtc = when (message) { val timestampUtc = when (message) {
is Message.TextMessage -> message.timestampUtc is Message.TextMessage -> message.timestampUtc
is Message.ImageMessage -> message.timestampUtc
} }
@Transient @Transient
val roomId = when (message) { val roomId = when (message) {
is Message.TextMessage -> message.roomId is Message.TextMessage -> message.roomId
is Message.ImageMessage -> message.roomId
} }
@Transient @Transient
val localId = when (message) { val localId = when (message) {
is Message.TextMessage -> message.localId is Message.TextMessage -> message.localId
is Message.ImageMessage -> message.localId
} }
@Serializable @Serializable

View File

@ -13,6 +13,7 @@ import java.net.SocketException
import java.net.UnknownHostException import java.net.UnknownHostException
private const val MATRIX_MESSAGE_TASK_TYPE = "matrix-text-message" private const val MATRIX_MESSAGE_TASK_TYPE = "matrix-text-message"
private const val MATRIX_IMAGE_MESSAGE_TASK_TYPE = "matrix-image-message"
internal class DefaultMessageService( internal class DefaultMessageService(
httpClient: MatrixHttpClient, httpClient: MatrixHttpClient,
@ -50,6 +51,7 @@ internal class DefaultMessageService(
localEchoStore.markSending(message) localEchoStore.markSending(message)
val localId = when (message) { val localId = when (message) {
is MessageService.Message.TextMessage -> message.localId is MessageService.Message.TextMessage -> message.localId
is MessageService.Message.ImageMessage -> message.localId
} }
backgroundScheduler.schedule(key = localId, message.toTask()) backgroundScheduler.schedule(key = localId, message.toTask())
} }
@ -68,6 +70,10 @@ internal class DefaultMessageService(
Json.encodeToString(MessageService.Message.TextMessage.serializer(), this) 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)
)
} }
} }

View File

@ -34,6 +34,36 @@ internal class SendMessageUseCase(
} }
httpClient.execute(request).eventId 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()
}
} }
} }

View File

@ -16,6 +16,7 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, con
method = MatrixHttpClient.Method.PUT, method = MatrixHttpClient.Method.PUT,
body = when (content) { body = when (content) {
is Message.Content.TextContent -> jsonBody(Message.Content.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults) is Message.Content.TextContent -> jsonBody(Message.Content.TextContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
is Message.Content.ImageContent -> jsonBody(Message.Content.ImageContent.serializer(), content, MatrixHttpClient.jsonWithDefaults)
} }
) )

View File

@ -115,6 +115,7 @@ class TestMatrix(
val result = serviceProvider.cryptoService().encrypt( val result = serviceProvider.cryptoService().encrypt(
roomId = when (message) { roomId = when (message) {
is MessageService.Message.TextMessage -> message.roomId is MessageService.Message.TextMessage -> message.roomId
is MessageService.Message.ImageMessage -> message.roomId
}, },
credentials = storeModule.credentialsStore().credentials()!!, credentials = storeModule.credentialsStore().credentials()!!,
when (message) { when (message) {
@ -128,6 +129,7 @@ class TestMatrix(
) )
) )
) )
is MessageService.Message.ImageMessage -> TODO()
} }
) )