mirror of
https://github.com/ouchadam/small-talk.git
synced 2025-02-16 12:10:45 +01:00
Merge pull request #73 from ouchadam/feature/share-images-via-small-talk
Share images via small talk
This commit is contained in:
commit
e9cbec04af
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal file
29
.idea/inspectionProfiles/Project_Default.xml
generated
Normal 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>
|
@ -71,8 +71,10 @@ dependencies {
|
||||
implementation project(":features:messenger")
|
||||
implementation project(":features:profile")
|
||||
implementation project(":features:navigator")
|
||||
implementation project(":features:share-entry")
|
||||
|
||||
implementation project(':domains:store')
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:core")
|
||||
implementation project(":domains:android:tracking")
|
||||
implementation project(":domains:android:push")
|
||||
|
@ -20,6 +20,11 @@
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.app.shortcuts"
|
||||
android:resource="@xml/shortcuts" />
|
||||
|
||||
</activity-alias>
|
||||
|
||||
</application>
|
||||
|
@ -19,6 +19,7 @@ import app.dapk.st.notifications.NotificationsModule
|
||||
import app.dapk.st.notifications.PushAndroidService
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.settings.SettingsModule
|
||||
import app.dapk.st.share.ShareEntryModule
|
||||
import app.dapk.st.work.TaskRunnerModule
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.cancel
|
||||
@ -75,6 +76,7 @@ class SmallTalkApplication : Application(), ModuleProvider {
|
||||
MessengerModule::class -> featureModules.messengerModule
|
||||
TaskRunnerModule::class -> appModule.domainModules.taskRunnerModule
|
||||
CoreAndroidModule::class -> appModule.coreAndroidModule
|
||||
ShareEntryModule::class -> featureModules.shareEntryModule
|
||||
else -> throw IllegalArgumentException("Unknown: $klass")
|
||||
} as T
|
||||
}
|
||||
|
@ -2,8 +2,11 @@ package app.dapk.st.graph
|
||||
|
||||
import android.app.Application
|
||||
import android.app.PendingIntent
|
||||
import android.content.ContentResolver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.BitmapFactory
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import app.dapk.db.DapkDb
|
||||
import app.dapk.st.BuildConfig
|
||||
@ -33,6 +36,7 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.installMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.push.installPushService
|
||||
import app.dapk.st.matrix.push.pushService
|
||||
@ -43,6 +47,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
|
||||
@ -50,6 +55,7 @@ import app.dapk.st.olm.OlmWrapper
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.settings.SettingsModule
|
||||
import app.dapk.st.share.ShareEntryModule
|
||||
import app.dapk.st.tracking.TrackingModule
|
||||
import app.dapk.st.work.TaskRunnerModule
|
||||
import app.dapk.st.work.WorkModule
|
||||
@ -84,7 +90,7 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
||||
private val workModule = WorkModule(context)
|
||||
private val imageLoaderModule = ImageLoaderModule(context)
|
||||
|
||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers)
|
||||
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, context.contentResolver)
|
||||
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker)
|
||||
|
||||
val coreAndroidModule = CoreAndroidModule(intentFactory = object : IntentFactory {
|
||||
@ -107,6 +113,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<MessageAttachment>) = MessengerActivity.newMessageAttachment(
|
||||
context,
|
||||
roomId,
|
||||
attachments
|
||||
)
|
||||
})
|
||||
|
||||
val featureModules = FeatureModules(
|
||||
@ -194,6 +205,10 @@ internal class FeatureModules internal constructor(
|
||||
)
|
||||
}
|
||||
|
||||
val shareEntryModule by unsafeLazy {
|
||||
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
internal class MatrixModules(
|
||||
@ -202,6 +217,7 @@ internal class MatrixModules(
|
||||
private val workModule: WorkModule,
|
||||
private val logger: MatrixLogger,
|
||||
private val coroutineDispatchers: CoroutineDispatchers,
|
||||
private val contentResolver: ContentResolver,
|
||||
) {
|
||||
|
||||
val matrix by unsafeLazy {
|
||||
@ -242,11 +258,13 @@ internal class MatrixModules(
|
||||
base64 = base64,
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler())) { serviceProvider ->
|
||||
val imageContentReader = AndroidImageContentReader(contentResolver)
|
||||
installMessageService(store.localEchoStore, BackgroundWorkAdapter(workModule.workScheduler()), imageContentReader) { serviceProvider ->
|
||||
MessageEncrypter { message ->
|
||||
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) {
|
||||
@ -261,6 +279,8 @@ internal class MatrixModules(
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> TODO()
|
||||
}
|
||||
)
|
||||
|
||||
@ -331,12 +351,14 @@ internal class MatrixModules(
|
||||
apiEvent.content.methods,
|
||||
apiEvent.content.timestampPosix,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -347,6 +369,7 @@ internal class MatrixModules(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||
is ApiToDeviceEvent.VerificationAccept -> TODO()
|
||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||
@ -354,6 +377,7 @@ internal class MatrixModules(
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.key
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
@ -374,6 +398,7 @@ internal class MatrixModules(
|
||||
val roomService = services.roomService()
|
||||
object : RoomMembersService {
|
||||
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
||||
override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
|
||||
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
||||
}
|
||||
},
|
||||
@ -403,3 +428,25 @@ internal class DomainModules(
|
||||
val pushModule by unsafeLazy { PushModule(matrixModules.push, errorTracker) }
|
||||
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) }
|
||||
}
|
||||
|
||||
internal class AndroidImageContentReader(private val contentResolver: ContentResolver) : ImageContentReader {
|
||||
override fun read(uri: String): ImageContentReader.ImageContent {
|
||||
val androidUri = Uri.parse(uri)
|
||||
val fileStream = contentResolver.openInputStream(androidUri) ?: throw IllegalArgumentException("Could not process $uri")
|
||||
|
||||
val options = BitmapFactory.Options().apply { inJustDecodeBounds = true }
|
||||
BitmapFactory.decodeStream(fileStream, null, options)
|
||||
|
||||
return contentResolver.openInputStream(androidUri)?.use { stream ->
|
||||
val output = stream.readBytes()
|
||||
ImageContentReader.ImageContent(
|
||||
height = options.outHeight,
|
||||
width = options.outWidth,
|
||||
size = output.size.toLong(),
|
||||
mimeType = options.outMimeType,
|
||||
fileName = androidUri.lastPathSegment ?: "file",
|
||||
content = output
|
||||
)
|
||||
} ?: throw IllegalArgumentException("Could not process $uri")
|
||||
}
|
||||
}
|
7
app/src/main/res/xml/shortcuts.xml
Normal file
7
app/src/main/res/xml/shortcuts.xml
Normal file
@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<share-target android:targetClass="app.dapk.st.share.ShareEntryActivity">
|
||||
<data android:mimeType="image/*" />
|
||||
<category android:name="android.shortcut.conversation" />
|
||||
</share-target>
|
||||
</shortcuts>
|
4
core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt
Normal file
4
core/src/main/kotlin/app/dapk/st/core/AndroidUri.kt
Normal file
@ -0,0 +1,4 @@
|
||||
package app.dapk.st.core
|
||||
|
||||
@JvmInline
|
||||
value class AndroidUri(val value: String)
|
5
core/src/main/kotlin/app/dapk/st/core/MimeType.kt
Normal file
5
core/src/main/kotlin/app/dapk/st/core/MimeType.kt
Normal file
@ -0,0 +1,5 @@
|
||||
package app.dapk.st.core
|
||||
|
||||
sealed interface MimeType {
|
||||
object Image: MimeType
|
||||
}
|
@ -3,5 +3,4 @@ plugins { id 'kotlin' }
|
||||
dependencies {
|
||||
compileOnly project(":domains:android:stub")
|
||||
implementation project(":core")
|
||||
implementation project(":features:navigator")
|
||||
}
|
||||
|
@ -35,4 +35,12 @@ class MemberPersistence(
|
||||
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun query(roomId: RoomId, limit: Int): List<RoomMember> {
|
||||
return coroutineDispatchers.withIoContext {
|
||||
database.roomMemberQueries.selectMembersByRoom(roomId.value, limit.toLong())
|
||||
.executeAsList()
|
||||
.map { Json.decodeFromString(RoomMember.serializer(), it) }
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -56,6 +58,7 @@ class LocalEchoPersistence(
|
||||
database.transaction {
|
||||
when (message) {
|
||||
is MessageService.Message.TextMessage -> database.localEchoQueries.delete(message.localId)
|
||||
is MessageService.Message.ImageMessage -> database.localEchoQueries.delete(message.localId)
|
||||
}
|
||||
}
|
||||
} catch (error: Exception) {
|
||||
@ -84,6 +87,14 @@ class LocalEchoPersistence(
|
||||
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> database.localEchoQueries.insert(
|
||||
DbLocalEcho(
|
||||
message.localId,
|
||||
message.roomId.value,
|
||||
Json.encodeToString(MessageService.LocalEcho.serializer(), localEcho)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,13 @@ SELECT blob
|
||||
FROM dbRoomMember
|
||||
WHERE room_id = ? AND user_id IN ?;
|
||||
|
||||
selectMembersByRoom:
|
||||
SELECT blob
|
||||
FROM dbRoomMember
|
||||
WHERE room_id = ?
|
||||
LIMIT ?;
|
||||
|
||||
|
||||
insert:
|
||||
INSERT OR REPLACE INTO dbRoomMember(user_id, room_id, blob)
|
||||
VALUES (?, ?, ?);
|
@ -84,6 +84,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,
|
||||
|
@ -2,6 +2,7 @@ package app.dapk.st.directory
|
||||
|
||||
import android.content.Context
|
||||
import android.content.pm.ShortcutInfo
|
||||
import androidx.core.app.Person
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
@ -14,21 +15,26 @@ class ShortcutHandler(private val context: Context) {
|
||||
|
||||
fun onDirectoryUpdate(overviews: List<RoomOverview>) {
|
||||
val update = overviews.map { it.roomId }
|
||||
|
||||
if (cachedRoomIds != update) {
|
||||
cachedRoomIds.clear()
|
||||
cachedRoomIds.addAll(update)
|
||||
|
||||
val currentShortcuts = ShortcutManagerCompat.getShortcuts(context, ShortcutManagerCompat.FLAG_MATCH_DYNAMIC)
|
||||
val maxShortcutCountPerActivity = ShortcutManagerCompat.getMaxShortcutCountPerActivity(context)
|
||||
|
||||
overviews
|
||||
.take(maxShortcutCountPerActivity)
|
||||
.filterNot { roomUpdate -> currentShortcuts.any { it.id == roomUpdate.roomId.value } }
|
||||
.forEachIndexed { index, room ->
|
||||
val build = ShortcutInfoCompat.Builder(context, room.roomId.value)
|
||||
.setShortLabel(room.roomName ?: "N/A")
|
||||
.setLongLabel(room.roomName ?: "N/A")
|
||||
.setRank(index)
|
||||
.run {
|
||||
this.setPerson(
|
||||
Person.Builder()
|
||||
.setName(room.roomName ?: "N/A")
|
||||
.setKey(room.roomId.value)
|
||||
.build()
|
||||
)
|
||||
}
|
||||
.setIntent(MessengerActivity.newShortcutInstance(context, room.roomId))
|
||||
.setLongLived(true)
|
||||
.setCategories(setOf(ShortcutInfo.SHORTCUT_CATEGORY_CONVERSATION))
|
||||
|
@ -8,7 +8,7 @@ import app.dapk.st.matrix.sync.RoomEvent
|
||||
|
||||
internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||
|
||||
fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent.Message {
|
||||
fun MessageService.LocalEcho.toMessage(member: RoomMember): RoomEvent {
|
||||
return when (val message = this.message) {
|
||||
is MessageService.Message.TextMessage -> {
|
||||
RoomEvent.Message(
|
||||
@ -19,6 +19,15 @@ internal class LocalEchoMapper(private val metaMapper: MetaMapper) {
|
||||
meta = metaMapper.toMeta(this)
|
||||
)
|
||||
}
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
RoomEvent.Image(
|
||||
eventId = this.eventId ?: EventId(this.localId),
|
||||
author = member,
|
||||
utcTimestamp = message.timestampUtc,
|
||||
meta = metaMapper.toMeta(this),
|
||||
imageMeta = RoomEvent.Image.ImageMeta(100, 100, message.content.uri, null),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -25,7 +25,7 @@ internal class MergeWithLocalEchosUseCaseImpl(
|
||||
return roomState.copy(events = sortedEvents)
|
||||
}
|
||||
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent.Message> {
|
||||
private fun uniqueEchos(echos: List<MessageService.LocalEcho>, stateByEventId: Map<EventId, RoomEvent>, member: RoomMember): List<RoomEvent> {
|
||||
return with(localEventMapper) {
|
||||
echos
|
||||
.filter { echo -> echo.eventId == null || stateByEventId[echo.eventId] == null }
|
||||
|
@ -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<MessageAttachment>): 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<MessagerActivityPayload>()
|
||||
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,10 @@ class MessengerActivity : DapkActivity() {
|
||||
|
||||
@Parcelize
|
||||
data class MessagerActivityPayload(
|
||||
val roomId: String
|
||||
val roomId: String,
|
||||
val attachments: List<MessageAttachment>? = null
|
||||
) : Parcelable
|
||||
|
||||
fun <T : Parcelable> Activity.readPayload(): T = intent.getParcelableExtra("key")!!
|
||||
fun <T : Parcelable> Activity.readPayload(): T = intent.getParcelableExtra("key") ?: intent.getStringExtra("shortcut_key")!!.let {
|
||||
MessagerActivityPayload(it) as T
|
||||
}
|
@ -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<MessageAttachment>?, 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) }
|
||||
)
|
||||
|
||||
@ -67,15 +69,23 @@ internal fun MessengerScreen(roomId: RoomId, viewModel: MessengerViewModel, navi
|
||||
}
|
||||
}
|
||||
})
|
||||
Room(state.roomState)
|
||||
when (state.composerState) {
|
||||
is ComposerState.Text -> {
|
||||
Composer(
|
||||
state.composerState.value,
|
||||
Room(state.roomState)
|
||||
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) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -122,6 +132,7 @@ private fun ColumnScope.Room(roomStateLce: Lce<MessengerState>) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is Lce.Error -> {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Column(horizontalAlignment = Alignment.CenterHorizontally) {
|
||||
@ -201,6 +212,7 @@ private fun <T : RoomEvent> LazyItemScope.AlignedBubble(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
false -> {
|
||||
Box(modifier = Modifier.fillParentMaxWidth(0.95f), contentAlignment = Alignment.TopStart) {
|
||||
Bubble(
|
||||
@ -311,9 +323,11 @@ private fun Bubble(
|
||||
wasPreviousMessageSameSender -> {
|
||||
Spacer(modifier = Modifier.width(displayImageSize))
|
||||
}
|
||||
|
||||
message.author.avatarUrl == null -> {
|
||||
MissingAvatarIcon(message.author.displayName ?: message.author.id.value, displayImageSize)
|
||||
}
|
||||
|
||||
else -> {
|
||||
MessengerUrlIcon(message.author.avatarUrl!!.value, displayImageSize)
|
||||
}
|
||||
@ -414,6 +428,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
|
||||
is RoomEvent.Image -> {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
@ -455,6 +470,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
||||
textAlign = TextAlign.Start,
|
||||
)
|
||||
}
|
||||
|
||||
is RoomEvent.Image -> {
|
||||
Spacer(modifier = Modifier.height(4.dp))
|
||||
Image(
|
||||
@ -498,6 +514,7 @@ private fun RowScope.SendStatus(message: RoomEvent) {
|
||||
MessageMeta.FromServer -> {
|
||||
// last message is self
|
||||
}
|
||||
|
||||
is MessageMeta.LocalEcho -> {
|
||||
when (val state = meta.state) {
|
||||
MessageMeta.LocalEcho.State.Sending, MessageMeta.LocalEcho.State.Sent -> {
|
||||
@ -514,6 +531,7 @@ private fun RowScope.SendStatus(message: RoomEvent) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is MessageMeta.LocalEcho.State.Error -> {
|
||||
Spacer(modifier = Modifier.width(4.dp))
|
||||
Box(
|
||||
@ -531,7 +549,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()
|
||||
@ -548,12 +566,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)),
|
||||
@ -564,10 +582,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
|
||||
@ -591,3 +609,36 @@ private fun Composer(message: String, onTextChange: (String) -> Unit, onSend: ()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun AttachmentComposer(state: ComposerState.Attachments, onSend: () -> Unit, onCancel: () -> Unit) {
|
||||
Box(modifier = Modifier.fillMaxSize()) {
|
||||
val context = LocalContext.current
|
||||
Image(
|
||||
modifier = Modifier.fillMaxHeight().wrapContentWidth().align(Alignment.Center),
|
||||
painter = rememberAsyncImagePainter(
|
||||
model = ImageRequest.Builder(context)
|
||||
.data(state.values.first().uri.value.toUri())
|
||||
.build()
|
||||
),
|
||||
contentDescription = null,
|
||||
)
|
||||
|
||||
Box(Modifier.align(Alignment.BottomEnd).padding(12.dp)) {
|
||||
IconButton(
|
||||
enabled = true,
|
||||
modifier = Modifier
|
||||
.clip(CircleShape)
|
||||
.background(MaterialTheme.colors.primary),
|
||||
onClick = onSend,
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Filled.Send,
|
||||
contentDescription = "",
|
||||
tint = MaterialTheme.colors.onPrimary,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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<MessageAttachment>,
|
||||
) : ComposerState
|
||||
|
||||
}
|
||||
|
@ -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.ApiImageContent(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>()
|
||||
.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<MessageAttachment>?) : MessengerAction
|
||||
object OnMessengerGone : MessengerAction
|
||||
}
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -71,7 +71,7 @@ class MessengerViewModelTest {
|
||||
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
|
||||
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state))
|
||||
|
||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID))
|
||||
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
|
||||
|
||||
assertStates<MessengerScreenState>(
|
||||
{ copy(roomId = A_ROOM_ID) },
|
||||
|
@ -1,4 +1,5 @@
|
||||
plugins { id 'kotlin' }
|
||||
applyAndroidLibraryModule(project)
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
dependencies {
|
||||
compileOnly project(":domains:android:stub")
|
||||
|
2
features/navigator/src/main/AndroidManifest.xml
Normal file
2
features/navigator/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,2 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="app.dapk.st.navigator"/>
|
@ -4,7 +4,13 @@ import android.app.Activity
|
||||
import android.app.PendingIntent
|
||||
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
|
||||
|
||||
@ -30,6 +36,11 @@ interface Navigator {
|
||||
activity.navigateUpTo(intentFactory.home(activity))
|
||||
}
|
||||
|
||||
fun toMessenger(roomId: RoomId, attachments: List<MessageAttachment>) {
|
||||
val intent = intentFactory.messengerAttachments(activity, roomId, attachments)
|
||||
activity.startActivity(intent)
|
||||
}
|
||||
|
||||
fun toFilePicker(requestCode: Int) {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
|
||||
addCategory(Intent.CATEGORY_OPENABLE)
|
||||
@ -46,6 +57,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<MessageAttachment>): Intent
|
||||
|
||||
}
|
||||
|
||||
@ -64,3 +76,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<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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -51,7 +51,6 @@ class NotificationRendererTest {
|
||||
}
|
||||
|
||||
notificationRenderer.render(NotificationState(emptyMap(), removedRooms, emptySet(), emptySet()))
|
||||
|
||||
verifyExpects()
|
||||
}
|
||||
|
||||
|
13
features/share-entry/build.gradle
Normal file
13
features/share-entry/build.gradle
Normal file
@ -0,0 +1,13 @@
|
||||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':matrix:services:sync')
|
||||
implementation project(':matrix:services:room')
|
||||
implementation project(':matrix:services:message')
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
implementation project(":features:navigator")
|
||||
}
|
29
features/share-entry/src/main/AndroidManifest.xml
Normal file
29
features/share-entry/src/main/AndroidManifest.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="app.dapk.st.share">
|
||||
|
||||
<application>
|
||||
<activity
|
||||
android:name="app.dapk.st.share.ShareEntryActivity"
|
||||
android:exported="true">
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.SEND_MULTIPLE" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:mimeType="image/*" />
|
||||
</intent-filter>
|
||||
|
||||
<meta-data
|
||||
android:name="android.service.chooser.chooser_target_service"
|
||||
android:value="androidx.sharetarget.ChooserTargetServiceCompat" />
|
||||
|
||||
</activity>
|
||||
</application>
|
||||
|
||||
</manifest>
|
@ -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 }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,52 @@
|
||||
package app.dapk.st.share
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Parcelable
|
||||
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.design.components.SmallTalkTheme
|
||||
|
||||
class ShareEntryActivity : DapkActivity() {
|
||||
|
||||
private val viewModel by viewModel { module<ShareEntryModule>().shareEntryViewModel() }
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val urisToShare = intent.readSendUrisOrNull() ?: throw IllegalArgumentException("Expected deeplink uris but they were missing")
|
||||
setContent {
|
||||
SmallTalkTheme {
|
||||
Surface(Modifier.fillMaxSize()) {
|
||||
ShareEntryScreen(navigator, viewModel)
|
||||
}
|
||||
}
|
||||
}
|
||||
viewModel.withUris(urisToShare)
|
||||
}
|
||||
}
|
||||
|
||||
private fun Intent.readSendUrisOrNull(): List<Uri>? {
|
||||
return when (this.action) {
|
||||
Intent.ACTION_SEND -> {
|
||||
if (this.hasExtra(Intent.EXTRA_STREAM)) {
|
||||
listOf(this.getParcelableExtra<Parcelable>(Intent.EXTRA_STREAM) as Uri)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
Intent.ACTION_SEND_MULTIPLE -> {
|
||||
if (this.hasExtra(Intent.EXTRA_STREAM)) {
|
||||
(this.getParcelableArrayExtra(Intent.EXTRA_STREAM) as Array<Uri>).toList()
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
else -> null
|
||||
}
|
||||
}
|
@ -0,0 +1,15 @@
|
||||
package app.dapk.st.share
|
||||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
|
||||
class ShareEntryModule(
|
||||
private val syncService: SyncService,
|
||||
private val roomService: RoomService,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun shareEntryViewModel(): ShareEntryViewModel {
|
||||
return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService))
|
||||
}
|
||||
}
|
@ -0,0 +1,121 @@
|
||||
package app.dapk.st.share
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||
import androidx.compose.material.MaterialTheme
|
||||
import androidx.compose.material.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
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.navigator.MessageAttachment
|
||||
import app.dapk.st.navigator.Navigator
|
||||
import app.dapk.st.share.DirectoryScreenState.*
|
||||
|
||||
@Composable
|
||||
fun ShareEntryScreen(navigator: Navigator, viewModel: ShareEntryViewModel) {
|
||||
val state = viewModel.state
|
||||
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) {
|
||||
EmptyLoading -> CenteredLoading()
|
||||
Empty -> GenericEmpty()
|
||||
is Error -> GenericError {
|
||||
// TODO
|
||||
}
|
||||
is Content -> Content(listState, state) {
|
||||
viewModel.onRoomSelected(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ShareEntryViewModel.ObserveEvents(navigator: Navigator) {
|
||||
StartObserving {
|
||||
this@ObserveEvents.events.launch {
|
||||
when (it) {
|
||||
is DirectoryEvent.SelectRoom -> {
|
||||
navigator.navigate.toMessenger(it.item.id, it.uris.map { MessageAttachment(it, MimeType.Image) })
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Composable
|
||||
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 = onClick)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DirectoryItem(item: Item, onClick: (Item) -> Unit) {
|
||||
val roomName = item.roomName.ifEmpty { "Empty " }
|
||||
|
||||
Box(
|
||||
Modifier
|
||||
.height(IntrinsicSize.Min)
|
||||
.fillMaxWidth()
|
||||
.clickable { onClick(item) }
|
||||
) {
|
||||
Row(Modifier.padding(20.dp)) {
|
||||
val secondaryText = MaterialTheme.colors.onBackground.copy(alpha = 0.5f)
|
||||
|
||||
Box(Modifier.fillMaxHeight(), contentAlignment = Alignment.Center) {
|
||||
CircleishAvatar(item.roomAvatarUrl?.value, roomName, size = 50.dp)
|
||||
}
|
||||
Spacer(Modifier.width(20.dp))
|
||||
Column {
|
||||
Row(horizontalArrangement = Arrangement.SpaceBetween, modifier = Modifier.fillMaxWidth()) {
|
||||
Text(
|
||||
modifier = Modifier.weight(1f),
|
||||
maxLines = 1,
|
||||
fontSize = 18.sp,
|
||||
text = roomName,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
fontFamily = FontFamily.SansSerif,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = MaterialTheme.colors.onBackground
|
||||
)
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
}
|
||||
Text(text = item.members.joinToString(), color = secondaryText, maxLines = 1, overflow = TextOverflow.Ellipsis)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,20 @@
|
||||
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
|
||||
|
||||
sealed interface DirectoryScreenState {
|
||||
|
||||
object EmptyLoading : DirectoryScreenState
|
||||
object Empty : DirectoryScreenState
|
||||
data class Content(
|
||||
val items: List<Item>,
|
||||
) : DirectoryScreenState
|
||||
}
|
||||
|
||||
sealed interface 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>)
|
@ -0,0 +1,43 @@
|
||||
package app.dapk.st.share
|
||||
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.core.AndroidUri
|
||||
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.launch
|
||||
|
||||
class ShareEntryViewModel(
|
||||
private val fetchRoomsUseCase: FetchRoomsUseCase,
|
||||
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
|
||||
initialState = DirectoryScreenState.EmptyLoading,
|
||||
factory,
|
||||
) {
|
||||
|
||||
private var urisToShare: List<AndroidUri>? = null
|
||||
private var syncJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
syncJob = viewModelScope.launch {
|
||||
state = DirectoryScreenState.Content(fetchRoomsUseCase.bar())
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
syncJob?.cancel()
|
||||
}
|
||||
|
||||
fun withUris(urisToShare: List<Uri>) {
|
||||
this.urisToShare = urisToShare.map { AndroidUri(it.toString()) }
|
||||
}
|
||||
|
||||
fun onRoomSelected(item: Item) {
|
||||
viewModelScope.launch {
|
||||
_events.emit(DirectoryEvent.SelectRoom(item, uris = urisToShare ?: throw IllegalArgumentException("Not uris set")))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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"),
|
||||
}
|
@ -1,10 +1,16 @@
|
||||
package app.dapk.st.matrix.message
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ApiSendResponse(
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ApiUploadResponse(
|
||||
@SerialName("content_uri") val contentUri: MxUrl,
|
||||
)
|
@ -4,11 +4,9 @@ import app.dapk.st.matrix.MatrixService
|
||||
import app.dapk.st.matrix.MatrixServiceInstaller
|
||||
import app.dapk.st.matrix.MatrixServiceProvider
|
||||
import app.dapk.st.matrix.ServiceDepFactory
|
||||
import app.dapk.st.matrix.common.AlgorithmName
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MessageType
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.message.internal.DefaultMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
@ -46,6 +44,16 @@ interface MessageService : MatrixService {
|
||||
@SerialName("timestamp") val timestampUtc: Long,
|
||||
) : Message()
|
||||
|
||||
@Serializable
|
||||
@SerialName("image_message")
|
||||
data class ImageMessage(
|
||||
@SerialName("content") val content: Content.ApiImageContent,
|
||||
@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 +61,27 @@ interface MessageService : MatrixService {
|
||||
@SerialName("body") val body: String,
|
||||
@SerialName("msgtype") val type: String = MessageType.TEXT.value,
|
||||
) : Content()
|
||||
|
||||
@Serializable
|
||||
data class ApiImageContent(
|
||||
@SerialName("uri") val uri: String,
|
||||
) : Content()
|
||||
|
||||
@Serializable
|
||||
data class ImageContent(
|
||||
@SerialName("url") val url: MxUrl,
|
||||
@SerialName("body") val filename: String,
|
||||
@SerialName("info") val info: Info,
|
||||
@SerialName("msgtype") val type: String = MessageType.IMAGE.value,
|
||||
) : Content() {
|
||||
|
||||
@Serializable
|
||||
data class Info(
|
||||
@SerialName("h") val height: Int,
|
||||
@SerialName("w") val width: Int,
|
||||
@SerialName("size") val size: Long,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -66,16 +95,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
|
||||
@ -108,10 +140,11 @@ interface MessageService : MatrixService {
|
||||
fun MatrixServiceInstaller.installMessageService(
|
||||
localEchoStore: LocalEchoStore,
|
||||
backgroundScheduler: BackgroundScheduler,
|
||||
imageContentReader: ImageContentReader,
|
||||
messageEncrypter: ServiceDepFactory<MessageEncrypter> = ServiceDepFactory { MissingMessageEncrypter },
|
||||
) {
|
||||
this.install { (httpClient, _, installedServices) ->
|
||||
SERVICE_KEY to DefaultMessageService(httpClient, localEchoStore, backgroundScheduler, messageEncrypter.create(installedServices))
|
||||
SERVICE_KEY to DefaultMessageService(httpClient, localEchoStore, backgroundScheduler, messageEncrypter.create(installedServices), imageContentReader)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -13,22 +13,27 @@ 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,
|
||||
private val localEchoStore: LocalEchoStore,
|
||||
private val backgroundScheduler: BackgroundScheduler,
|
||||
messageEncrypter: MessageEncrypter,
|
||||
imageContentReader: ImageContentReader,
|
||||
) : MessageService, MatrixTaskRunner {
|
||||
|
||||
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter)
|
||||
private val sendMessageUseCase = SendMessageUseCase(httpClient, messageEncrypter, imageContentReader)
|
||||
private val sendEventMessageUseCase = SendEventMessageUseCase(httpClient)
|
||||
|
||||
override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE
|
||||
override suspend fun canRun(task: MatrixTaskRunner.MatrixTask) = task.type == MATRIX_MESSAGE_TASK_TYPE || task.type == MATRIX_IMAGE_MESSAGE_TASK_TYPE
|
||||
|
||||
override suspend fun run(task: MatrixTaskRunner.MatrixTask): MatrixTaskRunner.TaskResult {
|
||||
require(task.type == MATRIX_MESSAGE_TASK_TYPE)
|
||||
val message = Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload)
|
||||
val message = when(task.type) {
|
||||
MATRIX_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.TextMessage.serializer(), task.jsonPayload)
|
||||
MATRIX_IMAGE_MESSAGE_TASK_TYPE -> Json.decodeFromString(MessageService.Message.ImageMessage.serializer(), task.jsonPayload)
|
||||
else -> throw IllegalStateException("Unhandled task type: ${task.type}")
|
||||
}
|
||||
return try {
|
||||
sendMessage(message)
|
||||
MatrixTaskRunner.TaskResult.Success
|
||||
@ -50,6 +55,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 +74,11 @@ 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)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,36 @@
|
||||
package app.dapk.st.matrix.message.internal
|
||||
|
||||
interface ImageContentReader {
|
||||
fun read(uri: String): ImageContent
|
||||
|
||||
data class ImageContent(
|
||||
val height: Int,
|
||||
val width: Int,
|
||||
val size: Long,
|
||||
val fileName: String,
|
||||
val mimeType: String,
|
||||
val content: ByteArray
|
||||
) {
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (javaClass != other?.javaClass) return false
|
||||
|
||||
other as ImageContent
|
||||
|
||||
if (height != other.height) return false
|
||||
if (width != other.width) return false
|
||||
if (size != other.size) return false
|
||||
if (!content.contentEquals(other.content)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = height
|
||||
result = 31 * result + width
|
||||
result = 31 * result + size.hashCode()
|
||||
result = 31 * result + content.contentHashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
}
|
@ -9,6 +9,7 @@ import app.dapk.st.matrix.message.MessageService
|
||||
internal class SendMessageUseCase(
|
||||
private val httpClient: MatrixHttpClient,
|
||||
private val messageEncrypter: MessageEncrypter,
|
||||
private val imageContentReader: ImageContentReader,
|
||||
) {
|
||||
|
||||
suspend fun sendMessage(message: MessageService.Message): EventId {
|
||||
@ -23,6 +24,7 @@ internal class SendMessageUseCase(
|
||||
content = messageEncrypter.encrypt(message),
|
||||
)
|
||||
}
|
||||
|
||||
false -> {
|
||||
sendRequest(
|
||||
roomId = message.roomId,
|
||||
@ -34,6 +36,26 @@ internal class SendMessageUseCase(
|
||||
}
|
||||
httpClient.execute(request).eventId
|
||||
}
|
||||
|
||||
is MessageService.Message.ImageMessage -> {
|
||||
val imageContent = imageContentReader.read(message.content.uri)
|
||||
val uri = httpClient.execute(uploadRequest(imageContent.content, imageContent.fileName, imageContent.mimeType)).contentUri
|
||||
val request = sendRequest(
|
||||
roomId = message.roomId,
|
||||
eventType = EventType.ROOM_MESSAGE,
|
||||
txId = message.localId,
|
||||
content = MessageService.Message.Content.ImageContent(
|
||||
url = uri,
|
||||
filename = imageContent.fileName,
|
||||
MessageService.Message.Content.ImageContent.Info(
|
||||
height = imageContent.height,
|
||||
width = imageContent.width,
|
||||
size = imageContent.size
|
||||
)
|
||||
),
|
||||
)
|
||||
httpClient.execute(request).eventId
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,9 +6,12 @@ import app.dapk.st.matrix.http.MatrixHttpClient
|
||||
import app.dapk.st.matrix.http.MatrixHttpClient.HttpRequest.Companion.httpRequest
|
||||
import app.dapk.st.matrix.http.jsonBody
|
||||
import app.dapk.st.matrix.message.ApiSendResponse
|
||||
import app.dapk.st.matrix.message.ApiUploadResponse
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService.EventMessage
|
||||
import app.dapk.st.matrix.message.MessageService.Message
|
||||
import io.ktor.content.*
|
||||
import io.ktor.http.*
|
||||
import java.util.*
|
||||
|
||||
internal fun sendRequest(roomId: RoomId, eventType: EventType, txId: String, content: Message.Content) = httpRequest<ApiSendResponse>(
|
||||
@ -16,6 +19,8 @@ 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)
|
||||
is Message.Content.ApiImageContent -> throw IllegalArgumentException()
|
||||
}
|
||||
)
|
||||
|
||||
@ -33,4 +38,12 @@ internal fun sendRequest(roomId: RoomId, eventType: EventType, content: EventMes
|
||||
}
|
||||
)
|
||||
|
||||
internal fun uploadRequest(body: ByteArray, filename: String, contentType: String) = httpRequest<ApiUploadResponse>(
|
||||
path = "_matrix/media/r0/upload/?filename=$filename",
|
||||
headers = listOf("Content-Type" to contentType),
|
||||
method = MatrixHttpClient.Method.POST,
|
||||
body = ByteArrayContent(body, ContentType.parse(contentType)),
|
||||
)
|
||||
|
||||
|
||||
fun txId() = "local.${UUID.randomUUID()}"
|
@ -21,6 +21,7 @@ interface RoomService : MatrixService {
|
||||
|
||||
suspend fun findMember(roomId: RoomId, userId: UserId): RoomMember?
|
||||
suspend fun findMembers(roomId: RoomId, userIds: List<UserId>): List<RoomMember>
|
||||
suspend fun findMembersSummary(roomId: RoomId): List<RoomMember>
|
||||
suspend fun insertMembers(roomId: RoomId, members: List<RoomMember>)
|
||||
|
||||
suspend fun createDm(userId: UserId, encrypted: Boolean): RoomId
|
||||
@ -50,6 +51,7 @@ fun MatrixServiceProvider.roomService(): RoomService = this.getService(key = SER
|
||||
interface MemberStore {
|
||||
suspend fun insert(roomId: RoomId, members: List<RoomMember>)
|
||||
suspend fun query(roomId: RoomId, userIds: List<UserId>): List<RoomMember>
|
||||
suspend fun query(roomId: RoomId, limit: Int): List<RoomMember>
|
||||
}
|
||||
|
||||
interface RoomMessenger {
|
||||
|
@ -39,6 +39,10 @@ class DefaultRoomService(
|
||||
return roomMembers.findMembers(roomId, userIds)
|
||||
}
|
||||
|
||||
override suspend fun findMembersSummary(roomId: RoomId): List<RoomMember> {
|
||||
return roomMembers.findMembersSummary(roomId)
|
||||
}
|
||||
|
||||
override suspend fun insertMembers(roomId: RoomId, members: List<RoomMember>) {
|
||||
roomMembers.insert(roomId, members)
|
||||
}
|
||||
|
@ -36,6 +36,8 @@ class RoomMembers(private val memberStore: MemberStore, private val membersCache
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun findMembersSummary(roomId: RoomId) = memberStore.query(roomId, limit = 8)
|
||||
|
||||
suspend fun insert(roomId: RoomId, members: List<RoomMember>) {
|
||||
membersCache.insert(roomId, members)
|
||||
memberStore.insert(roomId, members)
|
||||
|
@ -128,6 +128,7 @@ internal object NoOpKeySharer : KeySharer {
|
||||
|
||||
interface RoomMembersService {
|
||||
suspend fun find(roomId: RoomId, userIds: List<UserId>): List<RoomMember>
|
||||
suspend fun findSummary(roomId: RoomId): List<RoomMember>
|
||||
suspend fun insert(roomId: RoomId, members: List<RoomMember>)
|
||||
}
|
||||
|
||||
|
@ -0,0 +1,33 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class ApiAccountEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.direct")
|
||||
data class Direct(
|
||||
@SerialName("content") val content: Map<UserId, List<RoomId>>
|
||||
) : ApiAccountEvent()
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.fully_read")
|
||||
data class FullyRead(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiAccountEvent() {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiAccountEvent()
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.CipherText
|
||||
import app.dapk.st.matrix.common.Curve25519
|
||||
import app.dapk.st.matrix.common.DeviceId
|
||||
import app.dapk.st.matrix.common.SessionId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable(with = EncryptedContentDeserializer::class)
|
||||
internal sealed class ApiEncryptedContent {
|
||||
@Serializable
|
||||
data class OlmV1(
|
||||
@SerialName("ciphertext") val cipherText: Map<Curve25519, CipherTextInfo>,
|
||||
@SerialName("sender_key") val senderKey: Curve25519,
|
||||
) : ApiEncryptedContent()
|
||||
|
||||
@Serializable
|
||||
data class MegOlmV1(
|
||||
@SerialName("ciphertext") val cipherText: CipherText,
|
||||
@SerialName("device_id") val deviceId: DeviceId,
|
||||
@SerialName("sender_key") val senderKey: String,
|
||||
@SerialName("session_id") val sessionId: SessionId,
|
||||
@SerialName("m.relates_to") val relation: ApiTimelineEvent.TimelineMessage.Relation? = null,
|
||||
) : ApiEncryptedContent()
|
||||
|
||||
@Serializable
|
||||
data class CipherTextInfo(
|
||||
@SerialName("body") val body: CipherText,
|
||||
@SerialName("type") val type: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
object Unknown : ApiEncryptedContent()
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class ApiStrippedEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.member")
|
||||
internal data class RoomMember(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiStrippedEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("displayname") val displayName: String? = null,
|
||||
@SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null,
|
||||
@SerialName("is_direct") val isDirect: Boolean? = null,
|
||||
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.name")
|
||||
internal data class RoomName(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiStrippedEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("name") val name: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiStrippedEvent()
|
||||
}
|
@ -1,16 +1,11 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.*
|
||||
import kotlinx.serialization.KSerializer
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.ServerKeyCount
|
||||
import app.dapk.st.matrix.common.SyncToken
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
@Serializable
|
||||
internal data class ApiSyncResponse(
|
||||
@ -28,32 +23,6 @@ data class ApiAccountData(
|
||||
@SerialName("events") val events: List<ApiAccountEvent>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ApiAccountEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.direct")
|
||||
data class Direct(
|
||||
@SerialName("content") val content: Map<UserId, List<RoomId>>
|
||||
) : ApiAccountEvent()
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.fully_read")
|
||||
data class FullyRead(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiAccountEvent() {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiAccountEvent()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class DeviceLists(
|
||||
@SerialName("changed") val changed: List<UserId>? = null
|
||||
@ -64,152 +33,6 @@ internal data class ToDevice(
|
||||
@SerialName("events") val events: List<ApiToDeviceEvent>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ApiToDeviceEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encrypted")
|
||||
internal data class Encrypted(
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val content: ApiEncryptedContent,
|
||||
) : ApiToDeviceEvent()
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room_key")
|
||||
data class RoomKey(
|
||||
@SerialName("sender") val sender: UserId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiToDeviceEvent() {
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("room_id") val roomId: RoomId,
|
||||
@SerialName("algorithm") val algorithmName: AlgorithmName,
|
||||
@SerialName("session_id") val sessionId: SessionId,
|
||||
@SerialName("session_key") val sessionKey: String,
|
||||
@SerialName("chain_index") val chainIndex: Long,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.request")
|
||||
data class VerificationRequest(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("methods") val methods: List<String>,
|
||||
@SerialName("timestamp") val timestampPosix: Long,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.ready")
|
||||
data class VerificationReady(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("methods") val methods: List<String>,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.start")
|
||||
data class VerificationStart(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("method") val method: String,
|
||||
@SerialName("key_agreement_protocols") val protocols: List<String>,
|
||||
@SerialName("hashes") val hashes: List<String>,
|
||||
@SerialName("message_authentication_codes") val codes: List<String>,
|
||||
@SerialName("short_authentication_string") val short: List<String>,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.accept")
|
||||
data class VerificationAccept(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("method") val method: String,
|
||||
@SerialName("key_agreement_protocol") val protocol: String,
|
||||
@SerialName("hash") val hash: String,
|
||||
@SerialName("message_authentication_code") val code: String,
|
||||
@SerialName("short_authentication_string") val short: List<String>,
|
||||
@SerialName("commitment") val commitment: String,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.key")
|
||||
data class VerificationKey(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
@SerialName("key") val key: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.mac")
|
||||
data class VerificationMac(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
@SerialName("keys") val keys: String,
|
||||
@SerialName("mac") val mac: Map<String, String>,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.cancel")
|
||||
data class VerificationCancel(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("code") val code: String,
|
||||
@SerialName("reason") val reason: String,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiToDeviceEvent()
|
||||
|
||||
|
||||
sealed interface ApiVerificationEvent
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class ApiSyncRooms(
|
||||
@SerialName("join") val join: Map<RoomId, ApiSyncRoom>? = null,
|
||||
@ -227,41 +50,6 @@ internal data class ApiInviteEvents(
|
||||
@SerialName("events") val events: List<ApiStrippedEvent>
|
||||
)
|
||||
|
||||
@Serializable
|
||||
sealed class ApiStrippedEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.member")
|
||||
internal data class RoomMember(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiStrippedEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("displayname") val displayName: String? = null,
|
||||
@SerialName("membership") val membership: ApiTimelineEvent.RoomMember.Content.Membership? = null,
|
||||
@SerialName("is_direct") val isDirect: Boolean? = null,
|
||||
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.name")
|
||||
internal data class RoomName(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiStrippedEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("name") val name: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiStrippedEvent()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class ApiSyncRoom(
|
||||
@SerialName("timeline") val timeline: ApiSyncRoomTimeline,
|
||||
@ -317,265 +105,3 @@ internal sealed class DecryptedContent {
|
||||
@Serializable
|
||||
object Ignored : DecryptedContent()
|
||||
}
|
||||
|
||||
|
||||
@Serializable(with = EncryptedContentDeserializer::class)
|
||||
internal sealed class ApiEncryptedContent {
|
||||
@Serializable
|
||||
data class OlmV1(
|
||||
@SerialName("ciphertext") val cipherText: Map<Curve25519, CipherTextInfo>,
|
||||
@SerialName("sender_key") val senderKey: Curve25519,
|
||||
) : ApiEncryptedContent()
|
||||
|
||||
@Serializable
|
||||
data class MegOlmV1(
|
||||
@SerialName("ciphertext") val cipherText: CipherText,
|
||||
@SerialName("device_id") val deviceId: DeviceId,
|
||||
@SerialName("sender_key") val senderKey: String,
|
||||
@SerialName("session_id") val sessionId: SessionId,
|
||||
@SerialName("m.relates_to") val relation: ApiTimelineEvent.TimelineMessage.Relation? = null,
|
||||
) : ApiEncryptedContent()
|
||||
|
||||
@Serializable
|
||||
data class CipherTextInfo(
|
||||
@SerialName("body") val body: CipherText,
|
||||
@SerialName("type") val type: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
object Unknown : ApiEncryptedContent()
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal sealed class ApiTimelineEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.create")
|
||||
internal data class RoomCreate(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("type") val type: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.topic")
|
||||
internal data class RoomTopic(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("topic") val topic: String
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.name")
|
||||
internal data class RoomName(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("name") val name: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.avatar")
|
||||
internal data class RoomAvatar(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("url") val url: MxUrl? = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.member")
|
||||
internal data class RoomMember(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("displayname") val displayName: String? = null,
|
||||
@SerialName("membership") val membership: Membership,
|
||||
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
|
||||
) {
|
||||
|
||||
@JvmInline
|
||||
@Serializable
|
||||
value class Membership(val value: String) {
|
||||
fun isJoin() = value == "join"
|
||||
fun isInvite() = value == "invite"
|
||||
fun isLeave() = value == "leave"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class DecryptionStatus(
|
||||
@SerialName("is_verified") val isVerified: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.message")
|
||||
internal data class TimelineMessage(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable(with = ApiTimelineMessageContentDeserializer::class)
|
||||
internal sealed interface Content {
|
||||
val relation: Relation?
|
||||
|
||||
@Serializable
|
||||
data class Text(
|
||||
@SerialName("body") val body: String? = null,
|
||||
@SerialName("formatted_body") val formattedBody: String? = null,
|
||||
@SerialName("m.relates_to") override val relation: Relation? = null,
|
||||
@SerialName("msgtype") val messageType: String = "m.text",
|
||||
) : Content
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
@SerialName("url") val url: MxUrl? = null,
|
||||
@SerialName("file") val file: File? = null,
|
||||
@SerialName("info") val info: Info? = null,
|
||||
@SerialName("m.relates_to") override val relation: Relation? = null,
|
||||
@SerialName("msgtype") val messageType: String = "m.image",
|
||||
) : Content {
|
||||
|
||||
@Serializable
|
||||
data class File(
|
||||
@SerialName("url") val url: MxUrl,
|
||||
@SerialName("iv") val iv: String,
|
||||
@SerialName("v") val v: String,
|
||||
@SerialName("hashes") val hashes: Map<String, String>,
|
||||
@SerialName("key") val key: Key,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Key(
|
||||
@SerialName("k") val k: String,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class Info(
|
||||
@SerialName("h") val height: Int? = null,
|
||||
@SerialName("w") val width: Int? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : Content {
|
||||
override val relation: Relation? = null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Relation(
|
||||
@SerialName("m.in_reply_to") val inReplyTo: InReplyTo? = null,
|
||||
@SerialName("rel_type") val relationType: String? = null,
|
||||
@SerialName("event_id") val eventId: EventId? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class InReplyTo(
|
||||
@SerialName("event_id") val eventId: EventId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encryption")
|
||||
data class Encryption(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("algorithm") val algorithm: AlgorithmName,
|
||||
@SerialName("rotation_period_ms") val rotationMs: Long? = null,
|
||||
@SerialName("rotation_period_msgs") val rotationMessages: Long? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encrypted")
|
||||
internal data class Encrypted(
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val encryptedContent: ApiEncryptedContent,
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
) : ApiTimelineEvent()
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiTimelineEvent()
|
||||
}
|
||||
|
||||
|
||||
internal object EncryptedContentDeserializer : KSerializer<ApiEncryptedContent> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("encryptedContent")
|
||||
|
||||
override fun deserialize(decoder: Decoder): ApiEncryptedContent {
|
||||
require(decoder is JsonDecoder)
|
||||
val element = decoder.decodeJsonElement()
|
||||
return when (val algorithm = element.jsonObject["algorithm"]?.jsonPrimitive?.content) {
|
||||
"m.olm.v1.curve25519-aes-sha2" -> ApiEncryptedContent.OlmV1.serializer().deserialize(decoder)
|
||||
"m.megolm.v1.aes-sha2" -> ApiEncryptedContent.MegOlmV1.serializer().deserialize(decoder)
|
||||
null -> ApiEncryptedContent.Unknown
|
||||
else -> throw IllegalArgumentException("Unknown algorithm : $algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented")
|
||||
|
||||
}
|
||||
|
||||
internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineEvent.TimelineMessage.Content> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("messageContent")
|
||||
|
||||
override fun deserialize(decoder: Decoder): ApiTimelineEvent.TimelineMessage.Content {
|
||||
require(decoder is JsonDecoder)
|
||||
val element = decoder.decodeJsonElement()
|
||||
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
|
||||
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
|
||||
"m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
|
||||
else -> {
|
||||
println(element)
|
||||
ApiTimelineEvent.TimelineMessage.Content.Ignored
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ApiTimelineEvent.TimelineMessage.Content) = when (value) {
|
||||
ApiTimelineEvent.TimelineMessage.Content.Ignored -> {}
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Image -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().serialize(encoder, value)
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().serialize(encoder, value)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,197 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.AlgorithmName
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
internal sealed class ApiTimelineEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.create")
|
||||
internal data class RoomCreate(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("type") val type: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.topic")
|
||||
internal data class RoomTopic(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("topic") val topic: String
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.name")
|
||||
internal data class RoomName(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("name") val name: String
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.avatar")
|
||||
internal data class RoomAvatar(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("url") val url: MxUrl? = null
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.member")
|
||||
internal data class RoomMember(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable
|
||||
internal data class Content(
|
||||
@SerialName("displayname") val displayName: String? = null,
|
||||
@SerialName("membership") val membership: Membership,
|
||||
@SerialName("avatar_url") val avatarUrl: MxUrl? = null,
|
||||
) {
|
||||
|
||||
@JvmInline
|
||||
@Serializable
|
||||
value class Membership(val value: String) {
|
||||
fun isJoin() = value == "join"
|
||||
fun isInvite() = value == "invite"
|
||||
fun isLeave() = value == "leave"
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class DecryptionStatus(
|
||||
@SerialName("is_verified") val isVerified: Boolean
|
||||
)
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.message")
|
||||
internal data class TimelineMessage(
|
||||
@SerialName("event_id") val id: EventId,
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
@SerialName("st.decryption_status") val decryptionStatus: DecryptionStatus? = null
|
||||
) : ApiTimelineEvent() {
|
||||
|
||||
@Serializable(with = ApiTimelineMessageContentDeserializer::class)
|
||||
internal sealed interface Content {
|
||||
val relation: Relation?
|
||||
|
||||
@Serializable
|
||||
data class Text(
|
||||
@SerialName("body") val body: String? = null,
|
||||
@SerialName("formatted_body") val formattedBody: String? = null,
|
||||
@SerialName("m.relates_to") override val relation: Relation? = null,
|
||||
@SerialName("msgtype") val messageType: String = "m.text",
|
||||
) : Content
|
||||
|
||||
@Serializable
|
||||
data class Image(
|
||||
@SerialName("url") val url: MxUrl? = null,
|
||||
@SerialName("file") val file: File? = null,
|
||||
@SerialName("info") val info: Info? = null,
|
||||
@SerialName("m.relates_to") override val relation: Relation? = null,
|
||||
@SerialName("msgtype") val messageType: String = "m.image",
|
||||
) : Content {
|
||||
|
||||
@Serializable
|
||||
data class File(
|
||||
@SerialName("url") val url: MxUrl,
|
||||
@SerialName("iv") val iv: String,
|
||||
@SerialName("v") val v: String,
|
||||
@SerialName("hashes") val hashes: Map<String, String>,
|
||||
@SerialName("key") val key: Key,
|
||||
) {
|
||||
|
||||
@Serializable
|
||||
data class Key(
|
||||
@SerialName("k") val k: String,
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
internal data class Info(
|
||||
@SerialName("h") val height: Int? = null,
|
||||
@SerialName("w") val width: Int? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : Content {
|
||||
override val relation: Relation? = null
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Relation(
|
||||
@SerialName("m.in_reply_to") val inReplyTo: InReplyTo? = null,
|
||||
@SerialName("rel_type") val relationType: String? = null,
|
||||
@SerialName("event_id") val eventId: EventId? = null
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class InReplyTo(
|
||||
@SerialName("event_id") val eventId: EventId
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encryption")
|
||||
data class Encryption(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiTimelineEvent() {
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("algorithm") val algorithm: AlgorithmName,
|
||||
@SerialName("rotation_period_ms") val rotationMs: Long? = null,
|
||||
@SerialName("rotation_period_msgs") val rotationMessages: Long? = null,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encrypted")
|
||||
internal data class Encrypted(
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val encryptedContent: ApiEncryptedContent,
|
||||
@SerialName("event_id") val eventId: EventId,
|
||||
@SerialName("origin_server_ts") val utcTimestamp: Long,
|
||||
) : ApiTimelineEvent()
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiTimelineEvent()
|
||||
}
|
@ -0,0 +1,32 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal object ApiTimelineMessageContentDeserializer : KSerializer<ApiTimelineEvent.TimelineMessage.Content> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("messageContent")
|
||||
|
||||
override fun deserialize(decoder: Decoder): ApiTimelineEvent.TimelineMessage.Content {
|
||||
require(decoder is JsonDecoder)
|
||||
val element = decoder.decodeJsonElement()
|
||||
return when (element.jsonObject["msgtype"]?.jsonPrimitive?.content) {
|
||||
"m.text" -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().deserialize(decoder)
|
||||
"m.image" -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().deserialize(decoder)
|
||||
else -> ApiTimelineEvent.TimelineMessage.Content.Ignored
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ApiTimelineEvent.TimelineMessage.Content) = when (value) {
|
||||
ApiTimelineEvent.TimelineMessage.Content.Ignored -> {}
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Image -> ApiTimelineEvent.TimelineMessage.Content.Image.serializer().serialize(encoder, value)
|
||||
is ApiTimelineEvent.TimelineMessage.Content.Text -> ApiTimelineEvent.TimelineMessage.Content.Text.serializer().serialize(encoder, value)
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import app.dapk.st.matrix.common.*
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
sealed class ApiToDeviceEvent {
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room.encrypted")
|
||||
internal data class Encrypted(
|
||||
@SerialName("sender") val senderId: UserId,
|
||||
@SerialName("content") val content: ApiEncryptedContent,
|
||||
) : ApiToDeviceEvent()
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.room_key")
|
||||
data class RoomKey(
|
||||
@SerialName("sender") val sender: UserId,
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiToDeviceEvent() {
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("room_id") val roomId: RoomId,
|
||||
@SerialName("algorithm") val algorithmName: AlgorithmName,
|
||||
@SerialName("session_id") val sessionId: SessionId,
|
||||
@SerialName("session_key") val sessionKey: String,
|
||||
@SerialName("chain_index") val chainIndex: Long,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.request")
|
||||
data class VerificationRequest(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("methods") val methods: List<String>,
|
||||
@SerialName("timestamp") val timestampPosix: Long,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.ready")
|
||||
data class VerificationReady(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("methods") val methods: List<String>,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.start")
|
||||
data class VerificationStart(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("method") val method: String,
|
||||
@SerialName("key_agreement_protocols") val protocols: List<String>,
|
||||
@SerialName("hashes") val hashes: List<String>,
|
||||
@SerialName("message_authentication_codes") val codes: List<String>,
|
||||
@SerialName("short_authentication_string") val short: List<String>,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.accept")
|
||||
data class VerificationAccept(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("from_device") val fromDevice: DeviceId,
|
||||
@SerialName("method") val method: String,
|
||||
@SerialName("key_agreement_protocol") val protocol: String,
|
||||
@SerialName("hash") val hash: String,
|
||||
@SerialName("message_authentication_code") val code: String,
|
||||
@SerialName("short_authentication_string") val short: List<String>,
|
||||
@SerialName("commitment") val commitment: String,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.key")
|
||||
data class VerificationKey(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
@SerialName("key") val key: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.mac")
|
||||
data class VerificationMac(
|
||||
@SerialName("content") val content: Content,
|
||||
@SerialName("sender") val sender: UserId,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
@SerialName("keys") val keys: String,
|
||||
@SerialName("mac") val mac: Map<String, String>,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@SerialName("m.key.verification.cancel")
|
||||
data class VerificationCancel(
|
||||
@SerialName("content") val content: Content,
|
||||
) : ApiToDeviceEvent(), ApiVerificationEvent {
|
||||
|
||||
@Serializable
|
||||
data class Content(
|
||||
@SerialName("code") val code: String,
|
||||
@SerialName("reason") val reason: String,
|
||||
@SerialName("transaction_id") val transactionId: String,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
object Ignored : ApiToDeviceEvent()
|
||||
|
||||
|
||||
sealed interface ApiVerificationEvent
|
||||
}
|
@ -0,0 +1,29 @@
|
||||
package app.dapk.st.matrix.sync.internal.request
|
||||
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.JsonDecoder
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
internal object EncryptedContentDeserializer : KSerializer<ApiEncryptedContent> {
|
||||
|
||||
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("encryptedContent")
|
||||
|
||||
override fun deserialize(decoder: Decoder): ApiEncryptedContent {
|
||||
require(decoder is JsonDecoder)
|
||||
val element = decoder.decodeJsonElement()
|
||||
return when (val algorithm = element.jsonObject["algorithm"]?.jsonPrimitive?.content) {
|
||||
"m.olm.v1.curve25519-aes-sha2" -> ApiEncryptedContent.OlmV1.serializer().deserialize(decoder)
|
||||
"m.megolm.v1.aes-sha2" -> ApiEncryptedContent.MegOlmV1.serializer().deserialize(decoder)
|
||||
null -> ApiEncryptedContent.Unknown
|
||||
else -> throw IllegalArgumentException("Unknown algorithm : $algorithm")
|
||||
}
|
||||
}
|
||||
|
||||
override fun serialize(encoder: Encoder, value: ApiEncryptedContent) = TODO("Not yet implemented")
|
||||
|
||||
}
|
@ -19,6 +19,7 @@ include ':features:notifications'
|
||||
include ':features:messenger'
|
||||
include ':features:navigator'
|
||||
include ':features:verification'
|
||||
include ':features:share-entry'
|
||||
|
||||
include ':domains:android:stub'
|
||||
include ':domains:android:core'
|
||||
|
@ -22,6 +22,7 @@ import test.MatrixTestScope
|
||||
import test.TestMatrix
|
||||
import test.flowTest
|
||||
import test.restoreLoginAndInitialSync
|
||||
import java.nio.file.Paths
|
||||
import java.util.*
|
||||
|
||||
private const val HTTPS_TEST_SERVER_URL = "https://localhost:8080/"
|
||||
@ -73,6 +74,14 @@ class SmokeTest {
|
||||
|
||||
@Test
|
||||
@Order(6)
|
||||
fun `can send and receive clear image messages`() = testAfterInitialSync { alice, bob ->
|
||||
val testImage = loadResourceFile("test-image.png")
|
||||
alice.sendImageMessage(SharedState.sharedRoom, testImage, isEncrypted = false)
|
||||
bob.expectImageMessage(SharedState.sharedRoom, testImage, SharedState.alice.roomMember)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Order(7)
|
||||
fun `can request and verify devices`() = testAfterInitialSync { alice, bob ->
|
||||
alice.client.cryptoService().verificationAction(Verification.Action.Request(bob.userId(), bob.deviceId()))
|
||||
alice.client.cryptoService().verificationState().automaticVerification(alice).expectAsync { it == Verification.State.Done }
|
||||
@ -85,7 +94,7 @@ class SmokeTest {
|
||||
fun `can import E2E room keys file`() = runTest {
|
||||
val ignoredUser = TestUser("ignored", RoomMember(UserId("ignored"), null, null), "ignored")
|
||||
val cryptoService = TestMatrix(ignoredUser, includeLogging = true).client.cryptoService()
|
||||
val stream = Thread.currentThread().contextClassLoader.getResourceAsStream("element-keys.txt")!!
|
||||
val stream = loadResourceStream("element-keys.txt")
|
||||
|
||||
val result = with(cryptoService) {
|
||||
stream.importRoomKeys(password = "aaaaaa")
|
||||
@ -182,4 +191,7 @@ private fun Flow<Verification.State>.automaticVerification(testMatrix: TestMatri
|
||||
// do nothing
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadResourceStream(name: String) = Thread.currentThread().contextClassLoader.getResourceAsStream(name)!!
|
||||
private fun loadResourceFile(name: String) = Paths.get(Thread.currentThread().contextClassLoader.getResource(name)!!.toURI()).toFile()
|
||||
|
@ -5,17 +5,31 @@ package test
|
||||
import TestMessage
|
||||
import TestUser
|
||||
import app.dapk.st.core.extensions.ifNull
|
||||
import app.dapk.st.matrix.common.MxUrl
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.common.convertMxUrToUrl
|
||||
import app.dapk.st.matrix.http.MatrixHttpClient
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.syncService
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.util.cio.*
|
||||
import io.ktor.utils.io.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.fail
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import java.io.File
|
||||
import java.math.BigInteger
|
||||
import java.net.URL
|
||||
import java.security.MessageDigest
|
||||
import java.util.*
|
||||
|
||||
fun flowTest(block: suspend MatrixTestScope.() -> Unit) {
|
||||
@ -131,6 +145,20 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
.assert(message)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.expectImageMessage(roomId: RoomId, image: File, author: RoomMember) {
|
||||
println("expecting ${image.absolutePath} from ${author.displayName}")
|
||||
this.client.syncService().room(roomId)
|
||||
.map {
|
||||
it.events.filterIsInstance<RoomEvent.Image>().map {
|
||||
println("found: ${it.imageMeta.url}")
|
||||
val output = File(image.parentFile.absolutePath, "output.png")
|
||||
HttpClient().request(it.imageMeta.url).bodyAsChannel().copyAndClose(output.writeChannel())
|
||||
output.readBytes().md5Hash() to it.author
|
||||
}.firstOrNull()
|
||||
}
|
||||
.assert(image.readBytes().md5Hash() to author)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.sendTextMessage(roomId: RoomId, content: String, isEncrypted: Boolean) {
|
||||
println("sending $content")
|
||||
this.client.messageService().scheduleMessage(
|
||||
@ -144,6 +172,21 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.sendImageMessage(roomId: RoomId, file: File, isEncrypted: Boolean) {
|
||||
println("sending ${file.name}")
|
||||
this.client.messageService().scheduleMessage(
|
||||
MessageService.Message.ImageMessage(
|
||||
content = MessageService.Message.Content.ApiImageContent(
|
||||
uri = file.absolutePath,
|
||||
),
|
||||
roomId = roomId,
|
||||
sendEncrypted = isEncrypted,
|
||||
localId = "local.${UUID.randomUUID()}",
|
||||
timestampUtc = System.currentTimeMillis(),
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun TestMatrix.loginWithInitialSync() {
|
||||
this.restoreLogin()
|
||||
client.syncService().startSyncing().collectAsync(testScope) {
|
||||
@ -162,5 +205,10 @@ class MatrixTestScope(private val testScope: TestScope) {
|
||||
suspend fun release() {
|
||||
inProgressInstances.forEach { it.release() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun ByteArray.md5Hash(): String {
|
||||
val md = MessageDigest.getInstance("MD5")
|
||||
val bigInt = BigInteger(1, md.digest(this))
|
||||
return String.format("%032x", bigInt)
|
||||
}
|
@ -22,6 +22,7 @@ import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
|
||||
import app.dapk.st.matrix.message.MessageEncrypter
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import app.dapk.st.matrix.message.installMessageService
|
||||
import app.dapk.st.matrix.message.internal.ImageContentReader
|
||||
import app.dapk.st.matrix.message.messageService
|
||||
import app.dapk.st.matrix.push.installPushService
|
||||
import app.dapk.st.matrix.room.RoomMessenger
|
||||
@ -41,7 +42,9 @@ import test.impl.InMemoryDatabase
|
||||
import test.impl.InMemoryPreferences
|
||||
import test.impl.InstantScheduler
|
||||
import test.impl.PrintingErrorTracking
|
||||
import java.io.File
|
||||
import java.time.Clock
|
||||
import javax.imageio.ImageIO
|
||||
|
||||
class TestMatrix(
|
||||
private val user: TestUser,
|
||||
@ -114,11 +117,12 @@ class TestMatrix(
|
||||
coroutineDispatchers = coroutineDispatchers,
|
||||
)
|
||||
|
||||
installMessageService(storeModule.localEchoStore, InstantScheduler(it)) { serviceProvider ->
|
||||
installMessageService(storeModule.localEchoStore, InstantScheduler(it), JavaImageContentReader()) { serviceProvider ->
|
||||
MessageEncrypter { message ->
|
||||
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) {
|
||||
@ -132,6 +136,8 @@ class TestMatrix(
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
is MessageService.Message.ImageMessage -> TODO()
|
||||
}
|
||||
)
|
||||
|
||||
@ -198,12 +204,14 @@ class TestMatrix(
|
||||
apiEvent.content.methods,
|
||||
apiEvent.content.timestampPosix,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationReady -> Verification.Event.Ready(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.methods,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationStart -> Verification.Event.Started(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -214,6 +222,7 @@ class TestMatrix(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationAccept -> Verification.Event.Accepted(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.fromDevice,
|
||||
@ -224,12 +233,14 @@ class TestMatrix(
|
||||
apiEvent.content.short,
|
||||
apiEvent.content.transactionId,
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationCancel -> TODO()
|
||||
is ApiToDeviceEvent.VerificationKey -> Verification.Event.Key(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
apiEvent.content.key
|
||||
)
|
||||
|
||||
is ApiToDeviceEvent.VerificationMac -> Verification.Event.Mac(
|
||||
apiEvent.sender,
|
||||
apiEvent.content.transactionId,
|
||||
@ -250,6 +261,7 @@ class TestMatrix(
|
||||
val roomService = services.roomService()
|
||||
object : RoomMembersService {
|
||||
override suspend fun find(roomId: RoomId, userIds: List<UserId>) = roomService.findMembers(roomId, userIds)
|
||||
override suspend fun findSummary(roomId: RoomId) = roomService.findMembersSummary(roomId)
|
||||
override suspend fun insert(roomId: RoomId, members: List<RoomMember>) = roomService.insertMembers(roomId, members)
|
||||
}
|
||||
},
|
||||
@ -285,6 +297,7 @@ class TestMatrix(
|
||||
|
||||
suspend fun deviceId() = storeModule.credentialsStore().credentials()!!.deviceId
|
||||
suspend fun userId() = storeModule.credentialsStore().credentials()!!.userId
|
||||
suspend fun credentials() = storeModule.credentialsStore().credentials()!!
|
||||
|
||||
suspend fun release() {
|
||||
coroutineDispatchers.global.waitForCancel()
|
||||
@ -313,4 +326,22 @@ class JavaBase64 : Base64 {
|
||||
override fun decode(input: String): ByteArray {
|
||||
return java.util.Base64.getDecoder().decode(input)
|
||||
}
|
||||
}
|
||||
|
||||
class JavaImageContentReader : ImageContentReader {
|
||||
|
||||
override fun read(uri: String): ImageContentReader.ImageContent {
|
||||
val file = File(uri)
|
||||
val size = file.length()
|
||||
val image = ImageIO.read(file)
|
||||
return ImageContentReader.ImageContent(
|
||||
height = image.height,
|
||||
width = image.width,
|
||||
size = size,
|
||||
mimeType = "image/${file.extension}",
|
||||
fileName = file.name,
|
||||
content = file.readBytes()
|
||||
)
|
||||
}
|
||||
|
||||
}
|
BIN
test-harness/src/test/resources/test-image.png
Normal file
BIN
test-harness/src/test/resources/test-image.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
Loading…
x
Reference in New Issue
Block a user