Merge pull request #200 from ouchadam/tech/chat-engine
Tech/chat engine
This commit is contained in:
commit
3444285592
|
@ -106,6 +106,9 @@ dependencies {
|
|||
|
||||
implementation project(":core")
|
||||
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":matrix-chat-engine")
|
||||
|
||||
implementation Dependencies.google.androidxComposeUi
|
||||
implementation Dependencies.mavenCentral.ktorAndroid
|
||||
implementation Dependencies.mavenCentral.sqldelightAndroid
|
||||
|
|
|
@ -17,43 +17,29 @@ import app.dapk.st.core.extensions.ErrorTracker
|
|||
import app.dapk.st.core.extensions.unsafeLazy
|
||||
import app.dapk.st.directory.DirectoryModule
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.engine.MatrixEngine
|
||||
import app.dapk.st.firebase.messaging.MessagingModule
|
||||
import app.dapk.st.home.HomeModule
|
||||
import app.dapk.st.home.MainActivity
|
||||
import app.dapk.st.imageloader.ImageLoaderModule
|
||||
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.authService
|
||||
import app.dapk.st.matrix.auth.installAuthService
|
||||
import app.dapk.st.matrix.common.*
|
||||
import app.dapk.st.matrix.crypto.RoomMembersProvider
|
||||
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.common.EventId
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
import app.dapk.st.matrix.common.MatrixLogger
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
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.MessengerModule
|
||||
import app.dapk.st.messenger.gallery.ImageGalleryModule
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
import app.dapk.st.notifications.MatrixPushHandler
|
||||
import app.dapk.st.notifications.NotificationsModule
|
||||
import app.dapk.st.olm.DeviceKeyFactory
|
||||
import app.dapk.st.olm.OlmPersistenceWrapper
|
||||
import app.dapk.st.olm.OlmWrapper
|
||||
import app.dapk.st.profile.ProfileModule
|
||||
import app.dapk.st.push.PushHandler
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.push.PushTokenPayload
|
||||
import app.dapk.st.push.messaging.MessagingServiceAdapter
|
||||
import app.dapk.st.settings.SettingsModule
|
||||
import app.dapk.st.share.ShareEntryModule
|
||||
|
@ -62,8 +48,8 @@ import app.dapk.st.work.TaskRunnerModule
|
|||
import app.dapk.st.work.WorkModule
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.io.InputStream
|
||||
import java.time.Clock
|
||||
|
||||
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 database = DapkDb(driver)
|
||||
private val clock = Clock.systemUTC()
|
||||
val coroutineDispatchers = CoroutineDispatchers(Dispatchers.IO)
|
||||
val base64 = AndroidBase64()
|
||||
private val base64 = AndroidBase64()
|
||||
|
||||
val storeModule = unsafeLazy {
|
||||
StoreModule(
|
||||
|
@ -95,9 +80,10 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
private val imageLoaderModule = ImageLoaderModule(context)
|
||||
|
||||
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(
|
||||
intentFactory = object : IntentFactory {
|
||||
|
@ -131,76 +117,57 @@ internal class AppModule(context: Application, logger: MatrixLogger) {
|
|||
|
||||
val featureModules = FeatureModules(
|
||||
storeModule,
|
||||
matrixModules,
|
||||
chatEngineModule,
|
||||
domainModules,
|
||||
trackingModule,
|
||||
coreAndroidModule,
|
||||
imageLoaderModule,
|
||||
imageContentReader,
|
||||
context,
|
||||
buildMeta,
|
||||
deviceMeta,
|
||||
coroutineDispatchers,
|
||||
clock,
|
||||
base64,
|
||||
)
|
||||
}
|
||||
|
||||
internal class FeatureModules internal constructor(
|
||||
private val storeModule: Lazy<StoreModule>,
|
||||
private val matrixModules: MatrixModules,
|
||||
private val chatEngineModule: ChatEngineModule,
|
||||
private val domainModules: DomainModules,
|
||||
private val trackingModule: TrackingModule,
|
||||
private val coreAndroidModule: CoreAndroidModule,
|
||||
imageLoaderModule: ImageLoaderModule,
|
||||
imageContentReader: ImageContentReader,
|
||||
context: Context,
|
||||
buildMeta: BuildMeta,
|
||||
deviceMeta: DeviceMeta,
|
||||
coroutineDispatchers: CoroutineDispatchers,
|
||||
clock: Clock,
|
||||
base64: Base64,
|
||||
) {
|
||||
|
||||
val directoryModule by unsafeLazy {
|
||||
DirectoryModule(
|
||||
syncService = matrixModules.sync,
|
||||
messageService = matrixModules.message,
|
||||
context = context,
|
||||
credentialsStore = storeModule.value.credentialsStore(),
|
||||
roomStore = storeModule.value.roomStore(),
|
||||
roomService = matrixModules.room,
|
||||
chatEngine = chatEngineModule.engine,
|
||||
)
|
||||
}
|
||||
val loginModule by unsafeLazy {
|
||||
LoginModule(
|
||||
matrixModules.auth,
|
||||
chatEngineModule.engine,
|
||||
domainModules.pushModule,
|
||||
matrixModules.profile,
|
||||
trackingModule.errorTracker
|
||||
)
|
||||
}
|
||||
val messengerModule by unsafeLazy {
|
||||
MessengerModule(
|
||||
matrixModules.sync,
|
||||
matrixModules.message,
|
||||
matrixModules.room,
|
||||
storeModule.value.credentialsStore(),
|
||||
storeModule.value.roomStore(),
|
||||
clock,
|
||||
chatEngineModule.engine,
|
||||
context,
|
||||
base64,
|
||||
imageContentReader,
|
||||
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 {
|
||||
SettingsModule(
|
||||
chatEngineModule.engine,
|
||||
storeModule.value,
|
||||
pushModule,
|
||||
matrixModules.crypto,
|
||||
matrixModules.sync,
|
||||
context.contentResolver,
|
||||
buildMeta,
|
||||
deviceMeta,
|
||||
|
@ -210,12 +177,11 @@ internal class FeatureModules internal constructor(
|
|||
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 {
|
||||
NotificationsModule(
|
||||
chatEngineModule.engine,
|
||||
imageLoaderModule.iconLoader(),
|
||||
storeModule.value.roomStore(),
|
||||
storeModule.value.overviewStore(),
|
||||
context,
|
||||
intentFactory = coreAndroidModule.intentFactory(),
|
||||
dispatchers = coroutineDispatchers,
|
||||
|
@ -224,7 +190,7 @@ internal class FeatureModules internal constructor(
|
|||
}
|
||||
|
||||
val shareEntryModule by unsafeLazy {
|
||||
ShareEntryModule(matrixModules.sync, matrixModules.room)
|
||||
ShareEntryModule(chatEngineModule.engine)
|
||||
}
|
||||
|
||||
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 trackingModule: TrackingModule,
|
||||
private val workModule: WorkModule,
|
||||
|
@ -252,232 +218,48 @@ internal class MatrixModules(
|
|||
private val buildMeta: BuildMeta,
|
||||
) {
|
||||
|
||||
val matrix by unsafeLazy {
|
||||
val engine by unsafeLazy {
|
||||
val store = storeModule.value
|
||||
val credentialsStore = store.credentialsStore()
|
||||
MatrixClient(
|
||||
KtorMatrixHttpClientFactory(
|
||||
credentialsStore,
|
||||
includeLogging = buildMeta.isDebug,
|
||||
),
|
||||
logger
|
||||
).also {
|
||||
it.install {
|
||||
installAuthService(credentialsStore, SmallTalkDeviceNameGenerator())
|
||||
installEncryptionService(store.knownDevicesStore())
|
||||
|
||||
val olmAccountStore = OlmPersistenceWrapper(store.olmStore(), base64)
|
||||
val singletonFlows = SingletonFlows(coroutineDispatchers)
|
||||
val olm = OlmWrapper(
|
||||
olmStore = olmAccountStore,
|
||||
singletonFlows = singletonFlows,
|
||||
jsonCanonicalizer = JsonCanonicalizer(),
|
||||
deviceKeyFactory = DeviceKeyFactory(JsonCanonicalizer()),
|
||||
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)
|
||||
}
|
||||
}
|
||||
MatrixEngine.Factory().create(
|
||||
base64,
|
||||
buildMeta,
|
||||
logger,
|
||||
SmallTalkDeviceNameGenerator(),
|
||||
coroutineDispatchers,
|
||||
trackingModule.errorTracker,
|
||||
imageContentReader,
|
||||
BackgroundWorkAdapter(workModule.workScheduler()),
|
||||
store.memberStore(),
|
||||
store.roomStore(),
|
||||
store.profileStore(),
|
||||
store.syncStore(),
|
||||
store.overviewStore(),
|
||||
store.filterStore(),
|
||||
store.localEchoStore,
|
||||
store.credentialsStore(),
|
||||
store.knownDevicesStore(),
|
||||
OlmPersistenceWrapper(store.olmStore(), base64),
|
||||
)
|
||||
}
|
||||
|
||||
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(
|
||||
private val matrixModules: MatrixModules,
|
||||
private val chatEngineModule: ChatEngineModule,
|
||||
private val errorTracker: ErrorTracker,
|
||||
private val workModule: WorkModule,
|
||||
private val storeModule: Lazy<StoreModule>,
|
||||
private val context: Application,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
) {
|
||||
|
||||
val pushHandler by unsafeLazy {
|
||||
val store = storeModule.value
|
||||
MatrixPushHandler(
|
||||
workScheduler = workModule.workScheduler(),
|
||||
credentialsStore = store.credentialsStore(),
|
||||
matrixModules.sync,
|
||||
store.roomStore(),
|
||||
)
|
||||
private val pushHandler by unsafeLazy {
|
||||
val enginePushHandler = chatEngineModule.engine.pushHandler()
|
||||
object : PushHandler {
|
||||
override fun onNewToken(payload: PushTokenPayload) {
|
||||
enginePushHandler.onNewToken(JsonString(Json.encodeToString(PushTokenPayload.serializer(), payload)))
|
||||
}
|
||||
|
||||
override fun onMessageReceived(eventId: EventId?, roomId: RoomId?) = enginePushHandler.onMessageReceived(eventId, roomId)
|
||||
}
|
||||
}
|
||||
|
||||
val messaging by unsafeLazy { MessagingModule(MessagingServiceAdapter(pushHandler), context) }
|
||||
|
@ -492,7 +274,9 @@ internal class DomainModules(
|
|||
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)))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
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.work.TaskRunner
|
||||
import io.ktor.client.plugins.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class AppTaskRunner(
|
||||
private val pushService: PushService,
|
||||
private val chatEngine: ChatEngine,
|
||||
) {
|
||||
|
||||
suspend fun run(workTask: TaskRunner.RunnableWorkTask): TaskRunner.TaskResult {
|
||||
|
@ -15,7 +15,7 @@ class AppTaskRunner(
|
|||
"push_token" -> {
|
||||
runCatching {
|
||||
val payload = Json.decodeFromString(PushTokenPayload.serializer(), workTask.task.jsonPayload)
|
||||
pushService.registerPush(payload.token, payload.gatewayUrl)
|
||||
chatEngine.registerPushToken(payload.token, payload.gatewayUrl)
|
||||
}.fold(
|
||||
onSuccess = { TaskRunner.TaskResult.Success(workTask.source) },
|
||||
onFailure = {
|
||||
|
|
|
@ -9,7 +9,7 @@ class BackgroundWorkAdapter(private val workScheduler: WorkScheduler) : Backgrou
|
|||
WorkScheduler.WorkTask(
|
||||
jobId = 1,
|
||||
type = task.type,
|
||||
jsonPayload = task.jsonPayload,
|
||||
jsonPayload = task.jsonPayload.value,
|
||||
)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
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
|
||||
|
||||
class TaskRunnerAdapter(
|
||||
private val matrixTaskRunner: suspend (MatrixTaskRunner.MatrixTask) -> MatrixTaskRunner.TaskResult,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val appTaskRunner: AppTaskRunner,
|
||||
) : TaskRunner {
|
||||
|
||||
|
@ -12,11 +13,12 @@ class TaskRunnerAdapter(
|
|||
return tasks.map {
|
||||
when {
|
||||
it.task.type.startsWith("matrix") -> {
|
||||
when (val result = matrixTaskRunner(MatrixTaskRunner.MatrixTask(it.task.type, it.task.jsonPayload))) {
|
||||
is MatrixTaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
||||
MatrixTaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
||||
when (val result = chatEngine.runTask(ChatEngineTask(it.task.type, it.task.jsonPayload))) {
|
||||
is app.dapk.st.engine.TaskRunner.TaskResult.Failure -> TaskRunner.TaskResult.Failure(it.source, canRetry = result.canRetry)
|
||||
app.dapk.st.engine.TaskRunner.TaskResult.Success -> TaskRunner.TaskResult.Success(it.source)
|
||||
}
|
||||
}
|
||||
|
||||
else -> appTaskRunner.run(it)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")))
|
||||
}
|
|
@ -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
|
||||
)
|
|
@ -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
|
||||
|
||||
}
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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)
|
|
@ -2,7 +2,7 @@ package fixture
|
|||
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.notifications.NotificationDiff
|
||||
import app.dapk.st.engine.NotificationDiff
|
||||
|
||||
object NotificationDiffFixtures {
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
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? {
|
||||
var counter = 0
|
||||
|
@ -18,5 +20,3 @@ suspend fun <T> Flow<T>.firstOrNull(count: Int, predicate: suspend (T) -> Boolea
|
|||
|
||||
return result
|
||||
}
|
||||
|
||||
fun <T> Flow<T>.startAndIgnoreEmissions(): Flow<Boolean> = this.map { false }.onStart { emit(true) }.filter { it }
|
|
@ -1,3 +1,5 @@
|
|||
package test
|
||||
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
|
@ -5,7 +5,6 @@ dependencies {
|
|||
implementation project(':core')
|
||||
implementation project(':domains:android:core')
|
||||
implementation project(':domains:store')
|
||||
implementation project(':matrix:services:push')
|
||||
|
||||
firebase(it, "messaging")
|
||||
|
||||
|
|
|
@ -3,6 +3,7 @@ import kotlinx.coroutines.CoroutineScope
|
|||
import kotlinx.coroutines.Dispatchers
|
||||
import org.amshove.kluent.internal.assertEquals
|
||||
import test.ExpectTestScope
|
||||
import test.FlowTestObserver
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal class ViewModelTestScopeImpl(
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":features:messenger")
|
||||
|
@ -13,11 +11,10 @@ dependencies {
|
|||
|
||||
kotlinTest(it)
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
}
|
|
@ -38,9 +38,10 @@ import app.dapk.st.design.components.Toolbar
|
|||
import app.dapk.st.directory.DirectoryEvent.OpenDownloadUrl
|
||||
import app.dapk.st.directory.DirectoryScreenState.Content
|
||||
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.sync.RoomOverview
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.messenger.MessengerActivity
|
||||
import kotlinx.coroutines.launch
|
||||
import java.time.Clock
|
||||
|
@ -147,7 +148,7 @@ private fun Content(listState: LazyListState, state: Content) {
|
|||
}
|
||||
|
||||
@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 roomName = overview.roomName ?: "Empty room"
|
||||
val hasUnread = room.unreadCount.value > 0
|
||||
|
@ -233,7 +234,7 @@ private fun DirectoryItem(room: RoomFoo, onClick: (RoomId) -> Unit, clock: Clock
|
|||
}
|
||||
|
||||
@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
|
||||
|
||||
when {
|
||||
|
|
|
@ -2,31 +2,17 @@ package app.dapk.st.directory
|
|||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
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
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
|
||||
class DirectoryModule(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
private val context: Context,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val roomStore: RoomStore,
|
||||
private val chatEngine: ChatEngine,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun directoryViewModel(): DirectoryViewModel {
|
||||
return DirectoryViewModel(
|
||||
ShortcutHandler(context),
|
||||
DirectoryUseCase(
|
||||
syncService,
|
||||
messageService,
|
||||
roomService,
|
||||
credentialsStore,
|
||||
roomStore,
|
||||
)
|
||||
chatEngine,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
package app.dapk.st.directory
|
||||
|
||||
import app.dapk.st.engine.DirectoryState
|
||||
|
||||
sealed interface DirectoryScreenState {
|
||||
|
||||
object EmptyLoading : DirectoryScreenState
|
||||
|
|
|
@ -2,6 +2,7 @@ package app.dapk.st.directory
|
|||
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.directory.DirectoryScreenState.*
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import app.dapk.st.viewmodel.MutableStateFactory
|
||||
import app.dapk.st.viewmodel.defaultStateFactory
|
||||
|
@ -12,7 +13,7 @@ import kotlinx.coroutines.launch
|
|||
|
||||
class DirectoryViewModel(
|
||||
private val shortcutHandler: ShortcutHandler,
|
||||
private val directoryUseCase: DirectoryUseCase,
|
||||
private val chatEngine: ChatEngine,
|
||||
factory: MutableStateFactory<DirectoryScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<DirectoryScreenState, DirectoryEvent>(
|
||||
initialState = EmptyLoading,
|
||||
|
@ -23,7 +24,7 @@ class DirectoryViewModel(
|
|||
|
||||
fun start() {
|
||||
syncJob = viewModelScope.launch {
|
||||
directoryUseCase.state().onEach {
|
||||
chatEngine.directory().onEach {
|
||||
shortcutHandler.onDirectoryUpdate(it.map { it.overview })
|
||||
state = when (it.isEmpty()) {
|
||||
true -> Empty
|
||||
|
|
|
@ -5,8 +5,8 @@ import android.content.pm.ShortcutInfo
|
|||
import androidx.core.app.Person
|
||||
import androidx.core.content.pm.ShortcutInfoCompat
|
||||
import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import app.dapk.st.engine.RoomOverview
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import app.dapk.st.messenger.MessengerActivity
|
||||
|
||||
class ShortcutHandler(private val context: Context) {
|
||||
|
|
|
@ -1,25 +1,26 @@
|
|||
package app.dapk.st.directory
|
||||
|
||||
import ViewModelTest
|
||||
import app.dapk.st.engine.DirectoryItem
|
||||
import app.dapk.st.engine.UnreadCount
|
||||
import fake.FakeChatEngine
|
||||
import fixture.aRoomOverview
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import org.junit.Test
|
||||
import test.delegateReturn
|
||||
|
||||
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 {
|
||||
|
||||
private val runViewModelTest = ViewModelTest()
|
||||
private val fakeDirectoryUseCase = FakeDirectoryUseCase()
|
||||
private val fakeShortcutHandler = FakeShortcutHandler()
|
||||
private val fakeChatEngine = FakeChatEngine()
|
||||
|
||||
private val viewModel = DirectoryViewModel(
|
||||
fakeShortcutHandler.instance,
|
||||
fakeDirectoryUseCase.instance,
|
||||
fakeChatEngine,
|
||||
runViewModelTest.testMutableStateFactory(),
|
||||
)
|
||||
|
||||
|
@ -33,7 +34,7 @@ class DirectoryViewModelTest {
|
|||
@Test
|
||||
fun `when starting, then updates shortcuts and emits room state`() = runViewModelTest {
|
||||
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()
|
||||
|
||||
|
@ -44,9 +45,4 @@ class DirectoryViewModelTest {
|
|||
|
||||
class FakeShortcutHandler {
|
||||
val instance = mockk<ShortcutHandler>()
|
||||
}
|
||||
|
||||
class FakeDirectoryUseCase {
|
||||
val instance = mockk<DirectoryUseCase>()
|
||||
fun given() = every { instance.state() }.delegateReturn()
|
||||
}
|
|
@ -1,9 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:profile")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":features:directory")
|
||||
implementation project(":features:login")
|
||||
implementation project(":features:settings")
|
||||
|
|
|
@ -4,31 +4,28 @@ import app.dapk.st.core.BuildMeta
|
|||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.directory.DirectoryViewModel
|
||||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
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
|
||||
|
||||
class HomeModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val storeModule: StoreModule,
|
||||
private val profileService: ProfileService,
|
||||
private val syncService: SyncService,
|
||||
private val buildMeta: BuildMeta,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun homeViewModel(directory: DirectoryViewModel, login: LoginViewModel, profileViewModel: ProfileViewModel): HomeViewModel {
|
||||
return HomeViewModel(
|
||||
chatEngine,
|
||||
storeModule.credentialsStore(),
|
||||
directory,
|
||||
login,
|
||||
profileViewModel,
|
||||
profileService,
|
||||
storeModule.cacheCleaner(),
|
||||
BetaVersionUpgradeUseCase(
|
||||
storeModule.applicationStore(),
|
||||
buildMeta,
|
||||
),
|
||||
syncService,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -4,13 +4,13 @@ import androidx.compose.material.icons.Icons
|
|||
import androidx.compose.material.icons.filled.Menu
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.engine.Me
|
||||
|
||||
sealed interface HomeScreenState {
|
||||
|
||||
object Loading : 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) {
|
||||
Directory(Icons.Filled.Menu),
|
||||
|
|
|
@ -3,12 +3,11 @@ package app.dapk.st.home
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.directory.DirectoryViewModel
|
||||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.home.HomeScreenState.*
|
||||
import app.dapk.st.login.LoginViewModel
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
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.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -20,14 +19,13 @@ import kotlinx.coroutines.flow.onEach
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val credentialsProvider: CredentialsStore,
|
||||
private val directoryViewModel: DirectoryViewModel,
|
||||
private val loginViewModel: LoginViewModel,
|
||||
private val profileViewModel: ProfileViewModel,
|
||||
private val profileService: ProfileService,
|
||||
private val cacheCleaner: StoreCleaner,
|
||||
private val betaVersionUpgradeUseCase: BetaVersionUpgradeUseCase,
|
||||
private val syncService: SyncService,
|
||||
) : DapkViewModel<HomeScreenState, HomeEvent>(
|
||||
initialState = Loading
|
||||
) {
|
||||
|
@ -56,8 +54,8 @@ class HomeViewModel(
|
|||
}
|
||||
|
||||
private suspend fun initialHomeContent(): SignedIn {
|
||||
val me = profileService.me(forceRefresh = false)
|
||||
val initialInvites = syncService.invites().first().size
|
||||
val me = chatEngine.me(forceRefresh = false)
|
||||
val initialInvites = chatEngine.invites().first().size
|
||||
return SignedIn(Page.Directory, me, invites = initialInvites)
|
||||
}
|
||||
|
||||
|
@ -70,7 +68,7 @@ class HomeViewModel(
|
|||
|
||||
private fun CoroutineScope.listenForInviteChanges() {
|
||||
listenForInvitesJob?.cancel()
|
||||
listenForInvitesJob = syncService.invites()
|
||||
listenForInvitesJob = chatEngine.invites()
|
||||
.onEach { invites ->
|
||||
when (val currentState = state) {
|
||||
is SignedIn -> updateState { currentState.copy(invites = invites.size) }
|
||||
|
|
|
@ -1,12 +1,10 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:push")
|
||||
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(":core")
|
||||
}
|
|
@ -2,18 +2,16 @@ package app.dapk.st.login
|
|||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
import app.dapk.st.matrix.auth.AuthService
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.push.PushModule
|
||||
|
||||
class LoginModule(
|
||||
private val authService: AuthService,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val pushModule: PushModule,
|
||||
private val profileService: ProfileService,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun loginViewModel(): LoginViewModel {
|
||||
return LoginViewModel(authService, pushModule.pushTokenRegistrar(), profileService, errorTracker)
|
||||
return LoginViewModel(chatEngine, pushModule.pushTokenRegistrar(), errorTracker)
|
||||
}
|
||||
}
|
|
@ -3,10 +3,11 @@ package app.dapk.st.login
|
|||
import androidx.lifecycle.viewModelScope
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
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.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.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.async
|
||||
|
@ -14,9 +15,8 @@ import kotlinx.coroutines.awaitAll
|
|||
import kotlinx.coroutines.launch
|
||||
|
||||
class LoginViewModel(
|
||||
private val authService: AuthService,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val pushTokenRegistrar: PushTokenRegistrar,
|
||||
private val profileService: ProfileService,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : DapkViewModel<LoginScreenState, LoginEvent>(
|
||||
initialState = Content(showServerUrl = false)
|
||||
|
@ -28,8 +28,8 @@ class LoginViewModel(
|
|||
state = Loading
|
||||
viewModelScope.launch {
|
||||
logP("login") {
|
||||
when (val result = authService.login(AuthService.LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
|
||||
is AuthService.LoginResult.Success -> {
|
||||
when (val result = chatEngine.login(LoginRequest(userName, password, serverUrl.takeIfNotEmpty()))) {
|
||||
is LoginResult.Success -> {
|
||||
runCatching {
|
||||
listOf(
|
||||
async { pushTokenRegistrar.registerCurrentToken() },
|
||||
|
@ -38,11 +38,13 @@ class LoginViewModel(
|
|||
}
|
||||
_events.tryEmit(LoginComplete)
|
||||
}
|
||||
is AuthService.LoginResult.Error -> {
|
||||
|
||||
is LoginResult.Error -> {
|
||||
errorTracker.track(result.cause)
|
||||
state = Error(result.cause)
|
||||
}
|
||||
AuthService.LoginResult.MissingWellKnown -> {
|
||||
|
||||
LoginResult.MissingWellKnown -> {
|
||||
_events.tryEmit(LoginEvent.WellKnownMissing)
|
||||
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() {
|
||||
val showServerUrl = previousState?.let { it is Content && it.showServerUrl } ?: false
|
||||
|
|
|
@ -2,10 +2,7 @@ applyAndroidComposeLibraryModule(project)
|
|||
apply plugin: 'kotlin-parcelize'
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:message")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(":domains:store")
|
||||
|
@ -16,11 +13,10 @@ dependencies {
|
|||
|
||||
kotlinTest(it)
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:message"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
}
|
|
@ -2,10 +2,9 @@ package app.dapk.st.messenger
|
|||
|
||||
import android.content.Context
|
||||
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.crypto.MediaDecrypter
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import coil.ImageLoader
|
||||
import coil.decode.DataSource
|
||||
import coil.decode.ImageSource
|
||||
|
@ -20,9 +19,11 @@ import okio.BufferedSource
|
|||
import okio.Path.Companion.toOkioPath
|
||||
import java.io.File
|
||||
|
||||
class DecryptingFetcherFactory(private val context: Context, base64: Base64, private val roomId: RoomId) : Fetcher.Factory<RoomEvent.Image> {
|
||||
|
||||
private val mediaDecrypter = MediaDecrypter(base64)
|
||||
class DecryptingFetcherFactory(
|
||||
private val context: Context,
|
||||
private val roomId: RoomId,
|
||||
private val mediaDecrypter: MediaDecrypter,
|
||||
) : Fetcher.Factory<RoomEvent.Image> {
|
||||
|
||||
override fun create(data: RoomEvent.Image, options: Options, imageLoader: ImageLoader): Fetcher {
|
||||
return DecryptingFetcher(data, context, mediaDecrypter, roomId)
|
||||
|
|
|
@ -1,49 +1,23 @@
|
|||
package app.dapk.st.messenger
|
||||
|
||||
import android.content.Context
|
||||
import app.dapk.st.core.Base64
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
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.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(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val roomStore: RoomStore,
|
||||
private val clock: Clock,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val context: Context,
|
||||
private val base64: Base64,
|
||||
private val imageMetaReader: ImageContentReader,
|
||||
private val messageOptionsStore: MessageOptionsStore,
|
||||
) : ProvidableModule {
|
||||
|
||||
internal fun messengerViewModel(): MessengerViewModel {
|
||||
return MessengerViewModel(
|
||||
messageService,
|
||||
roomService,
|
||||
roomStore,
|
||||
credentialsStore,
|
||||
timelineUseCase(),
|
||||
LocalIdFactory(),
|
||||
imageMetaReader,
|
||||
chatEngine,
|
||||
messageOptionsStore,
|
||||
clock
|
||||
)
|
||||
}
|
||||
|
||||
private fun timelineUseCase(): TimelineUseCaseImpl {
|
||||
val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(LocalEchoMapper(MetaMapper()))
|
||||
return TimelineUseCaseImpl(syncService, messageService, roomService, mergeWithLocalEchosUseCase)
|
||||
}
|
||||
|
||||
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, base64, roomId)
|
||||
internal fun decryptingFetcherFactory(roomId: RoomId) = DecryptingFetcherFactory(context, roomId, chatEngine.mediaDecrypter())
|
||||
}
|
|
@ -44,12 +44,12 @@ import app.dapk.st.core.StartObserving
|
|||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.core.extensions.takeIfContent
|
||||
import app.dapk.st.design.components.*
|
||||
import app.dapk.st.engine.MessageMeta
|
||||
import app.dapk.st.engine.MessengerState
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.engine.RoomState
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
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.navigator.MessageAttachment
|
||||
import app.dapk.st.navigator.Navigator
|
||||
|
@ -196,7 +196,7 @@ private fun ColumnScope.RoomContent(self: UserId, state: RoomState, replyActions
|
|||
AlignedBubble(item, self, wasPreviousMessageSameSender, replyActions) {
|
||||
when (item) {
|
||||
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>)
|
||||
}
|
||||
}
|
||||
|
@ -482,7 +482,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||
)
|
||||
Spacer(modifier = Modifier.height(2.dp))
|
||||
when (val replyingTo = content.message.replyingTo) {
|
||||
is Message -> {
|
||||
is RoomEvent.Message -> {
|
||||
Text(
|
||||
text = replyingTo.content,
|
||||
color = content.textColor().copy(alpha = 0.8f),
|
||||
|
@ -525,7 +525,7 @@ private fun ReplyBubbleContent(content: BubbleContent<RoomEvent.Reply>) {
|
|||
)
|
||||
}
|
||||
when (val message = content.message.message) {
|
||||
is Message -> {
|
||||
is RoomEvent.Message -> {
|
||||
Text(
|
||||
text = message.content,
|
||||
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(8.dp).clickable { replyActions.onDismiss() }.wrapContentWidth().align(Alignment.TopEnd)) {
|
||||
Icon(
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
package app.dapk.st.messenger
|
||||
|
||||
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.sync.RoomEvent
|
||||
import app.dapk.st.navigator.MessageAttachment
|
||||
|
||||
data class MessengerScreenState(
|
||||
|
|
|
@ -4,35 +4,22 @@ import androidx.lifecycle.viewModelScope
|
|||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.core.extensions.takeIfContent
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.engine.SendMessage
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.common.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.viewmodel.DapkViewModel
|
||||
import app.dapk.st.viewmodel.MutableStateFactory
|
||||
import app.dapk.st.viewmodel.defaultStateFactory
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import java.time.Clock
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
internal class MessengerViewModel(
|
||||
private val messageService: MessageService,
|
||||
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 chatEngine: ChatEngine,
|
||||
private val messageOptionsStore: MessageOptionsStore,
|
||||
private val clock: Clock,
|
||||
factory: MutableStateFactory<MessengerScreenState> = defaultStateFactory(),
|
||||
) : DapkViewModel<MessengerScreenState, MessengerEvent>(
|
||||
initialState = MessengerScreenState(
|
||||
|
@ -83,31 +70,13 @@ internal class MessengerViewModel(
|
|||
|
||||
private fun start(action: MessengerAction.OnMessengerVisible) {
|
||||
updateState { copy(roomId = action.roomId, composerState = action.attachments?.let { ComposerState.Attachments(it, null) } ?: composerState) }
|
||||
syncJob = viewModelScope.launch {
|
||||
roomStore.markRead(action.roomId)
|
||||
|
||||
val credentials = credentialsStore.credentials()!!
|
||||
var lastKnownReadEvent: EventId? = null
|
||||
observeTimeline.invoke(action.roomId, credentials.userId).distinctUntilChanged().onEach { state ->
|
||||
state.latestMessageEventFromOthers(self = credentials.userId)?.let {
|
||||
if (lastKnownReadEvent != it) {
|
||||
updateRoomReadStateAsync(latestReadEvent = it, state)
|
||||
lastKnownReadEvent = it
|
||||
}
|
||||
}
|
||||
updateState { copy(roomState = Lce.Content(state)) }
|
||||
}.collect()
|
||||
viewModelScope.launch {
|
||||
syncJob = chatEngine.messages(action.roomId, disableReadReceipts = messageOptionsStore.isReadReceiptsDisabled())
|
||||
.onEach { updateState { copy(roomState = Lce.Content(it)) } }
|
||||
.launchIn(this)
|
||||
}
|
||||
}
|
||||
|
||||
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() {
|
||||
when (val composerState = state.composerState) {
|
||||
|
@ -118,27 +87,23 @@ internal class MessengerViewModel(
|
|||
state.roomState.takeIfContent()?.let { content ->
|
||||
val roomState = content.roomState
|
||||
viewModelScope.launch {
|
||||
messageService.scheduleMessage(
|
||||
MessageService.Message.TextMessage(
|
||||
MessageService.Message.Content.TextContent(body = copy.value),
|
||||
roomId = roomState.roomOverview.roomId,
|
||||
sendEncrypted = roomState.roomOverview.isEncrypted,
|
||||
localId = localIdFactory.create(),
|
||||
timestampUtc = clock.millis(),
|
||||
chatEngine.send(
|
||||
message = SendMessage.TextMessage(
|
||||
content = copy.value,
|
||||
reply = copy.reply?.let {
|
||||
MessageService.Message.TextMessage.Reply(
|
||||
SendMessage.TextMessage.Reply(
|
||||
author = it.author,
|
||||
originalMessage = when (it) {
|
||||
is RoomEvent.Image -> TODO()
|
||||
is RoomEvent.Reply -> TODO()
|
||||
is RoomEvent.Message -> it.content
|
||||
},
|
||||
replyContent = copy.value,
|
||||
eventId = it.eventId,
|
||||
timestampUtc = it.utcTimestamp,
|
||||
)
|
||||
}
|
||||
)
|
||||
),
|
||||
room = roomState.roomOverview,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -151,26 +116,7 @@ internal class MessengerViewModel(
|
|||
state.roomState.takeIfContent()?.let { content ->
|
||||
val roomState = content.roomState
|
||||
viewModelScope.launch {
|
||||
val imageUri = copy.values.first().uri.value
|
||||
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(),
|
||||
)
|
||||
)
|
||||
chatEngine.send(SendMessage.ImageMessage(uri = copy.values.first().uri.value), roomState.roomOverview)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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 {
|
||||
data class ComposerTextUpdate(val newValue: String) : MessengerAction
|
||||
data class ComposerEnterReplyMode(val replyingTo: RoomEvent) : MessengerAction
|
||||
|
|
|
@ -2,34 +2,22 @@ package app.dapk.st.messenger
|
|||
|
||||
import ViewModelTest
|
||||
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.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.RoomState
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import fake.FakeCredentialsStore
|
||||
import fake.FakeChatEngine
|
||||
import fake.FakeMessageOptionsStore
|
||||
import fake.FakeRoomStore
|
||||
import fixture.*
|
||||
import internalfake.FakeLocalIdFactory
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
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 val A_ROOM_ID = aRoomId("messenger state room id")
|
||||
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 A_SELF_ID = aUserId("self")
|
||||
|
||||
|
@ -37,23 +25,12 @@ class MessengerViewModelTest {
|
|||
|
||||
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 fakeChatEngine = FakeChatEngine()
|
||||
|
||||
private val viewModel = MessengerViewModel(
|
||||
fakeMessageService,
|
||||
fakeRoomService,
|
||||
fakeRoomStore,
|
||||
fakeCredentialsStore,
|
||||
fakeObserveTimelineUseCase,
|
||||
localIdFactory = FakeLocalIdFactory().also { it.givenCreate().returns(A_LOCAL_ID) }.instance,
|
||||
imageContentReader = FakeImageContentReader(),
|
||||
messageOptionsStore = fakeMessageOptionsStore.instance,
|
||||
clock = fixedClock(A_CURRENT_TIMESTAMP),
|
||||
fakeChatEngine,
|
||||
fakeMessageOptionsStore.instance,
|
||||
factory = runViewModelTest.testMutableStateFactory(),
|
||||
)
|
||||
|
||||
|
@ -73,10 +50,8 @@ class MessengerViewModelTest {
|
|||
@Test
|
||||
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)
|
||||
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)
|
||||
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))
|
||||
|
||||
|
@ -98,9 +73,10 @@ class MessengerViewModelTest {
|
|||
|
||||
@Test
|
||||
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)) })
|
||||
verifyExpects()
|
||||
|
@ -114,9 +90,8 @@ class MessengerViewModelTest {
|
|||
return aMessageScreenState(roomId, aMessengerState(roomState = roomState), messageContent)
|
||||
}
|
||||
|
||||
private fun expectEncryptedMessage(roomId: RoomId, localId: String, timestamp: Long, messageContent: String): MessageService.Message.TextMessage {
|
||||
val content = MessageService.Message.Content.TextContent(body = messageContent)
|
||||
return MessageService.Message.TextMessage(content, sendEncrypted = true, roomId, localId, timestamp)
|
||||
private fun expectTextMessage(messageContent: String): SendMessage.TextMessage {
|
||||
return SendMessage.TextMessage(messageContent, reply = null)
|
||||
}
|
||||
|
||||
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),
|
||||
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()
|
|
@ -4,5 +4,5 @@ apply plugin: 'kotlin-parcelize'
|
|||
dependencies {
|
||||
compileOnly project(":domains:android:stub")
|
||||
implementation project(":core")
|
||||
implementation project(":matrix:common")
|
||||
implementation project(":chat-engine")
|
||||
}
|
|
@ -1,8 +1,7 @@
|
|||
applyAndroidLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:push")
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(':domains:store')
|
||||
implementation project(":domains:android:work")
|
||||
implementation project(':domains:android:push')
|
||||
|
@ -12,12 +11,13 @@ dependencies {
|
|||
implementation project(":features:messenger")
|
||||
implementation project(":features:navigator")
|
||||
|
||||
|
||||
implementation Dependencies.mavenCentral.kotlinSerializationJson
|
||||
|
||||
kotlinTest(it)
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
}
|
|
@ -4,9 +4,9 @@ import android.app.Notification
|
|||
import android.content.Context
|
||||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.core.whenPOrHigher
|
||||
import app.dapk.st.engine.RoomOverview
|
||||
import app.dapk.st.imageloader.IconLoader
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
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)
|
||||
return AndroidNotification(
|
||||
channelId = INVITE_CHANNEL_ID,
|
||||
|
|
|
@ -10,7 +10,7 @@ class NotificationInviteRenderer(
|
|||
private val androidNotificationBuilder: AndroidNotificationBuilder,
|
||||
) {
|
||||
|
||||
fun render(inviteNotification: InviteNotification) {
|
||||
fun render(inviteNotification: app.dapk.st.engine.InviteNotification) {
|
||||
notificationManager.notify(
|
||||
inviteNotification.roomId.value,
|
||||
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)
|
||||
)
|
||||
|
||||
|
|
|
@ -5,9 +5,9 @@ import app.dapk.st.core.AppLogTag
|
|||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.extensions.ifNull
|
||||
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.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
private const val SUMMARY_NOTIFICATION_ID = 101
|
||||
|
|
|
@ -3,8 +3,8 @@ package app.dapk.st.notifications
|
|||
import android.annotation.SuppressLint
|
||||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.core.whenPOrHigher
|
||||
import app.dapk.st.engine.RoomOverview
|
||||
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.Messaging
|
||||
|
||||
|
|
|
@ -5,16 +5,14 @@ import android.content.Context
|
|||
import app.dapk.st.core.CoroutineDispatchers
|
||||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
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 java.time.Clock
|
||||
|
||||
class NotificationsModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val iconLoader: IconLoader,
|
||||
private val roomStore: RoomStore,
|
||||
private val overviewStore: OverviewStore,
|
||||
private val context: Context,
|
||||
private val intentFactory: IntentFactory,
|
||||
private val dispatchers: CoroutineDispatchers,
|
||||
|
@ -40,10 +38,9 @@ class NotificationsModule(
|
|||
)
|
||||
return RenderNotificationsUseCase(
|
||||
notificationRenderer = notificationMessageRenderer,
|
||||
observeRenderableUnreadEventsUseCase = ObserveUnreadNotificationsUseCaseImpl(roomStore),
|
||||
notificationChannels = NotificationChannels(notificationManager),
|
||||
observeInviteNotificationsUseCase = ObserveInviteNotificationsUseCaseImpl(overviewStore),
|
||||
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder)
|
||||
inviteRenderer = NotificationInviteRenderer(notificationManager, notificationFactory, androidNotificationBuilder),
|
||||
chatEngine = chatEngine,
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
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.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
|
@ -9,18 +11,17 @@ import kotlinx.coroutines.flow.onEach
|
|||
class RenderNotificationsUseCase(
|
||||
private val notificationRenderer: NotificationMessageRenderer,
|
||||
private val inviteRenderer: NotificationInviteRenderer,
|
||||
private val observeRenderableUnreadEventsUseCase: ObserveUnreadNotificationsUseCase,
|
||||
private val observeInviteNotificationsUseCase: ObserveInviteNotificationsUseCase,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val notificationChannels: NotificationChannels,
|
||||
) {
|
||||
|
||||
suspend fun listenForNotificationChanges(scope: CoroutineScope) {
|
||||
notificationChannels.initChannels()
|
||||
observeRenderableUnreadEventsUseCase()
|
||||
chatEngine.notificationsMessages()
|
||||
.onEach { (each, diff) -> renderUnreadChange(each, diff) }
|
||||
.launchIn(scope)
|
||||
|
||||
observeInviteNotificationsUseCase()
|
||||
chatEngine.notificationsInvites()
|
||||
.onEach { inviteRenderer.render(it) }
|
||||
.launchIn(scope)
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.engine.RoomEvent
|
||||
import app.dapk.st.matrix.common.RoomMember
|
||||
import app.dapk.st.matrix.sync.RoomEvent
|
||||
|
||||
class RoomEventsToNotifiableMapper {
|
||||
|
||||
|
|
|
@ -4,8 +4,8 @@ import android.app.Notification
|
|||
import android.app.PendingIntent
|
||||
import android.os.Build
|
||||
import app.dapk.st.core.DeviceMeta
|
||||
import app.dapk.st.engine.RoomOverview
|
||||
import app.dapk.st.matrix.common.AvatarUrl
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import fake.FakeContext
|
||||
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||
import fixture.NotificationDelegateFixtures.anInboxStyle
|
||||
|
@ -137,7 +137,7 @@ class NotificationFactoryTest {
|
|||
fakeIntentFactory.givenNotificationOpenApp(fakeContext.instance).returns(AN_OPEN_APP_INTENT)
|
||||
val content = "Content message"
|
||||
val result = notificationFactory.createInvite(
|
||||
InviteNotification(
|
||||
app.dapk.st.engine.InviteNotification(
|
||||
content = content,
|
||||
A_ROOM_ID,
|
||||
)
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
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.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import fake.FakeNotificationFactory
|
||||
import fake.FakeNotificationManager
|
||||
import fake.aFakeNotification
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
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.sync.RoomEvent
|
||||
import app.dapk.st.matrix.sync.RoomOverview
|
||||
import app.dapk.st.navigator.IntentFactory
|
||||
import fixture.NotificationDelegateFixtures.anAndroidNotification
|
||||
import fixture.NotificationFixtures.aDismissRoomNotification
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package app.dapk.st.notifications
|
||||
|
||||
import app.dapk.st.engine.UnreadNotifications
|
||||
import fake.*
|
||||
import fixture.NotificationDiffFixtures.aNotificationDiff
|
||||
import kotlinx.coroutines.test.TestScope
|
||||
|
@ -14,25 +15,23 @@ class RenderNotificationsUseCaseTest {
|
|||
|
||||
private val fakeNotificationMessageRenderer = FakeNotificationMessageRenderer()
|
||||
private val fakeNotificationInviteRenderer = FakeNotificationInviteRenderer()
|
||||
private val fakeObserveUnreadNotificationsUseCase = FakeObserveUnreadNotificationsUseCase()
|
||||
private val fakeObserveInviteNotificationsUseCase = FakeObserveInviteNotificationsUseCase()
|
||||
private val fakeNotificationChannels = FakeNotificationChannels().also {
|
||||
it.instance.expect { it.initChannels() }
|
||||
}
|
||||
private val fakeChatEngine = FakeChatEngine()
|
||||
|
||||
private val renderNotificationsUseCase = RenderNotificationsUseCase(
|
||||
fakeNotificationMessageRenderer.instance,
|
||||
fakeNotificationInviteRenderer.instance,
|
||||
fakeObserveUnreadNotificationsUseCase,
|
||||
fakeObserveInviteNotificationsUseCase,
|
||||
fakeChatEngine,
|
||||
fakeNotificationChannels.instance,
|
||||
)
|
||||
|
||||
@Test
|
||||
fun `given events, when listening for changes then initiates channels once`() = runTest {
|
||||
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
||||
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
||||
fakeObserveInviteNotificationsUseCase.given().emits()
|
||||
fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
|
||||
fakeChatEngine.givenNotificationsInvites().emits()
|
||||
|
||||
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
||||
|
||||
|
@ -42,8 +41,8 @@ class RenderNotificationsUseCaseTest {
|
|||
@Test
|
||||
fun `given renderable unread events, when listening for changes, then renders change`() = runTest {
|
||||
fakeNotificationMessageRenderer.instance.expect { it.render(any()) }
|
||||
fakeObserveUnreadNotificationsUseCase.given().emits(AN_UNREAD_NOTIFICATIONS)
|
||||
fakeObserveInviteNotificationsUseCase.given().emits()
|
||||
fakeChatEngine.givenNotificationsMessages().emits(AN_UNREAD_NOTIFICATIONS)
|
||||
fakeChatEngine.givenNotificationsInvites().emits()
|
||||
|
||||
renderNotificationsUseCase.listenForNotificationChanges(TestScope(UnconfinedTestDispatcher()))
|
||||
|
||||
|
|
|
@ -2,14 +2,14 @@ package fake
|
|||
|
||||
import app.dapk.st.notifications.NotificationMessageRenderer
|
||||
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.mockk
|
||||
|
||||
class FakeNotificationMessageRenderer {
|
||||
val instance = mockk<NotificationMessageRenderer>()
|
||||
|
||||
fun verifyRenders(vararg unreadNotifications: UnreadNotifications) {
|
||||
fun verifyRenders(vararg unreadNotifications: app.dapk.st.engine.UnreadNotifications) {
|
||||
unreadNotifications.forEach { unread ->
|
||||
coVerify {
|
||||
instance.render(
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:room")
|
||||
implementation project(":matrix:services:profile")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":features:settings")
|
||||
implementation project(':domains:store')
|
||||
implementation project(":domains:android:compose-core")
|
||||
|
|
|
@ -2,19 +2,15 @@ package app.dapk.st.profile
|
|||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
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.engine.ChatEngine
|
||||
|
||||
class ProfileModule(
|
||||
private val profileService: ProfileService,
|
||||
private val syncService: SyncService,
|
||||
private val roomService: RoomService,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun profileViewModel(): ProfileViewModel {
|
||||
return ProfileViewModel(profileService, syncService, roomService, errorTracker)
|
||||
return ProfileViewModel(chatEngine, errorTracker)
|
||||
}
|
||||
|
||||
}
|
|
@ -21,8 +21,8 @@ import app.dapk.st.core.Lce
|
|||
import app.dapk.st.core.LifecycleEffect
|
||||
import app.dapk.st.core.components.CenteredLoading
|
||||
import app.dapk.st.design.components.*
|
||||
import app.dapk.st.matrix.sync.InviteMeta
|
||||
import app.dapk.st.matrix.sync.RoomInvite
|
||||
import app.dapk.st.engine.RoomInvite
|
||||
import app.dapk.st.engine.RoomInvite.InviteMeta
|
||||
import app.dapk.st.settings.SettingsActivity
|
||||
|
||||
@Composable
|
||||
|
|
|
@ -3,9 +3,8 @@ package app.dapk.st.profile
|
|||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.design.components.Route
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.room.ProfileService
|
||||
import app.dapk.st.matrix.sync.RoomInvite
|
||||
import app.dapk.st.engine.Me
|
||||
import app.dapk.st.engine.RoomInvite
|
||||
|
||||
data class ProfileScreenState(
|
||||
val page: SpiderPage<out Page>,
|
||||
|
@ -14,12 +13,12 @@ data class ProfileScreenState(
|
|||
sealed interface Page {
|
||||
data class Profile(val content: Lce<Content>) : Page {
|
||||
data class Content(
|
||||
val me: ProfileService.Me,
|
||||
val me: Me,
|
||||
val invitationsCount: Int,
|
||||
)
|
||||
}
|
||||
|
||||
data class Invitations(val content: Lce<List<RoomInvite>>): Page
|
||||
data class Invitations(val content: Lce<List<RoomInvite>>) : Page
|
||||
|
||||
object Routes {
|
||||
val profile = Route<Profile>("Profile")
|
||||
|
|
|
@ -5,25 +5,20 @@ import androidx.lifecycle.viewModelScope
|
|||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.core.extensions.ErrorTracker
|
||||
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.room.ProfileService
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.viewmodel.DapkViewModel
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.*
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ProfileViewModel(
|
||||
private val profileService: ProfileService,
|
||||
private val syncService: SyncService,
|
||||
private val roomService: RoomService,
|
||||
private val chatEngine: ChatEngine,
|
||||
private val errorTracker: ErrorTracker,
|
||||
) : DapkViewModel<ProfileScreenState, ProfileEvent>(
|
||||
ProfileScreenState(SpiderPage(Page.Routes.profile, "Profile", null, Page.Profile(Lce.Loading()), hasToolbar = false))
|
||||
) {
|
||||
|
||||
private var syncingJob: Job? = null
|
||||
private var currentPageJob: Job? = null
|
||||
|
||||
fun start() {
|
||||
|
@ -31,15 +26,13 @@ class ProfileViewModel(
|
|||
}
|
||||
|
||||
private fun goToProfile() {
|
||||
syncingJob = syncService.startSyncing().launchIn(viewModelScope)
|
||||
|
||||
combine(
|
||||
flow {
|
||||
val result = runCatching { profileService.me(forceRefresh = true) }
|
||||
val result = runCatching { chatEngine.me(forceRefresh = true) }
|
||||
.onFailure { errorTracker.track(it, "Loading profile") }
|
||||
emit(result)
|
||||
},
|
||||
syncService.invites(),
|
||||
chatEngine.invites(),
|
||||
transform = { me, invites -> me to invites }
|
||||
)
|
||||
.onEach { (me, invites) ->
|
||||
|
@ -57,7 +50,7 @@ class ProfileViewModel(
|
|||
fun goToInvitations() {
|
||||
updateState { copy(page = SpiderPage(Page.Routes.invitation, "Invitations", Page.Routes.profile, Page.Invitations(Lce.Loading()))) }
|
||||
|
||||
syncService.invites()
|
||||
chatEngine.invites()
|
||||
.onEach {
|
||||
updatePageState<Page.Invitations> {
|
||||
copy(content = Lce.Content(it))
|
||||
|
@ -89,13 +82,13 @@ class ProfileViewModel(
|
|||
}
|
||||
|
||||
fun acceptRoomInvite(roomId: RoomId) {
|
||||
launchCatching { roomService.joinRoom(roomId) }.fold(
|
||||
launchCatching { chatEngine.joinRoom(roomId) }.fold(
|
||||
onError = {}
|
||||
)
|
||||
}
|
||||
|
||||
fun rejectRoomInvite(roomId: RoomId) {
|
||||
launchCatching { roomService.rejectJoinRoom(roomId) }.fold(
|
||||
launchCatching { chatEngine.rejectJoinRoom(roomId) }.fold(
|
||||
onError = {
|
||||
Log.e("!!!", it.message, it)
|
||||
}
|
||||
|
@ -115,7 +108,7 @@ class ProfileViewModel(
|
|||
}
|
||||
|
||||
fun stop() {
|
||||
syncingJob?.cancel()
|
||||
currentPageJob?.cancel()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
applyAndroidComposeLibraryModule(project)
|
||||
|
||||
dependencies {
|
||||
implementation project(":matrix:services:sync")
|
||||
implementation project(":matrix:services:crypto")
|
||||
implementation project(":chat-engine")
|
||||
implementation project(":features:navigator")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':domains:android:push')
|
||||
|
@ -13,11 +12,10 @@ dependencies {
|
|||
|
||||
kotlinTest(it)
|
||||
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:sync"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:services:crypto"))
|
||||
androidImportFixturesWorkaround(project, project(":matrix:common"))
|
||||
androidImportFixturesWorkaround(project, project(":core"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:store"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:viewmodel"))
|
||||
androidImportFixturesWorkaround(project, project(":domains:android:stub"))
|
||||
androidImportFixturesWorkaround(project, project(":chat-engine"))
|
||||
}
|
|
@ -5,16 +5,14 @@ import app.dapk.st.core.*
|
|||
import app.dapk.st.domain.StoreModule
|
||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.push.PushModule
|
||||
import app.dapk.st.settings.eventlogger.EventLoggerViewModel
|
||||
|
||||
class SettingsModule(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val storeModule: StoreModule,
|
||||
private val pushModule: PushModule,
|
||||
private val cryptoService: CryptoService,
|
||||
private val syncService: SyncService,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val buildMeta: BuildMeta,
|
||||
private val deviceMeta: DeviceMeta,
|
||||
|
@ -26,10 +24,9 @@ class SettingsModule(
|
|||
|
||||
internal fun settingsViewModel(): SettingsViewModel {
|
||||
return SettingsViewModel(
|
||||
chatEngine,
|
||||
storeModule.cacheCleaner(),
|
||||
contentResolver,
|
||||
cryptoService,
|
||||
syncService,
|
||||
UriFilenameResolver(contentResolver, coroutineDispatchers),
|
||||
SettingsItemFactory(buildMeta, deviceMeta, pushModule.pushTokenRegistrars(), themeStore, loggingStore, messageOptionsStore),
|
||||
pushModule.pushTokenRegistrars(),
|
||||
|
|
|
@ -42,7 +42,7 @@ import app.dapk.st.core.components.Header
|
|||
import app.dapk.st.core.extensions.takeAs
|
||||
import app.dapk.st.core.getActivity
|
||||
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.settings.SettingsEvent.*
|
||||
import app.dapk.st.settings.eventlogger.EventLogActivity
|
||||
|
|
|
@ -2,10 +2,9 @@ package app.dapk.st.settings
|
|||
|
||||
import android.net.Uri
|
||||
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.SpiderPage
|
||||
import app.dapk.st.matrix.crypto.ImportResult
|
||||
import app.dapk.st.engine.ImportResult
|
||||
import app.dapk.st.push.Registrar
|
||||
|
||||
internal data class SettingsScreenState(
|
||||
|
|
|
@ -9,9 +9,8 @@ import app.dapk.st.design.components.SpiderPage
|
|||
import app.dapk.st.domain.StoreCleaner
|
||||
import app.dapk.st.domain.application.eventlog.LoggingStore
|
||||
import app.dapk.st.domain.application.message.MessageOptionsStore
|
||||
import app.dapk.st.matrix.crypto.CryptoService
|
||||
import app.dapk.st.matrix.crypto.ImportResult
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import app.dapk.st.engine.ImportResult
|
||||
import app.dapk.st.push.PushTokenRegistrars
|
||||
import app.dapk.st.push.Registrar
|
||||
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/"
|
||||
|
||||
internal class SettingsViewModel(
|
||||
private val chatEngine: ChatEngine,
|
||||
private val cacheCleaner: StoreCleaner,
|
||||
private val contentResolver: ContentResolver,
|
||||
private val cryptoService: CryptoService,
|
||||
private val syncService: SyncService,
|
||||
private val uriFilenameResolver: UriFilenameResolver,
|
||||
private val settingsItemFactory: SettingsItemFactory,
|
||||
private val pushTokenRegistrars: PushTokenRegistrars,
|
||||
|
@ -142,26 +140,13 @@ internal class SettingsViewModel(
|
|||
fun importFromFileKeys(file: Uri, passphrase: String) {
|
||||
updatePageState<Page.ImportRoomKey> { copy(importProgress = ImportResult.Update(0)) }
|
||||
viewModelScope.launch {
|
||||
with(cryptoService) {
|
||||
with(chatEngine) {
|
||||
runCatching { contentResolver.openInputStream(file)!! }
|
||||
.fold(
|
||||
onSuccess = { fileStream ->
|
||||
fileStream.importRoomKeys(passphrase)
|
||||
.onEach {
|
||||
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)
|
||||
},
|
||||
|
|
|
@ -3,9 +3,8 @@ package app.dapk.st.settings
|
|||
import ViewModelTest
|
||||
import app.dapk.st.core.Lce
|
||||
import app.dapk.st.design.components.SpiderPage
|
||||
import app.dapk.st.matrix.crypto.ImportResult
|
||||
import app.dapk.st.engine.ImportResult
|
||||
import fake.*
|
||||
import fake.FakeStoreCleaner
|
||||
import fixture.aRoomId
|
||||
import internalfake.FakeSettingsItemFactory
|
||||
import internalfake.FakeUriFilenameResolver
|
||||
|
@ -35,20 +34,18 @@ internal class SettingsViewModelTest {
|
|||
|
||||
private val fakeStoreCleaner = FakeStoreCleaner()
|
||||
private val fakeContentResolver = FakeContentResolver()
|
||||
private val fakeCryptoService = FakeCryptoService()
|
||||
private val fakeSyncService = FakeSyncService()
|
||||
private val fakeUriFilenameResolver = FakeUriFilenameResolver()
|
||||
private val fakePushTokenRegistrars = FakePushRegistrars()
|
||||
private val fakeSettingsItemFactory = FakeSettingsItemFactory()
|
||||
private val fakeThemeStore = FakeThemeStore()
|
||||
private val fakeLoggingStore = FakeLoggingStore()
|
||||
private val fakeMessageOptionsStore = FakeMessageOptionsStore()
|
||||
private val fakeChatEngine = FakeChatEngine()
|
||||
|
||||
private val viewModel = SettingsViewModel(
|
||||
fakeChatEngine,
|
||||
fakeStoreCleaner,
|
||||
fakeContentResolver.instance,
|
||||
fakeCryptoService,
|
||||
fakeSyncService,
|
||||
fakeUriFilenameResolver.instance,
|
||||
fakeSettingsItemFactory.instance,
|
||||
fakePushTokenRegistrars.instance,
|
||||
|
@ -174,9 +171,8 @@ internal class SettingsViewModelTest {
|
|||
|
||||
@Test
|
||||
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)
|
||||
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
|
||||
.test(initialState = SettingsScreenState(A_IMPORT_ROOM_KEYS_PAGE_WITH_SELECTION))
|
||||
|
|
|
@ -4,9 +4,7 @@ dependencies {
|
|||
implementation project(":domains:android:compose-core")
|
||||
implementation project(":domains:android:viewmodel")
|
||||
implementation project(':domains:store')
|
||||
implementation project(':matrix:services:sync')
|
||||
implementation project(':matrix:services:room')
|
||||
implementation project(':matrix:services:message')
|
||||
implementation project(':chat-engine')
|
||||
implementation project(":core")
|
||||
implementation project(":design-library")
|
||||
implementation project(":features:navigator")
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
package app.dapk.st.share
|
||||
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
import kotlinx.coroutines.flow.first
|
||||
|
||||
class FetchRoomsUseCase(
|
||||
private val syncSyncService: SyncService,
|
||||
private val roomService: RoomService,
|
||||
private val chatEngine: ChatEngine,
|
||||
) {
|
||||
|
||||
suspend fun bar(): List<Item> {
|
||||
return syncSyncService.overview().first().map {
|
||||
suspend fun fetch(): List<Item> {
|
||||
return chatEngine.directory().first().map {
|
||||
val overview = it.overview
|
||||
Item(
|
||||
it.roomId,
|
||||
it.roomAvatarUrl,
|
||||
it.roomName ?: "",
|
||||
roomService.findMembersSummary(it.roomId).map { it.displayName ?: it.id.value }
|
||||
overview.roomId,
|
||||
overview.roomAvatarUrl,
|
||||
overview.roomName ?: "",
|
||||
chatEngine.findMembersSummary(overview.roomId).map { it.displayName ?: it.id.value }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,13 @@
|
|||
package app.dapk.st.share
|
||||
|
||||
import app.dapk.st.core.ProvidableModule
|
||||
import app.dapk.st.matrix.room.RoomService
|
||||
import app.dapk.st.matrix.sync.SyncService
|
||||
import app.dapk.st.engine.ChatEngine
|
||||
|
||||
class ShareEntryModule(
|
||||
private val syncService: SyncService,
|
||||
private val roomService: RoomService,
|
||||
private val chatEngine: ChatEngine,
|
||||
) : ProvidableModule {
|
||||
|
||||
fun shareEntryViewModel(): ShareEntryViewModel {
|
||||
return ShareEntryViewModel(FetchRoomsUseCase(syncService, roomService))
|
||||
return ShareEntryViewModel(FetchRoomsUseCase(chatEngine))
|
||||
}
|
||||
}
|
|
@ -22,7 +22,7 @@ class ShareEntryViewModel(
|
|||
|
||||
fun start() {
|
||||
syncJob = viewModelScope.launch {
|
||||
state = DirectoryScreenState.Content(fetchRoomsUseCase.bar())
|
||||
state = DirectoryScreenState.Content(fetchRoomsUseCase.fetch())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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")))
|
||||
}
|
|
@ -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.RoomId
|
||||
|
@ -6,22 +6,12 @@ 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.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 kotlinx.coroutines.flow.*
|
||||
|
||||
@JvmInline
|
||||
value class UnreadCount(val value: Int)
|
||||
|
||||
typealias DirectoryState = List<RoomFoo>
|
||||
|
||||
data class RoomFoo(
|
||||
val overview: RoomOverview,
|
||||
val unreadCount: UnreadCount,
|
||||
val typing: Typing?
|
||||
)
|
||||
|
||||
class DirectoryUseCase(
|
||||
internal class DirectoryUseCase(
|
||||
private val syncService: SyncService,
|
||||
private val messageService: MessageService,
|
||||
private val roomService: RoomService,
|
||||
|
@ -38,10 +28,10 @@ class DirectoryUseCase(
|
|||
syncService.events()
|
||||
) { overviewState, localEchos, unread, events ->
|
||||
overviewState.mergeWithLocalEchos(localEchos, userId).map { roomOverview ->
|
||||
RoomFoo(
|
||||
DirectoryItem(
|
||||
overview = roomOverview,
|
||||
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(
|
||||
syncService.startSyncing().map { false }.onStart { emit(true) },
|
||||
syncService.overview()
|
||||
) { isFirstLoad, overview ->
|
||||
when {
|
||||
isFirstLoad && overview.isEmpty() -> null
|
||||
else -> overview
|
||||
}
|
||||
}.filterNotNull()
|
||||
syncService.startSyncing(),
|
||||
syncService.overview().map { it.map { it.engine() } }
|
||||
) { _, overview -> overview }.filterNotNull()
|
||||
|
||||
private suspend fun OverviewState.mergeWithLocalEchos(localEchos: Map<RoomId, List<MessageService.LocalEcho>>, userId: UserId): OverviewState {
|
||||
return when {
|
||||
|
@ -81,7 +66,7 @@ class DirectoryUseCase(
|
|||
val latestEcho = echos.maxByOrNull { it.timestampUtc }
|
||||
return if (latestEcho != null && latestEcho.timestampUtc > (this.lastMessage?.utcTimestamp ?: 0)) {
|
||||
this.copy(
|
||||
lastMessage = LastMessage(
|
||||
lastMessage = RoomOverview.LastMessage(
|
||||
content = when (val message = latestEcho.message) {
|
||||
is MessageService.Message.TextMessage -> message.content.body
|
||||
is MessageService.Message.ImageMessage -> "\uD83D\uDCF7"
|
||||
|
@ -96,3 +81,6 @@ class DirectoryUseCase(
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -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()
|
||||
|
||||
}
|
|
@ -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.RoomMember
|
||||
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) {
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package app.dapk.st.engine
|
||||
|
||||
import java.util.*
|
||||
|
||||
internal class LocalIdFactory {
|
||||
fun create() = "local.${UUID.randomUUID()}"
|
||||
}
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
|
@ -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)
|
|
@ -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.log
|
||||
import app.dapk.st.matrix.common.CredentialsStore
|
||||
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.message.BackgroundScheduler
|
||||
import app.dapk.st.matrix.sync.RoomStore
|
||||
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.flow.*
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
private var previousJob: Job? = null
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
class MatrixPushHandler(
|
||||
private val workScheduler: WorkScheduler,
|
||||
private val backgroundScheduler: BackgroundScheduler,
|
||||
private val credentialsStore: CredentialsStore,
|
||||
private val syncService: SyncService,
|
||||
private val roomStore: RoomStore,
|
||||
) : PushHandler {
|
||||
|
||||
override fun onNewToken(payload: PushTokenPayload) {
|
||||
override fun onNewToken(payload: JsonString) {
|
||||
log(AppLogTag.PUSH, "new push token received")
|
||||
workScheduler.schedule(
|
||||
WorkScheduler.WorkTask(
|
||||
backgroundScheduler.schedule(
|
||||
key = "2",
|
||||
task = BackgroundScheduler.Task(
|
||||
type = "push_token",
|
||||
jobId = 2,
|
||||
jsonPayload = Json.encodeToString(PushTokenPayload.serializer(), payload)
|
||||
jsonPayload = payload
|
||||
)
|
||||
)
|
||||
}
|
||||
|
@ -66,7 +64,7 @@ class MatrixPushHandler(
|
|||
|
||||
private suspend fun waitForEvent(timeout: Long, eventId: EventId): EventId? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing().startInstantly(), syncService.observeEvent(eventId)) { _, event -> event }
|
||||
combine(syncService.startSyncing(), syncService.observeEvent(eventId)) { _, event -> event }
|
||||
.firstOrNull {
|
||||
it == eventId
|
||||
}
|
||||
|
@ -75,11 +73,9 @@ class MatrixPushHandler(
|
|||
|
||||
private suspend fun waitForUnreadChange(timeout: Long): String? {
|
||||
return withTimeoutOrNull(timeout) {
|
||||
combine(syncService.startSyncing().startInstantly(), roomStore.observeUnread()) { _, unread -> unread }
|
||||
combine(syncService.startSyncing(), roomStore.observeUnread()) { _, unread -> unread }
|
||||
.first()
|
||||
"ignored"
|
||||
}
|
||||
}
|
||||
|
||||
private fun Flow<Unit>.startInstantly() = this.onStart { emit(Unit) }
|
||||
}
|
||||
}
|
|
@ -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.RoomMember
|
||||
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
|
||||
|
|
@ -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.OverviewStore
|
||||
import app.dapk.st.matrix.sync.RoomInvite
|
||||
import kotlinx.coroutines.FlowPreview
|
||||
import kotlinx.coroutines.flow.*
|
||||
|
||||
internal typealias ObserveInviteNotificationsUseCase = suspend () -> Flow<InviteNotification>
|
||||
internal typealias ObserveInviteNotificationsUseCase = () -> Flow<InviteNotification>
|
||||
|
||||
class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewStore) : ObserveInviteNotificationsUseCase {
|
||||
|
||||
override suspend fun invoke(): Flow<InviteNotification> {
|
||||
override fun invoke(): Flow<InviteNotification> {
|
||||
return overviewStore.latestInvites()
|
||||
.diff()
|
||||
.drop(1)
|
||||
|
@ -43,8 +42,3 @@ class ObserveInviteNotificationsUseCaseImpl(private val overviewStore: OverviewS
|
|||
private fun <T> Flow<Set<T>>.flatten() = this.flatMapConcat { items ->
|
||||
flow { items.forEach { this.emit(it) } }
|
||||
}
|
||||
|
||||
data class InviteNotification(
|
||||
val content: String,
|
||||
val roomId: RoomId
|
||||
)
|
|
@ -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.extensions.clearAndPutAll
|
||||
|
@ -6,17 +6,16 @@ import app.dapk.st.core.extensions.containsKey
|
|||
import app.dapk.st.core.log
|
||||
import app.dapk.st.matrix.common.EventId
|
||||
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 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 = suspend () -> Flow<UnreadNotifications>
|
||||
internal typealias ObserveUnreadNotificationsUseCase = () -> Flow<UnreadNotifications>
|
||||
|
||||
class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) : ObserveUnreadNotificationsUseCase {
|
||||
|
||||
override suspend fun invoke(): Flow<UnreadNotifications> {
|
||||
override fun invoke(): Flow<UnreadNotifications> {
|
||||
return roomStore.observeUnread()
|
||||
.mapWithDiff()
|
||||
.avoidShowingPreviousNotificationsOnLaunch()
|
||||
|
@ -25,28 +24,7 @@ class ObserveUnreadNotificationsUseCaseImpl(private val roomStore: RoomStore) :
|
|||
|
||||
}
|
||||
|
||||
private fun Flow<UnreadNotifications>.onlyRenderableChanges(): Flow<UnreadNotifications> {
|
||||
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>> {
|
||||
private fun Flow<Map<MatrixRoomOverview, List<MatrixRoomEvent>>>.mapWithDiff(): Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>> {
|
||||
val previousUnreadEvents = mutableMapOf<RoomId, List<TimestampedEventId>>()
|
||||
return this.map { each ->
|
||||
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<RoomOverview, List<RoomEvent>>.toTimestampedIds() = this
|
||||
private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.toTimestampedIds() = this
|
||||
.mapValues { it.value.toEventIds() }
|
||||
.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)
|
||||
|
||||
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>
|
||||
)
|
||||
private fun Flow<Pair<Map<MatrixRoomOverview, List<MatrixRoomEvent>>, NotificationDiff>>.onlyRenderableChanges(): Flow<UnreadNotifications> {
|
||||
val inferredCurrentNotifications = mutableMapOf<RoomId, List<MatrixRoomEvent>>()
|
||||
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 }) }
|
||||
.map {
|
||||
val engineModels = it.first
|
||||
.mapKeys { it.key.engine() }
|
||||
.mapValues { it.value.map { it.engine() } }
|
||||
engineModels to it.second
|
||||
}
|
||||
}
|
||||
|
||||
typealias TimestampedEventId = Pair<EventId, Long>
|
|
@ -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
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
}
|
|
@ -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.RoomMember
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
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.SyncService
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.map
|
||||
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun roomDatasource(roomId: RoomId) = combine(
|
||||
syncService.startSyncing().startAndIgnoreEmissions(),
|
||||
syncService.room(roomId)
|
||||
syncService.startSyncing(),
|
||||
syncService.room(roomId).map { it.engine() }
|
||||
) { _, room -> room }
|
||||
}
|
||||
|
||||
private fun UserId.toFallbackMember() = RoomMember(this, displayName = null, avatarUrl = null)
|
||||
|
||||
data class MessengerState(
|
||||
val self: UserId,
|
||||
val roomState: RoomState,
|
||||
val typing: SyncService.SyncEvent.Typing?
|
||||
)
|
|
@ -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.message.MessageService
|
||||
import app.dapk.st.matrix.sync.MessageMeta
|
||||
import fake.FakeMetaMapper
|
||||
import fixture.*
|
||||
import internalfake.FakeMetaMapper
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -27,7 +27,7 @@ class LocalEchoMapperTest {
|
|||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
eventId = echo.eventId!!,
|
||||
content = AN_ECHO_CONTENT.content.body,
|
||||
meta = A_META
|
||||
meta = A_META.engine()
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -40,25 +40,25 @@ class LocalEchoMapperTest {
|
|||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
eventId = anEventId(echo.localId),
|
||||
content = AN_ECHO_CONTENT.content.body,
|
||||
meta = A_META
|
||||
meta = A_META.engine()
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
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 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)
|
||||
|
||||
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 {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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.MessageType
|
||||
import app.dapk.st.matrix.common.RoomId
|
||||
import app.dapk.st.matrix.message.MessageService
|
||||
import fixture.*
|
||||
import internalfake.FakeLocalEventMapper
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
||||
|
@ -18,7 +15,7 @@ private val ANOTHER_ROOM_MESSAGE_EVENT = A_ROOM_MESSAGE_EVENT.copy(eventId = anE
|
|||
|
||||
class MergeWithLocalEchosUseCaseTest {
|
||||
|
||||
private val fakeLocalEchoMapper = FakeLocalEventMapper()
|
||||
private val fakeLocalEchoMapper = fake.FakeLocalEventMapper()
|
||||
private val mergeWithLocalEchosUseCase = MergeWithLocalEchosUseCaseImpl(fakeLocalEchoMapper.instance)
|
||||
|
||||
@Test
|
||||
|
@ -60,4 +57,4 @@ class MergeWithLocalEchosUseCaseTest {
|
|||
aTextMessage(aTextContent(body)),
|
||||
state,
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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.sync.MessageMeta
|
||||
import fixture.aLocalEcho
|
||||
import fixture.aTextMessage
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
|
@ -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 fixture.NotificationDiffFixtures.aNotificationDiff
|
||||
import fixture.aMatrixRoomMessageEvent
|
||||
import fixture.aMatrixRoomOverview
|
||||
import fixture.aRoomId
|
||||
import fixture.aRoomMessageEvent
|
||||
import fixture.aRoomOverview
|
||||
import fixture.anEventId
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
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 A_MESSAGE = aRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
|
||||
private val A_MESSAGE_2 = aRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
|
||||
private val A_ROOM_OVERVIEW = aRoomOverview(roomId = aRoomId("1"))
|
||||
private val A_ROOM_OVERVIEW_2 = aRoomOverview(roomId = aRoomId("2"))
|
||||
private val NO_UNREADS = emptyMap<MatrixRoomOverview, List<MatrixRoomEvent>>()
|
||||
private val A_MESSAGE = aMatrixRoomMessageEvent(eventId = anEventId("1"), content = "hello", utcTimestamp = 1000)
|
||||
private val A_MESSAGE_2 = aMatrixRoomMessageEvent(eventId = anEventId("2"), content = "world", utcTimestamp = 2000)
|
||||
private val A_ROOM_OVERVIEW = aMatrixRoomOverview(roomId = aRoomId("1"))
|
||||
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 {
|
||||
|
||||
|
@ -33,7 +36,7 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||
val result = useCase.invoke().toList()
|
||||
|
||||
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),
|
||||
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
||||
)
|
||||
|
@ -47,11 +50,11 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||
val result = useCase.invoke().toList()
|
||||
|
||||
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),
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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),
|
||||
newRooms = setOf(A_ROOM_OVERVIEW.roomId)
|
||||
),
|
||||
|
@ -110,8 +113,10 @@ class ObserveUnreadRenderNotificationsUseCaseTest {
|
|||
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 RoomOverview.toDiff(vararg events: RoomEvent) = mapOf(this.roomId to events.map { it.eventId })
|
||||
private fun Map<MatrixRoomOverview, List<MatrixRoomEvent>>.engine() = this
|
||||
.mapKeys { it.key.engine() }
|
||||
.mapValues { it.value.map { it.engine() } }
|
|
@ -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.RoomMember
|
||||
import app.dapk.st.matrix.common.UserId
|
||||
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.SyncService
|
||||
import fake.FakeSyncService
|
||||
import fixture.*
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
|
@ -16,12 +18,13 @@ import kotlinx.coroutines.flow.collect
|
|||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Test
|
||||
import test.FlowTestObserver
|
||||
import test.delegateReturn
|
||||
|
||||
private val A_ROOM_ID = aRoomId()
|
||||
private val AN_USER_ID = aUserId()
|
||||
private val A_ROOM_STATE = aRoomState()
|
||||
private val A_MERGED_ROOM_STATE = A_ROOM_STATE.copy(events = listOf(aRoomMessageEvent(content = "a merged event")))
|
||||
private val A_ROOM_STATE = aMatrixRoomState()
|
||||
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_ROOM_MEMBER = aRoomMember()
|
||||
|
||||
|
@ -47,7 +50,7 @@ class TimelineUseCaseTest {
|
|||
.test(this)
|
||||
.assertValues(
|
||||
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)
|
||||
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)
|
||||
.test(this)
|
||||
.assertValues(
|
||||
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)
|
||||
.assertValues(
|
||||
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() {
|
||||
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()
|
||||
}
|
||||
|
||||
fun aTypingSyncEvent(
|
||||
roomId: RoomId = aRoomId(),
|
||||
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)
|
|
@ -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()
|
||||
}
|
|
@ -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.message.MessageService
|
||||
import app.dapk.st.messenger.LocalEchoMapper
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
|
|
@ -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.mockk
|
||||
import test.delegateReturn
|
|
@ -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.messenger.MetaMapper
|
||||
import io.mockk.every
|
||||
import io.mockk.mockk
|
||||
import test.delegateReturn
|
|
@ -1,6 +1,6 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.notifications.ObserveInviteNotificationsUseCase
|
||||
import app.dapk.st.engine.ObserveInviteNotificationsUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import test.delegateEmit
|
|
@ -1,6 +1,6 @@
|
|||
package fake
|
||||
|
||||
import app.dapk.st.notifications.ObserveUnreadNotificationsUseCase
|
||||
import app.dapk.st.engine.ObserveUnreadNotificationsUseCase
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import test.delegateEmit
|
|
@ -12,7 +12,7 @@ private const val CIPHER_ALGORITHM = "AES/CTR/NoPadding"
|
|||
private const val SECRET_KEY_SPEC_ALGORITHM = "AES"
|
||||
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 {
|
||||
val key = base64.decode(k.replace('-', '+').replace('_', '/'))
|
|
@ -1,9 +1,11 @@
|
|||
package app.dapk.st.matrix.message
|
||||
|
||||
import app.dapk.st.matrix.common.JsonString
|
||||
|
||||
interface BackgroundScheduler {
|
||||
|
||||
fun schedule(key: String, task: Task)
|
||||
|
||||
data class Task(val type: String, val jsonPayload: String)
|
||||
data class Task(val type: String, val jsonPayload: JsonString)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
package app.dapk.st.matrix.message.internal
|
||||
|
||||
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.http.MatrixHttpClient
|
||||
import app.dapk.st.matrix.message.*
|
||||
|
@ -69,13 +70,13 @@ internal class DefaultMessageService(
|
|||
is MessageService.Message.TextMessage -> {
|
||||
BackgroundScheduler.Task(
|
||||
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(
|
||||
type = MATRIX_IMAGE_MESSAGE_TASK_TYPE,
|
||||
Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this)
|
||||
JsonString(Json.encodeToString(MessageService.Message.ImageMessage.serializer(), this))
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,10 +18,15 @@ interface SyncService : MatrixService {
|
|||
fun invites(): Flow<InviteState>
|
||||
fun overview(): Flow<OverviewState>
|
||||
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 events(roomId: RoomId? = null): Flow<List<SyncEvent>>
|
||||
suspend fun observeEvent(eventId: EventId): Flow<EventId>
|
||||
suspend fun forceManualRefresh(roomIds: List<RoomId>)
|
||||
suspend fun forceManualRefresh(roomIds: Set<RoomId>)
|
||||
|
||||
@JvmInline
|
||||
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
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun MatrixServiceInstaller.installSyncService(
|
||||
|
|
|
@ -24,7 +24,7 @@ private val syncSubscriptionCount = AtomicInteger()
|
|||
|
||||
internal class DefaultSyncService(
|
||||
httpClient: MatrixHttpClient,
|
||||
syncStore: SyncStore,
|
||||
private val syncStore: SyncStore,
|
||||
private val overviewStore: OverviewStore,
|
||||
private val roomStore: RoomStore,
|
||||
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 overview() = overviewStore.latest()
|
||||
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 suspend fun observeEvent(eventId: EventId) = roomStore.observeEvent(eventId)
|
||||
override suspend fun forceManualRefresh(roomIds: List<RoomId>) {
|
||||
override suspend fun forceManualRefresh(roomIds: Set<RoomId>) {
|
||||
coroutineDispatchers.withIoContext {
|
||||
roomIds.map {
|
||||
async {
|
||||
|
|
|
@ -15,7 +15,7 @@ import org.junit.Test
|
|||
|
||||
private const val A_DECRYPTED_MESSAGE_CONTENT = "decrypted - content"
|
||||
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(
|
||||
message = AN_ENCRYPTED_ROOM_MESSAGE,
|
||||
replyingTo = AN_ENCRYPTED_ROOM_MESSAGE.copy(eventId = anEventId("other-event"))
|
||||
|
@ -37,7 +37,7 @@ class RoomEventsDecrypterTest {
|
|||
|
||||
@Test
|
||||
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))
|
||||
|
||||
result shouldBeEqualTo listOf(aClearMessageEvent)
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
package app.dapk.st.matrix.sync.internal.sync
|
||||
|
||||
import fake.FakeRoomStore
|
||||
import fixture.aRoomMessageEvent
|
||||
import fixture.aMatrixRoomMessageEvent
|
||||
import fixture.anEventId
|
||||
import internalfixture.aTimelineTextEventContent
|
||||
import internalfixture.anApiTimelineTextEvent
|
||||
|
@ -11,8 +11,8 @@ import org.junit.Test
|
|||
|
||||
private val AN_EVENT_ID = anEventId()
|
||||
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_PERSISTED_EVENT = aRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
|
||||
private val A_ROOM_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "previous room event")
|
||||
private val A_PERSISTED_EVENT = aMatrixRoomMessageEvent(AN_EVENT_ID, content = "persisted event")
|
||||
|
||||
class EventLookupUseCaseTest {
|
||||
|
||||
|
|
|
@ -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.ApiTimelineEvent
|
||||
import fake.FakeErrorTracker
|
||||
import fake.FakeMatrixLogger
|
||||
import fake.FakeRoomMembersService
|
||||
import fixture.*
|
||||
import internalfixture.*
|
||||
|
@ -41,7 +40,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
val result = with(roomEventCreator) { megolmEvent.toRoomEvent(A_ROOM_ID) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = megolmEvent.eventId,
|
||||
utcTimestamp = megolmEvent.utcTimestamp,
|
||||
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) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = A_TEXT_EVENT.id,
|
||||
utcTimestamp = A_TEXT_EVENT.utcTimestamp,
|
||||
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) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = A_TEXT_EVENT_WITHOUT_CONTENT.id,
|
||||
utcTimestamp = A_TEXT_EVENT_WITHOUT_CONTENT.utcTimestamp,
|
||||
content = "redacted",
|
||||
|
@ -103,7 +102,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
val result = with(roomEventCreator) { editEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = editEvent.id,
|
||||
utcTimestamp = editEvent.utcTimestamp,
|
||||
content = editEvent.asTextContent().body!!,
|
||||
|
@ -121,7 +120,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = originalMessage.id,
|
||||
utcTimestamp = editedMessage.utcTimestamp,
|
||||
content = A_TEXT_EVENT_MESSAGE,
|
||||
|
@ -133,13 +132,13 @@ internal class RoomEventCreatorTest {
|
|||
@Test
|
||||
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)
|
||||
val originalMessage = aRoomMessageEvent()
|
||||
val originalMessage = aMatrixRoomMessageEvent()
|
||||
val editedMessage = originalMessage.toEditEvent(newTimestamp = 1000, messageContent = A_TEXT_EVENT_MESSAGE)
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
val result = with(roomEventCreator) { editedMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = originalMessage.eventId,
|
||||
utcTimestamp = editedMessage.utcTimestamp,
|
||||
content = A_TEXT_EVENT_MESSAGE,
|
||||
|
@ -151,7 +150,7 @@ internal class RoomEventCreatorTest {
|
|||
@Test
|
||||
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)
|
||||
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 lookup = givenLookup(originalMessage)
|
||||
|
||||
|
@ -159,7 +158,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||
replyingTo = originalMessage.replyingTo,
|
||||
message = aRoomMessageEvent(
|
||||
message = aMatrixRoomMessageEvent(
|
||||
eventId = originalMessage.eventId,
|
||||
utcTimestamp = editedMessage.utcTimestamp,
|
||||
content = A_TEXT_EVENT_MESSAGE,
|
||||
|
@ -182,7 +181,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
@Test
|
||||
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 lookup = givenLookup(originalMessage)
|
||||
|
||||
|
@ -199,7 +198,7 @@ internal class RoomEventCreatorTest {
|
|||
println(replyEvent.content)
|
||||
val result = with(roomEventCreator) { replyEvent.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, EMPTY_LOOKUP) }
|
||||
|
||||
result shouldBeEqualTo aRoomMessageEvent(
|
||||
result shouldBeEqualTo aMatrixRoomMessageEvent(
|
||||
eventId = replyEvent.id,
|
||||
utcTimestamp = replyEvent.utcTimestamp,
|
||||
content = replyEvent.asTextContent().body!!,
|
||||
|
@ -217,13 +216,13 @@ internal class RoomEventCreatorTest {
|
|||
val result = with(roomEventCreator) { replyMessage.toRoomEvent(A_USER_CREDENTIALS, A_ROOM_ID, lookup) }
|
||||
|
||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||
replyingTo = aRoomMessageEvent(
|
||||
replyingTo = aMatrixRoomMessageEvent(
|
||||
eventId = originalMessage.id,
|
||||
utcTimestamp = originalMessage.utcTimestamp,
|
||||
content = originalMessage.asTextContent().body!!,
|
||||
author = A_SENDER,
|
||||
),
|
||||
message = aRoomMessageEvent(
|
||||
message = aMatrixRoomMessageEvent(
|
||||
eventId = replyMessage.id,
|
||||
utcTimestamp = replyMessage.utcTimestamp,
|
||||
content = A_REPLY_EVENT_MESSAGE,
|
||||
|
@ -235,7 +234,7 @@ internal class RoomEventCreatorTest {
|
|||
@Test
|
||||
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)
|
||||
val originalMessage = aRoomMessageEvent()
|
||||
val originalMessage = aMatrixRoomMessageEvent()
|
||||
val replyMessage = originalMessage.toReplyEvent(messageContent = A_REPLY_EVENT_MESSAGE)
|
||||
val lookup = givenLookup(originalMessage)
|
||||
|
||||
|
@ -243,7 +242,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||
replyingTo = originalMessage,
|
||||
message = aRoomMessageEvent(
|
||||
message = aMatrixRoomMessageEvent(
|
||||
eventId = replyMessage.id,
|
||||
utcTimestamp = replyMessage.utcTimestamp,
|
||||
content = A_REPLY_EVENT_MESSAGE,
|
||||
|
@ -263,7 +262,7 @@ internal class RoomEventCreatorTest {
|
|||
|
||||
result shouldBeEqualTo aRoomReplyMessageEvent(
|
||||
replyingTo = originalMessage.message,
|
||||
message = aRoomMessageEvent(
|
||||
message = aMatrixRoomMessageEvent(
|
||||
eventId = replyMessage.id,
|
||||
utcTimestamp = replyMessage.utcTimestamp,
|
||||
content = A_REPLY_EVENT_MESSAGE,
|
||||
|
|
|
@ -5,8 +5,8 @@ import app.dapk.st.matrix.sync.RoomEvent
|
|||
import app.dapk.st.matrix.sync.RoomState
|
||||
import fake.FakeMatrixLogger
|
||||
import fake.FakeRoomDataSource
|
||||
import internalfake.FakeRoomEventsDecrypter
|
||||
import fixture.*
|
||||
import internalfake.FakeRoomEventsDecrypter
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.amshove.kluent.shouldBeEqualTo
|
||||
import org.junit.Test
|
||||
|
@ -15,13 +15,14 @@ import test.expect
|
|||
private val A_ROOM_ID = aRoomId()
|
||||
|
||||
private object ARoom {
|
||||
val MESSAGE_EVENT = aRoomMessageEvent(utcTimestamp = 0)
|
||||
val MESSAGE_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 0)
|
||||
val ENCRYPTED_EVENT = anEncryptedRoomMessageEvent(utcTimestamp = 1)
|
||||
val DECRYPTED_EVENT = aRoomMessageEvent(utcTimestamp = 2)
|
||||
val PREVIOUS_STATE = RoomState(aRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_EVENT))
|
||||
val DECRYPTED_EVENT = aMatrixRoomMessageEvent(utcTimestamp = 2)
|
||||
val PREVIOUS_STATE = RoomState(aMatrixRoomOverview(), listOf(MESSAGE_EVENT, ENCRYPTED_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()
|
||||
|
||||
internal class RoomRefresherTest {
|
||||
|
|
|
@ -19,7 +19,7 @@ private val A_ROOM_ID = aRoomId()
|
|||
private val ANY_LOOKUP_RESULT = LookupResult(anApiTimelineTextEvent(), roomEvent = null)
|
||||
private val AN_ENCRYPTED_TIMELINE_EVENT = anEncryptedApiTimelineEvent()
|
||||
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 A_LOOKUP_EVENT_ID = anEventId("lookup-id")
|
||||
private val A_USER_CREDENTIALS = aUserCredentials()
|
||||
|
@ -52,7 +52,7 @@ class TimelineEventsProcessorTest {
|
|||
|
||||
@Test
|
||||
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 roomToProcess = aRoomToProcess(apiSyncRoom = anApiSyncRoom(anApiSyncRoomTimeline(newTimelineEvents)))
|
||||
fakeRoomEventsDecrypter.givenDecrypts(A_USER_CREDENTIALS, previousEvents)
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue