Merge pull request #200 from ouchadam/tech/chat-engine

Tech/chat engine
This commit is contained in:
Adam Brown 2022-10-12 21:51:12 +01:00 committed by GitHub
commit 3444285592
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
108 changed files with 1627 additions and 893 deletions

View File

@ -106,6 +106,9 @@ dependencies {
implementation project(":core") implementation project(":core")
implementation project(":chat-engine")
implementation project(":matrix-chat-engine")
implementation Dependencies.google.androidxComposeUi implementation Dependencies.google.androidxComposeUi
implementation Dependencies.mavenCentral.ktorAndroid implementation Dependencies.mavenCentral.ktorAndroid
implementation Dependencies.mavenCentral.sqldelightAndroid implementation Dependencies.mavenCentral.sqldelightAndroid

View File

@ -17,43 +17,29 @@ import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.extensions.unsafeLazy import app.dapk.st.core.extensions.unsafeLazy
import app.dapk.st.directory.DirectoryModule import app.dapk.st.directory.DirectoryModule
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.engine.MatrixEngine
import app.dapk.st.firebase.messaging.MessagingModule import app.dapk.st.firebase.messaging.MessagingModule
import app.dapk.st.home.HomeModule import app.dapk.st.home.HomeModule
import app.dapk.st.home.MainActivity import app.dapk.st.home.MainActivity
import app.dapk.st.imageloader.ImageLoaderModule import app.dapk.st.imageloader.ImageLoaderModule
import app.dapk.st.login.LoginModule import app.dapk.st.login.LoginModule
import app.dapk.st.matrix.MatrixClient
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
import app.dapk.st.matrix.auth.authService import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.auth.installAuthService import app.dapk.st.matrix.common.JsonString
import app.dapk.st.matrix.common.* import app.dapk.st.matrix.common.MatrixLogger
import app.dapk.st.matrix.crypto.RoomMembersProvider import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.crypto.Verification
import app.dapk.st.matrix.crypto.cryptoService
import app.dapk.st.matrix.crypto.installCryptoService
import app.dapk.st.matrix.device.deviceService
import app.dapk.st.matrix.device.installEncryptionService
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
import app.dapk.st.matrix.message.*
import app.dapk.st.matrix.message.internal.ImageContentReader import app.dapk.st.matrix.message.internal.ImageContentReader
import app.dapk.st.matrix.push.installPushService
import app.dapk.st.matrix.push.pushService
import app.dapk.st.matrix.room.*
import app.dapk.st.matrix.sync.*
import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerActivity
import app.dapk.st.messenger.MessengerModule import app.dapk.st.messenger.MessengerModule
import app.dapk.st.messenger.gallery.ImageGalleryModule import app.dapk.st.messenger.gallery.ImageGalleryModule
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.notifications.MatrixPushHandler
import app.dapk.st.notifications.NotificationsModule import app.dapk.st.notifications.NotificationsModule
import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmPersistenceWrapper import app.dapk.st.olm.OlmPersistenceWrapper
import app.dapk.st.olm.OlmWrapper
import app.dapk.st.profile.ProfileModule import app.dapk.st.profile.ProfileModule
import app.dapk.st.push.PushHandler
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
import app.dapk.st.push.PushTokenPayload
import app.dapk.st.push.messaging.MessagingServiceAdapter import app.dapk.st.push.messaging.MessagingServiceAdapter
import app.dapk.st.settings.SettingsModule import app.dapk.st.settings.SettingsModule
import app.dapk.st.share.ShareEntryModule import app.dapk.st.share.ShareEntryModule
@ -62,8 +48,8 @@ import app.dapk.st.work.TaskRunnerModule
import app.dapk.st.work.WorkModule import app.dapk.st.work.WorkModule
import com.squareup.sqldelight.android.AndroidSqliteDriver import com.squareup.sqldelight.android.AndroidSqliteDriver
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.serialization.json.Json
import java.io.InputStream import java.io.InputStream
import java.time.Clock
internal class AppModule(context: Application, logger: MatrixLogger) { internal class AppModule(context: Application, logger: MatrixLogger) {
@ -77,9 +63,8 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db") private val driver = AndroidSqliteDriver(DapkDb.Schema, context, "dapk.db")
private val database = DapkDb(driver) private val database = DapkDb(driver)
private val clock = Clock.systemUTC()
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO) val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
val base64 = AndroidBase64() private val base64 = AndroidBase64()
val storeModule = unsafeLazy { val storeModule = unsafeLazy {
StoreModule( StoreModule(
@ -95,9 +80,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
private val imageLoaderModule = ImageLoaderModule(context) private val imageLoaderModule = ImageLoaderModule(context)
private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) } private val imageContentReader by unsafeLazy { AndroidImageContentReader(context.contentResolver) }
private val matrixModules = MatrixModules(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta) private val chatEngineModule =
ChatEngineModule(storeModule, trackingModule, workModule, logger, coroutineDispatchers, imageContentReader, base64, buildMeta)
val domainModules = DomainModules(matrixModules, trackingModule.errorTracker, workModule, storeModule, context, coroutineDispatchers) val domainModules = DomainModules(chatEngineModule, trackingModule.errorTracker, context, coroutineDispatchers)
val coreAndroidModule = CoreAndroidModule( val coreAndroidModule = CoreAndroidModule(
intentFactory = object : IntentFactory { intentFactory = object : IntentFactory {
@ -131,76 +117,57 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
val featureModules = FeatureModules( val featureModules = FeatureModules(
storeModule, storeModule,
matrixModules, chatEngineModule,
domainModules, domainModules,
trackingModule, trackingModule,
coreAndroidModule, coreAndroidModule,
imageLoaderModule, imageLoaderModule,
imageContentReader,
context, context,
buildMeta, buildMeta,
deviceMeta, deviceMeta,
coroutineDispatchers, coroutineDispatchers,
clock,
base64,
) )
} }
internal class FeatureModules internal constructor( internal class FeatureModules internal constructor(
private val storeModule: Lazy<StoreModule>, private val storeModule: Lazy<StoreModule>,
private val matrixModules: MatrixModules, private val chatEngineModule: ChatEngineModule,
private val domainModules: DomainModules, private val domainModules: DomainModules,
private val trackingModule: TrackingModule, private val trackingModule: TrackingModule,
private val coreAndroidModule: CoreAndroidModule, private val coreAndroidModule: CoreAndroidModule,
imageLoaderModule: ImageLoaderModule, imageLoaderModule: ImageLoaderModule,
imageContentReader: ImageContentReader,
context: Context, context: Context,
buildMeta: BuildMeta, buildMeta: BuildMeta,
deviceMeta: DeviceMeta, deviceMeta: DeviceMeta,
coroutineDispatchers: CoroutineDispatchers, coroutineDispatchers: CoroutineDispatchers,
clock: Clock,
base64: Base64,
) { ) {
val directoryModule by unsafeLazy { val directoryModule by unsafeLazy {
DirectoryModule( DirectoryModule(
syncService = matrixModules.sync,
messageService = matrixModules.message,
context = context, context = context,
credentialsStore = storeModule.value.credentialsStore(), chatEngine = chatEngineModule.engine,
roomStore = storeModule.value.roomStore(),
roomService = matrixModules.room,
) )
} }
val loginModule by unsafeLazy { val loginModule by unsafeLazy {
LoginModule( LoginModule(
matrixModules.auth, chatEngineModule.engine,
domainModules.pushModule, domainModules.pushModule,
matrixModules.profile,
trackingModule.errorTracker trackingModule.errorTracker
) )
} }
val messengerModule by unsafeLazy { val messengerModule by unsafeLazy {
MessengerModule( MessengerModule(
matrixModules.sync, chatEngineModule.engine,
matrixModules.message,
matrixModules.room,
storeModule.value.credentialsStore(),
storeModule.value.roomStore(),
clock,
context, context,
base64,
imageContentReader,
storeModule.value.messageStore(), storeModule.value.messageStore(),
) )
} }
val homeModule by unsafeLazy { HomeModule(storeModule.value, matrixModules.profile, matrixModules.sync, buildMeta) } val homeModule by unsafeLazy { HomeModule(chatEngineModule.engine, storeModule.value, buildMeta) }
val settingsModule by unsafeLazy { val settingsModule by unsafeLazy {
SettingsModule( SettingsModule(
chatEngineModule.engine,
storeModule.value, storeModule.value,
pushModule, pushModule,
matrixModules.crypto,
matrixModules.sync,
context.contentResolver, context.contentResolver,
buildMeta, buildMeta,
deviceMeta, deviceMeta,
@ -210,12 +177,11 @@ internal class FeatureModules internal constructor(
storeModule.value.messageStore(), storeModule.value.messageStore(),
) )
} }
val profileModule by unsafeLazy { ProfileModule(matrixModules.profile, matrixModules.sync, matrixModules.room, trackingModule.errorTracker) } val profileModule by unsafeLazy { ProfileModule(chatEngineModule.engine, trackingModule.errorTracker) }
val notificationsModule by unsafeLazy { val notificationsModule by unsafeLazy {
NotificationsModule( NotificationsModule(
chatEngineModule.engine,
imageLoaderModule.iconLoader(), imageLoaderModule.iconLoader(),
storeModule.value.roomStore(),
storeModule.value.overviewStore(),
context, context,
intentFactory = coreAndroidModule.intentFactory(), intentFactory = coreAndroidModule.intentFactory(),
dispatchers = coroutineDispatchers, dispatchers = coroutineDispatchers,
@ -224,7 +190,7 @@ internal class FeatureModules internal constructor(
} }
val shareEntryModule by unsafeLazy { val shareEntryModule by unsafeLazy {
ShareEntryModule(matrixModules.sync, matrixModules.room) ShareEntryModule(chatEngineModule.engine)
} }
val imageGalleryModule by unsafeLazy { val imageGalleryModule by unsafeLazy {
@ -241,7 +207,7 @@ internal class FeatureModules internal constructor(
} }
internal class MatrixModules( internal class ChatEngineModule(
private val storeModule: Lazy<StoreModule>, private val storeModule: Lazy<StoreModule>,
private val trackingModule: TrackingModule, private val trackingModule: TrackingModule,
private val workModule: WorkModule, private val workModule: WorkModule,
@ -252,232 +218,48 @@ internal class MatrixModules(
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) { ) {
val matrix by unsafeLazy { val engine by unsafeLazy {
val store = storeModule.value val store = storeModule.value
val credentialsStore = store.credentialsStore() MatrixEngine.Factory().create(
MatrixClient( base64,
KtorMatrixHttpClientFactory( buildMeta,
credentialsStore, logger,
includeLogging = buildMeta.isDebug, SmallTalkDeviceNameGenerator(),
), coroutineDispatchers,
logger trackingModule.errorTracker,
).also { imageContentReader,
it.install { BackgroundWorkAdapter(workModule.workScheduler()),
installAuthService(credentialsStore, SmallTalkDeviceNameGenerator()) store.memberStore(),
installEncryptionService(store.knownDevicesStore()) store.roomStore(),
store.profileStore(),
val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64) store.syncStore(),
val singletonFlows = SingletonFlows(coroutineDispatchers) store.overviewStore(),
val olm = OlmWrapper( store.filterStore(),
olmStore = olmAccountStore, store.localEchoStore,
singletonFlows = singletonFlows, store.credentialsStore(),
jsonCanonicalizer = JsonCanonicalizer(), store.knownDevicesStore(),
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()), OlmPersistenceWrapper(store.olmStore(), base64),
errorTracker = trackingModule.errorTracker, )
logger = logger,
clock = Clock.systemUTC(),
coroutineDispatchers = coroutineDispatchers,
)
installCryptoService(
credentialsStore,
olm,
roomMembersProvider = { services ->
RoomMembersProvider {
services.roomService().joinedMembers(it).map { it.userId }
}
},
base64 = base64,
coroutineDispatchers = coroutineDispatchers,
)
installMessageService(
store.localEchoStore,
BackgroundWorkAdapter(workModule.workScheduler()),
imageContentReader,
messageEncrypter = {
val cryptoService = it.cryptoService()
MessageEncrypter { message ->
val result = cryptoService.encrypt(
roomId = message.roomId,
credentials = credentialsStore.credentials()!!,
messageJson = message.contents,
)
MessageEncrypter.EncryptedMessagePayload(
result.algorithmName,
result.senderKey,
result.cipherText,
result.sessionId,
result.deviceId,
)
}
},
mediaEncrypter = {
val cryptoService = it.cryptoService()
MediaEncrypter { input ->
val result = cryptoService.encrypt(input)
MediaEncrypter.Result(
uri = result.uri,
contentLength = result.contentLength,
algorithm = result.algorithm,
ext = result.ext,
keyOperations = result.keyOperations,
kty = result.kty,
k = result.k,
iv = result.iv,
hashes = result.hashes,
v = result.v,
)
}
},
)
val overviewStore = store.overviewStore()
installRoomService(
storeModule.value.memberStore(),
roomMessenger = {
val messageService = it.messageService()
object : RoomMessenger {
override suspend fun enableEncryption(roomId: RoomId) {
messageService.sendEventMessage(
roomId, MessageService.EventMessage.Encryption(
algorithm = AlgorithmName("m.megolm.v1.aes-sha2")
)
)
}
}
},
roomInviteRemover = {
overviewStore.removeInvites(listOf(it))
}
)
installProfileService(storeModule.value.profileStore(), singletonFlows, credentialsStore)
installSyncService(
credentialsStore,
overviewStore,
store.roomStore(),
store.syncStore(),
store.filterStore(),
deviceNotifier = { services ->
val encryption = services.deviceService()
val crypto = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryption.updateStaleDevices(userIds)
crypto.updateOlmSession(userIds, syncToken)
}
},
messageDecrypter = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
MessageDecrypter {
cryptoService.decrypt(it)
}
},
keySharer = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
KeySharer { sharedRoomKeys ->
cryptoService.importRoomKeys(sharedRoomKeys)
}
},
verificationHandler = { services ->
val cryptoService = services.cryptoService()
VerificationHandler { apiEvent ->
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
cryptoService.onVerificationEvent(
when (apiEvent) {
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
apiEvent.sender,
apiEvent.content.fromDevice,
apiEvent.content.transactionId,
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,
apiEvent.content.method,
apiEvent.content.protocols,
apiEvent.content.hashes,
apiEvent.content.codes,
apiEvent.content.short,
apiEvent.content.transactionId,
)
is ApiToDeviceEvent.VerificationCancel -> TODO()
is ApiToDeviceEvent.VerificationAccept -> 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,
apiEvent.content.keys,
apiEvent.content.mac,
)
}
)
}
},
oneTimeKeyProducer = { services ->
val cryptoService = services.cryptoService()
MaybeCreateMoreKeys {
cryptoService.maybeCreateMoreKeys(it)
}
},
roomMembersService = { services ->
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)
}
},
errorTracker = trackingModule.errorTracker,
coroutineDispatchers = coroutineDispatchers,
)
installPushService(credentialsStore)
}
}
} }
val auth by unsafeLazy { matrix.authService() }
val push by unsafeLazy { matrix.pushService() }
val sync by unsafeLazy { matrix.syncService() }
val message by unsafeLazy { matrix.messageService() }
val room by unsafeLazy { matrix.roomService() }
val profile by unsafeLazy { matrix.profileService() }
val crypto by unsafeLazy { matrix.cryptoService() }
} }
internal class DomainModules( internal class DomainModules(
private val matrixModules: MatrixModules, private val chatEngineModule: ChatEngineModule,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
private val workModule: WorkModule,
private val storeModule: Lazy<StoreModule>,
private val context: Application, private val context: Application,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
) { ) {
val pushHandler by unsafeLazy { private val pushHandler by unsafeLazy {
val store = storeModule.value val enginePushHandler = chatEngineModule.engine.pushHandler()
MatrixPushHandler( object : PushHandler {
workScheduler = workModule.workScheduler(), override fun onNewToken(payload: PushTokenPayload) {
credentialsStore = store.credentialsStore(), enginePushHandler.onNewToken(JsonString(Json.encodeToString(PushTokenPayload.serializer(), payload)))
matrixModules.sync, }
store.roomStore(),
) override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) = enginePushHandler.onMessageReceived(eventId, roomId)
}
} }
val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) } val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) }
@ -492,7 +274,9 @@ internal class DomainModules(
messaging.messaging, messaging.messaging,
) )
} }
val taskRunnerModule by unsafeLazy { TaskRunnerModule(TaskRunnerAdapter(matrixModules.matrix::run, AppTaskRunner(matrixModules.push))) } val taskRunnerModule by unsafeLazy {
TaskRunnerModule(TaskRunnerAdapter(chatEngineModule.engine, AppTaskRunner(chatEngineModule.engine)))
}
} }

View File

@ -1,13 +1,13 @@
package app.dapk.st.graph package app.dapk.st.graph
import app.dapk.st.matrix.push.PushService import app.dapk.st.engine.ChatEngine
import app.dapk.st.push.PushTokenPayload import app.dapk.st.push.PushTokenPayload
import app.dapk.st.work.TaskRunner import app.dapk.st.work.TaskRunner
import io.ktor.client.plugins.* import io.ktor.client.plugins.*
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
class AppTaskRunner( class AppTaskRunner(
private val pushService: PushService, private val chatEngine: ChatEngine,
) { ) {
suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult { suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
@ -15,7 +15,7 @@ class AppTaskRunner(
"push_token" -> { "push_token" -> {
runCatching { runCatching {
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload) val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
pushService.registerPush(payload.token, payload.gatewayUrl) chatEngine.registerPushToken(payload.token, payload.gatewayUrl)
}.fold( }.fold(
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) }, onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
onFailure = { onFailure = {

View File

@ -9,7 +9,7 @@ class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : Backgrou
WorkScheduler.WorkTask( WorkScheduler.WorkTask(
jobId = 1, jobId = 1,
type = task.type, type = task.type,
jsonPayload = task.jsonPayload, jsonPayload = task.jsonPayload.value,
) )
) )
} }

View File

@ -1,10 +1,11 @@
package app.dapk.st.graph package app.dapk.st.graph
import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.engine.ChatEngine
import app.dapk.st.engine.ChatEngineTask
import app.dapk.st.work.TaskRunner import app.dapk.st.work.TaskRunner
class TaskRunnerAdapter( class TaskRunnerAdapter(
private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult, private val chatEngine: ChatEngine,
private val appTaskRunner: AppTaskRunner, private val appTaskRunner: AppTaskRunner,
) : TaskRunner { ) : TaskRunner {
@ -12,11 +13,12 @@ class TaskRunnerAdapter(
return tasks.map { return tasks.map {
when { when {
it.task.type.startsWith("matrix") -> { it.task.type.startsWith("matrix") -> {
when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) { when (val result = chatEngine.runTask(ChatEngineTask(it.task.type, it.task.jsonPayload))) {
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry) is app.dapk.st.engine.TaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source) app.dapk.st.engine.TaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
} }
} }
else -> appTaskRunner.run(it) else -> appTaskRunner.run(it)
} }
} }

13
chat-engine/build.gradle Normal file
View File

@ -0,0 +1,13 @@
plugins {
id 'kotlin'
id 'java-test-fixtures'
}
dependencies {
api Dependencies.mavenCentral.kotlinCoroutinesCore
api project(":matrix:common")
kotlinFixtures(it)
testFixturesImplementation(testFixtures(project(":matrix:common")))
testFixturesImplementation(testFixtures(project(":core")))
}

View File

@ -0,0 +1,82 @@
package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.JsonString
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember
import kotlinx.coroutines.flow.Flow
import java.io.InputStream
interface ChatEngine : TaskRunner {
fun directory(): Flow<DirectoryState>
fun invites(): Flow<InviteState>
fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState>
fun notificationsMessages(): Flow<UnreadNotifications>
fun notificationsInvites(): Flow<InviteNotification>
suspend fun login(request: LoginRequest): LoginResult
suspend fun me(forceRefresh: Boolean): Me
suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult>
suspend fun send(message: SendMessage, room: RoomOverview)
suspend fun registerPushToken(token: String, gatewayUrl: String)
suspend fun joinRoom(roomId: RoomId)
suspend fun rejectJoinRoom(roomId: RoomId)
suspend fun findMembersSummary(roomId: RoomId): List<RoomMember>
fun mediaDecrypter(): MediaDecrypter
fun pushHandler(): PushHandler
}
interface TaskRunner {
suspend fun runTask(task: ChatEngineTask): TaskResult
sealed interface TaskResult {
object Success : TaskResult
data class Failure(val canRetry: Boolean) : TaskResult
}
}
data class ChatEngineTask(val type: String, val jsonPayload: String)
interface MediaDecrypter {
fun decrypt(input: InputStream, k: String, iv: String): Collector
fun interface Collector {
fun collect(partial: (ByteArray) -> Unit)
}
}
interface PushHandler {
fun onNewToken(payload: JsonString)
fun onMessageReceived(eventId: EventId?, roomId: RoomId?)
}
typealias UnreadNotifications = Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>
data class NotificationDiff(
val unchanged: Map<RoomId, List<EventId>>,
val changedOrNew: Map<RoomId, List<EventId>>,
val removed: Map<RoomId, List<EventId>>,
val newRooms: Set<RoomId>
)
data class InviteNotification(
val content: String,
val roomId: RoomId
)

View File

@ -0,0 +1,221 @@
package app.dapk.st.engine
import app.dapk.st.matrix.common.*
import java.time.Instant
import java.time.ZoneId
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
typealias DirectoryState = List<DirectoryItem>
typealias OverviewState = List<RoomOverview>
typealias InviteState = List<RoomInvite>
data class DirectoryItem(
val overview: RoomOverview,
val unreadCount: UnreadCount,
val typing: Typing?
)
data class RoomOverview(
val roomId: RoomId,
val roomCreationUtc: Long,
val roomName: String?,
val roomAvatarUrl: AvatarUrl?,
val lastMessage: LastMessage?,
val isGroup: Boolean,
val readMarker: EventId?,
val isEncrypted: Boolean,
) {
data class LastMessage(
val content: String,
val utcTimestamp: Long,
val author: RoomMember,
)
}
data class RoomInvite(
val from: RoomMember,
val roomId: RoomId,
val inviteMeta: InviteMeta,
) {
sealed class InviteMeta {
object DirectMessage : InviteMeta()
data class Room(val roomName: String? = null) : InviteMeta()
}
}
@JvmInline
value class UnreadCount(val value: Int)
data class Typing(val roomId: RoomId, val members: List<RoomMember>)
data class LoginRequest(val userName: String, val password: String, val serverUrl: String?)
sealed interface LoginResult {
data class Success(val userCredentials: UserCredentials) : LoginResult
object MissingWellKnown : LoginResult
data class Error(val cause: Throwable) : LoginResult
}
data class Me(
val userId: UserId,
val displayName: String?,
val avatarUrl: AvatarUrl?,
val homeServerUrl: HomeServerUrl,
)
sealed interface ImportResult {
data class Success(val roomIds: Set<RoomId>, val totalImportedKeysCount: Long) : ImportResult
data class Error(val cause: Type) : ImportResult {
sealed interface Type {
data class Unknown(val cause: Throwable) : Type
object NoKeysFound : Type
object UnexpectedDecryptionOutput : Type
object UnableToOpenFile : Type
object InvalidFile : Type
}
}
data class Update(val importedKeysCount: Long) : ImportResult
}
data class MessengerState(
val self: UserId,
val roomState: RoomState,
val typing: Typing?
)
data class RoomState(
val roomOverview: RoomOverview,
val events: List<RoomEvent>,
)
internal val DEFAULT_ZONE = ZoneId.systemDefault()
internal val MESSAGE_TIME_FORMAT = DateTimeFormatter.ofPattern("HH:mm")
sealed class RoomEvent {
abstract val eventId: EventId
abstract val utcTimestamp: Long
abstract val author: RoomMember
abstract val meta: MessageMeta
data class Message(
override val eventId: EventId,
override val utcTimestamp: Long,
val content: String,
override val author: RoomMember,
override val meta: MessageMeta,
val edited: Boolean = false,
val redacted: Boolean = false,
) : RoomEvent() {
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
}
}
data class Reply(
val message: RoomEvent,
val replyingTo: RoomEvent,
) : RoomEvent() {
override val eventId: EventId = message.eventId
override val utcTimestamp: Long = message.utcTimestamp
override val author: RoomMember = message.author
override val meta: MessageMeta = message.meta
val replyingToSelf = replyingTo.author == message.author
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
}
}
data class Image(
override val eventId: EventId,
override val utcTimestamp: Long,
val imageMeta: ImageMeta,
override val author: RoomMember,
override val meta: MessageMeta,
val edited: Boolean = false,
) : RoomEvent() {
val time: String by lazy(mode = LazyThreadSafetyMode.NONE) {
val instant = Instant.ofEpochMilli(utcTimestamp)
ZonedDateTime.ofInstant(instant, DEFAULT_ZONE).toLocalTime().format(MESSAGE_TIME_FORMAT)
}
data class ImageMeta(
val width: Int?,
val height: Int?,
val url: String,
val keys: Keys?,
) {
data class Keys(
val k: String,
val iv: String,
val v: String,
val hashes: Map<String, String>,
)
}
}
}
sealed class MessageMeta {
object FromServer : MessageMeta()
data class LocalEcho(
val echoId: String,
val state: State
) : MessageMeta() {
sealed class State {
object Sending : State()
object Sent : State()
data class Error(
val message: String,
val type: Type,
) : State() {
enum class Type {
UNKNOWN
}
}
}
}
}
sealed interface SendMessage {
data class TextMessage(
val content: String,
val reply: Reply? = null,
) : SendMessage {
data class Reply(
val author: RoomMember,
val originalMessage: String,
val eventId: EventId,
val timestampUtc: Long,
)
}
data class ImageMessage(
val uri: String,
) : SendMessage
}

View File

@ -0,0 +1,24 @@
package fake
import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import test.delegateEmit
import test.delegateReturn
import java.io.InputStream
class FakeChatEngine : ChatEngine by mockk() {
fun givenMessages(roomId: RoomId, disableReadReceipts: Boolean) = every { messages(roomId, disableReadReceipts) }.delegateReturn()
fun givenDirectory() = every { directory() }.delegateReturn()
fun givenImportKeys(inputStream: InputStream, passphrase: String) = coEvery { inputStream.importRoomKeys(passphrase) }.delegateReturn()
fun givenNotificationsInvites() = every { notificationsInvites() }.delegateEmit()
fun givenNotificationsMessages() = every { notificationsMessages() }.delegateEmit()
}

View File

@ -0,0 +1,66 @@
package fixture
import app.dapk.st.engine.*
import app.dapk.st.matrix.common.*
fun aMessengerState(
self: UserId = aUserId(),
roomState: RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)
fun aRoomOverview(
roomId: RoomId = aRoomId(),
roomCreationUtc: Long = 0L,
roomName: String? = null,
roomAvatarUrl: AvatarUrl? = null,
lastMessage: RoomOverview.LastMessage? = null,
isGroup: Boolean = false,
readMarker: EventId? = null,
isEncrypted: Boolean = false,
) = RoomOverview(roomId, roomCreationUtc, roomName, roomAvatarUrl, lastMessage, isGroup, readMarker, isEncrypted)
fun anEncryptedRoomMessageEvent(
eventId: EventId = anEventId(),
utcTimestamp: Long = 0L,
content: String = "encrypted-content",
author: RoomMember = aRoomMember(),
meta: MessageMeta = MessageMeta.FromServer,
edited: Boolean = false,
redacted: Boolean = false,
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited, redacted)
fun aRoomImageMessageEvent(
eventId: EventId = anEventId(),
utcTimestamp: Long = 0L,
content: RoomEvent.Image.ImageMeta = anImageMeta(),
author: RoomMember = aRoomMember(),
meta: MessageMeta = MessageMeta.FromServer,
edited: Boolean = false,
) = RoomEvent.Image(eventId, utcTimestamp, content, author, meta, edited)
fun aRoomReplyMessageEvent(
message: RoomEvent = aRoomMessageEvent(),
replyingTo: RoomEvent = aRoomMessageEvent(eventId = anEventId("in-reply-to-id")),
) = RoomEvent.Reply(message, replyingTo)
fun aRoomMessageEvent(
eventId: EventId = anEventId(),
utcTimestamp: Long = 0L,
content: String = "message-content",
author: RoomMember = aRoomMember(),
meta: MessageMeta = MessageMeta.FromServer,
edited: Boolean = false,
) = RoomEvent.Message(eventId, utcTimestamp, content, author, meta, edited)
fun anImageMeta(
width: Int? = 100,
height: Int? = 100,
url: String = "https://a-url.com",
keys: RoomEvent.Image.ImageMeta.Keys? = null
) = RoomEvent.Image.ImageMeta(width, height, url, keys)
fun aRoomState(
roomOverview: RoomOverview = aRoomOverview(),
events: List<RoomEvent> = listOf(aRoomMessageEvent()),
) = RoomState(roomOverview, events)

View File

@ -2,7 +2,7 @@ package fixture
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.notifications.NotificationDiff import app.dapk.st.engine.NotificationDiff
object NotificationDiffFixtures { object NotificationDiffFixtures {

View File

@ -1,6 +1,8 @@
package app.dapk.st.core.extensions package app.dapk.st.core.extensions
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.takeWhile
suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? { suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolean): T? {
var counter = 0 var counter = 0
@ -18,5 +20,3 @@ suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolea
return result return result
} }
fun <T> Flow<T>.startAndIgnoreEmissions(): Flow<Boolean> = this.map { false }.onStart { emit(true) }.filter { it }

View File

@ -1,3 +1,5 @@
package test
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow

View File

@ -5,7 +5,6 @@ dependencies {
implementation project(':core') implementation project(':core')
implementation project(':domains:android:core') implementation project(':domains:android:core')
implementation project(':domains:store') implementation project(':domains:store')
implementation project(':matrix:services:push')
firebase(it, "messaging") firebase(it, "messaging")

View File

@ -3,6 +3,7 @@ import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import org.amshove.kluent.internal.assertEquals import org.amshove.kluent.internal.assertEquals
import test.ExpectTestScope import test.ExpectTestScope
import test.FlowTestObserver
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal class ViewModelTestScopeImpl( internal class ViewModelTestScopeImpl(

View File

@ -1,9 +1,7 @@
applyAndroidComposeLibraryModule(project) applyAndroidComposeLibraryModule(project)
dependencies { dependencies {
implementation project(":matrix:services:sync") implementation project(":chat-engine")
implementation project(":matrix:services:message")
implementation project(":matrix:services:room")
implementation project(":domains:android:compose-core") implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel") implementation project(":domains:android:viewmodel")
implementation project(":features:messenger") implementation project(":features:messenger")
@ -13,11 +11,10 @@ dependencies {
kotlinTest(it) kotlinTest(it)
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))
} }

View File

@ -38,9 +38,10 @@ import app.dapk.st.design.components.Toolbar
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
import app.dapk.st.directory.DirectoryScreenState.Content import app.dapk.st.directory.DirectoryScreenState.Content
import app.dapk.st.directory.DirectoryScreenState.EmptyLoading import app.dapk.st.directory.DirectoryScreenState.EmptyLoading
import app.dapk.st.engine.DirectoryItem
import app.dapk.st.engine.RoomOverview
import app.dapk.st.engine.Typing
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerActivity
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.time.Clock import java.time.Clock
@ -147,7 +148,7 @@ private fun Content(listState: LazyListState, state: Content) {
} }
@Composable @Composable
private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock) { private fun DirectoryItem(room: DirectoryItem, onClick: (RoomId) -> Unit, clock: Clock) {
val overview = room.overview val overview = room.overview
val roomName = overview.roomName ?: "Empty room" val roomName = overview.roomName ?: "Empty room"
val hasUnread = room.unreadCount.value > 0 val hasUnread = room.unreadCount.value > 0
@ -233,7 +234,7 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock
} }
@Composable @Composable
private fun body(overview: RoomOverview, secondaryText: Color, typing: SyncService.SyncEvent.Typing?) { private fun body(overview: RoomOverview, secondaryText: Color, typing: Typing?) {
val bodySize = 14.sp val bodySize = 14.sp
when { when {

View File

@ -2,31 +2,17 @@ package app.dapk.st.directory
import android.content.Context import android.content.Context
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.matrix.sync.SyncService
class DirectoryModule( class DirectoryModule(
private val syncService: SyncService,
private val messageService: MessageService,
private val roomService: RoomService,
private val context: Context, private val context: Context,
private val credentialsStore: CredentialsStore, private val chatEngine: ChatEngine,
private val roomStore: RoomStore,
) : ProvidableModule { ) : ProvidableModule {
fun directoryViewModel(): DirectoryViewModel { fun directoryViewModel(): DirectoryViewModel {
return DirectoryViewModel( return DirectoryViewModel(
ShortcutHandler(context), ShortcutHandler(context),
DirectoryUseCase( chatEngine,
syncService,
messageService,
roomService,
credentialsStore,
roomStore,
)
) )
} }
} }

View File

@ -1,5 +1,7 @@
package app.dapk.st.directory package app.dapk.st.directory
import app.dapk.st.engine.DirectoryState
sealed interface DirectoryScreenState { sealed interface DirectoryScreenState {
object EmptyLoading : DirectoryScreenState object EmptyLoading : DirectoryScreenState

View File

@ -2,6 +2,7 @@ package app.dapk.st.directory
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.dapk.st.directory.DirectoryScreenState.* import app.dapk.st.directory.DirectoryScreenState.*
import app.dapk.st.engine.ChatEngine
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory import app.dapk.st.viewmodel.defaultStateFactory
@ -12,7 +13,7 @@ import kotlinx.coroutines.launch
class DirectoryViewModel( class DirectoryViewModel(
private val shortcutHandler: ShortcutHandler, private val shortcutHandler: ShortcutHandler,
private val directoryUseCase: DirectoryUseCase, private val chatEngine: ChatEngine,
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(), factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>( ) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
initialState = EmptyLoading, initialState = EmptyLoading,
@ -23,7 +24,7 @@ class DirectoryViewModel(
fun start() { fun start() {
syncJob = viewModelScope.launch { syncJob = viewModelScope.launch {
directoryUseCase.state().onEach { chatEngine.directory().onEach {
shortcutHandler.onDirectoryUpdate(it.map { it.overview }) shortcutHandler.onDirectoryUpdate(it.map { it.overview })
state = when (it.isEmpty()) { state = when (it.isEmpty()) {
true -> Empty true -> Empty

View File

@ -5,8 +5,8 @@ import android.content.pm.ShortcutInfo
import androidx.core.app.Person import androidx.core.app.Person
import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.content.pm.ShortcutManagerCompat
import app.dapk.st.engine.RoomOverview
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.messenger.MessengerActivity import app.dapk.st.messenger.MessengerActivity
class ShortcutHandler(private val context: Context) { class ShortcutHandler(private val context: Context) {

View File

@ -1,25 +1,26 @@
package app.dapk.st.directory package app.dapk.st.directory
import ViewModelTest import ViewModelTest
import app.dapk.st.engine.DirectoryItem
import app.dapk.st.engine.UnreadCount
import fake.FakeChatEngine
import fixture.aRoomOverview import fixture.aRoomOverview
import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.Test import org.junit.Test
import test.delegateReturn
private val AN_OVERVIEW = aRoomOverview() private val AN_OVERVIEW = aRoomOverview()
private val AN_OVERVIEW_STATE = RoomFoo(AN_OVERVIEW, UnreadCount(1), null) private val AN_OVERVIEW_STATE = DirectoryItem(AN_OVERVIEW, UnreadCount(1), null)
class DirectoryViewModelTest { class DirectoryViewModelTest {
private val runViewModelTest = ViewModelTest() private val runViewModelTest = ViewModelTest()
private val fakeDirectoryUseCase = FakeDirectoryUseCase()
private val fakeShortcutHandler = FakeShortcutHandler() private val fakeShortcutHandler = FakeShortcutHandler()
private val fakeChatEngine = FakeChatEngine()
private val viewModel = DirectoryViewModel( private val viewModel = DirectoryViewModel(
fakeShortcutHandler.instance, fakeShortcutHandler.instance,
fakeDirectoryUseCase.instance, fakeChatEngine,
runViewModelTest.testMutableStateFactory(), runViewModelTest.testMutableStateFactory(),
) )
@ -33,7 +34,7 @@ class DirectoryViewModelTest {
@Test @Test
fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest { fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest {
fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) } fakeShortcutHandler.instance.expectUnit { it.onDirectoryUpdate(listOf(AN_OVERVIEW)) }
fakeDirectoryUseCase.given().returns(flowOf(listOf(AN_OVERVIEW_STATE))) fakeChatEngine.givenDirectory().returns(flowOf(listOf(AN_OVERVIEW_STATE)))
viewModel.test().start() viewModel.test().start()
@ -44,9 +45,4 @@ class DirectoryViewModelTest {
class FakeShortcutHandler { class FakeShortcutHandler {
val instance = mockk<ShortcutHandler>() val instance = mockk<ShortcutHandler>()
}
class FakeDirectoryUseCase {
val instance = mockk<DirectoryUseCase>()
fun given() = every { instance.state() }.delegateReturn()
} }

View File

@ -1,9 +1,7 @@
applyAndroidComposeLibraryModule(project) applyAndroidComposeLibraryModule(project)
dependencies { dependencies {
implementation project(":matrix:services:profile") implementation project(":chat-engine")
implementation project(":matrix:services:crypto")
implementation project(":matrix:services:sync")
implementation project(":features:directory") implementation project(":features:directory")
implementation project(":features:login") implementation project(":features:login")
implementation project(":features:settings") implementation project(":features:settings")

View File

@ -4,31 +4,28 @@ import app.dapk.st.core.BuildMeta
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.directory.DirectoryViewModel import app.dapk.st.directory.DirectoryViewModel
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.engine.ChatEngine
import app.dapk.st.login.LoginViewModel import app.dapk.st.login.LoginViewModel
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.profile.ProfileViewModel import app.dapk.st.profile.ProfileViewModel
class HomeModule( class HomeModule(
private val chatEngine: ChatEngine,
private val storeModule: StoreModule, private val storeModule: StoreModule,
private val profileService: ProfileService,
private val syncService: SyncService,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
) : ProvidableModule { ) : ProvidableModule {
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel { fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
return HomeViewModel( return HomeViewModel(
chatEngine,
storeModule.credentialsStore(), storeModule.credentialsStore(),
directory, directory,
login, login,
profileViewModel, profileViewModel,
profileService,
storeModule.cacheCleaner(), storeModule.cacheCleaner(),
BetaVersionUpgradeUseCase( BetaVersionUpgradeUseCase(
storeModule.applicationStore(), storeModule.applicationStore(),
buildMeta, buildMeta,
), ),
syncService,
) )
} }

View File

@ -4,13 +4,13 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu import androidx.compose.material.icons.filled.Menu
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import app.dapk.st.matrix.room.ProfileService import app.dapk.st.engine.Me
sealed interface HomeScreenState { sealed interface HomeScreenState {
object Loading : HomeScreenState object Loading : HomeScreenState
object SignedOut : HomeScreenState object SignedOut : HomeScreenState
data class SignedIn(val page: Page, val me: ProfileService.Me, val invites: Int) : HomeScreenState data class SignedIn(val page: Page, val me: Me, val invites: Int) : HomeScreenState
enum class Page(val icon: ImageVector) { enum class Page(val icon: ImageVector) {
Directory(Icons.Filled.Menu), Directory(Icons.Filled.Menu),

View File

@ -3,12 +3,11 @@ package app.dapk.st.home
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.dapk.st.directory.DirectoryViewModel import app.dapk.st.directory.DirectoryViewModel
import app.dapk.st.domain.StoreCleaner import app.dapk.st.domain.StoreCleaner
import app.dapk.st.engine.ChatEngine
import app.dapk.st.home.HomeScreenState.* import app.dapk.st.home.HomeScreenState.*
import app.dapk.st.login.LoginViewModel import app.dapk.st.login.LoginViewModel
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.isSignedIn import app.dapk.st.matrix.common.isSignedIn
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.profile.ProfileViewModel import app.dapk.st.profile.ProfileViewModel
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -20,14 +19,13 @@ import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class HomeViewModel( class HomeViewModel(
private val chatEngine: ChatEngine,
private val credentialsProvider: CredentialsStore, private val credentialsProvider: CredentialsStore,
private val directoryViewModel: DirectoryViewModel, private val directoryViewModel: DirectoryViewModel,
private val loginViewModel: LoginViewModel, private val loginViewModel: LoginViewModel,
private val profileViewModel: ProfileViewModel, private val profileViewModel: ProfileViewModel,
private val profileService: ProfileService,
private val cacheCleaner: StoreCleaner, private val cacheCleaner: StoreCleaner,
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase, private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
private val syncService: SyncService,
) : DapkViewModel<HomeScreenState, HomeEvent>( ) : DapkViewModel<HomeScreenState, HomeEvent>(
initialState = Loading initialState = Loading
) { ) {
@ -56,8 +54,8 @@ class HomeViewModel(
} }
private suspend fun initialHomeContent(): SignedIn { private suspend fun initialHomeContent(): SignedIn {
val me = profileService.me(forceRefresh = false) val me = chatEngine.me(forceRefresh = false)
val initialInvites = syncService.invites().first().size val initialInvites = chatEngine.invites().first().size
return SignedIn(Page.Directory, me, invites = initialInvites) return SignedIn(Page.Directory, me, invites = initialInvites)
} }
@ -70,7 +68,7 @@ class HomeViewModel(
private fun CoroutineScope.listenForInviteChanges() { private fun CoroutineScope.listenForInviteChanges() {
listenForInvitesJob?.cancel() listenForInvitesJob?.cancel()
listenForInvitesJob = syncService.invites() listenForInvitesJob = chatEngine.invites()
.onEach { invites -> .onEach { invites ->
when (val currentState = state) { when (val currentState = state) {
is SignedIn -> updateState { currentState.copy(invites = invites.size) } is SignedIn -> updateState { currentState.copy(invites = invites.size) }

View File

@ -1,12 +1,10 @@
applyAndroidComposeLibraryModule(project) applyAndroidComposeLibraryModule(project)
dependencies { dependencies {
implementation project(":chat-engine")
implementation project(":domains:android:compose-core") implementation project(":domains:android:compose-core")
implementation project(":domains:android:push") implementation project(":domains:android:push")
implementation project(":domains:android:viewmodel") implementation project(":domains:android:viewmodel")
implementation project(":matrix:services:auth")
implementation project(":matrix:services:profile")
implementation project(":matrix:services:crypto")
implementation project(":design-library") implementation project(":design-library")
implementation project(":core") implementation project(":core")
} }

View File

@ -2,18 +2,16 @@ package app.dapk.st.login
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.matrix.auth.AuthService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
class LoginModule( class LoginModule(
private val authService: AuthService, private val chatEngine: ChatEngine,
private val pushModule: PushModule, private val pushModule: PushModule,
private val profileService: ProfileService,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
) : ProvidableModule { ) : ProvidableModule {
fun loginViewModel(): LoginViewModel { fun loginViewModel(): LoginViewModel {
return LoginViewModel(authService, pushModule.pushTokenRegistrar(), profileService, errorTracker) return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker)
} }
} }

View File

@ -3,10 +3,11 @@ package app.dapk.st.login
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.core.logP import app.dapk.st.core.logP
import app.dapk.st.engine.ChatEngine
import app.dapk.st.engine.LoginRequest
import app.dapk.st.engine.LoginResult
import app.dapk.st.login.LoginEvent.LoginComplete import app.dapk.st.login.LoginEvent.LoginComplete
import app.dapk.st.login.LoginScreenState.* import app.dapk.st.login.LoginScreenState.*
import app.dapk.st.matrix.auth.AuthService
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.push.PushTokenRegistrar import app.dapk.st.push.PushTokenRegistrar
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.async import kotlinx.coroutines.async
@ -14,9 +15,8 @@ import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class LoginViewModel( class LoginViewModel(
private val authService: AuthService, private val chatEngine: ChatEngine,
private val pushTokenRegistrar: PushTokenRegistrar, private val pushTokenRegistrar: PushTokenRegistrar,
private val profileService: ProfileService,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
) : DapkViewModel<LoginScreenState, LoginEvent>( ) : DapkViewModel<LoginScreenState, LoginEvent>(
initialState = Content(showServerUrl = false) initialState = Content(showServerUrl = false)
@ -28,8 +28,8 @@ class LoginViewModel(
state = Loading state = Loading
viewModelScope.launch { viewModelScope.launch {
logP("login") { logP("login") {
when (val result = authService.login(AuthService.LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) { when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
is AuthService.LoginResult.Success -> { is LoginResult.Success -> {
runCatching { runCatching {
listOf( listOf(
async { pushTokenRegistrar.registerCurrentToken() }, async { pushTokenRegistrar.registerCurrentToken() },
@ -38,11 +38,13 @@ class LoginViewModel(
} }
_events.tryEmit(LoginComplete) _events.tryEmit(LoginComplete)
} }
is AuthService.LoginResult.Error -> {
is LoginResult.Error -> {
errorTracker.track(result.cause) errorTracker.track(result.cause)
state = Error(result.cause) state = Error(result.cause)
} }
AuthService.LoginResult.MissingWellKnown -> {
LoginResult.MissingWellKnown -> {
_events.tryEmit(LoginEvent.WellKnownMissing) _events.tryEmit(LoginEvent.WellKnownMissing)
state = Content(showServerUrl = true) state = Content(showServerUrl = true)
} }
@ -51,7 +53,7 @@ class LoginViewModel(
} }
} }
private suspend fun preloadMe() = profileService.me(forceRefresh = false) private suspend fun preloadMe() = chatEngine.me(forceRefresh = false)
fun start() { fun start() {
val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false

View File

@ -2,10 +2,7 @@ applyAndroidComposeLibraryModule(project)
apply plugin: 'kotlin-parcelize' apply plugin: 'kotlin-parcelize'
dependencies { dependencies {
implementation project(":matrix:services:sync") implementation project(":chat-engine")
implementation project(":matrix:services:message")
implementation project(":matrix:services:crypto")
implementation project(":matrix:services:room")
implementation project(":domains:android:compose-core") implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel") implementation project(":domains:android:viewmodel")
implementation project(":domains:store") implementation project(":domains:store")
@ -16,11 +13,10 @@ dependencies {
kotlinTest(it) kotlinTest(it)
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))
} }

View File

@ -2,10 +2,9 @@ package app.dapk.st.messenger
import android.content.Context import android.content.Context
import android.os.Environment import android.os.Environment
import app.dapk.st.core.Base64 import app.dapk.st.engine.MediaDecrypter
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.crypto.MediaDecrypter
import app.dapk.st.matrix.sync.RoomEvent
import coil.ImageLoader import coil.ImageLoader
import coil.decode.DataSource import coil.decode.DataSource
import coil.decode.ImageSource import coil.decode.ImageSource
@ -20,9 +19,11 @@ import okio.BufferedSource
import okio.Path.Companion.toOkioPath import okio.Path.Companion.toOkioPath
import java.io.File import java.io.File
class DecryptingFetcherFactory(private val context: Context, base64: Base64, private val roomId: RoomId) : Fetcher.Factory<RoomEvent.Image> { class DecryptingFetcherFactory(
private val context: Context,
private val mediaDecrypter = MediaDecrypter(base64) private val roomId: RoomId,
private val mediaDecrypter: MediaDecrypter,
) : Fetcher.Factory<RoomEvent.Image> {
override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher { override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher {
return DecryptingFetcher(data, context, mediaDecrypter, roomId) return DecryptingFetcher(data, context, mediaDecrypter, roomId)

View File

@ -1,49 +1,23 @@
package app.dapk.st.messenger package app.dapk.st.messenger
import android.content.Context import android.content.Context
import app.dapk.st.core.Base64
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.internal.ImageContentReader
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.matrix.sync.SyncService
import java.time.Clock
class MessengerModule( class MessengerModule(
private val syncService: SyncService, private val chatEngine: ChatEngine,
private val messageService: MessageService,
private val roomService: RoomService,
private val credentialsStore: CredentialsStore,
private val roomStore: RoomStore,
private val clock: Clock,
private val context: Context, private val context: Context,
private val base64: Base64,
private val imageMetaReader: ImageContentReader,
private val messageOptionsStore: MessageOptionsStore, private val messageOptionsStore: MessageOptionsStore,
) : ProvidableModule { ) : ProvidableModule {
internal fun messengerViewModel(): MessengerViewModel { internal fun messengerViewModel(): MessengerViewModel {
return MessengerViewModel( return MessengerViewModel(
messageService, chatEngine,
roomService,
roomStore,
credentialsStore,
timelineUseCase(),
LocalIdFactory(),
imageMetaReader,
messageOptionsStore, messageOptionsStore,
clock
) )
} }
private fun timelineUseCase(): TimelineUseCaseImpl { internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, roomId, chatEngine.mediaDecrypter())
val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
}
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId)
} }

View File

@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving
import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.design.components.* import app.dapk.st.design.components.*
import app.dapk.st.engine.MessageMeta
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomState
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
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.messenger.gallery.ImageGalleryActivityPayload import app.dapk.st.messenger.gallery.ImageGalleryActivityPayload
import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.MessageAttachment
import app.dapk.st.navigator.Navigator import app.dapk.st.navigator.Navigator
@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) { AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
when (item) { when (item) {
is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>) is RoomEvent.Image -> MessageImage(it as BubbleContent<RoomEvent.Image>)
is Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>) is RoomEvent.Message -> TextBubbleContent(it as BubbleContent<RoomEvent.Message>)
is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>) is RoomEvent.Reply -> ReplyBubbleContent(it as BubbleContent<RoomEvent.Reply>)
} }
} }
@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
) )
Spacer(modifier = Modifier.height(2.dp)) Spacer(modifier = Modifier.height(2.dp))
when (val replyingTo = content.message.replyingTo) { when (val replyingTo = content.message.replyingTo) {
is Message -> { is RoomEvent.Message -> {
Text( Text(
text = replyingTo.content, text = replyingTo.content,
color = content.textColor().copy(alpha = 0.8f), color = content.textColor().copy(alpha = 0.8f),
@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
) )
} }
when (val message = content.message.message) { when (val message = content.message.message) {
is Message -> { is RoomEvent.Message -> {
Text( Text(
text = message.content, text = message.content,
color = content.textColor(), color = content.textColor(),
@ -642,7 +642,7 @@ private fun TextComposer(state: ComposerState.Text, onTextChange: (String) -> Un
) )
} }
) { ) {
if (it is Message) { if (it is RoomEvent.Message) {
Box(Modifier.padding(12.dp)) { Box(Modifier.padding(12.dp)) {
Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) { Box(Modifier.padding(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
Icon( Icon(

View File

@ -1,8 +1,9 @@
package app.dapk.st.messenger package app.dapk.st.messenger
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.navigator.MessageAttachment import app.dapk.st.navigator.MessageAttachment
data class MessengerScreenState( data class MessengerScreenState(

View File

@ -4,35 +4,22 @@ import androidx.lifecycle.viewModelScope
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.takeIfContent import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.EventId import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.internal.ImageContentReader
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.navigator.MessageAttachment
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import app.dapk.st.viewmodel.MutableStateFactory import app.dapk.st.viewmodel.MutableStateFactory
import app.dapk.st.viewmodel.defaultStateFactory import app.dapk.st.viewmodel.defaultStateFactory
import kotlinx.coroutines.* import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import java.time.Clock import kotlinx.coroutines.launch
internal class MessengerViewModel( internal class MessengerViewModel(
private val messageService: MessageService, private val chatEngine: ChatEngine,
private val roomService: RoomService,
private val roomStore: RoomStore,
private val credentialsStore: CredentialsStore,
private val observeTimeline: ObserveTimelineUseCase,
private val localIdFactory: LocalIdFactory,
private val imageContentReader: ImageContentReader,
private val messageOptionsStore: MessageOptionsStore, private val messageOptionsStore: MessageOptionsStore,
private val clock: Clock,
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(), factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
) : DapkViewModel<MessengerScreenState, MessengerEvent>( ) : DapkViewModel<MessengerScreenState, MessengerEvent>(
initialState = MessengerScreenState( initialState = MessengerScreenState(
@ -83,31 +70,13 @@ internal class MessengerViewModel(
private fun start(action: MessengerAction.OnMessengerVisible) { private fun start(action: MessengerAction.OnMessengerVisible) {
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) } updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
syncJob = viewModelScope.launch { viewModelScope.launch {
roomStore.markRead(action.roomId) syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
.onEach { updateState { copy(roomState = Lce.Content(it)) } }
val credentials = credentialsStore.credentials()!! .launchIn(this)
var lastKnownReadEvent: EventId? = null
observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
if (lastKnownReadEvent != it) {
updateRoomReadStateAsync(latestReadEvent = it, state)
lastKnownReadEvent = it
}
}
updateState { copy(roomState = Lce.Content(state)) }
}.collect()
} }
} }
private fun CoroutineScope.updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState): Deferred<Unit> {
return async {
runCatching {
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = messageOptionsStore.isReadReceiptsDisabled())
roomStore.markRead(state.roomState.roomOverview.roomId)
}
}
}
private fun sendMessage() { private fun sendMessage() {
when (val composerState = state.composerState) { when (val composerState = state.composerState) {
@ -118,27 +87,23 @@ internal class MessengerViewModel(
state.roomState.takeIfContent()?.let { content -> state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState val roomState = content.roomState
viewModelScope.launch { viewModelScope.launch {
messageService.scheduleMessage( chatEngine.send(
MessageService.Message.TextMessage( message = SendMessage.TextMessage(
MessageService.Message.Content.TextContent(body = copy.value), content = copy.value,
roomId = roomState.roomOverview.roomId,
sendEncrypted = roomState.roomOverview.isEncrypted,
localId = localIdFactory.create(),
timestampUtc = clock.millis(),
reply = copy.reply?.let { reply = copy.reply?.let {
MessageService.Message.TextMessage.Reply( SendMessage.TextMessage.Reply(
author = it.author, author = it.author,
originalMessage = when (it) { originalMessage = when (it) {
is RoomEvent.Image -> TODO() is RoomEvent.Image -> TODO()
is RoomEvent.Reply -> TODO() is RoomEvent.Reply -> TODO()
is RoomEvent.Message -> it.content is RoomEvent.Message -> it.content
}, },
replyContent = copy.value,
eventId = it.eventId, eventId = it.eventId,
timestampUtc = it.utcTimestamp, timestampUtc = it.utcTimestamp,
) )
} }
) ),
room = roomState.roomOverview,
) )
} }
} }
@ -151,26 +116,7 @@ internal class MessengerViewModel(
state.roomState.takeIfContent()?.let { content -> state.roomState.takeIfContent()?.let { content ->
val roomState = content.roomState val roomState = content.roomState
viewModelScope.launch { viewModelScope.launch {
val imageUri = copy.values.first().uri.value chatEngine.send(SendMessage.ImageMessage(uri = copy.values.first().uri.value), roomState.roomOverview)
val meta = imageContentReader.meta(imageUri)
messageService.scheduleMessage(
MessageService.Message.ImageMessage(
MessageService.Message.Content.ImageContent(
uri = imageUri, MessageService.Message.Content.ImageContent.Meta(
height = meta.height,
width = meta.width,
size = meta.size,
fileName = meta.fileName,
mimeType = meta.mimeType,
)
),
roomId = roomState.roomOverview.roomId,
sendEncrypted = roomState.roomOverview.isEncrypted,
localId = localIdFactory.create(),
timestampUtc = clock.millis(),
)
)
} }
} }
@ -190,12 +136,6 @@ internal class MessengerViewModel(
} }
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
.filterIsInstance<RoomEvent.Message>()
.filterNot { it.author.id == self }
.firstOrNull()
?.eventId
sealed interface MessengerAction { sealed interface MessengerAction {
data class ComposerTextUpdate(val newValue: String) : MessengerAction data class ComposerTextUpdate(val newValue: String) : MessengerAction
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction

View File

@ -2,34 +2,22 @@ package app.dapk.st.messenger
import ViewModelTest import ViewModelTest
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.takeIfContent
import app.dapk.st.engine.MessengerState
import app.dapk.st.engine.RoomState
import app.dapk.st.engine.SendMessage
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService import fake.FakeChatEngine
import app.dapk.st.matrix.message.internal.ImageContentReader
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.SyncService
import fake.FakeCredentialsStore
import fake.FakeMessageOptionsStore import fake.FakeMessageOptionsStore
import fake.FakeRoomStore
import fixture.* import fixture.*
import internalfake.FakeLocalIdFactory
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import org.junit.Test import org.junit.Test
import test.delegateReturn
import java.time.Clock
import java.time.Instant
import java.time.ZoneOffset
private const val A_CURRENT_TIMESTAMP = 10000L
private const val READ_RECEIPTS_ARE_DISABLED = true private const val READ_RECEIPTS_ARE_DISABLED = true
private val A_ROOM_ID = aRoomId("messenger state room id") private val A_ROOM_ID = aRoomId("messenger state room id")
private const val A_MESSAGE_CONTENT = "message content" private const val A_MESSAGE_CONTENT = "message content"
private const val A_LOCAL_ID = "local.1111-2222-3333"
private val AN_EVENT_ID = anEventId("state event") private val AN_EVENT_ID = anEventId("state event")
private val A_SELF_ID = aUserId("self") private val A_SELF_ID = aUserId("self")
@ -37,23 +25,12 @@ class MessengerViewModelTest {
private val runViewModelTest = ViewModelTest() private val runViewModelTest = ViewModelTest()
private val fakeMessageService = FakeMessageService()
private val fakeRoomService = FakeRoomService()
private val fakeRoomStore = FakeRoomStore()
private val fakeCredentialsStore = FakeCredentialsStore().also { it.givenCredentials().returns(aUserCredentials(userId = A_SELF_ID)) }
private val fakeObserveTimelineUseCase = FakeObserveTimelineUseCase()
private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val fakeMessageOptionsStore = FakeMessageOptionsStore()
private val fakeChatEngine = FakeChatEngine()
private val viewModel = MessengerViewModel( private val viewModel = MessengerViewModel(
fakeMessageService, fakeChatEngine,
fakeRoomService, fakeMessageOptionsStore.instance,
fakeRoomStore,
fakeCredentialsStore,
fakeObserveTimelineUseCase,
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
imageContentReader = FakeImageContentReader(),
messageOptionsStore = fakeMessageOptionsStore.instance,
clock = fixedClock(A_CURRENT_TIMESTAMP),
factory = runViewModelTest.testMutableStateFactory(), factory = runViewModelTest.testMutableStateFactory(),
) )
@ -73,10 +50,8 @@ class MessengerViewModelTest {
@Test @Test
fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest { fun `given timeline emits state, when starting, then updates state and marks room and events as read`() = runViewModelTest {
fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED) fakeMessageOptionsStore.givenReadReceiptsDisabled().returns(READ_RECEIPTS_ARE_DISABLED)
fakeRoomStore.expectUnit(times = 2) { it.markRead(A_ROOM_ID) }
fakeRoomService.expectUnit { it.markFullyRead(A_ROOM_ID, AN_EVENT_ID, isPrivate = READ_RECEIPTS_ARE_DISABLED) }
val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID) val state = aMessengerStateWithEvent(AN_EVENT_ID, A_SELF_ID)
fakeObserveTimelineUseCase.given(A_ROOM_ID, A_SELF_ID).returns(flowOf(state)) fakeChatEngine.givenMessages(A_ROOM_ID, READ_RECEIPTS_ARE_DISABLED).returns(flowOf(state))
viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null)) viewModel.test().post(MessengerAction.OnMessengerVisible(A_ROOM_ID, attachments = null))
@ -98,9 +73,10 @@ class MessengerViewModelTest {
@Test @Test
fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest { fun `given composer message state when posting send text, then resets composer state and sends message`() = runViewModelTest {
fakeMessageService.expectUnit { it.scheduleMessage(expectEncryptedMessage(A_ROOM_ID, A_LOCAL_ID, A_CURRENT_TIMESTAMP, A_MESSAGE_CONTENT)) } val initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)
fakeChatEngine.expectUnit { it.send(expectTextMessage(A_MESSAGE_CONTENT), initialState.roomState.takeIfContent()!!.roomState.roomOverview) }
viewModel.test(initialState = initialStateWithComposerMessage(A_ROOM_ID, A_MESSAGE_CONTENT)).post(MessengerAction.ComposerSendText) viewModel.test(initialState = initialState).post(MessengerAction.ComposerSendText)
assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("", reply = null)) }) assertStates<MessengerScreenState>({ copy(composerState = ComposerState.Text("", reply = null)) })
verifyExpects() verifyExpects()
@ -114,9 +90,8 @@ class MessengerViewModelTest {
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent) return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
} }
private fun expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage { private fun expectTextMessage(messageContent: String): SendMessage.TextMessage {
val content = MessageService.Message.Content.TextContent(body = messageContent) return SendMessage.TextMessage(messageContent, reply = null)
return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp)
} }
private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId) private fun aMessengerStateWithEvent(eventId: EventId, selfId: UserId) = aRoomStateWithEventId(eventId).toMessengerState(selfId)
@ -135,27 +110,3 @@ fun aMessageScreenState(roomId: RoomId = aRoomId(), roomState: MessengerState, m
roomState = Lce.Content(roomState), roomState = Lce.Content(roomState),
composerState = ComposerState.Text(value = messageContent ?: "", reply = null) composerState = ComposerState.Text(value = messageContent ?: "", reply = null)
) )
fun aMessengerState(
self: UserId = aUserId(),
roomState: RoomState,
typing: SyncService.SyncEvent.Typing? = null
) = MessengerState(self, roomState, typing)
class FakeObserveTimelineUseCase : ObserveTimelineUseCase by mockk() {
fun given(roomId: RoomId, selfId: UserId) = coEvery { this@FakeObserveTimelineUseCase.invoke(roomId, selfId) }.delegateReturn()
}
class FakeMessageService : MessageService by mockk() {
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
}
class FakeRoomService : RoomService by mockk() {
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
}
fun fixedClock(timestamp: Long = 0) = Clock.fixed(Instant.ofEpochMilli(timestamp), ZoneOffset.UTC)
class FakeImageContentReader : ImageContentReader by mockk()

View File

@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize'
dependencies { dependencies {
compileOnly project(":domains:android:stub") compileOnly project(":domains:android:stub")
implementation project(":core") implementation project(":core")
implementation project(":matrix:common") implementation project(":chat-engine")
} }

View File

@ -1,8 +1,7 @@
applyAndroidLibraryModule(project) applyAndroidLibraryModule(project)
dependencies { dependencies {
implementation project(":matrix:services:push") implementation project(":chat-engine")
implementation project(":matrix:services:sync")
implementation project(':domains:store') implementation project(':domains:store')
implementation project(":domains:android:work") implementation project(":domains:android:work")
implementation project(':domains:android:push') implementation project(':domains:android:push')
@ -12,12 +11,13 @@ dependencies {
implementation project(":features:messenger") implementation project(":features:messenger")
implementation project(":features:navigator") implementation project(":features:navigator")
implementation Dependencies.mavenCentral.kotlinSerializationJson implementation Dependencies.mavenCentral.kotlinSerializationJson
kotlinTest(it) kotlinTest(it)
androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":matrix:services:sync")) androidImportFixturesWorkaround(project, project(":chat-engine"))
androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":domains:android:stub"))
} }

View File

@ -4,9 +4,9 @@ import android.app.Notification
import android.content.Context import android.content.Context
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.whenPOrHigher import app.dapk.st.core.whenPOrHigher
import app.dapk.st.engine.RoomOverview
import app.dapk.st.imageloader.IconLoader import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import java.time.Clock import java.time.Clock
@ -87,7 +87,7 @@ class NotificationFactory(
) )
} }
fun createInvite(inviteNotification: InviteNotification): AndroidNotification { fun createInvite(inviteNotification: app.dapk.st.engine.InviteNotification): AndroidNotification {
val openAppIntent = intentFactory.notificationOpenApp(context) val openAppIntent = intentFactory.notificationOpenApp(context)
return AndroidNotification( return AndroidNotification(
channelId = INVITE_CHANNEL_ID, channelId = INVITE_CHANNEL_ID,

View File

@ -10,7 +10,7 @@ class NotificationInviteRenderer(
private val androidNotificationBuilder: AndroidNotificationBuilder, private val androidNotificationBuilder: AndroidNotificationBuilder,
) { ) {
fun render(inviteNotification: InviteNotification) { fun render(inviteNotification: app.dapk.st.engine.InviteNotification) {
notificationManager.notify( notificationManager.notify(
inviteNotification.roomId.value, inviteNotification.roomId.value,
INVITE_NOTIFICATION_ID, INVITE_NOTIFICATION_ID,
@ -18,7 +18,7 @@ class NotificationInviteRenderer(
) )
} }
private fun InviteNotification.toAndroidNotification() = androidNotificationBuilder.build( private fun app.dapk.st.engine.InviteNotification.toAndroidNotification() = androidNotificationBuilder.build(
notificationFactory.createInvite(this) notificationFactory.createInvite(this)
) )

View File

@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.extensions.ifNull import app.dapk.st.core.extensions.ifNull
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomOverview
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
private const val SUMMARY_NOTIFICATION_ID = 101 private const val SUMMARY_NOTIFICATION_ID = 101

View File

@ -3,8 +3,8 @@ package app.dapk.st.notifications
import android.annotation.SuppressLint import android.annotation.SuppressLint
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.whenPOrHigher import app.dapk.st.core.whenPOrHigher
import app.dapk.st.engine.RoomOverview
import app.dapk.st.imageloader.IconLoader import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.notifications.AndroidNotificationStyle.Inbox import app.dapk.st.notifications.AndroidNotificationStyle.Inbox
import app.dapk.st.notifications.AndroidNotificationStyle.Messaging import app.dapk.st.notifications.AndroidNotificationStyle.Messaging

View File

@ -5,16 +5,14 @@ import android.content.Context
import app.dapk.st.core.CoroutineDispatchers import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.engine.ChatEngine
import app.dapk.st.imageloader.IconLoader import app.dapk.st.imageloader.IconLoader
import app.dapk.st.matrix.sync.OverviewStore
import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import java.time.Clock import java.time.Clock
class NotificationsModule( class NotificationsModule(
private val chatEngine: ChatEngine,
private val iconLoader: IconLoader, private val iconLoader: IconLoader,
private val roomStore: RoomStore,
private val overviewStore: OverviewStore,
private val context: Context, private val context: Context,
private val intentFactory: IntentFactory, private val intentFactory: IntentFactory,
private val dispatchers: CoroutineDispatchers, private val dispatchers: CoroutineDispatchers,
@ -40,10 +38,9 @@ class NotificationsModule(
) )
return RenderNotificationsUseCase( return RenderNotificationsUseCase(
notificationRenderer = notificationMessageRenderer, notificationRenderer = notificationMessageRenderer,
observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore),
notificationChannels = NotificationChannels(notificationManager), notificationChannels = NotificationChannels(notificationManager),
observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore), inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder),
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder) chatEngine = chatEngine,
) )
} }

View File

@ -1,7 +1,9 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.matrix.sync.RoomEvent import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.sync.RoomOverview import app.dapk.st.engine.NotificationDiff
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomOverview
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach
class RenderNotificationsUseCase( class RenderNotificationsUseCase(
private val notificationRenderer: NotificationMessageRenderer, private val notificationRenderer: NotificationMessageRenderer,
private val inviteRenderer: NotificationInviteRenderer, private val inviteRenderer: NotificationInviteRenderer,
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase, private val chatEngine: ChatEngine,
private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase,
private val notificationChannels: NotificationChannels, private val notificationChannels: NotificationChannels,
) { ) {
suspend fun listenForNotificationChanges(scope: CoroutineScope) { suspend fun listenForNotificationChanges(scope: CoroutineScope) {
notificationChannels.initChannels() notificationChannels.initChannels()
observeRenderableUnreadEventsUseCase() chatEngine.notificationsMessages()
.onEach { (each, diff) -> renderUnreadChange(each, diff) } .onEach { (each, diff) -> renderUnreadChange(each, diff) }
.launchIn(scope) .launchIn(scope)
observeInviteNotificationsUseCase() chatEngine.notificationsInvites()
.onEach { inviteRenderer.render(it) } .onEach { inviteRenderer.render(it) }
.launchIn(scope) .launchIn(scope)
} }

View File

@ -1,7 +1,7 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.engine.RoomEvent
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.sync.RoomEvent
class RoomEventsToNotifiableMapper { class RoomEventsToNotifiableMapper {

View File

@ -4,8 +4,8 @@ import android.app.Notification
import android.app.PendingIntent import android.app.PendingIntent
import android.os.Build import android.os.Build
import app.dapk.st.core.DeviceMeta import app.dapk.st.core.DeviceMeta
import app.dapk.st.engine.RoomOverview
import app.dapk.st.matrix.common.AvatarUrl import app.dapk.st.matrix.common.AvatarUrl
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeContext import fake.FakeContext
import fixture.NotificationDelegateFixtures.anAndroidNotification import fixture.NotificationDelegateFixtures.anAndroidNotification
import fixture.NotificationDelegateFixtures.anInboxStyle import fixture.NotificationDelegateFixtures.anInboxStyle
@ -137,7 +137,7 @@ class NotificationFactoryTest {
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT) fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
val content = "Content message" val content = "Content message"
val result = notificationFactory.createInvite( val result = notificationFactory.createInvite(
InviteNotification( app.dapk.st.engine.InviteNotification(
content = content, content = content,
A_ROOM_ID, A_ROOM_ID,
) )

View File

@ -1,8 +1,8 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomOverview
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeNotificationFactory import fake.FakeNotificationFactory
import fake.FakeNotificationManager import fake.FakeNotificationManager
import fake.aFakeNotification import fake.aFakeNotification

View File

@ -1,9 +1,9 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import android.content.Context import android.content.Context
import app.dapk.st.engine.RoomEvent
import app.dapk.st.engine.RoomOverview
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.navigator.IntentFactory import app.dapk.st.navigator.IntentFactory
import fixture.NotificationDelegateFixtures.anAndroidNotification import fixture.NotificationDelegateFixtures.anAndroidNotification
import fixture.NotificationFixtures.aDismissRoomNotification import fixture.NotificationFixtures.aDismissRoomNotification

View File

@ -1,5 +1,6 @@
package app.dapk.st.notifications package app.dapk.st.notifications
import app.dapk.st.engine.UnreadNotifications
import fake.* import fake.*
import fixture.NotificationDiffFixtures.aNotificationDiff import fixture.NotificationDiffFixtures.aNotificationDiff
import kotlinx.coroutines.test.TestScope import kotlinx.coroutines.test.TestScope
@ -14,25 +15,23 @@ class RenderNotificationsUseCaseTest {
private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer() private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer()
private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer() private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer()
private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase()
private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase()
private val fakeNotificationChannels = FakeNotificationChannels().also { private val fakeNotificationChannels = FakeNotificationChannels().also {
it.instance.expect { it.initChannels() } it.instance.expect { it.initChannels() }
} }
private val fakeChatEngine = FakeChatEngine()
private val renderNotificationsUseCase = RenderNotificationsUseCase( private val renderNotificationsUseCase = RenderNotificationsUseCase(
fakeNotificationMessageRenderer.instance, fakeNotificationMessageRenderer.instance,
fakeNotificationInviteRenderer.instance, fakeNotificationInviteRenderer.instance,
fakeObserveUnreadNotificationsUseCase, fakeChatEngine,
fakeObserveInviteNotificationsUseCase,
fakeNotificationChannels.instance, fakeNotificationChannels.instance,
) )
@Test @Test
fun `given events, when listening for changes then initiates channels once`() = runTest { fun `given events, when listening for changes then initiates channels once`() = runTest {
fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
fakeObserveInviteNotificationsUseCase.given().emits() fakeChatEngine.givenNotificationsInvites().emits()
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
@ -42,8 +41,8 @@ class RenderNotificationsUseCaseTest {
@Test @Test
fun `given renderable unread events, when listening for changes, then renders change`() = runTest { fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
fakeNotificationMessageRenderer.instance.expect { it.render(any()) } fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS) fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
fakeObserveInviteNotificationsUseCase.given().emits() fakeChatEngine.givenNotificationsInvites().emits()
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher())) renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))

View File

@ -2,14 +2,14 @@ package fake
import app.dapk.st.notifications.NotificationMessageRenderer import app.dapk.st.notifications.NotificationMessageRenderer
import app.dapk.st.notifications.NotificationState import app.dapk.st.notifications.NotificationState
import app.dapk.st.notifications.UnreadNotifications import app.dapk.st.engine.UnreadNotifications
import io.mockk.coVerify import io.mockk.coVerify
import io.mockk.mockk import io.mockk.mockk
class FakeNotificationMessageRenderer { class FakeNotificationMessageRenderer {
val instance = mockk<NotificationMessageRenderer>() val instance = mockk<NotificationMessageRenderer>()
fun verifyRenders(vararg unreadNotifications: UnreadNotifications) { fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) {
unreadNotifications.forEach { unread -> unreadNotifications.forEach { unread ->
coVerify { coVerify {
instance.render( instance.render(

View File

@ -1,9 +1,7 @@
applyAndroidComposeLibraryModule(project) applyAndroidComposeLibraryModule(project)
dependencies { dependencies {
implementation project(":matrix:services:sync") implementation project(":chat-engine")
implementation project(":matrix:services:room")
implementation project(":matrix:services:profile")
implementation project(":features:settings") implementation project(":features:settings")
implementation project(':domains:store') implementation project(':domains:store')
implementation project(":domains:android:compose-core") implementation project(":domains:android:compose-core")

View File

@ -2,19 +2,15 @@ package app.dapk.st.profile
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.matrix.room.ProfileService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.SyncService
class ProfileModule( class ProfileModule(
private val profileService: ProfileService, private val chatEngine: ChatEngine,
private val syncService: SyncService,
private val roomService: RoomService,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
) : ProvidableModule { ) : ProvidableModule {
fun profileViewModel(): ProfileViewModel { fun profileViewModel(): ProfileViewModel {
return ProfileViewModel(profileService, syncService, roomService, errorTracker) return ProfileViewModel(chatEngine, errorTracker)
} }
} }

View File

@ -21,8 +21,8 @@ import app.dapk.st.core.Lce
import app.dapk.st.core.LifecycleEffect import app.dapk.st.core.LifecycleEffect
import app.dapk.st.core.components.CenteredLoading import app.dapk.st.core.components.CenteredLoading
import app.dapk.st.design.components.* import app.dapk.st.design.components.*
import app.dapk.st.matrix.sync.InviteMeta import app.dapk.st.engine.RoomInvite
import app.dapk.st.matrix.sync.RoomInvite import app.dapk.st.engine.RoomInvite.InviteMeta
import app.dapk.st.settings.SettingsActivity import app.dapk.st.settings.SettingsActivity
@Composable @Composable

View File

@ -3,9 +3,8 @@ package app.dapk.st.profile
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.design.components.Route import app.dapk.st.design.components.Route
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.matrix.common.RoomId import app.dapk.st.engine.Me
import app.dapk.st.matrix.room.ProfileService import app.dapk.st.engine.RoomInvite
import app.dapk.st.matrix.sync.RoomInvite
data class ProfileScreenState( data class ProfileScreenState(
val page: SpiderPage<out Page>, val page: SpiderPage<out Page>,
@ -14,12 +13,12 @@ data class ProfileScreenState(
sealed interface Page { sealed interface Page {
data class Profile(val content: Lce<Content>) : Page { data class Profile(val content: Lce<Content>) : Page {
data class Content( data class Content(
val me: ProfileService.Me, val me: Me,
val invitationsCount: Int, val invitationsCount: Int,
) )
} }
data class Invitations(val content: Lce<List<RoomInvite>>): Page data class Invitations(val content: Lce<List<RoomInvite>>) : Page
object Routes { object Routes {
val profile = Route<Profile>("Profile") val profile = Route<Profile>("Profile")

View File

@ -5,25 +5,20 @@ import androidx.lifecycle.viewModelScope
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.core.extensions.ErrorTracker import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.room.ProfileService
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.viewmodel.DapkViewModel import app.dapk.st.viewmodel.DapkViewModel
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class ProfileViewModel( class ProfileViewModel(
private val profileService: ProfileService, private val chatEngine: ChatEngine,
private val syncService: SyncService,
private val roomService: RoomService,
private val errorTracker: ErrorTracker, private val errorTracker: ErrorTracker,
) : DapkViewModel<ProfileScreenState, ProfileEvent>( ) : DapkViewModel<ProfileScreenState, ProfileEvent>(
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false)) ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
) { ) {
private var syncingJob: Job? = null
private var currentPageJob: Job? = null private var currentPageJob: Job? = null
fun start() { fun start() {
@ -31,15 +26,13 @@ class ProfileViewModel(
} }
private fun goToProfile() { private fun goToProfile() {
syncingJob = syncService.startSyncing().launchIn(viewModelScope)
combine( combine(
flow { flow {
val result = runCatching { profileService.me(forceRefresh = true) } val result = runCatching { chatEngine.me(forceRefresh = true) }
.onFailure { errorTracker.track(it, "Loading profile") } .onFailure { errorTracker.track(it, "Loading profile") }
emit(result) emit(result)
}, },
syncService.invites(), chatEngine.invites(),
transform = { me, invites -> me to invites } transform = { me, invites -> me to invites }
) )
.onEach { (me, invites) -> .onEach { (me, invites) ->
@ -57,7 +50,7 @@ class ProfileViewModel(
fun goToInvitations() { fun goToInvitations() {
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) } updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
syncService.invites() chatEngine.invites()
.onEach { .onEach {
updatePageState<Page.Invitations> { updatePageState<Page.Invitations> {
copy(content = Lce.Content(it)) copy(content = Lce.Content(it))
@ -89,13 +82,13 @@ class ProfileViewModel(
} }
fun acceptRoomInvite(roomId: RoomId) { fun acceptRoomInvite(roomId: RoomId) {
launchCatching { roomService.joinRoom(roomId) }.fold( launchCatching { chatEngine.joinRoom(roomId) }.fold(
onError = {} onError = {}
) )
} }
fun rejectRoomInvite(roomId: RoomId) { fun rejectRoomInvite(roomId: RoomId) {
launchCatching { roomService.rejectJoinRoom(roomId) }.fold( launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold(
onError = { onError = {
Log.e("!!!", it.message, it) Log.e("!!!", it.message, it)
} }
@ -115,7 +108,7 @@ class ProfileViewModel(
} }
fun stop() { fun stop() {
syncingJob?.cancel() currentPageJob?.cancel()
} }
} }

View File

@ -1,8 +1,7 @@
applyAndroidComposeLibraryModule(project) applyAndroidComposeLibraryModule(project)
dependencies { dependencies {
implementation project(":matrix:services:sync") implementation project(":chat-engine")
implementation project(":matrix:services:crypto")
implementation project(":features:navigator") implementation project(":features:navigator")
implementation project(':domains:store') implementation project(':domains:store')
implementation project(':domains:android:push') implementation project(':domains:android:push')
@ -13,11 +12,10 @@ dependencies {
kotlinTest(it) kotlinTest(it)
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
androidImportFixturesWorkaround(project, project(":matrix:services:crypto"))
androidImportFixturesWorkaround(project, project(":matrix:common")) androidImportFixturesWorkaround(project, project(":matrix:common"))
androidImportFixturesWorkaround(project, project(":core")) androidImportFixturesWorkaround(project, project(":core"))
androidImportFixturesWorkaround(project, project(":domains:store")) androidImportFixturesWorkaround(project, project(":domains:store"))
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel")) androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
androidImportFixturesWorkaround(project, project(":domains:android:stub")) androidImportFixturesWorkaround(project, project(":domains:android:stub"))
androidImportFixturesWorkaround(project, project(":chat-engine"))
} }

View File

@ -5,16 +5,14 @@ import app.dapk.st.core.*
import app.dapk.st.domain.StoreModule import app.dapk.st.domain.StoreModule
import app.dapk.st.domain.application.eventlog.LoggingStore import app.dapk.st.domain.application.eventlog.LoggingStore
import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.push.PushModule import app.dapk.st.push.PushModule
import app.dapk.st.settings.eventlogger.EventLoggerViewModel import app.dapk.st.settings.eventlogger.EventLoggerViewModel
class SettingsModule( class SettingsModule(
private val chatEngine: ChatEngine,
private val storeModule: StoreModule, private val storeModule: StoreModule,
private val pushModule: PushModule, private val pushModule: PushModule,
private val cryptoService: CryptoService,
private val syncService: SyncService,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val buildMeta: BuildMeta, private val buildMeta: BuildMeta,
private val deviceMeta: DeviceMeta, private val deviceMeta: DeviceMeta,
@ -26,10 +24,9 @@ class SettingsModule(
internal fun settingsViewModel(): SettingsViewModel { internal fun settingsViewModel(): SettingsViewModel {
return SettingsViewModel( return SettingsViewModel(
chatEngine,
storeModule.cacheCleaner(), storeModule.cacheCleaner(),
contentResolver, contentResolver,
cryptoService,
syncService,
UriFilenameResolver(contentResolver, coroutineDispatchers), UriFilenameResolver(contentResolver, coroutineDispatchers),
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore), SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
pushModule.pushTokenRegistrars(), pushModule.pushTokenRegistrars(),

View File

@ -42,7 +42,7 @@ import app.dapk.st.core.components.Header
import app.dapk.st.core.extensions.takeAs import app.dapk.st.core.extensions.takeAs
import app.dapk.st.core.getActivity import app.dapk.st.core.getActivity
import app.dapk.st.design.components.* import app.dapk.st.design.components.*
import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.engine.ImportResult
import app.dapk.st.navigator.Navigator import app.dapk.st.navigator.Navigator
import app.dapk.st.settings.SettingsEvent.* import app.dapk.st.settings.SettingsEvent.*
import app.dapk.st.settings.eventlogger.EventLogActivity import app.dapk.st.settings.eventlogger.EventLogActivity

View File

@ -2,10 +2,9 @@ package app.dapk.st.settings
import android.net.Uri import android.net.Uri
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.core.LceWithProgress
import app.dapk.st.design.components.Route import app.dapk.st.design.components.Route
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.engine.ImportResult
import app.dapk.st.push.Registrar import app.dapk.st.push.Registrar
internal data class SettingsScreenState( internal data class SettingsScreenState(

View File

@ -9,9 +9,8 @@ import app.dapk.st.design.components.SpiderPage
import app.dapk.st.domain.StoreCleaner import app.dapk.st.domain.StoreCleaner
import app.dapk.st.domain.application.eventlog.LoggingStore import app.dapk.st.domain.application.eventlog.LoggingStore
import app.dapk.st.domain.application.message.MessageOptionsStore import app.dapk.st.domain.application.message.MessageOptionsStore
import app.dapk.st.matrix.crypto.CryptoService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.engine.ImportResult
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.push.PushTokenRegistrars import app.dapk.st.push.PushTokenRegistrars
import app.dapk.st.push.Registrar import app.dapk.st.push.Registrar
import app.dapk.st.settings.SettingItem.Id.* import app.dapk.st.settings.SettingItem.Id.*
@ -26,10 +25,9 @@ import kotlinx.coroutines.launch
private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/" private const val PRIVACY_POLICY_URL = "https://ouchadam.github.io/small-talk/privacy/"
internal class SettingsViewModel( internal class SettingsViewModel(
private val chatEngine: ChatEngine,
private val cacheCleaner: StoreCleaner, private val cacheCleaner: StoreCleaner,
private val contentResolver: ContentResolver, private val contentResolver: ContentResolver,
private val cryptoService: CryptoService,
private val syncService: SyncService,
private val uriFilenameResolver: UriFilenameResolver, private val uriFilenameResolver: UriFilenameResolver,
private val settingsItemFactory: SettingsItemFactory, private val settingsItemFactory: SettingsItemFactory,
private val pushTokenRegistrars: PushTokenRegistrars, private val pushTokenRegistrars: PushTokenRegistrars,
@ -142,26 +140,13 @@ internal class SettingsViewModel(
fun importFromFileKeys(file: Uri, passphrase: String) { fun importFromFileKeys(file: Uri, passphrase: String) {
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) } updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) }
viewModelScope.launch { viewModelScope.launch {
with(cryptoService) { with(chatEngine) {
runCatching { contentResolver.openInputStream(file)!! } runCatching { contentResolver.openInputStream(file)!! }
.fold( .fold(
onSuccess = { fileStream -> onSuccess = { fileStream ->
fileStream.importRoomKeys(passphrase) fileStream.importRoomKeys(passphrase)
.onEach { .onEach {
updatePageState<Page.ImportRoomKey> { copy(importProgress = it) } updatePageState<Page.ImportRoomKey> { copy(importProgress = it) }
when (it) {
is ImportResult.Error -> {
// do nothing
}
is ImportResult.Update -> {
// do nothing
}
is ImportResult.Success -> {
syncService.forceManualRefresh(it.roomIds.toList())
}
}
} }
.launchIn(viewModelScope) .launchIn(viewModelScope)
}, },

View File

@ -3,9 +3,8 @@ package app.dapk.st.settings
import ViewModelTest import ViewModelTest
import app.dapk.st.core.Lce import app.dapk.st.core.Lce
import app.dapk.st.design.components.SpiderPage import app.dapk.st.design.components.SpiderPage
import app.dapk.st.matrix.crypto.ImportResult import app.dapk.st.engine.ImportResult
import fake.* import fake.*
import fake.FakeStoreCleaner
import fixture.aRoomId import fixture.aRoomId
import internalfake.FakeSettingsItemFactory import internalfake.FakeSettingsItemFactory
import internalfake.FakeUriFilenameResolver import internalfake.FakeUriFilenameResolver
@ -35,20 +34,18 @@ internal class SettingsViewModelTest {
private val fakeStoreCleaner = FakeStoreCleaner() private val fakeStoreCleaner = FakeStoreCleaner()
private val fakeContentResolver = FakeContentResolver() private val fakeContentResolver = FakeContentResolver()
private val fakeCryptoService = FakeCryptoService()
private val fakeSyncService = FakeSyncService()
private val fakeUriFilenameResolver = FakeUriFilenameResolver() private val fakeUriFilenameResolver = FakeUriFilenameResolver()
private val fakePushTokenRegistrars = FakePushRegistrars() private val fakePushTokenRegistrars = FakePushRegistrars()
private val fakeSettingsItemFactory = FakeSettingsItemFactory() private val fakeSettingsItemFactory = FakeSettingsItemFactory()
private val fakeThemeStore = FakeThemeStore() private val fakeThemeStore = FakeThemeStore()
private val fakeLoggingStore = FakeLoggingStore() private val fakeLoggingStore = FakeLoggingStore()
private val fakeMessageOptionsStore = FakeMessageOptionsStore() private val fakeMessageOptionsStore = FakeMessageOptionsStore()
private val fakeChatEngine = FakeChatEngine()
private val viewModel = SettingsViewModel( private val viewModel = SettingsViewModel(
fakeChatEngine,
fakeStoreCleaner, fakeStoreCleaner,
fakeContentResolver.instance, fakeContentResolver.instance,
fakeCryptoService,
fakeSyncService,
fakeUriFilenameResolver.instance, fakeUriFilenameResolver.instance,
fakeSettingsItemFactory.instance, fakeSettingsItemFactory.instance,
fakePushTokenRegistrars.instance, fakePushTokenRegistrars.instance,
@ -174,9 +171,8 @@ internal class SettingsViewModelTest {
@Test @Test
fun `given success when importing room keys, then emits progress`() = runViewModelTest { fun `given success when importing room keys, then emits progress`() = runViewModelTest {
fakeSyncService.expectUnit { it.forceManualRefresh(A_LIST_OF_ROOM_IDS) }
fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance) fakeContentResolver.givenFile(A_URI.instance).returns(AN_INPUT_STREAM.instance)
fakeCryptoService.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS)) fakeChatEngine.givenImportKeys(AN_INPUT_STREAM.instance, A_PASSPHRASE).returns(flowOf(AN_IMPORT_SUCCESS))
viewModel viewModel
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION)) .test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))

View File

@ -4,9 +4,7 @@ dependencies {
implementation project(":domains:android:compose-core") implementation project(":domains:android:compose-core")
implementation project(":domains:android:viewmodel") implementation project(":domains:android:viewmodel")
implementation project(':domains:store') implementation project(':domains:store')
implementation project(':matrix:services:sync') implementation project(':chat-engine')
implementation project(':matrix:services:room')
implementation project(':matrix:services:message')
implementation project(":core") implementation project(":core")
implementation project(":design-library") implementation project(":design-library")
implementation project(":features:navigator") implementation project(":features:navigator")

View File

@ -1,21 +1,20 @@
package app.dapk.st.share package app.dapk.st.share
import app.dapk.st.matrix.room.RoomService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.sync.SyncService
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
class FetchRoomsUseCase( class FetchRoomsUseCase(
private val syncSyncService: SyncService, private val chatEngine: ChatEngine,
private val roomService: RoomService,
) { ) {
suspend fun bar(): List<Item> { suspend fun fetch(): List<Item> {
return syncSyncService.overview().first().map { return chatEngine.directory().first().map {
val overview = it.overview
Item( Item(
it.roomId, overview.roomId,
it.roomAvatarUrl, overview.roomAvatarUrl,
it.roomName ?: "", overview.roomName ?: "",
roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value } chatEngine.findMembersSummary(overview.roomId).map { it.displayName ?: it.id.value }
) )
} }
} }

View File

@ -1,15 +1,13 @@
package app.dapk.st.share package app.dapk.st.share
import app.dapk.st.core.ProvidableModule import app.dapk.st.core.ProvidableModule
import app.dapk.st.matrix.room.RoomService import app.dapk.st.engine.ChatEngine
import app.dapk.st.matrix.sync.SyncService
class ShareEntryModule( class ShareEntryModule(
private val syncService: SyncService, private val chatEngine: ChatEngine,
private val roomService: RoomService,
) : ProvidableModule { ) : ProvidableModule {
fun shareEntryViewModel(): ShareEntryViewModel { fun shareEntryViewModel(): ShareEntryViewModel {
return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService)) return ShareEntryViewModel(FetchRoomsUseCase(chatEngine))
} }
} }

View File

@ -22,7 +22,7 @@ class ShareEntryViewModel(
fun start() { fun start() {
syncJob = viewModelScope.launch { syncJob = viewModelScope.launch {
state = DirectoryScreenState.Content(fetchRoomsUseCase.bar()) state = DirectoryScreenState.Content(fetchRoomsUseCase.fetch())
} }
} }

View File

@ -0,0 +1,34 @@
plugins {
id 'java-test-fixtures'
id 'kotlin'
}
dependencies {
api Dependencies.mavenCentral.kotlinCoroutinesCore
implementation project(":core")
implementation project(":chat-engine")
implementation project(":domains:olm")
implementation project(":matrix:matrix")
implementation project(":matrix:matrix-http-ktor")
implementation project(":matrix:services:auth")
implementation project(":matrix:services:sync")
implementation project(":matrix:services:room")
implementation project(":matrix:services:push")
implementation project(":matrix:services:message")
implementation project(":matrix:services:device")
implementation project(":matrix:services:crypto")
implementation project(":matrix:services:profile")
kotlinTest(it)
kotlinFixtures(it)
testImplementation(testFixtures(project(":matrix:services:sync")))
testImplementation(testFixtures(project(":matrix:services:message")))
testImplementation(testFixtures(project(":matrix:common")))
testImplementation(testFixtures(project(":core")))
testImplementation(testFixtures(project(":domains:store")))
testImplementation(testFixtures(project(":chat-engine")))
}

View File

@ -1,4 +1,4 @@
package app.dapk.st.directory package app.dapk.st.engine
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
@ -6,22 +6,12 @@ import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.* import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
@JvmInline internal class DirectoryUseCase(
value class UnreadCount(val value: Int)
typealias DirectoryState = List<RoomFoo>
data class RoomFoo(
val overview: RoomOverview,
val unreadCount: UnreadCount,
val typing: Typing?
)
class DirectoryUseCase(
private val syncService: SyncService, private val syncService: SyncService,
private val messageService: MessageService, private val messageService: MessageService,
private val roomService: RoomService, private val roomService: RoomService,
@ -38,10 +28,10 @@ class DirectoryUseCase(
syncService.events() syncService.events()
) { overviewState, localEchos, unread, events -> ) { overviewState, localEchos, unread, events ->
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview -> overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
RoomFoo( DirectoryItem(
overview = roomOverview, overview = roomOverview,
unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0), unreadCount = UnreadCount(unread[roomOverview.roomId] ?: 0),
typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId } typing = events.filterIsInstance<Typing>().firstOrNull { it.roomId == roomOverview.roomId }?.engine()
) )
} }
} }
@ -49,14 +39,9 @@ class DirectoryUseCase(
} }
private fun overviewDatasource() = combine( private fun overviewDatasource() = combine(
syncService.startSyncing().map { false }.onStart { emit(true) }, syncService.startSyncing(),
syncService.overview() syncService.overview().map { it.map { it.engine() } }
) { isFirstLoad, overview -> ) { _, overview -> overview }.filterNotNull()
when {
isFirstLoad && overview.isEmpty() -> null
else -> overview
}
}.filterNotNull()
private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map<RoomId, List<MessageService.LocalEcho>>, userId: UserId): OverviewState { private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map<RoomId, List<MessageService.LocalEcho>>, userId: UserId): OverviewState {
return when { return when {
@ -81,7 +66,7 @@ class DirectoryUseCase(
val latestEcho = echos.maxByOrNull { it.timestampUtc } val latestEcho = echos.maxByOrNull { it.timestampUtc }
return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) { return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) {
this.copy( this.copy(
lastMessage = LastMessage( lastMessage = RoomOverview.LastMessage(
content = when (val message = latestEcho.message) { content = when (val message = latestEcho.message) {
is MessageService.Message.TextMessage -> message.content.body is MessageService.Message.TextMessage -> message.content.body
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7" is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
@ -96,3 +81,6 @@ class DirectoryUseCase(
} }
} }

View File

@ -0,0 +1,19 @@
package app.dapk.st.engine
import app.dapk.st.matrix.sync.SyncService
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
class InviteUseCase(
private val syncService: SyncService
) {
fun invites() = invitesDatasource()
private fun invitesDatasource() = combine(
syncService.startSyncing(),
syncService.invites().map { it.map { it.engine() } }
) { _, invites -> invites }.filterNotNull()
}

View File

@ -1,10 +1,8 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.sync.MessageMeta
import app.dapk.st.matrix.sync.RoomEvent
internal class LocalEchoMapper(private val metaMapper: MetaMapper) { internal class LocalEchoMapper(private val metaMapper: MetaMapper) {

View File

@ -0,0 +1,7 @@
package app.dapk.st.engine
import java.util.*
internal class LocalIdFactory {
fun create() = "local.${UUID.randomUUID()}"
}

View File

@ -0,0 +1,116 @@
package app.dapk.st.engine
import app.dapk.st.matrix.auth.AuthService
import app.dapk.st.matrix.sync.InviteMeta
import app.dapk.st.matrix.auth.AuthService.LoginRequest as MatrixLoginRequest
import app.dapk.st.matrix.auth.AuthService.LoginResult as MatrixLoginResult
import app.dapk.st.matrix.crypto.ImportResult as MatrixImportResult
import app.dapk.st.matrix.room.ProfileService.Me as MatrixMe
import app.dapk.st.matrix.sync.LastMessage as MatrixLastMessage
import app.dapk.st.matrix.sync.MessageMeta as MatrixMessageMeta
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
import app.dapk.st.matrix.sync.RoomInvite as MatrixRoomInvite
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
import app.dapk.st.matrix.sync.RoomState as MatrixRoomState
import app.dapk.st.matrix.sync.SyncService.SyncEvent.Typing as MatrixTyping
fun MatrixRoomOverview.engine() = RoomOverview(
this.roomId,
this.roomCreationUtc,
this.roomName,
this.roomAvatarUrl,
this.lastMessage?.engine(),
this.isGroup,
this.readMarker,
this.isEncrypted
)
fun MatrixLastMessage.engine() = RoomOverview.LastMessage(
this.content,
this.utcTimestamp,
this.author,
)
fun MatrixTyping.engine() = Typing(
this.roomId,
this.members,
)
fun LoginRequest.engine() = MatrixLoginRequest(
this.userName,
this.password,
this.serverUrl
)
fun MatrixLoginResult.engine() = when (this) {
is AuthService.LoginResult.Error -> LoginResult.Error(this.cause)
AuthService.LoginResult.MissingWellKnown -> LoginResult.MissingWellKnown
is AuthService.LoginResult.Success -> LoginResult.Success(this.userCredentials)
}
fun MatrixMe.engine() = Me(
this.userId,
this.displayName,
this.avatarUrl,
this.homeServerUrl,
)
fun MatrixRoomInvite.engine() = RoomInvite(
this.from,
this.roomId,
this.inviteMeta.engine(),
)
fun InviteMeta.engine() = when (this) {
InviteMeta.DirectMessage -> RoomInvite.InviteMeta.DirectMessage
is InviteMeta.Room -> RoomInvite.InviteMeta.Room(this.roomName)
}
fun MatrixImportResult.engine() = when (this) {
is MatrixImportResult.Error -> ImportResult.Error(
when (val error = this.cause) {
MatrixImportResult.Error.Type.InvalidFile -> ImportResult.Error.Type.InvalidFile
MatrixImportResult.Error.Type.NoKeysFound -> ImportResult.Error.Type.NoKeysFound
MatrixImportResult.Error.Type.UnableToOpenFile -> ImportResult.Error.Type.UnableToOpenFile
MatrixImportResult.Error.Type.UnexpectedDecryptionOutput -> ImportResult.Error.Type.UnexpectedDecryptionOutput
is MatrixImportResult.Error.Type.Unknown -> ImportResult.Error.Type.Unknown(error.cause)
}
)
is MatrixImportResult.Success -> ImportResult.Success(this.roomIds, this.totalImportedKeysCount)
is MatrixImportResult.Update -> ImportResult.Update(this.importedKeysCount)
}
fun MatrixRoomState.engine() = RoomState(
this.roomOverview.engine(),
this.events.map { it.engine() }
)
fun MatrixRoomEvent.engine(): RoomEvent = when (this) {
is MatrixRoomEvent.Image -> RoomEvent.Image(this.eventId, this.utcTimestamp, this.imageMeta.engine(), this.author, this.meta.engine(), this.edited)
is MatrixRoomEvent.Message -> RoomEvent.Message(this.eventId, this.utcTimestamp, this.content, this.author, this.meta.engine(), this.edited, this.redacted)
is MatrixRoomEvent.Reply -> RoomEvent.Reply(this.message.engine(), this.replyingTo.engine())
}
fun MatrixRoomEvent.Image.ImageMeta.engine() = RoomEvent.Image.ImageMeta(
this.width,
this.height,
this.url,
this.keys?.let { RoomEvent.Image.ImageMeta.Keys(it.k, it.iv, it.v, it.hashes) }
)
fun MatrixMessageMeta.engine() = when (this) {
MatrixMessageMeta.FromServer -> MessageMeta.FromServer
is MatrixMessageMeta.LocalEcho -> MessageMeta.LocalEcho(
this.echoId, when (val echo = this.state) {
is MatrixMessageMeta.LocalEcho.State.Error -> MessageMeta.LocalEcho.State.Error(
echo.message, when (echo.type) {
MatrixMessageMeta.LocalEcho.State.Error.Type.UNKNOWN -> MessageMeta.LocalEcho.State.Error.Type.UNKNOWN
}
)
MatrixMessageMeta.LocalEcho.State.Sending -> MessageMeta.LocalEcho.State.Sending
MatrixMessageMeta.LocalEcho.State.Sent -> MessageMeta.LocalEcho.State.Sent
}
)
}

View File

@ -0,0 +1,430 @@
package app.dapk.st.engine
import app.dapk.st.core.Base64
import app.dapk.st.core.BuildMeta
import app.dapk.st.core.CoroutineDispatchers
import app.dapk.st.core.SingletonFlows
import app.dapk.st.core.extensions.ErrorTracker
import app.dapk.st.matrix.MatrixClient
import app.dapk.st.matrix.MatrixTaskRunner
import app.dapk.st.matrix.auth.DeviceDisplayNameGenerator
import app.dapk.st.matrix.auth.authService
import app.dapk.st.matrix.auth.installAuthService
import app.dapk.st.matrix.common.*
import app.dapk.st.matrix.crypto.*
import app.dapk.st.matrix.device.KnownDeviceStore
import app.dapk.st.matrix.device.deviceService
import app.dapk.st.matrix.device.installEncryptionService
import app.dapk.st.matrix.http.ktor.KtorMatrixHttpClientFactory
import app.dapk.st.matrix.message.*
import app.dapk.st.matrix.message.internal.ImageContentReader
import app.dapk.st.matrix.push.installPushService
import app.dapk.st.matrix.push.pushService
import app.dapk.st.matrix.room.*
import app.dapk.st.matrix.sync.*
import app.dapk.st.matrix.sync.internal.request.ApiToDeviceEvent
import app.dapk.st.matrix.sync.internal.room.MessageDecrypter
import app.dapk.st.olm.DeviceKeyFactory
import app.dapk.st.olm.OlmStore
import app.dapk.st.olm.OlmWrapper
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import java.io.InputStream
import java.time.Clock
class MatrixEngine internal constructor(
private val directoryUseCase: Lazy<DirectoryUseCase>,
private val matrix: Lazy<MatrixClient>,
private val timelineUseCase: Lazy<ReadMarkingTimeline>,
private val sendMessageUseCase: Lazy<SendMessageUseCase>,
private val matrixMediaDecrypter: Lazy<MatrixMediaDecrypter>,
private val matrixPushHandler: Lazy<MatrixPushHandler>,
private val inviteUseCase: Lazy<InviteUseCase>,
private val notificationMessagesUseCase: Lazy<ObserveUnreadNotificationsUseCase>,
private val notificationInvitesUseCase: Lazy<ObserveInviteNotificationsUseCase>,
) : ChatEngine {
override fun directory() = directoryUseCase.value.state()
override fun invites() = inviteUseCase.value.invites()
override fun messages(roomId: RoomId, disableReadReceipts: Boolean): Flow<MessengerState> {
return timelineUseCase.value.fetch(roomId, isReadReceiptsDisabled = disableReadReceipts)
}
override fun notificationsMessages(): Flow<UnreadNotifications> {
return notificationMessagesUseCase.value.invoke()
}
override fun notificationsInvites(): Flow<InviteNotification> {
return notificationInvitesUseCase.value.invoke()
}
override suspend fun login(request: LoginRequest): LoginResult {
return matrix.value.authService().login(request.engine()).engine()
}
override suspend fun me(forceRefresh: Boolean): Me {
return matrix.value.profileService().me(forceRefresh).engine()
}
override suspend fun InputStream.importRoomKeys(password: String): Flow<ImportResult> {
return with(matrix.value.cryptoService()) {
importRoomKeys(password).map { it.engine() }.onEach {
when (it) {
is ImportResult.Error,
is ImportResult.Update -> {
// do nothing
}
is ImportResult.Success -> matrix.value.syncService().forceManualRefresh(it.roomIds)
}
}
}
}
override suspend fun send(message: SendMessage, room: RoomOverview) {
sendMessageUseCase.value.send(message, room)
}
override suspend fun registerPushToken(token: String, gatewayUrl: String) {
matrix.value.pushService().registerPush(token, gatewayUrl)
}
override suspend fun joinRoom(roomId: RoomId) {
matrix.value.roomService().joinRoom(roomId)
}
override suspend fun rejectJoinRoom(roomId: RoomId) {
matrix.value.roomService().rejectJoinRoom(roomId)
}
override suspend fun findMembersSummary(roomId: RoomId) = matrix.value.roomService().findMembersSummary(roomId)
override fun mediaDecrypter(): MediaDecrypter {
val mediaDecrypter = matrixMediaDecrypter.value
return object : MediaDecrypter {
override fun decrypt(input: InputStream, k: String, iv: String): MediaDecrypter.Collector {
return MediaDecrypter.Collector {
mediaDecrypter.decrypt(input, k, iv).collect(it)
}
}
}
}
override fun pushHandler() = matrixPushHandler.value
override suspend fun runTask(task: ChatEngineTask): TaskRunner.TaskResult {
return when (val result = matrix.value.run(MatrixTaskRunner.MatrixTask(task.type, task.jsonPayload))) {
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(result.canRetry)
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success
}
}
class Factory {
fun create(
base64: Base64,
buildMeta: BuildMeta,
logger: MatrixLogger,
nameGenerator: DeviceDisplayNameGenerator,
coroutineDispatchers: CoroutineDispatchers,
errorTracker: ErrorTracker,
imageContentReader: ImageContentReader,
backgroundScheduler: BackgroundScheduler,
memberStore: MemberStore,
roomStore: RoomStore,
profileStore: ProfileStore,
syncStore: SyncStore,
overviewStore: OverviewStore,
filterStore: FilterStore,
localEchoStore: LocalEchoStore,
credentialsStore: CredentialsStore,
knownDeviceStore: KnownDeviceStore,
olmStore: OlmStore,
): ChatEngine {
val lazyMatrix = lazy {
MatrixFactory.createMatrix(
base64,
buildMeta,
logger,
nameGenerator,
coroutineDispatchers,
errorTracker,
imageContentReader,
backgroundScheduler,
memberStore,
roomStore,
profileStore,
syncStore,
overviewStore,
filterStore,
localEchoStore,
credentialsStore,
knownDeviceStore,
olmStore
)
}
val directoryUseCase = unsafeLazy {
val matrix = lazyMatrix.value
DirectoryUseCase(
matrix.syncService(),
matrix.messageService(),
matrix.roomService(),
credentialsStore,
roomStore
)
}
val timelineUseCase = unsafeLazy {
val matrix = lazyMatrix.value
val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
val timeline = TimelineUseCaseImpl(matrix.syncService(), matrix.messageService(), matrix.roomService(), mergeWithLocalEchosUseCase)
ReadMarkingTimeline(roomStore, credentialsStore, timeline, matrix.roomService())
}
val sendMessageUseCase = unsafeLazy {
val matrix = lazyMatrix.value
SendMessageUseCase(matrix.messageService(), LocalIdFactory(), imageContentReader, Clock.systemUTC())
}
val mediaDecrypter = unsafeLazy { MatrixMediaDecrypter(base64) }
val pushHandler = unsafeLazy { MatrixPushHandler(backgroundScheduler, credentialsStore, lazyMatrix.value.syncService(), roomStore) }
val invitesUseCase = unsafeLazy { InviteUseCase(lazyMatrix.value.syncService()) }
return MatrixEngine(
directoryUseCase,
lazyMatrix,
timelineUseCase,
sendMessageUseCase,
mediaDecrypter,
pushHandler,
invitesUseCase,
unsafeLazy { ObserveUnreadNotificationsUseCaseImpl(roomStore) },
unsafeLazy { ObserveInviteNotificationsUseCaseImpl(overviewStore) },
)
}
}
}
object MatrixFactory {
fun createMatrix(
base64: Base64,
buildMeta: BuildMeta,
logger: MatrixLogger,
nameGenerator: DeviceDisplayNameGenerator,
coroutineDispatchers: CoroutineDispatchers,
errorTracker: ErrorTracker,
imageContentReader: ImageContentReader,
backgroundScheduler: BackgroundScheduler,
memberStore: MemberStore,
roomStore: RoomStore,
profileStore: ProfileStore,
syncStore: SyncStore,
overviewStore: OverviewStore,
filterStore: FilterStore,
localEchoStore: LocalEchoStore,
credentialsStore: CredentialsStore,
knownDeviceStore: KnownDeviceStore,
olmStore: OlmStore,
) = MatrixClient(
KtorMatrixHttpClientFactory(
credentialsStore,
includeLogging = buildMeta.isDebug,
),
logger
).also {
it.install {
installAuthService(credentialsStore, nameGenerator)
installEncryptionService(knownDeviceStore)
val singletonFlows = SingletonFlows(coroutineDispatchers)
val olm = OlmWrapper(
olmStore = olmStore,
singletonFlows = singletonFlows,
jsonCanonicalizer = JsonCanonicalizer(),
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
errorTracker = errorTracker,
logger = logger,
clock = Clock.systemUTC(),
coroutineDispatchers = coroutineDispatchers,
)
installCryptoService(
credentialsStore,
olm,
roomMembersProvider = { services ->
RoomMembersProvider {
services.roomService().joinedMembers(it).map { it.userId }
}
},
base64 = base64,
coroutineDispatchers = coroutineDispatchers,
)
installMessageService(
localEchoStore,
backgroundScheduler,
imageContentReader,
messageEncrypter = {
val cryptoService = it.cryptoService()
MessageEncrypter { message ->
val result = cryptoService.encrypt(
roomId = message.roomId,
credentials = credentialsStore.credentials()!!,
messageJson = message.contents,
)
MessageEncrypter.EncryptedMessagePayload(
result.algorithmName,
result.senderKey,
result.cipherText,
result.sessionId,
result.deviceId,
)
}
},
mediaEncrypter = {
val cryptoService = it.cryptoService()
MediaEncrypter { input ->
val result = cryptoService.encrypt(input)
MediaEncrypter.Result(
uri = result.uri,
contentLength = result.contentLength,
algorithm = result.algorithm,
ext = result.ext,
keyOperations = result.keyOperations,
kty = result.kty,
k = result.k,
iv = result.iv,
hashes = result.hashes,
v = result.v,
)
}
},
)
installRoomService(
memberStore,
roomMessenger = {
val messageService = it.messageService()
object : RoomMessenger {
override suspend fun enableEncryption(roomId: RoomId) {
messageService.sendEventMessage(
roomId, MessageService.EventMessage.Encryption(
algorithm = AlgorithmName("m.megolm.v1.aes-sha2")
)
)
}
}
},
roomInviteRemover = {
overviewStore.removeInvites(listOf(it))
}
)
installProfileService(profileStore, singletonFlows, credentialsStore)
installSyncService(
credentialsStore,
overviewStore,
roomStore,
syncStore,
filterStore,
deviceNotifier = { services ->
val encryption = services.deviceService()
val crypto = services.cryptoService()
DeviceNotifier { userIds, syncToken ->
encryption.updateStaleDevices(userIds)
crypto.updateOlmSession(userIds, syncToken)
}
},
messageDecrypter = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
MessageDecrypter {
cryptoService.decrypt(it)
}
},
keySharer = { serviceProvider ->
val cryptoService = serviceProvider.cryptoService()
KeySharer { sharedRoomKeys ->
cryptoService.importRoomKeys(sharedRoomKeys)
}
},
verificationHandler = { services ->
val cryptoService = services.cryptoService()
VerificationHandler { apiEvent ->
logger.matrixLog(MatrixLogTag.VERIFICATION, "got a verification request $it")
cryptoService.onVerificationEvent(
when (apiEvent) {
is ApiToDeviceEvent.VerificationRequest -> Verification.Event.Requested(
apiEvent.sender,
apiEvent.content.fromDevice,
apiEvent.content.transactionId,
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,
apiEvent.content.method,
apiEvent.content.protocols,
apiEvent.content.hashes,
apiEvent.content.codes,
apiEvent.content.short,
apiEvent.content.transactionId,
)
is ApiToDeviceEvent.VerificationCancel -> TODO()
is ApiToDeviceEvent.VerificationAccept -> 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,
apiEvent.content.keys,
apiEvent.content.mac,
)
}
)
}
},
oneTimeKeyProducer = { services ->
val cryptoService = services.cryptoService()
MaybeCreateMoreKeys {
cryptoService.maybeCreateMoreKeys(it)
}
},
roomMembersService = { services ->
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)
}
},
errorTracker = errorTracker,
coroutineDispatchers = coroutineDispatchers,
)
installPushService(credentialsStore)
}
}
}
fun <T> unsafeLazy(initializer: () -> T): Lazy<T> = lazy(mode = LazyThreadSafetyMode.NONE, initializer = initializer)

View File

@ -1,36 +1,34 @@
package app.dapk.st.notifications package app.dapk.st.engine
import app.dapk.st.core.AppLogTag import app.dapk.st.core.AppLogTag
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.matrix.common.CredentialsStore import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.JsonString
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.message.BackgroundScheduler
import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.RoomStore
import app.dapk.st.matrix.sync.SyncService import app.dapk.st.matrix.sync.SyncService
import app.dapk.st.push.PushHandler
import app.dapk.st.push.PushTokenPayload
import app.dapk.st.work.WorkScheduler
import kotlinx.coroutines.* import kotlinx.coroutines.*
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import kotlinx.serialization.json.Json
private var previousJob: Job? = null private var previousJob: Job? = null
@OptIn(DelicateCoroutinesApi::class) @OptIn(DelicateCoroutinesApi::class)
class MatrixPushHandler( class MatrixPushHandler(
private val workScheduler: WorkScheduler, private val backgroundScheduler: BackgroundScheduler,
private val credentialsStore: CredentialsStore, private val credentialsStore: CredentialsStore,
private val syncService: SyncService, private val syncService: SyncService,
private val roomStore: RoomStore, private val roomStore: RoomStore,
) : PushHandler { ) : PushHandler {
override fun onNewToken(payload: PushTokenPayload) { override fun onNewToken(payload: JsonString) {
log(AppLogTag.PUSH, "new push token received") log(AppLogTag.PUSH, "new push token received")
workScheduler.schedule( backgroundScheduler.schedule(
WorkScheduler.WorkTask( key = "2",
task = BackgroundScheduler.Task(
type = "push_token", type = "push_token",
jobId = 2, jsonPayload = payload
jsonPayload = Json.encodeToString(PushTokenPayload.serializer(), payload)
) )
) )
} }
@ -66,7 +64,7 @@ class MatrixPushHandler(
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? { private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
return withTimeoutOrNull(timeout) { return withTimeoutOrNull(timeout) {
combine(syncService.startSyncing().startInstantly(), syncService.observeEvent(eventId)) { _, event -> event } combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event }
.firstOrNull { .firstOrNull {
it == eventId it == eventId
} }
@ -75,11 +73,9 @@ class MatrixPushHandler(
private suspend fun waitForUnreadChange(timeout: Long): String? { private suspend fun waitForUnreadChange(timeout: Long): String? {
return withTimeoutOrNull(timeout) { return withTimeoutOrNull(timeout) {
combine(syncService.startSyncing().startInstantly(), roomStore.observeUnread()) { _, unread -> unread } combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread }
.first() .first()
"ignored" "ignored"
} }
} }
}
private fun Flow<Unit>.startInstantly() = this.onStart { emit(Unit) }
}

View File

@ -1,10 +1,8 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
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.RoomState
internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState internal typealias MergeWithLocalEchosUseCase = (RoomState, RoomMember, List<MessageService.LocalEcho>) -> RoomState

View File

@ -1,17 +1,16 @@
package app.dapk.st.notifications package app.dapk.st.engine
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.InviteMeta import app.dapk.st.matrix.sync.InviteMeta
import app.dapk.st.matrix.sync.OverviewStore import app.dapk.st.matrix.sync.OverviewStore
import app.dapk.st.matrix.sync.RoomInvite import app.dapk.st.matrix.sync.RoomInvite
import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.FlowPreview
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow<InviteNotification> internal typealias ObserveInviteNotificationsUseCase = () -> Flow<InviteNotification>
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase { class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
override suspend fun invoke(): Flow<InviteNotification> { override fun invoke(): Flow<InviteNotification> {
return overviewStore.latestInvites() return overviewStore.latestInvites()
.diff() .diff()
.drop(1) .drop(1)
@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items -> private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
flow { items.forEach { this.emit(it) } } flow { items.forEach { this.emit(it) } }
} }
data class InviteNotification(
val content: String,
val roomId: RoomId
)

View File

@ -1,4 +1,4 @@
package app.dapk.st.notifications package app.dapk.st.engine
import app.dapk.st.core.AppLogTag import app.dapk.st.core.AppLogTag
import app.dapk.st.core.extensions.clearAndPutAll import app.dapk.st.core.extensions.clearAndPutAll
@ -6,17 +6,16 @@ import app.dapk.st.core.extensions.containsKey
import app.dapk.st.core.log import app.dapk.st.core.log
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import app.dapk.st.matrix.sync.RoomStore import app.dapk.st.matrix.sync.RoomStore
import kotlinx.coroutines.flow.* import kotlinx.coroutines.flow.*
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
typealias UnreadNotifications = Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff> internal typealias ObserveUnreadNotificationsUseCase = () -> Flow<UnreadNotifications>
internal typealias ObserveUnreadNotificationsUseCase = suspend () -> Flow<UnreadNotifications>
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase { class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
override suspend fun invoke(): Flow<UnreadNotifications> { override fun invoke(): Flow<UnreadNotifications> {
return roomStore.observeUnread() return roomStore.observeUnread()
.mapWithDiff() .mapWithDiff()
.avoidShowingPreviousNotificationsOnLaunch() .avoidShowingPreviousNotificationsOnLaunch()
@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) :
} }
private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifications> { private fun Flow<Map<MatrixRoomOverview, List<MatrixRoomEvent>>>.mapWithDiff(): Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>> {
val inferredCurrentNotifications = mutableMapOf<RoomId, List<RoomEvent>>()
return this
.filter { (_, diff) ->
when {
diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
false
}
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
false
}
else -> true
}
}
.onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) }
}
private fun Flow<Map<RoomOverview, List<RoomEvent>>>.mapWithDiff(): Flow<Pair<Map<RoomOverview, List<RoomEvent>>, NotificationDiff>> {
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>() val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
return this.map { each -> return this.map { each ->
val allUnreadIds = each.toTimestampedIds() val allUnreadIds = each.toTimestampedIds()
@ -83,19 +61,39 @@ private fun Map<RoomId, List<TimestampedEventId>>?.toLatestTimestamps() = this?.
private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } } private fun Map<RoomId, List<TimestampedEventId>>.toEventIds() = this.mapValues { it.value.map { it.first } }
private fun Map<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.toTimestampedIds() = this
.mapValues { it.value.toEventIds() } .mapValues { it.value.toEventIds() }
.mapKeys { it.key.roomId } .mapKeys { it.key.roomId }
private fun List<RoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp } private fun List<MatrixRoomEvent>.toEventIds() = this.map { it.eventId to it.utcTimestamp }
private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1) private fun <T> Flow<T>.avoidShowingPreviousNotificationsOnLaunch() = drop(1)
data class NotificationDiff( private fun Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>>.onlyRenderableChanges(): Flow<UnreadNotifications> {
val unchanged: Map<RoomId, List<EventId>>, val inferredCurrentNotifications = mutableMapOf<RoomId, List<MatrixRoomEvent>>()
val changedOrNew: Map<RoomId, List<EventId>>, return this
val removed: Map<RoomId, List<EventId>>, .filter { (_, diff) ->
val newRooms: Set<RoomId> when {
) diff.changedOrNew.isEmpty() && diff.removed.isEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no renderable changes")
false
}
inferredCurrentNotifications.isEmpty() && diff.removed.isNotEmpty() -> {
log(AppLogTag.NOTIFICATION, "Ignoring unread change due to no currently showing messages and changes are all messages marked as read")
false
}
else -> true
}
}
.onEach { (allUnread, _) -> inferredCurrentNotifications.clearAndPutAll(allUnread.mapKeys { it.key.roomId }) }
.map {
val engineModels = it.first
.mapKeys { it.key.engine() }
.mapValues { it.value.map { it.engine() } }
engineModels to it.second
}
}
typealias TimestampedEventId = Pair<EventId, Long> typealias TimestampedEventId = Pair<EventId, Long>

View File

@ -0,0 +1,57 @@
package app.dapk.st.engine
import app.dapk.st.matrix.common.CredentialsStore
import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomStore
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.*
class ReadMarkingTimeline(
private val roomStore: RoomStore,
private val credentialsStore: CredentialsStore,
private val observeTimelineUseCase: ObserveTimelineUseCase,
private val roomService: RoomService,
) {
fun fetch(roomId: RoomId, isReadReceiptsDisabled: Boolean): Flow<MessengerState> {
return flow {
val credentials = credentialsStore.credentials()!!
roomStore.markRead(roomId)
emit(credentials)
}.flatMapMerge { credentials ->
var lastKnownReadEvent: EventId? = null
observeTimelineUseCase.invoke(roomId, credentials.userId).distinctUntilChanged().onEach { state ->
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
if (lastKnownReadEvent != it) {
updateRoomReadStateAsync(latestReadEvent = it, state, isReadReceiptsDisabled)
lastKnownReadEvent = it
}
}
}
}
}
private suspend fun updateRoomReadStateAsync(latestReadEvent: EventId, state: MessengerState, isReadReceiptsDisabled: Boolean): Deferred<*> {
return coroutineScope {
async {
runCatching {
roomService.markFullyRead(state.roomState.roomOverview.roomId, latestReadEvent, isPrivate = isReadReceiptsDisabled)
roomStore.markRead(state.roomState.roomOverview.roomId)
}
}
}
}
}
private fun MessengerState.latestMessageEventFromOthers(self: UserId) = this.roomState.events
.filterIsInstance<RoomEvent.Message>()
.filterNot { it.author.id == self }
.firstOrNull()
?.eventId

View File

@ -0,0 +1,60 @@
package app.dapk.st.engine
import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.message.internal.ImageContentReader
import java.time.Clock
internal class SendMessageUseCase(
private val messageService: MessageService,
private val localIdFactory: LocalIdFactory,
private val imageContentReader: ImageContentReader,
private val clock: Clock,
) {
suspend fun send(message: SendMessage, room: RoomOverview) {
when (message) {
is SendMessage.ImageMessage -> createImageMessage(message, room)
is SendMessage.TextMessage -> messageService.scheduleMessage(createTextMessage(message, room))
}
}
private suspend fun createImageMessage(message: SendMessage.ImageMessage, room: RoomOverview) {
val meta = imageContentReader.meta(message.uri)
messageService.scheduleMessage(
MessageService.Message.ImageMessage(
MessageService.Message.Content.ImageContent(
uri = message.uri,
MessageService.Message.Content.ImageContent.Meta(
height = meta.height,
width = meta.width,
size = meta.size,
fileName = meta.fileName,
mimeType = meta.mimeType,
)
),
roomId = room.roomId,
sendEncrypted = room.isEncrypted,
localId = localIdFactory.create(),
timestampUtc = clock.millis(),
)
)
}
private fun createTextMessage(message: SendMessage.TextMessage, room: RoomOverview) = MessageService.Message.TextMessage(
content = MessageService.Message.Content.TextContent(message.content),
roomId = room.roomId,
sendEncrypted = room.isEncrypted,
localId = localIdFactory.create(),
timestampUtc = clock.millis(),
reply = message.reply?.let {
MessageService.Message.TextMessage.Reply(
author = it.author,
originalMessage = it.originalMessage,
replyContent = message.content,
eventId = it.eventId,
timestampUtc = it.timestampUtc,
)
}
)
}

View File

@ -1,15 +1,14 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.core.extensions.startAndIgnoreEmissions
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.SyncService import app.dapk.st.matrix.sync.SyncService
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState> internal typealias ObserveTimelineUseCase = (RoomId, UserId) -> Flow<MessengerState>
@ -37,22 +36,16 @@ internal class TimelineUseCaseImpl(
) )
} }
}, },
typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }, typing = events.filterIsInstance<SyncService.SyncEvent.Typing>().firstOrNull { it.roomId == roomId }?.engine(),
self = userId, self = userId,
) )
} }
} }
private fun roomDatasource(roomId: RoomId) = combine( private fun roomDatasource(roomId: RoomId) = combine(
syncService.startSyncing().startAndIgnoreEmissions(), syncService.startSyncing(),
syncService.room(roomId) syncService.room(roomId).map { it.engine() }
) { _, room -> room } ) { _, room -> room }
} }
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null) private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
data class MessengerState(
val self: UserId,
val roomState: RoomState,
val typing: SyncService.SyncEvent.Typing?
)

View File

@ -1,10 +1,10 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId import app.dapk.st.matrix.common.EventId
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.sync.MessageMeta import app.dapk.st.matrix.sync.MessageMeta
import fake.FakeMetaMapper
import fixture.* import fixture.*
import internalfake.FakeMetaMapper
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
@ -27,7 +27,7 @@ class LocalEchoMapperTest {
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aRoomMessageEvent(
eventId = echo.eventId!!, eventId = echo.eventId!!,
content = AN_ECHO_CONTENT.content.body, content = AN_ECHO_CONTENT.content.body,
meta = A_META meta = A_META.engine()
) )
} }
@ -40,25 +40,25 @@ class LocalEchoMapperTest {
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aRoomMessageEvent(
eventId = anEventId(echo.localId), eventId = anEventId(echo.localId),
content = AN_ECHO_CONTENT.content.body, content = AN_ECHO_CONTENT.content.body,
meta = A_META meta = A_META.engine()
) )
} }
@Test @Test
fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) { fun `when merging with echo then updates meta with the echos meta`() = runWith(localEchoMapper) {
val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending) val previousMeta = MessageMeta.LocalEcho("previous", MessageMeta.LocalEcho.State.Sending).engine()
val event = aRoomMessageEvent(meta = previousMeta) val event = aRoomMessageEvent(meta = previousMeta)
val echo = aLocalEcho() val echo = aLocalEcho()
fakeMetaMapper.given(echo).returns(A_META) fakeMetaMapper.given(echo).returns(A_META.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
val result = event.mergeWith(echo) val result = event.mergeWith(echo)
result shouldBeEqualTo aRoomMessageEvent(meta = A_META) result shouldBeEqualTo aRoomMessageEvent(meta = A_META.engine())
} }
private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho { private fun givenEcho(eventId: EventId? = null, localId: String = "", meta: MessageMeta.LocalEcho = A_META): MessageService.LocalEcho {
return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also { return aLocalEcho(eventId = eventId, message = aTextMessage(localId = localId)).also {
fakeMetaMapper.given(it).returns(meta) fakeMetaMapper.given(it).returns(meta.engine() as app.dapk.st.engine.MessageMeta.LocalEcho)
} }
} }
} }

View File

@ -1,11 +1,8 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.matrix.common.EventId 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.message.MessageService import app.dapk.st.matrix.message.MessageService
import fixture.* import fixture.*
import internalfake.FakeLocalEventMapper
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
@ -18,7 +15,7 @@ private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anE
class MergeWithLocalEchosUseCaseTest { class MergeWithLocalEchosUseCaseTest {
private val fakeLocalEchoMapper = FakeLocalEventMapper() private val fakeLocalEchoMapper = fake.FakeLocalEventMapper()
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance) private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
@Test @Test
@ -60,4 +57,4 @@ class MergeWithLocalEchosUseCaseTest {
aTextMessage(aTextContent(body)), aTextMessage(aTextContent(body)),
state, state,
) )
} }

View File

@ -1,7 +1,6 @@
package app.dapk.st.messenger package app.dapk.st.engine
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.sync.MessageMeta
import fixture.aLocalEcho import fixture.aLocalEcho
import fixture.aTextMessage import fixture.aTextMessage
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo

View File

@ -1,24 +1,27 @@
package app.dapk.st.notifications package app.dapk.st.engine
import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomOverview
import fake.FakeRoomStore import fake.FakeRoomStore
import fixture.NotificationDiffFixtures.aNotificationDiff import fixture.NotificationDiffFixtures.aNotificationDiff
import fixture.aMatrixRoomMessageEvent
import fixture.aMatrixRoomOverview
import fixture.aRoomId import fixture.aRoomId
import fixture.aRoomMessageEvent
import fixture.aRoomOverview
import fixture.anEventId import fixture.anEventId
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.toList import kotlinx.coroutines.flow.toList
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
import app.dapk.st.matrix.sync.RoomEvent as MatrixRoomEvent
import app.dapk.st.matrix.sync.RoomOverview as MatrixRoomOverview
private val NO_UNREADS = emptyMap<RoomOverview, List<RoomEvent>>() private val NO_UNREADS = emptyMap<MatrixRoomOverview, List<MatrixRoomEvent>>()
private val A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000) private val A_MESSAGE = aMatrixRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000) private val A_MESSAGE_2 = aMatrixRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1")) private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1"))
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2")) private val A_ROOM_OVERVIEW_2 = aMatrixRoomOverview(roomId = aRoomId("2"))
private fun MatrixRoomOverview.withUnreads(vararg events: MatrixRoomEvent) = mapOf(this to events.toList())
private fun MatrixRoomOverview.toDiff(vararg events: MatrixRoomEvent) = mapOf(this.roomId to events.map { it.eventId })
class ObserveUnreadRenderNotificationsUseCaseTest { class ObserveUnreadRenderNotificationsUseCaseTest {
@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) newRooms = setOf(A_ROOM_OVERVIEW.roomId)
) )
@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) newRooms = setOf(A_ROOM_OVERVIEW.roomId)
), ),
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
) )
} }
@ -64,7 +67,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2) to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2)) A_ROOM_OVERVIEW.withUnreads(A_MESSAGE, A_MESSAGE_2).engine() to aNotificationDiff(changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE_2))
) )
} }
@ -92,7 +95,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
val result = useCase.invoke().toList() val result = useCase.invoke().toList()
result shouldBeEqualTo listOf( result shouldBeEqualTo listOf(
A_ROOM_OVERVIEW.withUnreads(A_MESSAGE) to aNotificationDiff( A_ROOM_OVERVIEW.withUnreads(A_MESSAGE).engine() to aNotificationDiff(
changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE), changedOrNew = A_ROOM_OVERVIEW.toDiff(A_MESSAGE),
newRooms = setOf(A_ROOM_OVERVIEW.roomId) newRooms = setOf(A_ROOM_OVERVIEW.roomId)
), ),
@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
result shouldBeEqualTo emptyList() result shouldBeEqualTo emptyList()
} }
private fun givenNoInitialUnreads(vararg unreads: Map<RoomOverview, List<RoomEvent>>) = fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads)) private fun givenNoInitialUnreads(vararg unreads: Map<MatrixRoomOverview, List<MatrixRoomEvent>>) =
fakeRoomStore.givenUnreadEvents(flowOf(NO_UNREADS, *unreads))
} }
private fun RoomOverview.withUnreads(vararg events: RoomEvent) = mapOf(this to events.toList()) private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.engine() = this
private fun RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId }) .mapKeys { it.key.engine() }
.mapValues { it.value.map { it.engine() } }

View File

@ -1,13 +1,15 @@
package app.dapk.st.messenger package app.dapk.st.engine
import FlowTestObserver
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.common.UserId
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.matrix.room.RoomService
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import app.dapk.st.matrix.sync.SyncService import app.dapk.st.matrix.sync.SyncService
import fake.FakeSyncService import fake.FakeSyncService
import fixture.* import fixture.*
import io.mockk.coEvery
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
@ -16,12 +18,13 @@ import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Test import org.junit.Test
import test.FlowTestObserver
import test.delegateReturn import test.delegateReturn
private val A_ROOM_ID = aRoomId() private val A_ROOM_ID = aRoomId()
private val AN_USER_ID = aUserId() private val AN_USER_ID = aUserId()
private val A_ROOM_STATE = aRoomState() private val A_ROOM_STATE = aMatrixRoomState()
private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aRoomMessageEvent(content = "a merged event"))) private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aMatrixRoomMessageEvent(content = "a merged event")))
private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho()) private val A_LOCAL_ECHOS_LIST = listOf(aLocalEcho())
private val A_ROOM_MEMBER = aRoomMember() private val A_ROOM_MEMBER = aRoomMember()
@ -47,7 +50,7 @@ class TimelineUseCaseTest {
.test(this) .test(this)
.assertValues( .assertValues(
listOf( listOf(
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE) aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE.engine())
) )
) )
} }
@ -57,13 +60,13 @@ class TimelineUseCaseTest {
givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST) givenSyncEmission(roomState = A_ROOM_STATE, echos = A_LOCAL_ECHOS_LIST)
fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER) fakeRoomService.givenFindMember(A_ROOM_ID, AN_USER_ID).returns(A_ROOM_MEMBER)
fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE) fakeMergeWithLocalEchosUseCase.givenMerging(A_ROOM_STATE, A_ROOM_MEMBER, A_LOCAL_ECHOS_LIST).returns(A_MERGED_ROOM_STATE.engine())
timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID) timelineUseCase.invoke(A_ROOM_ID, AN_USER_ID)
.test(this) .test(this)
.assertValues( .assertValues(
listOf( listOf(
aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE) aMessengerState(self = AN_USER_ID, roomState = A_MERGED_ROOM_STATE.engine())
) )
) )
} }
@ -81,7 +84,11 @@ class TimelineUseCaseTest {
.test(this) .test(this)
.assertValues( .assertValues(
listOf( listOf(
aMessengerState(self = AN_USER_ID, roomState = A_ROOM_STATE, typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER))) aMessengerState(
self = AN_USER_ID,
roomState = A_ROOM_STATE.engine(),
typing = aTypingSyncEvent(A_ROOM_ID, members = listOf(A_ROOM_MEMBER)).engine()
)
) )
) )
} }
@ -104,11 +111,27 @@ suspend fun <T> Flow<T>.test(scope: CoroutineScope) = FlowTestObserver(scope, th
class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() { class FakeMergeWithLocalEchosUseCase : MergeWithLocalEchosUseCase by mockk() {
fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List<MessageService.LocalEcho>) = every { fun givenMerging(roomState: RoomState, roomMember: RoomMember, echos: List<MessageService.LocalEcho>) = every {
this@FakeMergeWithLocalEchosUseCase.invoke(roomState, roomMember, echos) this@FakeMergeWithLocalEchosUseCase.invoke(roomState.engine(), roomMember, echos)
}.delegateReturn() }.delegateReturn()
} }
fun aTypingSyncEvent( fun aTypingSyncEvent(
roomId: RoomId = aRoomId(), roomId: RoomId = aRoomId(),
members: List<RoomMember> = listOf(aRoomMember()) members: List<RoomMember> = listOf(aRoomMember())
) = SyncService.SyncEvent.Typing(roomId, members) ) = SyncService.SyncEvent.Typing(roomId, members)
class FakeMessageService : MessageService by mockk() {
fun givenEchos(roomId: RoomId) = every { localEchos(roomId) }.delegateReturn()
}
class FakeRoomService : RoomService by mockk() {
fun givenFindMember(roomId: RoomId, userId: UserId) = coEvery { findMember(roomId, userId) }.delegateReturn()
}
fun aMessengerState(
self: UserId = aUserId(),
roomState: app.dapk.st.engine.RoomState,
typing: Typing? = null
) = MessengerState(self, roomState, typing)

View File

@ -0,0 +1,11 @@
package fake
import app.dapk.st.engine.DirectoryUseCase
import io.mockk.every
import io.mockk.mockk
import test.delegateReturn
internal class FakeDirectoryUseCase {
val instance = mockk<DirectoryUseCase>()
fun given() = every { instance.state() }.delegateReturn()
}

View File

@ -1,8 +1,8 @@
package internalfake package fake
import app.dapk.st.engine.LocalEchoMapper
import app.dapk.st.matrix.common.RoomMember import app.dapk.st.matrix.common.RoomMember
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.messenger.LocalEchoMapper
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk

View File

@ -1,6 +1,6 @@
package internalfake package fake
import app.dapk.st.messenger.LocalIdFactory import app.dapk.st.engine.LocalIdFactory
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import test.delegateReturn import test.delegateReturn

View File

@ -1,7 +1,7 @@
package internalfake package fake
import app.dapk.st.engine.MetaMapper
import app.dapk.st.matrix.message.MessageService import app.dapk.st.matrix.message.MessageService
import app.dapk.st.messenger.MetaMapper
import io.mockk.every import io.mockk.every
import io.mockk.mockk import io.mockk.mockk
import test.delegateReturn import test.delegateReturn

View File

@ -1,6 +1,6 @@
package fake package fake
import app.dapk.st.notifications.ObserveInviteNotificationsUseCase import app.dapk.st.engine.ObserveInviteNotificationsUseCase
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import test.delegateEmit import test.delegateEmit

View File

@ -1,6 +1,6 @@
package fake package fake
import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase import app.dapk.st.engine.ObserveUnreadNotificationsUseCase
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.mockk import io.mockk.mockk
import test.delegateEmit import test.delegateEmit

View File

@ -12,7 +12,7 @@ private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
private const val SECRET_KEY_SPEC_ALGORITHM = "AES" private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256" private const val MESSAGE_DIGEST_ALGORITHM = "SHA-256"
class MediaDecrypter(private val base64: Base64) { class MatrixMediaDecrypter(private val base64: Base64) {
fun decrypt(input: InputStream, k: String, iv: String): Collector { fun decrypt(input: InputStream, k: String, iv: String): Collector {
val key = base64.decode(k.replace('-', '+').replace('_', '/')) val key = base64.decode(k.replace('-', '+').replace('_', '/'))

View File

@ -1,9 +1,11 @@
package app.dapk.st.matrix.message package app.dapk.st.matrix.message
import app.dapk.st.matrix.common.JsonString
interface BackgroundScheduler { interface BackgroundScheduler {
fun schedule(key: String, task: Task) fun schedule(key: String, task: Task)
data class Task(val type: String, val jsonPayload: String) data class Task(val type: String, val jsonPayload: JsonString)
} }

View File

@ -1,6 +1,7 @@
package app.dapk.st.matrix.message.internal package app.dapk.st.matrix.message.internal
import app.dapk.st.matrix.MatrixTaskRunner import app.dapk.st.matrix.MatrixTaskRunner
import app.dapk.st.matrix.common.JsonString
import app.dapk.st.matrix.common.RoomId import app.dapk.st.matrix.common.RoomId
import app.dapk.st.matrix.http.MatrixHttpClient import app.dapk.st.matrix.http.MatrixHttpClient
import app.dapk.st.matrix.message.* import app.dapk.st.matrix.message.*
@ -69,13 +70,13 @@ internal class DefaultMessageService(
is MessageService.Message.TextMessage -> { is MessageService.Message.TextMessage -> {
BackgroundScheduler.Task( BackgroundScheduler.Task(
type = MATRIX_MESSAGE_TASK_TYPE, type = MATRIX_MESSAGE_TASK_TYPE,
Json.encodeToString(MessageService.Message.TextMessage.serializer(), this) JsonString(Json.encodeToString(MessageService.Message.TextMessage.serializer(), this))
) )
} }
is MessageService.Message.ImageMessage -> BackgroundScheduler.Task( is MessageService.Message.ImageMessage -> BackgroundScheduler.Task(
type = MATRIX_IMAGE_MESSAGE_TASK_TYPE, type = MATRIX_IMAGE_MESSAGE_TASK_TYPE,
Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this) JsonString(Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this))
) )
} }
} }

View File

@ -18,10 +18,15 @@ interface SyncService : MatrixService {
fun invites(): Flow<InviteState> fun invites(): Flow<InviteState>
fun overview(): Flow<OverviewState> fun overview(): Flow<OverviewState>
fun room(roomId: RoomId): Flow<RoomState> fun room(roomId: RoomId): Flow<RoomState>
/**
* Subscribe to keep the background syncing alive
* Emits once, either when the initial sync completes or immediately if has already sync'd once
*/
fun startSyncing(): Flow<Unit> fun startSyncing(): Flow<Unit>
fun events(roomId: RoomId? = null): Flow<List<SyncEvent>> fun events(roomId: RoomId? = null): Flow<List<SyncEvent>>
suspend fun observeEvent(eventId: EventId): Flow<EventId> suspend fun observeEvent(eventId: EventId): Flow<EventId>
suspend fun forceManualRefresh(roomIds: List<RoomId>) suspend fun forceManualRefresh(roomIds: Set<RoomId>)
@JvmInline @JvmInline
value class FilterId(val value: String) value class FilterId(val value: String)
@ -31,6 +36,7 @@ interface SyncService : MatrixService {
data class Typing(override val roomId: RoomId, val members: List<RoomMember>) : SyncEvent data class Typing(override val roomId: RoomId, val members: List<RoomMember>) : SyncEvent
} }
} }
fun MatrixServiceInstaller.installSyncService( fun MatrixServiceInstaller.installSyncService(

View File

@ -24,7 +24,7 @@ private val syncSubscriptionCount = AtomicInteger()
internal class DefaultSyncService( internal class DefaultSyncService(
httpClient: MatrixHttpClient, httpClient: MatrixHttpClient,
syncStore: SyncStore, private val syncStore: SyncStore,
private val overviewStore: OverviewStore, private val overviewStore: OverviewStore,
private val roomStore: RoomStore, private val roomStore: RoomStore,
filterStore: FilterStore, filterStore: FilterStore,
@ -104,13 +104,24 @@ internal class DefaultSyncService(
} }
} }
override fun startSyncing() = syncFlow override fun startSyncing(): Flow<Unit> {
return flow { emit(syncStore.read(SyncStore.SyncKey.Overview) != null) }.flatMapMerge { hasSynced ->
when (hasSynced) {
true -> syncFlow.filter { false }.onStart { emit(Unit) }
false -> {
var counter = 0
syncFlow.filter { counter < 1 }.onEach { counter++ }
}
}
}
}
override fun invites() = overviewStore.latestInvites() override fun invites() = overviewStore.latestInvites()
override fun overview() = overviewStore.latest() override fun overview() = overviewStore.latest()
override fun room(roomId: RoomId) = roomStore.latest(roomId) override fun room(roomId: RoomId) = roomStore.latest(roomId)
override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow override fun events(roomId: RoomId?) = roomId?.let { syncEventsFlow.map { it.filter { it.roomId == roomId } }.distinctUntilChanged() } ?: syncEventsFlow
override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId) override suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
override suspend fun forceManualRefresh(roomIds: List<RoomId>) { override suspend fun forceManualRefresh(roomIds: Set<RoomId>) {
coroutineDispatchers.withIoContext { coroutineDispatchers.withIoContext {
roomIds.map { roomIds.map {
async { async {

View File

@ -15,7 +15,7 @@ import org.junit.Test
private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content" private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content"
private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1() private val AN_ENCRYPTED_ROOM_CONTENT = aMegolmV1()
private val AN_ENCRYPTED_ROOM_MESSAGE = aRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT) private val AN_ENCRYPTED_ROOM_MESSAGE = aMatrixRoomMessageEvent(encryptedContent = AN_ENCRYPTED_ROOM_CONTENT)
private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent( private val AN_ENCRYPTED_ROOM_REPLY = aRoomReplyMessageEvent(
message = AN_ENCRYPTED_ROOM_MESSAGE, message = AN_ENCRYPTED_ROOM_MESSAGE,
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event")) replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event"))
@ -37,7 +37,7 @@ class RoomEventsDecrypterTest {
@Test @Test
fun `given clear message event, when decrypting, then does nothing`() = runTest { fun `given clear message event, when decrypting, then does nothing`() = runTest {
val aClearMessageEvent = aRoomMessageEvent(encryptedContent = null) val aClearMessageEvent = aMatrixRoomMessageEvent(encryptedContent = null)
val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent)) val result = roomEventsDecrypter.decryptRoomEvents(A_USER_CREDENTIALS, listOf(aClearMessageEvent))
result shouldBeEqualTo listOf(aClearMessageEvent) result shouldBeEqualTo listOf(aClearMessageEvent)

View File

@ -1,7 +1,7 @@
package app.dapk.st.matrix.sync.internal.sync package app.dapk.st.matrix.sync.internal.sync
import fake.FakeRoomStore import fake.FakeRoomStore
import fixture.aRoomMessageEvent import fixture.aMatrixRoomMessageEvent
import fixture.anEventId import fixture.anEventId
import internalfixture.aTimelineTextEventContent import internalfixture.aTimelineTextEventContent
import internalfixture.anApiTimelineTextEvent import internalfixture.anApiTimelineTextEvent
@ -11,8 +11,8 @@ import org.junit.Test
private val AN_EVENT_ID = anEventId() private val AN_EVENT_ID = anEventId()
private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event")) private val A_TIMELINE_EVENT = anApiTimelineTextEvent(AN_EVENT_ID, content = aTimelineTextEventContent(body = "timeline event"))
private val A_ROOM_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "previous room event") private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "previous room event")
private val A_PERSISTED_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "persisted event") private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
class EventLookupUseCaseTest { class EventLookupUseCaseTest {

View File

@ -5,7 +5,6 @@ import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent import app.dapk.st.matrix.sync.internal.request.ApiEncryptedContent
import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent import app.dapk.st.matrix.sync.internal.request.ApiTimelineEvent
import fake.FakeErrorTracker import fake.FakeErrorTracker
import fake.FakeMatrixLogger
import fake.FakeRoomMembersService import fake.FakeRoomMembersService
import fixture.* import fixture.*
import internalfixture.* import internalfixture.*
@ -41,7 +40,7 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) } val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = megolmEvent.eventId, eventId = megolmEvent.eventId,
utcTimestamp = megolmEvent.utcTimestamp, utcTimestamp = megolmEvent.utcTimestamp,
content = "Encrypted message", content = "Encrypted message",
@ -74,7 +73,7 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } val result = with(roomEventCreator) { A_TEXT_EVENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = A_TEXT_EVENT.id, eventId = A_TEXT_EVENT.id,
utcTimestamp = A_TEXT_EVENT.utcTimestamp, utcTimestamp = A_TEXT_EVENT.utcTimestamp,
content = A_TEXT_EVENT_MESSAGE, content = A_TEXT_EVENT_MESSAGE,
@ -88,7 +87,7 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } val result = with(roomEventCreator) { A_TEXT_EVENT_WITHOUT_CONTENT.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id, eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp, utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
content = "redacted", content = "redacted",
@ -103,7 +102,7 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = editEvent.id, eventId = editEvent.id,
utcTimestamp = editEvent.utcTimestamp, utcTimestamp = editEvent.utcTimestamp,
content = editEvent.asTextContent().body!!, content = editEvent.asTextContent().body!!,
@ -121,7 +120,7 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = originalMessage.id, eventId = originalMessage.id,
utcTimestamp = editedMessage.utcTimestamp, utcTimestamp = editedMessage.utcTimestamp,
content = A_TEXT_EVENT_MESSAGE, content = A_TEXT_EVENT_MESSAGE,
@ -133,13 +132,13 @@ internal class RoomEventCreatorTest {
@Test @Test
fun `given edited event which relates to a room event then updates existing message`() = runTest { fun `given edited event which relates to a room event then updates existing message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomMessageEvent() val originalMessage = aMatrixRoomMessageEvent()
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) val lookup = givenLookup(originalMessage)
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = originalMessage.eventId, eventId = originalMessage.eventId,
utcTimestamp = editedMessage.utcTimestamp, utcTimestamp = editedMessage.utcTimestamp,
content = A_TEXT_EVENT_MESSAGE, content = A_TEXT_EVENT_MESSAGE,
@ -151,7 +150,7 @@ internal class RoomEventCreatorTest {
@Test @Test
fun `given edited event which relates to a room reply event then only updates message`() = runTest { fun `given edited event which relates to a room reply event then only updates message`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomReplyMessageEvent(message = aRoomMessageEvent()) val originalMessage = aRoomReplyMessageEvent(message = aMatrixRoomMessageEvent())
val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = (originalMessage.message as RoomEvent.Message).toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) val lookup = givenLookup(originalMessage)
@ -159,7 +158,7 @@ internal class RoomEventCreatorTest {
result shouldBeEqualTo aRoomReplyMessageEvent( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.replyingTo, replyingTo = originalMessage.replyingTo,
message = aRoomMessageEvent( message = aMatrixRoomMessageEvent(
eventId = originalMessage.eventId, eventId = originalMessage.eventId,
utcTimestamp = editedMessage.utcTimestamp, utcTimestamp = editedMessage.utcTimestamp,
content = A_TEXT_EVENT_MESSAGE, content = A_TEXT_EVENT_MESSAGE,
@ -182,7 +181,7 @@ internal class RoomEventCreatorTest {
@Test @Test
fun `given edited event is older than related room event then ignores edit`() = runTest { fun `given edited event is older than related room event then ignores edit`() = runTest {
val originalMessage = aRoomMessageEvent(utcTimestamp = 1000) val originalMessage = aMatrixRoomMessageEvent(utcTimestamp = 1000)
val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE) val editedMessage = originalMessage.toEditEvent(newTimestamp = 0, messageContent = A_TEXT_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) val lookup = givenLookup(originalMessage)
@ -199,7 +198,7 @@ internal class RoomEventCreatorTest {
println(replyEvent.content) println(replyEvent.content)
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) } val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
result shouldBeEqualTo aRoomMessageEvent( result shouldBeEqualTo aMatrixRoomMessageEvent(
eventId = replyEvent.id, eventId = replyEvent.id,
utcTimestamp = replyEvent.utcTimestamp, utcTimestamp = replyEvent.utcTimestamp,
content = replyEvent.asTextContent().body!!, content = replyEvent.asTextContent().body!!,
@ -217,13 +216,13 @@ internal class RoomEventCreatorTest {
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) } val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
result shouldBeEqualTo aRoomReplyMessageEvent( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = aRoomMessageEvent( replyingTo = aMatrixRoomMessageEvent(
eventId = originalMessage.id, eventId = originalMessage.id,
utcTimestamp = originalMessage.utcTimestamp, utcTimestamp = originalMessage.utcTimestamp,
content = originalMessage.asTextContent().body!!, content = originalMessage.asTextContent().body!!,
author = A_SENDER, author = A_SENDER,
), ),
message = aRoomMessageEvent( message = aMatrixRoomMessageEvent(
eventId = replyMessage.id, eventId = replyMessage.id,
utcTimestamp = replyMessage.utcTimestamp, utcTimestamp = replyMessage.utcTimestamp,
content = A_REPLY_EVENT_MESSAGE, content = A_REPLY_EVENT_MESSAGE,
@ -235,7 +234,7 @@ internal class RoomEventCreatorTest {
@Test @Test
fun `given reply event which relates to a room event then maps to reply`() = runTest { fun `given reply event which relates to a room event then maps to reply`() = runTest {
fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER) fakeRoomMembersService.givenMember(A_ROOM_ID, A_SENDER.id, A_SENDER)
val originalMessage = aRoomMessageEvent() val originalMessage = aMatrixRoomMessageEvent()
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE) val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
val lookup = givenLookup(originalMessage) val lookup = givenLookup(originalMessage)
@ -243,7 +242,7 @@ internal class RoomEventCreatorTest {
result shouldBeEqualTo aRoomReplyMessageEvent( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage, replyingTo = originalMessage,
message = aRoomMessageEvent( message = aMatrixRoomMessageEvent(
eventId = replyMessage.id, eventId = replyMessage.id,
utcTimestamp = replyMessage.utcTimestamp, utcTimestamp = replyMessage.utcTimestamp,
content = A_REPLY_EVENT_MESSAGE, content = A_REPLY_EVENT_MESSAGE,
@ -263,7 +262,7 @@ internal class RoomEventCreatorTest {
result shouldBeEqualTo aRoomReplyMessageEvent( result shouldBeEqualTo aRoomReplyMessageEvent(
replyingTo = originalMessage.message, replyingTo = originalMessage.message,
message = aRoomMessageEvent( message = aMatrixRoomMessageEvent(
eventId = replyMessage.id, eventId = replyMessage.id,
utcTimestamp = replyMessage.utcTimestamp, utcTimestamp = replyMessage.utcTimestamp,
content = A_REPLY_EVENT_MESSAGE, content = A_REPLY_EVENT_MESSAGE,

View File

@ -5,8 +5,8 @@ import app.dapk.st.matrix.sync.RoomEvent
import app.dapk.st.matrix.sync.RoomState import app.dapk.st.matrix.sync.RoomState
import fake.FakeMatrixLogger import fake.FakeMatrixLogger
import fake.FakeRoomDataSource import fake.FakeRoomDataSource
import internalfake.FakeRoomEventsDecrypter
import fixture.* import fixture.*
import internalfake.FakeRoomEventsDecrypter
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.amshove.kluent.shouldBeEqualTo import org.amshove.kluent.shouldBeEqualTo
import org.junit.Test import org.junit.Test
@ -15,13 +15,14 @@ import test.expect
private val A_ROOM_ID = aRoomId() private val A_ROOM_ID = aRoomId()
private object ARoom { private object ARoom {
val MESSAGE_EVENT = aRoomMessageEvent(utcTimestamp = 0) val MESSAGE_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 0)
val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1) val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1)
val DECRYPTED_EVENT = aRoomMessageEvent(utcTimestamp = 2) val DECRYPTED_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 2)
val PREVIOUS_STATE = RoomState(aRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT)) val PREVIOUS_STATE = RoomState(aMatrixRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT))
val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT) val DECRYPTED_EVENTS = listOf(MESSAGE_EVENT, DECRYPTED_EVENT)
val NEW_STATE = RoomState(aRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS) val NEW_STATE = RoomState(aMatrixRoomOverview(lastMessage = DECRYPTED_EVENT.asLastMessage()), DECRYPTED_EVENTS)
} }
private val A_USER_CREDENTIALS = aUserCredentials() private val A_USER_CREDENTIALS = aUserCredentials()
internal class RoomRefresherTest { internal class RoomRefresherTest {

View File

@ -19,7 +19,7 @@ private val A_ROOM_ID = aRoomId()
private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null) private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null)
private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent() private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent()
private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent() private val A_TEXT_TIMELINE_EVENT = anApiTimelineTextEvent()
private val A_MESSAGE_ROOM_EVENT = aRoomMessageEvent(anEventId("a-message")) private val A_MESSAGE_ROOM_EVENT = aMatrixRoomMessageEvent(anEventId("a-message"))
private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message")) private val AN_ENCRYPTED_ROOM_EVENT = anEncryptedRoomMessageEvent(anEventId("encrypted-message"))
private val A_LOOKUP_EVENT_ID = anEventId("lookup-id") private val A_LOOKUP_EVENT_ID = anEventId("lookup-id")
private val A_USER_CREDENTIALS = aUserCredentials() private val A_USER_CREDENTIALS = aUserCredentials()
@ -52,7 +52,7 @@ class TimelineEventsProcessorTest {
@Test @Test
fun `given encrypted and text timeline events when processing then maps to room events`() = runTest { fun `given encrypted and text timeline events when processing then maps to room events`() = runTest {
val previousEvents = listOf(aRoomMessageEvent(eventId = anEventId("previous-event"))) val previousEvents = listOf(aMatrixRoomMessageEvent(eventId = anEventId("previous-event")))
val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT) val newTimelineEvents = listOf(AN_ENCRYPTED_TIMELINE_EVENT, A_TEXT_TIMELINE_EVENT)
val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents))) val roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents) fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)

Some files were not shown because too many files have changed in this diff Show More